Skip to content

Commit 7b927cc

Browse files
committed
Improve test runner ergonomics
Add `./run-tests` executable that doesn't require arguments, automatically detecting available runtimes and tests. Simplify `README.md` accordingly. Console test case reporter more concise and useful: - Single line to indicate total results - One copy-pastable command line per failure - Requires refactors to record command lines, remove cwd change Allow testing multiple engines in one run. - Required changing `TEST_RUNTIME_EXE` environment variable convention to one env var per executable - Requires refactor to collect which runtime ran a testcase
1 parent cc75080 commit 7b927cc

File tree

17 files changed

+269
-130
lines changed

17 files changed

+269
-130
lines changed

README.md

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -44,32 +44,31 @@ executor is quite simple; see the [specification] document for the details and t
4444
5. Execute the test suites from this repository:
4545

4646
```bash
47-
python3 test-runner/wasi_test_runner.py \
48-
-t ./tests/assemblyscript/testsuite/ `# path to folders containing .wasm test files` \
49-
./tests/c/testsuite/wasm32-wasip1 \
50-
./tests/rust/testsuite/ \
51-
-r adapters/wasmtime.py # path to a runtime adapter
47+
./run-tests
5248
```
5349

54-
Optionally you can specify test cases to skip with the `--exclude-filter` option.
50+
By default, the test runner will detect available WASI runtimes from
51+
those available in [adapters/](adapters/), and will run tests on all
52+
available runtimes. Pass `--runtime` to instead use a specific runtime.
53+
54+
```
55+
./run-tests --runtime adapters/wasmtime.py
56+
```
57+
58+
Running tests will invoke the WASI runtime's binary in a subprocess:
59+
`wasmtime` for `adapters/wasmtime.py`, `iwasm` for
60+
`adapters/wasm-micro-runtime.py`, and so on. These binaries can be
61+
overridden by setting corresponding environment variables (`WASMTIME`,
62+
`IWASM`, etc):
5563

56-
```bash
57-
python3 test-runner/wasi_test_runner.py \
58-
-t ./tests/assemblyscript/testsuite/ `# path to folders containing .wasm test files` \
59-
./tests/c/testsuite/wasm32-wasip1 \
60-
./tests/rust/testsuite/ \
61-
--exclude-filter examples/skip.json \
62-
-r adapters/wasmtime.py # path to a runtime adapter
64+
```
65+
WASMTIME="wasmtime --wasm-features all" ./run-tests
6366
```
6467

65-
The default executable in the adapter used for test execution can be
66-
overridden using `TEST_RUNTIME_EXE` variable. This only works with adapters defined in
67-
[adapters/](adapters/), and might not work with 3rd party adapters.
68+
Optionally you can specify test cases to skip with the `--exclude-filter` option.
6869

6970
```bash
70-
TEST_RUNTIME_EXE="wasmtime --wasm-features all" python3 test-runner/wasi_test_runner.py \
71-
-t ./tests/assemblyscript/testsuite/ \
72-
-r adapters/wasmtime.py
71+
./run-tests --exclude-filter examples/skip.json \
7372
```
7473

7574
## Contributing

adapters/wasm-micro-runtime.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import shlex
66

77
# shlex.split() splits according to shell quoting rules
8-
IWASM = shlex.split(os.getenv("TEST_RUNTIME_EXE", "iwasm"))
8+
IWASM = shlex.split(os.getenv("IWASM", "iwasm"))
99

1010
parser = argparse.ArgumentParser()
1111
parser.add_argument("--version", action="store_true")
@@ -23,7 +23,7 @@
2323
TEST_FILE = args.test_file
2424
PROG_ARGS = args.arg
2525
ENV_ARGS = [f"--env={i}" for i in args.env]
26-
DIR_ARGS = [f"--dir={i}" for i in args.dir]
26+
DIR_ARGS = [f"--map-dir={i}" for i in args.dir]
2727

2828
r = subprocess.run(IWASM + ENV_ARGS + DIR_ARGS + [TEST_FILE] + PROG_ARGS)
2929
sys.exit(r.returncode)

adapters/wasmtime.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import shlex
66

77
# shlex.split() splits according to shell quoting rules
8-
WASMTIME = shlex.split(os.getenv("TEST_RUNTIME_EXE", "wasmtime"))
8+
WASMTIME = shlex.split(os.getenv("WASMTIME", "wasmtime"))
99

1010
parser = argparse.ArgumentParser()
1111
parser.add_argument("--version", action="store_true")

adapters/wizard.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import shlex
66

77
# shlex.split() splits according to shell quoting rules
8-
WIZARD = shlex.split(os.getenv("TEST_RUNTIME_EXE", "wizeng.x86-64-linux"))
8+
WIZARD = shlex.split(os.getenv("WIZARD", "wizeng.x86-64-linux"))
99

1010
parser = argparse.ArgumentParser()
1111
parser.add_argument("--version", action="store_true")

