|
| 1 | +"""Assertions about what a child Robot Framework run did. |
| 2 | +
|
| 3 | +Used by atest/test/13_Python_Extension/python_extension.robot, which starts |
| 4 | +child `robot` runs and then verifies their results. This lives here rather than |
| 5 | +next to those suites on purpose: the files under 13_Python_Extension/ are |
| 6 | +quoted by path in the documentation on robotframework-browser.org and must stay |
| 7 | +free of test scaffolding. |
| 8 | +""" |
| 9 | + |
| 10 | +import json |
| 11 | +import re |
| 12 | +from pathlib import Path |
| 13 | + |
| 14 | +from robot.api import logger |
| 15 | +from robot.result import ExecutionResult, Result, TestCase |
| 16 | + |
| 17 | + |
| 18 | +def get_child_result(output_xml: Path) -> Result: |
| 19 | + """Parse the output.xml of a child run.""" |
| 20 | + result = ExecutionResult(str(output_xml)) |
| 21 | + logger.info(f"Parsed {output_xml}") |
| 22 | + return result |
| 23 | + |
| 24 | + |
| 25 | +def child_test_status_should_be( |
| 26 | + result: Result, |
| 27 | + name: str, |
| 28 | + status: str, |
| 29 | + message_pattern: str | None = None, |
| 30 | +): |
| 31 | + """Verify status, and optionally the failure message, of a child run test.""" |
| 32 | + test = _get_test(result, name) |
| 33 | + assert test.status == status, ( |
| 34 | + f"Test '{name}' status was '{test.status}', expected '{status}'. " |
| 35 | + f"Message: {test.message}" |
| 36 | + ) |
| 37 | + if message_pattern is not None: |
| 38 | + assert re.search(message_pattern, test.message), ( |
| 39 | + f"Test '{name}' message '{test.message}' does not match '{message_pattern}'" |
| 40 | + ) |
| 41 | + |
| 42 | + |
| 43 | +def child_keyword_should_log( |
| 44 | + result: Result, |
| 45 | + test_name: str, |
| 46 | + owner: str, |
| 47 | + message_pattern: str, |
| 48 | +): |
| 49 | + """Verify a message was logged inside a keyword owned by the given library. |
| 50 | +
|
| 51 | + Browser keywords called from Python do not appear in output.xml as keywords |
| 52 | + of their own, but everything they log lands inside the keyword of the |
| 53 | + library that called them. |
| 54 | + """ |
| 55 | + messages = [] |
| 56 | + for keyword in _get_keywords(_get_test(result, test_name)): |
| 57 | + if keyword.owner != owner: |
| 58 | + continue |
| 59 | + for message in keyword.messages: |
| 60 | + messages.append(message.message) |
| 61 | + if re.search(message_pattern, message.message): |
| 62 | + logger.info( |
| 63 | + f"Found '{message_pattern}' in keyword " |
| 64 | + f"'{keyword.full_name}': {message.message}" |
| 65 | + ) |
| 66 | + return |
| 67 | + raise AssertionError( |
| 68 | + f"No message matching '{message_pattern}' logged by a '{owner}' keyword " |
| 69 | + f"in test '{test_name}'. Messages: {messages}" |
| 70 | + ) |
| 71 | + |
| 72 | + |
| 73 | +def child_should_not_use_library(result: Result, library: str): |
| 74 | + """Verify the child run did not call any keyword of the given library. |
| 75 | +
|
| 76 | + Guards context B: if a child suite ever imports Browser as a Robot |
| 77 | + Framework library, the listener registers and the context collapses into |
| 78 | + context A without any test failing. |
| 79 | + """ |
| 80 | + for test in result.suite.tests: |
| 81 | + for keyword in _get_keywords(test): |
| 82 | + assert keyword.owner != library, ( |
| 83 | + f"Test '{test.name}' called '{keyword.full_name}' " |
| 84 | + f"from library '{library}'" |
| 85 | + ) |
| 86 | + |
| 87 | + |
| 88 | +def playwright_log_should_have_rf_context(log_file: Path, expected: str): |
| 89 | + """Verify node side log records carry the Robot Framework test name.""" |
| 90 | + test_names = _logged_test_names(log_file) |
| 91 | + assert any(expected in name for name in test_names), ( |
| 92 | + f"No node side log record with test name containing '{expected}'. " |
| 93 | + f"Found: {sorted(test_names)}" |
| 94 | + ) |
| 95 | + |
| 96 | + |
| 97 | +def playwright_log_should_not_have_rf_context(log_file: Path): |
| 98 | + """Verify node side log records carry no Robot Framework test name.""" |
| 99 | + test_names = _logged_test_names(log_file) |
| 100 | + assert not test_names, ( |
| 101 | + f"Node side log records unexpectedly carry test names: {sorted(test_names)}" |
| 102 | + ) |
| 103 | + |
| 104 | + |
| 105 | +def _logged_test_names(log_file: Path) -> set: |
| 106 | + log_file = Path(log_file) |
| 107 | + assert log_file.is_file(), ( |
| 108 | + f"{log_file} not found. The child run did not start its own node process." |
| 109 | + ) |
| 110 | + test_names = set() |
| 111 | + for raw_line in log_file.read_text(encoding="utf-8", errors="replace").splitlines(): |
| 112 | + line = raw_line.strip() |
| 113 | + if not line.startswith("{"): |
| 114 | + continue |
| 115 | + try: |
| 116 | + record = json.loads(line) |
| 117 | + except json.JSONDecodeError: |
| 118 | + continue |
| 119 | + if isinstance(record, dict) and record.get("test_name"): |
| 120 | + test_names.add(record["test_name"]) |
| 121 | + logger.info(f"Test names in {log_file}: {sorted(test_names)}") |
| 122 | + return test_names |
| 123 | + |
| 124 | + |
| 125 | +def _get_test(result: Result, name: str) -> TestCase: |
| 126 | + for test in result.suite.tests: |
| 127 | + if test.name == name: |
| 128 | + return test |
| 129 | + names = [test.name for test in result.suite.tests] |
| 130 | + raise AssertionError(f"Test '{name}' not found from child run. Found: {names}") |
| 131 | + |
| 132 | + |
| 133 | +def _get_keywords(item) -> list: |
| 134 | + keywords = [] |
| 135 | + for child in item.body: |
| 136 | + if child.type == child.KEYWORD: |
| 137 | + keywords.append(child) |
| 138 | + keywords.extend(_get_keywords(child)) |
| 139 | + return keywords |
0 commit comments