diff --git a/tests/test_live_core.py b/tests/test_live_core.py index 24c9648..da2c9e1 100644 --- a/tests/test_live_core.py +++ b/tests/test_live_core.py @@ -1398,6 +1398,51 @@ def test_a_runner_started_by_a_launcher_counts_as_a_verification(cmd): assert g.verifications == 1 and not g.resolved_proxy() +@pytest.mark.parametrize("cmd", ["ruby test/ledger_test.rb", "ruby -w -Ilib test/slugify_test.rb", + "ruby -I lib test/x_test.rb", "ruby /w/repo/test/x_test.rb", + "ruby -Itest test/models/user_test.rb", "ruby spec/foo_spec.rb", + "ruby -W0 --disable-gems test/test_parser.rb", + "ruby -W:no-deprecated test/a_test.rb", "ruby -wv test/a_test.rb", + "ruby -C /w -Itest test/a_test.rb", + "bundle exec ruby -Itest test/a_test.rb -n test_x", + "rake test", "bundle exec rake test", "rake test:models", + "bin/rake test TEST=test/a_test.rb", "rake spec", "bin/rails test", + "rails test test/models/user_test.rb"]) +def test_a_ruby_test_run_counts_as_a_verification(cmd): + """minitest and test-unit are run as `ruby` given a test file, or through rake or rails, not + by a runner of their own as `rspec` is.""" + assert turn.status_of_tests(cmd) == "own" + g = guard.TurnGuard(turn_key="t") + g.fold("Edit", {"file_path": "/r/lib/a.rb", "old_string": "x", "new_string": "y"}, "ok") + g.fold("Bash", {"command": cmd}, "8 runs, 8 assertions, 1 failures, 0 errors, 0 skips", 1) + assert g.verifications == 1 and not g.resolved_proxy() + + +@pytest.mark.parametrize("cmd", ["ruby -Ilib -e 'require \"a\"; p A.new'", "ruby -wc lib/retry.rb", + "ruby -c test/a_test.rb", "ruby -wc test/a_test.rb", + "ruby -Wc test/a_test.rb", "ruby -cw test/a_test.rb", + "ruby -we 'load \"test/a_test.rb\"'", "ruby repro.rb", "ruby -v", + "ruby -e 'load \"test/a_test.rb\"'", "ruby lib/contest.rb", + "ruby -Icore lib/x.rb", "rake", "rake db:migrate", + "rails server", "cat test/a_test.rb"]) +def test_ruby_that_runs_no_test_file_is_not_a_verification(cmd): + """A one-off `ruby -e`, a syntax check and a script are not the project's checks.""" + assert turn.status_of_tests(cmd) is None + + +def test_a_piped_minitest_run_is_judged_by_its_summary(): + """`ruby test/x_test.rb | tail -5` exits with `tail`'s status, so minitest's summary line + decides whether the turn ended green.""" + g = guard.TurnGuard(turn_key="t") + g.fold("Edit", {"file_path": "/r/lib/a.rb", "old_string": "x", "new_string": "y"}, "ok") + g.fold("Bash", {"command": "ruby test/a_test.rb 2>&1 | tail -5"}, + "8 runs, 8 assertions, 1 failures, 0 errors, 0 skips", 0) + assert g.verifications == 1 and not g.resolved_proxy() + g.fold("Bash", {"command": "ruby test/a_test.rb 2>&1 | tail -5"}, + "8 runs, 8 assertions, 0 failures, 0 errors, 0 skips", 0) + assert g.verifications == 2 and g.resolved_proxy() + + @pytest.mark.parametrize("cmd,status", [("uv run ruff check", None), ("npx prettier --check .", None), ("pnpm exec jest --help", None), @@ -1517,6 +1562,10 @@ def test_a_test_run_followed_by_other_commands_is_judged_by_its_runners_summary( ("ok \tacme/x\t0.012s", False), ("test result: FAILED. 1 passed; 1 failed", True), ("test result: ok. 3 passed; 0 failed", False), ("5 examples, 1 failure", True), ("Tests run: 4, Failures: 0, Errors: 1", True), ("[INFO] BUILD SUCCESS", False), + ("8 runs, 8 assertions, 1 failures, 0 errors, 0 skips", True), + ("8 runs, 8 assertions, 0 failures, 2 errors, 0 skips", True), + ("Finished in 0.01s\n5 runs, 9 assertions, 0 failures, 0 errors, 1 skips", False), + ("1 tests, 1 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions", False), ("+ raise ValueError('no')", None), ("ok 1 - add", None)]) def test_a_runners_summary_gives_its_verdict(out, failed): assert turn.runner_verdict(out) is failed diff --git a/tokeneconomizer/loopguard/turn.py b/tokeneconomizer/loopguard/turn.py index cf2f2e9..c23255a 100644 --- a/tokeneconomizer/loopguard/turn.py +++ b/tokeneconomizer/loopguard/turn.py @@ -41,6 +41,14 @@ r"(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?test|go\s+test|cargo\s+(?:test|nextest)|" r"(?:mvn|mvnw|\./mvnw)\s+(?:\S+\s+)*test|(?:gradle|gradlew|\./gradlew)\s+(?:\S+\s+)*test|" r"make\s+(?:test|check)|ctest|rspec|phpunit|dotnet\s+test|swift\s+test|mix\s+test|" + # Ruby: `rake test`, `bin/rails test`, and `ruby` given a test file (`ruby -Itest + # test/models/user_test.rb`), but not `ruby -e '...'`, a syntax check (`ruby -wc x.rb`) or a + # script that is not a test (`ruby repro.rb`): the options it takes are spelled out, and + # case-sensitive, so `-W` does not also take `-wc` under the pattern's `(?i)`. + r"rake(?:\s+\w+=\S*)*\s+(?:test|spec)(?::[\w:]+)?|rails\s+test(?::[\w:]+)?|" + r"ruby(?-i:\s+(?:-[Ir](?:\S+|\s+[^\s\-]\S*)|-C\s*\S+|-W[0-2]?(?::\S+)?|" + r"--[\w\-]+(?:=\S+)?|-[abdf-zABDF-Z]+))*\s+" + r"(?:[\w.\-~]*/)*(?:test_[\w.\-]*|[\w.\-]*_(?:test|spec))\.rb|" r"deno\s+test|node\s+--test|bazel\s+test|(?:\./)?bin/test)(?![\w.\-/])") # A command line's exit status is its LAST command's, so it may not be the test run's: `npm test @@ -178,6 +186,9 @@ def status_of_tests(cmd): r"^(?:--- FAIL: |FAIL\t|FAIL$)", # go r"^test result: FAILED", # cargo r"\b\d+ examples?, [1-9]\d* failures?\b", # rspec + # minitest (`8 runs, 9 assertions, 1 failures, 0 errors, 0 skips`), test-unit (`tests`) + r"^\d+ (?:runs|tests), \d+ assertions, (?:[1-9]\d* failures, \d+ errors|\d+ failures, " + r"[1-9]\d* errors)\b", r"Tests run: \d+, Failures: (?:[1-9]|0, Errors: [1-9])", # JUnit (Maven, Gradle) r"BUILD FAIL(?:URE|ED)")), re.MULTILINE) _RUN_PASSED_RE = re.compile("|".join(( @@ -192,6 +203,7 @@ def status_of_tests(cmd): r"^ok\s+\S+\s+(?:\d+(?:\.\d+)?s|\(cached\))", r"^test result: ok\.", r"\b\d+ examples?, 0 failures\b", + r"^\d+ (?:runs|tests), \d+ assertions, 0 failures, 0 errors\b", # minitest, test-unit r"BUILD SUCCESS")), re.MULTILINE)