Test all --help messages (and fix platform newlines).
Change-Id: I143557d446a67950961ca743bb7fe283742cdb2f
diff --git a/src/main/java/com/android/tools/r8/DexSegments.java b/src/main/java/com/android/tools/r8/DexSegments.java
index 798f0f6..e2e6e01 100644
--- a/src/main/java/com/android/tools/r8/DexSegments.java
+++ b/src/main/java/com/android/tools/r8/DexSegments.java
@@ -12,7 +12,7 @@
import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.StringDiagnostic;
-import com.google.common.collect.ImmutableList;
+import com.android.tools.r8.utils.internal.StringUtils;
import it.unimi.dsi.fastutil.ints.Int2ReferenceLinkedOpenHashMap;
import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
import java.io.IOException;
@@ -49,11 +49,12 @@
}
}
- static final String USAGE_MESSAGE = String.join("\n", ImmutableList.of(
- "Usage: dexsegments [options] <input-files>",
- " where <input-files> are dex files",
- " --version # Print the version of r8.",
- " --help # Print this message."));
+ static final String USAGE_MESSAGE =
+ StringUtils.joinLines(
+ "Usage: dexsegments [options] <input-files>",
+ " where <input-files> are dex files",
+ " --version # Print the version of r8.",
+ " --help # Print this message.");
public static Command.Builder builder() {
return new Command.Builder();
diff --git a/src/main/java/com/android/tools/r8/Disassemble.java b/src/main/java/com/android/tools/r8/Disassemble.java
index ccfd8fd..67d3231 100644
--- a/src/main/java/com/android/tools/r8/Disassemble.java
+++ b/src/main/java/com/android/tools/r8/Disassemble.java
@@ -16,13 +16,14 @@
import com.android.tools.r8.references.MethodReference;
import com.android.tools.r8.references.Reference;
import com.android.tools.r8.utils.AndroidApp;
-import com.android.tools.r8.utils.internal.ConsumerUtils;
import com.android.tools.r8.utils.FieldReferenceUtils;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.MethodReferenceUtils;
import com.android.tools.r8.utils.ProgramResourceProviderUtils;
import com.android.tools.r8.utils.StringDiagnostic;
import com.android.tools.r8.utils.ThreadUtils;
+import com.android.tools.r8.utils.internal.ConsumerUtils;
+import com.android.tools.r8.utils.internal.StringUtils;
import com.android.tools.r8.utils.timing.Timing;
import java.io.Closeable;
import java.io.IOException;
@@ -141,24 +142,25 @@
}
static final String USAGE_MESSAGE =
- "Usage: disasm [options] <input-files>\n"
- + " where <input-files> are dex files\n"
- + " and options are:\n"
- + " --all # Include all information in disassembly.\n"
- + " --smali # Disassemble using smali syntax.\n"
- + " --ir # Print IR before and after optimization.\n"
- + " --nocode # No printing of code objects.\n"
- + " --pg-map <file> # Proguard map <file> for mapping names.\n"
- + " --pg-map-charset <charset> # Charset for Proguard map file.\n"
- + " --output # Specify a file or directory to write to.\n"
- + " --class <descriptor> # Only disassemble the given class "
- + "(e.g., Lcom/example/Class;).\n"
- + " --field <descriptor> # Only disassemble the given field "
- + "(e.g., Lcom/example/Class;->method()V).\n"
- + " --method <descriptor> # Only disassemble the given method "
- + "(e.g., Lcom/example/Class;->field:I).\n"
- + " --version # Print the version of r8.\n"
- + " --help # Print this message.";
+ StringUtils.joinLines(
+ "Usage: disasm [options] <input-files>",
+ " where <input-files> are dex files",
+ " and options are:",
+ " --all # Include all information in disassembly.",
+ " --smali # Disassemble using smali syntax.",
+ " --ir # Print IR before and after optimization.",
+ " --nocode # No printing of code objects.",
+ " --pg-map <file> # Proguard map <file> for mapping names.",
+ " --pg-map-charset <charset> # Charset for Proguard map file.",
+ " --output # Specify a file or directory to write to.",
+ " --class <descriptor> # Only disassemble the given class (e.g.,"
+ + " Lcom/example/Class;).",
+ " --field <descriptor> # Only disassemble the given field (e.g.,"
+ + " Lcom/example/Class;->method()V).",
+ " --method <descriptor> # Only disassemble the given method (e.g.,"
+ + " Lcom/example/Class;->field:I).",
+ " --version # Print the version of r8.",
+ " --help # Print this message.");
private final boolean allInfo;
private final boolean useSmali;
diff --git a/src/main/java/com/android/tools/r8/ExtractMarkerCommand.java b/src/main/java/com/android/tools/r8/ExtractMarkerCommand.java
index 115fcd8..2838917 100644
--- a/src/main/java/com/android/tools/r8/ExtractMarkerCommand.java
+++ b/src/main/java/com/android/tools/r8/ExtractMarkerCommand.java
@@ -7,8 +7,8 @@
import com.android.tools.r8.keepanno.annotations.KeepForApi;
import com.android.tools.r8.origin.Origin;
import com.android.tools.r8.origin.PathOrigin;
+import com.android.tools.r8.utils.internal.StringUtils;
import com.android.tools.r8.utils.internal.collections.Pair;
-import com.google.common.collect.ImmutableList;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
@@ -104,12 +104,10 @@
}
static final String USAGE_MESSAGE =
- String.join(
- "\n",
- ImmutableList.of(
- "Usage: extractmarker [options] <input-files>",
- " where <input-files> are D8 supported input/output files and options are:",
- " --help # Print this message."));
+ StringUtils.joinLines(
+ "Usage: extractmarker [options] <input-files>",
+ " where <input-files> are D8 supported input/output files and options are:",
+ " --help # Print this message.");
public static Builder builder() {
return builder(new DiagnosticsHandler() {});
diff --git a/src/main/java/com/android/tools/r8/GenerateMainDexListCommand.java b/src/main/java/com/android/tools/r8/GenerateMainDexListCommand.java
index ceb96f7..f5926eb 100644
--- a/src/main/java/com/android/tools/r8/GenerateMainDexListCommand.java
+++ b/src/main/java/com/android/tools/r8/GenerateMainDexListCommand.java
@@ -20,6 +20,7 @@
import com.android.tools.r8.utils.JoiningStringConsumer;
import com.android.tools.r8.utils.Reporter;
import com.android.tools.r8.utils.StringDiagnostic;
+import com.android.tools.r8.utils.internal.StringUtils;
import com.google.common.collect.ImmutableList;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -131,18 +132,18 @@
}
}
- static final String USAGE_MESSAGE = String.join("\n", ImmutableList.of(
- "Usage: maindex [options] <input-files>",
- " where <input-files> are JAR files",
- " and options are:",
- " --lib <file> # Add <file> as a library resource.",
- " --main-dex-rules <file> # Proguard keep rules for classes to place in the",
- " # primary dex file.",
- " --main-dex-list <file> # List of classes to place in the primary dex file.",
- " --main-dex-list-output <file> # Output the full main-dex list in <file>.",
- " --version # Print the version.",
- " --help # Print this message."));
-
+ static final String USAGE_MESSAGE =
+ StringUtils.joinLines(
+ "Usage: maindex [options] <input-files>",
+ " where <input-files> are JAR files",
+ " and options are:",
+ " --lib <file> # Add <file> as a library resource.",
+ " --main-dex-rules <file> # Proguard keep rules for classes to place in the",
+ " # primary dex file.",
+ " --main-dex-list <file> # List of classes to place in the primary dex file.",
+ " --main-dex-list-output <file> # Output the full main-dex list in <file>.",
+ " --version # Print the version.",
+ " --help # Print this message.");
public static GenerateMainDexListCommand.Builder builder() {
return new GenerateMainDexListCommand.Builder();
diff --git a/src/main/java/com/android/tools/r8/GlobalSyntheticsGeneratorCommandParser.java b/src/main/java/com/android/tools/r8/GlobalSyntheticsGeneratorCommandParser.java
index 1bb711c..c331b84 100644
--- a/src/main/java/com/android/tools/r8/GlobalSyntheticsGeneratorCommandParser.java
+++ b/src/main/java/com/android/tools/r8/GlobalSyntheticsGeneratorCommandParser.java
@@ -46,7 +46,7 @@
.build();
}
- static String getUsageMessage() {
+ public static String getUsageMessage() {
StringBuilder builder = new StringBuilder();
StringUtils.appendLines(builder, USAGE_MESSAGE);
new ParseFlagPrinter().addFlags(getFlags()).appendLinesToBuilder(builder);
diff --git a/src/main/java/com/android/tools/r8/relocator/RelocatorCommand.java b/src/main/java/com/android/tools/r8/relocator/RelocatorCommand.java
index 6ffb7b9..dd70705 100644
--- a/src/main/java/com/android/tools/r8/relocator/RelocatorCommand.java
+++ b/src/main/java/com/android/tools/r8/relocator/RelocatorCommand.java
@@ -37,10 +37,10 @@
import com.android.tools.r8.utils.ThreadUtils;
import com.android.tools.r8.utils.internal.Box;
import com.android.tools.r8.utils.internal.FileUtils;
+import com.android.tools.r8.utils.internal.StringUtils;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Iterables;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -57,23 +57,20 @@
ImmutableSet.of("--output", "--input", "--input-no-res", "--map", THREAD_COUNT_FLAG);
static final String USAGE_MESSAGE =
- String.join(
- "\n",
- Iterables.concat(
- Arrays.asList(
- "The Relocator CLI is EXPERIMENTAL and is subject to change",
- "Usage: relocator [options]",
- " where options are:",
- " --input <file> # Input file to remap, class, zip or jar.",
- " --input-no-res <file> # Input file to remap, zip or jar.",
- " # Only .class file entries are included.",
- " --output <file> # Output result in <outfile>.",
- " --map <from->to> # Registers a mapping.",
- " --map-diagnostics [:<type>] <from-level> <to-level>",
- " # Map diagnostics level.",
- " --thread-count <number> # A specified number of threads to run with.",
- " --version # Print the version of d8.",
- " --help # Print this message.")));
+ StringUtils.joinLines(
+ "The Relocator CLI is EXPERIMENTAL and is subject to change",
+ "Usage: relocator [options]",
+ " where options are:",
+ " --input <file> # Input file to remap, class, zip or jar.",
+ " --input-no-res <file> # Input file to remap, zip or jar.",
+ " # Only .class file entries are included.",
+ " --output <file> # Output result in <outfile>.",
+ " --map <from->to> # Registers a mapping.",
+ " --map-diagnostics [:<type>] <from-level> <to-level>",
+ " # Map diagnostics level.",
+ " --thread-count <number> # A specified number of threads to run with.",
+ " --version # Print the version of d8.",
+ " --help # Print this message.");
private final boolean printHelp;
private final boolean printVersion;
diff --git a/src/test/java/com/android/tools/r8/ApiDatabaseGeneratorCommandParserTest.java b/src/test/java/com/android/tools/r8/ApiDatabaseGeneratorCommandParserTest.java
new file mode 100644
index 0000000..6dabf54
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ApiDatabaseGeneratorCommandParserTest.java
@@ -0,0 +1,41 @@
+// 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 static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.utils.internal.StringUtils;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ApiDatabaseGeneratorCommandParserTest extends TestBase {
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withNoneRuntime().build();
+ }
+
+ public ApiDatabaseGeneratorCommandParserTest(TestParameters parameters) {
+ parameters.assertNoneRuntime();
+ }
+
+ @Test
+ public void testHelpMessage() {
+ assertEquals(
+ StringUtils.lines(
+ "Usage: apidatabasegenerator [options] <input-files>",
+ "where options are:",
+ " --help",
+ " -h # Print help.",
+ " --version # Print version.",
+ " --output <database-file>",
+ " # Output result in <database-file> (must be a file, not a"
+ + " directory).",
+ " # Defaults to 'api_database.ser'."),
+ ApiDatabaseGeneratorCommandParser.getUsageMessage());
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/BackportedMethodListTest.java b/src/test/java/com/android/tools/r8/BackportedMethodListTest.java
index dc9c64e..ff16a70 100644
--- a/src/test/java/com/android/tools/r8/BackportedMethodListTest.java
+++ b/src/test/java/com/android/tools/r8/BackportedMethodListTest.java
@@ -6,14 +6,15 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
import com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification;
import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.internal.StringUtils;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
-import org.junit.Assume;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@@ -138,6 +139,27 @@
}
@Test
+ public void testHelpMessage() {
+ assumeTrue(mode == Mode.NO_LIBRARY);
+ assertEquals(
+ StringUtils.lines(
+ "Usage: BackportedMethodList [options]",
+ " Options are:",
+ "",
+ " --output <file> # Output result in <file>.",
+ " --min-api <number> # Minimum Android API level for the application.",
+ " --desugared-lib <file> # Desugared library configuration (JSON from the"
+ + " configuration).",
+ " --lib <file> # The compilation SDK library (android.jar).",
+ " --android-platform-build",
+ " # Compilation of platform code.",
+ " --version # Print the version of BackportedMethodList.",
+ " --help",
+ " -h # Print this message."),
+ BackportedMethodListCommand.usageMessage());
+ }
+
+ @Test
public void testConsumer() throws Exception {
for (int apiLevel = 1; apiLevel < AndroidApiLevel.LATEST.getLevel(); apiLevel++) {
ListStringConsumer consumer = new ListStringConsumer();
@@ -172,7 +194,7 @@
@Test
public void testFullList() throws Exception {
- Assume.assumeTrue(mode == Mode.NO_LIBRARY);
+ assumeTrue(mode == Mode.NO_LIBRARY);
ListStringConsumer consumer = new ListStringConsumer();
// Not setting neither min API level not library should produce the full list.
BackportedMethodList.run(BackportedMethodListCommand.builder().setConsumer(consumer).build());
@@ -182,7 +204,7 @@
@Test
public void requireLibraryForDesugar() {
- Assume.assumeTrue(mode == Mode.LIBRARY_DESUGAR || mode == Mode.LIBRARY_DESUGAR_11);
+ assumeTrue(mode == Mode.LIBRARY_DESUGAR || mode == Mode.LIBRARY_DESUGAR_11);
// Require library when a desugar configuration is passed.
try {
BackportedMethodList.run(
diff --git a/src/test/java/com/android/tools/r8/D8CommandTest.java b/src/test/java/com/android/tools/r8/D8CommandTest.java
index 4160500..caac031 100644
--- a/src/test/java/com/android/tools/r8/D8CommandTest.java
+++ b/src/test/java/com/android/tools/r8/D8CommandTest.java
@@ -29,6 +29,7 @@
import com.android.tools.r8.utils.ThreadUtils;
import com.android.tools.r8.utils.ZipUtils;
import com.android.tools.r8.utils.internal.FileUtils;
+import com.android.tools.r8.utils.internal.StringUtils;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@@ -167,6 +168,95 @@
}
@Test
+ public void testHelpMessage() {
+ assertEquals(
+ StringUtils.lines(
+ "Usage: d8 [options] [@<argfile>] <input-files>",
+ " where <input-files> are any combination of dex, class, zip, jar, or apk files",
+ " and each <argfile> is a file containing additional arguments (one per line)",
+ " and options are:",
+ " --debug # Compile with debugging information (default).",
+ " --release # Compile without debugging information.",
+ " --output <file> # Output result in <file>.",
+ " # <file> must be an existing directory or a zip file.",
+ " --globals <file> # Global synthetics <file> from a previous intermediate"
+ + " compilation.",
+ " # The <file> may be either a zip-archive of global"
+ + " synthetics or the",
+ " # global-synthetic files directly.",
+ " --globals-output <file> # Output global synthetics in <file>.",
+ " # <file> must be an existing directory or a non-existent zip"
+ + " archive.",
+ " --lib <file|jdk-home> # Add <file|jdk-home> as a library resource.",
+ " --classpath <file> # Add <file> as a classpath resource.",
+ " --min-api <number> # Minimum Android API level compatibility (default: 1).",
+ " --pg-map <file> # Use <file> as a mapping file for distribution.",
+ " --intermediate # Compile an intermediate result intended for later",
+ " # merging.",
+ " --file-per-class # Produce a separate dex file per class.",
+ " # Synthetic classes are in their own file.",
+ " --file-per-class-file # Produce a separate dex file per input .class file.",
+ " # Synthetic classes are with their originating class.",
+ " --no-desugaring # Force disable desugaring.",
+ " --desugared-lib <file> # Specify desugared library configuration.",
+ " # <file> is a desugared library configuration (json).",
+ " --main-dex-rules <file> # Proguard keep rules for classes to place in the",
+ " # primary dex file.",
+ " --main-dex-list <file> # List of classes to place in the primary dex file.",
+ " --main-dex-list-output <file>",
+ " # Output resulting main dex list in <file>.",
+ " --force-enable-assertions[:[<class name>|<package name>...]]",
+ " --force-ea[:[<class name>|<package name>...]]",
+ " # Forcefully enable javac generated assertion code.",
+ " --force-disable-assertions[:[<class name>|<package name>...]]",
+ " --force-da[:[<class name>|<package name>...]]",
+ " # Forcefully disable javac generated assertion code.",
+ " # This is the default handling of javac assertion code",
+ " # when generating DEX file format.",
+ " --force-passthrough-assertions[:[<class name>|<package name>...]]",
+ " --force-pa[:[<class name>|<package name>...]]",
+ " # Don't change javac generated assertion code. This",
+ " # is the default handling of javac assertion code when",
+ " # generating class file format.",
+ " --force-assertions-handler:<handler method>[:[<class name>|<package name>...]]",
+ " --force-ah:<handler method>[:[<class name>|<package name>...]]",
+ " # Change javac and kotlinc generated assertion code",
+ " # to invoke the method <handler method> with each",
+ " # assertion error instead of throwing it.",
+ " # The <handler method> is specified as a class name",
+ " # followed by a dot and the method name.",
+ " # The handler method must take a single argument of",
+ " # type java.lang.Throwable and have return type void.",
+ " --thread-count <number> # Use <number> of threads for compilation.",
+ " # If not specified the number will be based on",
+ " # heuristics taking the number of cores into account.",
+ " --map-diagnostics[:<type>] <from-level> <to-level>",
+ " # Map diagnostics of <type> (default any) reported as",
+ " # <from-level> to <to-level> where <from-level> and",
+ " # <to-level> are one of 'info', 'warning', or 'error'",
+ " # and the optional <type> is either the simple or",
+ " # fully qualified Java type name of a diagnostic.",
+ " # If <type> is unspecified, all diagnostics at ",
+ " # <from-level> will be mapped.",
+ " # Note that fatal compiler errors cannot be mapped.",
+ " --android-platform-build",
+ " # Compile as a platform build where the"
+ + " runtime/bootclasspath",
+ " # is assumed to be the version specified by --min-api.",
+ " --art-profile <input> <output>",
+ " # Rewrite human readable ART profile read from <input> and"
+ + " write to <output>.",
+ " --startup-profile <file>",
+ " # Startup profile <file> to use for dex layout.",
+ " --verbose-synthetic-names",
+ " # Enable verbose synthetic names that use the"
+ + " `$$ExternalSynthetic` marker.",
+ " --version # Print the version of d8.",
+ " --help # Print this message."),
+ D8CommandParser.getUsageMessage());
+ }
+
+ @Test
public void validOutputPath() throws Throwable {
Path existingDir = temp.getRoot().toPath();
Path nonExistingZip = existingDir.resolve("a-non-existing-archive.zip");
diff --git a/src/test/java/com/android/tools/r8/DexSegmentsTest.java b/src/test/java/com/android/tools/r8/DexSegmentsTest.java
new file mode 100644
index 0000000..9bcd6a4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/DexSegmentsTest.java
@@ -0,0 +1,36 @@
+// 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 static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.utils.internal.StringUtils;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class DexSegmentsTest extends TestBase {
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withNoneRuntime().build();
+ }
+
+ public DexSegmentsTest(TestParameters parameters) {
+ parameters.assertNoneRuntime();
+ }
+
+ @Test
+ public void testHelpMessage() {
+ assertEquals(
+ StringUtils.joinLines(
+ "Usage: dexsegments [options] <input-files>",
+ " where <input-files> are dex files",
+ " --version # Print the version of r8.",
+ " --help # Print this message."),
+ DexSegments.Command.USAGE_MESSAGE);
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/DisassembleTest.java b/src/test/java/com/android/tools/r8/DisassembleTest.java
new file mode 100644
index 0000000..862d390
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/DisassembleTest.java
@@ -0,0 +1,50 @@
+// 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 static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.utils.internal.StringUtils;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class DisassembleTest extends TestBase {
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withNoneRuntime().build();
+ }
+
+ public DisassembleTest(TestParameters parameters) {
+ parameters.assertNoneRuntime();
+ }
+
+ @Test
+ public void testHelpMessage() {
+ assertEquals(
+ StringUtils.joinLines(
+ "Usage: disasm [options] <input-files>",
+ " where <input-files> are dex files",
+ " and options are:",
+ " --all # Include all information in disassembly.",
+ " --smali # Disassemble using smali syntax.",
+ " --ir # Print IR before and after optimization.",
+ " --nocode # No printing of code objects.",
+ " --pg-map <file> # Proguard map <file> for mapping names.",
+ " --pg-map-charset <charset> # Charset for Proguard map file.",
+ " --output # Specify a file or directory to write to.",
+ " --class <descriptor> # Only disassemble the given class (e.g.,"
+ + " Lcom/example/Class;).",
+ " --field <descriptor> # Only disassemble the given field (e.g.,"
+ + " Lcom/example/Class;->method()V).",
+ " --method <descriptor> # Only disassemble the given method (e.g.,"
+ + " Lcom/example/Class;->field:I).",
+ " --version # Print the version of r8.",
+ " --help # Print this message."),
+ Disassemble.DisassembleCommand.USAGE_MESSAGE);
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/ExtractKeepAnnoRulesCommandTest.java b/src/test/java/com/android/tools/r8/ExtractKeepAnnoRulesCommandTest.java
new file mode 100644
index 0000000..45ad42c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ExtractKeepAnnoRulesCommandTest.java
@@ -0,0 +1,37 @@
+// 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 static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.utils.internal.StringUtils;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ExtractKeepAnnoRulesCommandTest extends TestBase {
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withNoneRuntime().build();
+ }
+
+ public ExtractKeepAnnoRulesCommandTest(TestParameters parameters) {
+ parameters.assertNoneRuntime();
+ }
+
+ @Test
+ public void testHelpMessage() {
+ assertEquals(
+ StringUtils.lines(
+ "Usage: EXPERIMENTAL tool to extract keep rules from keep annotations",
+ " --rules-output <file> # Output the extracted keep rules.",
+ " --rules-target r8|pg # Optimizer rules are for (default r8).",
+ " --version # Print the version.",
+ " --help # Print this message."),
+ ExtractKeepAnnoRulesCommand.USAGE_MESSAGE);
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/ExtractMarkerTest.java b/src/test/java/com/android/tools/r8/ExtractMarkerTest.java
index 0d5c8f4..1826b7e 100644
--- a/src/test/java/com/android/tools/r8/ExtractMarkerTest.java
+++ b/src/test/java/com/android/tools/r8/ExtractMarkerTest.java
@@ -43,6 +43,17 @@
getTestParameters().withAllRuntimes().withAllApiLevels().build(), BooleanUtils.values());
}
+ @Test
+ public void testHelpMessage() {
+ parameters.assumeIsOrSimulateNoneRuntime();
+ assertEquals(
+ StringUtils.joinLines(
+ "Usage: extractmarker [options] <input-files>",
+ " where <input-files> are D8 supported input/output files and options are:",
+ " --help # Print this message."),
+ ExtractMarkerCommand.USAGE_MESSAGE);
+ }
+
public ExtractMarkerTest(TestParameters parameters, boolean includeClassesChecksum) {
this.parameters = parameters;
this.includeClassesChecksum = includeClassesChecksum;
diff --git a/src/test/java/com/android/tools/r8/ExtractR8RulesCommandTest.java b/src/test/java/com/android/tools/r8/ExtractR8RulesCommandTest.java
new file mode 100644
index 0000000..4175e2d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ExtractR8RulesCommandTest.java
@@ -0,0 +1,38 @@
+// 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 static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.utils.internal.StringUtils;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ExtractR8RulesCommandTest extends TestBase {
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withNoneRuntime().build();
+ }
+
+ public ExtractR8RulesCommandTest(TestParameters parameters) {
+ parameters.assertNoneRuntime();
+ }
+
+ @Test
+ public void testHelpMessage() {
+ assertEquals(
+ StringUtils.lines(
+ "Usage: TBD",
+ " --rules-output <file> # Output the extracted keep rules.",
+ " --compiler-version <version> # Output the proguard rules extracted.",
+ " --include-origin-comments # Include comments with origin for extracted rules.",
+ " --version # Print the version.",
+ " --help # Print this message."),
+ ExtractR8RulesCommand.USAGE_MESSAGE);
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/GenerateMainDexListCommandParserTest.java b/src/test/java/com/android/tools/r8/GenerateMainDexListCommandParserTest.java
new file mode 100644
index 0000000..7c9d4ab
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/GenerateMainDexListCommandParserTest.java
@@ -0,0 +1,42 @@
+// 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 static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.utils.internal.StringUtils;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class GenerateMainDexListCommandParserTest extends TestBase {
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withNoneRuntime().build();
+ }
+
+ public GenerateMainDexListCommandParserTest(TestParameters parameters) {
+ parameters.assertNoneRuntime();
+ }
+
+ @Test
+ public void testHelpMessage() {
+ assertEquals(
+ StringUtils.joinLines(
+ "Usage: maindex [options] <input-files>",
+ " where <input-files> are JAR files",
+ " and options are:",
+ " --lib <file> # Add <file> as a library resource.",
+ " --main-dex-rules <file> # Proguard keep rules for classes to place in the",
+ " # primary dex file.",
+ " --main-dex-list <file> # List of classes to place in the primary dex file.",
+ " --main-dex-list-output <file> # Output the full main-dex list in <file>.",
+ " --version # Print the version.",
+ " --help # Print this message."),
+ GenerateMainDexListCommand.USAGE_MESSAGE);
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/GlobalSyntheticsGeneratorCommandParserTest.java b/src/test/java/com/android/tools/r8/GlobalSyntheticsGeneratorCommandParserTest.java
new file mode 100644
index 0000000..0084fa0
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/GlobalSyntheticsGeneratorCommandParserTest.java
@@ -0,0 +1,47 @@
+// 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 static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.utils.internal.StringUtils;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class GlobalSyntheticsGeneratorCommandParserTest extends TestBase {
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withNoneRuntime().build();
+ }
+
+ public GlobalSyntheticsGeneratorCommandParserTest(TestParameters parameters) {
+ parameters.assertNoneRuntime();
+ }
+
+ @Test
+ public void testHelpMessage() {
+ assertEquals(
+ StringUtils.lines(
+ "Usage: globalsyntheticsgenerator [options] where options are:",
+ "",
+ " --min-api <number> # Minimum Android API level compatibility (default: 1).",
+ " --lib <file|jdk-home> # Add <file|jdk-home> as a library resource.",
+ " --output <globals-file> # Output result in <globals-file>.",
+ " --classfile # Generate globals for only classfile to classfile"
+ + " desugaring.",
+ " # (By default globals for both classfile and dex desugaring"
+ + " are generated).",
+ " --verbose-synthetic-names",
+ " # Enable verbose synthetic names that use the"
+ + " `$$ExternalSynthetic` marker.",
+ " --version # Print the version of globalsyntheticsgenerator.",
+ " --help # Print this message."),
+ GlobalSyntheticsGeneratorCommandParser.getUsageMessage());
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/L8CommandTest.java b/src/test/java/com/android/tools/r8/L8CommandTest.java
index 593c434..ff59328 100644
--- a/src/test/java/com/android/tools/r8/L8CommandTest.java
+++ b/src/test/java/com/android/tools/r8/L8CommandTest.java
@@ -26,6 +26,7 @@
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.ThreadUtils;
import com.android.tools.r8.utils.internal.FileUtils;
+import com.android.tools.r8.utils.internal.StringUtils;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@@ -84,6 +85,68 @@
.build());
}
+ @Test
+ public void testHelpMessage() {
+ assertEquals(
+ StringUtils.lines(
+ "Usage: l8 [options] <input-files>",
+ " where <input-files> are any combination class, zip, or jar files",
+ " where <input-files> are any combination of dex, class, zip, jar, or apk files",
+ " and options are:",
+ " --debug # Compile with debugging information (default).",
+ " --release # Compile without debugging information.",
+ " --output <file> # Output result in <file>.",
+ " # <file> must be an existing directory or a zip file.",
+ " --lib <file|jdk-home> # Add <file|jdk-home> as a library resource.",
+ " --min-api <number> # Minimum Android API level compatibility (default: 1).",
+ " --pg-conf <file> # Proguard configuration <file>.",
+ " --pg-map-output <file> # Output the resulting name and line mapping to <file>.",
+ " --partition-map-output <file>",
+ " # Output the resulting mapping to <file>.",
+ " --desugared-lib <file> # Specify desugared library configuration.",
+ " # <file> is a desugared library configuration (json).",
+ " --force-enable-assertions[:[<class name>|<package name>...]]",
+ " --force-ea[:[<class name>|<package name>...]]",
+ " # Forcefully enable javac generated assertion code.",
+ " --force-disable-assertions[:[<class name>|<package name>...]]",
+ " --force-da[:[<class name>|<package name>...]]",
+ " # Forcefully disable javac generated assertion code.",
+ " # This is the default handling of javac assertion code",
+ " # when generating DEX file format.",
+ " --force-passthrough-assertions[:[<class name>|<package name>...]]",
+ " --force-pa[:[<class name>|<package name>...]]",
+ " # Don't change javac generated assertion code. This",
+ " # is the default handling of javac assertion code when",
+ " # generating class file format.",
+ " --force-assertions-handler:<handler method>[:[<class name>|<package name>...]]",
+ " --force-ah:<handler method>[:[<class name>|<package name>...]]",
+ " # Change javac and kotlinc generated assertion code",
+ " # to invoke the method <handler method> with each",
+ " # assertion error instead of throwing it.",
+ " # The <handler method> is specified as a class name",
+ " # followed by a dot and the method name.",
+ " # The handler method must take a single argument of",
+ " # type java.lang.Throwable and have return type void.",
+ " --thread-count <number> # Use <number> of threads for compilation.",
+ " # If not specified the number will be based on",
+ " # heuristics taking the number of cores into account.",
+ " --map-diagnostics[:<type>] <from-level> <to-level>",
+ " # Map diagnostics of <type> (default any) reported as",
+ " # <from-level> to <to-level> where <from-level> and",
+ " # <to-level> are one of 'info', 'warning', or 'error'",
+ " # and the optional <type> is either the simple or",
+ " # fully qualified Java type name of a diagnostic.",
+ " # If <type> is unspecified, all diagnostics at ",
+ " # <from-level> will be mapped.",
+ " # Note that fatal compiler errors cannot be mapped.",
+ " --art-profile <input> <output>",
+ " # Rewrite human readable ART profile read from <input> and"
+ + " write to <output>.",
+ " --version # Print the version of l8.",
+ " --help # Print this message."),
+ L8CommandParser.getUsageMessage());
+ }
+
private void verifyEmptyCommand(L8Command command) {
BaseCompilerCommand compilationCommand =
command.getD8Command() == null ? command.getR8Command() : command.getD8Command();
diff --git a/src/test/java/com/android/tools/r8/R8CommandTest.java b/src/test/java/com/android/tools/r8/R8CommandTest.java
index a81cdab..79ab660 100644
--- a/src/test/java/com/android/tools/r8/R8CommandTest.java
+++ b/src/test/java/com/android/tools/r8/R8CommandTest.java
@@ -28,6 +28,7 @@
import com.android.tools.r8.utils.ThreadUtils;
import com.android.tools.r8.utils.ZipUtils;
import com.android.tools.r8.utils.internal.FileUtils;
+import com.android.tools.r8.utils.internal.StringUtils;
import com.google.common.collect.ImmutableList;
import java.io.File;
import java.io.IOException;
@@ -356,6 +357,118 @@
}
@Test
+ public void testHelpMessage() {
+ assertEquals(
+ StringUtils.lines(
+ "Usage: r8 [options] [@<argfile>] <input-files>",
+ " where <input-files> are any combination class, zip, or jar files",
+ " and each <argfile> is a file containing additional arguments (one per line)",
+ " and options are:",
+ " --release # Compile without debugging information (default).",
+ " --debug # Compile with debugging information.",
+ " --dex # Compile program to DEX file format (default).",
+ " --classfile # Compile program to Java classfile format.",
+ " --output <file> # Output result in <file>.",
+ " # <file> must be an existing directory or a zip file.",
+ " --lib <file|jdk-home> # Add <file|jdk-home> as a library resource.",
+ " --classpath <file> # Add <file> as a classpath resource.",
+ " --min-api <number> # Minimum Android API level compatibility (default: 1).",
+ " --pg-compat # Compile with R8 in Proguard compatibility mode.",
+ " --pg-conf <file> # Proguard configuration <file>.",
+ " --pg-conf-output <file> # Output the collective configuration to <file>.",
+ " --pg-map <file> # Use <file> as a mapping file for distribution and"
+ + " composition with output mapping file.",
+ " --pg-map-output <file> # Output the resulting name and line mapping to <file>.",
+ " --partition-map-output <file>",
+ " # Output the resulting mapping to <file>.",
+ " --desugared-lib <file> # Specify desugared library configuration.",
+ " # <file> is a desugared library configuration (json).",
+ " --no-tree-shaking # Force disable tree shaking of unreachable classes.",
+ " --no-minification # Force disable minification of names.",
+ " --no-data-resources # Ignore all data resources.",
+ " --no-desugaring # Force disable desugaring.",
+ " --main-dex-rules <file> # Proguard keep rules for classes to place in the",
+ " # primary dex file.",
+ " --main-dex-list <file> # List of classes to place in the primary dex file.",
+ " --android-resources <input> <output>",
+ " # Add android resource input and output to be used in"
+ + " resource shrinking. Both ",
+ " # input and output must be specified.",
+ " --android-resources-usage-log <file>",
+ " # Write the resource shrinking usage log to <file>.",
+ " --feature <input>[:|;<res-input>] <output>[:|;<res-output>]",
+ " # Add feature <input> file to <output> file. Several ",
+ " # occurrences can map to the same output. If <res-input> and"
+ + " <res-output> are ",
+ " # specified use these as resource shrinker input and output."
+ + " Separator is : on ",
+ " # linux/mac, ; on windows. It is possible to supply resource"
+ + " only features by ",
+ " # using an empty string for <input> and <output>, e.g."
+ + " --feature :in.ap_ :out.ap_",
+ " --isolated-splits # Specifies that the application is using isolated splits,"
+ + " i.e., if split APKs installed for this application are loaded into their own"
+ + " Context objects.",
+ " --main-dex-list-output <file>",
+ " # Output the full main-dex list in <file>.",
+ " --force-enable-assertions[:[<class name>|<package name>...]]",
+ " --force-ea[:[<class name>|<package name>...]]",
+ " # Forcefully enable javac generated assertion code.",
+ " --force-disable-assertions[:[<class name>|<package name>...]]",
+ " --force-da[:[<class name>|<package name>...]]",
+ " # Forcefully disable javac generated assertion code.",
+ " # This is the default handling of javac assertion code",
+ " # when generating DEX file format.",
+ " --force-passthrough-assertions[:[<class name>|<package name>...]]",
+ " --force-pa[:[<class name>|<package name>...]]",
+ " # Don't change javac generated assertion code. This",
+ " # is the default handling of javac assertion code when",
+ " # generating class file format.",
+ " --force-assertions-handler:<handler method>[:[<class name>|<package name>...]]",
+ " --force-ah:<handler method>[:[<class name>|<package name>...]]",
+ " # Change javac and kotlinc generated assertion code",
+ " # to invoke the method <handler method> with each",
+ " # assertion error instead of throwing it.",
+ " # The <handler method> is specified as a class name",
+ " # followed by a dot and the method name.",
+ " # The handler method must take a single argument of",
+ " # type java.lang.Throwable and have return type void.",
+ " --thread-count <number> # Use <number> of threads for compilation.",
+ " # If not specified the number will be based on",
+ " # heuristics taking the number of cores into account.",
+ " --map-diagnostics[:<type>] <from-level> <to-level>",
+ " # Map diagnostics of <type> (default any) reported as",
+ " # <from-level> to <to-level> where <from-level> and",
+ " # <to-level> are one of 'info', 'warning', or 'error'",
+ " # and the optional <type> is either the simple or",
+ " # fully qualified Java type name of a diagnostic.",
+ " # If <type> is unspecified, all diagnostics at ",
+ " # <from-level> will be mapped.",
+ " # Note that fatal compiler errors cannot be mapped.",
+ " --map-id-template <template>",
+ " # Set the map-id to <template>.",
+ " # The <template> can reference the variables:",
+ " # %MAP_HASH: compiler generated mapping hash.",
+ " --source-file-template <template>",
+ " # Set all source-file attributes to <template>",
+ " # The <template> can reference the variables:",
+ " # %MAP_ID: map id (e.g., value of --map-id-template).",
+ " # %MAP_HASH: compiler generated mapping hash.",
+ " --android-platform-build",
+ " # Compile as a platform build where the"
+ + " runtime/bootclasspath",
+ " # is assumed to be the version specified by --min-api.",
+ " --art-profile <input> <output>",
+ " # Rewrite human readable ART profile read from <input> and"
+ + " write to <output>.",
+ " --startup-profile <file>",
+ " # Startup profile <file> to use for dex layout.",
+ " --version # Print the version of r8.",
+ " --help # Print this message."),
+ R8CommandParser.getUsageMessage());
+ }
+
+ @Test
public void validOutputPath() throws Throwable {
Path existingDir = temp.getRoot().toPath();
Path nonExistingZip = existingDir.resolve("a-non-existing-archive.zip");
diff --git a/src/test/java/com/android/tools/r8/bisect/BisectTest.java b/src/test/java/com/android/tools/r8/bisect/BisectTest.java
index cc872e6..11daf47 100644
--- a/src/test/java/com/android/tools/r8/bisect/BisectTest.java
+++ b/src/test/java/com/android/tools/r8/bisect/BisectTest.java
@@ -18,8 +18,11 @@
import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.internal.StringUtils;
import com.android.tools.r8.utils.timing.Timing;
import com.google.common.collect.ImmutableList;
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
import java.nio.file.Path;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -36,6 +39,31 @@
return TestParameters.builder().withNoneRuntime().build();
}
+ @Test
+ public void testHelpMessage() throws Exception {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ PrintStream originalOut = System.out;
+ System.setOut(new PrintStream(out));
+ try {
+ BisectOptions.printHelp();
+ } finally {
+ System.setOut(originalOut);
+ }
+ assertEquals(
+ StringUtils.lines(
+ "--bad <apk> Known bad APK.",
+ "--command <file> Command to run after each bisection.",
+ "--good <apk> Known good APK.",
+ "--help",
+ "--output <dir> Output directory.",
+ "--result-bad Bisect again assuming previous run was",
+ " bad.",
+ "--result-good Bisect again assuming previous run was",
+ " good.",
+ "--state <file> Bisection state."),
+ out.toString());
+ }
+
public BisectTest(TestParameters parameters) {
parameters.assertNoneRuntime();
}
diff --git a/src/test/java/com/android/tools/r8/compatproguard/CompatProguardTest.java b/src/test/java/com/android/tools/r8/compatproguard/CompatProguardTest.java
index abf86b0..4f03ca0 100644
--- a/src/test/java/com/android/tools/r8/compatproguard/CompatProguardTest.java
+++ b/src/test/java/com/android/tools/r8/compatproguard/CompatProguardTest.java
@@ -13,6 +13,9 @@
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.TestParametersCollection;
import com.android.tools.r8.compatproguard.CompatProguard.CompatProguardOptions;
+import com.android.tools.r8.utils.internal.StringUtils;
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@@ -35,6 +38,38 @@
}
@Test
+ public void testHelpMessage() throws Exception {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ PrintStream originalOut = System.out;
+ System.setOut(new PrintStream(out));
+ try {
+ CompatProguard.main(new String[] {"--help"});
+ } finally {
+ System.setOut(originalOut);
+ }
+ assertEquals(
+ StringUtils.lines(
+ "",
+ "CompatProguard main (build engineering)",
+ "",
+ "compatproguard [options] --output <dir> <proguard-config>*",
+ "",
+ "Where options are:",
+ "-h/--help : print this help message",
+ "--release : compile without debugging information (default).",
+ "--debug : compile with debugging information.",
+ "--min-api n : specify the targeted min android api level",
+ "--main-dex-list list : specify main dex list for multi-dexing",
+ "--minimal-main-dex : ignored (provided for compatibility)",
+ "--multi-dex : ignored (provided for compatibility)",
+ "--no-locals : ignored (provided for compatibility)",
+ "--core-library : ignored (provided for compatibility)",
+ "--force-proguard-compatibility : Proguard compatibility mode",
+ "--no-data-resources : ignore all data resources"),
+ out.toString());
+ }
+
+ @Test
public void testDefaultDataResources() {
CompatProguardOptions options = parseArgs();
assertNull(options.output);
diff --git a/src/test/java/com/android/tools/r8/ir/desugar/desugaredlibrary/lint/DesugaredMethodsListCommandTest.java b/src/test/java/com/android/tools/r8/ir/desugar/desugaredlibrary/lint/DesugaredMethodsListCommandTest.java
new file mode 100644
index 0000000..03432b4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/desugar/desugaredlibrary/lint/DesugaredMethodsListCommandTest.java
@@ -0,0 +1,50 @@
+// 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.ir.desugar.desugaredlibrary.lint;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.internal.StringUtils;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class DesugaredMethodsListCommandTest extends TestBase {
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withNoneRuntime().build();
+ }
+
+ public DesugaredMethodsListCommandTest(TestParameters parameters) {
+ parameters.assertNoneRuntime();
+ }
+
+ @Test
+ public void testHelpMessage() {
+ assertEquals(
+ StringUtils.lines(
+ "Usage: desugaredmethods [options] where options are:",
+ " --output <file> # Output result in <file>.",
+ " # <file> must be an existing directory or a zip file.",
+ " --lib <file|jdk-home> # Add <file|jdk-home> as a library resource.",
+ " --min-api <number> # Minimum Android API level compatibility (default: 1).",
+ " --version # Print the version of DesugaredMethods.",
+ " --help # Print this message.",
+ " --desugared-lib <file> # Specify desugared library configuration.",
+ " # <file> is a desugared library configuration (json).",
+ " --android-platform-build",
+ " # Compile as a platform build where the"
+ + " runtime/bootclasspath",
+ " # is assumed to be the version specified by --min-api.",
+ " --desugared-lib-jar <file>",
+ " # Specify desugared library jar."),
+ DesugaredMethodsListCommand.getUsageMessage());
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/relocator/RelocatorCommandTest.java b/src/test/java/com/android/tools/r8/relocator/RelocatorCommandTest.java
index 6766ee7..6c46829 100644
--- a/src/test/java/com/android/tools/r8/relocator/RelocatorCommandTest.java
+++ b/src/test/java/com/android/tools/r8/relocator/RelocatorCommandTest.java
@@ -22,6 +22,7 @@
import com.android.tools.r8.references.PackageReference;
import com.android.tools.r8.references.Reference;
import com.android.tools.r8.utils.ArchiveResourceProvider;
+import com.android.tools.r8.utils.internal.StringUtils;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
@@ -46,6 +47,26 @@
}
@Test
+ public void testHelpMessage() {
+ assertEquals(
+ StringUtils.joinLines(
+ "The Relocator CLI is EXPERIMENTAL and is subject to change",
+ "Usage: relocator [options]",
+ " where options are:",
+ " --input <file> # Input file to remap, class, zip or jar.",
+ " --input-no-res <file> # Input file to remap, zip or jar.",
+ " # Only .class file entries are included.",
+ " --output <file> # Output result in <outfile>.",
+ " --map <from->to> # Registers a mapping.",
+ " --map-diagnostics [:<type>] <from-level> <to-level>",
+ " # Map diagnostics level.",
+ " --thread-count <number> # A specified number of threads to run with.",
+ " --version # Print the version of d8.",
+ " --help # Print this message."),
+ RelocatorCommand.USAGE_MESSAGE);
+ }
+
+ @Test
public void testCommandBuilder() throws CompilationFailedException, IOException {
Path input1 = temp.newFile("in1.jar").toPath();
Path input2 = temp.newFile("in2.jar").toPath();
diff --git a/src/test/java/com/android/tools/r8/retrace/PartitionCommandParserTest.java b/src/test/java/com/android/tools/r8/retrace/PartitionCommandParserTest.java
new file mode 100644
index 0000000..de95ad3
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/retrace/PartitionCommandParserTest.java
@@ -0,0 +1,43 @@
+// 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.retrace;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.internal.StringUtils;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class PartitionCommandParserTest extends TestBase {
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withNoneRuntime().build();
+ }
+
+ public PartitionCommandParserTest(TestParameters parameters) {
+ parameters.assertNoneRuntime();
+ }
+
+ @Test
+ public void testHelpMessage() {
+ assertEquals(
+ StringUtils.lines(
+ "Usage: partition [options] <proguard-map>",
+ " where <proguard-map> is a generated mapping file and options are:",
+ "",
+ " --output <partition-map>",
+ " # Output destination of partitioned map.",
+ " --version # Print the version.",
+ " --help",
+ " -h # Print this message."),
+ PartitionCommandParser.getUsageMessage());
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/retrace/RetraceCommandLineTests.java b/src/test/java/com/android/tools/r8/retrace/RetraceCommandLineTests.java
index 60d2e23..2fe5bb5 100644
--- a/src/test/java/com/android/tools/r8/retrace/RetraceCommandLineTests.java
+++ b/src/test/java/com/android/tools/r8/retrace/RetraceCommandLineTests.java
@@ -194,6 +194,27 @@
}
@Test
+ public void testHelpMessage() {
+ assertEquals(
+ StringUtils.lines(
+ "Usage: retrace [options] <proguard-map> [stack-trace-file] where <proguard-map> is a"
+ + " generated mapping file and options are:",
+ " --regex <regexp>",
+ " --r <regexp> # Regular expression for parsing stack-trace-file as lines",
+ " --verbose # Get verbose retraced output",
+ " --info # Write information messages to stdout",
+ " --quiet # Silence ordinary messages printed to stdout",
+ " --verify-mapping-file-hash",
+ " # Verify the mapping file hash",
+ " --partition-map <file>",
+ " --p <file> # Partition map to use",
+ " --help",
+ " -h # Print this message.",
+ " --version # Print the version."),
+ Retrace.getUsageMessage());
+ }
+
+ @Test
public void testVersion() throws Exception {
ProcessResult processResult = runRetraceCommandLine(null, Arrays.asList("--version"));
assertEquals(0, processResult.exitCode);
diff --git a/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesCommandTest.java b/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesCommandTest.java
index 5501710..9dd6eef 100644
--- a/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesCommandTest.java
+++ b/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesCommandTest.java
@@ -66,6 +66,44 @@
TraceReferencesCommand.builder().build();
}
+ @Test
+ public void testHelpMessage() {
+ assertEquals(
+ StringUtils.lines(
+ "Usage: tracereferences <command> [<options>] [@<argfile>]",
+ " Where <command> is one of:",
+ " --check # Run emitting only diagnostics messages.",
+ " --keep-rules [<keep-rules-options>]",
+ " # Traced references will be output in the keep-rules",
+ " # format.",
+ " --natives # EXPERIMENTAL.",
+ " and each <argfile> is a file containing additional options (one per line)",
+ " and options are:",
+ " --lib <file|jdk-home> # Add <file|jdk-home> runtime library.",
+ " --source <file> # Add <file> as a source for tracing references.",
+ " --target <file> # Add <file> as a target for tracing references. When",
+ " # target is not specified all references from source",
+ " # outside of library are treated as a missing",
+ " # references.",
+ " --output <file> # Output result in <outfile>. If not passed the",
+ " # result will go to standard out.",
+ " --map-diagnostics[:<type>] <from-level> <to-level>",
+ " # Map diagnostics of <type> (default any) reported as",
+ " # <from-level> to <to-level> where <from-level> and",
+ " # <to-level> are one of 'info', 'warning', or 'error'",
+ " # and the optional <type> is either the simple or",
+ " # fully qualified Java type name of a diagnostic.",
+ " # If <type> is unspecified, all diagnostics at ",
+ " # <from-level> will be mapped.",
+ " # Note that fatal compiler errors cannot be mapped.",
+ " --version # Print the version of tracereferences.",
+ " --help # Print this message.",
+ " and <keep-rule-options> are:",
+ " --allowobfuscation # Output keep rules with the allowobfuscation",
+ " # modifier (defaults to rules without the modifier)"),
+ TraceReferencesCommandParser.getUsageMessage());
+ }
+
@Test(expected = CompilationFailedException.class)
public void emptyRun() throws Throwable {
DiagnosticsChecker.checkErrorsContains(