diff --git a/src/falsegreen_robot/scanner.py b/src/falsegreen_robot/scanner.py index 005f2ba..475e36a 100644 --- a/src/falsegreen_robot/scanner.py +++ b/src/falsegreen_robot/scanner.py @@ -263,15 +263,20 @@ def is_verification(keyword, args, local_keywords=None, extra_verify=None): key, sep, val = (a or "").partition("=") if sep and _norm(key) == "expected_status": return _norm(val) not in _EXPECTED_STATUS_OFF and bool(val.strip()) - # Run Keyword And Continue On Failure / And Warn On Failure *args - - # the soft-assert wrappers: the wrapped keyword still runs and is verified, the - # wrapper only changes how a failure is reported (continue / warn). It is an - # oracle exactly when the wrapped keyword is one, so recurse on the inner call. - if n in ("run keyword and continue on failure", "run keyword and warn on failure"): + # Run Keyword And Continue On Failure *args - a soft-assert wrapper: + # the wrapped keyword still runs and a failure is recorded, so the test FAILS at + # the end. It is an oracle exactly when the wrapped keyword is one, so recurse. + if n == "run keyword and continue on failure": inner = list(args or ()) if inner: return is_verification(inner[0], inner[1:], local_keywords, extra_verify) return False + # Run Keyword And Warn On Failure *args - a failure only logs a WARN + # and the test stays green (BuiltIn semantics). The wrapped assertion can never + # reprove the test, so this is NOT an oracle regardless of the inner keyword - + # same swallow problem as Run Keyword And Ignore Error (#88). + if n == "run keyword and warn on failure": + return False # Run Keywords A AND B AND Should Be Equal ... — a chain that # runs each keyword in sequence. The verification can be any segment, so split # on the AND separator and check each segment's first token as a nested call. diff --git a/tests/test_scanner.py b/tests/test_scanner.py index ab2b889..a2fc52d 100644 --- a/tests/test_scanner.py +++ b/tests/test_scanner.py @@ -2275,14 +2275,36 @@ def test_no_r2_continue_on_failure_around_assertion(tmp_path): assert "R2" not in codes(tmp_path, body) -def test_no_r2_warn_on_failure_around_assertion(tmp_path): - # The Warn On Failure variant is the same soft-assert wrapper. +def test_r2_warn_on_failure_around_assertion_is_hollow(tmp_path): + # #88: Warn On Failure only logs a WARN, the test stays green - the wrapped + # assertion cannot reprove the test, so the verifier keyword is hollow (R2). body = """\ *** Keywords *** Verify X Run Keyword And Warn On Failure Should Be Equal ${a} ${b} """ - assert "R2" not in codes(tmp_path, body) + assert "R2" in codes(tmp_path, body) + + +# #88: Warn On Failure is not an oracle (the failure only logs a WARN); Continue +# On Failure still fails the test at the end and remains a valid oracle. +def test_warn_on_failure_is_not_an_oracle_c2b(tmp_path): + body = """\ +*** Test Cases *** +Falso Verde + Run Keyword And Warn On Failure Should Be Equal ${1} ${2} +""" + assert "C2b" in codes(tmp_path, body) + + +def test_continue_on_failure_stays_an_oracle(tmp_path): + body = """\ +*** Test Cases *** +Soft Assert + Continue On Failure + Run Keyword And Continue On Failure Should Be Equal ${1} ${2} +""" + assert "C2b" not in codes(tmp_path, body) # FP-3 (C9b): expected_status=any with a manual status assert on the next line.