From bc9a0b9c1f6d3bb6a5e218f8ca85432e78f68914 Mon Sep 17 00:00:00 2001 From: oleks-dev <1228369+oleks-dev@users.noreply.github.com> Date: Mon, 29 Sep 2025 21:21:03 +0200 Subject: [PATCH] add windows to the github workflow matrix --- .github/workflows/pytest-and-coverage.yaml | 11 ++- prich/core/steps/step_run_command.py | 6 +- pyproject.toml | 2 +- tests/fixtures/paths.py | 2 +- tests/test_engine.py | 9 ++- tests/test_init.py | 2 +- tests/test_providers.py | 7 +- tests/test_run_template.py | 8 +- tests/test_template.py | 14 ++-- tests/test_validate.py | 91 +++++++++++----------- 10 files changed, 81 insertions(+), 71 deletions(-) diff --git a/.github/workflows/pytest-and-coverage.yaml b/.github/workflows/pytest-and-coverage.yaml index 167d9c3..559aee5 100644 --- a/.github/workflows/pytest-and-coverage.yaml +++ b/.github/workflows/pytest-and-coverage.yaml @@ -3,12 +3,14 @@ name: CI - Pytest & Coverage on: push: paths: + - ".github/**" - "pyproject.toml" - "tests/**" - "prich/**" branches: [ main ] pull_request: paths: + - ".github/**" - "pyproject.toml" - "tests/**" - "prich/**" @@ -16,7 +18,6 @@ on: jobs: test: - runs-on: ubuntu-latest # read repo and publish checks permissions: @@ -26,6 +27,8 @@ jobs: strategy: matrix: python-version: ["3.10", "3.11"] + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} steps: - name: Checkout repository @@ -48,11 +51,7 @@ jobs: - name: Run tests with coverage + JUnit report run: | - pytest \ - -n auto \ - --junitxml=pytest-results.xml \ - --cov-report=xml \ - --cov-report=html + pytest -n auto --junitxml=pytest-results.xml --cov-report=xml --cov-report=html # Upload artifacts only for Python 3.11 to avoid duplicates - name: Upload coverage reports (only on 3.11) diff --git a/prich/core/steps/step_run_command.py b/prich/core/steps/step_run_command.py index b94787e..008b22c 100644 --- a/prich/core/steps/step_run_command.py +++ b/prich/core/steps/step_run_command.py @@ -22,9 +22,9 @@ def run_command_step(template: TemplateModel, step: PythonStep | CommandStep, va if isinstance(step, PythonStep) and step.type == "python": method_path = template_dir / "scripts" / method if not method_path.exists(): - raise click.ClickException(f"Python script not found: {method_path}") + raise click.ClickException(f"Python script not found: {str(method_path)}") if not method.endswith(".py"): - raise click.ClickException(f"Python script file should end with .py: {method_path}") + raise click.ClickException(f"Python script file should end with .py: {str(method_path)}") if template.venv in ["shared", "isolated"]: if template.venv == "shared": @@ -33,7 +33,7 @@ def run_command_step(template: TemplateModel, step: PythonStep | CommandStep, va venv_path = template_dir / "scripts" / "venv" python_path = venv_path / "bin" / "python" if not python_path.exists(): - raise click.ClickException(f"{template.venv.capitalize()} venv python not found: {python_path}") + raise click.ClickException(f"{template.venv.capitalize()} venv python not found: {str(python_path)}") cmd = [str(python_path), str(method_path)] elif template.venv is None: cmd = ["python", str(method_path)] diff --git a/pyproject.toml b/pyproject.toml index f4da2d8..53a243b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ classifiers = [ [project.optional-dependencies] openai = ["openai>=1.0.0,<2.0.0"] mlx = ["mlx_lm>=0.24.1,<1.0.0"] -dev = ["openai", "mlx_lm", "pytest", "coverage", "pytest-cov", "pytest-xdist", "twine", "build", "faker"] +dev = ["openai", "pytest", "coverage", "pytest-cov", "pytest-xdist", "twine", "build", "faker"] [project.urls] Homepage = "https://github.com/oleks-dev/prich" diff --git a/tests/fixtures/paths.py b/tests/fixtures/paths.py index 2a785d7..154abc3 100644 --- a/tests/fixtures/paths.py +++ b/tests/fixtures/paths.py @@ -42,7 +42,7 @@ def mock_paths(tmp_path, monkeypatch): local_templates=local_prich_templates_dir, #cwd_dir / ".prich" / "templates" ) ) - if "/pytest-" in str(tmp_path): + if "pytest-" in str(tmp_path): shutil.rmtree(tmp_path) else: raise RuntimeError(f"Failed to check folder before removing! {tmp_path}") diff --git a/tests/test_engine.py b/tests/test_engine.py index 070f17e..63b267f 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -1,4 +1,5 @@ import os +import re import tempfile from subprocess import CompletedProcess @@ -414,14 +415,14 @@ def test_validate_step_output(mock_paths, basic_config, case): "step": PythonStep(name="test", type="python", call="test.py"), "mock_output": CompletedProcess(args=["python", "test.py"], returncode=0, stdout="hello"), "expected_exception": click.ClickException, - "expected_exception_message": "Python script not found: test/templates/test-template/scripts/test.py" + "expected_exception_message": r"Python script not found: test[\/|\\]templates[\/|\\]test-template[\/|\\]scripts[\/|\\]test.py" }, {"id": "python_step_file_not_found_isolated_venv", "template": generate_template(template_id="test-template", isolated_venv=True), "step": PythonStep(name="test", type="python", call="test.py"), "mock_output": CompletedProcess(args=["python", "test.py"], returncode=0, stdout="hello"), "expected_exception": click.ClickException, - "expected_exception_message": "Python script not found: test/templates/test-template/scripts/test.py" + "expected_exception_message": r"Python script not found: test[\/|\\]templates[\/|\\]test-template[\/|\\]scripts[\/|\\]test.py" }, {"id": "command_step", "template": generate_template(template_id="test-template"), @@ -441,7 +442,7 @@ def test_validate_step_output(mock_paths, basic_config, case): "template": generate_template(template_id="test-template"), "step": CommandStep(name="test", type="command", call="notexistingcommand", args=["hello"]), "expected_exception": click.ClickException, - "expected_exception_message": "Unexpected error in notexistingcommand: [Errno 2] No such file or directory: 'notexistingcommand'" + "expected_exception_message": r"Unexpected error in notexistingcommand: [\[Errno 2\] No such file or directory: 'notexistingcommand'|\[WinErrno 2\] The system cannot find the file specified]" }, ] @pytest.mark.parametrize("case", get_run_command_step_CASES, ids=[c["id"] for c in get_run_command_step_CASES]) @@ -455,7 +456,7 @@ def test_run_command_step(case, monkeypatch): with pytest.raises(case.get("expected_exception")) as e: run_command_step(case.get("template"), case.get("step"), case.get("variables", {})) if case.get("expected_exception_message"): - assert str(e.value) in case.get("expected_exception_message") + assert re.search(case.get("expected_exception_message"), str(e.value)) else: actual, actual_exitcode = run_command_step(case.get("template"), case.get("step"), case.get("variables", {})) if case.get("expected_result") is not None: diff --git a/tests/test_init.py b/tests/test_init.py index 692a144..f28c81a 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -36,7 +36,7 @@ def test_init_cmd(mock_paths, monkeypatch, case): else: assert False, "Wrong init_folder param specified" - if "/pytest-" in str(prich_dir) and prich_dir.exists(): + if "pytest-" in str(prich_dir) and prich_dir.exists(): shutil.rmtree(prich_dir) else: raise RuntimeError(f"Failed to check folder before removing! {prich_dir}") diff --git a/tests/test_providers.py b/tests/test_providers.py index 2921553..89d9d5e 100644 --- a/tests/test_providers.py +++ b/tests/test_providers.py @@ -1,4 +1,6 @@ import json +import re + import click import pytest from dataclasses import dataclass @@ -60,7 +62,7 @@ class OpenAIStream: provider_type="stdin_consumer", name="echo", call="catt", args=[], mode="flat" ), "expected_exception": click.ClickException, - "expected_exception_messages": ["STDIN consumer provider error: [Errno 2] No such file or director"], + "expected_exception_messages_regex": [r"STDIN consumer provider error: [\[Errno 2\] No such file or directory|\[WinErrno 2\] The system cannot find the file specified]"], }, # OpenAI @@ -450,6 +452,9 @@ def fake_response_stream(self, **kwargs): if case.get("expected_exception_messages") is not None: for message in case.get("expected_exception_messages"): assert message in str(e.value) + if case.get("expected_exception_messages_regex") is not None: + for message in case.get("expected_exception_messages_regex"): + assert re.search(message, str(e.value)) else: result, output = capture_stdout(provider.send_prompt, prompt=prompt, instructions=instructions, input_=input_) result_repeat, output_repeat = capture_stdout(provider.send_prompt, prompt=prompt, instructions=instructions, input_=input_) diff --git a/tests/test_run_template.py b/tests/test_run_template.py index 9464519..a141538 100644 --- a/tests/test_run_template.py +++ b/tests/test_run_template.py @@ -347,7 +347,7 @@ call="echo1", args=["test"], validate=ValidateStepOutput( - match="No such file or directory: 'echo1'", + match="No such file or directory: 'echo1'|The system cannot find the file specified", not_match="test", match_exit_code=1, on_fail="error" @@ -358,7 +358,7 @@ # ), }, "expected_exception": click.ClickException, - "expected_exception_message": "No such file or directory: 'echo1'", + "expected_exception_message_regex": "No such file or directory: 'echo1'|The system cannot find the file specified", }, {"id": "run_cmd_and_validate_fail_exitcode_format", "template": # TemplateModel( @@ -372,7 +372,7 @@ call="echo", args=["test"], validate=ValidateStepOutput( - match="No such file or directory: 'echo1'", + match="No such file or directory: 'echo1'|The system cannot find the file specified", not_match="test", match_exit_code="hello", on_fail="error" @@ -907,6 +907,8 @@ def test_run_template(case, monkeypatch, basic_config): run_template(test_template.id) if case.get("expected_exception_message") is not None: assert case.get("expected_exception_message") in str(e.value) + if case.get("expected_exception_message_regex") is not None: + assert re.search(case.get("expected_exception_message_regex"), str(e.value)) else: result, out = capture_stdout(run_template, test_template.id) if case.get("expected_output"): diff --git a/tests/test_template.py b/tests/test_template.py index 7fab3a9..5f40c49 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -1,3 +1,5 @@ +import re + import pytest from click.testing import CliRunner from prich.models.file_scope import FileScope @@ -222,18 +224,18 @@ def test_show_template(mock_paths, template, case): {"id": "create_template_no_template_id", "iterations": [ {"args": [], - "expected_exception_messages": ["Usage: create [OPTIONS] TEMPLATE_ID"], + "expected_exception_messages": [r"Usage: create \[OPTIONS\] TEMPLATE_ID"], "expected_exit_code": 2}, ] }, {"id": "create_template_local", "iterations": [ {"args": ["test-tpl"], - "expected_exception_messages": ["Template test-tpl created in", "local/.prich/templates/test-tpl/test-tpl"], + "expected_exception_messages": [r"Template test-tpl created in", r"local[\/|\\]\.prich[\/|\\]templates[\/|\\]test-tpl[\/|\\]test-tpl"], "expected_exit_code": 0, "check_file": ""}, {"args": ["test-tpl"], - "expected_exception_messages": ["Error: Template test-tpl already exists."], + "expected_exception_messages": [r"Error: Template test-tpl already exists\."], "expected_exit_code": 1, "check_file": ""}, ] @@ -241,11 +243,11 @@ def test_show_template(mock_paths, template, case): {"id": "create_template_global", "iterations": [ {"args": ["test-tpl", "-g"], - "expected_exception_message": ["Template test-tpl created in", "global/.prich/templates/test-tpl/test-tpl"], + "expected_exception_message": [r"Template test-tpl created in", r"global[\/|\\]\.prich[\/|\\]templates[\/|\\]test-tpl[\/|\\]test-tpl"], "expected_exit_code": 0, "check_file": ""}, {"args": ["test-tpl", "-g"], - "expected_exception_message": ["Error: Template test-tpl already exists."], + "expected_exception_message": [r"Error: Template test-tpl already exists\."], "expected_exit_code": 1, "check_file": ""}, ] @@ -261,7 +263,7 @@ def test_create_template(mock_paths, case): result = runner.invoke(create_template, iteration.get("args")) if iteration.get("expected_exception_messages") is not None: for message in iteration.get("expected_exception_messages"): - assert message in result.output.replace("\n", "") + assert re.search(message, result.output.replace("\n", "")), f"'{message}' not found in " + result.output.replace("\n", "") if iteration.get("expected_exit_code") is not None: assert result.exit_code == iteration.get("expected_exit_code") diff --git a/tests/test_validate.py b/tests/test_validate.py index 9225aef..6147f12 100644 --- a/tests/test_validate.py +++ b/tests/test_validate.py @@ -1,4 +1,5 @@ import os +import re from pathlib import Path import pytest from click.testing import CliRunner @@ -20,24 +21,24 @@ {"id": "file_with_template_id_param", "args": ["--file", "test.yaml", "--id", "test-template"], "expected_output": "When YAML file is selected it doesn't combine with local, global, or id options"}, {"id": "file_not_present", "args": ["--file", "test.yaml"], - "expected_output": ["Failed to find", "test.yaml template file."]}, + "expected_output": ["Failed to find", r"test\.yaml template file\."]}, {"id": "path_as_file", "args": ["--file", "."], - "expected_output": ["Failed to find", " template file."]}, + "expected_output": ["Failed to find", r" template file\."]}, {"id": "wrong_template_from_file", "add_wrong_template": True, "args": ["--file", "./.prich/templates/tpl-local-wrong/tpl-local-wrong.yaml"], "expected_output": "tpl-local-wrong.yaml: is not valid"}, {"id": "no_templates_found", "args": [], - "expected_output": "No Templates found."}, + "expected_output": r"No Templates found\."}, {"id": "no_template_if_found", "args": ["--id", "test-template"], "expected_output": "Failed to find template with id: test-template"}, {"id": "local_template_id", "add_template": True, "args": ["--id", "template-local"], - "expected_output": "- template-local (local)"}, + "expected_output": r"- template-local \(local\)"}, {"id": "local_template_id_w_local", "add_template": True, "args": ["--id", "template-local", "-l"], - "expected_output": "- template-local (local)"}, + "expected_output": r"- template-local \(local\)"}, {"id": "local_template_id_w_global_not_found", "add_template": True, "args": ["--id", "template-local", "-g"], "expected_output": "Failed to find template with id: template-local"}, {"id": "global_template_id", "add_template": True, "args": ["--id", "template-global"], - "expected_output": "- template-global (global)"}, + "expected_output": r"- template-global \(global\)"}, {"id": "global_template_id_w_local_not_found", "add_template": True, "args": ["--id", "template-global", "-l"], "expected_output": "Failed to find template with id: template-global"}, {"id": "local_template_wrong", "add_wrong_template": True, "args": [], @@ -56,12 +57,12 @@ } ], "expected_output": [ - "template-global.yaml: is not valid", - "Failed to find call command file ~/.prich/templates/template-global/scripts/echo1", - "Failed to find call command file ~/.prich/templates/template-global/scripts/echo2", + r"template-global\.yaml: is not valid", + r"Failed to find call command file ~[\/|\\].prich[\/|\\]templates[\/|\\]template-global[\/|\\]scripts[\/|\\]echo1", + r"Failed to find call command file ~[\/|\\].prich[\/|\\]templates[\/|\\]template-global[\/|\\]scripts[\/|\\]echo2", "template-local.yaml: is not valid", - "Failed to find call command file ./.prich/templates/template-local/scripts/echo1", - "Failed to find call command file ./.prich/templates/template-local/scripts/echo2", + r"Failed to find call command file \.[\/|\\].prich[\/|\\]templates[\/|\\]template-local[\/|\\]scripts[\/|\\]echo1", + r"Failed to find call command file \.[\/|\\].prich[\/|\\]templates[\/|\\]template-local[\/|\\]scripts[\/|\\]echo2", ]}, {"id": "local_template_not_found_python", "add_template": True, "args": [], "override_venv": "isolated", @@ -78,73 +79,73 @@ } ], "expected_output": [ - "template-global.yaml: is not valid", - "Failed to find isolated venv at ~/.prich/templates/template-global/scripts", - "Failed to find call python file ~/.prich/templates/template-global/scripts/echo1.py", - "Failed to find call python file ~/.prich/templates/template-global/scripts/echo2.py", - "template-local.yaml: is not valid", - "Failed to find isolated venv at ./.prich/templates/template-local/scripts", - "Failed to find call python file ./.prich/templates/template-local/scripts/echo1.py", - "Failed to find call python file ./.prich/templates/template-local/scripts/echo2.py", + r"template-global\.yaml: is not valid", + r"Failed to find isolated venv at ~[\/|\\]\.prich[\/|\\]templates[\/|\\]template-global[\/|\\]scripts", + r"Failed to find call python file ~[\/|\\].prich[\/|\\]templates[\/|\\]template-global[\/|\\]scripts[\/|\\]echo1.py", + r"Failed to find call python file ~[\/|\\].prich[\/|\\]templates[\/|\\]template-global[\/|\\]scripts[\/|\\]echo2.py", + r"template-local\.yaml: is not valid", + r"Failed to find isolated venv at \.[\/|\\].prich[\/|\\]templates[\/|\\]template-local[\/|\\]scripts", + r"Failed to find call python file \.[\/|\\].prich[\/|\\]templates[\/|\\]template-local[\/|\\]scripts[\/|\\]echo1.py", + r"Failed to find call python file \.[\/|\\].prich[\/|\\]templates[\/|\\]template-local[\/|\\]scripts[\/|\\]echo2.py", ]}, # wrong templates from resources {"id": "file_empty", "args": ["--file", "{resources}/empty.yaml"], - "expected_output": ["empty.yaml: is not valid", "check if file or contents are correct"]}, + "expected_output": [r"empty\.yaml: is not valid", "check if file or contents are correct"]}, {"id": "file_no_schema_version", "args": ["--file", "{resources}/no_schema_version.yaml"], - "expected_output": ["no_schema_version.yaml", "is not valid", "Failed to load template", + "expected_output": [r"no_schema_version\.yaml", "is not valid", "Failed to load template", "Unsupported template schema version NOT SET"]}, {"id": "file_not_supported_schema", "args": ["--file", "{resources}/not_supported_schema.yaml"], - "expected_output": ["not_supported_schema.yaml", "is not valid", "Failed to load template", - "Unsupported template schema version 0.1"]}, + "expected_output": [r"not_supported_schema\.yaml", "is not valid", "Failed to load template", + r"Unsupported template schema version 0\.1"]}, {"id": "file_no_id", "args": ["--file", "{resources}/no_id.yaml"], - "expected_output": ["no_id.yaml", "is not valid (1 issue)", "Failed to load template", - "Missing required field at 'id'", "+id: ..."]}, + "expected_output": [r"no_id\.yaml", r"is not valid \(1 issue\)", "Failed to load template", + "Missing required field at 'id'", r"\+id: \.\.\."]}, {"id": "file_no_name", "args": ["--file", "{resources}/no_name.yaml"], - "expected_output": ["no_name.yaml", "is not valid", "Failed to load template", - "Missing required field at 'name'", "+name: ..."]}, + "expected_output": [r"no_name\.yaml", "is not valid", "Failed to load template", + "Missing required field at 'name'", r"\+name: \.\.\."]}, {"id": "file_no_steps", "args": ["--file", "{resources}/no_steps.yaml"], - "expected_output": ["no_steps.yaml", "is not valid", "Failed to load template", + "expected_output": [r"no_steps\.yaml", "is not valid", "Failed to load template", "Field value should be a valid list at 'steps'", "steps: null", "See Steps documentation:", "https://oleks-dev.github.io/prich/reference/template/steps/"]}, {"id": "file_wrong_steps", "args": ["--file", "{resources}/wrong_steps.yaml"], - "expected_output": ["wrong_steps.yaml", "is not valid (3 issues)", "Failed to load template", - "1. Field value 'provider' found using 'type' does not match any of the expected values: 'python', 'command', 'llm', 'render' at 'steps[1]': --- - name: Ask to generate 1st", - "2. Missing required field at 'steps[2].name': --- step_name: Ask to generate 2nd", - "3. Unrecognized field at 'steps[2].step_name': --- step_name: Ask to generate 2nd", + "expected_output": [r"wrong_steps\.yaml", r"is not valid \(3 issues\)", "Failed to load template", + r"1\. Field value 'provider' found using 'type' does not match any of the expected values: 'python', 'command', 'llm', 'render' at 'steps\[1\]': --- - name: Ask to generate 1st", + r"2\. Missing required field at 'steps\[2\]\.name': --- step_name: Ask to generate 2nd", + r"3\. Unrecognized field at 'steps\[2\].step_name': --- step_name: Ask to generate 2nd", "See Steps documentation:", "https://oleks-dev.github.io/prich/reference/template/steps/"]}, {"id": "file_wrong_variables", "args": ["--file", "{resources}/wrong_variables.yaml"], - "expected_output": ["wrong_variables.yaml", "is not valid (4 issues)", "Failed to load template", - "1. Missing required field at 'variables[1].name': --- var_name: test +name: ...", - "2. Unrecognized field at 'variables[1].var_name': --- var_name: test ... ", - "3. Field value should be 'str',", - "4. Field value should be a valid boolean, unable to interpret input at 'variables[4].required'", + "expected_output": [r"wrong_variables\.yaml", r"is not valid \(4 issues\)", "Failed to load template", + r"1\. Missing required field at 'variables\[1\]\.name': --- var_name: test \+name: \.\.\.", + r"2\. Unrecognized field at 'variables\[1\]\.var_name': --- var_name: test \.\.\. ", + r"3\. Field value should be 'str',", + r"4\. Field value should be a valid boolean, unable to interpret input at 'variables\[4\]\.required'", "See Variables documentation https://oleks-dev.github.io/prich/reference/template/variables/"]}, {"id": "file_no_python_not_found_isolated_venv", "args": ["--file", "{resources}/no_venv.yaml"], - "expected_output": ["no_venv.yaml", "is not valid (1 issue)", - "1. Failed to find isolated venv at", + "expected_output": [r"no_venv\.yaml", r"is not valid \(1 issue\)", + r"1\. Failed to find isolated venv at", "There are no steps with type 'python' found", - "Install it by running 'prich venv-install test-template'." + r"Install it by running 'prich venv-install test-template'\." ]}, {"id": "file_no_python_not_found_shared_venv", "args": ["--file", "{resources}/no_shared_venv.yaml"], - "expected_output": ["no_shared_venv.yaml", "is not valid (1 issue)", - "1. Failed to find shared venv at", + "expected_output": [r"no_shared_venv\.yaml", r"is not valid \(1 issue\)", + r"1\. Failed to find shared venv at", "There are no steps with type 'python' found", ]}, {"id": "file_yaml_error", "args": ["--file", "{resources}/yaml_error.yaml"], - "expected_output": ["yaml_error.yaml", "is not valid (1 issue)", - "Failed to load template:", "1. while scanning a simple key", + "expected_output": [r"yaml_error\.yaml", r"is not valid \(1 issue\)", + "Failed to load template:", r"1\. while scanning a simple key", "could not find expected ':'", ]}, ] @@ -201,4 +202,4 @@ def test_validate_template(mock_paths, monkeypatch, case, template, basic_config if isinstance(case.get("expected_output"), str): case["expected_output"] = [case.get("expected_output")] for expected_output in case.get("expected_output"): - assert expected_output in result.output.replace("\n", " ").replace(" ", " ") + assert re.search(expected_output, result.output.replace("\n", " ").replace(" ", " "))