diff --git a/docs/03_reference/config.md b/docs/03_reference/config.md index 2007dcc70..c7ffbe05e 100644 --- a/docs/03_reference/config.md +++ b/docs/03_reference/config.md @@ -1329,7 +1329,7 @@ corresponds to the `-N --name name` option of _robot_ ## no-status-rc -Type: `bool | Flag | None` +Type: `bool | Flag | Condition | None` Sets the return code to zero regardless of failures in test cases. Error codes are returned normally. @@ -1341,6 +1341,11 @@ Examples: no-status-rc = true ``` +```toml +# decide from the current branch +no-status-rc = { if = "environ.get('CI_COMMIT_REF_NAME') == 'main'" } +``` + corresponds to the `--nostatusrc` option of _robot_ ## output @@ -2271,7 +2276,7 @@ corresponds to the `-N --name name` option of _robot_ ## rebot.no-status-rc -Type: `bool | Flag | None` +Type: `bool | Flag | Condition | None` Sets the return code to zero regardless of failures in test cases. Error codes are returned normally. @@ -2283,6 +2288,11 @@ Examples: no-status-rc = true ``` +```toml +# decide from the current branch +no-status-rc = { if = "environ.get('CI_COMMIT_REF_NAME') == 'main'" } +``` + corresponds to the `--nostatusrc` option of _robot_ ## rebot.output diff --git a/docs/public/schemas/robot.toml.json b/docs/public/schemas/robot.toml.json index ef344bad2..e8badd67c 100644 --- a/docs/public/schemas/robot.toml.json +++ b/docs/public/schemas/robot.toml.json @@ -559,12 +559,7 @@ ], "markdownDescription": "Condition to evaluate. This must be a Python \"eval\" expression.\nFor security reasons, only certain expressions and functions are allowed.\n\nExamples:\n```toml\nif = \"re.match(r'^\\d+$', environ.get('TEST_VAR', ''))\"\nif = \"platform.system() == 'Linux'\"\n```\n\nsee also `expr` for allowed global names.\n", "title": "If", - "type": "string", - "x-taplo": { - "links": { - "key": "https://robotcode.io/03_reference/config#profile-enabled-if" - } - } + "type": "string" } }, "required": [ @@ -2167,16 +2162,20 @@ "default" ] }, + { + "$ref": "#/definitions/Condition" + }, { "type": "null" } ], "default": null, - "description": "Sets the return code to zero regardless of failures\nin test cases. Error codes are returned normally.\n\nExamples:\n\n```toml\n# always exit 0 regardless of failed tests\nno-status-rc = true\n```\n\ncorresponds to the `--nostatusrc` option of _robot_\n", + "description": "Sets the return code to zero regardless of failures\nin test cases. Error codes are returned normally.\n\nExamples:\n\n```toml\n# always exit 0 regardless of failed tests\nno-status-rc = true\n```\n\n```toml\n# decide from the current branch\nno-status-rc = { if = \"environ.get('CI_COMMIT_REF_NAME') == 'main'\" }\n```\n\ncorresponds to the `--nostatusrc` option of _robot_\n", "examples": [ - "# always exit 0 regardless of failed tests\nno-status-rc = true" + "# always exit 0 regardless of failed tests\nno-status-rc = true", + "# decide from the current branch\nno-status-rc = { if = \"environ.get('CI_COMMIT_REF_NAME') == 'main'\" }" ], - "markdownDescription": "Sets the return code to zero regardless of failures\nin test cases. Error codes are returned normally.\n\nExamples:\n\n```toml\n# always exit 0 regardless of failed tests\nno-status-rc = true\n```\n\ncorresponds to the `--nostatusrc` option of _robot_\n", + "markdownDescription": "Sets the return code to zero regardless of failures\nin test cases. Error codes are returned normally.\n\nExamples:\n\n```toml\n# always exit 0 regardless of failed tests\nno-status-rc = true\n```\n\n```toml\n# decide from the current branch\nno-status-rc = { if = \"environ.get('CI_COMMIT_REF_NAME') == 'main'\" }\n```\n\ncorresponds to the `--nostatusrc` option of _robot_\n", "title": "No Status Rc", "x-taplo": { "links": { @@ -4865,16 +4864,20 @@ "default" ] }, + { + "$ref": "#/definitions/Condition" + }, { "type": "null" } ], "default": null, - "description": "Sets the return code to zero regardless of failures\nin test cases. Error codes are returned normally.\n\nExamples:\n\n```toml\n# always exit 0 regardless of failed tests\nno-status-rc = true\n```\n\ncorresponds to the `--nostatusrc` option of _robot_\n", + "description": "Sets the return code to zero regardless of failures\nin test cases. Error codes are returned normally.\n\nExamples:\n\n```toml\n# always exit 0 regardless of failed tests\nno-status-rc = true\n```\n\n```toml\n# decide from the current branch\nno-status-rc = { if = \"environ.get('CI_COMMIT_REF_NAME') == 'main'\" }\n```\n\ncorresponds to the `--nostatusrc` option of _robot_\n", "examples": [ - "# always exit 0 regardless of failed tests\nno-status-rc = true" + "# always exit 0 regardless of failed tests\nno-status-rc = true", + "# decide from the current branch\nno-status-rc = { if = \"environ.get('CI_COMMIT_REF_NAME') == 'main'\" }" ], - "markdownDescription": "Sets the return code to zero regardless of failures\nin test cases. Error codes are returned normally.\n\nExamples:\n\n```toml\n# always exit 0 regardless of failed tests\nno-status-rc = true\n```\n\ncorresponds to the `--nostatusrc` option of _robot_\n", + "markdownDescription": "Sets the return code to zero regardless of failures\nin test cases. Error codes are returned normally.\n\nExamples:\n\n```toml\n# always exit 0 regardless of failed tests\nno-status-rc = true\n```\n\n```toml\n# decide from the current branch\nno-status-rc = { if = \"environ.get('CI_COMMIT_REF_NAME') == 'main'\" }\n```\n\ncorresponds to the `--nostatusrc` option of _robot_\n", "title": "No Status Rc", "x-taplo": { "links": { @@ -8106,16 +8109,20 @@ "default" ] }, + { + "$ref": "#/definitions/Condition" + }, { "type": "null" } ], "default": null, - "description": "Sets the return code to zero regardless of failures\nin test cases. Error codes are returned normally.\n\nExamples:\n\n```toml\n# always exit 0 regardless of failed tests\nno-status-rc = true\n```\n\ncorresponds to the `--nostatusrc` option of _robot_\n", + "description": "Sets the return code to zero regardless of failures\nin test cases. Error codes are returned normally.\n\nExamples:\n\n```toml\n# always exit 0 regardless of failed tests\nno-status-rc = true\n```\n\n```toml\n# decide from the current branch\nno-status-rc = { if = \"environ.get('CI_COMMIT_REF_NAME') == 'main'\" }\n```\n\ncorresponds to the `--nostatusrc` option of _robot_\n", "examples": [ - "# always exit 0 regardless of failed tests\nno-status-rc = true" + "# always exit 0 regardless of failed tests\nno-status-rc = true", + "# decide from the current branch\nno-status-rc = { if = \"environ.get('CI_COMMIT_REF_NAME') == 'main'\" }" ], - "markdownDescription": "Sets the return code to zero regardless of failures\nin test cases. Error codes are returned normally.\n\nExamples:\n\n```toml\n# always exit 0 regardless of failed tests\nno-status-rc = true\n```\n\ncorresponds to the `--nostatusrc` option of _robot_\n", + "markdownDescription": "Sets the return code to zero regardless of failures\nin test cases. Error codes are returned normally.\n\nExamples:\n\n```toml\n# always exit 0 regardless of failed tests\nno-status-rc = true\n```\n\n```toml\n# decide from the current branch\nno-status-rc = { if = \"environ.get('CI_COMMIT_REF_NAME') == 'main'\" }\n```\n\ncorresponds to the `--nostatusrc` option of _robot_\n", "title": "No Status Rc", "x-taplo": { "links": { diff --git a/packages/robot/src/robotcode/robot/config/model.py b/packages/robot/src/robotcode/robot/config/model.py index bf7482ba1..6fd9faf30 100644 --- a/packages/robot/src/robotcode/robot/config/model.py +++ b/packages/robot/src/robotcode/robot/config/model.py @@ -682,7 +682,7 @@ class CommonOptions(RobotBaseOptions): robot_priority=500, robot_short_name="N", ) - no_status_rc: Union[bool, Flag, None] = field( + no_status_rc: Union[bool, Flag, Condition, None] = field( description="""\ Sets the return code to zero regardless of failures in test cases. Error codes are returned normally. @@ -694,6 +694,11 @@ class CommonOptions(RobotBaseOptions): no-status-rc = true ``` + ```toml + # decide from the current branch + no-status-rc = { if = "environ.get('CI_COMMIT_REF_NAME') == 'main'" } + ``` + corresponds to the `--nostatusrc` option of _robot_ """, robot_name="statusrc", diff --git a/scripts/create_robot_toml_json_schema.py b/scripts/create_robot_toml_json_schema.py index 4c2a9a232..200a5fee6 100644 --- a/scripts/create_robot_toml_json_schema.py +++ b/scripts/create_robot_toml_json_schema.py @@ -248,6 +248,16 @@ def process_def(def_name: str, path_prefix: str) -> None: if isinstance(root_ref, str) and root_ref.startswith("#/$defs/"): process_def(root_ref.rsplit("/", 1)[-1], "") + # Condition is shared by enabled, hidden, and conditional flag fields, so + # its nested key has no single context-independent documentation anchor. + condition_schema = defs.get("Condition") + if isinstance(condition_schema, dict): + condition_properties = condition_schema.get("properties") + if isinstance(condition_properties, dict): + condition_if = condition_properties.get("if") + if isinstance(condition_if, dict): + condition_if.pop("x-taplo", None) + def _convert_to_draft7_schema(schema_dict: dict) -> None: """Convert generated schema to draft-07 compatible structure in-place.""" diff --git a/scripts/generate_rf_options.py b/scripts/generate_rf_options.py index 8809abda6..142f9986f 100644 --- a/scripts/generate_rf_options.py +++ b/scripts/generate_rf_options.py @@ -87,7 +87,7 @@ # --- numeric options --- "--consolewidth": ["console-width = 100"], "--maxassignlength": ["max-assign-length = 200"], - "--maxerrorlines": ["max-error-lines = 40"], + "--maxerrorlines": ["max-error-lines = 40", 'max-error-lines = "NONE"'], "--suitestatlevel": ["suite-stat-level = 2"], # --- single-string options (paths, names, titles, timestamps) --- "--debugfile": ['debug-file = "debug.log"'], @@ -161,6 +161,7 @@ "--splitlog": ["split-log = true"], "--nostatusrc": [ "# always exit 0 regardless of failed tests\nno-status-rc = true", + "# decide from the current branch\nno-status-rc = { if = \"environ.get('CI_COMMIT_REF_NAME') == 'main'\" }", ], "--timestampoutputs": ["timestamp-outputs = true"], } @@ -345,6 +346,8 @@ def get_type( ) -> str: if not option.get("param"): if is_flag: + if name == "no_status_rc": + return "Union[bool, Flag, Condition, None]" return "Union[bool, Flag, None]" return "Optional[bool]" @@ -421,6 +424,9 @@ def build_class_fields(output: List[str], opts: Dict[str, Any], extend: bool = F output.append(" robot_is_flag=True,") if not flag_default: output.append(f" robot_flag_default={flag_default},") + alias = name.replace("_", "-") + if alias != name: + output.append(f' alias="{alias}",') output.append(" )") return result @@ -549,6 +555,8 @@ def build_class_fields(output: List[str], opts: Dict[str, Any], extend: bool = F tool="testdoc", ) +output.extend(["", ""]) + model_file = Path("packages/robot/src/robotcode/robot/config/model.py") original_lines = model_file.read_text().splitlines() diff --git a/tests/robotcode/robot/config/test_profile.py b/tests/robotcode/robot/config/test_profile.py index b4b93df81..c0a2f047b 100644 --- a/tests/robotcode/robot/config/test_profile.py +++ b/tests/robotcode/robot/config/test_profile.py @@ -374,3 +374,57 @@ def test_type_that_wants_alist_should_throw_an_error() -> None: """ with pytest.raises(TypeError, match=r".*Value '.*' must be of type.*"): load_robot_config_from_robot_toml_str(data) + + +_STATUS_RC_FLAGS = {"--nostatusrc", "--statusrc"} + + +def _status_rc_flags(data: str, *profiles: str, rebot: bool = False) -> list[str]: + config = load_robot_config_from_robot_toml_str(data) + options = config.rebot if rebot else config.combine_profiles(*profiles) + assert options is not None + return [arg for arg in options.evaluated().build_command_line() if arg in _STATUS_RC_FLAGS] + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + ("true", ["--nostatusrc"]), + ("false", ["--statusrc"]), + ('"on"', ["--nostatusrc"]), + ('"off"', ["--statusrc"]), + ('"default"', []), + ], +) +def test_no_status_rc_literals_keep_flag_semantics(value: str, expected: list[str]) -> None: + assert _status_rc_flags(f"no-status-rc = {value}") == expected + + +@pytest.mark.parametrize(("section", "rebot"), [("", False), ("[rebot]\n", True)]) +@pytest.mark.parametrize( + ("condition", "expected"), + [("True", ["--nostatusrc"]), ("False", ["--statusrc"])], +) +def test_no_status_rc_accepts_conditions( + section: str, + rebot: bool, + condition: str, + expected: list[str], +) -> None: + assert _status_rc_flags(f'{section}no-status-rc = {{ if = "{condition}" }}', rebot=rebot) == expected + + +def test_no_status_rc_rejects_invalid_condition() -> None: + data = """\ + [profiles.ci] + no-status-rc = { if = "environ.get('CI') = 'true'" } + """ + + with pytest.raises( + ValueError, + match=( + r"^Evaluation of 'no_status_rc' failed: EvaluationError: " + r"Evaluation of \"environ\.get\('CI'\) = 'true'\" failed: invalid syntax" + ), + ): + _status_rc_flags(data, "ci") diff --git a/tests/robotcode/robot/config/test_schema.py b/tests/robotcode/robot/config/test_schema.py new file mode 100644 index 000000000..da67070a5 --- /dev/null +++ b/tests/robotcode/robot/config/test_schema.py @@ -0,0 +1,10 @@ +import json +from pathlib import Path + + +def test_shared_condition_has_no_context_specific_documentation_link() -> None: + schema_path = Path(__file__).parents[4] / "docs/public/schemas/robot.toml.json" + schema = json.loads(schema_path.read_text(encoding="utf-8")) + condition_if = schema["definitions"]["Condition"]["properties"]["if"] + + assert "x-taplo" not in condition_if diff --git a/tests/robotcode/runner/cli/test_conditional_no_status_rc.py b/tests/robotcode/runner/cli/test_conditional_no_status_rc.py new file mode 100644 index 000000000..f915b8830 --- /dev/null +++ b/tests/robotcode/runner/cli/test_conditional_no_status_rc.py @@ -0,0 +1,64 @@ +import os +import subprocess +import sys +from pathlib import Path + +import pytest + + +@pytest.mark.parametrize( + ("branch", "expected_flag", "expected_returncode"), + [ + ("main", "--nostatusrc", 0), + ("feature/example", "--statusrc", 1), + ], +) +def test_ci_profile_conditional_no_status_rc_controls_robot_exit_status( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + branch: str, + expected_flag: str, + expected_returncode: int, +) -> None: + (tmp_path / "robot.toml").write_text( + """\ +default-profiles = ["ci"] + +[profiles.ci] +enabled = { if = "environ.get('CI') == 'true'" } +no-status-rc = { if = "environ.get('CI_COMMIT_REF_NAME') == 'main'" } +""", + encoding="utf-8", + ) + (tmp_path / "suite.robot").write_text( + """\ +*** Test Cases *** +Fails + Fail expected failure +""", + encoding="utf-8", + ) + monkeypatch.setenv("CI", "true") + monkeypatch.setenv("CI_COMMIT_REF_NAME", branch) + user_dir = tmp_path / "user" + for name in ("HOME", "XDG_CONFIG_HOME", "APPDATA", "LOCALAPPDATA"): + monkeypatch.setenv(name, str(user_dir)) + monkeypatch.setenv("ROBOTCODE_CACHE_DIR", str(tmp_path / "cache")) + + result = subprocess.run( + [sys.executable, "-m", "robotcode.cli", "--verbose", "run", "suite.robot"], + cwd=tmp_path, + capture_output=True, + text=True, + timeout=120, + env=os.environ.copy(), + ) + output = result.stdout + result.stderr + unexpected_flag = "--statusrc" if expected_flag == "--nostatusrc" else "--nostatusrc" + + assert result.returncode == expected_returncode, output + assert "Selected profiles: ci" in output + assert 'Using profile "ci".' in output + assert "1 test, 0 passed, 1 failed" in output + assert expected_flag in output + assert unexpected_flag not in output