From 327a365550de055f5399c0e20f018c69b9bd74b0 Mon Sep 17 00:00:00 2001 From: Wanja Krah Date: Fri, 30 Oct 2026 15:50:51 +0100 Subject: [PATCH] S8 integrate pinned Palantir formatting through OpenRewrite --- README.md | 55 ++++++++ ci/CiTests.java | 2 + ci/Pipeline.java | 2 +- pom.xml | 49 +++++++ specs/S8-formatting-evidence.md | 96 +++++++++++++ .../S8_palentir-formatting-be | 30 ++-- tooling/format/Format.java | 131 ++++++++++++++++++ tooling/format/benchmark-downloads.py | 31 +++++ tooling/format/benchmark.py | 49 +++++++ tooling/format/test.py | 53 +++++++ 10 files changed, 483 insertions(+), 15 deletions(-) create mode 100644 specs/S8-formatting-evidence.md rename specs/{refined => in_review}/S8_palentir-formatting-be (90%) create mode 100644 tooling/format/Format.java create mode 100644 tooling/format/benchmark-downloads.py create mode 100644 tooling/format/benchmark.py create mode 100644 tooling/format/test.py diff --git a/README.md b/README.md index 2eb1c23..928c2a4 100644 --- a/README.md +++ b/README.md @@ -253,3 +253,58 @@ Only successful `main` runs publish images to the Synology registry. See [the CI management guide](docs/gitea-ci.md) for runner setup, registry secrets, image names, diagnostics and recovery, and [validation evidence](docs/s4-validation.md) for the current operational status. + +### Java formatting (S8) + +Maven is the authoritative formatting path. The root build runs the actual +Palantir Java Format **2.101.0**, Palantir style, through a custom OpenRewrite +**8.63.4** text recipe in `tooling/format/Format.java`. The pinned +`exec-maven-plugin` **3.6.3** launches that helper with JDK 25. Native import +ordering and unused-import removal are enabled; extra Javadoc formatting is off. +No OpenRewrite AutoFormat or additional refactoring recipes run. + +From the repository root: + +```sh +mvn -N exec:exec@format # apply, without application compilation +mvn -N exec:exec@format -Dformat.mode=check # check, without application compilation +mvn compile # applies during validate, before compilation +mvn -Dformat.mode=check clean verify # CI: fails before compilation on differences +``` + +Root `compile`, `test`, `verify` and `install` may rewrite maintained Java files +in the validate phase. CI's Java orchestration passes `-Dformat.mode=check` and +never repairs source files. On failure, the output lists paths and the apply +command. Apply locally, inspect and commit the result, then repeat the check. +Run root Maven commands; selecting only the backend with `-pl agiledancers-be` +bypasses the root formatting execution. + +The scope is every `.java` file recursively under `agiledancers-be/src/main/java`, +`agiledancers-be/src/test/java`, `ci` (including the runner helper), and the +formatter helper itself. Generated `target` files and frontend files are excluded. +The adapter keeps paths and UTF-8 content, computes changes before writing, and +uses atomic per-file replacements. Unsupported syntax fails with its path/cause. +The source launcher compiles the adapter directly in memory from a clean checkout; +no custom recipe jar needs compiling or installing beforehand. Maven downloads +only pinned external tooling dependencies on first use. + +Successfully checked/formatted content hashes are cached in +`target/palantir-cache.properties`; new or changed files are always processed. +The root POM, adapter source, dependency classpath and JDK version invalidate the cache. `clean` removes +it. Check and apply share the engine, options, scope and successful-content cache; +a failing check does not write sources or update the cache. For controlled timing +comparisons only, `-Dformat.skip=true` disables the execution. + +Editors are optional. Palantir supplies an IntelliJ plugin (see the +[upstream instructions](https://github.com/palantir/palantir-java-format#intellij-plugin)). +Use it only with the same engine version/style and Javadoc setting; if the editor +cannot match them, use Maven to format instead. Editor import optimization or +format-on-save must not override Maven's result. Other editors can invoke the +standalone Maven apply command as an external tool. + +Verification and measurements are recorded in +[the S8 evidence](specs/S8-formatting-evidence.md). Reproduce the isolated failure, +UTF-8, import, cache and idempotence checks with +`python3 tooling/format/test.py`, and the sequential end-to-end build measurements +with `python3 tooling/format/benchmark.py` (requires the ordinary backend test +prerequisites). Results are written under `target/`. diff --git a/ci/CiTests.java b/ci/CiTests.java index f1be880..adfc81a 100644 --- a/ci/CiTests.java +++ b/ci/CiTests.java @@ -98,6 +98,8 @@ class CiTests { f.fake.mavenFailure = true; try { f.pipeline.verify(); throw new AssertionError("Expected failure"); } catch (Commands.Failure error) { equal(42,error.exitCode); } + equal(List.of("mvn", "-B", "-ntp", "-Dformat.mode=check", "clean", "verify"), + f.fake.calls.stream().filter(a -> a.getFirst().equals("mvn")).findFirst().orElseThrow()); equal(false,Files.exists(f.root.resolve("ci/artifacts/verified-commit"))); equal(0L,f.calls("build")); equal(0L,f.calls("push")); } diff --git a/ci/Pipeline.java b/ci/Pipeline.java index f265fe8..a26a91a 100644 --- a/ci/Pipeline.java +++ b/ci/Pipeline.java @@ -66,7 +66,7 @@ class Pipeline { try { requirePlatform(); isolateDocker(); check(commit.equals(capture("git", "rev-parse", "HEAD")), "Checkout does not match pushed commit"); - run("java", "-version"); run("node", "--version"); run("mvn", "-B", "-ntp", "clean", "verify"); + run("java", "-version"); run("node", "--version"); run("mvn", "-B", "-ntp", "-Dformat.mode=check", "clean", "verify"); CiTests.run(); Path frontend = root.resolve("agiledancers-fe"); verifyFrontend(); diff --git a/pom.xml b/pom.xml index bdd4768..22ca652 100644 --- a/pom.xml +++ b/pom.xml @@ -24,6 +24,11 @@ UTF-8 3.40.1 + apply + false + 2.101.0 + 8.63.4 + 9.15.6755 1.18.48 @@ -77,6 +82,50 @@ + + org.codehaus.mojo + exec-maven-plugin + 3.6.3 + false + + + format + validate + exec + + + + ${format.skip} + ${java.home}/bin/java + false + true + + --add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED + --add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED + --add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED + --add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED + --add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED + --add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED + --class-path + ${maven.multiModuleProjectDirectory}/tooling/format/Format.java + ${maven.multiModuleProjectDirectory} + ${format.mode} + + + + + com.palantir.javaformat + palantir-java-format + ${palantir-java-format.version} + + + org.openrewrite + rewrite-core + ${rewrite-core.version} + + + + org.apache.maven.plugins maven-compiler-plugin diff --git a/specs/S8-formatting-evidence.md b/specs/S8-formatting-evidence.md new file mode 100644 index 0000000..0725125 --- /dev/null +++ b/specs/S8-formatting-evidence.md @@ -0,0 +1,96 @@ +# S8 implementation evidence + +Measured on 2026-10-30 on Linux x86_64 (kernel 7.0.0-38-generic), AMD Ryzen 7 +7730U, 16 logical CPUs; Ubuntu OpenJDK 25.0.4.1, Maven 3.9.16. These are local +wall-clock measurements, not universal performance guarantees. + +## Integration and scope + +- exec-maven-plugin 3.6.3; OpenRewrite core 8.63.4; Palantir Java Format 2.101.0. +- Custom `Format.PalantirRecipe` processes OpenRewrite PlainText source files + using Palantir's `formatSourceAndFixImports`, Palantir style, `formatJavadoc(false)`. + No Java type attribution, OpenRewrite Java parser or AutoFormat is involved. +- 73 maintained Java files: 65 backend production/test files, 7 CI/runner helpers, + and the formatter helper. Recursive discovery excludes generated build output. +- Root validate executes before compilation; compile/test/verify/install all reach + validate. CI explicitly passes check mode to clean verify. Standalone commands + run only the root exec execution and do not compile the backend. +- Bootstrap was first executed with neither Palantir nor OpenRewrite in the local + Maven repository. The helper is source-launched, with dependencies supplied by + Maven's plugin classpath. There is no project recipe artifact to preinstall. + The isolated regression fixture also has no compiled adapter or backend module. + +## Correctness + +- Full `mvn -B -ntp clean verify` passed after formatting: 33 backend tests, + zero failures/errors/skips. Both benchmark clean verifications also pass. +- `java ci/Ci.java test`: 17 passed, including assertion of the exact check-only + Maven invocation and that Maven failure removes the success marker and prevents + image build/publication. +- `python3 tooling/format/test.py` checks an isolated source-launcher checkout: + read-only check failure with path and apply command; native unused-import + removal; UTF-8 non-ASCII preservation; check/apply agreement; second uncached + check with no differences; malformed Java fails with its path and leaves the + source bytes unchanged; a newly introduced file cannot evade the content cache. +- All current Java files have been processed by the real engine on JDK 25, + including records, switch expressions/multiple labels, pattern instanceof, + lambdas and source-launcher CI classes. Tests compile/run the resulting sources. + This demonstrates the syntax used here, not every possible Java 25 construct. +- Palantir uses javac internals (explicit JVM exports are in the root POM) and its + modern AST visitor. Recheck formatter compatibility when upgrading the JDK. + Compact source files / instance main methods are a known upstream limitation + ([upstream issue](https://github.com/palantir/palantir-java-format/issues/1602)); + they are not used here. Unsupported future syntax fails, rather than being + silently excluded. Extra Markdown/Javadoc formatting is disabled. +- The initial Java rewrite is committed separately from integration changes; + its only transformations are the formatter's native output and import handling. + +## Performance + +`python3 tooling/format/benchmark.py` runs sequentially, with normal dependencies +and OS filesystem caches warm. Each configuration receives a compile warmup. +The baseline temporarily removes the entire exec plugin declaration from the +root POM, then restores its original bytes before the formatting build. Therefore +these comparisons include plugin loading, dependency resolution, JVM launch, +source-launcher compilation, hashing, cache IO and recipe execution. They do not +compare only the inner formatter function or a skipped plugin against itself. + +The successful-content cache is warm for unchanged builds; it is part of the +integration. Changed/new files are processed by Palantir. Clean builds remove the +cache and process every maintained Java file. Each run's stdout and elapsed time +are saved under `target/`; the benchmark restores the root POM and changed helper. + +| Repeated compile | Baseline (s) | Integration (s) | Added overhead (s) | +| --- | ---: | ---: | ---: | +| 1 | 4.887 | 6.414 | 1.527 | +| 2 | 4.821 | 5.964 | 1.143 | +| 3 | 4.559 | 6.024 | 1.465 | + +Maximum observed added overhead: **1.527 seconds**, below the agreed three-second +limit. Baseline compile warmup: 5.307 s; integration warmup: 7.604 s. + +The changed-file case appends a newline to `ci/runner/WaitForDocker.java`, then +runs `mvn -B -ntp compile`: **5.506 s**, successful; Palantir processes that +changed file. The script restores its original bytes afterward. + +Clean baseline `mvn -B -ntp clean verify`: **24.797 s**. Clean integration +`mvn -B -ntp -Dformat.mode=check clean verify`: **27.747 s**, all 73 files +processed, no differences, all tests pass. The three-second criterion applies +to repeated unchanged builds, not the clean-build comparison. + +### First-use downloads (separate measurement) + +A disposable root POM with the same pinned plugin/dependencies and an empty +`-Dmaven.repo.local` replaces only the Java executable with `/usr/bin/true`. +Thus dependency resolution runs without compiling the launcher or executing the +formatter/application. Command: +`mvn -N -B -ntp -Dmaven.repo.local= org.codehaus.mojo:exec-maven-plugin:3.6.3:exec@format`. +Cold resolution took **9.041 s**; the same command with that repository populated +took **1.826 s**. The **7.215 s** cold-versus-warm difference represents first-use +network/repository population overhead in this environment, separately from the +warm-cache build measurements above. Cold timing also includes root POM/BOM and +plugin bootstrap resolution; it is not reported as pure formatter execution. +Reproduce with `python3 tooling/format/benchmark-downloads.py` (Linux, network +access). The initial real source-launcher bootstrap/check also successfully +processed all files; its expected failure listed formatting differences, rather +than a missing project recipe or parser/bootstrap error. diff --git a/specs/refined/S8_palentir-formatting-be b/specs/in_review/S8_palentir-formatting-be similarity index 90% rename from specs/refined/S8_palentir-formatting-be rename to specs/in_review/S8_palentir-formatting-be index f1cb0e9..be9e5b2 100644 --- a/specs/refined/S8_palentir-formatting-be +++ b/specs/in_review/S8_palentir-formatting-be @@ -1,6 +1,6 @@ # S8 — Palantir formatting for Java -Status: REFINED +Status: IN_REVIEW Agreed after the grill-me review; shared understanding confirmed. @@ -163,30 +163,30 @@ style or silently take precedence over the Maven result. ## Acceptance criteria -- [ ] The actual Palantir engine formats all maintained backend Java sources, +- [x] The actual Palantir engine formats all maintained backend Java sources, Java tests and `ci/*.java`; generated code is excluded. -- [ ] OpenRewrite integration, custom adapter/recipe and bootstrap are implemented +- [x] OpenRewrite integration, custom adapter/recipe and bootstrap are implemented and reproducible from a clean checkout without manual recipe installation. -- [ ] Plugin, formatter and adapter dependencies are pinned, with matching local +- [x] Plugin, formatter and adapter dependencies are pinned, with matching local and CI configuration. -- [ ] Local Maven builds apply formatting before compilation; `compile`, `test`, +- [x] Local Maven builds apply formatting before compilation; `compile`, `test`, `verify` and `install` reach the intended integration. -- [ ] Standalone apply and check commands work without the full application build. -- [ ] CI checks only, leaves maintained sources unchanged, reports formatting +- [x] Standalone apply and check commands work without the full application build. +- [x] CI checks only, leaves maintained sources unchanged, reports formatting differences and fails before image publication. -- [ ] Import ordering and unused-import removal follow Palantir; optional extra +- [x] Import ordering and unused-import removal follow Palantir; optional extra Javadoc reformatting remains disabled and no unrelated refactoring is added. -- [ ] All currently used Java 25 syntax is handled, or causes an explicit failure +- [x] All currently used Java 25 syntax is handled, or causes an explicit failure requiring resolution rather than silent exclusion. -- [ ] Apply/check results agree and a second apply produces no differences. -- [ ] Initial formatting is isolated in a formatting-only commit; application +- [x] Apply/check results agree and a second apply produces no differences. +- [x] Initial formatting is isolated in a formatting-only commit; application builds and tests remain green. -- [ ] Equivalent repeated unchanged-build measurements demonstrate at most three +- [x] Equivalent repeated unchanged-build measurements demonstrate at most three seconds of additional integration overhead. Changed-file and clean-build measurements and one-time downloads are reported separately. -- [ ] Failure to meet the performance bound is reported before changing the route +- [x] Failure to meet the performance bound is reported before changing the route or bound; a writing formatter is not killed to force compliance. -- [ ] README instructions cover local and CI commands, bootstrap, optional IDE +- [x] README instructions cover local and CI commands, bootstrap, optional IDE use, authoritative Maven formatting and verification evidence. ## Implementation handoff @@ -196,3 +196,5 @@ create an adapter, change Maven/CI behavior or reformat source files. Recipe loa Java 25 compatibility and performance must be demonstrated during implementation; there is no pre-existing project benchmark establishing the preferred route's performance. + +Implementation and measured acceptance evidence: [S8-formatting-evidence.md](../S8-formatting-evidence.md). diff --git a/tooling/format/Format.java b/tooling/format/Format.java new file mode 100644 index 0000000..d8b42da --- /dev/null +++ b/tooling/format/Format.java @@ -0,0 +1,131 @@ +import com.palantir.javaformat.java.Formatter; +import com.palantir.javaformat.java.JavaFormatterOptions; +import java.nio.file.*; +import java.security.MessageDigest; +import java.util.*; +import org.openrewrite.*; +import org.openrewrite.internal.InMemoryLargeSourceSet; +import org.openrewrite.text.*; + +/** Source-launcher bootstrap: no project-specific artifact is resolved or installed. */ +public class Format { + public static void main(String[] args) throws Exception { + Path root = Path.of(args[0]).toAbsolutePath().normalize(); + boolean check = switch (args[1]) { + case "check" -> true; + case "apply" -> false; + default -> throw new IllegalArgumentException("format.mode must be apply or check"); + }; + Path cacheFile = root.resolve("target/palantir-cache.properties"); + var cache = new Properties(); + if (Files.exists(cacheFile)) { + try (var input = Files.newInputStream(cacheFile)) { + cache.load(input); + } + } + String configuration = digest(Files.readString(root.resolve("pom.xml")) + + Files.readString(root.resolve("tooling/format/Format.java")) + + System.getProperty("java.version") + + System.getProperty("java.class.path")); + if (!configuration.equals(cache.getProperty("configuration"))) cache.clear(); + cache.setProperty("configuration", configuration); + List sources = new ArrayList<>(); + for (String directory : + List.of("agiledancers-be/src/main/java", "agiledancers-be/src/test/java", "ci", "tooling/format")) { + try (var paths = Files.walk(root.resolve(directory))) { + for (Path path : paths.filter(p -> p.toString().endsWith(".java")) + .sorted() + .toList()) { + try { + String content = Files.readString(path); + String key = root.relativize(path).toString(); + if (!digest(content).equals(cache.getProperty(key))) { + sources.add(PlainText.builder() + .sourcePath(root.relativize(path)) + .text(content) + .build()); + } + } catch (Exception error) { + throw new IllegalStateException(path + ": " + error.getMessage(), error); + } + } + } + } + var context = new InMemoryExecutionContext(error -> { + throw new IllegalStateException("Formatting failed: " + error.getMessage(), error); + }); + var results = new PalantirRecipe() + .run(new InMemoryLargeSourceSet(sources), context) + .getChangeset() + .getAllResults(); + for (Result result : results) { + System.out.println((check ? "Needs formatting: " : "Formatted: ") + + result.getAfter().getSourcePath()); + } + if (check && !results.isEmpty()) { + throw new IllegalStateException("Run mvn -N exec:exec@format to apply Palantir formatting."); + } + // Complete parsing/formatting before writing any maintained file. Each replacement is atomic. + if (!check) { + for (Result result : results) { + Path destination = root.resolve(result.getAfter().getSourcePath()); + Path temporary = Files.createTempFile(destination.getParent(), ".palantir-", ".tmp"); + try { + Files.writeString(temporary, result.getAfter().printAll()); + Files.setPosixFilePermissions(temporary, Files.getPosixFilePermissions(destination)); + Files.move( + temporary, + destination, + StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING); + } finally { + Files.deleteIfExists(temporary); + } + } + } + for (SourceFile source : sources) { + cache.setProperty( + source.getSourcePath().toString(), digest(Files.readString(root.resolve(source.getSourcePath())))); + } + Files.createDirectories(cacheFile.getParent()); + try (var output = Files.newOutputStream(cacheFile)) { + cache.store(output, "Successfully formatted content only"); + } + System.out.println( + "Palantir " + args[1] + ": " + sources.size() + " Java files, " + results.size() + " differences"); + } + + static String digest(String value) throws Exception { + return HexFormat.of() + .formatHex(MessageDigest.getInstance("SHA-256") + .digest(value.getBytes(java.nio.charset.StandardCharsets.UTF_8))); + } + + public static final class PalantirRecipe extends Recipe { + public String getDisplayName() { + return "Palantir Java Format"; + } + + public String getDescription() { + return "Apply the pinned Palantir engine and its native import handling."; + } + + public TreeVisitor getVisitor() { + return new PlainTextVisitor() { + final Formatter formatter = Formatter.createFormatter(JavaFormatterOptions.builder() + .style(JavaFormatterOptions.Style.PALANTIR) + .formatJavadoc(false) + .build()); + + @Override + public PlainText visitText(PlainText text, ExecutionContext context) { + try { + return text.withText(formatter.formatSourceAndFixImports(text.getText())); + } catch (Exception | LinkageError error) { + throw new IllegalStateException(text.getSourcePath() + ": " + error, error); + } + } + }; + } + } +} diff --git a/tooling/format/benchmark-downloads.py b/tooling/format/benchmark-downloads.py new file mode 100644 index 0000000..1dc6b2f --- /dev/null +++ b/tooling/format/benchmark-downloads.py @@ -0,0 +1,31 @@ +"""Measure dependency resolution separately; run from the root with network access. + +A disposable copy replaces the Java executable with /usr/bin/true, keeping the +pinned exec-plugin dependencies. No formatter/application code executes. The +second invocation uses the populated repository; neither invocation touches the +normal Maven cache. Logs and measurements are written under target/. +""" +from pathlib import Path +import json +import shutil +import subprocess +import tempfile +import time + +root = Path(__file__).resolve().parents[2] +(root / "target").mkdir(exist_ok=True) +with tempfile.TemporaryDirectory(prefix="s8-resolution-") as temporary: + fixture = Path(temporary) + shutil.copytree(root / ".mvn", fixture / ".mvn") + pom = (root / "pom.xml").read_text().replace("${java.home}/bin/java", "/usr/bin/true") + (fixture / "pom.xml").write_text(pom) + command = ["mvn", "-N", "-B", "-ntp", "-Dmaven.repo.local=" + str(fixture / "repository"), + "org.codehaus.mojo:exec-maven-plugin:3.6.3:exec@format"] + for label in ("cold-resolution", "warm-resolution"): + start = time.monotonic() + result = subprocess.run(command, cwd=fixture, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + elapsed = time.monotonic() - start + (root / "target" / ("s8-" + label + ".log")).write_text(result.stdout) + print(json.dumps(dict(label=label, seconds=round(elapsed, 3), exit=result.returncode)), flush=True) + if result.returncode: + raise RuntimeError(result.stdout) diff --git a/tooling/format/benchmark.py b/tooling/format/benchmark.py new file mode 100644 index 0000000..3cd5dcb --- /dev/null +++ b/tooling/format/benchmark.py @@ -0,0 +1,49 @@ +"""Run sequential, warm-cache end-to-end Maven comparisons from the repository root.""" +from pathlib import Path +import json +import subprocess +import time + +root = Path(__file__).resolve().parents[2] +results = [] +pom = root / "pom.xml" +original_pom = pom.read_bytes() +pom_text = original_pom.decode() +start = pom_text.rfind(" ", 0, pom_text.index("exec-maven-plugin")) +end = pom_text.index(" ", start) + len(" \n") +baseline_pom = (pom_text[:start] + pom_text[end:]).encode() + +def run(label, *arguments): + command = ["mvn", "-B", "-ntp", *arguments] + start = time.monotonic() + baseline = "baseline" in label + try: + if baseline: + pom.write_bytes(baseline_pom) + result = subprocess.run(command, cwd=root, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + finally: + pom.write_bytes(original_pom) + elapsed = time.monotonic() - start + (root / "target").mkdir(exist_ok=True) + (root / "target" / ("s8-" + label + ".log")).write_text(result.stdout) + row = dict(label=label, command=command, seconds=round(elapsed, 3), exit=result.returncode) + results.append(row) + print(json.dumps(row), flush=True) + if result.returncode: + raise RuntimeError(result.stdout) + +run("warmup", "compile") +run("baseline-warmup", "compile") +for number in range(3): + run("baseline-" + str(number), "compile") + run("formatted-" + str(number), "compile") +source = root / "ci/runner/WaitForDocker.java" +original = source.read_bytes() +try: + source.write_bytes(original + b"\n") + run("changed-file", "compile") +finally: + source.write_bytes(original) +run("clean-baseline", "clean", "verify") +run("clean-formatted", "-Dformat.mode=check", "clean", "verify") +(root / "target/s8-benchmark.json").write_text(json.dumps(results, indent=2) + "\n") diff --git a/tooling/format/test.py b/tooling/format/test.py new file mode 100644 index 0000000..2d72f1f --- /dev/null +++ b/tooling/format/test.py @@ -0,0 +1,53 @@ +"""Integration checks against an isolated checkout; run python3 tooling/format/test.py.""" +from pathlib import Path +import shutil +import subprocess +import tempfile + +root = Path(__file__).resolve().parents[2] +with tempfile.TemporaryDirectory(prefix="palantir-test-") as temporary: + fixture = Path(temporary) + for name in ("pom.xml", ".mvn", "tooling/format"): + source = root / name + destination = fixture / name + if source.is_dir(): + shutil.copytree(source, destination) + else: + shutil.copy2(source, destination) + for name in ("agiledancers-be/src/main/java", "agiledancers-be/src/test/java", "ci/runner"): + (fixture / name).mkdir(parents=True) + source = fixture / "ci/runner/Example.java" + original = "import java.util.List;\nclass Example{String text=\"héllo\";int value(){return 1;}}\n" + source.write_text(original) + + def run(mode, success=True): + result = subprocess.run( + ["mvn", "-N", "-B", "-ntp", "exec:exec@format", "-Dformat.mode=" + mode], + cwd=fixture, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + assert (result.returncode == 0) == success, result.stdout + return result.stdout + + output = run("check", False) + assert "ci/runner/Example.java" in output and "mvn -N exec:exec@format" in output + assert source.read_text() == original + run("apply") + formatted = source.read_text() + assert "import java.util.List" not in formatted and "héllo" in formatted + run("check") + run("apply") + assert source.read_text() == formatted + # Force a full (uncached) second pass to prove engine idempotence. + (fixture / "target/palantir-cache.properties").unlink() + run("check") + source.write_text(formatted + "class Broken { ??? }\n") + invalid = source.read_bytes() + output = run("apply", False) + assert "ci/runner/Example.java" in output + assert source.read_bytes() == invalid + # A new file is discovered even after a successful cached check. + source.write_text(formatted) + extra = fixture / "ci/New.java" + extra.write_text("class New{}") + run("check", False) + assert extra.read_text() == "class New{}" + print("PASS: check is read-only, apply/check agree, imports, UTF-8, idempotence, parser errors, new files")