1 package org.robolectric.interceptors; 2 3 import static com.google.common.truth.Truth.assertThat; 4 5 import android.os.SystemClock; 6 import androidx.test.ext.junit.runners.AndroidJUnit4; 7 import java.io.ByteArrayOutputStream; 8 import java.io.FileDescriptor; 9 import java.io.PrintStream; 10 import java.lang.invoke.CallSite; 11 import java.lang.invoke.MethodHandles; 12 import java.lang.invoke.MethodType; 13 import java.lang.reflect.Field; 14 import java.net.Socket; 15 import java.time.Duration; 16 import java.util.Arrays; 17 import java.util.regex.Pattern; 18 import java.util.stream.Collectors; 19 import org.junit.Test; 20 import org.junit.runner.RunWith; 21 import org.robolectric.annotation.LooperMode; 22 import org.robolectric.internal.bytecode.InvokeDynamicSupport; 23 import org.robolectric.shadows.ShadowLooper; 24 import org.robolectric.shadows.ShadowSystemClock; 25 import org.robolectric.util.ReflectionHelpers.ClassParameter; 26 27 /** Integration tests for Android interceptors. */ 28 @RunWith(AndroidJUnit4.class) 29 public class AndroidInterceptorsIntegrationTest { 30 31 @Test systemLog_shouldWriteToStderr()32 public void systemLog_shouldWriteToStderr() throws Throwable { 33 PrintStream stderr = System.err; 34 ByteArrayOutputStream stream = new ByteArrayOutputStream(); 35 PrintStream printStream = new PrintStream(stream); 36 System.setErr(printStream); 37 try { 38 for (String methodName : new String[] {"logE", "logW"}) { 39 stream.reset(); 40 // One parameter: [message] 41 invokeDynamic( 42 System.class, methodName, void.class, ClassParameter.from(String.class, "hello")); 43 String expected1 = String.format("System\\.%s: hello", methodName); 44 // Two parameters: [message, throwable] 45 // We verify that the stack trace is dumped by looking for the name of this method. 46 invokeDynamic( 47 System.class, 48 methodName, 49 void.class, 50 ClassParameter.from(String.class, "world"), 51 ClassParameter.from(Throwable.class, new Throwable("message"))); 52 String expected2 = 53 String.format( 54 "System.%s: world.*java\\.lang\\.Throwable: message.*" 55 + "at .*AndroidInterceptorsIntegrationTest\\.systemLog_shouldWriteToStderr", 56 methodName); 57 // Due to the possibility of running tests in Parallel, assertions checking stderr contents 58 // should not assert equality. 59 assertThat(stream.toString()) 60 .matches(Pattern.compile(".*" + expected1 + ".*" + expected2 + ".*", Pattern.DOTALL)); 61 } 62 } finally { 63 System.setErr(stderr); 64 } 65 } 66 67 @Test systemNanoTime_shouldReturnShadowClockTime()68 public void systemNanoTime_shouldReturnShadowClockTime() throws Throwable { 69 if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) { 70 ShadowSystemClock.setNanoTime(Duration.ofMillis(200).toNanos()); 71 } else { 72 SystemClock.setCurrentTimeMillis(200); 73 } 74 75 long nanoTime = invokeDynamic(System.class, "nanoTime", long.class); 76 assertThat(nanoTime).isEqualTo(Duration.ofMillis(200).toNanos()); 77 } 78 79 @Test systemCurrentTimeMillis_shouldReturnShadowClockTime()80 public void systemCurrentTimeMillis_shouldReturnShadowClockTime() throws Throwable { 81 if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) { 82 ShadowSystemClock.setNanoTime(Duration.ofMillis(200).toNanos()); 83 } else { 84 SystemClock.setCurrentTimeMillis(200); 85 } 86 87 long currentTimeMillis = invokeDynamic(System.class, "currentTimeMillis", long.class); 88 assertThat(currentTimeMillis).isEqualTo(200); 89 } 90 91 /* Creates a FileDescriptor-object using reflection. Note that the "fd"-field is present for 92 * both the Windows and Unix implementations of FileDescriptor in OpenJDK. 93 */ createFd(int fd)94 private static FileDescriptor createFd(int fd) throws Throwable { 95 FileDescriptor ret = new FileDescriptor(); 96 Field accessor = FileDescriptor.class.getDeclaredField("fd"); 97 accessor.setAccessible(true); 98 accessor.set(ret, fd); 99 return ret; 100 } 101 102 @Test fileDescriptorRelease_isValid_correctResultsAfterRelease()103 public void fileDescriptorRelease_isValid_correctResultsAfterRelease() throws Throwable { 104 FileDescriptor original = createFd(42); 105 assertThat(original.valid()).isTrue(); 106 FileDescriptor copy = 107 invokeDynamic( 108 FileDescriptor.class, 109 "release$", 110 FileDescriptor.class, 111 ClassParameter.from(FileDescriptor.class, original)); 112 assertThat(copy.valid()).isTrue(); 113 assertThat(original.valid()).isFalse(); 114 } 115 116 @Test fileDescriptorRelease_allowsReleaseOnInvalidFd()117 public void fileDescriptorRelease_allowsReleaseOnInvalidFd() throws Throwable { 118 FileDescriptor original = new FileDescriptor(); 119 assertThat(original.valid()).isFalse(); 120 FileDescriptor copy = 121 invokeDynamic( 122 FileDescriptor.class, 123 "release$", 124 FileDescriptor.class, 125 ClassParameter.from(FileDescriptor.class, original)); 126 assertThat(copy.valid()).isFalse(); 127 assertThat(original.valid()).isFalse(); 128 } 129 130 @Test fileDescriptorRelease_doubleReleaseReturnsInvalidFd()131 public void fileDescriptorRelease_doubleReleaseReturnsInvalidFd() throws Throwable { 132 FileDescriptor original = createFd(42); 133 assertThat(original.valid()).isTrue(); 134 FileDescriptor copy = 135 invokeDynamic( 136 FileDescriptor.class, 137 "release$", 138 FileDescriptor.class, 139 ClassParameter.from(FileDescriptor.class, original)); 140 FileDescriptor copy2 = 141 invokeDynamic( 142 FileDescriptor.class, 143 "release$", 144 FileDescriptor.class, 145 ClassParameter.from(FileDescriptor.class, original)); 146 assertThat(copy.valid()).isTrue(); 147 assertThat(copy2.valid()).isFalse(); 148 assertThat(original.valid()).isFalse(); 149 } 150 151 @Test fileDescriptorRelease_releaseFdCorrect()152 public void fileDescriptorRelease_releaseFdCorrect() throws Throwable { 153 FileDescriptor original = createFd(42); 154 FileDescriptor copy = 155 invokeDynamic( 156 FileDescriptor.class, 157 "release$", 158 FileDescriptor.class, 159 ClassParameter.from(FileDescriptor.class, original)); 160 Field accessor = FileDescriptor.class.getDeclaredField("fd"); 161 accessor.setAccessible(true); 162 assertThat(accessor.get(copy)).isEqualTo(42); 163 } 164 165 @Test socketFileDescriptor_returnsNullFileDescriptor()166 public void socketFileDescriptor_returnsNullFileDescriptor() throws Throwable { 167 FileDescriptor fd = 168 invokeDynamic( 169 Socket.class, 170 "getFileDescriptor$", 171 void.class, 172 ClassParameter.from(Socket.class, new Socket())); 173 assertThat(fd).isNull(); 174 } 175 176 @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) invokeDynamic( Class<?> cls, String methodName, Class<?> returnType, ClassParameter<?>... params)177 private static <T> T invokeDynamic( 178 Class<?> cls, String methodName, Class<?> returnType, ClassParameter<?>... params) 179 throws Throwable { 180 MethodType methodType = 181 MethodType.methodType( 182 returnType, Arrays.stream(params).map(param -> param.clazz).toArray(Class[]::new)); 183 CallSite callsite = 184 InvokeDynamicSupport.bootstrapIntrinsic( 185 MethodHandles.lookup(), methodName, methodType, cls.getName()); 186 return (T) 187 callsite 188 .dynamicInvoker() 189 .invokeWithArguments( 190 Arrays.stream(params).map(param -> param.value).collect(Collectors.toList())); 191 } 192 } 193