1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.testing; 18 19 import android.util.Log; 20 21 import com.android.internal.annotations.VisibleForTesting; 22 23 import org.junit.rules.TestRule; 24 import org.junit.runner.Description; 25 import org.junit.runners.model.Statement; 26 27 import java.util.ConcurrentModificationException; 28 29 30 /** 31 * Runs the test such that mocks created in it don't use a dedicated classloader. 32 * 33 * This allows mocking package-private methods. 34 * 35 * WARNING: This is absolutely incompatible with running tests in parallel! 36 */ 37 public class DexmakerShareClassLoaderRule implements TestRule { 38 39 private static final String TAG = "ShareClassloaderRule"; 40 @VisibleForTesting 41 static final String DEXMAKER_SHARE_CLASSLOADER_PROPERTY = "dexmaker.share_classloader"; 42 43 private static Thread sOwningThread = null; 44 45 @Override apply(Statement base, Description description)46 public Statement apply(Statement base, Description description) { 47 return apply(base::evaluate).toStatement(); 48 } 49 50 /** 51 * Runs the runnable such that mocks created in it don't use a dedicated classloader. 52 * 53 * This allows mocking package-private methods. 54 * 55 * WARNING: This is absolutely incompatible with running tests in parallel! 56 */ runWithDexmakerShareClassLoader(Runnable r)57 public static void runWithDexmakerShareClassLoader(Runnable r) { 58 apply(r::run).run(); 59 } 60 61 /** 62 * Returns a statement that first makes sure that only one thread at the time is modifying 63 * the property. Then actually sets the property, and runs the statement. 64 */ apply(ThrowingRunnable<T> r)65 private static <T extends Throwable> ThrowingRunnable<T> apply(ThrowingRunnable<T> r) { 66 return wrapInMutex(wrapInSetAndClearProperty(r)); 67 } 68 wrapInSetAndClearProperty( ThrowingRunnable<T> r)69 private static <T extends Throwable> ThrowingRunnable<T> wrapInSetAndClearProperty( 70 ThrowingRunnable<T> r) { 71 return () -> { 72 final String previousValue = System.getProperty(DEXMAKER_SHARE_CLASSLOADER_PROPERTY); 73 try { 74 System.setProperty(DEXMAKER_SHARE_CLASSLOADER_PROPERTY, "true"); 75 r.run(); 76 } finally { 77 if (previousValue != null) { 78 System.setProperty(DEXMAKER_SHARE_CLASSLOADER_PROPERTY, previousValue); 79 } else { 80 System.clearProperty(DEXMAKER_SHARE_CLASSLOADER_PROPERTY); 81 } 82 } 83 }; 84 } 85 86 /** 87 * Runs the given statement, and while doing so prevents other threads from running statements. 88 */ wrapInMutex(ThrowingRunnable<T> r)89 private static <T extends Throwable> ThrowingRunnable<T> wrapInMutex(ThrowingRunnable<T> r) { 90 return () -> { 91 final boolean isOwner; 92 synchronized (DexmakerShareClassLoaderRule.class) { 93 isOwner = (sOwningThread == null); 94 if (isOwner) { 95 sOwningThread = Thread.currentThread(); 96 } else if (sOwningThread != Thread.currentThread()) { 97 final RuntimeException e = new ConcurrentModificationException( 98 "Tried to set dexmaker.share_classloader from " + Thread.currentThread() 99 + ", but was already set from " + sOwningThread); 100 // Also log in case exception gets swallowed. 101 Log.e(TAG, e.getMessage(), e); 102 throw e; 103 } 104 } 105 try { 106 r.run(); 107 } finally { 108 synchronized (DexmakerShareClassLoaderRule.class) { 109 if (isOwner) { 110 sOwningThread = null; 111 } 112 } 113 } 114 }; 115 } 116 117 private interface ThrowingRunnable<T extends Throwable> { 118 void run() throws T; 119 120 default Statement toStatement() { 121 return new Statement() { 122 @Override 123 public void evaluate() throws Throwable { 124 ThrowingRunnable.this.run(); 125 } 126 }; 127 } 128 } 129 } 130