Skip to content

Commit 63808c8

Browse files
Add graalvm build and test suite - experimental (#482)
This PR adds graalvm build, and a test runner for it. It works quite well, but there are still some tests that are skipped.
1 parent 7073005 commit 63808c8

4 files changed

Lines changed: 194 additions & 0 deletions

File tree

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.mill linguist-language=Scala

.github/workflows/pr-build.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,6 @@ jobs:
3434
- name: Run sbt tests
3535
if: ${{ matrix.lang == 'jvm' }}
3636
run: sbt test
37+
- name: Run Native Image Test Suites
38+
if: ${{ (matrix.lang == 'jvm') && (matrix.java == '21') }}
39+
run: sjsonnet/test/graalvm/run_test_suites.py

build.mill

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import scalanativelib.api._
1414
import scalajslib.api._
1515
import scalafmt._
1616
import javalib.api.JvmWorkerUtil
17+
import javalib.NativeImageModule
1718
import contrib.versionfile.VersionFileModule
1819
import contrib.jmh.JmhModule
1920

@@ -306,4 +307,23 @@ object sjsonnet extends VersionFileModule {
306307
object test extends ScalaTests with TestModule.Junit4
307308
}
308309
}
310+
311+
object graal extends NativeImageModule {
312+
def finalMainClass = jvm(scalaVersions(0)).mainClass().get
313+
def runClasspath = Seq()
314+
def jvmId = "graalvm-community:21.0.2"
315+
def localBinName = "sjsonnet-native"
316+
def nativeImageClasspath = Seq(jvm(scalaVersions(0)).assembly())
317+
def nativeImageOptions = super.nativeImageOptions() ++ Seq(
318+
"--no-fallback",
319+
"-O3",
320+
"-R:MaxHeapSize=2G",
321+
"-march=native",
322+
"--strict-image-heap",
323+
"--initialize-at-build-time",
324+
"--initialize-at-run-time=os.package$",
325+
"--install-exit-handlers",
326+
"-H:+ReportExceptionStackTraces",
327+
)
328+
}
309329
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
#!/usr/bin/env python3
2+
3+
import os
4+
import sys
5+
import subprocess
6+
import glob
7+
import unittest
8+
from pathlib import Path
9+
from typing import List, Tuple, Dict, Optional
10+
11+
def strip_trailing_empty_lines(text: str) -> str:
12+
"""Remove trailing empty lines from text."""
13+
lines = text.splitlines()
14+
while lines and not lines[-1].strip():
15+
lines.pop()
16+
return '\n'.join(lines)
17+
18+
19+
def run_binary(binary_path: str, test_dir: str, jsonnet_file: str) -> Tuple[str, int]:
20+
"""Run the binary on a jsonnet file and return output and exit code."""
21+
cmd = [binary_path, '-Xss100m', '-J', test_dir, jsonnet_file]
22+
try:
23+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
24+
return result.stdout + result.stderr, result.returncode
25+
except subprocess.TimeoutExpired:
26+
return "TIMEOUT: Test execution exceeded 10 seconds", 1
27+
except Exception as e:
28+
return f"ERROR: Failed to execute binary: {e}", 1
29+
30+
class BaseGraalVMTestSuite(unittest.TestCase):
31+
"""Base class for GraalVM test suites."""
32+
33+
@classmethod
34+
def setUpClass(cls):
35+
"""Set up the test class by building the GraalVM native binary."""
36+
if not hasattr(cls, '_binary_built'):
37+
subprocess.run(["./mill", "sjsonnet.graal.nativeImage"], check=True, cwd=".")
38+
BaseGraalVMTestSuite._binary_built = True
39+
cls.binary_path = "out/sjsonnet/graal/nativeImage.dest/native-executable"
40+
41+
def run_individual_test(self, test_dir: str, suite_name: str, jsonnet_file: str, base_name: str):
42+
"""Run a single jsonnet test and assert it passes."""
43+
# Run the binary on the jsonnet file and capture output
44+
output, exit_code = run_binary(self.binary_path, test_dir, jsonnet_file)
45+
with open(os.path.join(test_dir, f"{base_name}.jsonnet.golden"), 'r', encoding='utf-8') as f:
46+
golden_content = f.read()
47+
48+
# Strip test directory path from output only for go_test_suite
49+
if suite_name == "go_test_suite":
50+
normalized_output = strip_trailing_empty_lines(output.replace(f"{test_dir}/", ""))
51+
normalized_golden_content = strip_trailing_empty_lines(golden_content.replace(f"{test_dir}/", ""))
52+
else:
53+
normalized_output = strip_trailing_empty_lines(output)
54+
normalized_golden_content = strip_trailing_empty_lines(golden_content)
55+
56+
# Compare with golden file, ignoring trailing empty lines
57+
if len(normalized_golden_content) > 10000 and normalized_golden_content != normalized_output:
58+
print(f"normalized_golden_content: {normalized_golden_content[:100]}")
59+
print(f"normalized_output: {normalized_output[:100]}")
60+
self.fail("Mismatch - but content very very large")
61+
self.assertEqual(normalized_golden_content, normalized_output)
62+
63+
64+
class MainTestSuite(BaseGraalVMTestSuite):
65+
"""Test suite for the main jsonnet test files."""
66+
67+
def test_all_files(self):
68+
"""Test all files in the main test suite using subTest for each file."""
69+
test_dir = "sjsonnet/test/resources/test_suite"
70+
suite_name = "test_suite"
71+
72+
if not os.path.exists(test_dir):
73+
self.fail(f"Test directory {test_dir} not found")
74+
75+
# Skip list for main test suite
76+
skip_list = [
77+
"error.obj_recursive_manifest",
78+
"error.recursive_object_non_term",
79+
"error.recursive_import",
80+
"error.recursive_function_nonterm",
81+
"error.function_infinite_default",
82+
"error.obj_recursive",
83+
"error.array_recursive_manifest",
84+
"error.function_no_default_arg",
85+
"error.invariant.option",
86+
"error.negative_shfit",
87+
"error.overflow",
88+
"error.overflow2",
89+
"error.overflow3",
90+
"error.parse_json",
91+
"error.parse.string.invalid_escape",
92+
"error.top_level_func",
93+
"stdlib",
94+
"tla.simple",
95+
"trace"
96+
]
97+
98+
# Find all .jsonnet files in the test directory
99+
jsonnet_pattern = os.path.join(test_dir, "*.jsonnet")
100+
jsonnet_files = glob.glob(jsonnet_pattern)
101+
102+
self.assertTrue(jsonnet_files, f"No .jsonnet files found in {test_dir}")
103+
104+
for jsonnet_file in sorted(jsonnet_files):
105+
base_name = Path(jsonnet_file).stem
106+
107+
# Check if this test should be skipped
108+
if base_name in skip_list:
109+
continue
110+
111+
with self.subTest(file=base_name):
112+
self.run_individual_test(test_dir, suite_name, jsonnet_file, base_name)
113+
114+
class GoTestSuite(BaseGraalVMTestSuite):
115+
"""Test suite for the Go jsonnet test files."""
116+
117+
def test_all_files(self):
118+
"""Test all files in the go test suite using subTest for each file."""
119+
test_dir = "sjsonnet/test/resources/go_test_suite"
120+
suite_name = "go_test_suite"
121+
122+
if not os.path.exists(test_dir):
123+
self.fail(f"Test directory {test_dir} not found")
124+
125+
# Skip list for go_test_suite
126+
skip_list = [
127+
"builtin_cos",
128+
"builtin_exp4",
129+
"builtin_log3",
130+
"div3",
131+
"extvar_code",
132+
"extvar_error",
133+
"extvar_hermetic",
134+
"extvar_mutually_recursive",
135+
"extvar_self_recursive",
136+
"extvar_static_error",
137+
"extvar_string",
138+
"function_too_many_params",
139+
"native1",
140+
"native2",
141+
"native3",
142+
"native4",
143+
"native5",
144+
"native6",
145+
"native7",
146+
"native_error",
147+
"native_panic",
148+
"std.mantissa3",
149+
"stdlib_smoke_test"
150+
]
151+
152+
# Find all .jsonnet files in the test directory
153+
jsonnet_pattern = os.path.join(test_dir, "*.jsonnet")
154+
jsonnet_files = glob.glob(jsonnet_pattern)
155+
156+
self.assertTrue(jsonnet_files, f"No .jsonnet files found in {test_dir}")
157+
158+
for jsonnet_file in sorted(jsonnet_files):
159+
base_name = Path(jsonnet_file).stem
160+
161+
# Check if this test should be skipped
162+
if base_name in skip_list:
163+
continue
164+
165+
with self.subTest(file=base_name):
166+
self.run_individual_test(test_dir, suite_name, jsonnet_file, base_name)
167+
168+
169+
if __name__ == "__main__":
170+
unittest.main(verbosity=2)

0 commit comments

Comments
 (0)