From 36baba6ad28ad6eb8b8affdd34da7e6bd27b11e3 Mon Sep 17 00:00:00 2001 From: Ben Lee Date: Thu, 16 Jul 2026 00:46:50 +0100 Subject: [PATCH 1/7] Replace the slow script_tests with repo-level assertions --- .bazelignore | 1 - .github/workflows/ci.bazelrc | 2 - .github/workflows/ci.yaml | 4 +- .gitignore | 2 +- CONTRIBUTING.md | 30 +- MODULE.bazel | 12 +- docs/rules.md | 2 - {examples => e2e}/simple-android/.bazelrc | 0 .../simple-android/AndroidManifest.xml | 0 {examples => e2e}/simple-android/BUILD.bazel | 0 {examples => e2e}/simple-android/Foo.java | 0 {examples => e2e}/simple-android/MODULE.bazel | 2 +- e2e/simple-android/README.md | 9 + .../simple-android/WORKSPACE.bazel | 0 .../lib_lint_test_lint_baseline.xml | 0 {examples => e2e}/simple-android/lint.xml | 0 .../simple-android/lint_global.xml | 0 maven_install.json | 103 +-- maven_install_test.json | 8 +- renovate.json | 4 +- src/BUILD.bazel | 8 +- src/android_lint_output_validator.py | 34 +- tests/BUILD.bazel | 19 +- tests/bashunit/BUILD | 13 - tests/bashunit/unittest.bash | 845 ------------------ tests/bashunit/unittest_utils.sh | 181 ---- tests/integration/BUILD.bazel | 167 ++++ tests/integration/fixture_aar.bzl | 39 + tests/integration/fixture_aar_builder.py | 36 + .../fixtures/aar/AndroidManifest.xml | 2 + tests/integration/fixtures/basic/Clean.java | 7 + tests/integration/fixtures/basic/Dirty.java | 7 + .../fixtures/basic/DirtyBaseline.xml | 22 + .../androidx/compose/runtime/Composable.kt | 9 + .../compose/androidx/compose/ui/Modifier.kt | 5 + .../compose/com/example/CleanComposables.kt | 9 + .../compose/com/example/DirtyComposables.kt | 9 + .../fixtures/compose/com/example/Stubs.kt | 8 + tests/integration/lint_report_test.bzl | 39 + tests/integration/report_assertion.py | 45 + tests/rules/BUILD.bazel | 19 + tests/rules/Fixture.java | 7 + tests/rules/android_lint_analysis_test.bzl | 74 ++ tests/scripts/BUILD | 53 -- .../android_lint_aar_scenarios_test.sh | 50 -- tests/scripts/android_lint_scenarios_test.sh | 140 --- tests/scripts/lint_helper.sh | 398 --------- tests/src/BUILD.bazel | 12 + .../src/android_lint_output_validator_test.py | 70 ++ tests/src/cli/AndroidLintRunnerTest.kt | 95 ++ tests/src/cli/BUILD | 20 + 51 files changed, 817 insertions(+), 1804 deletions(-) rename {examples => e2e}/simple-android/.bazelrc (100%) rename {examples => e2e}/simple-android/AndroidManifest.xml (100%) rename {examples => e2e}/simple-android/BUILD.bazel (100%) rename {examples => e2e}/simple-android/Foo.java (100%) rename {examples => e2e}/simple-android/MODULE.bazel (97%) create mode 100644 e2e/simple-android/README.md rename {examples => e2e}/simple-android/WORKSPACE.bazel (100%) rename {examples => e2e}/simple-android/lib_lint_test_lint_baseline.xml (100%) rename {examples => e2e}/simple-android/lint.xml (100%) rename {examples => e2e}/simple-android/lint_global.xml (100%) delete mode 100644 tests/bashunit/BUILD delete mode 100644 tests/bashunit/unittest.bash delete mode 100644 tests/bashunit/unittest_utils.sh create mode 100644 tests/integration/BUILD.bazel create mode 100644 tests/integration/fixture_aar.bzl create mode 100644 tests/integration/fixture_aar_builder.py create mode 100644 tests/integration/fixtures/aar/AndroidManifest.xml create mode 100644 tests/integration/fixtures/basic/Clean.java create mode 100644 tests/integration/fixtures/basic/Dirty.java create mode 100644 tests/integration/fixtures/basic/DirtyBaseline.xml create mode 100644 tests/integration/fixtures/compose/androidx/compose/runtime/Composable.kt create mode 100644 tests/integration/fixtures/compose/androidx/compose/ui/Modifier.kt create mode 100644 tests/integration/fixtures/compose/com/example/CleanComposables.kt create mode 100644 tests/integration/fixtures/compose/com/example/DirtyComposables.kt create mode 100644 tests/integration/fixtures/compose/com/example/Stubs.kt create mode 100644 tests/integration/lint_report_test.bzl create mode 100644 tests/integration/report_assertion.py create mode 100644 tests/rules/BUILD.bazel create mode 100644 tests/rules/Fixture.java create mode 100644 tests/rules/android_lint_analysis_test.bzl delete mode 100644 tests/scripts/BUILD delete mode 100755 tests/scripts/android_lint_aar_scenarios_test.sh delete mode 100755 tests/scripts/android_lint_scenarios_test.sh delete mode 100755 tests/scripts/lint_helper.sh create mode 100644 tests/src/BUILD.bazel create mode 100644 tests/src/android_lint_output_validator_test.py create mode 100644 tests/src/cli/AndroidLintRunnerTest.kt diff --git a/.bazelignore b/.bazelignore index 7bd5d5c..b1e3af6 100644 --- a/.bazelignore +++ b/.bazelignore @@ -1,2 +1 @@ e2e/ -examples/ diff --git a/.github/workflows/ci.bazelrc b/.github/workflows/ci.bazelrc index 3b4aad2..40f1b4c 100644 --- a/.github/workflows/ci.bazelrc +++ b/.github/workflows/ci.bazelrc @@ -11,5 +11,3 @@ build --disk_cache=~/.cache/bazel # Don't rely on test logs being easily accessible from the test runner, # though it makes the log noisier. test --test_output=errors -# Allows tests to run bazelisk-in-bazel, since this is the cache folder used -test --test_env=XDG_CACHE_HOME diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d7b01c7..20118e1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -19,6 +19,7 @@ concurrency: jobs: tests: strategy: + fail-fast: false matrix: os: [ubuntu-latest] bazel_version: ["8.x", "9.x"] @@ -41,9 +42,10 @@ jobs: run: bazel test //... integration-tests: strategy: + fail-fast: false matrix: os: [ubuntu-latest] - directory: [examples/simple-android] + directory: [e2e/simple-android] strategy: [local, worker] bazel_version: ["8.x", "9.x"] runs-on: ${{ matrix.os }} diff --git a/.gitignore b/.gitignore index 6e816bc..d85524f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ bazel-* .idea/ .ijwb/ .aswb -MODULE.bazel.lock \ No newline at end of file +MODULE.bazel.lock diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 25cade4..438851c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,11 +13,39 @@ pre-commit install Otherwise later tooling on CI will yell at you about formatting/linting violations. +## Testing + +Run the complete repository test suite with: + +```shell +bazel test //... +``` + +For a focused run of the lightweight unit and analysis tests, use: + +```shell +bazel test //tests:unit_tests +``` + +The rule behavior tests under `tests/integration` are part of the normal suite. Run them directly +with: + +```shell +bazel test //tests/integration:tests +``` + +The broader end-to-end test uses a standalone consumer workspace: + +```shell +cd e2e/simple-android +bazel test //... +``` + ## Using this as a development dependency of other rules You'll commonly find that you develop in another WORKSPACE, such as some other ruleset that depends on rules_android_lint, or in a nested -WORKSPACE in the integration_tests folder. +WORKSPACE under the `e2e` directory. To always tell Bazel to use this directory rather than some release artifact or a version fetched from the internet, run this from this diff --git a/MODULE.bazel b/MODULE.bazel index 891b325..5840923 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -22,16 +22,6 @@ bazel_dep(name = "protobuf", version = "33.4", repo_name = "com_google_protobuf" bazel_dep(name = "aspect_bazel_lib", version = "2.22.5", dev_dependency = True) bazel_dep(name = "buildifier_prebuilt", version = "8.2.1.2", dev_dependency = True) -bazel_dep(name = "rules_bazel_integration_test", version = "0.37.1", dev_dependency = True) -bazel_dep(name = "rules_shell", version = "0.6.1", dev_dependency = True) - -bazel_binaries = use_extension( - "@rules_bazel_integration_test//:extensions.bzl", - "bazel_binaries", - dev_dependency = True, -) -bazel_binaries.download(version_file = "//:.bazelversion") -use_repo(bazel_binaries, "bazel_binaries", "bazel_binaries_bazelisk", "build_bazel_bazel_.bazelversion") android = use_extension( "@hermetic_android_toolchains//:extensions.bzl", @@ -94,7 +84,7 @@ maven_test.install( artifacts = [ "org.assertj:assertj-core:3.24.2", "junit:junit:4.13.2", - # Custom lint check used as a test fixture by //tests/scripts + # Custom lint check used by the top-level integration fixtures. "com.slack.lint.compose:compose-lint-checks:1.5.2", ], lock_file = "//:maven_install_test.json", diff --git a/docs/rules.md b/docs/rules.md index 237ad0b..733cb58 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -87,5 +87,3 @@ Info needed to evaluate lint results | Name | Description | | :------------- | :------------- | | output | The Android Lint baseline output | - - diff --git a/examples/simple-android/.bazelrc b/e2e/simple-android/.bazelrc similarity index 100% rename from examples/simple-android/.bazelrc rename to e2e/simple-android/.bazelrc diff --git a/examples/simple-android/AndroidManifest.xml b/e2e/simple-android/AndroidManifest.xml similarity index 100% rename from examples/simple-android/AndroidManifest.xml rename to e2e/simple-android/AndroidManifest.xml diff --git a/examples/simple-android/BUILD.bazel b/e2e/simple-android/BUILD.bazel similarity index 100% rename from examples/simple-android/BUILD.bazel rename to e2e/simple-android/BUILD.bazel diff --git a/examples/simple-android/Foo.java b/e2e/simple-android/Foo.java similarity index 100% rename from examples/simple-android/Foo.java rename to e2e/simple-android/Foo.java diff --git a/examples/simple-android/MODULE.bazel b/e2e/simple-android/MODULE.bazel similarity index 97% rename from examples/simple-android/MODULE.bazel rename to e2e/simple-android/MODULE.bazel index 0581585..2a81fb8 100644 --- a/examples/simple-android/MODULE.bazel +++ b/e2e/simple-android/MODULE.bazel @@ -1,5 +1,5 @@ module( - name = "examples_simple", + name = "e2e_simple_android", version = "TODO", ) diff --git a/e2e/simple-android/README.md b/e2e/simple-android/README.md new file mode 100644 index 0000000..b2948b0 --- /dev/null +++ b/e2e/simple-android/README.md @@ -0,0 +1,9 @@ +# Simple Android end-to-end test + +This standalone Bazel workspace verifies that a consumer can register the Android lint toolchain and use `android_lint` and `android_lint_test` with a real Android dependency. CI runs it with both local and worker execution strategies. + +Run it from this directory: + +```sh +bazel test //... +``` diff --git a/examples/simple-android/WORKSPACE.bazel b/e2e/simple-android/WORKSPACE.bazel similarity index 100% rename from examples/simple-android/WORKSPACE.bazel rename to e2e/simple-android/WORKSPACE.bazel diff --git a/examples/simple-android/lib_lint_test_lint_baseline.xml b/e2e/simple-android/lib_lint_test_lint_baseline.xml similarity index 100% rename from examples/simple-android/lib_lint_test_lint_baseline.xml rename to e2e/simple-android/lib_lint_test_lint_baseline.xml diff --git a/examples/simple-android/lint.xml b/e2e/simple-android/lint.xml similarity index 100% rename from examples/simple-android/lint.xml rename to e2e/simple-android/lint.xml diff --git a/examples/simple-android/lint_global.xml b/e2e/simple-android/lint_global.xml similarity index 100% rename from examples/simple-android/lint_global.xml rename to e2e/simple-android/lint_global.xml diff --git a/maven_install.json b/maven_install.json index 4255a60..e59ab4a 100755 --- a/maven_install.json +++ b/maven_install.json @@ -524,12 +524,8 @@ "net.java.dev.jna:jna-platform", "org.jetbrains.kotlin:kotlin-stdlib-jdk8" ], - "com.android.tools:dvlib": [ - "com.android.tools:common" - ], - "com.android.tools:play-sdk-proto": [ - "com.google.protobuf:protobuf-java" - ], + "com.android.tools:dvlib": ["com.android.tools:common"], + "com.android.tools:play-sdk-proto": ["com.google.protobuf:protobuf-java"], "com.android.tools:repository": [ "com.android.tools.analytics-library:shared", "com.android.tools:common", @@ -580,9 +576,7 @@ "com.google.j2objc:j2objc-annotations", "org.checkerframework:checker-qual" ], - "com.google.jimfs:jimfs": [ - "com.google.guava:guava" - ], + "com.google.jimfs:jimfs": ["com.google.guava:guava"], "com.sun.istack:istack-commons-runtime": [ "jakarta.activation:jakarta.activation-api" ], @@ -590,15 +584,11 @@ "com.xenomachina:xenocom", "org.jetbrains.kotlin:kotlin-stdlib" ], - "com.xenomachina:xenocom": [ - "org.jetbrains.kotlin:kotlin-stdlib" - ], + "com.xenomachina:xenocom": ["org.jetbrains.kotlin:kotlin-stdlib"], "jakarta.xml.bind:jakarta.xml.bind-api": [ "jakarta.activation:jakarta.activation-api" ], - "net.java.dev.jna:jna-platform": [ - "net.java.dev.jna:jna" - ], + "net.java.dev.jna:jna-platform": ["net.java.dev.jna:jna"], "org.apache.commons:commons-compress": [ "commons-codec:commons-codec", "commons-io:commons-io", @@ -616,9 +606,7 @@ "org.bouncycastle:bcprov-jdk18on", "org.bouncycastle:bcutil-jdk18on" ], - "org.bouncycastle:bcutil-jdk18on": [ - "org.bouncycastle:bcprov-jdk18on" - ], + "org.bouncycastle:bcutil-jdk18on": ["org.bouncycastle:bcprov-jdk18on"], "org.glassfish.jaxb:jaxb-runtime": [ "com.sun.istack:istack-commons-runtime", "com.sun.xml.fastinfoset:FastInfoset", @@ -630,9 +618,7 @@ "org.jetbrains.kotlin:kotlin-reflect": [ "org.jetbrains.kotlin:kotlin-stdlib" ], - "org.jetbrains.kotlin:kotlin-stdlib": [ - "org.jetbrains:annotations" - ], + "org.jetbrains.kotlin:kotlin-stdlib": ["org.jetbrains:annotations"], "org.jetbrains.kotlin:kotlin-stdlib-jdk7": [ "org.jetbrains.kotlin:kotlin-stdlib" ], @@ -651,16 +637,9 @@ "jakarta.activation:jakarta.activation-api", "jakarta.xml.bind:jakarta.xml.bind-api" ], - "org.ow2.asm:asm-analysis": [ - "org.ow2.asm:asm-tree" - ], - "org.ow2.asm:asm-commons": [ - "org.ow2.asm:asm", - "org.ow2.asm:asm-tree" - ], - "org.ow2.asm:asm-tree": [ - "org.ow2.asm:asm" - ] + "org.ow2.asm:asm-analysis": ["org.ow2.asm:asm-tree"], + "org.ow2.asm:asm-commons": ["org.ow2.asm:asm", "org.ow2.asm:asm-tree"], + "org.ow2.asm:asm-tree": ["org.ow2.asm:asm"] }, "packages": { "com.android.tools.analytics-library:protos": [ @@ -688,9 +667,7 @@ "com.android.builder.model.v2.models", "com.android.builder.model.v2.models.ndk" ], - "com.android.tools.build:manifest-merger": [ - "com.android.manifmerger" - ], + "com.android.tools.build:manifest-merger": ["com.android.manifmerger"], "com.android.tools.ddms:ddmlib": [ "com.android.commands.am", "com.android.ddmlib", @@ -2250,9 +2227,7 @@ "com.android.tools.lint.checks.optional", "com.android.tools.lint.checks.plurals" ], - "com.android.tools.lint:lint-model": [ - "com.android.tools.lint.model" - ], + "com.android.tools.lint:lint-model": ["com.android.tools.lint.model"], "com.android.tools:annotations": [ "com.android.annotations", "com.android.annotations.concurrency" @@ -2283,12 +2258,8 @@ "com.android.xml", "com.android.xml.sax" ], - "com.android.tools:dvlib": [ - "com.android.dvlib" - ], - "com.android.tools:play-sdk-proto": [ - "com.android.tools.lint.checks" - ], + "com.android.tools:dvlib": ["com.android.dvlib"], + "com.android.tools:play-sdk-proto": ["com.android.tools.lint.checks"], "com.android.tools:repository": [ "com.android.repository", "com.android.repository.api", @@ -2431,12 +2402,8 @@ "com.google.common.xml", "com.google.thirdparty.publicsuffix" ], - "com.google.j2objc:j2objc-annotations": [ - "com.google.j2objc.annotations" - ], - "com.google.jimfs:jimfs": [ - "com.google.common.jimfs" - ], + "com.google.j2objc:j2objc-annotations": ["com.google.j2objc.annotations"], + "com.google.jimfs:jimfs": ["com.google.common.jimfs"], "com.google.protobuf:protobuf-java": [ "com.google.protobuf", "com.google.protobuf.compiler" @@ -2465,9 +2432,7 @@ "org.jvnet.fastinfoset.sax.helpers", "org.jvnet.fastinfoset.stax" ], - "com.xenomachina:kotlin-argparser": [ - "com.xenomachina.argparser" - ], + "com.xenomachina:kotlin-argparser": ["com.xenomachina.argparser"], "com.xenomachina:xenocom": [ "com.xenomachina.common", "com.xenomachina.text", @@ -2503,9 +2468,7 @@ "org.apache.commons.logging", "org.apache.commons.logging.impl" ], - "jakarta.activation:jakarta.activation-api": [ - "javax.activation" - ], + "jakarta.activation:jakarta.activation-api": ["javax.activation"], "jakarta.xml.bind:jakarta.xml.bind-api": [ "javax.xml.bind", "javax.xml.bind.annotation", @@ -2514,9 +2477,7 @@ "javax.xml.bind.helpers", "javax.xml.bind.util" ], - "javax.inject:javax.inject": [ - "javax.inject" - ], + "javax.inject:javax.inject": ["javax.inject"], "net.java.dev.jna:jna": [ "com.sun.jna", "com.sun.jna.internal", @@ -3151,23 +3112,11 @@ "org.intellij.lang.annotations", "org.jetbrains.annotations" ], - "org.jvnet.staxex:stax-ex": [ - "org.jvnet.staxex", - "org.jvnet.staxex.util" - ], - "org.ow2.asm:asm": [ - "org.objectweb.asm", - "org.objectweb.asm.signature" - ], - "org.ow2.asm:asm-analysis": [ - "org.objectweb.asm.tree.analysis" - ], - "org.ow2.asm:asm-commons": [ - "org.objectweb.asm.commons" - ], - "org.ow2.asm:asm-tree": [ - "org.objectweb.asm.tree" - ] + "org.jvnet.staxex:stax-ex": ["org.jvnet.staxex", "org.jvnet.staxex.util"], + "org.ow2.asm:asm": ["org.objectweb.asm", "org.objectweb.asm.signature"], + "org.ow2.asm:asm-analysis": ["org.objectweb.asm.tree.analysis"], + "org.ow2.asm:asm-commons": ["org.objectweb.asm.commons"], + "org.ow2.asm:asm-tree": ["org.objectweb.asm.tree"] }, "repositories": { "https://maven.google.com/": [ @@ -3418,9 +3367,7 @@ ] }, "org.glassfish.jaxb:jaxb-runtime": { - "javax.xml.bind.JAXBContext": [ - "com.sun.xml.bind.v2.ContextFactory" - ] + "javax.xml.bind.JAXBContext": ["com.sun.xml.bind.v2.ContextFactory"] }, "org.jetbrains.kotlin:kotlin-reflect": { "kotlin.reflect.jvm.internal.impl.builtins.BuiltInsLoader": [ diff --git a/maven_install_test.json b/maven_install_test.json index 93fbf3d..e150496 100755 --- a/maven_install_test.json +++ b/maven_install_test.json @@ -44,12 +44,8 @@ "com.slack.lint.compose:compose-lint-checks": [ "com.google.auto.service:auto-service-annotations" ], - "junit:junit": [ - "org.hamcrest:hamcrest-core" - ], - "org.assertj:assertj-core": [ - "net.bytebuddy:byte-buddy" - ] + "junit:junit": ["org.hamcrest:hamcrest-core"], + "org.assertj:assertj-core": ["net.bytebuddy:byte-buddy"] }, "packages": { "com.google.auto.service:auto-service-annotations": [ diff --git a/renovate.json b/renovate.json index b9f83e8..135a4fc 100644 --- a/renovate.json +++ b/renovate.json @@ -10,9 +10,7 @@ ], "packageRules": [ { - "matchFileNames": [ - "MODULE.bazel" - ], + "matchFileNames": ["MODULE.bazel"], "enabled": false } ] diff --git a/src/BUILD.bazel b/src/BUILD.bazel index b066f3f..a13ad2d 100644 --- a/src/BUILD.bazel +++ b/src/BUILD.bazel @@ -1,4 +1,10 @@ -load("@rules_python//python:defs.bzl", "py_binary") +load("@rules_python//python:defs.bzl", "py_binary", "py_library") + +py_library( + name = "android_lint_output_validator_lib", + srcs = ["android_lint_output_validator.py"], + visibility = ["//tests:__subpackages__"], +) py_binary( name = "android_lint_output_validator", diff --git a/src/android_lint_output_validator.py b/src/android_lint_output_validator.py index fa1c111..7954864 100755 --- a/src/android_lint_output_validator.py +++ b/src/android_lint_output_validator.py @@ -5,13 +5,19 @@ from typing import List from xml.etree import ElementTree -def __validate_output(lint_baseline) -> int: + +def validate_output(lint_baseline: str) -> int: # Read in the lint baseline file with open(lint_baseline, "r") as f: output = f.read() - # Parse it into an actual array of issues - output_parsed = __parse_android_lint(output) + # Parse it into an actual array of issues. A missing or malformed report must + # fail the test rather than being mistaken for a clean lint result. + try: + output_parsed = parse_android_lint(output) + except (ElementTree.ParseError, ValueError) as error: + print(f"Unable to parse Android lint report {lint_baseline}: {error}", file=sys.stderr) + return 1 # Look at the flagged issues and categorize them # issues - valid issues that need to be fixed or baselined @@ -35,22 +41,24 @@ def __validate_output(lint_baseline) -> int: return 0 -def __parse_android_lint(input: str) -> List[Dict[str, Any]]: +def parse_android_lint(report: str) -> List[Dict[str, Any]]: results = [] # type: List[Dict[str, Any]] - try: - root = ElementTree.fromstring(input) - except ElementTree.ParseError: - return results + root = ElementTree.fromstring(report) + if root.tag != "issues": + raise ValueError(f"expected root element, found <{root.tag}>") for issue in root.findall("issue"): - issues = {"id": issue.get("id", None), "message": issue.get("message", None)} - results.append({k: v for k, v in issues.items() if v}) + issue_id = issue.get("id") + message = issue.get("message") + if not issue_id or message is None: + raise ValueError("expected each to have id and message attributes") + results.append({"id": issue_id, "message": message}) return results -def __main(lint_baseline): - return __validate_output(lint_baseline=lint_baseline) +def main(lint_baseline: str) -> int: + return validate_output(lint_baseline=lint_baseline) if __name__ == "__main__": @@ -60,7 +68,7 @@ def __main(lint_baseline): args = parser.parse_args(sys.argv[1:]) # Run it and exit with the proper exit code - exit_code = __main( + exit_code = main( lint_baseline=args.lint_baseline, ) sys.exit(exit_code) diff --git a/tests/BUILD.bazel b/tests/BUILD.bazel index aec1916..040f863 100644 --- a/tests/BUILD.bazel +++ b/tests/BUILD.bazel @@ -3,12 +3,25 @@ load(":versions_test.bzl", "versions_test_suite") versions_test_suite(name = "versions_test") test_suite( - name = "tests", + name = "unit_tests", tests = [ ":versions_test", - "//tests/scripts:android_lint_aar_scenarios_test", - "//tests/scripts:android_lint_scenarios_test", + "//tests/rules:tests", + "//tests/src:tests", "//tests/src/cli:tests", "//tests/src/worker:tests", ], ) + +test_suite( + name = "rule_tests", + tests = ["//tests/integration:tests"], +) + +test_suite( + name = "tests", + tests = [ + ":rule_tests", + ":unit_tests", + ], +) diff --git a/tests/bashunit/BUILD b/tests/bashunit/BUILD deleted file mode 100644 index da766d0..0000000 --- a/tests/bashunit/BUILD +++ /dev/null @@ -1,13 +0,0 @@ -# Vendored from rules_android's test/bashunit (Apache 2.0), which itself vendors -# Bazel's unittest.bash. -load("@rules_shell//shell:sh_library.bzl", "sh_library") - -sh_library( - name = "bashunit", - testonly = True, - srcs = [ - "unittest.bash", - "unittest_utils.sh", - ], - visibility = ["//tests:__subpackages__"], -) diff --git a/tests/bashunit/unittest.bash b/tests/bashunit/unittest.bash deleted file mode 100644 index c88ba2c..0000000 --- a/tests/bashunit/unittest.bash +++ /dev/null @@ -1,845 +0,0 @@ -#!/bin/bash -# -# Copyright 2015 The Bazel Authors. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# Common utility file for Bazel shell tests -# -# unittest.bash: a unit test framework in Bash. -# -# A typical test suite looks like so: -# -# ------------------------------------------------------------------------ -# #!/bin/bash -# -# source path/to/unittest.bash || exit 1 -# -# # Test that foo works. -# function test_foo() { -# foo >$TEST_log || fail "foo failed"; -# expect_log "blah" "Expected to see 'blah' in output of 'foo'." -# } -# -# # Test that bar works. -# function test_bar() { -# bar 2>$TEST_log || fail "bar failed"; -# expect_not_log "ERROR" "Unexpected error from 'bar'." -# ... -# assert_equals $x $y -# } -# -# run_suite "Test suite for blah" -# ------------------------------------------------------------------------ -# -# Each test function is considered to pass iff fail() is not called -# while it is active. fail() may be called directly, or indirectly -# via other assertions such as expect_log(). run_suite must be called -# at the very end. -# -# A test suite may redefine functions "set_up" and/or "tear_down"; -# these functions are executed before and after each test function, -# respectively. Similarly, "cleanup" and "timeout" may be redefined, -# and these function are called upon exit (of any kind) or a timeout. -# -# The user can pass --test_filter to blaze test to select specific tests -# to run with Bash globs. A union of tests matching any of the provided globs -# will be run. Additionally the user may define TESTS=(test_foo test_bar ...) to -# specify a subset of test functions to execute, for example, a working set -# during debugging. By default, all functions called test_* will be executed. -# -# This file provides utilities for assertions over the output of a -# command. The output of the command under test is directed to the -# file $TEST_log, and then the expect_log* assertions can be used to -# test for the presence of certain regular expressions in that file. -# -# The test framework is responsible for restoring the original working -# directory before each test. -# -# The order in which test functions are run is not defined, so it is -# important that tests clean up after themselves. -# -# Each test will be run in a new subshell. -# -# Functions named __* are not intended for use by clients. -# -# This framework implements the "test sharding protocol". -# - -[[ -n "$BASH_VERSION" ]] || - { echo "unittest.bash only works with bash!" >&2; exit 1; } - -export BAZEL_SHELL_TEST=1 - -DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) - -# Load the environment support utilities. -source "${DIR}/unittest_utils.sh" || { echo "unittest_utils.sh not found" >&2; exit 1; } - -#### Global variables: - -TEST_name="" # The name of the current test. - -TEST_log=$TEST_TMPDIR/log # The log file over which the - # expect_log* assertions work. Must - # be absolute to be robust against - # tests invoking 'cd'! - -TEST_passed="true" # The result of the current test; - # failed assertions cause this to - # become false. - -# These variables may be overridden by the test suite: - -TESTS=() # A subset or "working set" of test - # functions that should be run. By - # default, all tests called test_* are - # run. - -_TEST_FILTERS=() # List of globs to use to filter the tests. - # If non-empty, all tests matching at least one - # of the globs are run and test list provided in - # the arguments is ignored if present. - -__in_tear_down=0 # Indicates whether we are in `tear_down` phase - # of test. Used to avoid re-entering `tear_down` - # on failures within it. - -if (( $# > 0 )); then - ( - IFS=':' - echo "WARNING: Passing test names in arguments (--test_arg) is deprecated, please use --test_filter='$*' instead." >&2 - ) - - # Legacy behavior is to ignore missing regexp, but with errexit - # the following line fails without || true. - # TODO(dmarting): maybe we should revisit the way of selecting - # test with that framework (use Bazel's environment variable instead). - TESTS=($(for i in "$@"; do echo $i; done | grep ^test_ || true)) - if (( ${#TESTS[@]} == 0 )); then - echo "WARNING: Arguments do not specify tests!" >&2 - fi -fi -# TESTBRIDGE_TEST_ONLY contains the value of --test_filter, if any. We want to -# preferentially use that instead of $@ to determine which tests to run. -if [[ ${TESTBRIDGE_TEST_ONLY:-} != "" ]]; then - if (( ${#TESTS[@]} != 0 )); then - echo "WARNING: Both --test_arg and --test_filter specified, ignoring --test_arg" >&2 - TESTS=() - fi - # Split TESTBRIDGE_TEST_ONLY on colon and store it in `_TEST_FILTERS` array. - IFS=':' read -r -a _TEST_FILTERS <<< "$TESTBRIDGE_TEST_ONLY" -fi - -TEST_verbose="true" # Whether or not to be verbose. A - # command; "true" or "false" are - # acceptable. The default is: true. - -TEST_script="$0" # Full path to test script -# Check if the script path is absolute, if not prefix the PWD. -if [[ ! "$TEST_script" = /* ]]; then - TEST_script="${PWD}/$0" -fi - - -#### Internal functions - -function __show_log() { - echo "-- Test log: -----------------------------------------------------------" - [[ -e $TEST_log ]] && cat "$TEST_log" || echo "(Log file did not exist.)" - echo "------------------------------------------------------------------------" -} - -# Usage: __pad <pad-char> -# Print $title padded to 80 columns with $pad_char. -function __pad() { - local title=$1 - local pad=$2 - # Ignore the subshell error -- `head` closes the fd before reading to the - # end, therefore the subshell will get SIGPIPE while stuck in `write`. - { - echo -n "${pad}${pad} ${title} " - printf "%80s" " " | tr ' ' "$pad" - } | head -c 80 || true - echo -} - -#### Exported functions - -# Usage: init_test ... -# Deprecated. Has no effect. -function init_test() { - : -} - - -# Usage: set_up -# Called before every test function. May be redefined by the test suite. -function set_up() { - : -} - -# Usage: tear_down -# Called after every test function. May be redefined by the test suite. -function tear_down() { - : -} - -# Usage: cleanup -# Called upon eventual exit of the test suite. May be redefined by -# the test suite. -function cleanup() { - : -} - -# Usage: timeout -# Called upon early exit from a test due to timeout. -function timeout() { - : -} - -# Usage: testenv_set_up -# Called prior to set_up. For use by testenv.sh. -function testenv_set_up() { - : -} - -# Usage: testenv_tear_down -# Called after tear_down. For use by testenv.sh. -function testenv_tear_down() { - : -} - -# Usage: fail <message> [<message> ...] -# Print failure message with context information, and mark the test as -# a failure. The context includes a stacktrace including the longest sequence -# of calls outside this module. (We exclude the top and bottom portions of -# the stack because they just add noise.) Also prints the contents of -# $TEST_log. -function fail() { - __show_log >&2 - echo "${TEST_name} FAILED: $*." >&2 - # Keep the original error message if we fail in `tear_down` after a failure. - [[ "${TEST_passed}" == "true" ]] && echo "$@" >"$TEST_TMPDIR"/__fail - TEST_passed="false" - __show_stack - # Cleanup as we are leaving the subshell now - __run_tear_down_after_failure - exit 1 -} - -function __run_tear_down_after_failure() { - # Skip `tear_down` after a failure in `tear_down` to prevent infinite - # recursion. - (( __in_tear_down )) && return - __in_tear_down=1 - echo -e "\nTear down:\n" >&2 - tear_down - testenv_tear_down -} - -# Usage: warn <message> -# Print a test warning with context information. -# The context includes a stacktrace including the longest sequence -# of calls outside this module. (We exclude the top and bottom portions of -# the stack because they just add noise.) -function warn() { - __show_log >&2 - echo "${TEST_name} WARNING: $1." >&2 - __show_stack - - if [[ -n "${TEST_WARNINGS_OUTPUT_FILE:-}" ]]; then - echo "${TEST_name} WARNING: $1." >> "$TEST_WARNINGS_OUTPUT_FILE" - fi -} - -# Usage: show_stack -# Prints the portion of the stack that does not belong to this module, -# i.e. the user's code that called a failing assertion. Stack may not -# be available if Bash is reading commands from stdin; an error is -# printed in that case. -__show_stack() { - local i=0 - local trace_found=0 - - # Skip over active calls within this module: - while (( i < ${#FUNCNAME[@]} )) && [[ ${BASH_SOURCE[i]:-} == "${BASH_SOURCE[0]}" ]]; do - (( ++i )) - done - - # Show all calls until the next one within this module (typically run_suite): - while (( i < ${#FUNCNAME[@]} )) && [[ ${BASH_SOURCE[i]:-} != "${BASH_SOURCE[0]}" ]]; do - # Read online docs for BASH_LINENO to understand the strange offset. - # Undefined can occur in the BASH_SOURCE stack apparently when one exits from a subshell - echo "${BASH_SOURCE[i]:-"Unknown"}:${BASH_LINENO[i - 1]:-"Unknown"}: in call to ${FUNCNAME[i]:-"Unknown"}" >&2 - (( ++i )) - trace_found=1 - done - - (( trace_found )) || echo "[Stack trace not available]" >&2 -} - -# Usage: expect_log <regexp> [error-message] -# Asserts that $TEST_log matches regexp. Prints the contents of -# $TEST_log and the specified (optional) error message otherwise, and -# returns non-zero. -function expect_log() { - local pattern=$1 - local message=${2:-Expected regexp "$pattern" not found} - grep -sq -- "$pattern" $TEST_log && return 0 - - fail "$message" - return 1 -} - -# Usage: expect_log_warn <regexp> [error-message] -# Warns if $TEST_log does not match regexp. Prints the contents of -# $TEST_log and the specified (optional) error message on mismatch. -function expect_log_warn() { - local pattern=$1 - local message=${2:-Expected regexp "$pattern" not found} - grep -sq -- "$pattern" $TEST_log && return 0 - - warn "$message" - return 1 -} - -# Usage: expect_log_once <regexp> [error-message] -# Asserts that $TEST_log contains one line matching <regexp>. -# Prints the contents of $TEST_log and the specified (optional) -# error message otherwise, and returns non-zero. -function expect_log_once() { - local pattern=$1 - local message=${2:-Expected regexp "$pattern" not found exactly once} - expect_log_n "$pattern" 1 "$message" -} - -# Usage: expect_log_n <regexp> <count> [error-message] -# Asserts that $TEST_log contains <count> lines matching <regexp>. -# Prints the contents of $TEST_log and the specified (optional) -# error message otherwise, and returns non-zero. -function expect_log_n() { - local pattern=$1 - local expectednum=${2:-1} - local message=${3:-Expected regexp "$pattern" not found exactly $expectednum times} - local count=$(grep -sc -- "$pattern" $TEST_log) - (( count == expectednum )) && return 0 - fail "$message" - return 1 -} - -# Usage: expect_not_log <regexp> [error-message] -# Asserts that $TEST_log does not match regexp. Prints the contents -# of $TEST_log and the specified (optional) error message otherwise, and -# returns non-zero. -function expect_not_log() { - local pattern=$1 - local message=${2:-Unexpected regexp "$pattern" found} - grep -sq -- "$pattern" $TEST_log || return 0 - - fail "$message" - return 1 -} - -# Usage: expect_query_targets <arguments> -# Checks that log file contains exactly the targets in the argument list. -function expect_query_targets() { - for arg in "$@"; do - expect_log_once "^$arg$" - done - -# Checks that the number of lines started with '//' equals to the number of -# arguments provided. - expect_log_n "^//[^ ]*$" $# -} - -# Usage: expect_log_with_timeout <regexp> <timeout> [error-message] -# Waits for the given regexp in the $TEST_log for up to timeout seconds. -# Prints the contents of $TEST_log and the specified (optional) -# error message otherwise, and returns non-zero. -function expect_log_with_timeout() { - local pattern=$1 - local timeout=$2 - local message=${3:-Regexp "$pattern" not found in "$timeout" seconds} - local count=0 - while (( count < timeout )); do - grep -sq -- "$pattern" "$TEST_log" && return 0 - let count=count+1 - sleep 1 - done - - grep -sq -- "$pattern" "$TEST_log" && return 0 - fail "$message" - return 1 -} - -# Usage: expect_cmd_with_timeout <expected> <cmd> [timeout] -# Repeats the command once a second for up to timeout seconds (10s by default), -# until the output matches the expected value. Fails and returns 1 if -# the command does not return the expected value in the end. -function expect_cmd_with_timeout() { - local expected="$1" - local cmd="$2" - local timeout=${3:-10} - local count=0 - while (( count < timeout )); do - local actual="$($cmd)" - [[ "$expected" == "$actual" ]] && return 0 - (( ++count )) - sleep 1 - done - - [[ "$expected" == "$actual" ]] && return 0 - fail "Expected '${expected}' within ${timeout}s, was '${actual}'" - return 1 -} - -# Usage: assert_one_of <expected_list>... <actual> -# Asserts that actual is one of the items in expected_list -# -# Example: -# local expected=( "foo", "bar", "baz" ) -# assert_one_of $expected $actual -function assert_one_of() { - local args=("$@") - local last_arg_index=$((${#args[@]} - 1)) - local actual=${args[last_arg_index]} - unset args[last_arg_index] - for expected_item in "${args[@]}"; do - [[ "$expected_item" == "$actual" ]] && return 0 - done; - - fail "Expected one of '${args[*]}', was '$actual'" - return 1 -} - -# Usage: assert_not_one_of <expected_list>... <actual> -# Asserts that actual is not one of the items in expected_list -# -# Example: -# local unexpected=( "foo", "bar", "baz" ) -# assert_not_one_of $unexpected $actual -function assert_not_one_of() { - local args=("$@") - local last_arg_index=$((${#args[@]} - 1)) - local actual=${args[last_arg_index]} - unset args[last_arg_index] - for expected_item in "${args[@]}"; do - if [[ "$expected_item" == "$actual" ]]; then - fail "'${args[*]}' contains '$actual'" - return 1 - fi - done; - - return 0 -} - -# Usage: assert_equals <expected> <actual> -# Asserts [[ expected == actual ]]. -function assert_equals() { - local expected=$1 actual=$2 - [[ "$expected" == "$actual" ]] && return 0 - - fail "Expected '$expected', was '$actual'" - return 1 -} - -# Usage: assert_not_equals <unexpected> <actual> -# Asserts [[ unexpected != actual ]]. -function assert_not_equals() { - local unexpected=$1 actual=$2 - [[ "$unexpected" != "$actual" ]] && return 0; - - fail "Expected not '${unexpected}', was '${actual}'" - return 1 -} - -# Usage: assert_contains <regexp> <file> [error-message] -# Asserts that file matches regexp. Prints the contents of -# file and the specified (optional) error message otherwise, and -# returns non-zero. -function assert_contains() { - local pattern=$1 - local file=$2 - local message=${3:-Expected regexp "$pattern" not found in "$file"} - grep -sq -- "$pattern" "$file" && return 0 - - cat "$file" >&2 - fail "$message" - return 1 -} - -# Usage: assert_not_contains <regexp> <file> [error-message] -# Asserts that file does not match regexp. Prints the contents of -# file and the specified (optional) error message otherwise, and -# returns non-zero. -function assert_not_contains() { - local pattern=$1 - local file=$2 - local message=${3:-Expected regexp "$pattern" found in "$file"} - - if [[ -f "$file" ]]; then - grep -sq -- "$pattern" "$file" || return 0 - else - fail "$file is not a file: $message" - return 1 - fi - - cat "$file" >&2 - fail "$message" - return 1 -} - -function assert_contains_n() { - local pattern=$1 - local expectednum=${2:-1} - local file=$3 - local message=${4:-Expected regexp "$pattern" not found exactly $expectednum times} - local count - if [[ -f "$file" ]]; then - count=$(grep -sc -- "$pattern" "$file") - else - fail "$file is not a file: $message" - return 1 - fi - (( count == expectednum )) && return 0 - - cat "$file" >&2 - fail "$message" - return 1 -} - -# Updates the global variables TESTS if -# sharding is enabled, i.e. ($TEST_TOTAL_SHARDS > 0). -function __update_shards() { - [[ -z "${TEST_TOTAL_SHARDS-}" ]] && return 0 - - (( TEST_TOTAL_SHARDS > 0 )) || - { echo "Invalid total shards ${TEST_TOTAL_SHARDS}" >&2; exit 1; } - - (( TEST_SHARD_INDEX < 0 || TEST_SHARD_INDEX >= TEST_TOTAL_SHARDS )) && - { echo "Invalid shard ${TEST_SHARD_INDEX}" >&2; exit 1; } - - IFS=$'\n' read -rd $'\0' -a TESTS < <( - for test in "${TESTS[@]}"; do echo "$test"; done | - awk "NR % ${TEST_TOTAL_SHARDS} == ${TEST_SHARD_INDEX}" && - echo -en '\0') - - [[ -z "${TEST_SHARD_STATUS_FILE-}" ]] || touch "$TEST_SHARD_STATUS_FILE" -} - -# Usage: __test_terminated <signal-number> -# Handler that is called when the test terminated unexpectedly -function __test_terminated() { - __show_log >&2 - echo "$TEST_name FAILED: terminated by signal $1." >&2 - TEST_passed="false" - __show_stack - timeout - exit 1 -} - -# Usage: __test_terminated_err -# Handler that is called when the test terminated unexpectedly due to "errexit". -function __test_terminated_err() { - # When a subshell exits due to signal ERR, its parent shell also exits, - # thus the signal handler is called recursively and we print out the - # error message and stack trace multiple times. We're only interested - # in the first one though, as it contains the most information, so ignore - # all following. - if [[ -f $TEST_TMPDIR/__err_handled ]]; then - exit 1 - fi - __show_log >&2 - if [[ ! -z "$TEST_name" ]]; then - echo -n "$TEST_name " >&2 - fi - echo "FAILED: terminated because this command returned a non-zero status:" >&2 - touch $TEST_TMPDIR/__err_handled - TEST_passed="false" - __show_stack - # If $TEST_name is still empty, the test suite failed before we even started - # to run tests, so we shouldn't call tear_down. - if [[ -n "$TEST_name" ]]; then - __run_tear_down_after_failure - fi - exit 1 -} - -# Usage: __trap_with_arg <handler> <signals ...> -# Helper to install a trap handler for several signals preserving the signal -# number, so that the signal number is available to the trap handler. -function __trap_with_arg() { - func="$1" ; shift - for sig ; do - trap "$func $sig" "$sig" - done -} - -# Usage: <node> <block> -# Adds the block to the given node in the report file. Quotes in the in -# arguments need to be escaped. -function __log_to_test_report() { - local node="$1" - local block="$2" - if [[ ! -e "$XML_OUTPUT_FILE" ]]; then - local xml_header='<?xml version="1.0" encoding="UTF-8"?>' - echo "${xml_header}<testsuites></testsuites>" > "$XML_OUTPUT_FILE" - fi - - # replace match on node with block and match - # replacement expression only needs escaping for quotes - perl -e "\ -\$input = @ARGV[0]; \ -\$/=undef; \ -open FILE, '+<$XML_OUTPUT_FILE'; \ -\$content = <FILE>; \ -if (\$content =~ /($node.*)\$/) { \ - seek FILE, 0, 0; \ - print FILE \$\` . \$input . \$1; \ -}; \ -close FILE" "$block" -} - -# Usage: <total> <passed> -# Adds the test summaries to the xml nodes. -function __finish_test_report() { - local suite_name="$1" - local total="$2" - local passed="$3" - local failed=$((total - passed)) - - # Update the xml output with the suite name and total number of - # passed/failed tests. - cat "$XML_OUTPUT_FILE" | \ - sed \ - "s/<testsuites>/<testsuites tests=\"$total\" failures=\"0\" errors=\"$failed\">/" | \ - sed \ - "s/<testsuite>/<testsuite name=\"${suite_name}\" tests=\"$total\" failures=\"0\" errors=\"$failed\">/" \ - > "${XML_OUTPUT_FILE}.bak" - - rm -f "$XML_OUTPUT_FILE" - mv "${XML_OUTPUT_FILE}.bak" "$XML_OUTPUT_FILE" -} - -# Multi-platform timestamp function -UNAME=$(uname -s | tr 'A-Z' 'a-z') -if [[ "$UNAME" == "linux" ]] || [[ "$UNAME" =~ msys_nt* ]]; then - function timestamp() { - echo $(($(date +%s%N)/1000000)) - } -else - function timestamp() { - # macOS and BSDs do not have %N, so Python is the best we can do. - # LC_ALL=C works around python 3.8 and 3.9 crash on macOS when the - # filesystem encoding is unspecified (e.g. when LANG=en_US). - local PYTHON=python - command -v python3 &> /dev/null && PYTHON=python3 - LC_ALL=C "${PYTHON}" -c 'import time; print(int(round(time.time() * 1000)))' - } -fi - -function get_run_time() { - local ts_start=$1 - local ts_end=$2 - run_time_ms=$((ts_end - ts_start)) - echo $((run_time_ms / 1000)).${run_time_ms: -3} -} - -# Usage: run_tests <suite-comment> -# Must be called from the end of the user's test suite. -# Calls exit with zero on success, non-zero otherwise. -function run_suite() { - local message="$1" - # The name of the suite should be the script being run, which - # will be the filename with the ".sh" extension removed. - local suite_name="$(basename "$0")" - - echo >&2 - echo "$message" >&2 - echo >&2 - - __log_to_test_report "<\/testsuites>" "<testsuite></testsuite>" - - local total=0 - local passed=0 - - atexit "cleanup" - - # If the user didn't specify an explicit list of tests (e.g. a - # working set), use them all. - if (( ${#TESTS[@]} == 0 )); then - # Even if there aren't any tests, this needs to succeed. - local all_tests=() - IFS=$'\n' read -d $'\0' -ra all_tests < <( - declare -F | awk '{print $3}' | grep ^test_ || true; echo -en '\0') - - if (( "${#_TEST_FILTERS[@]}" == 0 )); then - # Use ${array[@]+"${array[@]}"} idiom to avoid errors when running with - # Bash version <= 4.4 with `nounset` when `all_tests` is empty ( - # https://github.com/bminor/bash/blob/a0c0a00fc419b7bc08202a79134fcd5bc0427071/CHANGES#L62-L63). - TESTS=("${all_tests[@]+${all_tests[@]}}") - else - for t in "${all_tests[@]+${all_tests[@]}}"; do - local matches=0 - for f in "${_TEST_FILTERS[@]}"; do - # We purposely want to glob match. - # shellcheck disable=SC2053 - [[ "$t" = $f ]] && matches=1 && break - done - if (( matches )); then - TESTS+=("$t") - fi - done - fi - - elif [[ -n "${TEST_WARNINGS_OUTPUT_FILE:-}" ]]; then - if grep -q "TESTS=" "$TEST_script" ; then - echo "TESTS variable overridden in sh_test. Please remove before submitting" \ - >> "$TEST_WARNINGS_OUTPUT_FILE" - fi - fi - - # Reset TESTS in the common case where it contains a single empty string. - if [[ -z "${TESTS[*]-}" ]]; then - TESTS=() - fi - local original_tests_size=${#TESTS[@]} - - __update_shards - - if [[ "${#TESTS[@]}" -ne 0 ]]; then - for TEST_name in "${TESTS[@]}"; do - >"$TEST_log" # Reset the log. - TEST_passed="true" - - (( ++total )) - if [[ "$TEST_verbose" == "true" ]]; then - date >&2 - __pad "$TEST_name" '*' >&2 - fi - - local run_time="0.0" - rm -f "${TEST_TMPDIR}"/{__ts_start,__ts_end} - - if [[ "$(type -t "$TEST_name")" == function ]]; then - # Save exit handlers eventually set. - local SAVED_ATEXIT="$ATEXIT"; - ATEXIT= - - # Run test in a subshell. - rm -f "${TEST_TMPDIR}"/__err_handled - __trap_with_arg __test_terminated INT KILL PIPE TERM ABRT FPE ILL QUIT SEGV - - # Remember -o pipefail value and disable it for the subshell result - # collection. - if [[ "${SHELLOPTS}" =~ (^|:)pipefail(:|$) ]]; then - local __opt_switch=-o - else - local __opt_switch=+o - fi - set +o pipefail - ( - set "${__opt_switch}" pipefail - # if errexit is enabled, make sure we run cleanup and collect the log. - if [[ "$-" = *e* ]]; then - set -E - trap __test_terminated_err ERR - fi - timestamp >"${TEST_TMPDIR}"/__ts_start - testenv_set_up - set_up - eval "$TEST_name" - __in_tear_down=1 - tear_down - testenv_tear_down - timestamp >"${TEST_TMPDIR}"/__ts_end - test "$TEST_passed" == "true" - ) 2>&1 | tee "${TEST_TMPDIR}"/__log - # Note that tee will prevent the control flow continuing if the test - # spawned any processes which are still running and have not closed - # their stdout. - - test_subshell_status=${PIPESTATUS[0]} - set "${__opt_switch}" pipefail - if (( test_subshell_status != 0 )); then - TEST_passed="false" - # Ensure that an end time is recorded in case the test subshell - # terminated prematurely. - [[ -f "$TEST_TMPDIR"/__ts_end ]] || timestamp >"$TEST_TMPDIR"/__ts_end - fi - - # Calculate run time for the testcase. - local ts_start - ts_start=$(<"${TEST_TMPDIR}"/__ts_start) - local ts_end - ts_end=$(<"${TEST_TMPDIR}"/__ts_end) - run_time=$(get_run_time $ts_start $ts_end) - - # Eventually restore exit handlers. - if [[ -n "$SAVED_ATEXIT" ]]; then - ATEXIT="$SAVED_ATEXIT" - trap "$ATEXIT" EXIT - fi - else # Bad test explicitly specified in $TESTS. - fail "Not a function: '$TEST_name'" - fi - - local testcase_tag="" - - local red='\033[0;31m' - local green='\033[0;32m' - local no_color='\033[0m' - - if [[ "$TEST_verbose" == "true" ]]; then - echo >&2 - fi - - if [[ "$TEST_passed" == "true" ]]; then - if [[ "$TEST_verbose" == "true" ]]; then - echo -e "${green}PASSED${no_color}: ${TEST_name}" >&2 - fi - (( ++passed )) - testcase_tag="<testcase name=\"${TEST_name}\" status=\"run\" time=\"${run_time}\" classname=\"\"></testcase>" - else - echo -e "${red}FAILED${no_color}: ${TEST_name}" >&2 - # end marker in CDATA cannot be escaped, we need to split the CDATA sections - log=$(sed 's/]]>/]]>]]><![CDATA[/g' "${TEST_TMPDIR}"/__log) - fail_msg=$(cat "${TEST_TMPDIR}"/__fail 2> /dev/null || echo "No failure message") - # Replacing '&' with '&', '<' with '<', '>' with '>', and '"' with '"' - escaped_fail_msg=$(echo "$fail_msg" | sed 's/&/\&/g' | sed 's/</\</g' | sed 's/>/\>/g' | sed 's/"/\"/g') - testcase_tag="<testcase name=\"${TEST_name}\" status=\"run\" time=\"${run_time}\" classname=\"\"><error message=\"${escaped_fail_msg}\"><![CDATA[${log}]]></error></testcase>" - fi - - if [[ "$TEST_verbose" == "true" ]]; then - echo >&2 - fi - __log_to_test_report "<\/testsuite>" "$testcase_tag" - done - fi - - __finish_test_report "$suite_name" $total $passed - __pad "${passed} / ${total} tests passed." '*' >&2 - if (( original_tests_size == 0 )); then - __pad "No tests found." '*' - exit 1 - elif (( total != passed )); then - __pad "There were errors." '*' >&2 - exit 1 - elif (( total == 0 )); then - __pad "No tests executed due to sharding. Check your test's shard_count." '*' - __pad "Succeeding anyway." '*' - fi - - exit 0 -} diff --git a/tests/bashunit/unittest_utils.sh b/tests/bashunit/unittest_utils.sh deleted file mode 100644 index be3409e..0000000 --- a/tests/bashunit/unittest_utils.sh +++ /dev/null @@ -1,181 +0,0 @@ -# Copyright 2020 The Bazel Authors. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Support for unittest.bash - -#### Set up the test environment. - -set -euo pipefail - -cat_jvm_log () { - if [[ "$log_content" =~ \ - "(error code:".*", error message: '".*"', log file: '"(.*)"')" ]]; then - echo >&2 - echo "Content of ${BASH_REMATCH[1]}:" >&2 - cat "${BASH_REMATCH[1]}" >&2 - fi -} - -# Print message in "$1" then exit with status "$2" -die () { - # second argument is optional, defaulting to 1 - local status_code=${2:-1} - # Stop capturing stdout/stderr, and dump captured output - if [[ "$CAPTURED_STD_ERR" -ne 0 || "$CAPTURED_STD_OUT" -ne 0 ]]; then - restore_outputs - if [[ "$CAPTURED_STD_OUT" -ne 0 ]]; then - cat "${TEST_TMPDIR}/captured.out" - CAPTURED_STD_OUT=0 - fi - if [[ "$CAPTURED_STD_ERR" -ne 0 ]]; then - cat "${TEST_TMPDIR}/captured.err" 1>&2 - cat_jvm_log "$(cat "${TEST_TMPDIR}/captured.err")" - CAPTURED_STD_ERR=0 - fi - fi - - if [[ -n "${1-}" ]] ; then - echo "$1" 1>&2 - fi - if [[ -n "${BASH-}" ]]; then - local caller_n=0 - while [[ $caller_n -lt 4 ]] && \ - caller_out=$(caller $caller_n 2>/dev/null); do - test $caller_n -eq 0 && echo "CALLER stack (max 4):" - echo " $caller_out" - let caller_n=caller_n+1 - done 1>&2 - fi - if [[ -n "${status_code}" && "${status_code}" -ne 0 ]]; then - exit "$status_code" - else - exit 1 - fi -} - -# Print message in "$1" then record that a non-fatal error occurred in -# ERROR_COUNT -ERROR_COUNT="${ERROR_COUNT:-0}" -error () { - if [[ -n "$1" ]] ; then - echo "$1" 1>&2 - fi - ERROR_COUNT=$(($ERROR_COUNT + 1)) -} - -# Die if "$1" != "$2", print $3 as death reason -check_eq () { - [[ "$1" = "$2" ]] || die "Check failed: '$1' == '$2' ${3:+ ($3)}" -} - -# Die if "$1" == "$2", print $3 as death reason -check_ne () { - [[ "$1" != "$2" ]] || die "Check failed: '$1' != '$2' ${3:+ ($3)}" -} - -# The structure of the following if statements is such that if '[[' fails -# (e.g., a non-number was passed in) then the check will fail. - -# Die if "$1" > "$2", print $3 as death reason -check_le () { - [[ "$1" -gt "$2" ]] || die "Check failed: '$1' <= '$2' ${3:+ ($3)}" -} - -# Die if "$1" >= "$2", print $3 as death reason -check_lt () { - [[ "$1" -lt "$2" ]] || die "Check failed: '$1' < '$2' ${3:+ ($3)}" -} - -# Die if "$1" < "$2", print $3 as death reason -check_ge () { - [[ "$1" -ge "$2" ]] || die "Check failed: '$1' >= '$2' ${3:+ ($3)}" -} - -# Die if "$1" <= "$2", print $3 as death reason -check_gt () { - [[ "$1" -gt "$2" ]] || die "Check failed: '$1' > '$2' ${3:+ ($3)}" -} - -# Die if $2 !~ $1; print $3 as death reason -check_match () -{ - expr match "$2" "$1" >/dev/null || \ - die "Check failed: '$2' does not match regex '$1' ${3:+ ($3)}" -} - -# Run command "$1" at exit. Like "trap" but multiple atexits don't -# overwrite each other. Will break if someone does call trap -# directly. So, don't do that. -ATEXIT="${ATEXIT-}" -atexit () { - if [[ -z "$ATEXIT" ]]; then - ATEXIT="$1" - else - ATEXIT="$1 ; $ATEXIT" - fi - trap "$ATEXIT" EXIT -} - -## TEST_TMPDIR -if [[ -z "${TEST_TMPDIR:-}" ]]; then - export TEST_TMPDIR="$(mktemp -d ${TMPDIR:-/tmp}/bazel-test.XXXXXXXX)" -fi -if [[ ! -e "${TEST_TMPDIR}" ]]; then - mkdir -p -m 0700 "${TEST_TMPDIR}" - # Clean TEST_TMPDIR on exit - atexit "rm -fr ${TEST_TMPDIR}" -fi - -# Functions to compare the actual output of a test to the expected -# (golden) output. -# -# Usage: -# capture_test_stdout -# ... do something ... -# diff_test_stdout "$TEST_SRCDIR/path/to/golden.out" - -# Redirect a file descriptor to a file. -CAPTURED_STD_OUT="${CAPTURED_STD_OUT:-0}" -CAPTURED_STD_ERR="${CAPTURED_STD_ERR:-0}" - -capture_test_stdout () { - exec 3>&1 # Save stdout as fd 3 - exec 4>"${TEST_TMPDIR}/captured.out" - exec 1>&4 - CAPTURED_STD_OUT=1 -} - -capture_test_stderr () { - exec 6>&2 # Save stderr as fd 6 - exec 7>"${TEST_TMPDIR}/captured.err" - exec 2>&7 - CAPTURED_STD_ERR=1 -} - -# Force XML_OUTPUT_FILE to an existing path -if [[ -z "${XML_OUTPUT_FILE:-}" ]]; then - XML_OUTPUT_FILE=${TEST_TMPDIR}/output.xml -fi - -# Functions to provide easy access to external repository outputs in the sibling -# repository layout. -# -# Usage: -# bin_dir <repository name> -# genfiles_dir <repository name> -# testlogs_dir <repository name> - -testlogs_dir() { - echo $(bazel info bazel-testlogs | sed "s|bazel-out|bazel-out/$1|") -} diff --git a/tests/integration/BUILD.bazel b/tests/integration/BUILD.bazel new file mode 100644 index 0000000..e5a8048 --- /dev/null +++ b/tests/integration/BUILD.bazel @@ -0,0 +1,167 @@ +load("@rules_android//rules:rules.bzl", "aar_import") +load("@rules_python//python:defs.bzl", "py_binary") +load("//rules:defs.bzl", "android_lint", "android_lint_test") +load(":fixture_aar.bzl", "lint_aar_fixture") +load(":lint_report_test.bzl", "lint_report_test") + +py_binary( + name = "fixture_aar_builder", + srcs = ["fixture_aar_builder.py"], +) + +filegroup( + name = "basic_clean_lib", + srcs = ["fixtures/basic/Clean.java"], +) + +android_lint_test( + name = "basic_clean_test", + srcs = ["fixtures/basic/Clean.java"], + lib = ":basic_clean_lib", + warnings_as_errors = True, +) + +filegroup( + name = "basic_dirty_lib", + srcs = ["fixtures/basic/Dirty.java"], +) + +android_lint( + name = "basic_dirty_lint", + srcs = ["fixtures/basic/Dirty.java"], + lib = ":basic_dirty_lib", + warnings_as_errors = True, +) + +lint_report_test( + name = "basic_dirty_report_test", + expected_issues = ["DefaultLocale"], + lint = ":basic_dirty_lint", +) + +android_lint_test( + name = "basic_disabled_check_test", + srcs = ["fixtures/basic/Dirty.java"], + disable_checks = ["DefaultLocale"], + lib = ":basic_dirty_lib", + warnings_as_errors = True, +) + +android_lint_test( + name = "basic_baseline_test", + srcs = ["fixtures/basic/Dirty.java"], + baseline = "fixtures/basic/DirtyBaseline.xml", + lib = ":basic_dirty_lib", + warnings_as_errors = True, +) + +filegroup( + name = "compose_clean_lib", + srcs = [ + "fixtures/compose/androidx/compose/runtime/Composable.kt", + "fixtures/compose/androidx/compose/ui/Modifier.kt", + "fixtures/compose/com/example/CleanComposables.kt", + "fixtures/compose/com/example/Stubs.kt", + ], +) + +filegroup( + name = "compose_dirty_lib", + srcs = [ + "fixtures/compose/androidx/compose/runtime/Composable.kt", + "fixtures/compose/androidx/compose/ui/Modifier.kt", + "fixtures/compose/com/example/DirtyComposables.kt", + "fixtures/compose/com/example/Stubs.kt", + ], +) + +android_lint_test( + name = "custom_rules_clean_test", + srcs = [ + "fixtures/compose/androidx/compose/runtime/Composable.kt", + "fixtures/compose/androidx/compose/ui/Modifier.kt", + "fixtures/compose/com/example/CleanComposables.kt", + "fixtures/compose/com/example/Stubs.kt", + ], + custom_rules = ["@rules_android_lint_test_deps//:com_slack_lint_compose_compose_lint_checks"], + lib = ":compose_clean_lib", + warnings_as_errors = True, +) + +android_lint( + name = "custom_rules_dirty_lint", + srcs = [ + "fixtures/compose/androidx/compose/runtime/Composable.kt", + "fixtures/compose/androidx/compose/ui/Modifier.kt", + "fixtures/compose/com/example/DirtyComposables.kt", + "fixtures/compose/com/example/Stubs.kt", + ], + custom_rules = ["@rules_android_lint_test_deps//:com_slack_lint_compose_compose_lint_checks"], + lib = ":compose_dirty_lib", + warnings_as_errors = True, +) + +lint_report_test( + name = "custom_rules_dirty_report_test", + expected_issues = ["ComposeNamingUppercase"], + lint = ":custom_rules_dirty_lint", +) + +lint_aar_fixture( + name = "compose_lint_fixture", + lint_jar = "@rules_android_lint_test_deps//:com_slack_lint_compose_compose_lint_checks", + manifest = "fixtures/aar/AndroidManifest.xml", + visibility = ["//tests:__subpackages__"], +) + +aar_import( + name = "compose_lint_aar", + aar = ":compose_lint_fixture", + visibility = ["//tests:__subpackages__"], +) + +android_lint_test( + name = "aar_rules_clean_test", + srcs = [ + "fixtures/compose/androidx/compose/runtime/Composable.kt", + "fixtures/compose/androidx/compose/ui/Modifier.kt", + "fixtures/compose/com/example/CleanComposables.kt", + "fixtures/compose/com/example/Stubs.kt", + ], + lib = ":compose_clean_lib", + warnings_as_errors = True, + deps = [":compose_lint_aar"], +) + +android_lint( + name = "aar_rules_dirty_lint", + srcs = [ + "fixtures/compose/androidx/compose/runtime/Composable.kt", + "fixtures/compose/androidx/compose/ui/Modifier.kt", + "fixtures/compose/com/example/DirtyComposables.kt", + "fixtures/compose/com/example/Stubs.kt", + ], + lib = ":compose_dirty_lib", + warnings_as_errors = True, + deps = [":compose_lint_aar"], +) + +lint_report_test( + name = "aar_rules_dirty_report_test", + expected_issues = ["ComposeNamingUppercase"], + lint = ":aar_rules_dirty_lint", +) + +test_suite( + name = "tests", + tests = [ + ":aar_rules_clean_test", + ":aar_rules_dirty_report_test", + ":basic_baseline_test", + ":basic_clean_test", + ":basic_dirty_report_test", + ":basic_disabled_check_test", + ":custom_rules_clean_test", + ":custom_rules_dirty_report_test", + ], +) diff --git a/tests/integration/fixture_aar.bzl b/tests/integration/fixture_aar.bzl new file mode 100644 index 0000000..e9ad482 --- /dev/null +++ b/tests/integration/fixture_aar.bzl @@ -0,0 +1,39 @@ +"""A hermetic AAR fixture containing a supplied custom lint jar.""" + +def _lint_aar_fixture_impl(ctx): + output = ctx.actions.declare_file(ctx.label.name + ".aar") + ctx.actions.run( + executable = ctx.executable._builder, + inputs = [ctx.file.lint_jar, ctx.file.manifest], + outputs = [output], + arguments = [ + "--lint-jar", + ctx.file.lint_jar.path, + "--manifest", + ctx.file.manifest.path, + "--output", + output.path, + ], + mnemonic = "BuildLintAarFixture", + progress_message = "Building lint AAR fixture %{label}", + ) + return [DefaultInfo(files = depset([output]))] + +lint_aar_fixture = rule( + implementation = _lint_aar_fixture_impl, + attrs = { + "lint_jar": attr.label( + allow_single_file = [".jar"], + mandatory = True, + ), + "manifest": attr.label( + allow_single_file = [".xml"], + mandatory = True, + ), + "_builder": attr.label( + default = Label("//tests/integration:fixture_aar_builder"), + cfg = "exec", + executable = True, + ), + }, +) diff --git a/tests/integration/fixture_aar_builder.py b/tests/integration/fixture_aar_builder.py new file mode 100644 index 0000000..4fd419e --- /dev/null +++ b/tests/integration/fixture_aar_builder.py @@ -0,0 +1,36 @@ +import argparse +import io +import zipfile +from pathlib import Path + + +_FIXED_TIMESTAMP = (1980, 1, 1, 0, 0, 0) + + +def _write(archive: zipfile.ZipFile, name: str, content: bytes) -> None: + entry = zipfile.ZipInfo(name, date_time=_FIXED_TIMESTAMP) + entry.compress_type = zipfile.ZIP_STORED + entry.external_attr = 0o644 << 16 + archive.writestr(entry, content) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--lint-jar", required=True) + parser.add_argument("--manifest", required=True) + parser.add_argument("--output", required=True) + args = parser.parse_args() + + classes = io.BytesIO() + with zipfile.ZipFile(classes, "w"): + pass + + with zipfile.ZipFile(args.output, "w") as archive: + _write(archive, "AndroidManifest.xml", Path(args.manifest).read_bytes()) + _write(archive, "classes.jar", classes.getvalue()) + _write(archive, "R.txt", b"") + _write(archive, "lint.jar", Path(args.lint_jar).read_bytes()) + + +if __name__ == "__main__": + main() diff --git a/tests/integration/fixtures/aar/AndroidManifest.xml b/tests/integration/fixtures/aar/AndroidManifest.xml new file mode 100644 index 0000000..e985bff --- /dev/null +++ b/tests/integration/fixtures/aar/AndroidManifest.xml @@ -0,0 +1,2 @@ +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.rules.android.lint.tests.fixture" /> diff --git a/tests/integration/fixtures/basic/Clean.java b/tests/integration/fixtures/basic/Clean.java new file mode 100644 index 0000000..3599dd0 --- /dev/null +++ b/tests/integration/fixtures/basic/Clean.java @@ -0,0 +1,7 @@ +package tests.integration.fixtures.basic; + +public class Clean { + public int add(int first, int second) { + return first + second; + } +} diff --git a/tests/integration/fixtures/basic/Dirty.java b/tests/integration/fixtures/basic/Dirty.java new file mode 100644 index 0000000..7bbae50 --- /dev/null +++ b/tests/integration/fixtures/basic/Dirty.java @@ -0,0 +1,7 @@ +package tests.integration.fixtures.basic; + +public class Dirty { + public String shout(String input) { + return input.toUpperCase(); + } +} diff --git a/tests/integration/fixtures/basic/DirtyBaseline.xml b/tests/integration/fixtures/basic/DirtyBaseline.xml new file mode 100644 index 0000000..ca8a209 --- /dev/null +++ b/tests/integration/fixtures/basic/DirtyBaseline.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<issues format="6" by="lint 9.2.1"> + + <issue + id="DefaultLocale" + severity="Error" + message="Implicitly using the default locale is a common source of bugs: Use `toUpperCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`." + category="Correctness" + priority="6" + summary="Implied default locale in case conversion" + explanation="Calling `String#toLowerCase()` or `#toUpperCase()` **without specifying an explicit locale** is a common source of bugs. The reason for that is that those methods will use the current locale on the user's device, and even though the code appears to work correctly when you are developing the app, it will fail in some locales. For example, in the Turkish locale, the uppercase replacement for `i` is **not** `I`. If you want the methods to just perform ASCII replacement, for example to convert an enum name, call `String#toUpperCase(Locale.ROOT)` instead. If you really want to use the current locale, call `String#toUpperCase(Locale.getDefault())` instead." + url="https://developer.android.com/reference/java/util/Locale.html#default_locale" + urls="https://developer.android.com/reference/java/util/Locale.html#default_locale" + errorLine1=" return input.toUpperCase();" + errorLine2=" ~~~~~~~~~~~"> + <location + file="tests/integration/fixtures/basic/Dirty.java" + line="5" + column="18"/> + </issue> + +</issues> diff --git a/tests/integration/fixtures/compose/androidx/compose/runtime/Composable.kt b/tests/integration/fixtures/compose/androidx/compose/runtime/Composable.kt new file mode 100644 index 0000000..2ae5204 --- /dev/null +++ b/tests/integration/fixtures/compose/androidx/compose/runtime/Composable.kt @@ -0,0 +1,9 @@ +package androidx.compose.runtime + +@Target( + AnnotationTarget.FUNCTION, + AnnotationTarget.TYPE, + AnnotationTarget.TYPE_PARAMETER, + AnnotationTarget.PROPERTY_GETTER, +) +annotation class Composable diff --git a/tests/integration/fixtures/compose/androidx/compose/ui/Modifier.kt b/tests/integration/fixtures/compose/androidx/compose/ui/Modifier.kt new file mode 100644 index 0000000..27f73be --- /dev/null +++ b/tests/integration/fixtures/compose/androidx/compose/ui/Modifier.kt @@ -0,0 +1,5 @@ +package androidx.compose.ui + +interface Modifier { + companion object : Modifier +} diff --git a/tests/integration/fixtures/compose/com/example/CleanComposables.kt b/tests/integration/fixtures/compose/com/example/CleanComposables.kt new file mode 100644 index 0000000..b0fb7c1 --- /dev/null +++ b/tests/integration/fixtures/compose/com/example/CleanComposables.kt @@ -0,0 +1,9 @@ +package com.example + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun GoodButton(modifier: Modifier = Modifier) { + Text("hello") +} diff --git a/tests/integration/fixtures/compose/com/example/DirtyComposables.kt b/tests/integration/fixtures/compose/com/example/DirtyComposables.kt new file mode 100644 index 0000000..47f4783 --- /dev/null +++ b/tests/integration/fixtures/compose/com/example/DirtyComposables.kt @@ -0,0 +1,9 @@ +package com.example + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun badButton(modifier: Modifier = Modifier) { + Text("hello") +} diff --git a/tests/integration/fixtures/compose/com/example/Stubs.kt b/tests/integration/fixtures/compose/com/example/Stubs.kt new file mode 100644 index 0000000..3eacd0f --- /dev/null +++ b/tests/integration/fixtures/compose/com/example/Stubs.kt @@ -0,0 +1,8 @@ +package com.example + +import androidx.compose.runtime.Composable + +@Suppress("ComposeRedundantComposable") +@Composable +fun Text(text: String) { +} diff --git a/tests/integration/lint_report_test.bzl b/tests/integration/lint_report_test.bzl new file mode 100644 index 0000000..6b06b5b --- /dev/null +++ b/tests/integration/lint_report_test.bzl @@ -0,0 +1,39 @@ +"""Test macro for making assertions against an android_lint XML report.""" + +load("@rules_python//python:defs.bzl", "py_test") + +_REPORT_ASSERTION = Label("//tests/integration:report_assertion.py") + +def lint_report_test( + name, + lint, + expected_issues = [], + rejected_issues = [], + **kwargs): + """Defines a portable Python test that checks an android_lint report. + + Args: + name: Name of the generated test. + lint: android_lint target whose XML output should be checked. + expected_issues: Lint issue IDs that must be present. + rejected_issues: Lint issue IDs that must be absent. + **kwargs: Additional arguments forwarded to py_test. + """ + arguments = [ + "--report", + "$(rlocationpath {})".format(lint), + ] + for issue in expected_issues: + arguments.extend(["--expect-issue", issue]) + for issue in rejected_issues: + arguments.extend(["--reject-issue", issue]) + + py_test( + name = name, + srcs = [_REPORT_ASSERTION], + main = _REPORT_ASSERTION, + args = arguments, + data = [lint], + deps = ["@rules_python//python/runfiles"], + **kwargs + ) diff --git a/tests/integration/report_assertion.py b/tests/integration/report_assertion.py new file mode 100644 index 0000000..68c7e32 --- /dev/null +++ b/tests/integration/report_assertion.py @@ -0,0 +1,45 @@ +import argparse +import sys +from xml.etree import ElementTree + +from python.runfiles import runfiles + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--report", required=True) + parser.add_argument("--expect-issue", action="append", default=[]) + parser.add_argument("--reject-issue", action="append", default=[]) + args = parser.parse_args() + + runfiles_tree = runfiles.Create() + report_path = runfiles_tree.Rlocation(args.report) if runfiles_tree else None + if not report_path: + print(f"Unable to locate lint report runfile: {args.report}", file=sys.stderr) + return 1 + + try: + root = ElementTree.parse(report_path).getroot() + except (ElementTree.ParseError, OSError) as error: + print(f"Unable to parse lint report {args.report}: {error}", file=sys.stderr) + return 1 + if root.tag != "issues": + print(f"Invalid lint report root element: <{root.tag}>", file=sys.stderr) + return 1 + + issue_ids = {issue.get("id") for issue in root.findall("issue") if issue.get("id")} + + missing = sorted(set(args.expect_issue) - issue_ids) + unexpected = sorted(set(args.reject_issue) & issue_ids) + if missing or unexpected: + if missing: + print(f"Missing expected lint issues: {', '.join(missing)}", file=sys.stderr) + if unexpected: + print(f"Found rejected lint issues: {', '.join(unexpected)}", file=sys.stderr) + print(f"Actual lint issues: {', '.join(sorted(issue_ids))}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/rules/BUILD.bazel b/tests/rules/BUILD.bazel new file mode 100644 index 0000000..e7d7b71 --- /dev/null +++ b/tests/rules/BUILD.bazel @@ -0,0 +1,19 @@ +load("//rules:defs.bzl", "android_lint") +load(":android_lint_analysis_test.bzl", "android_lint_analysis_test_suite") + +filegroup( + name = "fixture_lib", + srcs = ["Fixture.java"], +) + +android_lint( + name = "analysis_fixture_lint", + srcs = ["Fixture.java"], + custom_rules = ["@rules_android_lint_test_deps//:com_slack_lint_compose_compose_lint_checks"], + disable_checks = ["DefaultLocale"], + lib = ":fixture_lib", + tags = ["manual"], + warnings_as_errors = True, +) + +android_lint_analysis_test_suite(name = "tests") diff --git a/tests/rules/Fixture.java b/tests/rules/Fixture.java new file mode 100644 index 0000000..5089928 --- /dev/null +++ b/tests/rules/Fixture.java @@ -0,0 +1,7 @@ +package tests.rules; + +public class Fixture { + public String shout(String input) { + return input.toUpperCase(); + } +} diff --git a/tests/rules/android_lint_analysis_test.bzl b/tests/rules/android_lint_analysis_test.bzl new file mode 100644 index 0000000..ab85e33 --- /dev/null +++ b/tests/rules/android_lint_analysis_test.bzl @@ -0,0 +1,74 @@ +"""Analysis tests for android_lint action and provider wiring.""" + +load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts") +load("//rules:providers.bzl", "AndroidLintResultsInfo") + +def _argument_value(argv, flag): + for index in range(len(argv)): + if argv[index] == flag: + if index + 1 < len(argv): + return argv[index + 1] + return None + return None + +def _android_lint_action_impl(ctx): + env = analysistest.begin(ctx) + target = analysistest.target_under_test(env) + asserts.true(env, AndroidLintResultsInfo in target) + asserts.equals(env, "analysis_fixture_lint.xml", target[AndroidLintResultsInfo].output.basename) + + actions = [ + action + for action in analysistest.target_actions(env) + if action.mnemonic == "AndroidLint" + ] + asserts.equals(env, 1, len(actions)) + if actions: + action = actions[0] + argv = action.argv + source = _argument_value(argv, "--src") + custom_rule = _argument_value(argv, "--custom-rule") + output = _argument_value(argv, "--output") + + asserts.true(env, source != None and source.endswith("/Fixture.java")) + asserts.true( + env, + custom_rule != None and "compose-lint-checks" in custom_rule and custom_rule.endswith(".jar"), + ) + asserts.equals(env, "DefaultLocale", _argument_value(argv, "--disable-check")) + asserts.true(env, "--warnings-as-errors" in argv) + asserts.equals(env, target[AndroidLintResultsInfo].output.path, output) + asserts.false(env, "--baseline-file" in argv) + asserts.false(env, "--regenerate-baseline-files" in argv) + + inputs = [file.basename for file in action.inputs.to_list()] + outputs = [file.basename for file in action.outputs.to_list()] + asserts.true(env, "Fixture.java" in inputs) + asserts.true( + env, + any(["compose-lint-checks" in input and input.endswith(".jar") for input in inputs]), + ) + asserts.true(env, "analysis_fixture_lint.xml" in outputs) + + asserts.equals(env, "1", action.execution_info.get("supports-workers")) + asserts.equals(env, "1", action.execution_info.get("supports-multiplex-workers")) + + return analysistest.end(env) + +_android_lint_action_test = analysistest.make(_android_lint_action_impl) + +def android_lint_analysis_test_suite(name): + """Defines the android_lint analysis test suite. + + Args: + name: Name of the generated test suite. + """ + action_test = name + "_action_test" + _android_lint_action_test( + name = action_test, + target_under_test = ":analysis_fixture_lint", + ) + native.test_suite( + name = name, + tests = [":" + action_test], + ) diff --git a/tests/scripts/BUILD b/tests/scripts/BUILD deleted file mode 100644 index c338119..0000000 --- a/tests/scripts/BUILD +++ /dev/null @@ -1,53 +0,0 @@ -load("@bazel_binaries//:defs.bzl", "bazel_binaries") -load("@rules_bazel_integration_test//bazel_integration_test:defs.bzl", "script_test") -load("@rules_shell//shell:sh_library.bzl", "sh_library") - -# Copies the compose-lint-checks jar to a stable runfiles path so the helper can -# rlocation it without depending on rules_jvm_external's repository layout. -genrule( - name = "compose_lint_checks_jar", - testonly = True, - srcs = ["@rules_android_lint_test_deps//:com_slack_lint_compose_compose_lint_checks"], - outs = ["compose_lint_checks.jar"], - cmd = "cp $$(echo '$(SRCS)' | tr ' ' '\\n' | grep 'compose-lint-checks' | head -1) $@", -) - -sh_library( - name = "lint_helper", - testonly = True, - srcs = ["lint_helper.sh"], - data = [ - ":compose_lint_checks_jar", - "//rules:all_files", - "//src:all_files", - "//src/cli:cli_deploy.jar", - "//third_party:com_android_tools_lint_lint_deploy.jar", - "//toolchains:all_files", - ], -) - -script_test( - name = "android_lint_scenarios_test", - size = "enormous", - srcs = ["android_lint_scenarios_test.sh"], - bazel_binaries = bazel_binaries, - bazel_version = bazel_binaries.versions.current, - tags = ["exclusive"], - deps = [ - ":lint_helper", - "//tests/bashunit", - ], -) - -script_test( - name = "android_lint_aar_scenarios_test", - size = "enormous", - srcs = ["android_lint_aar_scenarios_test.sh"], - bazel_binaries = bazel_binaries, - bazel_version = bazel_binaries.versions.current, - tags = ["exclusive"], - deps = [ - ":lint_helper", - "//tests/bashunit", - ], -) diff --git a/tests/scripts/android_lint_aar_scenarios_test.sh b/tests/scripts/android_lint_aar_scenarios_test.sh deleted file mode 100755 index 02b1fe8..0000000 --- a/tests/scripts/android_lint_aar_scenarios_test.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash -# -# Scenarios for custom lint checks embedded in AAR dependencies (lint.jar), exercising the -# collect_aar_outputs_aspect extraction and auto-discovery path. - -# --- begin runfiles.bash initialization v2 --- -# Copy-pasted from the Bazel Bash runfiles library v2. -set -uo pipefail; f=bazel_tools/tools/bash/runfiles/runfiles.bash -source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \ - source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \ - source "$0.runfiles/$f" 2>/dev/null || \ - source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \ - source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \ - { echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e -# --- end runfiles.bash initialization v2 --- - -source "$(rlocation rules_android_lint/tests/bashunit/unittest.bash)" || \ - (echo >&2 "Failed to locate unittest.bash" && exit 1) - -source "$(rlocation rules_android_lint/tests/scripts/lint_helper.sh)" || \ - (echo >&2 "Failed to locate lint_helper.sh" && exit 1) - -function set_up() { - rm -rf -- * .bazelrc 2>/dev/null || true - set_up_lint_workspace - enable_android_in_workspace -} - -function test_aar_embedded_lint_jar_flags_issue() { - write_composable_annotation_stub - write_dirty_composable - write_fixture_aar - write_aar_dep_lint_targets - - "${BIT_BAZEL_BINARY}" test --test_output=all //:lib_lint_test >& "$TEST_log" \ - && fail "Expected lint test with AAR-embedded checks to fail" || true - expect_log "ComposeNamingUppercase" -} - -function test_aar_embedded_lint_jar_accepts_clean_composable() { - write_composable_annotation_stub - write_clean_composable - write_fixture_aar - write_aar_dep_lint_targets - - "${BIT_BAZEL_BINARY}" test --test_output=all //:lib_lint_test >& "$TEST_log" \ - || fail "Expected lint test with AAR-embedded checks to pass" -} - -run_suite "android_lint AAR lint.jar discovery scenarios" diff --git a/tests/scripts/android_lint_scenarios_test.sh b/tests/scripts/android_lint_scenarios_test.sh deleted file mode 100755 index a31649a..0000000 --- a/tests/scripts/android_lint_scenarios_test.sh +++ /dev/null @@ -1,140 +0,0 @@ -#!/bin/bash -# -# Behavior scenarios for the android_lint and android_lint_test rules, driven through a real -# child Bazel against a scaffolded consumer workspace. - -# --- begin runfiles.bash initialization v2 --- -# Copy-pasted from the Bazel Bash runfiles library v2. -set -uo pipefail; f=bazel_tools/tools/bash/runfiles/runfiles.bash -source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \ - source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \ - source "$0.runfiles/$f" 2>/dev/null || \ - source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \ - source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \ - { echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e -# --- end runfiles.bash initialization v2 --- - -source "$(rlocation rules_android_lint/tests/bashunit/unittest.bash)" || \ - (echo >&2 "Failed to locate unittest.bash" && exit 1) - -source "$(rlocation rules_android_lint/tests/scripts/lint_helper.sh)" || \ - (echo >&2 "Failed to locate lint_helper.sh" && exit 1) - -function set_up() { - rm -rf -- * .bazelrc 2>/dev/null || true - set_up_lint_workspace -} - -function test_clean_sources_pass() { - write_clean_source - write_lint_targets "lib" "Foo.java" - - "${BIT_BAZEL_BINARY}" test //:lib_lint_test >& "$TEST_log" || fail "Expected lint test to pass" -} - -function test_new_issue_fails_the_lint_test() { - write_dirty_source - write_lint_targets "lib" "Foo.java" - - "${BIT_BAZEL_BINARY}" test --test_output=all //:lib_lint_test >& "$TEST_log" \ - && fail "Expected lint test to fail" || true - expect_log "DefaultLocale" -} - -function test_build_rule_reports_issue_in_xml() { - write_dirty_source - write_lint_targets "lib" "Foo.java" - - "${BIT_BAZEL_BINARY}" build //:lib_lint >& "$TEST_log" || fail "Expected lint build to pass" - local bazel_bin - bazel_bin="$("${BIT_BAZEL_BINARY}" info bazel-bin 2>/dev/null)" - grep -q "DefaultLocale" "${bazel_bin}/lib_lint.xml" \ - || fail "Expected DefaultLocale issue in the XML report" -} - -function test_baseline_suppresses_existing_issue() { - write_dirty_source - write_lint_targets "lib" "Foo.java" - - # Generate the baseline from the build rule's report, mirroring the regenerate workflow. - "${BIT_BAZEL_BINARY}" build //:lib_lint >& "$TEST_log" || fail "Expected lint build to pass" - local bazel_bin - bazel_bin="$("${BIT_BAZEL_BINARY}" info bazel-bin 2>/dev/null)" - cp "${bazel_bin}/lib_lint.xml" lint_baseline.xml - - cat >> BUILD.bazel <<EOF - -android_lint_test( - name = "lib_baselined_lint_test", - srcs = ["Foo.java"], - baseline = "lint_baseline.xml", - lib = ":lib", - warnings_as_errors = True, -) -EOF - - "${BIT_BAZEL_BINARY}" test //:lib_baselined_lint_test >& "$TEST_log" \ - || fail "Expected baselined lint test to pass" -} - -function test_disable_checks_suppresses_issue() { - write_dirty_source - cat > BUILD.bazel <<EOF -load("@rules_android_lint//rules:defs.bzl", "android_lint_test") - -filegroup( - name = "lib", - srcs = ["Foo.java"], -) - -android_lint_test( - name = "lib_lint_test", - srcs = ["Foo.java"], - disable_checks = ["DefaultLocale"], - lib = ":lib", - warnings_as_errors = True, -) -EOF - - "${BIT_BAZEL_BINARY}" test //:lib_lint_test >& "$TEST_log" \ - || fail "Expected lint test with disabled check to pass" -} - -function test_worker_and_local_strategies_agree() { - write_dirty_source - write_lint_targets "lib" "Foo.java" - - "${BIT_BAZEL_BINARY}" test --test_output=all --strategy=AndroidLint=worker \ - //:lib_lint_test >& "$TEST_log" \ - && fail "Expected worker-strategy lint test to fail" || true - expect_log "DefaultLocale" - - "${BIT_BAZEL_BINARY}" clean >& /dev/null - "${BIT_BAZEL_BINARY}" test --test_output=all --strategy=AndroidLint=local \ - //:lib_lint_test >& "$TEST_log" \ - && fail "Expected local-strategy lint test to fail" || true - expect_log "DefaultLocale" -} - -function test_custom_rules_jar_flags_compose_issue() { - write_composable_annotation_stub - write_dirty_composable - copy_compose_lint_checks - write_custom_rules_lint_targets - - "${BIT_BAZEL_BINARY}" test --test_output=all //:lib_lint_test >& "$TEST_log" \ - && fail "Expected lint test with custom compose-lints rules to fail" || true - expect_log "ComposeNamingUppercase" -} - -function test_custom_rules_jar_accepts_clean_composable() { - write_composable_annotation_stub - write_clean_composable - copy_compose_lint_checks - write_custom_rules_lint_targets - - "${BIT_BAZEL_BINARY}" test --test_output=all //:lib_lint_test >& "$TEST_log" \ - || fail "Expected lint test with custom compose-lints rules to pass" -} - -run_suite "android_lint rule behavior scenarios" diff --git a/tests/scripts/lint_helper.sh b/tests/scripts/lint_helper.sh deleted file mode 100755 index b7c226f..0000000 --- a/tests/scripts/lint_helper.sh +++ /dev/null @@ -1,398 +0,0 @@ -#!/bin/bash -# -# Helpers for scaffolding a minimal consumer workspace of rules_android_lint inside a -# script_test. The scaffolded copy of the ruleset keeps the Starlark sources verbatim but -# replaces the Kotlin CLI and the maven-derived lint jar with prebuilt deploy jars from the -# outer build, so the child Bazel never compiles Kotlin or resolves maven. - -# Writes a scaffolded rules_android_lint module and a consumer workspace in the CWD. -function set_up_lint_workspace() { - local dest="${TEST_TMPDIR}/rules_android_lint" - rm -rf "${dest}" - mkdir -p "${dest}/rules" "${dest}/toolchains" "${dest}/src/cli" "${dest}/third_party" - - # Starlark sources, copied verbatim. - local rules_dir - rules_dir="$(dirname "$(rlocation rules_android_lint/rules/defs.bzl)")" - cp -L "${rules_dir}"/*.bzl "${dest}/rules/" - touch "${dest}/rules/BUILD" - - cp -L "$(rlocation rules_android_lint/toolchains/toolchain.bzl)" "${dest}/toolchains/" - - # Prebuilt binaries replace the Kotlin + maven subtrees. The jar is renamed because - # cli_deploy.jar collides with the java_binary's implicit <name>_deploy.jar output. - cp -L "$(rlocation rules_android_lint/src/cli/cli_deploy.jar)" "${dest}/src/cli/cli_prebuilt.jar" - cp -L "$(rlocation rules_android_lint/third_party/com_android_tools_lint_lint_deploy.jar)" \ - "${dest}/third_party/" - cp -L "$(rlocation rules_android_lint/src/android_lint_output_validator.py)" "${dest}/src/" - - cat > "${dest}/MODULE.bazel" <<EOF -module(name = "rules_android_lint") - -bazel_dep(name = "hermetic_android_toolchains", version = "0.2.0", dev_dependency = True) -bazel_dep(name = "rules_android", version = "0.7.3") -bazel_dep(name = "rules_java", version = "9.3.0") -bazel_dep(name = "rules_python", version = "1.7.0") -bazel_dep(name = "bazel_skylib", version = "1.9.0") -bazel_dep(name = "platforms", version = "1.0.0") - -android = use_extension( - "@hermetic_android_toolchains//:extensions.bzl", - "android", - dev_dependency = True, -) -android.sdk( - build_tools_version = "35.0.0", - version = "34", -) -use_repo(android, "androidsdk") - -rules_android_sdk = use_extension( - "@rules_android//rules/android_sdk_repository:rule.bzl", - "android_sdk_repository_extension", -) -override_repo(rules_android_sdk, "androidsdk") - -register_toolchains("@androidsdk//:all", dev_dependency = True) -register_toolchains("//toolchains:default_toolchain", dev_dependency = True) -EOF - - cat > "${dest}/src/BUILD.bazel" <<EOF -load("@rules_python//python:defs.bzl", "py_binary") - -py_binary( - name = "android_lint_output_validator", - srcs = ["android_lint_output_validator.py"], - visibility = ["//visibility:public"], -) -EOF - - cat > "${dest}/src/cli/BUILD" <<EOF -load("@rules_java//java:defs.bzl", "java_binary", "java_import") - -java_import( - name = "cli_jar", - jars = ["cli_prebuilt.jar"], -) - -java_binary( - name = "cli", - main_class = "com.rules.android.lint.cli.AndroidLintAction", - visibility = ["//visibility:public"], - runtime_deps = [":cli_jar"], -) -EOF - - cat > "${dest}/third_party/BUILD.bazel" <<EOF -exports_files(["com_android_tools_lint_lint_deploy.jar"]) -EOF - - cat > "${dest}/toolchains/BUILD.bazel" <<EOF -load(":toolchain.bzl", "android_lint_toolchain") - -toolchain_type( - name = "toolchain_type", - visibility = ["//visibility:public"], -) - -android_lint_toolchain( - name = "default_toolchain_impl", - android_home = "@androidsdk//:files", - android_lint = "//third_party:com_android_tools_lint_lint_deploy.jar", -) - -toolchain( - name = "default_toolchain", - toolchain = ":default_toolchain_impl", - toolchain_type = ":toolchain_type", -) -EOF - - # The consumer workspace, in the CWD. - cat > MODULE.bazel <<EOF -bazel_dep(name = "rules_android_lint") -bazel_dep(name = "hermetic_android_toolchains", version = "0.2.0", dev_dependency = True) -bazel_dep(name = "rules_android", version = "0.7.3") - -local_path_override( - module_name = "rules_android_lint", - path = "${dest}", -) - -android = use_extension( - "@hermetic_android_toolchains//:extensions.bzl", - "android", - dev_dependency = True, -) -android.sdk( - build_tools_version = "35.0.0", - version = "34", -) -use_repo(android, "androidsdk") - -rules_android_sdk = use_extension( - "@rules_android//rules/android_sdk_repository:rule.bzl", - "android_sdk_repository_extension", - dev_dependency = True, -) -override_repo(rules_android_sdk, "androidsdk") - -register_toolchains("@androidsdk//:all", dev_dependency = True) -register_toolchains("//toolchains:default_toolchain", dev_dependency = True) -EOF - - mkdir -p toolchains - cat > toolchains/BUILD.bazel <<EOF -load("@rules_android_lint//toolchains:toolchain.bzl", "android_lint_toolchain") - -android_lint_toolchain( - name = "default_toolchain_impl", - android_home = "@androidsdk//:files", -) - -toolchain( - name = "default_toolchain", - toolchain = ":default_toolchain_impl", - toolchain_type = "@rules_android_lint//toolchains:toolchain_type", -) -EOF - - cat > .bazelrc <<EOF -common --lockfile_mode=off -# Lint 32.x ships Java 17 bytecode; the lint worker must run on a 17+ JVM. -common --java_language_version=17 -common --java_runtime_version=remotejdk_17 -common --tool_java_language_version=17 -common --tool_java_runtime_version=remotejdk_17 -# Required by hermetic_android_toolchains for the pinned SDK version. -common --repo_env=ACCEPTED_ANDROID_SDK_LICENSE_VERSION=34 -# Shared across the test functions in one script_test run. -common --repository_cache=${TEST_TMPDIR}/repository_cache -EOF -} - -# Writes a lintable target. $1: target name, $2: java source file name. -function write_lint_targets() { - local name="$1" - local src="$2" - cat > BUILD.bazel <<EOF -load("@rules_android_lint//rules:defs.bzl", "android_lint", "android_lint_test") - -# A filegroup keeps the scenario free of java toolchain setup; the lint action only -# needs the source files. (AndroidLibraryResourceClassJarProvider is optional on lib.) -filegroup( - name = "lib", - srcs = ["${src}"], -) - -android_lint( - name = "${name}_lint", - srcs = ["${src}"], - lib = ":lib", - warnings_as_errors = True, -) - -android_lint_test( - name = "${name}_lint_test", - srcs = ["${src}"], - lib = ":lib", - warnings_as_errors = True, -) -EOF -} - -# Foo.java with no lint issues. -function write_clean_source() { - cat > Foo.java <<EOF -package com.example; - -public class Foo { - public int add(int a, int b) { - return a + b; - } -} -EOF -} - -# Foo.java that trips the DefaultLocale check (error under warnings_as_errors). -function write_dirty_source() { - cat > Foo.java <<EOF -package com.example; - -public class Foo { - public String shout(String input) { - return input.toUpperCase(); - } -} -EOF -} - -# Copies the compose-lint-checks jar (the custom-rule test fixture) into the CWD. -function copy_compose_lint_checks() { - cp -L "$(rlocation rules_android_lint/tests/scripts/compose_lint_checks.jar)" \ - compose_lint_checks.jar -} - -# A source-level stand-in for androidx.compose.runtime.Composable so the compose-lints -# checks can resolve the annotation without the Compose runtime on the classpath. -function write_composable_annotation_stub() { - cat > Composable.kt <<EOF -package androidx.compose.runtime - -@Target( - AnnotationTarget.FUNCTION, - AnnotationTarget.TYPE, - AnnotationTarget.TYPE_PARAMETER, - AnnotationTarget.PROPERTY_GETTER, -) -annotation class Composable -EOF - - cat > Modifier.kt <<EOF -package androidx.compose.ui - -interface Modifier { - companion object : Modifier -} -EOF - - # A leaf composable for fixtures to call so they actually "use composition"; - # the suppression keeps the stub itself out of ComposeRedundantComposable. - cat > Stubs.kt <<EOF -package com.example - -import androidx.compose.runtime.Composable - -@Suppress("ComposeRedundantComposable") -@Composable -fun Text(text: String) { -} -EOF -} - -# A unit-returning composable whose lowercase name trips ComposeNamingUppercase. -function write_dirty_composable() { - cat > MyComposables.kt <<EOF -package com.example - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier - -@Composable -fun badButton(modifier: Modifier = Modifier) { - Text("hello") -} -EOF -} - -# A correctly named composable that compose-lints accepts. -function write_clean_composable() { - cat > MyComposables.kt <<EOF -package com.example - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier - -@Composable -fun GoodButton(modifier: Modifier = Modifier) { - Text("hello") -} -EOF -} - -# Enables rules_android's legacy aar_import API in the consumer workspace. -function enable_android_in_workspace() { - cat >> .bazelrc <<EOF -common --experimental_google_legacy_api -EOF -} - -# Builds a minimal AAR in the CWD with the compose-lints jar embedded as its lint.jar, -# exercising the collect_aar_outputs_aspect extraction + lint.jar auto-discovery path. -function write_fixture_aar() { - copy_compose_lint_checks - python3 - <<'PYEOF' -import io -import zipfile - -classes = io.BytesIO() -zipfile.ZipFile(classes, "w").close() - -with zipfile.ZipFile("fixture.aar", "w") as aar: - aar.writestr( - "AndroidManifest.xml", - '<manifest xmlns:android="http://schemas.android.com/apk/res/android" ' - 'package="com.example.aarfixture"/>', - ) - aar.writestr("classes.jar", classes.getvalue()) - aar.writestr("R.txt", "") - aar.write("compose_lint_checks.jar", "lint.jar") -PYEOF -} - -# Lint targets that depend on the fixture AAR; custom checks must be discovered from the -# AAR's embedded lint.jar rather than the custom_rules attribute. -function write_aar_dep_lint_targets() { - cat > BUILD.bazel <<EOF -load("@rules_android//rules:rules.bzl", "aar_import") -load("@rules_android_lint//rules:defs.bzl", "android_lint_test") - -aar_import( - name = "fixture_aar", - aar = "fixture.aar", -) - -filegroup( - name = "lib", - srcs = [ - "Composable.kt", - "Modifier.kt", - "MyComposables.kt", - "Stubs.kt", - ], -) - -android_lint_test( - name = "lib_lint_test", - srcs = [ - "Composable.kt", - "Modifier.kt", - "MyComposables.kt", - "Stubs.kt", - ], - lib = ":lib", - warnings_as_errors = True, - deps = [":fixture_aar"], -) -EOF -} - -# Lint targets whose custom checks come from the compose-lints jar via custom_rules. -function write_custom_rules_lint_targets() { - cat > BUILD.bazel <<EOF -load("@rules_android_lint//rules:defs.bzl", "android_lint_test") - -filegroup( - name = "lib", - srcs = [ - "Composable.kt", - "Modifier.kt", - "MyComposables.kt", - "Stubs.kt", - ], -) - -android_lint_test( - name = "lib_lint_test", - srcs = [ - "Composable.kt", - "Modifier.kt", - "MyComposables.kt", - "Stubs.kt", - ], - custom_rules = ["compose_lint_checks.jar"], - lib = ":lib", - warnings_as_errors = True, -) -EOF -} diff --git a/tests/src/BUILD.bazel b/tests/src/BUILD.bazel new file mode 100644 index 0000000..1037dfb --- /dev/null +++ b/tests/src/BUILD.bazel @@ -0,0 +1,12 @@ +load("@rules_python//python:defs.bzl", "py_test") + +py_test( + name = "android_lint_output_validator_test", + srcs = ["android_lint_output_validator_test.py"], + deps = ["//src:android_lint_output_validator_lib"], +) + +test_suite( + name = "tests", + tests = [":android_lint_output_validator_test"], +) diff --git a/tests/src/android_lint_output_validator_test.py b/tests/src/android_lint_output_validator_test.py new file mode 100644 index 0000000..57eb7ea --- /dev/null +++ b/tests/src/android_lint_output_validator_test.py @@ -0,0 +1,70 @@ +import contextlib +import io +import tempfile +import unittest +from pathlib import Path + +from src.android_lint_output_validator import validate_output + + +class AndroidLintOutputValidatorTest(unittest.TestCase): + def test_clean_report_passes(self) -> None: + self.assertEqual(0, self._validate("<issues/>")) + + def test_regular_issue_fails_and_prints_report(self) -> None: + report = '<issues><issue id="DefaultLocale" message="Use Locale.ROOT"/></issues>' + output = io.StringIO() + + with contextlib.redirect_stdout(output): + exit_code = self._validate(report) + + self.assertEqual(1, exit_code) + self.assertIn("DefaultLocale", output.getvalue()) + + def test_baseline_metadata_issue_is_ignored(self) -> None: + report = '<issues><issue id="LintBaseline" message="Baseline information"/></issues>' + self.assertEqual(0, self._validate(report)) + + def test_fixed_baseline_issue_is_ignored(self) -> None: + report = ( + '<issues><issue id="ObsoleteLintCustomCheck" ' + 'message="Some baseline issues are missing; perhaps they have been fixed"/></issues>' + ) + self.assertEqual(0, self._validate(report)) + + def test_malformed_report_fails(self) -> None: + output = io.StringIO() + + with contextlib.redirect_stderr(output): + exit_code = self._validate("not xml") + + self.assertEqual(1, exit_code) + self.assertIn("Unable to parse Android lint report", output.getvalue()) + + def test_wrong_root_element_fails(self) -> None: + output = io.StringIO() + + with contextlib.redirect_stderr(output): + exit_code = self._validate("<report/>") + + self.assertEqual(1, exit_code) + self.assertIn("expected <issues> root element", output.getvalue()) + + def test_issue_without_required_attributes_fails(self) -> None: + output = io.StringIO() + + with contextlib.redirect_stderr(output): + exit_code = self._validate('<issues><issue id="DefaultLocale"/></issues>') + + self.assertEqual(1, exit_code) + self.assertIn("expected each <issue> to have id and message attributes", output.getvalue()) + + def _validate(self, report: str) -> int: + with tempfile.TemporaryDirectory() as temp_dir: + report_path = Path(temp_dir) / "lint.xml" + report_path.write_text(report) + return validate_output(str(report_path)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/src/cli/AndroidLintRunnerTest.kt b/tests/src/cli/AndroidLintRunnerTest.kt new file mode 100644 index 0000000..3c50752 --- /dev/null +++ b/tests/src/cli/AndroidLintRunnerTest.kt @@ -0,0 +1,95 @@ +package com.rules.android.lint.cli + +import com.android.tools.lint.Main +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.util.jar.JarEntry +import java.util.jar.JarOutputStream + +@RunWith(JUnit4::class) +class AndroidLintRunnerTest { + @get:Rule + val temporaryFolder = TemporaryFolder() + + @Before + fun setUp() { + Main.reset() + } + + @Test + fun `project includes explicit and aar embedded lint checks`() { + val lintCli = writeStubLintJar("lint-cli.jar") + val customRule = writeStubLintJar("custom-rule.jar") + val aar = temporaryFolder.newFile("fixture.aar").toPath() + val aarDirectory = temporaryFolder.newFolder("fixture-aar").toPath() + val embeddedRule = aarDirectory.resolve("lint.jar") + Files.copy(writeStubLintJar("embedded-rule.jar"), embeddedRule) + val output = temporaryFolder.root.toPath().resolve("output.xml") + val workingDirectory = temporaryFolder.newFolder("working").toPath() + val args = + AndroidLintActionArgs.parseArgs( + listOf( + "--android-lint-cli-tool", + lintCli.toString(), + "--label", + "runner-test", + "--output", + output.toString(), + "--custom-rule", + customRule.toString(), + "--classpath-aar", + "$aar:$aarDirectory", + "--compile-sdk-version", + "34", + "--java-language-level", + "17", + "--kotlin-language-level", + "1.9", + ), + ) + + val exitCode = + AndroidLintRunner(AndroidLintCliInvokerCache(parentClassloader = javaClass.classLoader)) + .runAndroidLint(args, workingDirectory) + + val projectFile = + Paths.get( + Main.recordedRuns + .single() + .args + .argumentAfter("--project"), + ) + val projectXml = Files.readString(projectFile) + assertThat(exitCode).isEqualTo(0) + assertThat(projectXml) + .contains("<lint-checks jar=\"${customRule.toAbsolutePath()}\"/>") + .contains("<lint-checks jar=\"${embeddedRule.toAbsolutePath()}\"/>") + .contains( + "<aar extracted=\"${aarDirectory.toAbsolutePath()}\" file=\"${aar.toAbsolutePath()}\"/>", + ) + } + + private fun writeStubLintJar(name: String): Path { + val jar = temporaryFolder.newFile(name).toPath() + JarOutputStream(Files.newOutputStream(jar)).use { stream -> + stream.putNextEntry(JarEntry("stub.txt")) + stream.write("stub".toByteArray()) + stream.closeEntry() + } + return jar + } +} + +private fun List<String>.argumentAfter(argument: String): String { + val index = indexOf(argument) + check(index >= 0 && index + 1 < size) { "Missing value for $argument in $this" } + return this[index + 1] +} diff --git a/tests/src/cli/BUILD b/tests/src/cli/BUILD index 168711f..fdbe711 100644 --- a/tests/src/cli/BUILD +++ b/tests/src/cli/BUILD @@ -62,6 +62,18 @@ kt_jvm_test( ], ) +kt_jvm_test( + name = "AndroidLintRunnerTest", + srcs = ["AndroidLintRunnerTest.kt"], + associates = ["//src/cli:lint"], + test_class = "com.rules.android.lint.cli.AndroidLintRunnerTest", + deps = [ + "//tests/src/cli/stubs", + "@rules_android_lint_test_deps//:junit_junit", + "@rules_android_lint_test_deps//:org_assertj_assertj_core", + ], +) + android_lint_test( name = "AndroidLintProjectTest_lint_test", srcs = ["AndroidLintProjectTest.kt"], @@ -69,6 +81,13 @@ android_lint_test( visibility = ["//visibility:private"], ) +android_lint_test( + name = "AndroidLintRunnerTest_lint_test", + srcs = ["AndroidLintRunnerTest.kt"], + lib = ":AndroidLintRunnerTest", + visibility = ["//visibility:private"], +) + ktlint_test( name = "lint_ktlint_test", srcs = glob(["*.kt"]), @@ -90,5 +109,6 @@ test_suite( ":AndroidLintActionTest", ":AndroidLintCliInvokerCacheTest", ":AndroidLintProjectTest", + ":AndroidLintRunnerTest", ], ) From 4cef210a72dae68ed1ea832ce6177436c09c9cd6 Mon Sep 17 00:00:00 2001 From: Ben Lee <ben@ben.cm> Date: Thu, 16 Jul 2026 01:00:34 +0100 Subject: [PATCH 2/7] Zipper --- tests/integration/BUILD.bazel | 6 ---- tests/integration/fixture_aar.bzl | 31 ++++++++++++++------ tests/integration/fixture_aar_builder.py | 36 ------------------------ 3 files changed, 22 insertions(+), 51 deletions(-) delete mode 100644 tests/integration/fixture_aar_builder.py diff --git a/tests/integration/BUILD.bazel b/tests/integration/BUILD.bazel index e5a8048..9282c5b 100644 --- a/tests/integration/BUILD.bazel +++ b/tests/integration/BUILD.bazel @@ -1,14 +1,8 @@ load("@rules_android//rules:rules.bzl", "aar_import") -load("@rules_python//python:defs.bzl", "py_binary") load("//rules:defs.bzl", "android_lint", "android_lint_test") load(":fixture_aar.bzl", "lint_aar_fixture") load(":lint_report_test.bzl", "lint_report_test") -py_binary( - name = "fixture_aar_builder", - srcs = ["fixture_aar_builder.py"], -) - filegroup( name = "basic_clean_lib", srcs = ["fixtures/basic/Clean.java"], diff --git a/tests/integration/fixture_aar.bzl b/tests/integration/fixture_aar.bzl index e9ad482..28881b0 100644 --- a/tests/integration/fixture_aar.bzl +++ b/tests/integration/fixture_aar.bzl @@ -2,17 +2,30 @@ def _lint_aar_fixture_impl(ctx): output = ctx.actions.declare_file(ctx.label.name + ".aar") + classes_jar = ctx.actions.declare_file(ctx.label.name + "_classes.jar") + + ctx.actions.run( + executable = ctx.executable._zipper, + outputs = [classes_jar], + arguments = [ + "c", + classes_jar.path, + "META-INF/MANIFEST.MF=", + ], + mnemonic = "BuildEmptyLintAarClassesJar", + progress_message = "Building empty classes jar for lint AAR fixture %{label}", + ) ctx.actions.run( - executable = ctx.executable._builder, - inputs = [ctx.file.lint_jar, ctx.file.manifest], + executable = ctx.executable._zipper, + inputs = [classes_jar, ctx.file.lint_jar, ctx.file.manifest], outputs = [output], arguments = [ - "--lint-jar", - ctx.file.lint_jar.path, - "--manifest", - ctx.file.manifest.path, - "--output", + "c", output.path, + "AndroidManifest.xml={}".format(ctx.file.manifest.path), + "classes.jar={}".format(classes_jar.path), + "R.txt=", + "lint.jar={}".format(ctx.file.lint_jar.path), ], mnemonic = "BuildLintAarFixture", progress_message = "Building lint AAR fixture %{label}", @@ -30,8 +43,8 @@ lint_aar_fixture = rule( allow_single_file = [".xml"], mandatory = True, ), - "_builder": attr.label( - default = Label("//tests/integration:fixture_aar_builder"), + "_zipper": attr.label( + default = Label("@bazel_tools//tools/zip:zipper"), cfg = "exec", executable = True, ), diff --git a/tests/integration/fixture_aar_builder.py b/tests/integration/fixture_aar_builder.py deleted file mode 100644 index 4fd419e..0000000 --- a/tests/integration/fixture_aar_builder.py +++ /dev/null @@ -1,36 +0,0 @@ -import argparse -import io -import zipfile -from pathlib import Path - - -_FIXED_TIMESTAMP = (1980, 1, 1, 0, 0, 0) - - -def _write(archive: zipfile.ZipFile, name: str, content: bytes) -> None: - entry = zipfile.ZipInfo(name, date_time=_FIXED_TIMESTAMP) - entry.compress_type = zipfile.ZIP_STORED - entry.external_attr = 0o644 << 16 - archive.writestr(entry, content) - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument("--lint-jar", required=True) - parser.add_argument("--manifest", required=True) - parser.add_argument("--output", required=True) - args = parser.parse_args() - - classes = io.BytesIO() - with zipfile.ZipFile(classes, "w"): - pass - - with zipfile.ZipFile(args.output, "w") as archive: - _write(archive, "AndroidManifest.xml", Path(args.manifest).read_bytes()) - _write(archive, "classes.jar", classes.getvalue()) - _write(archive, "R.txt", b"") - _write(archive, "lint.jar", Path(args.lint_jar).read_bytes()) - - -if __name__ == "__main__": - main() From a6df598000bd85467f0f3db20874be6d6fb1e7ca Mon Sep 17 00:00:00 2001 From: Ben Lee <ben@ben.cm> Date: Thu, 16 Jul 2026 01:02:45 +0100 Subject: [PATCH 3/7] Docs --- docs/rules.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/rules.md b/docs/rules.md index 733cb58..237ad0b 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -87,3 +87,5 @@ Info needed to evaluate lint results | Name | Description | | :------------- | :------------- | | <a id="AndroidLintResultsInfo-output"></a>output | The Android Lint baseline output | + + From 31fdeca815e9b3ba1e7d014708fe614c33ed7aad Mon Sep 17 00:00:00 2001 From: Ben Lee <ben@ben.cm> Date: Thu, 16 Jul 2026 01:15:00 +0100 Subject: [PATCH 4/7] Don't fail-fast on CI --- .github/workflows/ci.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 20118e1..0d2117e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -19,7 +19,6 @@ concurrency: jobs: tests: strategy: - fail-fast: false matrix: os: [ubuntu-latest] bazel_version: ["8.x", "9.x"] @@ -42,7 +41,6 @@ jobs: run: bazel test //... integration-tests: strategy: - fail-fast: false matrix: os: [ubuntu-latest] directory: [e2e/simple-android] From 86a54ba8c04c74af34337e9220bc892b8e0fe400 Mon Sep 17 00:00:00 2001 From: Ben Lee <ben@ben.cm> Date: Thu, 16 Jul 2026 01:27:54 +0100 Subject: [PATCH 5/7] Drop the local exec from the e2e matrix --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0d2117e..93c2ebc 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -44,7 +44,7 @@ jobs: matrix: os: [ubuntu-latest] directory: [e2e/simple-android] - strategy: [local, worker] + strategy: [worker] bazel_version: ["8.x", "9.x"] runs-on: ${{ matrix.os }} steps: From 3e330e3622e9b62de236961e1fab0f9f335b7460 Mon Sep 17 00:00:00 2001 From: Ben Lee <ben@ben.cm> Date: Thu, 16 Jul 2026 01:28:28 +0100 Subject: [PATCH 6/7] repin --- maven_install.json | 103 ++++++++++++++++++++++++++++++---------- maven_install_test.json | 8 +++- 2 files changed, 84 insertions(+), 27 deletions(-) diff --git a/maven_install.json b/maven_install.json index e59ab4a..4255a60 100755 --- a/maven_install.json +++ b/maven_install.json @@ -524,8 +524,12 @@ "net.java.dev.jna:jna-platform", "org.jetbrains.kotlin:kotlin-stdlib-jdk8" ], - "com.android.tools:dvlib": ["com.android.tools:common"], - "com.android.tools:play-sdk-proto": ["com.google.protobuf:protobuf-java"], + "com.android.tools:dvlib": [ + "com.android.tools:common" + ], + "com.android.tools:play-sdk-proto": [ + "com.google.protobuf:protobuf-java" + ], "com.android.tools:repository": [ "com.android.tools.analytics-library:shared", "com.android.tools:common", @@ -576,7 +580,9 @@ "com.google.j2objc:j2objc-annotations", "org.checkerframework:checker-qual" ], - "com.google.jimfs:jimfs": ["com.google.guava:guava"], + "com.google.jimfs:jimfs": [ + "com.google.guava:guava" + ], "com.sun.istack:istack-commons-runtime": [ "jakarta.activation:jakarta.activation-api" ], @@ -584,11 +590,15 @@ "com.xenomachina:xenocom", "org.jetbrains.kotlin:kotlin-stdlib" ], - "com.xenomachina:xenocom": ["org.jetbrains.kotlin:kotlin-stdlib"], + "com.xenomachina:xenocom": [ + "org.jetbrains.kotlin:kotlin-stdlib" + ], "jakarta.xml.bind:jakarta.xml.bind-api": [ "jakarta.activation:jakarta.activation-api" ], - "net.java.dev.jna:jna-platform": ["net.java.dev.jna:jna"], + "net.java.dev.jna:jna-platform": [ + "net.java.dev.jna:jna" + ], "org.apache.commons:commons-compress": [ "commons-codec:commons-codec", "commons-io:commons-io", @@ -606,7 +616,9 @@ "org.bouncycastle:bcprov-jdk18on", "org.bouncycastle:bcutil-jdk18on" ], - "org.bouncycastle:bcutil-jdk18on": ["org.bouncycastle:bcprov-jdk18on"], + "org.bouncycastle:bcutil-jdk18on": [ + "org.bouncycastle:bcprov-jdk18on" + ], "org.glassfish.jaxb:jaxb-runtime": [ "com.sun.istack:istack-commons-runtime", "com.sun.xml.fastinfoset:FastInfoset", @@ -618,7 +630,9 @@ "org.jetbrains.kotlin:kotlin-reflect": [ "org.jetbrains.kotlin:kotlin-stdlib" ], - "org.jetbrains.kotlin:kotlin-stdlib": ["org.jetbrains:annotations"], + "org.jetbrains.kotlin:kotlin-stdlib": [ + "org.jetbrains:annotations" + ], "org.jetbrains.kotlin:kotlin-stdlib-jdk7": [ "org.jetbrains.kotlin:kotlin-stdlib" ], @@ -637,9 +651,16 @@ "jakarta.activation:jakarta.activation-api", "jakarta.xml.bind:jakarta.xml.bind-api" ], - "org.ow2.asm:asm-analysis": ["org.ow2.asm:asm-tree"], - "org.ow2.asm:asm-commons": ["org.ow2.asm:asm", "org.ow2.asm:asm-tree"], - "org.ow2.asm:asm-tree": ["org.ow2.asm:asm"] + "org.ow2.asm:asm-analysis": [ + "org.ow2.asm:asm-tree" + ], + "org.ow2.asm:asm-commons": [ + "org.ow2.asm:asm", + "org.ow2.asm:asm-tree" + ], + "org.ow2.asm:asm-tree": [ + "org.ow2.asm:asm" + ] }, "packages": { "com.android.tools.analytics-library:protos": [ @@ -667,7 +688,9 @@ "com.android.builder.model.v2.models", "com.android.builder.model.v2.models.ndk" ], - "com.android.tools.build:manifest-merger": ["com.android.manifmerger"], + "com.android.tools.build:manifest-merger": [ + "com.android.manifmerger" + ], "com.android.tools.ddms:ddmlib": [ "com.android.commands.am", "com.android.ddmlib", @@ -2227,7 +2250,9 @@ "com.android.tools.lint.checks.optional", "com.android.tools.lint.checks.plurals" ], - "com.android.tools.lint:lint-model": ["com.android.tools.lint.model"], + "com.android.tools.lint:lint-model": [ + "com.android.tools.lint.model" + ], "com.android.tools:annotations": [ "com.android.annotations", "com.android.annotations.concurrency" @@ -2258,8 +2283,12 @@ "com.android.xml", "com.android.xml.sax" ], - "com.android.tools:dvlib": ["com.android.dvlib"], - "com.android.tools:play-sdk-proto": ["com.android.tools.lint.checks"], + "com.android.tools:dvlib": [ + "com.android.dvlib" + ], + "com.android.tools:play-sdk-proto": [ + "com.android.tools.lint.checks" + ], "com.android.tools:repository": [ "com.android.repository", "com.android.repository.api", @@ -2402,8 +2431,12 @@ "com.google.common.xml", "com.google.thirdparty.publicsuffix" ], - "com.google.j2objc:j2objc-annotations": ["com.google.j2objc.annotations"], - "com.google.jimfs:jimfs": ["com.google.common.jimfs"], + "com.google.j2objc:j2objc-annotations": [ + "com.google.j2objc.annotations" + ], + "com.google.jimfs:jimfs": [ + "com.google.common.jimfs" + ], "com.google.protobuf:protobuf-java": [ "com.google.protobuf", "com.google.protobuf.compiler" @@ -2432,7 +2465,9 @@ "org.jvnet.fastinfoset.sax.helpers", "org.jvnet.fastinfoset.stax" ], - "com.xenomachina:kotlin-argparser": ["com.xenomachina.argparser"], + "com.xenomachina:kotlin-argparser": [ + "com.xenomachina.argparser" + ], "com.xenomachina:xenocom": [ "com.xenomachina.common", "com.xenomachina.text", @@ -2468,7 +2503,9 @@ "org.apache.commons.logging", "org.apache.commons.logging.impl" ], - "jakarta.activation:jakarta.activation-api": ["javax.activation"], + "jakarta.activation:jakarta.activation-api": [ + "javax.activation" + ], "jakarta.xml.bind:jakarta.xml.bind-api": [ "javax.xml.bind", "javax.xml.bind.annotation", @@ -2477,7 +2514,9 @@ "javax.xml.bind.helpers", "javax.xml.bind.util" ], - "javax.inject:javax.inject": ["javax.inject"], + "javax.inject:javax.inject": [ + "javax.inject" + ], "net.java.dev.jna:jna": [ "com.sun.jna", "com.sun.jna.internal", @@ -3112,11 +3151,23 @@ "org.intellij.lang.annotations", "org.jetbrains.annotations" ], - "org.jvnet.staxex:stax-ex": ["org.jvnet.staxex", "org.jvnet.staxex.util"], - "org.ow2.asm:asm": ["org.objectweb.asm", "org.objectweb.asm.signature"], - "org.ow2.asm:asm-analysis": ["org.objectweb.asm.tree.analysis"], - "org.ow2.asm:asm-commons": ["org.objectweb.asm.commons"], - "org.ow2.asm:asm-tree": ["org.objectweb.asm.tree"] + "org.jvnet.staxex:stax-ex": [ + "org.jvnet.staxex", + "org.jvnet.staxex.util" + ], + "org.ow2.asm:asm": [ + "org.objectweb.asm", + "org.objectweb.asm.signature" + ], + "org.ow2.asm:asm-analysis": [ + "org.objectweb.asm.tree.analysis" + ], + "org.ow2.asm:asm-commons": [ + "org.objectweb.asm.commons" + ], + "org.ow2.asm:asm-tree": [ + "org.objectweb.asm.tree" + ] }, "repositories": { "https://maven.google.com/": [ @@ -3367,7 +3418,9 @@ ] }, "org.glassfish.jaxb:jaxb-runtime": { - "javax.xml.bind.JAXBContext": ["com.sun.xml.bind.v2.ContextFactory"] + "javax.xml.bind.JAXBContext": [ + "com.sun.xml.bind.v2.ContextFactory" + ] }, "org.jetbrains.kotlin:kotlin-reflect": { "kotlin.reflect.jvm.internal.impl.builtins.BuiltInsLoader": [ diff --git a/maven_install_test.json b/maven_install_test.json index e150496..93fbf3d 100755 --- a/maven_install_test.json +++ b/maven_install_test.json @@ -44,8 +44,12 @@ "com.slack.lint.compose:compose-lint-checks": [ "com.google.auto.service:auto-service-annotations" ], - "junit:junit": ["org.hamcrest:hamcrest-core"], - "org.assertj:assertj-core": ["net.bytebuddy:byte-buddy"] + "junit:junit": [ + "org.hamcrest:hamcrest-core" + ], + "org.assertj:assertj-core": [ + "net.bytebuddy:byte-buddy" + ] }, "packages": { "com.google.auto.service:auto-service-annotations": [ From 1da423faa87aaddb02379600b72f16d70389fb67 Mon Sep 17 00:00:00 2001 From: Ben Lee <ben@ben.cm> Date: Thu, 16 Jul 2026 01:31:49 +0100 Subject: [PATCH 7/7] Enable worker/local matrix for test shards --- .github/workflows/ci.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 93c2ebc..d9e16ee 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -21,6 +21,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] + strategy: [local, worker] bazel_version: ["8.x", "9.x"] runs-on: ${{ matrix.os }} steps: @@ -38,7 +39,7 @@ jobs: - name: "Running tests //..." env: USE_BAZEL_VERSION: ${{ matrix.bazel_version }} - run: bazel test //... + run: bazel test //... --strategy=AndroidLint=${{ matrix.strategy }} integration-tests: strategy: matrix: