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.adservices.shared.testing; 17 18 import com.android.adservices.shared.testing.Logger.RealLogger; 19 20 import com.google.common.truth.Expect; 21 22 import org.junit.Rule; 23 24 import java.lang.reflect.Field; 25 import java.util.Arrays; 26 import java.util.Objects; 27 import java.util.concurrent.atomic.AtomicInteger; 28 29 /** 30 * "Uber" superclass for all tests. 31 * 32 * <p>It provide the bare minimum features that will be used by all sort of tests (unit / CTS, 33 * device/host side, project-specific). 34 */ 35 public abstract class SidelessTestCase implements TestNamer { 36 37 private static final boolean FAIL_ON_PROHIBITED_FIELDS = true; 38 39 private static final AtomicInteger sNextInvocationId = new AtomicInteger(); 40 41 // TODO(b/342639109): set order 42 @Rule public final Expect expect = Expect.create(); 43 44 private final int mInvocationId = sNextInvocationId.incrementAndGet(); 45 46 // TODO(b/285014040): log test number / to String on constructor (will require logV()). 47 // Something like (which used to be on AdServicesTestCase): 48 // Log.d(TAG, "setTestNumber(): " + getTestName() + " is test #" + mTestNumber); 49 50 protected final Logger mLog; 51 protected final RealLogger mRealLogger; 52 SidelessTestCase()53 public SidelessTestCase() { 54 this(DynamicLogger.getInstance()); 55 } 56 SidelessTestCase(RealLogger realLogger)57 public SidelessTestCase(RealLogger realLogger) { 58 mRealLogger = realLogger; 59 mLog = new Logger(realLogger, getClass()); 60 } 61 62 @Override getTestName()63 public String getTestName() { 64 return DEFAULT_TEST_NAME; 65 } 66 67 /** Gets a unique id for the test invocation. */ getTestInvocationId()68 public final int getTestInvocationId() { 69 return mInvocationId; 70 } 71 72 // TODO(b/361555631): merge both methods below into a final 73 // testMeasurementJobServiceTestCaseFixtures() and annotate 74 // it with @MetaTest once we provide some infra to skip @Before / @After on them. 75 /** 76 * Test used to make sure subclasses don't define prohibited fields (as defined by {@link 77 * #assertValidTestCaseFixtures()}). 78 * 79 * <p>This method by default is not annotated with {@code Test}, so it must be overridden by 80 * test superclasses that wants to enforce such validation (ideally all of them should, but 81 * there are tests - particularly host-side ones - that have expensive <code>@Before</code> / 82 * <code>@Setup</code> methods which could cause problem when running those (for example, the 83 * whole test class might timeout). 84 * 85 * <p>Typically, the overridden method should simply call {@code assertValidTestCaseFixtures()} 86 * and be {@code final}. 87 */ 88 @SuppressWarnings("JUnit4TestNotRun") testValidTestCaseFixtures()89 public void testValidTestCaseFixtures() throws Exception { 90 mLog.i("testValidTestCaseFixtures(): ignored on %s", getTestName()); 91 } 92 93 /** 94 * Verifies this test class don't define prohibited fields, like fields that are already defined 95 * by a subclass or use names that could cause confusion). 96 * 97 * <p>Most test classes shouldn't care about this method, but it should be overridden by test 98 * superclasses that define their own fields (like {@code mMockFlags}. 99 * 100 * <p><b>Note: </b>when overriding it, make sure to call {@code 101 * super.assertValidTestCaseFixtures()} as the first statement. 102 */ 103 @CallSuper assertValidTestCaseFixtures()104 protected void assertValidTestCaseFixtures() throws Exception { 105 expect.withMessage("getTestName()").that(getTestName()).isNotNull(); 106 107 assertTestClassHasNoFieldsFromSuperclass( 108 SidelessTestCase.class, "mLog", "mRealLogger", "expect"); 109 } 110 111 /** 112 * Asserts that the test class doesn't declare any field with the given names. 113 * 114 * <p>"Base" superclasses should use this method to passing all protected and public fields they 115 * define. 116 */ assertTestClassHasNoSuchField(String name, String reason)117 public final void assertTestClassHasNoSuchField(String name, String reason) throws Exception { 118 Objects.requireNonNull(name, "name cannot be nul"); 119 Objects.requireNonNull(reason, "reason cannot be nul"); 120 121 if (!iHaveThisField(name)) { 122 return; 123 } 124 if (!FAIL_ON_PROHIBITED_FIELDS) { 125 mLog.e( 126 "Class %s should not define field %s (reason: %s) but test is not failing" 127 + " because FAIL_ON_PROHIBITED_FIELDS is false", 128 getClass().getSimpleName(), name, reason); 129 return; 130 } 131 expect.withMessage( 132 "Class %s should not define field %s. Reason: %s", 133 getClass().getSimpleName(), name, reason) 134 .fail(); 135 } 136 137 /** 138 * Asserts that the test class doesn't declare any field with the given names. 139 * 140 * <p>"Base" superclasses should use this method to passing all protected and public fields they 141 * define. 142 */ assertTestClassHasNoFieldsFromSuperclass(Class<?> superclass, String... names)143 public final void assertTestClassHasNoFieldsFromSuperclass(Class<?> superclass, String... names) 144 throws Exception { 145 Objects.requireNonNull(superclass, "superclass cannot be null"); 146 if (names == null || names.length == 0) { 147 throw new IllegalArgumentException("names cannot be empty or null"); 148 } 149 Class<?> myClass = getClass(); 150 String myClassName = myClass.getSimpleName(); 151 if (myClass.equals(superclass)) { 152 mLog.v( 153 "assertTestClassHasNoFieldsFromSuperclass(%s, %s): skipping on self", 154 myClassName, Arrays.toString(names)); 155 return; 156 } 157 158 StringBuilder violationsBuilder = new StringBuilder(); 159 for (String name : names) { 160 if (iHaveThisField(name)) { 161 violationsBuilder.append(' ').append(name); 162 } 163 } 164 String violations = violationsBuilder.toString(); 165 if (violations.isEmpty()) { 166 return; 167 } 168 if (!FAIL_ON_PROHIBITED_FIELDS) { 169 mLog.e( 170 "Class %s should not define the following fields (as they're defined by %s)," 171 + " but test is not failing because FAIL_ON_PROHIBITED_FIELDS is false:%s", 172 getClass().getSimpleName(), superclass.getSimpleName(), violations); 173 return; 174 } 175 expect.withMessage( 176 "%s should not define the following fields, as they're defined by %s:%s", 177 myClassName, superclass.getSimpleName(), violations) 178 .fail(); 179 } 180 iHaveThisField(String name)181 private boolean iHaveThisField(String name) { 182 try { 183 Field field = getClass().getDeclaredField(name); 184 // Logging as error as class is not expected to have it 185 mLog.e( 186 "Found field with name (%s) that shouldn't exist on class %s: %s", 187 name, getClass().getSimpleName(), field); 188 return true; 189 } catch (NoSuchFieldException e) { 190 return false; 191 } 192 } 193 194 // TODO(b/285014040): add more features like: 195 // - sleep() 196 // - logV() 197 // - toString() 198 } 199