blob: 1b8e58c3050d3c7c80731f2f50f30782670c313f [file] [log] [blame]
Adrian Roos3150dbf2018-03-28 18:06:52 +02001/*
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
17package android.testing;
18
19import android.util.Log;
20
21import com.android.internal.annotations.VisibleForTesting;
22
23import org.junit.rules.TestRule;
24import org.junit.runner.Description;
25import org.junit.runners.model.Statement;
26
27import 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 */
37public 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
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 */
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 */
65 private static <T extends Throwable> ThrowingRunnable<T> apply(ThrowingRunnable<T> r) {
66 return wrapInMutex(wrapInSetAndClearProperty(r));
67 }
68
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 */
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}