1 /* 2 * Copyright (C) 2024 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 package com.android.ravenwoodtest.runnercallbacktests; 17 18 import static com.google.common.truth.Truth.assertThat; 19 import static com.google.common.truth.Truth.assertWithMessage; 20 21 import android.platform.test.annotations.NoRavenizer; 22 import android.platform.test.ravenwood.RavenwoodAwareTestRunner; 23 import android.platform.test.ravenwood.RavenwoodConfigPrivate; 24 import android.util.Log; 25 26 import junitparams.JUnitParamsRunner; 27 import junitparams.Parameters; 28 29 import org.junit.Ignore; 30 import org.junit.Test; 31 import org.junit.runner.Description; 32 import org.junit.runner.JUnitCore; 33 import org.junit.runner.Result; 34 import org.junit.runner.RunWith; 35 import org.junit.runner.notification.Failure; 36 import org.junit.runner.notification.RunListener; 37 38 import java.lang.annotation.ElementType; 39 import java.lang.annotation.Retention; 40 import java.lang.annotation.RetentionPolicy; 41 import java.lang.annotation.Target; 42 import java.util.ArrayList; 43 import java.util.Arrays; 44 import java.util.function.BiConsumer; 45 46 47 /** 48 * Base class for tests to make sure {@link RavenwoodAwareTestRunner} produces expected callbacks 49 * in various situations. (most of them are error situations.) 50 * 51 * Subclasses must contain test classes as static inner classes with an {@link Expected} annotation. 52 * This class finds them using reflections and run them one by one directly using {@link JUnitCore}, 53 * and check the callbacks. 54 * 55 * Subclasses do no need to have any test methods. 56 * 57 * The {@link Expected} annotation must contain the expected result as a string. 58 * 59 * This test abuses the fact that atest + tradefed + junit won't run nested classes automatically. 60 * (So atest won't show any results directly from the nested classes.) 61 * 62 * The actual test method is {@link #doTest}, which is executed for each target test class, using 63 * junit-params. 64 */ 65 @RunWith(JUnitParamsRunner.class) 66 @NoRavenizer // This class shouldn't be executed with RavenwoodAwareTestRunner. 67 public abstract class RavenwoodRunnerTestBase { 68 private static final String TAG = "RavenwoodRunnerTestBase"; 69 70 /** 71 * Annotation to specify the expected result for a class. 72 */ 73 @Target({ElementType.TYPE}) 74 @Retention(RetentionPolicy.RUNTIME) 75 public @interface Expected { value()76 String value(); 77 } 78 79 /** 80 * Take a multiline string, strip all of them, remove empty lines, and return it. 81 */ stripMultiLines(String resultString)82 private static String stripMultiLines(String resultString) { 83 var list = new ArrayList<String>(); 84 for (var line : resultString.split("\n")) { 85 var s = line.strip(); 86 if (s.length() > 0) { 87 list.add(s); 88 } 89 } 90 return String.join("\n", list); 91 } 92 93 /** 94 * Extract the expected result from @Expected. 95 */ getExpectedResult(Class<?> testClazz)96 private String getExpectedResult(Class<?> testClazz) { 97 var expect = testClazz.getAnnotation(Expected.class); 98 return stripMultiLines(expect.value()); 99 } 100 101 /** 102 * List all the nested classrs with an {@link Expected} annotation in a given class. 103 */ getTestClasses()104 public Class<?>[] getTestClasses() { 105 var thisClass = this.getClass(); 106 var ret = Arrays.stream(thisClass.getNestMembers()) 107 .filter((c) -> c.getAnnotation(Expected.class) != null) 108 .filter((c) -> c.getAnnotation(Ignore.class) == null) 109 .toArray(Class[]::new); 110 111 assertThat(ret.length).isGreaterThan(0); 112 113 return ret; 114 } 115 116 /** 117 * This is the actual test method. We use junit-params to run this method for each target 118 * test class, which are returned by {@link #getTestClasses}. 119 * 120 * It runs each test class, and compare the result collected with 121 * {@link ResultCollectingListener} to expected results (as strings). 122 */ 123 @Test 124 @Parameters(method = "getTestClasses") doTest(Class<?> testClazz)125 public void doTest(Class<?> testClazz) { 126 doTest(testClazz, getExpectedResult(testClazz)); 127 } 128 129 /** 130 * Run a given test class, and compare the result collected with 131 * {@link ResultCollectingListener} to expected results (as a string). 132 */ doTest(Class<?> testClazz, String expectedResult)133 private void doTest(Class<?> testClazz, String expectedResult) { 134 Log.i(TAG, "Running test for " + testClazz); 135 var junitCore = new JUnitCore(); 136 137 // Create a listener. 138 var listener = new ResultCollectingListener(); 139 junitCore.addListener(listener); 140 141 // Set a listener to critical errors. This will also prevent 142 // {@link RavenwoodAwareTestRunner} from calling System.exit() when there's 143 // a critical error. 144 RavenwoodConfigPrivate.setCriticalErrorHandler(listener.sCriticalErrorListener); 145 146 try { 147 // Run the test class. 148 junitCore.run(testClazz); 149 } finally { 150 // Clear the critical error listener. 151 RavenwoodConfigPrivate.setCriticalErrorHandler(null); 152 } 153 154 // Check the result. 155 assertWithMessage("Failure in test class: " + testClazz.getCanonicalName() + "]") 156 .that(listener.getResult()) 157 .isEqualTo(expectedResult); 158 } 159 160 /** 161 * A JUnit RunListener that collects all the callbacks as a single string. 162 */ 163 private static class ResultCollectingListener extends RunListener { 164 private final ArrayList<String> mResult = new ArrayList<>(); 165 166 public final BiConsumer<String, Throwable> sCriticalErrorListener = (message, th) -> { 167 mResult.add("criticalError: " + message + ": " + th.getMessage()); 168 }; 169 170 @Override testRunStarted(Description description)171 public void testRunStarted(Description description) throws Exception { 172 mResult.add("testRunStarted: " + description); 173 } 174 175 @Override testRunFinished(Result result)176 public void testRunFinished(Result result) throws Exception { 177 mResult.add("testRunFinished: " 178 + result.getRunCount() + "," 179 + result.getFailureCount() + "," 180 + result.getAssumptionFailureCount() + "," 181 + result.getIgnoreCount()); 182 } 183 184 @Override testSuiteStarted(Description description)185 public void testSuiteStarted(Description description) throws Exception { 186 mResult.add("testSuiteStarted: " + description); 187 } 188 189 @Override testSuiteFinished(Description description)190 public void testSuiteFinished(Description description) throws Exception { 191 mResult.add("testSuiteFinished: " + description); 192 } 193 194 @Override testStarted(Description description)195 public void testStarted(Description description) throws Exception { 196 mResult.add("testStarted: " + description); 197 } 198 199 @Override testFinished(Description description)200 public void testFinished(Description description) throws Exception { 201 mResult.add("testFinished: " + description); 202 } 203 204 @Override testFailure(Failure failure)205 public void testFailure(Failure failure) throws Exception { 206 mResult.add("testFailure: " + failure.getException().getMessage()); 207 } 208 209 @Override testAssumptionFailure(Failure failure)210 public void testAssumptionFailure(Failure failure) { 211 mResult.add("testAssumptionFailure: " + failure.getException().getMessage()); 212 } 213 214 @Override testIgnored(Description description)215 public void testIgnored(Description description) throws Exception { 216 mResult.add("testIgnored: " + description); 217 } 218 getResult()219 public String getResult() { 220 return String.join("\n", mResult); 221 } 222 } 223 } 224