252 lines
9.6 KiB
Python
252 lines
9.6 KiB
Python
import os
|
|
import shutil
|
|
import stat
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
SCRIPT = Path(__file__).resolve().parent / "format_native.sh"
|
|
DIGEST = "a16be01dcc480aab2f55f444b620142152f66e31564b3b9376506d624c28a2ad"
|
|
JAVA_DIAGNOSTIC = (
|
|
"A working JDK 17+ is required for Kotlin formatting. "
|
|
"Set JAVA_HOME to a JDK 17+ installation or put java on PATH."
|
|
)
|
|
|
|
|
|
def executable(path: Path, contents: str) -> Path:
|
|
path.write_text(contents, encoding="utf-8")
|
|
path.chmod(path.stat().st_mode | stat.S_IXUSR)
|
|
return path
|
|
|
|
|
|
class NativeFormatterTest(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.temp = tempfile.TemporaryDirectory()
|
|
self.root = Path(self.temp.name)
|
|
(self.root / "scripts").mkdir()
|
|
shutil.copy2(SCRIPT, self.root / "scripts" / "format_native.sh")
|
|
(self.root / "android").mkdir()
|
|
(self.root / "android" / "fixture.kt").write_text("class Fixture\n", encoding="utf-8")
|
|
|
|
self.bin = self.root / "bin"
|
|
self.bin.mkdir()
|
|
executable(self.bin / "git", "#!/bin/bash\nprintf 'android/fixture.kt\\0'\n")
|
|
executable(
|
|
self.bin / "shasum",
|
|
f"""#!/bin/bash
|
|
for file do :; done
|
|
case "${{HASH_MODE:-valid}}" in
|
|
valid) digest={DIGEST} ;;
|
|
invalid) digest={'0' * 64} ;;
|
|
cache-invalid)
|
|
case "$file" in
|
|
*/ktlint-1.5.0) digest={'0' * 64} ;;
|
|
*) digest={DIGEST} ;;
|
|
esac
|
|
;;
|
|
esac
|
|
printf '%s %s\\n' "$digest" "$file"
|
|
printf 'shasum\\n' >> "$HASH_MARKER"
|
|
""",
|
|
)
|
|
self.download = executable(
|
|
self.root / "download-ktlint",
|
|
"""#!/bin/bash
|
|
set -e
|
|
java -version >/dev/null 2>&1
|
|
printf '%s\n' "$*" >> "$KTLINT_ARGS_MARKER"
|
|
printf 'launched\n' >> "$KTLINT_MARKER"
|
|
""",
|
|
)
|
|
executable(
|
|
self.bin / "curl",
|
|
"""#!/bin/bash
|
|
printf 'downloaded\n' >> "$CURL_MARKER"
|
|
out=''
|
|
while [ $# -gt 0 ]; do
|
|
if [ "$1" = '-o' ]; then out=$2; shift 2; else shift; fi
|
|
done
|
|
cp "$FAKE_DOWNLOAD" "$out"
|
|
""",
|
|
)
|
|
self.env = os.environ.copy()
|
|
self.env.pop("JAVA_HOME", None)
|
|
self.env.update(
|
|
{
|
|
"PATH": f"{self.bin}:{self.env['PATH']}",
|
|
"FAKE_DOWNLOAD": str(self.download),
|
|
"CURL_MARKER": str(self.root / "curl.marker"),
|
|
"HASH_MARKER": str(self.root / "hash.marker"),
|
|
"JAVA_MARKER": str(self.root / "java.marker"),
|
|
"KTLINT_ARGS_MARKER": str(self.root / "ktlint-args.marker"),
|
|
"KTLINT_MARKER": str(self.root / "ktlint.marker"),
|
|
}
|
|
)
|
|
|
|
def tearDown(self) -> None:
|
|
self.temp.cleanup()
|
|
|
|
@property
|
|
def cache(self) -> Path:
|
|
return self.root / ".dart_tool" / "native-format" / "ktlint-1.5.0"
|
|
|
|
def java(self, directory: Path, version: str | None, *, identity: str = "path", status: int = 0) -> Path:
|
|
directory.mkdir(parents=True, exist_ok=True)
|
|
if version is None:
|
|
output = "echo 'unrecognized java output' >&2"
|
|
else:
|
|
output = f"echo 'openjdk version \"{version}\"' >&2"
|
|
return executable(
|
|
directory / "java",
|
|
f"""#!/bin/bash
|
|
printf '{identity} %s\\n' "$*" >> "$JAVA_MARKER"
|
|
{output}
|
|
exit {status}
|
|
""",
|
|
)
|
|
|
|
def install_cache(self, contents: bytes | None = None) -> None:
|
|
self.cache.parent.mkdir(parents=True, exist_ok=True)
|
|
if contents is None:
|
|
shutil.copy2(self.download, self.cache)
|
|
else:
|
|
self.cache.write_bytes(contents)
|
|
self.cache.chmod(self.cache.stat().st_mode | stat.S_IXUSR)
|
|
|
|
def reset_markers(self) -> None:
|
|
for marker in self.root.glob("*.marker"):
|
|
marker.unlink()
|
|
|
|
def run_script(
|
|
self, *arguments: str, env: dict[str, str] | None = None
|
|
) -> subprocess.CompletedProcess[str]:
|
|
return subprocess.run(
|
|
["/bin/bash", "scripts/format_native.sh", *arguments],
|
|
cwd=self.root,
|
|
env=self.env if env is None else env,
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
|
|
def assert_java_rejection(self, result: subprocess.CompletedProcess[str]) -> None:
|
|
self.assertNotEqual(result.returncode, 0)
|
|
self.assertEqual(result.stderr.splitlines().count(JAVA_DIAGNOSTIC), 1)
|
|
self.assertNotIn("integer expression expected", result.stderr)
|
|
self.assertFalse((self.root / "curl.marker").exists())
|
|
self.assertFalse((self.root / "ktlint.marker").exists())
|
|
|
|
def test_unusable_java_rejects_cold_and_warm_cache_before_execution(self) -> None:
|
|
self.java(self.bin, "17.0.12", status=1)
|
|
self.assert_java_rejection(self.run_script("--check"))
|
|
|
|
self.install_cache()
|
|
self.reset_markers()
|
|
self.assert_java_rejection(self.run_script("--check"))
|
|
|
|
def test_java_home_runtime_and_ktlint_arguments_are_used_by_check_and_fix(self) -> None:
|
|
self.java(self.bin, "17.0.12", identity="path-shim", status=1)
|
|
home = self.root / "jdk home"
|
|
self.java(home / "bin", "17.0.12", identity="java-home")
|
|
env = self.env | {"JAVA_HOME": str(home)}
|
|
|
|
expected_arguments = {
|
|
"--check": "android/fixture.kt",
|
|
"--fix": "-F android/fixture.kt",
|
|
}
|
|
for mode, expected in expected_arguments.items():
|
|
with self.subTest(mode=mode):
|
|
self.reset_markers()
|
|
result = self.run_script(mode, env=env)
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
java_calls = (self.root / "java.marker").read_text(encoding="utf-8").splitlines()
|
|
self.assertEqual(java_calls, ["java-home -version", "java-home -version"])
|
|
self.assertEqual(
|
|
(self.root / "ktlint-args.marker").read_text(encoding="utf-8").strip(),
|
|
expected,
|
|
)
|
|
|
|
def test_java_version_support_floor_fails_closed(self) -> None:
|
|
for version in ("17.0.12", "21.0.2"):
|
|
with self.subTest(accepted=version):
|
|
self.java(self.bin, version)
|
|
self.reset_markers()
|
|
result = self.run_script("--check")
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertTrue((self.root / "ktlint.marker").exists())
|
|
|
|
for version, status in (("1.8.0_402", 0), ("16.0.2", 0), (None, 0), ("17.0.12", 1)):
|
|
with self.subTest(rejected=version, status=status):
|
|
self.java(self.bin, version, status=status)
|
|
self.reset_markers()
|
|
self.assert_java_rejection(self.run_script("--check"))
|
|
|
|
def test_cache_verification_replacement_and_mismatch_cleanup(self) -> None:
|
|
self.java(self.bin, "17.0.12")
|
|
self.install_cache()
|
|
valid = self.run_script("--check")
|
|
self.assertEqual(valid.returncode, 0, valid.stderr)
|
|
self.assertFalse((self.root / "curl.marker").exists())
|
|
self.assertEqual((self.root / "ktlint.marker").read_text(encoding="utf-8").splitlines(), ["launched"])
|
|
|
|
self.install_cache(b"invalid cache sentinel\n")
|
|
self.reset_markers()
|
|
replaced = self.run_script("--check", env=self.env | {"HASH_MODE": "cache-invalid"})
|
|
self.assertEqual(replaced.returncode, 0, replaced.stderr)
|
|
self.assertEqual(self.cache.read_bytes(), self.download.read_bytes())
|
|
self.assertTrue((self.root / "curl.marker").exists())
|
|
self.assertTrue((self.root / "ktlint.marker").exists())
|
|
|
|
sentinel = b"preserve invalid cache\n"
|
|
self.install_cache(sentinel)
|
|
self.reset_markers()
|
|
mismatch = self.run_script("--check", env=self.env | {"HASH_MODE": "invalid"})
|
|
self.assertNotEqual(mismatch.returncode, 0)
|
|
self.assertIn("failed SHA-256 verification", mismatch.stderr)
|
|
self.assertEqual(self.cache.read_bytes(), sentinel)
|
|
self.assertFalse((self.root / "ktlint.marker").exists())
|
|
self.assertEqual(list(self.cache.parent.glob(".ktlint-1.5.0.*")), [])
|
|
|
|
def test_sha256sum_fallback_verifies_and_executes(self) -> None:
|
|
self.java(self.bin, "17.0.12")
|
|
minimal = self.root / "minimal-bin"
|
|
minimal.mkdir()
|
|
for name in ("git", "java", "curl"):
|
|
shutil.copy2(self.bin / name, minimal / name)
|
|
executable(
|
|
minimal / "sha256sum",
|
|
f"#!/bin/bash\nfor file do :; done\nprintf '{DIGEST} %s\\n' \"$file\"\nprintf 'sha256sum\\n' >> \"$HASH_MARKER\"\n",
|
|
)
|
|
for name, source in {
|
|
"chmod": "/bin/chmod",
|
|
"cp": "/bin/cp",
|
|
"dirname": "/usr/bin/dirname",
|
|
"mkdir": "/bin/mkdir",
|
|
"mktemp": "/usr/bin/mktemp",
|
|
"mv": "/bin/mv",
|
|
"rm": "/bin/rm",
|
|
}.items():
|
|
(minimal / name).symlink_to(source)
|
|
|
|
result = self.run_script("--check", env=self.env | {"PATH": str(minimal)})
|
|
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertEqual((self.root / "hash.marker").read_text(encoding="utf-8").splitlines(), ["sha256sum"])
|
|
self.assertTrue((self.root / "ktlint.marker").exists())
|
|
|
|
def test_repository_without_kotlin_does_not_require_java_or_ktlint(self) -> None:
|
|
executable(self.bin / "git", "#!/bin/bash\nexit 0\n")
|
|
self.java(self.bin, "17.0.12", status=1)
|
|
|
|
result = self.run_script("--check")
|
|
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
for marker in ("curl.marker", "hash.marker", "java.marker", "ktlint.marker"):
|
|
self.assertFalse((self.root / marker).exists())
|
|
self.assertFalse(self.cache.parent.exists())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|