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
12 changes: 12 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
## Summary

<!-- Describe the problem and the resulting behavior. Link related issues. -->

## Check list

- [ ] Add tests
- [ ] Update `CHANGELOG.md`

## Compatibility

<!-- Describe changes to the public API, behavior, package dependencies, or required Jo/Python/Ruby versions. Note breaking changes and migration steps, or state that there are none. -->
13 changes: 0 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,6 @@ jobs:
- run: jo run test-python
- run: jo run test-ruby
- run: python3 scripts/check-output.py
- name: Check runner failure exit status
run: |
for module in test-python-failure test-ruby-failure; do
output=$(mktemp)
if jo run "$module" >"$output" 2>&1; then
cat "$output"
echo "$module unexpectedly succeeded"
exit 1
fi
grep -q 'tests failed' "$output"
cat "$output"
rm "$output"
done
- name: Build release artifacts
run: |
jo package core
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## [Unreleased]

### Fixed

- `Runner.runWithArgs` uses normal process exit with status 1 after reporting
failed tests on Python and Ruby, removing the extra runner exception
traceback. ([#2](https://github.com/typescope/jtest/issues/2))

## [0.2.0] - 2026-10-13

### Breaking changes
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ jo run tests -- "math/adds numbers"

Assertions accumulate failures and retain their source locations. Unexpected
exceptions fail the current test, and execution continues. `Runner.runWithArgs`
exits with a non-zero status if any selected test fails. Use `Runner.run` when
exits with status 1 if any selected test fails. Use `Runner.run` when
you want to handle its Boolean result yourself.

Filters are test-path prefixes. The root's name is not part of a path. A filter
Expand Down
25 changes: 23 additions & 2 deletions scripts/check-output.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Check observable reporting parity after building the conformance apps."""
"""Check reporting parity and clean failure exits on both backends."""

import re
import subprocess
Expand All @@ -20,7 +20,28 @@ def report(platform):
return re.sub(r" in [0-9.]+s", " in <elapsed>s", output)


def failure_report(platform):
result = subprocess.run(
["jo", "run", f"test-{platform}-failure"], capture_output=True, text=True
)
output = result.stdout + result.stderr
assert result.returncode == 1, (result.returncode, output)
assert (
"intentional failure: intentional (test-fixtures/failure/Run.jo:"
in result.stdout
), output
assert "0 passed, 1 failed" in result.stdout, output
assert "Traceback" not in output, output
assert "SystemExit" not in output, output
assert "tests failed" not in output, output
assert "src/runner/Runner.jo" not in output, output
return re.sub(r" in [0-9.]+s", " in <elapsed>s", result.stdout)


if __name__ == "__main__":
assert Path("jo.toml").exists(), "run from the repository root"
assert report("python") == report("ruby"), "backend reports differ"
print("Python and Ruby reports match")
assert failure_report("python") == failure_report("ruby"), (
"backend failure reports differ"
)
print("Python and Ruby reports match; failure exits are clean")
4 changes: 4 additions & 0 deletions src/python/Host.jo
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ private def hostFlush(): Unit =
py.module("sys").stdout.flush()
pass

private def hostExit(status: Int): Unit =
py.module("sys").exit(status)
pass

private def hostTry(body: () => Unit): Result[Bool, String] =
match py.try(body())
case Ok(_) => Ok(true)
Expand Down
4 changes: 4 additions & 0 deletions src/ruby/Host.jo
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ private def hostFlush(): Unit =
rb.const("STDOUT").flush()
pass

private def hostExit(status: Int): Unit =
rb.const("Kernel").exit(status)
pass

private def hostTry(body: () => Unit): Result[Bool, String] =
match rb.try(body())
case Ok(_) => Ok(true)
Expand Down
4 changes: 2 additions & 2 deletions src/runner/Runner.jo
Original file line number Diff line number Diff line change
Expand Up @@ -160,12 +160,12 @@ section Runner

//[ Run tests using the conventional first-argument filter.
!
! Aborts with a non-zero process status when any selected test fails. Use
! Exits with process status 1 when any selected test fails. Use
! `run` when the caller needs to handle the Boolean result itself.
//]
def runWithArgs(root: Testing.Suite): Unit receives stdout, args =
if !run(root, filterFromArgs(args)) then
abort("tests failed")
hostExit(1)

// The root contributes no path segment, so a filter reads as `providers/openai`.
private def walk(
Expand Down
Loading