1 /* 2 * Copyright (C) 2023 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.common; 17 18 import com.android.adservices.common.annotations.RequiresAndroidServiceAvailable; 19 import com.android.adservices.shared.testing.AndroidDevicePropertiesHelper; 20 import com.android.adservices.shared.testing.DeviceConditionsViolatedException; 21 import com.android.adservices.shared.testing.Logger; 22 import com.android.adservices.shared.testing.Logger.RealLogger; 23 import com.android.adservices.shared.testing.Nullable; 24 import com.android.adservices.shared.testing.ScreenSize; 25 import com.android.adservices.shared.testing.annotations.RequiresGoDevice; 26 import com.android.adservices.shared.testing.annotations.RequiresLowRamDevice; 27 import com.android.adservices.shared.testing.annotations.RequiresScreenSizeDevice; 28 29 import com.google.common.annotations.VisibleForTesting; 30 31 import org.junit.AssumptionViolatedException; 32 import org.junit.rules.TestRule; 33 import org.junit.runner.Description; 34 import org.junit.runners.model.Statement; 35 36 import java.lang.annotation.Annotation; 37 import java.util.ArrayList; 38 import java.util.List; 39 import java.util.Locale; 40 import java.util.Objects; 41 42 // NOTE: this class is used by device and host side, so it cannot have any Android dependency 43 /** 44 * Rule used to properly check a test behavior depending on whether the device supports {@code 45 * AdService}. 46 * 47 * <p>Typical usage: 48 * 49 * <pre class="prettyprint"> 50 * @Rule 51 * public final AdServicesDeviceSupportedRule adServicesDeviceSupportedRule = 52 * new AdServicesDeviceSupportedRule(); 53 * </pre> 54 * 55 * <p>In the example above, it assumes that every test should only be executed when the device 56 * supports {@code AdServices} - if the device doesn't support it, the test will be skipped (with an 57 * {@link AssumptionViolatedException}). 58 * 59 * <p>This rule can also be used to run tests only on devices that have {@link 60 * android.content.pm.PackageManager#FEATURE_RAM_LOW low memory}, by annotating them with {@link 61 * RequiresLowRamDevice}. 62 * 63 * <p>This rule can also be used to run tests only on devices that have certain screen size, by 64 * annotating them with {@link RequiresScreenSizeDevice}. 65 * 66 * <p>When used with another similar rules, you should organize them using the order of feature 67 * dependency. For example, if the test also requires a given SDK level, you should check use that 68 * rule first, as the device's SDK level is immutable (while whether or not {@code AdServices} 69 * supports a device depends on the device). Example: 70 * 71 * <pre class="prettyprint"> 72 * @Rule(order = 0) 73 * public final SdkLevelSupportRule sdkLevelRule = SdkLevelSupportRule.forAtLeastS(); 74 * 75 * @Rule(order = 1) 76 * public final AdServicesDeviceSupportedRule adServicesDeviceSupportedRule = 77 * new AdServicesDeviceSupportedRule(); 78 * </pre> 79 * 80 * <p><b>NOTE: </b>this class should NOT be used as {@code ClassRule}, as it would result in a "no 81 * tests run" scenario if it throws a {@link AssumptionViolatedException}. 82 */ 83 public abstract class AbstractAdServicesDeviceSupportedRule implements TestRule { 84 85 protected final Logger mLog; 86 private final AbstractDeviceSupportHelper mDeviceSupportHelper; 87 88 @VisibleForTesting 89 static final String REQUIRES_LOW_RAM_ASSUMPTION_FAILED_ERROR_MESSAGE = 90 "Test annotated with @RequiresLowRamDevice and device is not."; 91 92 @VisibleForTesting 93 static final String REQUIRES_SCREEN_SIZE_ASSUMPTION_FAILED_ERROR_MESSAGE = 94 "Test annotated with @RequiresScreenSizeDevice(size=%s) and device is not."; 95 96 @VisibleForTesting 97 static final String REQUIRES_GO_DEVICE_ASSUMPTION_FAILED_ERROR_MESSAGE = 98 "Test annotated with @RequiresGoDevice and device is not a Go device."; 99 100 @VisibleForTesting 101 static final String REQUIRES_ANDROID_SERVICE_ASSUMPTION_FAILED_ERROR_MSG = 102 "Test annotated with @RequiresAndroidServiceAvailable and device doesn't have the" 103 + " android service %s"; 104 105 /** Default constructor. */ AbstractAdServicesDeviceSupportedRule( RealLogger logger, AbstractDeviceSupportHelper deviceSupportHelper)106 public AbstractAdServicesDeviceSupportedRule( 107 RealLogger logger, AbstractDeviceSupportHelper deviceSupportHelper) { 108 mLog = new Logger(Objects.requireNonNull(logger), getClass()); 109 mDeviceSupportHelper = Objects.requireNonNull(deviceSupportHelper); 110 mLog.d("Constructor: logger=%s", logger); 111 } 112 113 /** Checks whether {@code AdServices} is supported by the device. */ isAdServicesSupportedOnDevice()114 public final boolean isAdServicesSupportedOnDevice() { 115 boolean isSupported = mDeviceSupportHelper.isDeviceSupported(); 116 mLog.v("isAdServicesSupportedOnDevice(): %b", isSupported); 117 return isSupported; 118 } 119 120 /** Checks whether the device has low ram. */ isLowRamDevice()121 public final boolean isLowRamDevice() { 122 boolean isLowRamDevice = mDeviceSupportHelper.isLowRamDevice(); 123 mLog.v("isLowRamDevice(): %b", isLowRamDevice); 124 return isLowRamDevice; 125 } 126 127 /** Checks whether the device has large screen. */ isLargeScreenDevice()128 public final boolean isLargeScreenDevice() { 129 boolean isLargeScreenDevice = mDeviceSupportHelper.isLargeScreenDevice(); 130 mLog.v("isLargeScreenDevice(): %b", isLargeScreenDevice); 131 return isLargeScreenDevice; 132 } 133 134 /** Checks whether the device is a go device. */ isGoDevice()135 public final boolean isGoDevice() { 136 boolean isGoDevice = mDeviceSupportHelper.isGoDevice(); 137 mLog.v("isGoDevice(): %b", isGoDevice); 138 return isGoDevice; 139 } 140 141 /** 142 * Check whether the device has a service. 143 * 144 * @return {@code true} when it has and only has one service. 145 */ isAndroidServiceAvailable(String intentAction)146 public final boolean isAndroidServiceAvailable(String intentAction) { 147 boolean isAndroidServiceAvailable = 148 mDeviceSupportHelper.isAndroidServiceAvailable(intentAction); 149 mLog.v( 150 "isAndroidServiceAvailable() for Intent action %s: %b", 151 intentAction, isAndroidServiceAvailable); 152 return isAndroidServiceAvailable; 153 } 154 155 @Override apply(Statement base, Description description)156 public Statement apply(Statement base, Description description) { 157 if (!description.isTest()) { 158 throw new IllegalStateException( 159 "This rule can only be applied to individual tests, it cannot be used as" 160 + " @ClassRule or in a test suite"); 161 } 162 return new Statement() { 163 @Override 164 public void evaluate() throws Throwable { 165 String testName = description.getDisplayName(); 166 boolean isDeviceSupported = isAdServicesSupportedOnDevice(); 167 boolean isLowRamDevice = isLowRamDevice(); 168 boolean isLargeScreenDevice = isLargeScreenDevice(); 169 boolean isGoDevice = isGoDevice(); 170 RequiresLowRamDevice requiresLowRamDevice = 171 description.getAnnotation(RequiresLowRamDevice.class); 172 ScreenSize requiresScreenDevice = getRequiresScreenDevice(description); 173 RequiresGoDevice requiresGoDevice = 174 description.getAnnotation(RequiresGoDevice.class); 175 RequiresAndroidServiceAvailable requiresAndroidServiceAvailable = 176 getRequiresAndroidServiceAvailable(description); 177 178 mLog.d( 179 "apply(): testName=%s, isDeviceSupported=%b, isLowRamDevice=%b," 180 + " requiresLowRamDevice=%s, requiresAndroidServiceAvailable=%s", 181 testName, 182 isDeviceSupported, 183 isLowRamDevice, 184 requiresLowRamDevice, 185 requiresAndroidServiceAvailable); 186 List<String> assumptionViolatedReasons = new ArrayList<>(); 187 188 if (!isDeviceSupported 189 && requiresLowRamDevice == null 190 && requiresGoDevice == null 191 && requiresScreenDevice == null) { 192 // Low-ram devices is a sub-set of unsupported, hence we cannot skip it right 193 // away as the test might be annotated with @RequiresLowRamDevice (which is 194 // checked below) 195 assumptionViolatedReasons.add("Device doesn't support Adservices"); 196 } else { 197 if (!isLowRamDevice && requiresLowRamDevice != null) { 198 assumptionViolatedReasons.add( 199 REQUIRES_LOW_RAM_ASSUMPTION_FAILED_ERROR_MESSAGE); 200 } 201 if (!isGoDevice && requiresGoDevice != null) { 202 assumptionViolatedReasons.add( 203 REQUIRES_GO_DEVICE_ASSUMPTION_FAILED_ERROR_MESSAGE); 204 } 205 if (requiresScreenDevice != null 206 && !AndroidDevicePropertiesHelper.matchScreenSize( 207 requiresScreenDevice, isLargeScreenDevice)) { 208 assumptionViolatedReasons.add( 209 String.format( 210 REQUIRES_SCREEN_SIZE_ASSUMPTION_FAILED_ERROR_MESSAGE, 211 requiresScreenDevice)); 212 } 213 if (requiresAndroidServiceAvailable != null) { 214 String intentAction = requiresAndroidServiceAvailable.intentAction(); 215 if (!isAndroidServiceAvailable(intentAction)) { 216 assumptionViolatedReasons.add( 217 String.format( 218 Locale.ENGLISH, 219 REQUIRES_ANDROID_SERVICE_ASSUMPTION_FAILED_ERROR_MSG, 220 intentAction)); 221 } 222 } 223 } 224 225 // Throw exception in case any of the assumption was violated. 226 if (!assumptionViolatedReasons.isEmpty()) { 227 throw new DeviceConditionsViolatedException(assumptionViolatedReasons); 228 } 229 230 base.evaluate(); 231 } 232 }; 233 } 234 235 @Nullable 236 private ScreenSize getRequiresScreenDevice(Description description) { 237 RequiresScreenSizeDevice requiresLargeScreenDevice = 238 description.getAnnotation(RequiresScreenSizeDevice.class); 239 if (requiresLargeScreenDevice != null) { 240 return requiresLargeScreenDevice.value(); 241 } 242 return null; 243 } 244 245 // Check both class and the method for the annotation RequiresAndroidServiceAvailable, while the 246 // method's annotation prevails. 247 @Nullable 248 private RequiresAndroidServiceAvailable getRequiresAndroidServiceAvailable( 249 Description description) { 250 Annotation[] annotations = description.getTestClass().getAnnotations(); 251 252 RequiresAndroidServiceAvailable classAnnotation = null; 253 for (Annotation annotation : annotations) { 254 if (annotation instanceof RequiresAndroidServiceAvailable) { 255 classAnnotation = (RequiresAndroidServiceAvailable) annotation; 256 break; 257 } 258 } 259 260 RequiresAndroidServiceAvailable methodAnnotation = 261 description.getAnnotation(RequiresAndroidServiceAvailable.class); 262 263 if (methodAnnotation == null) { 264 return classAnnotation; 265 } 266 267 return methodAnnotation; 268 } 269 } 270