run-tests

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#!/usr/bin/env python3
2+
3+
import argparse
4+
import subprocess
5+
import sys
6+
from typing import List
7+
from pathlib import Path
8+
9+
sys.path.insert(0, str(Path(__file__).parent / "test-runner"))
10+
11+
from wasi_test_runner.harness import run_tests
12+
from wasi_test_runner.runtime_adapter import RuntimeAdapter
13+
14+
parser = argparse.ArgumentParser(
15+
description="WASI test runner"
16+
)
17+
18+
parser.add_argument(
19+
"-f",
20+
"--exclude-filter",
21+
action="append",
22+
default=[],
23+
help="Path to JSON file indicating tests to exclude.",
24+
)
25+
parser.add_argument(
26+
"-r", "--runtime-adapter", help="Path to a runtime adapter."
27+
)
28+
parser.add_argument(
29+
"--json-output-location",
30+
help="JSON test result destination. If not specified, JSON output won't be generated.",
31+
)
32+
parser.add_argument(
33+
"--disable-colors",
34+
action="store_true",
35+
default=False,
36+
help="Disables color for console output reporter.",
37+
)
38+
39+
def find_test_dirs(root):
40+
test_dirs = []
41+
for root, dirs, files in root.walk(on_error=print):
42+
if "manifest.json" in files:
43+
test_dirs.append(root)
44+
return test_dirs
45+
46+
def find_runtime_adapters(root, verbose=False):
47+
print(f"Detecting WASI runtime availability:")
48+
adapters = []
49+
for candidate in root.glob("*.py"):
50+
adapter = RuntimeAdapter(candidate)
51+
try:
52+
print(f" {candidate.name}: {adapter.get_version()}")
53+
adapters.append(adapter)
54+
except subprocess.CalledProcessError as e:
55+
print(f" {candidate.name}: unavailable; pass `--runtime {candidate}` to debug.")
56+
print("")
57+
if len(adapters) == 0:
58+
print("Error: No WASI runtimes found")
59+
sys.exit(1)
60+
return adapters
61+
62+
options = parser.parse_args()
63+
test_suite = find_test_dirs(Path(__file__).parent / "tests")
64+
if options.runtime_adapter:
65+
runtime_adapters = [RuntimeAdapter(options.runtime_adapter)]
66+
# Ensure it works.
67+
try:
68+
runtime_adapters[0].get_version()
69+
except subprocess.CalledProcessError as e:
70+
print(f"Error: failed to load {options.runtime_adapter}:")
71+
print(f" Failed command line: {' '.join(e.cmd)}")
72+
if e.stdout.strip() != "":
73+
print(f" stdout:\n{e.stdout}")
74+
if e.stderr.strip() != "":
75+
print(f" stderr:\n{e.stderr}")
76+
sys.exit(1)
77+
else:
78+
runtime_adapters = find_runtime_adapters(Path(__file__).parent / "adapters")
79+
80+
exclude_filters = [JSONTestExcludeFilter(filt) for filt in options.exclude_filter]
81+
82+
sys.exit(run_tests(runtime_adapters, test_suite,
83+
color=not options.disable_colors,
84+
json_log_file=options.json_output_location,
85+
exclude_filters=exclude_filters))

test-runner/tests/test_test_suite.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44
import wasi_test_runner.test_suite as ts
55

66

7-
def create_test_case(name: str, is_executed: bool, is_failed: bool) -> tc.TestCase:
7+
def create_test_case(name: str, runtime: str,
8+
is_executed: bool, is_failed: bool) -> tc.TestCase:
89
failures = [tc.Failure("a", "b")] if is_failed else []
910
return tc.TestCase(
1011
name,
12+
runtime,
13+
[runtime, name],
1114
tc.Config(),
1215
tc.Result(tc.Output(0, "", ""), is_executed, failures),
1316
1.0,
@@ -20,10 +23,10 @@ def test_test_suite_should_return_correct_count() -> None:
2023
10.0,
2124
datetime.now(),
2225
[
23-
create_test_case("t1", True, True),
24-
create_test_case("t2", True, False),
25-
create_test_case("t3", False, True),
26-
create_test_case("t4", False, False),
26+
create_test_case("t1", "rt1", True, True),
27+
create_test_case("t2", "rt1", True, False),
28+
create_test_case("t3", "rt1", False, True),
29+
create_test_case("t4", "rt1", False, False),
2730
],
2831
)
2932

test-runner/tests/test_test_suite_runner.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,20 @@ def test_runner_end_to_end() -> None:
4545
tc.Config(stdout="output", env={"x": "1"}),
4646
]
4747

48+
runtime_name = "rt1"
49+
runtime_version_str = "4.2"
50+
4851
expected_test_cases = [
49-
tc.TestCase(test_name, config, result, ANY)
52+
tc.TestCase(test_name, runtime_name, [runtime_name, test_name],
53+
config, result, ANY)
5054
for config, test_name, result in zip(
5155
expected_config, ["test1", "test2", "test3"], expected_results
5256
)
5357
]
5458

