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