Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/justfile
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ test:
bun test sh/gh/ffi.test.ts sh/gh/cpp.test.ts sh/gh/publish.test.ts
python3 sh/rs/package-rename.test.py
python3 sh/dart/build.test.py
python3 sh/kt/gradle.test.py
python3 sh/rs/select.test.py
python3 sh/rs/config.test.py
python3 sh/js/workspace.test.py
Expand Down
9 changes: 8 additions & 1 deletion sh/kt/bench.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
KT_DIR="$(cd "$SCRIPT_DIR/../../kt" && pwd)"
export MOQ_BENCH_DIR="${MOQ_BENCH_DIR:-$KT_DIR/../target/bench/ffi}"

# `type -P` resolves only executables, so an inherited shell function named gradle cannot stand in for Gradle.
gradle_name="${GRADLE_CMD:-gradle}"
GRADLE_CMD="$(type -P "$gradle_name")" || {
echo "kt bench: Gradle executable '$gradle_name' not found; run 'nix develop'" >&2
exit 1
}

MOQ_FFI_PROFILE=release bash "$SCRIPT_DIR/generate.sh"
gradle -p "$KT_DIR" -Pmoqffi.version=0.0.0-dev :moq:jvmTest --tests dev.moq.FfiBench --rerun
"$GRADLE_CMD" -p "$KT_DIR" -Pmoqffi.version=0.0.0-dev :moq:jvmTest --tests dev.moq.FfiBench --rerun
echo "kt bench: wrote $MOQ_BENCH_DIR/kotlin.json"
8 changes: 5 additions & 3 deletions sh/kt/check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ if ! command -v java >/dev/null 2>&1; then
exit 1
fi

GRADLE_CMD="${GRADLE_CMD:-$(command -v gradle || true)}"
if [[ -z "$GRADLE_CMD" ]]; then
echo "kt check: gradle not on PATH; run 'nix develop' or use 'just kt generate' to only regenerate bindings" >&2
# `type -P` resolves only executables, so an inherited shell function named gradle can neither
# stand in for Gradle nor make the gradle() wrapper below call itself.
gradle_name="${GRADLE_CMD:-gradle}"
if ! GRADLE_CMD="$(type -P "$gradle_name")"; then
echo "kt check: Gradle executable '$gradle_name' not found; run 'nix develop' or use 'just kt generate' to only regenerate bindings" >&2
exit 1
fi

Expand Down
143 changes: 143 additions & 0 deletions sh/kt/gradle.test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
"""Kotlin runners select executables even when the shell exports Gradle functions."""

import json
import os
import shutil
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path

ROOT = Path(__file__).resolve().parents[2]
BASH = shutil.which("bash")
RUNNERS = ("check", "package", "bench")


class GradleTest(unittest.TestCase):
def setUp(self):
scratch = ROOT / ".tmp"
scratch.mkdir(exist_ok=True)
self.directory = tempfile.TemporaryDirectory(dir=scratch)
self.addCleanup(self.directory.cleanup)
self.root = Path(self.directory.name)
self.tools = self.root / "tools"
self.tools.mkdir()
self.calls = self.root / "calls.jsonl"
for name in ("bash", "dirname", "mkdir", "rm", "cp"):
(self.tools / name).symlink_to(shutil.which(name))
(self.tools / "python3").symlink_to(sys.executable)
self.write("tools/java", "exit 0\n")
for name in (f"{runner}.sh" for runner in RUNNERS):
target = self.root / "sh/kt" / name
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(ROOT / "sh/kt" / name, target)
for name in ("format.sh", "generate.sh"):
self.write(f"sh/kt/{name}", "exit 0\n")
self.write("doc/lib/samples.sh", "exit 0\n")
(self.root / "sh/kt/check-poms.py").write_text("")
(self.root / "kt/moq/src/jvmAndAndroidTest/kotlin/dev/moq/docs").mkdir(parents=True)
source = self.root / "bindings/uniffi/moq/moq.kt"
source.parent.mkdir(parents=True)
source.write_text("")
self.gradle("gradle")
self.gradle("custom-gradle")

def write(self, name, source):
target = self.root / name
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(f"#!{BASH}\n{source}")
target.chmod(0o755)

def gradle(self, name):
target = self.tools / name
target.write_text(
f"#!{sys.executable}\n"
"import json, os, sys\n"
'with open(os.environ["GRADLE_CALLS"], "a") as output:\n'
" output.write(json.dumps({\n"
' "executable": sys.argv[0], "args": sys.argv[1:],\n'
' "runner_keys": [key for key in os.environ\n'
' if key.startswith(("ACTIONS_", "GITHUB_", "RUNNER_"))],\n'
' }) + "\\n")\n'
'sys.exit(int(os.environ.get("GRADLE_STATUS", "0")))\n'
)
target.chmod(0o755)