5559
runtime = Mock()
60+
runtime.get_name.return_value = runtime_name
61+
runtime.get_version.return_value = (runtime_name, runtime_version_str)
5662
runtime.run_test.side_effect = outputs
5763

5864
validators = [
@@ -67,7 +73,9 @@ def test_runner_end_to_end() -> None:
6773
filters = [filt]
6874

6975
with patch("glob.glob", return_value=test_paths):
70-
suite = tsr.run_tests_from_test_suite("my-path", runtime, validators, reporters, filters) # type: ignore
76+
suite = tsr.run_tests_from_test_suite("my-path", [runtime],
77+
validators, reporters,
78+
filters) # type: ignore
7179

7280
# Assert manifest was read correctly
7381
assert suite.name == "test-suite"
@@ -99,7 +107,8 @@ def test_runner_end_to_end() -> None:
99107
for filt in filters:
100108
assert filt.should_skip.call_count == 3
101109
for test_case in expected_test_cases:
102-
filt.should_skip.assert_any_call(suite.name, test_case.name)
110+
filt.should_skip.assert_any_call(runtime, suite.name,
111+
test_case.name)
103112

104113

105114
@patch("os.path.exists", Mock(return_value=False))

test-runner/wasi_test_runner/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def main() -> int:
6060
filters.append(JSONTestExcludeFilter(filt))
6161

6262
return run_all_tests(
63-
RuntimeAdapter(options.runtime_adapter),
63+
[RuntimeAdapter(options.runtime_adapter)],
6464
options.test_suite,
6565
validators,
6666
reporters,

test-runner/wasi_test_runner/filters.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
class TestFilter(ABC):
99
@abstractmethod
1010
def should_skip(
11-
self, test_suite_name: str, test_name: str
11+
self, runtime, test_suite_name: str, test_name: str
1212
) -> Union[Tuple[Literal[True], str], Tuple[Literal[False], Literal[None]]]:
1313
pass
1414

@@ -19,7 +19,7 @@ def __init__(self, filename: str) -> None:
1919
self.filter_dict = json.load(file)
2020

2121
def should_skip(
22-
self, test_suite_name: str, test_name: str
22+
self, runtime, test_suite_name: str, test_name: str
2323
) -> Union[Tuple[Literal[True], str], Tuple[Literal[False], Literal[None]]]:
2424
test_suite_filter = self.filter_dict.get(test_suite_name)
2525
if test_suite_filter is None:
Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,30 @@
11
from typing import List
2+
from pathlib import Path
23

34
from .filters import TestFilter
45
from .reporters import TestReporter
6+
from .reporters.console import ConsoleTestReporter
7+
from .reporters.json import JSONTestReporter
58
from .test_suite_runner import run_tests_from_test_suite
69
from .runtime_adapter import RuntimeAdapter
7-
from .validators import Validator
10+
from .validators import exit_code_validator, stdout_validator, Validator
811

12+
def run_tests(runtimes: List[RuntimeAdapter],
13+
test_suite_paths: List[Path],
14+
exclude_filters: List[Path] | None = None,
15+
color: bool = True,
16+
json_log_file: str | None = None):
17+
validators = [exit_code_validator, stdout_validator]
18+
reporters = [ConsoleTestReporter(color)]
19+
if json_log_file:
20+
reporters.append(JSONTestReporter(json_log_file))
21+
filters = [JSONTestExcludeFilter(filt) for filt in exclude_filters]
22+
23+
return run_all_tests(runtimes, [str(p) for p in test_suite_paths],
24+
validators, reporters, filters)
925

1026
def run_all_tests(
11-
runtime: RuntimeAdapter,
27+
runtimes: List[RuntimeAdapter],
1228
test_suite_paths: List[str],
1329
validators: List[Validator],
1430
reporters: List[TestReporter],
@@ -17,15 +33,16 @@ def run_all_tests(
1733
ret = 0
1834

1935
for test_suite_path in test_suite_paths:
20-
test_suite = run_tests_from_test_suite(
21-
test_suite_path, runtime, validators, reporters, filters,
22-
)
23-
for reporter in reporters:
24-
reporter.report_test_suite(test_suite)
25-
if test_suite.fail_count > 0:
26-
ret = 1
36+
for runtime in runtimes:
37+
test_suite = run_tests_from_test_suite(
38+
test_suite_path, runtime, validators, reporters, filters,
39+
)
40+
for reporter in reporters:
41+
reporter.report_test_suite(test_suite)
42+
if test_suite.fail_count > 0:
43+
ret = 1
2744

2845
for reporter in reporters:
29-
reporter.finalize(runtime.get_version())
46+
reporter.finalize()
3047

3148
return ret

0 commit comments

Comments
 (0)