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 libcore.util.SneakyThrow;
24 
25 import org.junit.rules.TestRule;
26 import org.junit.runner.Description;
27 import org.junit.runners.model.Statement;
28 
29 import java.util.ConcurrentModificationException;
30 
31 
32 /**
33  * Runs the test such that mocks created in it don't use a dedicated classloader.
34  *
35  * This allows mocking package-private methods.
36  *
37  * WARNING: This is absolutely incompatible with running tests in parallel!
38  */
39 public class DexmakerShareClassLoaderRule implements TestRule {
40 
41     private static final String TAG = "ShareClassloaderRule";
42     @VisibleForTesting
43     static final String DEXMAKER_SHARE_CLASSLOADER_PROPERTY = "dexmaker.share_classloader";
44 
45     private static Thread sOwningThread = null;
46 
47     @Override
apply(Statement base, Description description)48     public Statement apply(Statement base, Description description) {
49         return apply(base::evaluate).toStatement();
50     }
51 
52     /**
53      * Runs the runnable such that mocks created in it don't use a dedicated classloader.
54      *
55      * This allows mocking package-private methods.
56      *
57      * WARNING: This is absolutely incompatible with running tests in parallel!
58      */
runWithDexmakerShareClassLoader(Runnable r)59     public static void runWithDexmakerShareClassLoader(Runnable r) {
60         try {
61             apply(r::run).run();
62         } catch (Throwable t) {
63             SneakyThrow.sneakyThrow(t);
64         }
65     }
66 
67     /**
68      * Returns a statement that first makes sure that only one thread at the time is modifying
69      * the property. Then actually sets the property, and runs the statement.
70      */
apply(ThrowingRunnable<T> r)71     private static <T extends Throwable> ThrowingRunnable<T> apply(ThrowingRunnable<T> r) {
72         return wrapInMutex(wrapInSetAndClearProperty(r));
73     }
74 
wrapInSetAndClearProperty( ThrowingRunnable<T> r)75     private static <T extends Throwable> ThrowingRunnable<T> wrapInSetAndClearProperty(
76             ThrowingRunnable<T> r) {
77         return () -> {
78             final String previousValue = System.getProperty(DEXMAKER_SHARE_CLASSLOADER_PROPERTY);
79             try {
80                 System.setProperty(DEXMAKER_SHARE_CLASSLOADER_PROPERTY, "true");
81                 r.run();
82             } finally {
83                 if (previousValue != null) {
84                     System.setProperty(DEXMAKER_SHARE_CLASSLOADER_PROPERTY, previousValue);
85                 } else {
86                     System.clearProperty(DEXMAKER_SHARE_CLASSLOADER_PROPERTY);
87                 }
88             }
89         };
90     }
91 
92     /**
93      * Runs the given statement, and while doing so prevents other threads from running statements.
94      */
wrapInMutex(ThrowingRunnable<T> r)95     private static <T extends Throwable> ThrowingRunnable<T> wrapInMutex(ThrowingRunnable<T> r) {
96         return () -> {
97             final boolean isOwner;
98             synchronized (DexmakerShareClassLoaderRule.class) {
99                 isOwner = (sOwningThread == null);
100                 if (isOwner) {
101                     sOwningThread = Thread.currentThread();
102                 } else if (sOwningThread != Thread.currentThread()) {
103                     final RuntimeException e = new ConcurrentModificationException(
104                             "Tried to set dexmaker.share_classloader from " + Thread.currentThread()
105                                     + ", but was already set from " + sOwningThread);
106                     // Also log in case exception gets swallowed.
107                     Log.e(TAG, e.getMessage(), e);
108                     throw e;
109                 }
110             }
111             try {
112                 r.run();
113             } finally {
114                 synchronized (DexmakerShareClassLoaderRule.class) {
115                     if (isOwner) {
116                         sOwningThread = null;
117                     }
118                 }
119             }
120         };
121     }
122 
123     private interface ThrowingRunnable<T extends Throwable> {
124         void run() throws T;
125 
126         default Statement toStatement() {
127             return new Statement() {
128                 @Override
129                 public void evaluate() throws Throwable {
130                     ThrowingRunnable.this.run();
131                 }
132             };
133         }
134     }
135 }
136