diff --git a/tools/hrw4u/src/hrw_symbols.py b/tools/hrw4u/src/hrw_symbols.py index 0a0339b2d0d..84f51a338b7 100644 --- a/tools/hrw4u/src/hrw_symbols.py +++ b/tools/hrw4u/src/hrw_symbols.py @@ -183,8 +183,7 @@ def _handle_prefix_conditions(self, tag: str, payload: str, section: SectionType if tag == "HEADER": return f"{self.get_prefix_for_context('header_condition', section)}{suffix}", False - else: - return f"{lhs_prefix}{suffix.replace(':', '.')}", False + return f"{lhs_prefix}{suffix.replace(':', '.')}", False return None def _should_lowercase_suffix(self, tag_match: str, lhs_prefix: str) -> bool: @@ -508,10 +507,15 @@ def op_to_hrw4u(self, cmd: str, args: list[str], section: SectionType | None, op commands = params.target if params else None if (isinstance(commands, (list, tuple)) and cmd in commands) or (cmd == commands): uppercase = params.upper if params else False + if params: + qualifier = toks[1] if len(toks) > 1 else "" + name = f"{lhs_key}{qualifier}" if lhs_key.endswith(".") else lhs_key + self.validate_section_access(name, section, params.sections) return self._handle_operator_command(cmd, toks, lhs_key, uppercase, section) for name, params in tables.STATEMENT_FUNCTION_MAP.items(): if params.target == cmd: + self.validate_section_access(name, section, params.sections) return self._handle_statement_function(name, args, section, op_state) raise SymbolResolutionError(line, f"Unknown operator: {cmd}") diff --git a/tools/hrw4u/src/hrw_visitor.py b/tools/hrw4u/src/hrw_visitor.py index b6dc61a90d0..7d33aff9208 100644 --- a/tools/hrw4u/src/hrw_visitor.py +++ b/tools/hrw4u/src/hrw_visitor.py @@ -371,8 +371,11 @@ def visitOpLine(self, ctx: u4wrhParser.OpLineContext) -> None: args = self._reconstruct_redirect_args(args) self.debug(f"reconstructed redirect: {args}") - stmt = self.symbol_resolver.op_to_hrw4u(cmd, args, self._section_label, op_state) - self.emit(stmt + ";") + stmt = None + with self.trap(ctx): + stmt = self.symbol_resolver.op_to_hrw4u(cmd, args, self._section_label, op_state) + if stmt is not None: + self.emit(stmt + ";") return None diff --git a/tools/hrw4u/tests/data/hooks/txn_close_http_op.reverse.fail.error.txt b/tools/hrw4u/tests/data/hooks/txn_close_http_op.reverse.fail.error.txt new file mode 100644 index 00000000000..3369fe1893c --- /dev/null +++ b/tools/hrw4u/tests/data/hooks/txn_close_http_op.reverse.fail.error.txt @@ -0,0 +1 @@ +is not available in the TXN_CLOSE section diff --git a/tools/hrw4u/tests/data/hooks/txn_close_http_op.reverse.fail.hrw.txt b/tools/hrw4u/tests/data/hooks/txn_close_http_op.reverse.fail.hrw.txt new file mode 100644 index 00000000000..e64852983ae --- /dev/null +++ b/tools/hrw4u/tests/data/hooks/txn_close_http_op.reverse.fail.hrw.txt @@ -0,0 +1,3 @@ +cond %{TXN_CLOSE_HOOK} [AND] +cond %{CLIENT-HEADER:@sampleratio} ="0.05" + add-header @TCPInfo "TC; %{TCP-INFO}" diff --git a/tools/hrw4u/tests/data/ops/txn_close_set_destination.reverse.fail.error.txt b/tools/hrw4u/tests/data/ops/txn_close_set_destination.reverse.fail.error.txt new file mode 100644 index 00000000000..3369fe1893c --- /dev/null +++ b/tools/hrw4u/tests/data/ops/txn_close_set_destination.reverse.fail.error.txt @@ -0,0 +1 @@ +is not available in the TXN_CLOSE section diff --git a/tools/hrw4u/tests/data/ops/txn_close_set_destination.reverse.fail.hrw.txt b/tools/hrw4u/tests/data/ops/txn_close_set_destination.reverse.fail.hrw.txt new file mode 100644 index 00000000000..5e366e4baa4 --- /dev/null +++ b/tools/hrw4u/tests/data/ops/txn_close_set_destination.reverse.fail.hrw.txt @@ -0,0 +1,2 @@ +cond %{TXN_CLOSE_HOOK} [AND] + set-destination HOST "foo" diff --git a/tools/hrw4u/tests/test_hooks_reverse.py b/tools/hrw4u/tests/test_hooks_reverse.py index 5db1958aff2..1111ff43f16 100644 --- a/tools/hrw4u/tests/test_hooks_reverse.py +++ b/tools/hrw4u/tests/test_hooks_reverse.py @@ -28,3 +28,12 @@ def test_reverse_conversion(input_file: Path, output_file: Path) -> None: """Test that u4wrh reverse conversion produces original hrw4u for hooks test cases.""" utils.run_reverse_test(input_file, output_file) + + +@pytest.mark.hooks +@pytest.mark.reverse +@pytest.mark.invalid +@pytest.mark.parametrize("input_file", utils.collect_reverse_failing_inputs("hooks")) +def test_reverse_invalid_inputs_fail(input_file: Path) -> None: + """Test that u4wrh surfaces section access errors for invalid HRW hook inputs.""" + utils.run_reverse_failing_test(input_file) diff --git a/tools/hrw4u/tests/test_ops_reverse.py b/tools/hrw4u/tests/test_ops_reverse.py index 9194c165e2d..2832359a7f7 100644 --- a/tools/hrw4u/tests/test_ops_reverse.py +++ b/tools/hrw4u/tests/test_ops_reverse.py @@ -28,3 +28,12 @@ def test_reverse_conversion(input_file: Path, output_file: Path) -> None: """Test that u4wrh reverse conversion produces original hrw4u for ops test cases.""" utils.run_reverse_test(input_file, output_file) + + +@pytest.mark.ops +@pytest.mark.reverse +@pytest.mark.invalid +@pytest.mark.parametrize("input_file", utils.collect_reverse_failing_inputs("ops")) +def test_reverse_invalid_inputs_fail(input_file: Path) -> None: + """Test that u4wrh surfaces section access errors for invalid HRW operator inputs.""" + utils.run_reverse_failing_test(input_file) diff --git a/tools/hrw4u/tests/utils.py b/tools/hrw4u/tests/utils.py index bd893e7291f..941fe1d615b 100644 --- a/tools/hrw4u/tests/utils.py +++ b/tools/hrw4u/tests/utils.py @@ -40,12 +40,14 @@ "collect_output_test_files", "collect_ast_test_files", "collect_failing_inputs", + "collect_reverse_failing_inputs", "collect_sandbox_deny_test_files", "collect_sandbox_allow_test_files", "collect_sandbox_warn_test_files", "run_output_test", "run_ast_test", "run_failing_test", + "run_reverse_failing_test", "run_sandbox_deny_test", "run_sandbox_allow_test", "run_sandbox_warn_test", @@ -128,6 +130,17 @@ def collect_failing_inputs(group: str) -> Iterator[pytest.param]: yield pytest.param(input_file, id=test_id) +def collect_reverse_failing_inputs(group: str) -> Iterator[pytest.param]: + # Reverse-fail fixtures use *.reverse.fail.hrw.txt (u4wrh input that must error) + # paired with *.reverse.fail.error.txt (expected error phrase). Forward-fail + # fixtures use *.fail.input.txt for the hrw4u compiler, which is why the glob + # here excludes them. + base_dir = Path("tests/data") / group + for input_file in base_dir.glob("*.reverse.fail.hrw.txt"): + test_id = input_file.stem + yield pytest.param(input_file, id=test_id) + + def _collect_sandbox_test_files(group: str, result_suffix: str) -> Iterator[pytest.param]: """Collect sandbox test files: (input, result, sandbox_config). @@ -373,6 +386,35 @@ def run_reverse_test(input_file: Path, output_file: Path, debug: bool = False) - assert actual_hrw4u == expected_hrw4u, f"Reverse conversion mismatch for {output_file}" +def run_reverse_failing_test(input_file: Path) -> None: + hrw_text = input_file.read_text() + lexer = u4wrhLexer(InputStream(hrw_text)) + stream = CommonTokenStream(lexer) + parser = u4wrhParser(stream) + tree = parser.program() + + error_file = input_file.with_name(input_file.name.replace(".reverse.fail.hrw.txt", ".reverse.fail.error.txt")) + if not error_file.exists(): + raise RuntimeError(f"Missing expected error file: {error_file}") + + expected_error_content = error_file.read_text().strip() + + error_collector = ErrorCollector() + visitor = HRWInverseVisitor(filename=str(input_file), error_collector=error_collector) + visitor.visit(tree) + + assert error_collector.has_errors(), f"Expected reverse errors but none were raised for {input_file}" + + actual_summary = error_collector.get_error_summary() + for line in expected_error_content.splitlines(): + line = line.strip() + if line: + assert line in actual_summary, ( + f"Expected phrase not found in error summary for {input_file}:\n" + f" Missing: {line!r}\n" + f"Actual summary:\n{actual_summary}") + + def create_output_test(group: str): import pytest