def run_script(self, script, **changes):
self.calls.unlink(missing_ok=True)
# An imported function must never satisfy executable discovery. FUNCNEST
# turns a recursive wrapper into a deterministic failure in the fixture.
env = {
"PATH": str(self.tools),
"HOME": str(self.root),
"LOCAL_SUPERVISED": "1",
"BASH_ENV": os.devnull,
"BASH_FUNC_gradle%%": "() { echo 'inherited function called' >&2; return 99; }",
"FUNCNEST": "10",
"GRADLE_CALLS": str(self.calls),
"ACTIONS_TEST": "fixture",
"GITHUB_TEST": "fixture",
"RUNNER_TEST": "fixture",
**changes,
}
args = [BASH, f"sh/kt/{script}.sh"]
if script == "package":
args += ["--version", "0.0.0-dev", "--lib-dir", "libs", "--bindings-dir", "bindings", "--output", "dist"]
return subprocess.run(args, cwd=self.root, env=env, text=True, capture_output=True)

def recorded(self):
return [json.loads(line) for line in self.calls.read_text().splitlines()]

def test_path_executable_wins_over_exported_function(self):
first = {"check": ":moq:jvmTest", "package": ":moq-ffi:assemble", "bench": "dev.moq.FfiBench"}
for script in RUNNERS:
for override in ({}, {"GRADLE_CMD": "gradle"}):
with self.subTest(script=script, override=override):
result = self.run_script(script, **override)
self.assertEqual(result.returncode, 0, result.stderr)
calls = self.recorded()
self.assertEqual(len(calls), 2 if script == "check" else 1)
self.assertTrue(all(Path(call["executable"]) == self.tools / "gradle" for call in calls))
self.assertIn(first[script], calls[0]["args"])
if script == "check":
self.assertTrue(all(call["runner_keys"] == [] for call in calls))
self.assertIn(":moq:generatePomFileForJvmPublication", calls[1]["args"])

def test_explicit_executable_is_preserved(self):
for script in RUNNERS:
for override in ("custom-gradle", "tools/custom-gradle", str(self.tools / "custom-gradle")):
with self.subTest(script=script, override=override):
result = self.run_script(script, GRADLE_CMD=override)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertTrue(all(Path(call["executable"]).name == "custom-gradle" for call in self.recorded()))

def test_function_without_executable_is_refused(self):
(self.tools / "gradle").unlink()
for script in RUNNERS:
with self.subTest(script=script):
result = self.run_script(script)
self.assertEqual(result.returncode, 1, result.stderr)
self.assertIn("Gradle executable 'gradle' not found", result.stderr)
self.assertNotIn("inherited function called", result.stderr)
self.assertFalse(self.calls.exists())

def test_missing_override_is_named(self):
for script in RUNNERS:
with self.subTest(script=script):
result = self.run_script(script, GRADLE_CMD="tools/missing-gradle")
self.assertEqual(result.returncode, 1, result.stderr)
self.assertIn("Gradle executable 'tools/missing-gradle' not found", result.stderr)
self.assertFalse(self.calls.exists())

def test_gradle_failure_stops_the_runner(self):
for script in RUNNERS:
with self.subTest(script=script):
result = self.run_script(script, GRADLE_STATUS="7")
self.assertEqual(result.returncode, 7, result.stderr)
self.assertEqual(len(self.recorded()), 1)
self.assertNotIn("kt bench: wrote", result.stdout)


if __name__ == "__main__":
unittest.main()
7 changes: 4 additions & 3 deletions sh/kt/package.sh
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,10 @@ if [[ "$HAVE_ANDROID_LIBS" == true ]]; then
GRADLE_ARGS+=("-Pandroid.enabled=true")
fi

GRADLE_CMD="${GRADLE_CMD:-$(command -v gradle || true)}"
[[ -n "$GRADLE_CMD" ]] || {
echo "Error: gradle not on PATH" >&2
# `type -P` resolves only executables, so an inherited shell function named gradle cannot stand in for Gradle.
gradle_name="${GRADLE_CMD:-gradle}"
GRADLE_CMD="$(type -P "$gradle_name")" || {
echo "Error: Gradle executable '$gradle_name' not found" >&2
exit 1
}

Expand Down