diff options
Diffstat (limited to 'adservices/tests/perf/src/android/adservices/test')
7 files changed, 1854 insertions, 0 deletions
diff --git a/adservices/tests/perf/src/android/adservices/test/scenario/adservices/MeasurementRegisterCalls.java b/adservices/tests/perf/src/android/adservices/test/scenario/adservices/MeasurementRegisterCalls.java new file mode 100644 index 0000000000..6cabc9700c --- /dev/null +++ b/adservices/tests/perf/src/android/adservices/test/scenario/adservices/MeasurementRegisterCalls.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.adservices.test.scenario.adservices; + +import android.Manifest; +import android.adservices.clients.measurement.MeasurementClient; +import android.adservices.test.scenario.adservices.utils.MockWebServerRule; +import android.content.Context; +import android.net.Uri; +import android.os.SystemClock; +import android.platform.test.scenario.annotation.Scenario; +import android.provider.DeviceConfig; +import android.support.test.uiautomator.UiDevice; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.google.mockwebserver.MockResponse; +import com.google.mockwebserver.MockWebServer; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.IOException; +import java.net.URL; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +/** Crystal Ball tests for Measurement API. */ +@Scenario +@RunWith(JUnit4.class) +public class MeasurementRegisterCalls { + protected static final Context sContext = ApplicationProvider.getApplicationContext(); + private static final Executor CALLBACK_EXECUTOR = Executors.newCachedThreadPool(); + private static MeasurementClient sMeasurementClient; + private static UiDevice sDevice; + + @BeforeClass + public static void setupDevicePropertiesAndInitializeClient() throws Exception { + sMeasurementClient = + new MeasurementClient.Builder() + .setContext(sContext) + .setExecutor(CALLBACK_EXECUTOR) + .build(); + InstrumentationRegistry.getInstrumentation() + .getUiAutomation() + .adoptShellPermissionIdentity(Manifest.permission.WRITE_DEVICE_CONFIG); + + // Override consent manager behavior to give user consent. + getUiDevice() + .executeShellCommand("setprop debug.adservices.consent_manager_debug_mode true"); + + // Override the flag to allow current package to call APIs. + DeviceConfig.setProperty( + DeviceConfig.NAMESPACE_ADSERVICES, + "ppapi_app_allow_list", + "*", + /* makeDefault */ false); + } + + @AfterClass + public static void resetDeviceProperties() throws Exception { + // Reset consent + getUiDevice() + .executeShellCommand("setprop debug.adservices.consent_manager_debug_mode false"); + + // Reset allowed packages. + DeviceConfig.setProperty( + DeviceConfig.NAMESPACE_ADSERVICES, + "ppapi_app_allow_list", + "null", + /* makeDefault */ false); + } + + @Test + public void testRegisterSourceAndTriggerAndRunAttributionAndReporting() throws Exception { + // Create source registration response. + MockResponse sourceResponse = new MockResponse(); + final JSONObject headerRegisterSource = + buildJson( + Map.of( + "source_event_id", 1, + "destination", "android-app://android.platform.test.scenario", + "priority", 1)); + sourceResponse.addHeader("Attribution-Reporting-Register-Source", headerRegisterSource); + sourceResponse.setResponseCode(200); + + // Create trigger registration response. + MockResponse triggerResponse = new MockResponse(); + final JSONObject eventTriggerData = + buildJson( + Map.of( + "trigger_data", "1", + "priority", "101")); + final JSONArray eventTriggerDataList = buildJsonArray(List.of(eventTriggerData)); + final JSONObject headerRegisterTrigger = + buildJson(Map.of("event_trigger_data", eventTriggerDataList)); + triggerResponse.addHeader("Attribution-Reporting-Register-Trigger", headerRegisterTrigger); + triggerResponse.setResponseCode(200); + + // Create report response. + MockResponse reportResponse = new MockResponse(); + reportResponse.setResponseCode(200); + + // Start mock web server. + List<MockResponse> responses = List.of(sourceResponse, triggerResponse, reportResponse); + MockWebServer mockWebServer = + MockWebServerRule.forHttps( + sContext, "adservices_test_server.p12", "adservices_test") + .startMockWebServer(responses); + + URL url = mockWebServer.getUrl("/mockServer"); + + // Set the initial time to register the source and trigger. + getUiDevice().executeShellCommand("date -s 2022-08-01"); + + sMeasurementClient.registerSource(Uri.parse(url.toString()), null).get(); + sMeasurementClient.registerTrigger(Uri.parse(url.toString())).get(); + runAttributionJob(); + + // Advance the time so that generated report is within the reporting window. + getUiDevice().executeShellCommand("date -s 2022-09-01"); + runReportingJob(); + } + + private void runAttributionJob() throws InterruptedException, IOException { + getUiDevice() + .executeShellCommand("cmd jobscheduler run -f com.google.android.adservices.api 5"); + // Wait for attribution to complete. + SystemClock.sleep(2000); + } + + private void runReportingJob() throws InterruptedException, IOException { + getUiDevice() + .executeShellCommand("cmd jobscheduler run -f com.google.android.adservices.api 3"); + // Wait for reporting to complete. + SystemClock.sleep(2000); + } + + private static JSONObject buildJson(Map<String, Object> fields) throws JSONException { + JSONObject json = new JSONObject(); + for (Map.Entry<String, Object> entry : fields.entrySet()) { + json.put(entry.getKey(), entry.getValue()); + } + return json; + } + + private static JSONArray buildJsonArray(List<JSONObject> list) throws JSONException { + JSONArray json = new JSONArray(); + for (int i = 0; i < list.size(); i++) { + json.put(i, list.get(i)); + } + return json; + } + + private static UiDevice getUiDevice() { + if (sDevice == null) { + sDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); + } + return sDevice; + } +} diff --git a/adservices/tests/perf/src/android/adservices/test/scenario/adservices/fledge/LimitPerfTests.java b/adservices/tests/perf/src/android/adservices/test/scenario/adservices/fledge/LimitPerfTests.java new file mode 100644 index 0000000000..0b15e94d60 --- /dev/null +++ b/adservices/tests/perf/src/android/adservices/test/scenario/adservices/fledge/LimitPerfTests.java @@ -0,0 +1,609 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.adservices.test.scenario.adservices.fledge; + +import android.Manifest; +import android.adservices.adselection.AdSelectionConfig; +import android.adservices.adselection.AdSelectionOutcome; +import android.adservices.adselection.ReportImpressionRequest; +import android.adservices.clients.adselection.AdSelectionClient; +import android.adservices.clients.customaudience.AdvertisingCustomAudienceClient; +import android.adservices.common.AdData; +import android.adservices.common.AdSelectionSignals; +import android.adservices.common.AdTechIdentifier; +import android.adservices.customaudience.CustomAudience; +import android.adservices.customaudience.TrustedBiddingData; +import android.adservices.test.scenario.adservices.utils.MockWebServerRule; +import android.adservices.test.scenario.adservices.utils.MockWebServerRuleFactory; +import android.content.Context; +import android.net.Uri; +import android.platform.test.scenario.annotation.Scenario; +import android.provider.DeviceConfig; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.compatibility.common.util.ShellUtils; + +import com.google.common.collect.ImmutableList; +import com.google.mockwebserver.Dispatcher; +import com.google.mockwebserver.MockResponse; +import com.google.mockwebserver.RecordedRequest; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +@Scenario +@RunWith(JUnit4.class) +public class LimitPerfTests { + + // The number of ms to sleep after killing the adservices process so it has time to recover + public static final long SLEEP_MS_AFTER_KILL = 2000L; + // Command to kill the adservices process + public static final String KILL_ADSERVICES_CMD = + "su 0 killall -9 com.google.android.adservices.api"; + // Command prevent activity manager from backing off on restarting the adservices process + public static final String DISABLE_ADSERVICES_BACKOFF_CMD = + "am service-restart-backoff disable com.google.android.adservices.api"; + + public static final Duration CUSTOM_AUDIENCE_EXPIRE_IN = Duration.ofDays(1); + public static final Instant VALID_ACTIVATION_TIME = Instant.now(); + public static final Instant VALID_EXPIRATION_TIME = + VALID_ACTIVATION_TIME.plus(CUSTOM_AUDIENCE_EXPIRE_IN); + public static final String VALID_NAME = "testCustomAudienceName"; + public static final AdSelectionSignals VALID_USER_BIDDING_SIGNALS = + AdSelectionSignals.fromString("{'valid': 'yep', 'opaque': 'definitely'}"); + public static final String VALID_TRUSTED_BIDDING_URI_PATH = "/trusted/bidding/"; + public static final ArrayList<String> VALID_TRUSTED_BIDDING_KEYS = + new ArrayList<>(Arrays.asList("example", "valid", "list", "of", "keys")); + public static final AdTechIdentifier SELLER = AdTechIdentifier.fromString("localhost"); + // Uri Constants + public static final String DECISION_LOGIC_PATH = "/decisionFragment"; + public static final String TRUSTED_SCORING_SIGNAL_PATH = "/trustedScoringSignalsFragment"; + public static final String CUSTOM_AUDIENCE_SHIRT = "ca_shirt"; + public static final String CUSTOM_AUDIENCE_SHOES = "ca_shoe"; + // TODO(b/244530379) Make compatible with multiple buyers + public static final AdTechIdentifier BUYER_1 = AdTechIdentifier.fromString("localhost"); + public static final List<AdTechIdentifier> CUSTOM_AUDIENCE_BUYERS = + Collections.singletonList(BUYER_1); + public static final AdSelectionSignals AD_SELECTION_SIGNALS = + AdSelectionSignals.fromString("{\"ad_selection_signals\":1}"); + public static final AdSelectionSignals SELLER_SIGNALS = + AdSelectionSignals.fromString("{\"test_seller_signals\":1}"); + public static final Map<AdTechIdentifier, AdSelectionSignals> PER_BUYER_SIGNALS = + Map.of(BUYER_1, AdSelectionSignals.fromString("{\"buyer_signals\":1}")); + private static final Executor CALLBACK_EXECUTOR = Executors.newCachedThreadPool(); + // Time allowed by current test setup for APIs to respond + // setting a large value for perf testing, to avoid failing for large datasets + private static final int API_RESPONSE_TIMEOUT_SECONDS = 100; + private static final String BUYER_BIDDING_LOGIC_URI_PATH = "/buyer/bidding/logic/"; + private static final String SELLER_REPORTING_PATH = "/reporting/seller"; + private static final String BUYER_REPORTING_PATH = "/reporting/buyer"; + private static final String DEFAULT_DECISION_LOGIC_JS = + "function scoreAd(ad, bid, auction_config, seller_signals," + + " trusted_scoring_signals, contextual_signal, user_signal," + + " custom_audience_signal) { \n" + + " return {'status': 0, 'score': bid };\n" + + "}\n" + + "function reportResult(ad_selection_config, render_uri, bid," + + " contextual_signals) { \n" + + " return {'status': 0, 'results': {'signals_for_buyer':" + + " '{\"signals_for_buyer\":1}', 'reporting_uri': '" + + SELLER_REPORTING_PATH + + "' } };\n" + + "}"; + private static final String DEFAULT_BIDDING_LOGIC_JS = + "function generateBid(ad, auction_signals, per_buyer_signals," + + " trusted_bidding_signals, contextual_signals," + + " custom_audience_signals) { \n" + + " return {'status': 0, 'ad': ad, 'bid': ad.metadata.result };\n" + + "}\n" + + "function reportWin(ad_selection_signals, per_buyer_signals," + + " signals_for_buyer, contextual_signals, custom_audience_signals) { \n" + + " return {'status': 0, 'results': {'reporting_uri': '" + + BUYER_REPORTING_PATH + + "' } };\n" + + "}"; + private static final String CALCULATION_INTENSE_JS = + "for (let i = 1; i < 1000000000; i++) {\n" + " Math.sqrt(i);\n" + "}"; + private static final String MEMORY_INTENSE_JS = + "var a = []\n" + "for (let i = 0; i < 10000; i++) {\n" + " a.push(i);" + "}"; + + private static final AdSelectionSignals TRUSTED_SCORING_SIGNALS = + AdSelectionSignals.fromString( + "{\n" + + "\t\"render_uri_1\": \"signals_for_1\",\n" + + "\t\"render_uri_2\": \"signals_for_2\"\n" + + "}"); + private static final AdSelectionSignals TRUSTED_BIDDING_SIGNALS = + AdSelectionSignals.fromString( + "{\n" + + "\t\"example\": \"example\",\n" + + "\t\"valid\": \"Also valid\",\n" + + "\t\"list\": \"list\",\n" + + "\t\"of\": \"of\",\n" + + "\t\"keys\": \"trusted bidding signal Values\"\n" + + "}"); + private static final String AD_URI_PREFIX = "/adverts/123/"; + private static final int DELAY_TO_AVOID_THROTTLE_MS = 1001; + protected final Context mContext = ApplicationProvider.getApplicationContext(); + private final AdSelectionClient mAdSelectionClient = + new AdSelectionClient.Builder() + .setContext(mContext) + .setExecutor(CALLBACK_EXECUTOR) + .build(); + private final AdvertisingCustomAudienceClient mCustomAudienceClient = + new AdvertisingCustomAudienceClient.Builder() + .setContext(mContext) + .setExecutor(CALLBACK_EXECUTOR) + .build(); + @Rule public MockWebServerRule mMockWebServerRule = MockWebServerRuleFactory.createForHttps(); + private Dispatcher mDefaultDispatcher; + + @BeforeClass + public static void setupBeforeClass() { + // Disable backoff since we will be killing the process between tests + ShellUtils.runShellCommand(DISABLE_ADSERVICES_BACKOFF_CMD); + InstrumentationRegistry.getInstrumentation() + .getUiAutomation() + .adoptShellPermissionIdentity(Manifest.permission.WRITE_DEVICE_CONFIG); + // TODO(b/245585645) Mark true for the heap size enforcement after installing M105 library + DeviceConfig.setProperty( + DeviceConfig.NAMESPACE_ADSERVICES, + "fledge_js_isolate_enforce_max_heap_size", + "false", + true); + DeviceConfig.setProperty( + DeviceConfig.NAMESPACE_ADSERVICES, "disable_fledge_enrollment_check", "true", true); + DeviceConfig.setProperty( + DeviceConfig.NAMESPACE_ADSERVICES, "ppapi_app_allow_list", "*", true); + } + + public static Uri getUri(String name, String path) { + return Uri.parse("https://" + name + path); + } + + @Before + public void setup() throws InterruptedException { + ShellUtils.runShellCommand(KILL_ADSERVICES_CMD); + Thread.sleep(SLEEP_MS_AFTER_KILL); + mDefaultDispatcher = + new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest request) { + if (DECISION_LOGIC_PATH.equals(request.getPath())) { + return new MockResponse().setBody(DEFAULT_DECISION_LOGIC_JS); + } else if (BUYER_BIDDING_LOGIC_URI_PATH.equals(request.getPath())) { + return new MockResponse().setBody(DEFAULT_BIDDING_LOGIC_JS); + } else if (BUYER_REPORTING_PATH.equals(request.getPath()) + || SELLER_REPORTING_PATH.equals(request.getPath())) { + return new MockResponse().setBody(""); + } else if (request.getPath().startsWith(TRUSTED_SCORING_SIGNAL_PATH)) { + return new MockResponse().setBody(TRUSTED_SCORING_SIGNALS.toString()); + } else if (request.getPath().startsWith(VALID_TRUSTED_BIDDING_URI_PATH)) { + return new MockResponse().setBody(TRUSTED_BIDDING_SIGNALS.toString()); + } + return new MockResponse().setResponseCode(404); + } + }; + } + + @Test + public void test_joinCustomAudience_success() throws Exception { + CustomAudience ca = + createCustomAudience( + BUYER_1, CUSTOM_AUDIENCE_SHOES, Collections.singletonList(1.0)); + addDelayToAvoidThrottle(); + mCustomAudienceClient + .joinCustomAudience(ca) + .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS); + addDelayToAvoidThrottle(); + mCustomAudienceClient + .leaveCustomAudience(ca.getBuyer(), ca.getName()) + .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } + + @Test + public void testAdSelectionAndReporting_normalFlow_success() throws Exception { + List<Double> bidsForBuyer1 = ImmutableList.of(1.1, 2.2); + List<Double> bidsForBuyer2 = ImmutableList.of(4.5, 6.7, 10.0); + CustomAudience customAudience1 = + createCustomAudience(BUYER_1, CUSTOM_AUDIENCE_SHOES, bidsForBuyer1); + + // TODO(b/244530379) Make compatible with multiple buyers + CustomAudience customAudience2 = + createCustomAudience(BUYER_1, CUSTOM_AUDIENCE_SHIRT, bidsForBuyer2); + List<CustomAudience> customAudienceList = Arrays.asList(customAudience1, customAudience2); + + mMockWebServerRule.startMockWebServer(mDefaultDispatcher); + + for (CustomAudience ca : customAudienceList) { + addDelayToAvoidThrottle(); + mCustomAudienceClient + .joinCustomAudience(ca) + .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } + + // Running ad selection and asserting that the outcome is returned in < 10 seconds + addDelayToAvoidThrottle(); + AdSelectionOutcome outcome = + mAdSelectionClient + .selectAds(createAdSelectionConfig()) + .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS); + + // The winning ad should be ad3 from CA shirt + Assert.assertEquals( + "Ad selection outcome is not expected", + createExpectedWinningUri(BUYER_1, CUSTOM_AUDIENCE_SHIRT, 3), + outcome.getRenderUri()); + + ReportImpressionRequest reportImpressionRequest = + new ReportImpressionRequest(outcome.getAdSelectionId(), createAdSelectionConfig()); + + // Performing reporting, and asserting that no exception is thrown + addDelayToAvoidThrottle(); + mAdSelectionClient + .reportImpression(reportImpressionRequest) + .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS); + + // Cleanup + for (CustomAudience ca : customAudienceList) { + addDelayToAvoidThrottle(DELAY_TO_AVOID_THROTTLE_MS); + mCustomAudienceClient + .leaveCustomAudience(ca.getBuyer(), ca.getName()) + .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } + } + + @Test + public void testAdSelectionAndReporting_executionHeavyJS_success() throws Exception { + List<Double> bidsForBuyer1 = ImmutableList.of(1.1, 2.2); + List<Double> bidsForBuyer2 = ImmutableList.of(4.5, 6.7, 10.0); + CustomAudience customAudience1 = + createCustomAudience(BUYER_1, CUSTOM_AUDIENCE_SHOES, bidsForBuyer1); + + // TODO(b/244530379) Make compatible with multiple buyers + CustomAudience customAudience2 = + createCustomAudience(BUYER_1, CUSTOM_AUDIENCE_SHIRT, bidsForBuyer2); + List<CustomAudience> customAudienceList = Arrays.asList(customAudience1, customAudience2); + + String calculation_intense_logic_js = + "function scoreAd(ad, bid, auction_config, seller_signals," + + " trusted_scoring_signals, contextual_signal, user_signal," + + " custom_audience_signal) { \n" + + CALCULATION_INTENSE_JS + + " return {'status': 0, 'score': bid };\n" + + "}\n" + + "function reportResult(ad_selection_config, render_uri, bid," + + " contextual_signals) { \n" + + " return {'status': 0, 'results': {'signals_for_buyer':" + + " '{\"signals_for_buyer\":1}', 'reporting_uri': '" + + SELLER_REPORTING_PATH + + "' } };\n" + + "}"; + + Dispatcher dispatcher = + new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest request) { + if (DECISION_LOGIC_PATH.equals(request.getPath())) { + return new MockResponse().setBody(calculation_intense_logic_js); + } else if (BUYER_BIDDING_LOGIC_URI_PATH.equals(request.getPath())) { + return new MockResponse().setBody(DEFAULT_BIDDING_LOGIC_JS); + } else if (BUYER_REPORTING_PATH.equals(request.getPath()) + || SELLER_REPORTING_PATH.equals(request.getPath())) { + return new MockResponse().setBody(""); + } else if (request.getPath().startsWith(TRUSTED_SCORING_SIGNAL_PATH)) { + return new MockResponse().setBody(TRUSTED_SCORING_SIGNALS.toString()); + } else if (request.getPath().startsWith(VALID_TRUSTED_BIDDING_URI_PATH)) { + return new MockResponse().setBody(TRUSTED_BIDDING_SIGNALS.toString()); + } + return new MockResponse().setResponseCode(404); + } + }; + + mMockWebServerRule.startMockWebServer(dispatcher); + + for (CustomAudience ca : customAudienceList) { + addDelayToAvoidThrottle(); + mCustomAudienceClient + .joinCustomAudience(ca) + .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } + + // Running ad selection and asserting that the outcome is returned in < 10 seconds + addDelayToAvoidThrottle(); + AdSelectionOutcome outcome = + mAdSelectionClient + .selectAds(createAdSelectionConfig()) + .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS); + + // The winning ad should be ad3 from CA shirt + Assert.assertEquals( + "Ad selection outcome is not expected", + createExpectedWinningUri(BUYER_1, CUSTOM_AUDIENCE_SHIRT, 3), + outcome.getRenderUri()); + + ReportImpressionRequest reportImpressionRequest = + new ReportImpressionRequest(outcome.getAdSelectionId(), createAdSelectionConfig()); + + // Performing reporting, and asserting that no exception is thrown + addDelayToAvoidThrottle(); + mAdSelectionClient + .reportImpression(reportImpressionRequest) + .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS); + + // Cleanup + for (CustomAudience ca : customAudienceList) { + addDelayToAvoidThrottle(DELAY_TO_AVOID_THROTTLE_MS); + mCustomAudienceClient + .leaveCustomAudience(ca.getBuyer(), ca.getName()) + .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } + } + + @Test + public void testAdSelectionAndReporting_memoryHeavyJS_success() throws Exception { + List<Double> bidsForBuyer1 = ImmutableList.of(1.1, 2.2); + List<Double> bidsForBuyer2 = ImmutableList.of(4.5, 6.7, 10.0); + CustomAudience customAudience1 = + createCustomAudience(BUYER_1, CUSTOM_AUDIENCE_SHOES, bidsForBuyer1); + + // TODO(b/244530379) Make compatible with multiple buyers + CustomAudience customAudience2 = + createCustomAudience(BUYER_1, CUSTOM_AUDIENCE_SHIRT, bidsForBuyer2); + List<CustomAudience> customAudienceList = Arrays.asList(customAudience1, customAudience2); + + String memory_intense_logic_js = + "function scoreAd(ad, bid, auction_config, seller_signals," + + " trusted_scoring_signals, contextual_signal, user_signal," + + " custom_audience_signal) { \n" + + MEMORY_INTENSE_JS + + " return {'status': 0, 'score': bid };\n" + + "}\n" + + "function reportResult(ad_selection_config, render_uri, bid," + + " contextual_signals) { \n" + + " return {'status': 0, 'results': {'signals_for_buyer':" + + " '{\"signals_for_buyer\":1}', 'reporting_uri': '" + + SELLER_REPORTING_PATH + + "' } };\n" + + "}"; + + Dispatcher dispatcher = + new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest request) { + if (DECISION_LOGIC_PATH.equals(request.getPath())) { + return new MockResponse().setBody(memory_intense_logic_js); + } else if (BUYER_BIDDING_LOGIC_URI_PATH.equals(request.getPath())) { + return new MockResponse().setBody(DEFAULT_BIDDING_LOGIC_JS); + } else if (BUYER_REPORTING_PATH.equals(request.getPath()) + || SELLER_REPORTING_PATH.equals(request.getPath())) { + return new MockResponse().setBody(""); + } else if (request.getPath().startsWith(TRUSTED_SCORING_SIGNAL_PATH)) { + return new MockResponse().setBody(TRUSTED_SCORING_SIGNALS.toString()); + } else if (request.getPath().startsWith(VALID_TRUSTED_BIDDING_URI_PATH)) { + return new MockResponse().setBody(TRUSTED_BIDDING_SIGNALS.toString()); + } + return new MockResponse().setResponseCode(404); + } + }; + + mMockWebServerRule.startMockWebServer(dispatcher); + + for (CustomAudience ca : customAudienceList) { + addDelayToAvoidThrottle(); + mCustomAudienceClient + .joinCustomAudience(ca) + .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } + + // Running ad selection and asserting that the outcome is returned in < 10 seconds + addDelayToAvoidThrottle(); + AdSelectionOutcome outcome = + mAdSelectionClient + .selectAds(createAdSelectionConfig()) + .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS); + + // The winning ad should be ad3 from CA shirt + Assert.assertEquals( + "Ad selection outcome is not expected", + createExpectedWinningUri(BUYER_1, CUSTOM_AUDIENCE_SHIRT, 3), + outcome.getRenderUri()); + + ReportImpressionRequest reportImpressionRequest = + new ReportImpressionRequest(outcome.getAdSelectionId(), createAdSelectionConfig()); + + // Performing reporting, and asserting that no exception is thrown + addDelayToAvoidThrottle(); + mAdSelectionClient + .reportImpression(reportImpressionRequest) + .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS); + + // Cleanup + for (CustomAudience ca : customAudienceList) { + addDelayToAvoidThrottle(DELAY_TO_AVOID_THROTTLE_MS); + mCustomAudienceClient + .leaveCustomAudience(ca.getBuyer(), ca.getName()) + .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } + } + + @Test + public void testAdSelectionAndReporting_multipleCustomAudienceList_success() throws Exception { + List<Double> bidsForBuyer1 = ImmutableList.of(1.1, 2.2); + List<Double> bidsForBuyer2 = ImmutableList.of(4.5, 6.7, 10.0); + CustomAudience customAudience1 = + createCustomAudience(BUYER_1, CUSTOM_AUDIENCE_SHOES, bidsForBuyer1); + + // TODO(b/244530379) Make compatible with multiple buyers + CustomAudience customAudience2 = + createCustomAudience(BUYER_1, CUSTOM_AUDIENCE_SHIRT, bidsForBuyer2); + List<CustomAudience> customAudienceList = new ArrayList<>(); + customAudienceList.add(customAudience1); + customAudienceList.add(customAudience2); + + // Create multiple generic custom audience entries + for (int i = 1; i <= 48; i++) { + CustomAudience customAudience = + createCustomAudience(BUYER_1, "GENERIC_CA_" + i, bidsForBuyer1); + customAudienceList.add(customAudience); + } + mMockWebServerRule.startMockWebServer(mDefaultDispatcher); + + for (CustomAudience ca : customAudienceList) { + addDelayToAvoidThrottle(); + mCustomAudienceClient + .joinCustomAudience(ca) + .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } + + // Running ad selection and asserting that the outcome is returned in < 10 seconds + addDelayToAvoidThrottle(); + AdSelectionOutcome outcome = + mAdSelectionClient + .selectAds(createAdSelectionConfig()) + .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS); + + // The winning ad should be ad3 from CA shirt + Assert.assertEquals( + "Ad selection outcome is not expected", + createExpectedWinningUri(BUYER_1, CUSTOM_AUDIENCE_SHIRT, 3), + outcome.getRenderUri()); + + ReportImpressionRequest reportImpressionRequest = + new ReportImpressionRequest(outcome.getAdSelectionId(), createAdSelectionConfig()); + + // Performing reporting, and asserting that no exception is thrown + addDelayToAvoidThrottle(); + mAdSelectionClient + .reportImpression(reportImpressionRequest) + .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS); + + // Cleanup + for (CustomAudience ca : customAudienceList) { + addDelayToAvoidThrottle(DELAY_TO_AVOID_THROTTLE_MS); + mCustomAudienceClient + .leaveCustomAudience(ca.getBuyer(), ca.getName()) + .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } + } + + private void addDelayToAvoidThrottle() throws InterruptedException { + addDelayToAvoidThrottle(DELAY_TO_AVOID_THROTTLE_MS); + } + + private void addDelayToAvoidThrottle(int delayValueMs) throws InterruptedException { + if (delayValueMs > 0) { + Thread.sleep(delayValueMs); + } + } + + private CustomAudience createCustomAudience( + final AdTechIdentifier buyer, + String name, + List<Double> bids, + Instant activationTime, + Instant expirationTime) { + // Generate ads for with bids provided + List<AdData> ads = new ArrayList<>(); + + // Create ads with the custom audience name and bid number as the ad URI + // Add the bid value to the metadata + for (int i = 0; i < bids.size(); i++) { + ads.add( + new AdData.Builder() + .setRenderUri( + getUri( + buyer.toString(), + AD_URI_PREFIX + name + "/ad" + (i + 1))) + .setMetadata("{\"result\":" + bids.get(i) + "}") + .build()); + } + + return new CustomAudience.Builder() + .setBuyer(buyer) + .setName(name) + .setActivationTime(activationTime) + .setExpirationTime(expirationTime) + .setDailyUpdateUri(getValidDailyUpdateUriByBuyer(buyer)) + .setUserBiddingSignals(VALID_USER_BIDDING_SIGNALS) + .setTrustedBiddingData(getValidTrustedBiddingDataByBuyer(buyer)) + .setBiddingLogicUri(mMockWebServerRule.uriForPath(BUYER_BIDDING_LOGIC_URI_PATH)) + .setAds(ads) + .build(); + } + + private CustomAudience createCustomAudience( + final AdTechIdentifier buyer, String name, List<Double> bids) { + return createCustomAudience( + buyer, name, bids, VALID_ACTIVATION_TIME, VALID_EXPIRATION_TIME); + } + + private AdSelectionConfig createAdSelectionConfig() { + return new AdSelectionConfig.Builder() + .setSeller(SELLER) + .setDecisionLogicUri(mMockWebServerRule.uriForPath(DECISION_LOGIC_PATH)) + .setCustomAudienceBuyers(CUSTOM_AUDIENCE_BUYERS) + .setAdSelectionSignals(AD_SELECTION_SIGNALS) + .setSellerSignals(SELLER_SIGNALS) + .setPerBuyerSignals(PER_BUYER_SIGNALS) + .setTrustedScoringSignalsUri( + mMockWebServerRule.uriForPath(TRUSTED_SCORING_SIGNAL_PATH)) + // TODO(b/244530379) Make compatible with multiple buyers + .setCustomAudienceBuyers(Collections.singletonList(BUYER_1)) + .build(); + } + + private Uri createExpectedWinningUri( + AdTechIdentifier buyer, String customAudienceName, int adNumber) { + return getUri(buyer.toString(), AD_URI_PREFIX + customAudienceName + "/ad" + adNumber); + } + + // TODO(b/244530379) Make compatible with multiple buyers + public Uri getValidDailyUpdateUriByBuyer(AdTechIdentifier buyer) { + return mMockWebServerRule.uriForPath("/update"); + } + + public TrustedBiddingData getValidTrustedBiddingDataByBuyer(AdTechIdentifier buyer) { + return new TrustedBiddingData.Builder() + .setTrustedBiddingKeys(VALID_TRUSTED_BIDDING_KEYS) + .setTrustedBiddingUri(getValidTrustedBiddingUriByBuyer(buyer)) + .build(); + } + + // TODO(b/244530379) Make compatible with multiple buyers + public Uri getValidTrustedBiddingUriByBuyer(AdTechIdentifier buyer) { + return mMockWebServerRule.uriForPath(VALID_TRUSTED_BIDDING_URI_PATH); + } +} diff --git a/adservices/tests/perf/src/android/adservices/test/scenario/adservices/fledge/SelectAdsLatency.java b/adservices/tests/perf/src/android/adservices/test/scenario/adservices/fledge/SelectAdsLatency.java new file mode 100644 index 0000000000..5af52346a9 --- /dev/null +++ b/adservices/tests/perf/src/android/adservices/test/scenario/adservices/fledge/SelectAdsLatency.java @@ -0,0 +1,301 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package src.android.adservices.test.scenario.adservices.fledge; + +import android.adservices.adselection.AdSelectionConfig; +import android.adservices.adselection.AdSelectionOutcome; +import android.adservices.clients.adselection.AdSelectionClient; +import android.adservices.clients.customaudience.AdvertisingCustomAudienceClient; +import android.adservices.common.AdSelectionSignals; +import android.adservices.common.AdTechIdentifier; +import android.adservices.test.scenario.adservices.utils.CustomAudienceSetupRule; +import android.adservices.test.scenario.adservices.utils.MockWebServerDispatcherFactory; +import android.adservices.test.scenario.adservices.utils.MockWebServerRule; +import android.adservices.test.scenario.adservices.utils.MockWebServerRuleFactory; +import android.content.Context; +import android.net.Uri; +import android.platform.test.scenario.annotation.Scenario; +import android.util.Log; + +import androidx.test.core.app.ApplicationProvider; + +import com.android.compatibility.common.util.ShellUtils; + +import com.google.common.base.Stopwatch; +import com.google.common.base.Ticker; + +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +/** Tests to compute latency runs for on device ad selection. */ + +// TODO(b/251802548): Consider parameterizing the tests. +@Scenario +@RunWith(JUnit4.class) +public class SelectAdsLatency { + private static final String TAG = "SelectAds"; + + private static final AdTechIdentifier BUYER = AdTechIdentifier.fromString("localhost"); + private static final List<AdTechIdentifier> CUSTOM_AUDIENCE_BUYERS = + Collections.singletonList(BUYER); + private static final AdSelectionSignals AD_SELECTION_SIGNALS = + AdSelectionSignals.fromString("{\"ad_selection_signals\":1}"); + private static final AdSelectionSignals SELLER_SIGNALS = + AdSelectionSignals.fromString("{\"test_seller_signals\":1}"); + private static final Map<AdTechIdentifier, AdSelectionSignals> PER_BUYER_SIGNALS = + Map.of(BUYER, AdSelectionSignals.fromString("{\"buyer_signals\":1}")); + private static final AdTechIdentifier SELLER = AdTechIdentifier.fromString("localhost"); + private static final int API_RESPONSE_TIMEOUT_SECONDS = 100; + private static final Executor CALLBACK_EXECUTOR = Executors.newCachedThreadPool(); + + // Estimates from + // https://docs.google.com/spreadsheets/d/1EP_cwBbwYI-NMro0Qq5uif1krwjIQhjK8fjOu15j7hQ/edit?usp=sharing&resourcekey=0-A67kzEnAKKz1k7qpshSedg + private static final int NUMBER_OF_CUSTOM_AUDIENCES_MEDIUM = 10; + private static final int NUMBER_OF_CUSTOM_AUDIENCES_LARGE = 50; + private static final int NUMBER_ADS_PER_CA_MEDIUM = 5; + private static final int NUMBER_ADS_PER_CA_LARGE = 10; + + private static final String LOG_LABEL_P50_5G = "SELECT_ADS_LATENCY_P50_5G"; + private static final String LOG_LABEL_P50_4GPLUS = "SELECT_ADS_LATENCY_P50_4GPLUS"; + private static final String LOG_LABEL_P50_4G = "SELECT_ADS_LATENCY_P50_4G"; + private static final String LOG_LABEL_P90_5G = "SELECT_ADS_LATENCY_P90_5G"; + private static final String LOG_LABEL_P90_4GPLUS = "SELECT_ADS_LATENCY_P90_4GPLUS"; + private static final String LOG_LABEL_P90_4G = "SELECT_ADS_LATENCY_P90_4G"; + + private static final String AD_SELECTION_FAILURE_MESSAGE = + "Ad selection outcome is not expected"; + + protected final Context mContext = ApplicationProvider.getApplicationContext(); + private final AdSelectionClient mAdSelectionClient = + new AdSelectionClient.Builder() + .setContext(mContext) + .setExecutor(CALLBACK_EXECUTOR) + .build(); + private final AdvertisingCustomAudienceClient mCustomAudienceClient = + new AdvertisingCustomAudienceClient.Builder() + .setContext(mContext) + .setExecutor(CALLBACK_EXECUTOR) + .build(); + @Rule public MockWebServerRule mMockWebServerRule = MockWebServerRuleFactory.createForHttps(); + + @Rule + public CustomAudienceSetupRule mCustomAudienceSetupRule = + new CustomAudienceSetupRule(mCustomAudienceClient, mMockWebServerRule); + + private final Ticker mTicker = + new Ticker() { + public long read() { + return android.os.SystemClock.elapsedRealtimeNanos(); + } + }; + + @BeforeClass + public static void setupBeforeClass() { + ShellUtils.runShellCommand("su 0 killall -9 com.google.android.adservices.api"); + } + + @Test + public void selectAds_p50_5G() throws Exception { + mMockWebServerRule.createMockWebServer(); + mMockWebServerRule.startCreatedMockWebServer( + MockWebServerDispatcherFactory.create5Gp50LatencyDispatcher(mMockWebServerRule)); + mCustomAudienceSetupRule.populateCustomAudiences( + NUMBER_OF_CUSTOM_AUDIENCES_MEDIUM, NUMBER_ADS_PER_CA_MEDIUM); + Stopwatch timer = Stopwatch.createStarted(mTicker); + + AdSelectionOutcome outcome = + mAdSelectionClient + .selectAds(createAdSelectionConfig()) + .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS); + + timer.stop(); + + // TODO(b/250851601): Currently we use logcat latency collector. Consider replacing this + // with a perfetto trace collector because it won't be affected by write to logcat latency. + Log.i(TAG, "(" + LOG_LABEL_P50_5G + ": " + timer.elapsed(TimeUnit.MILLISECONDS) + " ms)"); + Assert.assertEquals( + AD_SELECTION_FAILURE_MESSAGE, + createExpectedWinningUri(BUYER, "GENERIC_CA_1", NUMBER_ADS_PER_CA_MEDIUM), + outcome.getRenderUri()); + } + + @Test + public void selectAds_p50_4GPlus() throws Exception { + mMockWebServerRule.createMockWebServer(); + mMockWebServerRule.startCreatedMockWebServer( + MockWebServerDispatcherFactory.create4GPlusp50LatencyDispatcher( + mMockWebServerRule)); + mCustomAudienceSetupRule.populateCustomAudiences( + NUMBER_OF_CUSTOM_AUDIENCES_MEDIUM, NUMBER_ADS_PER_CA_MEDIUM); + + Stopwatch timer = Stopwatch.createStarted(mTicker); + AdSelectionOutcome outcome = + mAdSelectionClient + .selectAds(createAdSelectionConfig()) + .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS); + + timer.stop(); + + // TODO(b/250851601): Currently we use logcat latency collector. Consider replacing this + // with a perfetto trace collector because it won't be affected by write to logcat latency. + Log.i( + TAG, + "(" + LOG_LABEL_P50_4GPLUS + ": " + timer.elapsed(TimeUnit.MILLISECONDS) + " ms)"); + Assert.assertEquals( + AD_SELECTION_FAILURE_MESSAGE, + createExpectedWinningUri(BUYER, "GENERIC_CA_1", NUMBER_ADS_PER_CA_MEDIUM), + outcome.getRenderUri()); + } + + @Test + public void selectAds_p50_4G() throws Exception { + mMockWebServerRule.createMockWebServer(); + mMockWebServerRule.startCreatedMockWebServer( + MockWebServerDispatcherFactory.create4Gp50LatencyDispatcher(mMockWebServerRule)); + mCustomAudienceSetupRule.populateCustomAudiences( + NUMBER_OF_CUSTOM_AUDIENCES_MEDIUM, NUMBER_ADS_PER_CA_MEDIUM); + + Stopwatch timer = Stopwatch.createStarted(mTicker); + AdSelectionOutcome outcome = + mAdSelectionClient + .selectAds(createAdSelectionConfig()) + .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS); + + timer.stop(); + + // TODO(b/250851601): Currently we use logcat latency collector. Consider replacing this + // with a perfetto trace collector because it won't be affected by write to logcat latency. + Log.i(TAG, "(" + LOG_LABEL_P50_4G + ": " + timer.elapsed(TimeUnit.MILLISECONDS) + " ms)"); + Assert.assertEquals( + AD_SELECTION_FAILURE_MESSAGE, + createExpectedWinningUri(BUYER, "GENERIC_CA_1", NUMBER_ADS_PER_CA_MEDIUM), + outcome.getRenderUri()); + } + + @Test + public void selectAds_p90_5G() throws Exception { + mMockWebServerRule.createMockWebServer(); + mMockWebServerRule.startCreatedMockWebServer( + MockWebServerDispatcherFactory.create5Gp90LatencyDispatcher(mMockWebServerRule)); + mCustomAudienceSetupRule.populateCustomAudiences( + NUMBER_OF_CUSTOM_AUDIENCES_LARGE, NUMBER_ADS_PER_CA_LARGE); + + Stopwatch timer = Stopwatch.createStarted(mTicker); + AdSelectionOutcome outcome = + mAdSelectionClient + .selectAds(createAdSelectionConfig()) + .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS); + + timer.stop(); + + // TODO(b/250851601): Currently we use logcat latency collector. Consider replacing this + // with a perfetto trace collector because it won't be affected by write to logcat latency. + Log.i(TAG, "(" + LOG_LABEL_P90_5G + ": " + timer.elapsed(TimeUnit.MILLISECONDS) + " ms)"); + Assert.assertEquals( + AD_SELECTION_FAILURE_MESSAGE, + createExpectedWinningUri(BUYER, "GENERIC_CA_1", NUMBER_ADS_PER_CA_LARGE), + outcome.getRenderUri()); + } + + @Test + public void selectAds_p90_4G() throws Exception { + mMockWebServerRule.createMockWebServer(); + mMockWebServerRule.startCreatedMockWebServer( + MockWebServerDispatcherFactory.create4Gp90LatencyDispatcher(mMockWebServerRule)); + mCustomAudienceSetupRule.populateCustomAudiences( + NUMBER_OF_CUSTOM_AUDIENCES_LARGE, NUMBER_ADS_PER_CA_LARGE); + Stopwatch timer = Stopwatch.createStarted(mTicker); + + AdSelectionOutcome outcome = + mAdSelectionClient + .selectAds(createAdSelectionConfig()) + .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS); + + timer.stop(); + // TODO(b/250851601): Currently we use logcat latency collector. Consider replacing this + // with a perfetto trace collector because it won't be affected by write to logcat latency. + Log.i(TAG, "(" + LOG_LABEL_P90_4G + ": " + timer.elapsed(TimeUnit.MILLISECONDS) + " ms)"); + Assert.assertEquals( + AD_SELECTION_FAILURE_MESSAGE, + createExpectedWinningUri(BUYER, "GENERIC_CA_1", NUMBER_ADS_PER_CA_LARGE), + outcome.getRenderUri()); + } + + @Test + public void selectAds_p90_4GPlus() throws Exception { + mMockWebServerRule.createMockWebServer(); + mMockWebServerRule.startCreatedMockWebServer( + MockWebServerDispatcherFactory.create4GPlusp90LatencyDispatcher( + mMockWebServerRule)); + mCustomAudienceSetupRule.populateCustomAudiences( + NUMBER_OF_CUSTOM_AUDIENCES_LARGE, NUMBER_ADS_PER_CA_LARGE); + + Stopwatch timer = Stopwatch.createStarted(mTicker); + AdSelectionOutcome outcome = + mAdSelectionClient + .selectAds(createAdSelectionConfig()) + .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS); + + timer.stop(); + // TODO(b/250851601): Currently we use logcat latency collector. Consider replacing this + // with a perfetto trace collector because it won't be affected by write to logcat latency. + Log.i( + TAG, + "(" + LOG_LABEL_P90_4GPLUS + ": " + timer.elapsed(TimeUnit.MILLISECONDS) + " ms)"); + Assert.assertEquals( + AD_SELECTION_FAILURE_MESSAGE, + createExpectedWinningUri(BUYER, "GENERIC_CA_1", NUMBER_ADS_PER_CA_LARGE), + outcome.getRenderUri()); + } + + private static Uri getUri(String name, String path) { + return Uri.parse("https://" + name + path); + } + + private Uri createExpectedWinningUri( + AdTechIdentifier buyer, String customAudienceName, int adNumber) { + return getUri(buyer.toString(), "/adverts/123/" + customAudienceName + "/ad" + adNumber); + } + + private AdSelectionConfig createAdSelectionConfig() { + return new AdSelectionConfig.Builder() + .setSeller(SELLER) + .setDecisionLogicUri( + mMockWebServerRule.uriForPath( + MockWebServerDispatcherFactory.getDecisionLogicPath())) + // TODO(b/244530379) Make compatible with multiple buyers + .setCustomAudienceBuyers(CUSTOM_AUDIENCE_BUYERS) + .setAdSelectionSignals(AD_SELECTION_SIGNALS) + .setSellerSignals(SELLER_SIGNALS) + .setPerBuyerSignals(PER_BUYER_SIGNALS) + .setTrustedScoringSignalsUri( + mMockWebServerRule.uriForPath( + MockWebServerDispatcherFactory.getTrustedScoringSignalPath())) + .build(); + } +} diff --git a/adservices/tests/perf/src/android/adservices/test/scenario/adservices/utils/CustomAudienceSetupRule.java b/adservices/tests/perf/src/android/adservices/test/scenario/adservices/utils/CustomAudienceSetupRule.java new file mode 100644 index 0000000000..a9623520f1 --- /dev/null +++ b/adservices/tests/perf/src/android/adservices/test/scenario/adservices/utils/CustomAudienceSetupRule.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.adservices.test.scenario.adservices.utils; + +import android.adservices.clients.customaudience.AdvertisingCustomAudienceClient; +import android.adservices.common.AdData; +import android.adservices.common.AdSelectionSignals; +import android.adservices.common.AdTechIdentifier; +import android.adservices.customaudience.CustomAudience; +import android.adservices.customaudience.TrustedBiddingData; +import android.net.Uri; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class CustomAudienceSetupRule implements TestRule { + + private static final AdTechIdentifier BUYER = AdTechIdentifier.fromString("localhost"); + private static final String AD_URI_PREFIX = "/adverts/123/"; + private static final int DELAY_TO_AVOID_THROTTLE_MS = 1001; + private static final int API_RESPONSE_TIMEOUT_SECONDS = 100; + private static final Duration CUSTOM_AUDIENCE_EXPIRE_IN = Duration.ofDays(1); + private static final Instant VALID_ACTIVATION_TIME = Instant.now(); + private static final Instant VALID_EXPIRATION_TIME = + VALID_ACTIVATION_TIME.plus(CUSTOM_AUDIENCE_EXPIRE_IN); + private static final AdSelectionSignals VALID_USER_BIDDING_SIGNALS = + AdSelectionSignals.fromString("{'valid': 'yep', 'opaque': 'definitely'}"); + private final List<CustomAudience> mCustomAudiences; + private final AdvertisingCustomAudienceClient mAdvertisingCustomAudienceClient; + private final android.adservices.test.scenario.adservices.utils.MockWebServerRule + mMockWebServerRule; + + public CustomAudienceSetupRule( + AdvertisingCustomAudienceClient advertisingCustomAudienceClient, + MockWebServerRule mockWebServerRule) { + mAdvertisingCustomAudienceClient = advertisingCustomAudienceClient; + mMockWebServerRule = mockWebServerRule; + mCustomAudiences = new ArrayList<>(); + } + + private static Uri getUri(String name, String path) { + return Uri.parse("https://" + name + path); + } + + public void populateCustomAudiences( + int numberOfCustomAudiences, int numberOfAdsPerCustomAudiences) throws Exception { + List<Double> bidsForBuyer = new ArrayList<>(); + for (int i = 1; i <= numberOfAdsPerCustomAudiences; i++) { + bidsForBuyer.add(i + 0.1); + } + // Create multiple generic custom audience entries + for (int i = 1; i <= numberOfCustomAudiences; i++) { + CustomAudience customAudience = + createCustomAudience(BUYER, "GENERIC_CA_" + i, bidsForBuyer); + mCustomAudiences.add(customAudience); + } + + for (CustomAudience ca : mCustomAudiences) { + addDelayToAvoidThrottle(); + mAdvertisingCustomAudienceClient + .joinCustomAudience(ca) + .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } + } + + @Override + public Statement apply(Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + try { + base.evaluate(); + } finally { + if (mCustomAudiences != null) { + for (CustomAudience ca : mCustomAudiences) { + addDelayToAvoidThrottle(DELAY_TO_AVOID_THROTTLE_MS); + mAdvertisingCustomAudienceClient + .leaveCustomAudience(ca.getBuyer(), ca.getName()) + .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } + mCustomAudiences.clear(); + } + } + } + }; + } + + private CustomAudience createCustomAudience( + final AdTechIdentifier buyer, + String name, + List<Double> bids, + Instant activationTime, + Instant expirationTime) { + // Generate ads for with bids provided + List<AdData> ads = new ArrayList<>(); + + // Create ads with the custom audience name and bid number as the ad URI + // Add the bid value to the metadata + for (int i = 0; i < bids.size(); i++) { + ads.add( + new AdData.Builder() + .setRenderUri( + getUri( + buyer.toString(), + AD_URI_PREFIX + name + "/ad" + (i + 1))) + .setMetadata("{\"result\":" + bids.get(i) + "}") + .build()); + } + + return new CustomAudience.Builder() + .setBuyer(buyer) + .setName(name) + .setActivationTime(activationTime) + .setExpirationTime(expirationTime) + .setDailyUpdateUri(mMockWebServerRule.uriForPath("/update")) + .setUserBiddingSignals(VALID_USER_BIDDING_SIGNALS) + .setTrustedBiddingData( + getValidTrustedBiddingDataByBuyer( + mMockWebServerRule.uriForPath( + MockWebServerDispatcherFactory + .getTrustedBiddingSignalsPath()))) + .setBiddingLogicUri( + mMockWebServerRule.uriForPath( + MockWebServerDispatcherFactory.getBiddingLogicUriPath())) + .setAds(ads) + .build(); + } + + private CustomAudience createCustomAudience( + final AdTechIdentifier buyer, String name, List<Double> bids) { + return createCustomAudience( + buyer, name, bids, VALID_ACTIVATION_TIME, VALID_EXPIRATION_TIME); + } + + private TrustedBiddingData getValidTrustedBiddingDataByBuyer(Uri validTrustedBiddingUri) { + return new TrustedBiddingData.Builder() + .setTrustedBiddingKeys(MockWebServerDispatcherFactory.getValidTrustedBiddingKeys()) + .setTrustedBiddingUri(validTrustedBiddingUri) + .build(); + } + + private void addDelayToAvoidThrottle() throws InterruptedException { + addDelayToAvoidThrottle(DELAY_TO_AVOID_THROTTLE_MS); + } + + private void addDelayToAvoidThrottle(int delayValueMs) throws InterruptedException { + if (delayValueMs > 0) { + Thread.sleep(delayValueMs); + } + } +} diff --git a/adservices/tests/perf/src/android/adservices/test/scenario/adservices/utils/MockWebServerDispatcherFactory.java b/adservices/tests/perf/src/android/adservices/test/scenario/adservices/utils/MockWebServerDispatcherFactory.java new file mode 100644 index 0000000000..14005df1b3 --- /dev/null +++ b/adservices/tests/perf/src/android/adservices/test/scenario/adservices/utils/MockWebServerDispatcherFactory.java @@ -0,0 +1,264 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.adservices.test.scenario.adservices.utils; + +import android.adservices.common.AdSelectionSignals; + +import com.google.common.collect.ImmutableList; +import com.google.mockwebserver.Dispatcher; +import com.google.mockwebserver.MockResponse; +import com.google.mockwebserver.RecordedRequest; + +import java.util.ArrayList; +import java.util.Arrays; + +/** Setup the dispatcher for mock web server. */ +public final class MockWebServerDispatcherFactory { + + public static final String DECISION_LOGIC_PATH = "/seller/decision/simple_logic_with_delay"; + public static final String TRUSTED_SCORING_SIGNAL_PATH = + "/trusted/scoringsignals/simple_with_delay"; + public static final String TRUSTED_BIDDING_SIGNALS_PATH = + "/trusted/biddingsignals/simple_with_delay"; + public static final ArrayList<String> VALID_TRUSTED_BIDDING_KEYS = + new ArrayList<>(Arrays.asList("example", "valid", "list", "of", "keys")); + // Estimated based on + // https://docs.google.com/spreadsheets/d/1EP_cwBbwYI-NMro0Qq5uif1krwjIQhjK8fjOu15j7hQ/edit?usp=sharing&resourcekey=0-A67kzEnAKKz1k7qpshSedg + public static final int SCORING_JS_EXECUTION_TIME_p50_MS = 40; + public static final int BIDDING_JS_EXECUTION_TIME_p50_MS = 40; + public static final int SCORING_JS_EXECUTION_TIME_p90_MS = 70; + public static final int BIDDING_JS_EXECUTION_TIME_p90_MS = 70; + public static final int DECISION_LOGIC_FETCH_DELAY_5G_p50_MS = 22; + public static final int DECISION_LOGIC_FETCH_DELAY_5G_p90_MS = 23; + public static final int DECISION_LOGIC_FETCH_DELAY_4GPLUS_p50_MS = 56; + public static final int DECISION_LOGIC_FETCH_DELAY_4GPLUS_p90_MS = 57; + public static final int DECISION_LOGIC_FETCH_DELAY_4G_p50_MS = 114; + public static final int DECISION_LOGIC_FETCH_DELAY_4G_p90_MS = 116; + public static final int BIDDING_LOGIC_FETCH_DELAY_5G_p50_MS = 23; + public static final int BIDDING_LOGIC_FETCH_DELAY_5G_p90_MS = 25; + public static final int BIDDING_LOGIC_FETCH_DELAY_4GPLUS_p50_MS = 57; + public static final int BIDDING_LOGIC_FETCH_DELAY_4GPLUS_p90_MS = 62; + public static final int BIDDING_LOGIC_FETCH_DELAY_4G_p50_MS = 116; + public static final int BIDDING_LOGIC_FETCH_DELAY_4G_p90_MS = 128; + public static final int SCORING_SIGNALS_FETCH_DELAY_5G_p50_MS = 21; + public static final int SCORING_SIGNALS_FETCH_DELAY_5G_p90_MS = 22; + public static final int SCORING_SIGNALS_FETCH_DELAY_4GPLUS_p50_MS = 51; + public static final int SCORING_SIGNALS_FETCH_DELAY_4GPLUS_p90_MS = 52; + public static final int SCORING_SIGNALS_FETCH_DELAY_4G_p50_MS = 101; + public static final int SCORING_SIGNALS_FETCH_DELAY_4G_p90_MS = 104; + public static final int BIDDING_SIGNALS_FETCH_DELAY_5G_p50_MS = 22; + public static final int BIDDING_SIGNALS_FETCH_DELAY_5G_p90_MS = 47; + public static final int BIDDING_SIGNALS_FETCH_DELAY_4GPLUS_p50_MS = 53; + public static final int BIDDING_SIGNALS_FETCH_DELAY_4GPLUS_p90_MS = 123; + public static final int BIDDING_SIGNALS_FETCH_DELAY_4G_p50_MS = 105; + public static final int BIDDING_SIGNALS_FETCH_DELAY_4G_p90_MS = 275; + private static final String BUYER_REPORTING_PATH = "/reporting/buyer"; + private static final String SELLER_REPORTING_PATH = "/reporting/seller"; + private static final String BUYER_BIDDING_LOGIC_URI_PATH = + "/buyer/bidding/simple_logic_with_delay"; + private static final String DEFAULT_DECISION_LOGIC_JS_WITH_EXECUTION_TIME_FORMAT = + "function scoreAd(ad, bid, auction_config, seller_signals," + + " trusted_scoring_signals, contextual_signal, user_signal," + + " custom_audience_signal) { \n" + + " const start = Date.now(); let now = start; while (now-start < %d) " + + "{now=Date.now();}\n" + + " return {'status': 0, 'score': bid };\n" + + "}\n" + + "function reportResult(ad_selection_config, render_uri, bid," + + " contextual_signals) { \n" + + " return {'status': 0, 'results': {'signals_for_buyer':" + + " '{\"signals_for_buyer\":1}', 'reporting_uri': '%s" + + "' } };\n" + + "}"; + private static final String DEFAULT_BIDDING_LOGIC_JS_WITH_EXECUTION_TIME_FORMAT = + "function generateBid(ad, auction_signals, per_buyer_signals," + + " trusted_bidding_signals, contextual_signals," + + " custom_audience_signals) { \n" + + " const start = Date.now(); let now = start; while (now-start < %d) " + + "{now=Date.now();}\n" + + " return {'status': 0, 'ad': ad, 'bid': ad.metadata.result };\n" + + "}\n" + + "function reportWin(ad_selection_signals, per_buyer_signals," + + " signals_for_buyer, contextual_signals, custom_audience_signals) { \n" + + " return {'status': 0, 'results': {'reporting_uri': '%s" + + "' } };\n" + + "}"; + private static final AdSelectionSignals TRUSTED_SCORING_SIGNALS = + AdSelectionSignals.fromString( + "{\n" + + "\t\"render_uri_1\": \"signals_for_1\",\n" + + "\t\"render_uri_2\": \"signals_for_2\"\n" + + "}"); + private static final AdSelectionSignals TRUSTED_BIDDING_SIGNALS = + AdSelectionSignals.fromString( + "{\n" + + "\t\"example\": \"example\",\n" + + "\t\"valid\": \"Also valid\",\n" + + "\t\"list\": \"list\",\n" + + "\t\"of\": \"of\",\n" + + "\t\"keys\": \"trusted bidding signal Values\"\n" + + "}"); + + public static Dispatcher create5Gp50LatencyDispatcher(MockWebServerRule mockWebServerRule) { + return create( + DECISION_LOGIC_FETCH_DELAY_5G_p50_MS, + BIDDING_LOGIC_FETCH_DELAY_5G_p50_MS, + SCORING_SIGNALS_FETCH_DELAY_5G_p50_MS, + BIDDING_SIGNALS_FETCH_DELAY_5G_p50_MS, + BIDDING_JS_EXECUTION_TIME_p50_MS, + SCORING_JS_EXECUTION_TIME_p50_MS, + mockWebServerRule); + } + + public static Dispatcher create5Gp90LatencyDispatcher(MockWebServerRule mockWebServerRule) { + return create( + DECISION_LOGIC_FETCH_DELAY_5G_p90_MS, + BIDDING_LOGIC_FETCH_DELAY_5G_p90_MS, + SCORING_SIGNALS_FETCH_DELAY_5G_p90_MS, + BIDDING_SIGNALS_FETCH_DELAY_5G_p90_MS, + BIDDING_JS_EXECUTION_TIME_p90_MS, + SCORING_JS_EXECUTION_TIME_p90_MS, + mockWebServerRule); + } + + public static Dispatcher create4GPlusp50LatencyDispatcher(MockWebServerRule mockWebServerRule) { + return create( + DECISION_LOGIC_FETCH_DELAY_4GPLUS_p50_MS, + BIDDING_LOGIC_FETCH_DELAY_4GPLUS_p50_MS, + SCORING_SIGNALS_FETCH_DELAY_4GPLUS_p50_MS, + BIDDING_SIGNALS_FETCH_DELAY_4GPLUS_p50_MS, + BIDDING_JS_EXECUTION_TIME_p50_MS, + SCORING_JS_EXECUTION_TIME_p50_MS, + mockWebServerRule); + } + + public static Dispatcher create4GPlusp90LatencyDispatcher(MockWebServerRule mockWebServerRule) { + return create( + DECISION_LOGIC_FETCH_DELAY_4GPLUS_p90_MS, + BIDDING_LOGIC_FETCH_DELAY_4GPLUS_p90_MS, + SCORING_SIGNALS_FETCH_DELAY_4GPLUS_p90_MS, + BIDDING_SIGNALS_FETCH_DELAY_4GPLUS_p90_MS, + BIDDING_JS_EXECUTION_TIME_p90_MS, + SCORING_JS_EXECUTION_TIME_p90_MS, + mockWebServerRule); + } + + public static Dispatcher create4Gp50LatencyDispatcher(MockWebServerRule mockWebServerRule) { + return create( + DECISION_LOGIC_FETCH_DELAY_4G_p50_MS, + BIDDING_LOGIC_FETCH_DELAY_4G_p50_MS, + SCORING_SIGNALS_FETCH_DELAY_4G_p50_MS, + BIDDING_SIGNALS_FETCH_DELAY_4G_p50_MS, + BIDDING_JS_EXECUTION_TIME_p50_MS, + SCORING_JS_EXECUTION_TIME_p50_MS, + mockWebServerRule); + } + + public static Dispatcher create4Gp90LatencyDispatcher(MockWebServerRule mockWebServerRule) { + return create( + DECISION_LOGIC_FETCH_DELAY_4G_p90_MS, + BIDDING_LOGIC_FETCH_DELAY_4G_p90_MS, + SCORING_SIGNALS_FETCH_DELAY_4G_p90_MS, + BIDDING_SIGNALS_FETCH_DELAY_4G_p90_MS, + BIDDING_JS_EXECUTION_TIME_p90_MS, + SCORING_JS_EXECUTION_TIME_p90_MS, + mockWebServerRule); + } + + private static Dispatcher create( + int decisionLogicFetchDelayMs, + int biddingLogicFetchDelayMs, + int scoringSignalFetchDelayMs, + int biddingSignalFetchDelayMs, + int biddingLogicExecutionRunMs, + int scoringLogicExecutionRunMs, + MockWebServerRule mockWebServerRule) { + + return new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest request) { + if (DECISION_LOGIC_PATH.equals(request.getPath())) { + return new MockResponse() + .setBodyDelayTimeMs(decisionLogicFetchDelayMs) + .setBody( + getDecisionLogicJS( + scoringLogicExecutionRunMs, + mockWebServerRule + .uriForPath(SELLER_REPORTING_PATH) + .toString())); + } else if (BUYER_BIDDING_LOGIC_URI_PATH.equals(request.getPath())) { + return new MockResponse() + .setBodyDelayTimeMs(biddingLogicFetchDelayMs) + .setBody( + getBiddingLogicJS( + biddingLogicExecutionRunMs, + mockWebServerRule + .uriForPath(BUYER_REPORTING_PATH) + .toString())); + } else if (BUYER_REPORTING_PATH.equals(request.getPath()) + || SELLER_REPORTING_PATH.equals(request.getPath())) { + return new MockResponse().setBody(""); + } else if (request.getPath().startsWith(TRUSTED_SCORING_SIGNAL_PATH)) { + return new MockResponse() + .setBodyDelayTimeMs(scoringSignalFetchDelayMs) + .setBody(TRUSTED_SCORING_SIGNALS.toString()); + } else if (request.getPath().startsWith(TRUSTED_BIDDING_SIGNALS_PATH)) { + return new MockResponse() + .setBodyDelayTimeMs(biddingSignalFetchDelayMs) + .setBody(TRUSTED_BIDDING_SIGNALS.toString()); + } + return new MockResponse().setResponseCode(404); + } + }; + } + + public static String getBiddingLogicUriPath() { + return BUYER_BIDDING_LOGIC_URI_PATH; + } + + public static String getDecisionLogicPath() { + return DECISION_LOGIC_PATH; + } + + public static String getTrustedScoringSignalPath() { + return TRUSTED_SCORING_SIGNAL_PATH; + } + + public static ImmutableList<String> getValidTrustedBiddingKeys() { + return ImmutableList.copyOf(VALID_TRUSTED_BIDDING_KEYS); + } + + public static String getTrustedBiddingSignalsPath() { + return TRUSTED_BIDDING_SIGNALS_PATH; + } + + private static String getDecisionLogicJS( + int scoringLogicExecutionRunMs, String sellerReportingUri) { + return String.format( + DEFAULT_DECISION_LOGIC_JS_WITH_EXECUTION_TIME_FORMAT, + scoringLogicExecutionRunMs, + sellerReportingUri); + } + + private static String getBiddingLogicJS( + int biddingLogicExecutionRunMs, String buyerReportingUri) { + return String.format( + DEFAULT_BIDDING_LOGIC_JS_WITH_EXECUTION_TIME_FORMAT, + biddingLogicExecutionRunMs, + buyerReportingUri); + } +} diff --git a/adservices/tests/perf/src/android/adservices/test/scenario/adservices/utils/MockWebServerRule.java b/adservices/tests/perf/src/android/adservices/test/scenario/adservices/utils/MockWebServerRule.java new file mode 100644 index 0000000000..ecc447977f --- /dev/null +++ b/adservices/tests/perf/src/android/adservices/test/scenario/adservices/utils/MockWebServerRule.java @@ -0,0 +1,289 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.adservices.test.scenario.adservices.utils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import android.content.Context; +import android.net.Uri; + +import com.google.mockwebserver.Dispatcher; +import com.google.mockwebserver.MockResponse; +import com.google.mockwebserver.MockWebServer; +import com.google.mockwebserver.RecordedRequest; + +import org.junit.Assert; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import java.io.IOException; +import java.io.InputStream; +import java.net.ServerSocket; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; + +/** Instances of this class are not thread safe. */ +public class MockWebServerRule implements TestRule { + private static final int UNINITIALIZED = -1; + private final InputStream mCertificateInputStream; + private final char[] mKeyStorePassword; + private int mPort = UNINITIALIZED; + private MockWebServer mMockWebServer; + + private MockWebServerRule(InputStream inputStream, String keyStorePassword) { + mCertificateInputStream = inputStream; + mKeyStorePassword = keyStorePassword == null ? null : keyStorePassword.toCharArray(); + } + + public static MockWebServerRule forHttp() { + return new MockWebServerRule(null, null); + } + + /** + * Builds an instance of the MockWebServerRule configured for HTTPS traffic. + * + * @param context The app context used to load the PKCS12 key store + * @param assetName The name of the key store under the app assets folder + * @param keyStorePassword The password of the keystore + */ + public static MockWebServerRule forHttps( + Context context, String assetName, String keyStorePassword) { + try { + return new MockWebServerRule(context.getAssets().open(assetName), keyStorePassword); + } catch (IOException ioException) { + throw new RuntimeException("Unable to initialize MockWebServerRule", ioException); + } + } + + /** + * Builds an instance of the MockWebServerRule configured for HTTPS traffic. + * + * @param certificateInputStream An input stream to load the content of a PKCS12 key store + * @param keyStorePassword The password of the keystore + */ + public static MockWebServerRule forHttps( + InputStream certificateInputStream, String keyStorePassword) { + return new MockWebServerRule(certificateInputStream, keyStorePassword); + } + + private boolean useHttps() { + return Objects.nonNull(mCertificateInputStream); + } + + public MockWebServer startMockWebServer(List<MockResponse> responses) throws Exception { + if (mPort == UNINITIALIZED) { + reserveServerListeningPort(); + } + + mMockWebServer = new MockWebServer(); + if (useHttps()) { + mMockWebServer.useHttps(getTestingSslSocketFactory(), false); + } + for (MockResponse response : responses) { + mMockWebServer.enqueue(response); + } + mMockWebServer.play(mPort); + return mMockWebServer; + } + + public MockWebServer startMockWebServer(Function<RecordedRequest, MockResponse> lambda) + throws Exception { + Dispatcher dispatcher = + new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest request) { + return lambda.apply(request); + } + }; + return startMockWebServer(dispatcher); + } + + public MockWebServer startMockWebServer(Dispatcher dispatcher) throws Exception { + if (mPort == UNINITIALIZED) { + reserveServerListeningPort(); + } + + mMockWebServer = new MockWebServer(); + if (useHttps()) { + mMockWebServer.useHttps(getTestingSslSocketFactory(), false); + } + mMockWebServer.setDispatcher(dispatcher); + + mMockWebServer.play(mPort); + return mMockWebServer; + } + + public MockWebServer createMockWebServer() throws Exception { + if (mPort == UNINITIALIZED) { + reserveServerListeningPort(); + } + + mMockWebServer = new MockWebServer(); + if (useHttps()) { + mMockWebServer.useHttps(getTestingSslSocketFactory(), false); + } + return mMockWebServer; + } + + public MockWebServer startCreatedMockWebServer(Dispatcher dispatcher) throws Exception { + if (mMockWebServer == null || mPort == UNINITIALIZED) { + throw new IllegalStateException( + "MockWebServer is not created or the port is not reserved."); + } + mMockWebServer.setDispatcher(dispatcher); + + mMockWebServer.play(mPort); + return mMockWebServer; + } + + /** + * @return the mock web server for this rull and {@code null} if it hasn't been started yet by + * calling {@link #startMockWebServer(List)}. + */ + public MockWebServer getMockWebServer() { + return mMockWebServer; + } + + /** @return the base address the mock web server will be listening to when started. */ + public String getServerBaseAddress() { + return String.format("%s://localhost:%d", useHttps() ? "https" : "http", mPort); + } + + /** + * This method is equivalent to {@link MockWebServer#getUrl(String)} but it can be used before + * you prepare and start the server if you need to prepare responses that will reference the + * same test server. + * + * @return an Uri to use to reach the given {@code @path} on the mock web server. + */ + public Uri uriForPath(String path) { + return Uri.parse( + String.format( + "%s%s%s", getServerBaseAddress(), path.startsWith("/") ? "" : "/", path)); + } + + private void reserveServerListeningPort() throws IOException { + ServerSocket serverSocket = new ServerSocket(38383); + serverSocket.setReuseAddress(true); + mPort = serverSocket.getLocalPort(); + serverSocket.close(); + } + + private SSLSocketFactory getTestingSslSocketFactory() + throws GeneralSecurityException, IOException { + final KeyManagerFactory keyManagerFactory = + KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + keyStore.load(mCertificateInputStream, mKeyStorePassword); + keyManagerFactory.init(keyStore, mKeyStorePassword); + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(keyManagerFactory.getKeyManagers(), null, null); + return sslContext.getSocketFactory(); + } + + /** + * A utility that validates that the mock web server got the expected traffic. + * + * @param mockWebServer server instance used for making requests + * @param expectedRequestCount the number of requests expected to be received by the server + * @param expectedRequests the list of URLs that should have been requested, in case of repeat + * requests the size of expectedRequests list could be less than the expectedRequestCount + * @param requestMatcher A custom matcher that dictates if the request meets the criteria of + * being hit or not. This allows tests to do partial match of URLs in case of params or + * other sub path of URL. + */ + public void verifyMockServerRequests( + final MockWebServer mockWebServer, + final int expectedRequestCount, + final List<String> expectedRequests, + final RequestMatcher<String> requestMatcher) { + + assertEquals( + "Number of expected requests does not match actual request count", + expectedRequestCount, + mockWebServer.getRequestCount()); + + // For parallel executions requests should be checked agnostic of order + final Set<String> actualRequests = new HashSet<>(); + for (int i = 0; i < expectedRequestCount; i++) { + try { + actualRequests.add(mockWebServer.takeRequest().getPath()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + assertFalse( + String.format( + "Expected requests cannot be empty, actual requests <%s>", actualRequests), + expectedRequestCount != 0 && expectedRequests.isEmpty()); + + for (String request : expectedRequests) { + Assert.assertTrue( + String.format( + "Actual requests <%s> do not contain request <%s>", + actualRequests, request), + wasPathRequested(actualRequests, request, requestMatcher)); + } + } + + private boolean wasPathRequested( + final Set<String> actualRequests, + final String request, + final RequestMatcher<String> requestMatcher) { + for (String actualRequest : actualRequests) { + // Passing a custom comparator allows tests to do partial match of URLs in case of + // params or other sub path of URL + if (requestMatcher.wasRequestMade(actualRequest, request)) { + return true; + } + } + return false; + } + + @Override + public Statement apply(Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + reserveServerListeningPort(); + try { + base.evaluate(); + } finally { + if (mMockWebServer != null) { + mMockWebServer.shutdown(); + } + } + } + }; + } + + public interface RequestMatcher<T> { + boolean wasRequestMade(T actualRequest, T expectedRequest); + } +} diff --git a/adservices/tests/perf/src/android/adservices/test/scenario/adservices/utils/MockWebServerRuleFactory.java b/adservices/tests/perf/src/android/adservices/test/scenario/adservices/utils/MockWebServerRuleFactory.java new file mode 100644 index 0000000000..00f3143633 --- /dev/null +++ b/adservices/tests/perf/src/android/adservices/test/scenario/adservices/utils/MockWebServerRuleFactory.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.adservices.test.scenario.adservices.utils; + +import androidx.test.core.app.ApplicationProvider; + +/** Utility class for tests needing to mock web server calls */ +public class MockWebServerRuleFactory { + /** @return A mock {@link MockWebServerRule} initialized to use HTTPS. */ + public static MockWebServerRule createForHttps() { + return MockWebServerRule.forHttps( + ApplicationProvider.getApplicationContext(), + "adservices_test_server.p12", + "adservices_test"); + } + + /** @return A mock {@link MockWebServerRule} initialized to use HTTP cleartext. */ + public static MockWebServerRule createForHttp() { + return MockWebServerRule.forHttp(); + } +} |