Integrate Playwright for Configuration Analyzer HTML report testing
- Add playwright as a test dependency.
- Configure headless Chrome dependency.
- Create PlaywrightTestBase and BlastRadiusInspector to hide browser setup.
- Add BlastRadiusPlaywrightTest with comprehensive E2E tests for report rendering and UI features (search, tabs, details drilldown).
- Update .gitignore to exclude downloaded chrome binaries but allow sha1 files.
Change-Id: I4220aa636f6f23d47cf741a5ee03e5f3bbe46e73
diff --git a/.gitignore b/.gitignore
index 9e585c1..195e571 100644
--- a/.gitignore
+++ b/.gitignore
@@ -83,6 +83,8 @@
third_party/cf_segments.tar.gz
third_party/chrome/*
third_party/chrome/*.tar.gz
+third_party/chrome_headless/*/
+third_party/chrome_headless/*.tar.gz
third_party/classlib
third_party/classlib.tar.gz
third_party/core-lambda-stubs
diff --git a/d8_r8/commonBuildSrc/src/main/kotlin/DependenciesPlugin.kt b/d8_r8/commonBuildSrc/src/main/kotlin/DependenciesPlugin.kt
index c507534..c7857c4 100644
--- a/d8_r8/commonBuildSrc/src/main/kotlin/DependenciesPlugin.kt
+++ b/d8_r8/commonBuildSrc/src/main/kotlin/DependenciesPlugin.kt
@@ -369,7 +369,7 @@
public const val ddmLibVersion = "32.2.0"
public const val errorproneVersion = "2.18.0"
public const val fastUtilVersion = "7.2.1"
- public const val gsonVersion = "2.10.1"
+ public const val gsonVersion = "2.14.0"
public const val guavaVersion = "32.1.2-jre"
public const val javassist = "3.29.2-GA"
public const val junitJupiterVersion = "5.14.3"
@@ -377,9 +377,10 @@
public const val kotlinVersion = "1.9.20"
public const val kotlinMetadataVersion = "2.3.10"
public const val mockito = "2.10.0"
- public const val testRetry = "1.6.4"
- public const val smaliVersion = "3.0.3"
+ public const val playwrightVersion = "1.60.0"
public const val protobufVersion = "4.33.5"
+ public const val smaliVersion = "3.0.3"
+ public const val testRetry = "1.6.4"
public const val zipflingerVersion = "9.0.0"
}
@@ -424,15 +425,18 @@
"org.jetbrains.kotlin:kotlin-reflect:${Versions.kotlinVersion}"
}
public val mockito: String by lazy { "org.mockito:mockito-core:${Versions.mockito}" }
- public val testretry: String by lazy {
- "org.gradle.test-retry-gradle-plugin:${Versions.testRetry}"
+ public val playwright: String by lazy {
+ "com.microsoft.playwright:playwright:${Versions.playwrightVersion}"
+ }
+ public val protobuf: String by lazy {
+ "com.google.protobuf:protobuf-java:${Versions.protobufVersion}"
}
public val smali: String by lazy { "com.android.tools.smali:smali:${Versions.smaliVersion}" }
public val smaliUtil: String by lazy {
"com.android.tools.smali:smali-util:${Versions.smaliVersion}"
}
- public val protobuf: String by lazy {
- "com.google.protobuf:protobuf-java:${Versions.protobufVersion}"
+ public val testretry: String by lazy {
+ "org.gradle.test-retry-gradle-plugin:${Versions.testRetry}"
}
public val zipflinger: String by lazy { "com.android:zipflinger:${Versions.zipflingerVersion}" }
@@ -513,6 +517,7 @@
Paths.get("third_party", "opensource-apps", "chrome").toFile(),
Paths.get("third_party", "opensource-apps", "chrome.tar.gz.sha1").toFile(),
)
+ public val chromeHeadless: ThirdPartyDependency = getThirdPartyChromeHeadless()
public val compilerApi: ThirdPartyDependency =
ThirdPartyDependency(
"compiler-api",
@@ -1093,6 +1098,25 @@
)
}
+private fun getThirdPartyChromeHeadless(): ThirdPartyDependency {
+ val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
+ val subFolder =
+ when {
+ os.isLinux -> "linux"
+ os.isMacOsX -> "mac"
+ else -> {
+ assert(os.isWindows)
+ "windows"
+ }
+ }
+ return ThirdPartyDependency(
+ "chrome_headless",
+ Paths.get("third_party", "chrome_headless", subFolder).toFile(),
+ Paths.get("third_party", "chrome_headless", "$subFolder.tar.gz.sha1").toFile(),
+ testOnly = true,
+ )
+}
+
private fun getJdks(): List<ThirdPartyDependency> {
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
if (os.isLinux || os.isMacOsX) {
diff --git a/d8_r8/test_modules/testbase/build.gradle.kts b/d8_r8/test_modules/testbase/build.gradle.kts
index c85d365..650aad8 100644
--- a/d8_r8/test_modules/testbase/build.gradle.kts
+++ b/d8_r8/test_modules/testbase/build.gradle.kts
@@ -62,9 +62,10 @@
implementation(Deps.javassist)
implementation(Deps.junitJupiter)
implementation(Deps.junitVintageEngine)
- implementation(Deps.kotlinStdLib)
- implementation(Deps.kotlinReflect)
implementation(Deps.kotlinMetadata)
+ implementation(Deps.kotlinReflect)
+ implementation(Deps.kotlinStdLib)
+ implementation(Deps.playwright)
implementation(Deps.ddmLib)
implementation(resolve(ThirdPartyDeps.jasmin, "jasmin-2.4.jar"))
implementation(resolve(ThirdPartyDeps.jdwpTests, "apache-harmony-jdwp-tests-host.jar"))
diff --git a/d8_r8/test_modules/tests_java_8/build.gradle.kts b/d8_r8/test_modules/tests_java_8/build.gradle.kts
index 3040da9..42c44a3 100644
--- a/d8_r8/test_modules/tests_java_8/build.gradle.kts
+++ b/d8_r8/test_modules/tests_java_8/build.gradle.kts
@@ -86,11 +86,11 @@
implementation(project(":resourceshrinker", "resourceshrinkerDepsJar"))
implementation(project(":testbase"))
implementation(project(":testbase", "depsJar"))
-
// For each child project, add its test classes to the test class configuration.
childProjects.values.forEach { childProject ->
partialTestClassesScope(project(childProject.path, "partialTestClasses"))
}
+ implementation(Deps.playwright)
}
fun testDependencies(): FileCollection {
diff --git a/src/main/java/com/android/tools/r8/assistant/postprocessing/ReflectiveOperationJsonParser.java b/src/main/java/com/android/tools/r8/assistant/postprocessing/ReflectiveOperationJsonParser.java
index 4888a0d..093a59c 100644
--- a/src/main/java/com/android/tools/r8/assistant/postprocessing/ReflectiveOperationJsonParser.java
+++ b/src/main/java/com/android/tools/r8/assistant/postprocessing/ReflectiveOperationJsonParser.java
@@ -32,7 +32,7 @@
if (line.trim().isEmpty()) {
continue;
}
- JsonObject event = new JsonParser().parse(line).getAsJsonObject();
+ JsonObject event = JsonParser.parseString(line).getAsJsonObject();
if (event.isEmpty()) {
continue;
}
diff --git a/src/main/java/com/android/tools/r8/dex/ClassesChecksum.java b/src/main/java/com/android/tools/r8/dex/ClassesChecksum.java
index a69e843..d394fa0 100644
--- a/src/main/java/com/android/tools/r8/dex/ClassesChecksum.java
+++ b/src/main/java/com/android/tools/r8/dex/ClassesChecksum.java
@@ -61,7 +61,7 @@
&& dexString.content[2] == PREFIX_CHAR2) {
String str = dexString.toString().substring(3);
try {
- JsonElement result = new JsonParser().parse(str);
+ JsonElement result = JsonParser.parseString(str);
if (result.isJsonObject()) {
append(result.getAsJsonObject());
}
diff --git a/src/main/java/com/android/tools/r8/dex/Marker.java b/src/main/java/com/android/tools/r8/dex/Marker.java
index 40aa9a7..1297330 100644
--- a/src/main/java/com/android/tools/r8/dex/Marker.java
+++ b/src/main/java/com/android/tools/r8/dex/Marker.java
@@ -319,7 +319,7 @@
private static Marker internalParse(Tool tool, String str) {
try {
- JsonElement result = new JsonParser().parse(str);
+ JsonElement result = JsonParser.parseString(str);
if (result.isJsonObject()) {
return new Marker(tool, result.getAsJsonObject());
}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/DesugaredLibrarySpecificationParser.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/DesugaredLibrarySpecificationParser.java
index e62fade..9e5cd84 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/DesugaredLibrarySpecificationParser.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/DesugaredLibrarySpecificationParser.java
@@ -46,8 +46,7 @@
JsonObject jsonConfig;
try {
jsonConfigString = stringResource.getString();
- JsonParser parser = new JsonParser();
- jsonConfig = parser.parse(jsonConfigString).getAsJsonObject();
+ jsonConfig = JsonParser.parseString(jsonConfigString).getAsJsonObject();
} catch (Exception e) {
throw reporter.fatalError(new ExceptionDiagnostic(e, origin));
}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/HumanDesugaredLibrarySpecificationParser.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/HumanDesugaredLibrarySpecificationParser.java
index d993797..b558940 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/HumanDesugaredLibrarySpecificationParser.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/HumanDesugaredLibrarySpecificationParser.java
@@ -160,8 +160,7 @@
String jsonConfigString;
try {
jsonConfigString = stringResource.getString();
- JsonParser parser = new JsonParser();
- jsonConfig = parser.parse(jsonConfigString).getAsJsonObject();
+ jsonConfig = JsonParser.parseString(jsonConfigString).getAsJsonObject();
} catch (Exception e) {
throw reporter.fatalError(new ExceptionDiagnostic(e, origin));
}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/legacyspecification/LegacyDesugaredLibrarySpecificationParser.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/legacyspecification/LegacyDesugaredLibrarySpecificationParser.java
index 8c8e2a8..f9b1acc 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/legacyspecification/LegacyDesugaredLibrarySpecificationParser.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/legacyspecification/LegacyDesugaredLibrarySpecificationParser.java
@@ -15,8 +15,8 @@
import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.ExceptionDiagnostic;
import com.android.tools.r8.utils.Reporter;
-import com.android.tools.r8.utils.internal.SemanticVersion;
import com.android.tools.r8.utils.StringDiagnostic;
+import com.android.tools.r8.utils.internal.SemanticVersion;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
@@ -137,8 +137,7 @@
String jsonConfigString;
try {
jsonConfigString = stringResource.getString();
- JsonParser parser = new JsonParser();
- jsonConfig = parser.parse(jsonConfigString).getAsJsonObject();
+ jsonConfig = JsonParser.parseString(jsonConfigString).getAsJsonObject();
} catch (Exception e) {
throw reporter.fatalError(new ExceptionDiagnostic(e, origin));
}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/MachineDesugaredLibrarySpecificationParser.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/MachineDesugaredLibrarySpecificationParser.java
index a7b9165..fcb9916 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/MachineDesugaredLibrarySpecificationParser.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/machinespecification/MachineDesugaredLibrarySpecificationParser.java
@@ -156,8 +156,7 @@
String jsonConfigString;
try {
jsonConfigString = stringResource.getString();
- JsonParser parser = new JsonParser();
- jsonConfig = parser.parse(jsonConfigString).getAsJsonObject();
+ jsonConfig = JsonParser.parseString(jsonConfigString).getAsJsonObject();
} catch (Exception e) {
throw reporter.fatalError(new ExceptionDiagnostic(e, origin));
}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/specificationconversion/DesugaredLibraryConverter.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/specificationconversion/DesugaredLibraryConverter.java
index d416e45..77b02ca 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/specificationconversion/DesugaredLibraryConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/specificationconversion/DesugaredLibraryConverter.java
@@ -18,10 +18,10 @@
import com.android.tools.r8.ir.desugar.desugaredlibrary.machinespecification.MultiAPILevelMachineDesugaredLibrarySpecification;
import com.android.tools.r8.ir.desugar.desugaredlibrary.machinespecification.MultiAPILevelMachineDesugaredLibrarySpecificationJsonExporter;
import com.android.tools.r8.utils.AndroidApp;
-import com.android.tools.r8.utils.internal.Box;
import com.android.tools.r8.utils.ExceptionDiagnostic;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.ThreadUtils;
+import com.android.tools.r8.utils.internal.Box;
import com.android.tools.r8.utils.timing.Timing;
import com.google.common.collect.ImmutableSet;
import com.google.gson.JsonObject;
@@ -95,8 +95,7 @@
JsonObject jsonConfig;
try {
String jsonConfigString = jsonResource.getString();
- JsonParser parser = new JsonParser();
- jsonConfig = parser.parse(jsonConfigString).getAsJsonObject();
+ jsonConfig = JsonParser.parseString(jsonConfigString).getAsJsonObject();
} catch (Exception e) {
throw options.reporter.fatalError(new ExceptionDiagnostic(e, jsonResource.getOrigin()));
}
diff --git a/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java b/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
index 1d42a9f..4d287be 100644
--- a/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
+++ b/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
@@ -72,7 +72,6 @@
public class ProguardMapReader implements AutoCloseable {
private final LineReader reader;
- private final JsonParser jsonParser = new JsonParser();
private final DiagnosticsHandler diagnosticsHandler;
private final boolean allowEmptyMappedRanges;
private final boolean allowExperimentalMapping;
@@ -890,7 +889,7 @@
while (line.charAt(firstIndex) != '{') {
firstIndex++;
}
- return jsonParser.parse(line.substring(firstIndex)).getAsJsonObject();
+ return JsonParser.parseString(line.substring(firstIndex)).getAsJsonObject();
} catch (com.google.gson.JsonSyntaxException ex) {
// An info message is reported in MappingInformation.
return null;
diff --git a/src/test/java/com/android/tools/r8/MarkersTest.java b/src/test/java/com/android/tools/r8/MarkersTest.java
index 88eff76..a667860 100644
--- a/src/test/java/com/android/tools/r8/MarkersTest.java
+++ b/src/test/java/com/android/tools/r8/MarkersTest.java
@@ -93,8 +93,7 @@
L8.run(builder.build());
Collection<Marker> markers = ExtractMarkerUtils.extractMarkersFromFile(output);
JsonObject jsonObject =
- new JsonParser()
- .parse(
+ JsonParser.parseString(
FileUtils.readTextFile(
libraryDesugaringSpecification.getSpecification(), Charsets.UTF_8))
.getAsJsonObject();
diff --git a/src/test/java/com/android/tools/r8/keepradius/ui/KeepRadiusPlaywrightTest.java b/src/test/java/com/android/tools/r8/keepradius/ui/KeepRadiusPlaywrightTest.java
new file mode 100644
index 0000000..e19a2e5
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepradius/ui/KeepRadiusPlaywrightTest.java
@@ -0,0 +1,73 @@
+// Copyright (c) 2026, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepradius.ui;
+
+import com.android.tools.r8.PlaywrightTestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class KeepRadiusPlaywrightTest extends PlaywrightTestBase {
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withNoneRuntime().build();
+ }
+
+ @Test
+ public void testHtmlReportRendering() throws Exception {
+ testForR8(Backend.DEX)
+ .addProgramClasses(Main.class)
+ .addKeepAllClassesRule()
+ .enableConfigurationAnalysisReport()
+ .compile()
+ .inspectKeepRadiusHtmlReport(
+ page,
+ inspector -> {
+ inspector
+ .assertTitle("R8 Configuration Analyzer")
+ .assertTotalObfuscationNotEmpty()
+ // 2. Assert table contains the keep rule
+ .assertTableContains("-keep class ** { *; }")
+ // 3. Search for "keep"
+ .search("keep")
+ .assertTableContains("-keep class ** { *; }")
+ // Search for "nonexistent"
+ .search("nonexistent")
+ .assertTableContains("No results found.")
+ // Clear search
+ .search("")
+ // 4. Click the "Unused" tab (should be empty for our keep rule)
+ .clickLensTab("Unused")
+ .assertTableContains("No results found.")
+ // Go back to "All"
+ .clickLensTab("All")
+ // 5. Click the rule row to drill down to details
+ .clickRowWithText("-keep class ** { *; }")
+ // Assert details view is visible
+ .assertVisible("#details-view")
+ .assertHidden("#report-view")
+ // Assert Main class is kept in details
+ .assertDetailsClassesContains(Main.class.getTypeName())
+ // Go back
+ .clickDetailsBackToSummary()
+ .assertVisible("#report-view")
+ .assertHidden("#details-view");
+ });
+ }
+
+ static class Main {
+ public static void main(String[] args) {
+ System.out.println("Hello");
+ }
+ }
+}
diff --git a/src/test/testbase/java/com/android/tools/r8/PlaywrightTestBase.java b/src/test/testbase/java/com/android/tools/r8/PlaywrightTestBase.java
new file mode 100644
index 0000000..faea90d
--- /dev/null
+++ b/src/test/testbase/java/com/android/tools/r8/PlaywrightTestBase.java
@@ -0,0 +1,139 @@
+// Copyright (c) 2026, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8;
+
+import com.microsoft.playwright.Browser;
+import com.microsoft.playwright.BrowserType;
+import com.microsoft.playwright.Page;
+import com.microsoft.playwright.Playwright;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.rules.TemporaryFolder;
+
+public class PlaywrightTestBase extends TestBase {
+
+ @Rule public TemporaryFolder tempFolder = new TemporaryFolder();
+
+ private static Playwright playwright;
+ private static Browser browser;
+ protected Page page;
+
+ @BeforeClass
+ public static void setUpBrowser() {
+ Path chromeExecutable = resolveChromeExecutable();
+
+ Map<String, String> env = new HashMap<>();
+ env.put("PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD", "1");
+
+ Playwright.CreateOptions createOptions = new Playwright.CreateOptions().setEnv(env);
+ playwright = Playwright.create(createOptions);
+
+ BrowserType.LaunchOptions launchOptions = new BrowserType.LaunchOptions();
+ if (chromeExecutable == null) {
+ throw new RuntimeException("chrome-headless-shell binary is missing");
+ }
+ launchOptions.setExecutablePath(chromeExecutable);
+ launchOptions.setHeadless(true);
+
+ browser = playwright.chromium().launch(launchOptions);
+ }
+
+ @Before
+ public void setUpPage() {
+ page = browser.newPage();
+ }
+
+ @After
+ public void tearDownPage() {
+ if (page != null) {
+ page.close();
+ }
+ }
+
+ @AfterClass
+ public static void tearDownBrowser() {
+ if (browser != null) {
+ browser.close();
+ }
+ if (playwright != null) {
+ playwright.close();
+ }
+ }
+
+ protected Page loadHtml(String html) throws IOException {
+ Path tempFile = tempFolder.newFile("temp_report.html").toPath();
+ Files.write(tempFile, html.getBytes(StandardCharsets.UTF_8));
+ page.navigate(tempFile.toUri().toString());
+ return page;
+ }
+
+ private static Path resolveChromeExecutable() {
+ Path chromeDir = Paths.get(ToolHelper.THIRD_PARTY_DIR, "chrome_headless");
+ if (!Files.exists(chromeDir)) {
+ return null;
+ }
+
+ boolean isMac = ToolHelper.isMac();
+ boolean isWindows = ToolHelper.isWindows();
+ boolean isLinux = ToolHelper.isLinux();
+
+ if (isMac) {
+ chromeDir = chromeDir.resolve("mac");
+ } else if (isLinux) {
+ chromeDir = chromeDir.resolve("linux");
+ } else if (isWindows) {
+ chromeDir = chromeDir.resolve("windows");
+ }
+
+ if (!Files.exists(chromeDir)) {
+ return null;
+ }
+
+ String[] executables = {
+ "chrome-headless-shell", "chrome", "chromium", "chrome-headless-shell.exe", "chrome.exe"
+ };
+
+ String macArch = "";
+ if (isMac) {
+ String arch = System.getProperty("os.arch").toLowerCase();
+ if (arch.contains("aarch64") || arch.contains("arm64")) {
+ macArch = "mac-arm64";
+ } else {
+ macArch = "mac-x64";
+ }
+ }
+ final String targetMacArch = macArch;
+
+ try {
+ return Files.walk(chromeDir, 3)
+ .filter(
+ p -> {
+ String name = p.getFileName().toString();
+ if (isMac && !p.toString().contains(targetMacArch)) {
+ return false;
+ }
+ for (String exec : executables) {
+ if (name.equalsIgnoreCase(exec) && Files.isExecutable(p)) {
+ return true;
+ }
+ }
+ return false;
+ })
+ .findFirst()
+ .orElse(null);
+ } catch (IOException e) {
+ return null;
+ }
+ }
+}
diff --git a/src/test/testbase/java/com/android/tools/r8/R8CompatTestBuilder.java b/src/test/testbase/java/com/android/tools/r8/R8CompatTestBuilder.java
index b541d2b..98bff3e 100644
--- a/src/test/testbase/java/com/android/tools/r8/R8CompatTestBuilder.java
+++ b/src/test/testbase/java/com/android/tools/r8/R8CompatTestBuilder.java
@@ -8,8 +8,8 @@
import com.android.tools.r8.benchmarks.BenchmarkResults;
import com.android.tools.r8.shaking.ProguardConfigurationRule;
import com.android.tools.r8.utils.AndroidApp;
-import com.android.tools.r8.utils.internal.Box;
import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.internal.Box;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
@@ -63,6 +63,7 @@
resourceShrinkerOutput,
resourceShrinkerOutputForFeatures,
resourceShrinkerLogConsumer,
- buildMetadata != null ? buildMetadata.get() : null);
+ buildMetadata != null ? buildMetadata.get() : null,
+ configurationAnalysisHtmlReport);
}
}
diff --git a/src/test/testbase/java/com/android/tools/r8/R8FullTestBuilder.java b/src/test/testbase/java/com/android/tools/r8/R8FullTestBuilder.java
index 48b5bdd..5063a38 100644
--- a/src/test/testbase/java/com/android/tools/r8/R8FullTestBuilder.java
+++ b/src/test/testbase/java/com/android/tools/r8/R8FullTestBuilder.java
@@ -8,8 +8,8 @@
import com.android.tools.r8.benchmarks.BenchmarkResults;
import com.android.tools.r8.shaking.ProguardConfigurationRule;
import com.android.tools.r8.utils.AndroidApp;
-import com.android.tools.r8.utils.internal.Box;
import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.internal.Box;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
@@ -61,6 +61,7 @@
resourceShrinkerOutput,
resourceShrinkerOutputForFeatures,
resourceShrinkerLogConsumer,
- buildMetadata != null ? buildMetadata.get() : null);
+ buildMetadata != null ? buildMetadata.get() : null,
+ configurationAnalysisHtmlReport);
}
}
diff --git a/src/test/testbase/java/com/android/tools/r8/R8PartialTestBuilder.java b/src/test/testbase/java/com/android/tools/r8/R8PartialTestBuilder.java
index aa3686a..b2ab55e 100644
--- a/src/test/testbase/java/com/android/tools/r8/R8PartialTestBuilder.java
+++ b/src/test/testbase/java/com/android/tools/r8/R8PartialTestBuilder.java
@@ -10,12 +10,12 @@
import com.android.tools.r8.partial.R8PartialCompilationConfiguration;
import com.android.tools.r8.shaking.ProguardConfigurationRule;
import com.android.tools.r8.utils.AndroidApp;
-import com.android.tools.r8.utils.internal.Box;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.codeinspector.EnumUnboxingInspector;
import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
import com.android.tools.r8.utils.codeinspector.RepackagingInspector;
import com.android.tools.r8.utils.codeinspector.VerticallyMergedClassesInspector;
+import com.android.tools.r8.utils.internal.Box;
import com.android.tools.r8.utils.internal.exceptions.Unreachable;
import java.io.IOException;
import java.util.ArrayList;
@@ -157,7 +157,8 @@
resourceShrinkerOutput,
resourceShrinkerOutputForFeatures,
resourceShrinkerLogConsumer,
- buildMetadata != null ? buildMetadata.get() : null);
+ buildMetadata != null ? buildMetadata.get() : null,
+ configurationAnalysisHtmlReport);
}
@Override
diff --git a/src/test/testbase/java/com/android/tools/r8/R8PartialTestCompileResult.java b/src/test/testbase/java/com/android/tools/r8/R8PartialTestCompileResult.java
index 7478fc4..eb6b071 100644
--- a/src/test/testbase/java/com/android/tools/r8/R8PartialTestCompileResult.java
+++ b/src/test/testbase/java/com/android/tools/r8/R8PartialTestCompileResult.java
@@ -9,6 +9,7 @@
import com.android.tools.r8.shaking.CollectingGraphConsumer;
import com.android.tools.r8.shaking.ProguardConfigurationRule;
import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.internal.Box;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
@@ -31,7 +32,8 @@
Path resourceShrinkerOutput,
HashMap<String, Path> resourceShrinkerOutputForFeatures,
TestDebugConsumer resourceShrinkerLogConsumer,
- R8BuildMetadata buildMetadata) {
+ R8BuildMetadata buildMetadata,
+ Box<String> configurationAnalysisHtmlReport) {
super(
state,
outputMode,
@@ -47,7 +49,8 @@
resourceShrinkerOutput,
resourceShrinkerOutputForFeatures,
resourceShrinkerLogConsumer,
- buildMetadata);
+ buildMetadata,
+ configurationAnalysisHtmlReport);
}
@Override
diff --git a/src/test/testbase/java/com/android/tools/r8/R8TestBuilder.java b/src/test/testbase/java/com/android/tools/r8/R8TestBuilder.java
index 3aeed0b..d3b4b5b 100644
--- a/src/test/testbase/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/testbase/java/com/android/tools/r8/R8TestBuilder.java
@@ -4,6 +4,7 @@
package com.android.tools.r8;
import static com.android.tools.r8.R8TestBuilder.KeepAnnotationLibrary.ANDROIDX;
+import static org.junit.Assert.assertNull;
import com.android.tools.r8.DexIndexedConsumer.ArchiveConsumer;
import com.android.tools.r8.R8Command.Builder;
@@ -114,6 +115,7 @@
Box<R8BuildMetadata> buildMetadata;
private boolean androidPlatformBuild = false;
private Box<StringConsumer> proguardMapConsumer = null;
+ Box<String> configurationAnalysisHtmlReport = null;
@Override
public boolean isR8TestBuilder() {
@@ -185,6 +187,22 @@
if (buildMetadata != null) {
builder.setBuildMetadataConsumer(buildMetadata::set);
}
+ if (configurationAnalysisHtmlReport != null) {
+ builder.setConfigurationAnalysisHtmlReportConsumer(
+ new StringConsumer() {
+ private final StringBuilder sb = new StringBuilder();
+
+ @Override
+ public void accept(String string, DiagnosticsHandler handler) {
+ sb.append(string);
+ }
+
+ @Override
+ public void finished(DiagnosticsHandler handler) {
+ configurationAnalysisHtmlReport.set(sb.toString());
+ }
+ });
+ }
StringBuilder pgConfOutput = wrapProguardConfigConsumer(builder);
CR compileResult =
internalCompileR8(
@@ -853,6 +871,12 @@
return self();
}
+ public T enableConfigurationAnalysisReport() {
+ assertNull(this.configurationAnalysisHtmlReport);
+ this.configurationAnalysisHtmlReport = new Box<>();
+ return self();
+ }
+
public T setKeptGraphConsumer(GraphConsumer graphConsumer) {
assert this.graphConsumer == null;
builder.setKeptGraphConsumer(graphConsumer);
diff --git a/src/test/testbase/java/com/android/tools/r8/R8TestCompileResult.java b/src/test/testbase/java/com/android/tools/r8/R8TestCompileResult.java
index 8bd3dc3..75dd1ec 100644
--- a/src/test/testbase/java/com/android/tools/r8/R8TestCompileResult.java
+++ b/src/test/testbase/java/com/android/tools/r8/R8TestCompileResult.java
@@ -9,6 +9,7 @@
import com.android.tools.r8.shaking.CollectingGraphConsumer;
import com.android.tools.r8.shaking.ProguardConfigurationRule;
import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.internal.Box;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
@@ -30,7 +31,8 @@
Path resourceShrinkerOutput,
HashMap<String, Path> resourceShrinkerOutputForFeatures,
TestDebugConsumer resourceShrinkerLogConsumer,
- R8BuildMetadata buildMetadata) {
+ R8BuildMetadata buildMetadata,
+ Box<String> configurationAnalysisHtmlReport) {
super(
state,
outputMode,
@@ -46,7 +48,8 @@
resourceShrinkerOutput,
resourceShrinkerOutputForFeatures,
resourceShrinkerLogConsumer,
- buildMetadata);
+ buildMetadata,
+ configurationAnalysisHtmlReport);
}
@Override
diff --git a/src/test/testbase/java/com/android/tools/r8/R8TestCompileResultBase.java b/src/test/testbase/java/com/android/tools/r8/R8TestCompileResultBase.java
index 673eb16..6075512 100644
--- a/src/test/testbase/java/com/android/tools/r8/R8TestCompileResultBase.java
+++ b/src/test/testbase/java/com/android/tools/r8/R8TestCompileResultBase.java
@@ -7,6 +7,7 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
import com.android.tools.r8.DexSegments.SegmentInfo;
import com.android.tools.r8.TestRuntime.DexRuntime;
@@ -22,6 +23,7 @@
import com.android.tools.r8.graph.DexEncodedMethod;
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.keepradius.ui.KeepRadiusInspector;
import com.android.tools.r8.metadata.R8BuildMetadata;
import com.android.tools.r8.profile.art.model.ExternalArtProfile;
import com.android.tools.r8.profile.art.utils.ArtProfileInspector;
@@ -35,9 +37,11 @@
import com.android.tools.r8.utils.codeinspector.CodeInspector;
import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
import com.android.tools.r8.utils.graphinspector.GraphInspector;
+import com.android.tools.r8.utils.internal.Box;
import com.android.tools.r8.utils.internal.FileUtils;
import com.android.tools.r8.utils.internal.ThrowingBiConsumer;
import com.android.tools.r8.utils.internal.ThrowingConsumer;
+import com.microsoft.playwright.Page;
import it.unimi.dsi.fastutil.ints.Int2ReferenceLinkedOpenHashMap;
import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
import java.io.IOException;
@@ -63,6 +67,7 @@
private final Map<String, Path> resourceShrinkerOutputForFeatures;
private final TestDebugConsumer resourceShrinkerLogConsumer;
private final R8BuildMetadata buildMetadata;
+ private final Box<String> configurationAnalysisHtmlReport;
R8TestCompileResultBase(
TestState state,
@@ -79,7 +84,8 @@
Path resourceShrinkerOutput,
HashMap<String, Path> resourceShrinkerOutputForFeatures,
TestDebugConsumer resourceShrinkerLogConsumer,
- R8BuildMetadata buildMetadata) {
+ R8BuildMetadata buildMetadata,
+ Box<String> configurationAnalysisHtmlReport) {
super(state, app, minApiLevel, outputMode, libraryDesugaringTestConfiguration);
this.proguardConfiguration = proguardConfiguration;
this.syntheticProguardRules = syntheticProguardRules;
@@ -91,6 +97,7 @@
this.resourceShrinkerOutputForFeatures = resourceShrinkerOutputForFeatures;
this.resourceShrinkerLogConsumer = resourceShrinkerLogConsumer;
this.buildMetadata = buildMetadata;
+ this.configurationAnalysisHtmlReport = configurationAnalysisHtmlReport;
}
public CR benchmarkResourceSize(BenchmarkResults results) throws IOException {
@@ -252,6 +259,21 @@
return new GraphInspector(graphConsumer, inspector());
}
+ public KeepRadiusInspector keepRadiusInspector(Page page) throws IOException {
+ assertNotNull(configurationAnalysisHtmlReport);
+ assertTrue(configurationAnalysisHtmlReport.isSet());
+ Path tempFile = state.getNewTempFolder().resolve("report.html");
+ FileUtils.writeTextFile(tempFile, configurationAnalysisHtmlReport.get());
+ page.navigate(tempFile.toUri().toString());
+ return new KeepRadiusInspector(page);
+ }
+
+ public <E extends Throwable> CR inspectKeepRadiusHtmlReport(
+ Page page, ThrowingConsumer<KeepRadiusInspector, E> consumer) throws E, IOException {
+ consumer.accept(keepRadiusInspector(page));
+ return self();
+ }
+
public String getProguardConfiguration() {
return proguardConfiguration;
}
diff --git a/src/test/testbase/java/com/android/tools/r8/keepradius/ui/KeepRadiusInspector.java b/src/test/testbase/java/com/android/tools/r8/keepradius/ui/KeepRadiusInspector.java
new file mode 100644
index 0000000..98275ff
--- /dev/null
+++ b/src/test/testbase/java/com/android/tools/r8/keepradius/ui/KeepRadiusInspector.java
@@ -0,0 +1,86 @@
+// Copyright (c) 2026, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepradius.ui;
+
+import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
+
+import com.microsoft.playwright.Locator;
+import com.microsoft.playwright.Page;
+import com.microsoft.playwright.options.BoundingBox;
+
+public class KeepRadiusInspector {
+
+ private final Page page;
+
+ public KeepRadiusInspector(Page page) {
+ this.page = page;
+ }
+
+ public KeepRadiusInspector assertTitle(String expected) {
+ assertThat(page).hasTitle(expected);
+ return this;
+ }
+
+ public KeepRadiusInspector assertTotalObfuscationNotEmpty() {
+ assertThat(page.locator("#total-obfuscation")).not().isEmpty();
+ return this;
+ }
+
+ public KeepRadiusInspector clickLensTab(String lens) {
+ page.click("#lens-tabs button[data-lens=\"" + lens + "\"]");
+ return this;
+ }
+
+ public KeepRadiusInspector search(String term) {
+ BoundingBox box = page.locator("#search-input").boundingBox();
+ if (box == null || box.width == 0) {
+ page.click("#search-icon-btn");
+ }
+ page.fill("#search-input", term);
+ return this;
+ }
+
+ public KeepRadiusInspector clickRowWithText(String text) {
+ page.locator("#table-data td")
+ .filter(new Locator.FilterOptions().setHasText(text))
+ .first()
+ .click();
+ return this;
+ }
+
+ public KeepRadiusInspector assertTableContains(String text) {
+ assertThat(page.locator("#table-data")).containsText(text);
+ return this;
+ }
+
+ public KeepRadiusInspector assertTableNotContains(String text) {
+ assertThat(page.locator("#table-data")).not().containsText(text);
+ return this;
+ }
+
+ public KeepRadiusInspector assertVisible(String selector) {
+ assertThat(page.locator(selector)).isVisible();
+ return this;
+ }
+
+ public KeepRadiusInspector assertHidden(String selector) {
+ assertThat(page.locator(selector)).isHidden();
+ return this;
+ }
+
+ public KeepRadiusInspector assertTableRowsCount(int expectedCount) {
+ assertThat(page.locator("#table-data tr")).hasCount(expectedCount);
+ return this;
+ }
+
+ public KeepRadiusInspector assertDetailsClassesContains(String className) {
+ assertThat(page.locator("#details-classes-content")).containsText(className);
+ return this;
+ }
+
+ public KeepRadiusInspector clickDetailsBackToSummary() {
+ page.click("#details-back-to-summary");
+ return this;
+ }
+}
diff --git a/third_party/chrome_headless/linux.tar.gz.sha1 b/third_party/chrome_headless/linux.tar.gz.sha1
new file mode 100644
index 0000000..dc59451
--- /dev/null
+++ b/third_party/chrome_headless/linux.tar.gz.sha1
@@ -0,0 +1 @@
+fd81a2a204f7528ca52a9cfb0a802027fa0ef9b0
\ No newline at end of file
diff --git a/third_party/chrome_headless/mac.tar.gz.sha1 b/third_party/chrome_headless/mac.tar.gz.sha1
new file mode 100644
index 0000000..32237b5
--- /dev/null
+++ b/third_party/chrome_headless/mac.tar.gz.sha1
@@ -0,0 +1 @@
+6a1bda8761ca6cfb377bde6c8eef8947150840c8
\ No newline at end of file
diff --git a/third_party/chrome_headless/windows.tar.gz.sha1 b/third_party/chrome_headless/windows.tar.gz.sha1
new file mode 100644
index 0000000..42f5f4e
--- /dev/null
+++ b/third_party/chrome_headless/windows.tar.gz.sha1
@@ -0,0 +1 @@
+616ed97744c5950fd6fec7a2c3df8211392958b1
\ No newline at end of file
diff --git a/third_party/dependencies.tar.gz.sha1 b/third_party/dependencies.tar.gz.sha1
index 62aca42..7ba547d 100644
--- a/third_party/dependencies.tar.gz.sha1
+++ b/third_party/dependencies.tar.gz.sha1
@@ -1 +1 @@
-ea3befd18630378e7d73b067a49c0623892675b7
\ No newline at end of file
+cdd0a821609f82f6101d4c3b6ccd42ee9ab78228
\ No newline at end of file
diff --git a/tools/create_local_maven_with_dependencies.py b/tools/create_local_maven_with_dependencies.py
index 307848a..e61b569 100755
--- a/tools/create_local_maven_with_dependencies.py
+++ b/tools/create_local_maven_with_dependencies.py
@@ -30,7 +30,8 @@
# the dependencies untangled.
KOTLIN_VERSION = '2.2.21'
GUAVA_VERSION = '32.1.2-jre'
-GSON_VERSION = '2.10.1'
+GSON_VERSION = '2.14.0'
+PLAYWRIGHT_VERSION = '1.60.0'
JAVASSIST_VERSION = '3.29.2-GA'
MOCKITO_VERSION = '2.10.0'
TEST_RETRY_VERSION = '1.6.4'
@@ -120,6 +121,8 @@
version=JUNIT_JUPITER_VERSION),
'org.junit.platform:junit-platform-launcher:{version}'.format(
version=JUNIT_PLATFORM_VERSION),
+ 'com.microsoft.playwright:playwright:{version}'.format(
+ version=PLAYWRIGHT_VERSION),
]
PLUGIN_DEPENDENCIES = [
diff --git a/tools/update_chrome_headless.py b/tools/update_chrome_headless.py
new file mode 100755
index 0000000..1b6bb7e
--- /dev/null
+++ b/tools/update_chrome_headless.py
@@ -0,0 +1,169 @@
+#!/usr/bin/env python3
+# Copyright (c) 2026, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+import argparse
+import json
+import os
+import shutil
+import stat
+import sys
+import tempfile
+import urllib.request
+import zipfile
+
+import utils
+
+VERSION = "148.0.7778.96" # Default version, but can be overridden
+API_URL = "https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json"
+
+
+def get_latest_stable_version():
+ print("Fetching latest stable Chrome version from googlechromelabs...")
+ try:
+ req = urllib.request.Request(API_URL,
+ headers={'User-Agent': 'Mozilla/5.0'})
+ with urllib.request.urlopen(req) as response:
+ data = json.loads(response.read().decode('utf-8'))
+ channel_data = data.get("channels", {}).get("Stable")
+ if channel_data:
+ return channel_data.get("version")
+ except Exception as e:
+ print(f"Failed to fetch latest version: {e}")
+ return None
+
+
+def parse_options():
+ parser = argparse.ArgumentParser(
+ description=
+ 'Update chrome-headless-shell binaries for Playwright in third_party.')
+ parser.add_argument(
+ '--version',
+ help=
+ f'Chrome version to download. If not specified and --latest is not used, defaults to {VERSION}.'
+ )
+ parser.add_argument(
+ '--latest',
+ action='store_true',
+ help='Download the latest stable version of chrome-headless-shell.')
+ return parser.parse_args()
+
+
+def download_and_extract(name, url, extract_to):
+ with tempfile.TemporaryDirectory() as tmpdir:
+ zip_path = os.path.join(tmpdir, f"{name}.zip")
+ print(f"Downloading {url} to {zip_path}...")
+ try:
+ urllib.request.urlretrieve(url, zip_path)
+ except Exception as e:
+ print(f"Failed to download {url}: {e}")
+ return False
+
+ print(f"Extracting {zip_path} to {tmpdir}...")
+ with zipfile.ZipFile(zip_path, 'r') as zip_ref:
+ zip_ref.extractall(tmpdir)
+
+ # Chrome zip usually contains a single top-level directory
+ zip_dirs = [
+ d for d in os.listdir(tmpdir)
+ if os.path.isdir(os.path.join(tmpdir, d))
+ ]
+ if not zip_dirs:
+ print(f"No directory found in zip for {name}")
+ return False
+
+ src_dir = os.path.join(tmpdir, zip_dirs[0])
+ print(f"Moving contents of {src_dir} to {extract_to}...")
+ os.makedirs(extract_to, exist_ok=True)
+ for item in os.listdir(src_dir):
+ s = os.path.join(src_dir, item)
+ d = os.path.join(extract_to, item)
+ if os.path.exists(d):
+ if os.path.isdir(d):
+ shutil.rmtree(d)
+ else:
+ os.remove(d)
+ shutil.move(s, d)
+ return True
+
+
+def set_executable(file_path):
+ if os.path.exists(file_path):
+ print(f"Making {file_path} executable...")
+ st = os.stat(file_path)
+ os.chmod(file_path,
+ st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
+ else:
+ print(f"Warning: File {file_path} not found to set executable.")
+
+
+def main():
+ args = parse_options()
+ version = args.version
+ if args.latest:
+ version = get_latest_stable_version()
+ if not version:
+ print("Error: Could not retrieve latest stable version.")
+ sys.exit(1)
+ elif not version:
+ version = VERSION
+
+ print(f"Using Chrome version: {version}")
+ base_url = f"https://storage.googleapis.com/chrome-for-testing-public/{version}"
+
+ urls = {
+ "linux":
+ f"{base_url}/linux64/chrome-headless-shell-linux64.zip",
+ "mac-arm64":
+ f"{base_url}/mac-arm64/chrome-headless-shell-mac-arm64.zip",
+ "mac-x64":
+ f"{base_url}/mac-x64/chrome-headless-shell-mac-x64.zip",
+ "windows":
+ f"{base_url}/win64/chrome-headless-shell-win64.zip"
+ }
+
+ chrome_headless_dir = os.path.join(utils.THIRD_PARTY, 'chrome_headless')
+
+ linux_dir = os.path.join(chrome_headless_dir, 'linux')
+ mac_dir = os.path.join(chrome_headless_dir, 'mac')
+ mac_arm_dir = os.path.join(mac_dir, 'mac-arm64')
+ mac_x64_dir = os.path.join(mac_dir, 'mac-x64')
+ windows_dir = os.path.join(chrome_headless_dir, 'windows')
+
+ # Clean up existing directories if they exist
+ for d in [linux_dir, mac_dir, windows_dir]:
+ if os.path.exists(d):
+ print(f"Removing existing directory {d}...")
+ shutil.rmtree(d)
+
+ # 1. Linux
+ if download_and_extract("linux", urls["linux"], linux_dir):
+ set_executable(os.path.join(linux_dir, "chrome-headless-shell"))
+
+ # 2. Mac
+ download_arm = download_and_extract("mac-arm64", urls["mac-arm64"],
+ mac_arm_dir)
+ download_x64 = download_and_extract("mac-x64", urls["mac-x64"], mac_x64_dir)
+ if download_arm and download_x64:
+ set_executable(os.path.join(mac_arm_dir, "chrome-headless-shell"))
+ set_executable(os.path.join(mac_x64_dir, "chrome-headless-shell"))
+
+ # 3. Windows
+ download_and_extract("windows", urls["windows"], windows_dir)
+
+ print("\nLocal setup complete.")
+ print("If you want to upload these to Google Storage, run:")
+ print(
+ f" (cd {chrome_headless_dir}; upload_to_google_storage.py -a --bucket r8-deps linux)"
+ )
+ print(
+ f" (cd {chrome_headless_dir}; upload_to_google_storage.py -a --bucket r8-deps mac)"
+ )
+ print(
+ f" (cd {chrome_headless_dir}; upload_to_google_storage.py -a --bucket r8-deps windows)"
+ )
+
+
+if __name__ == '__main__':
+ sys.exit(main())