diff --git a/.github/workflows/update-schema.yaml b/.github/workflows/update-schema.yaml new file mode 100644 index 0000000..770a263 --- /dev/null +++ b/.github/workflows/update-schema.yaml @@ -0,0 +1,39 @@ +--- +name: Update Schema + +"on": + push: + branches: + - main + workflow_dispatch: + inputs: {} + +jobs: + generate-schema: + name: Generate and Upload Schema + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.12" + + - name: Install + run: python -m pip install retasc + + - name: Generate schema + run: retasc generate-schema schema.yaml + + - name: Prepare deployment directory + run: | + mkdir -p public + cp schema.yaml public/ + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: public + publish_branch: gh-pages diff --git a/src/retasc/__main__.py b/src/retasc/__main__.py index 7d5cc3b..92f217e 100755 --- a/src/retasc/__main__.py +++ b/src/retasc/__main__.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: GPL-3.0-or-later import argparse +import sys from retasc import __doc__ as doc from retasc import __version__ @@ -27,17 +28,22 @@ def parse_args(): generate_parser = subparsers.add_parser("generate-schema", help="Generate a schema") generate_parser.add_argument( - "schema_file", type=str, help="Path to the schema file to generate" + "schema_file", type=str, help="Output schema file to generate" ) return parser.parse_args() -args = parse_args() -init_logging() -init_tracing() +def main(): + args = parse_args() + init_logging() + init_tracing() -if args.command == "validate-rule": - validate_rule(args.rule_file) -elif args.command == "generate-schema": - generate_schema(args.schema_file) + try: + if args.command == "validate-rule": + validate_rule(args.rule_file) + elif args.command == "generate-schema": + generate_schema(args.schema_file) + except Exception as e: + print(f"An error occurred: {e}") + sys.exit(1) diff --git a/src/retasc/validator/schemas/example_rule.yaml b/src/retasc/validator/example_rule.yaml similarity index 97% rename from src/retasc/validator/schemas/example_rule.yaml rename to src/retasc/validator/example_rule.yaml index 69fe778..18973a8 100644 --- a/src/retasc/validator/schemas/example_rule.yaml +++ b/src/retasc/validator/example_rule.yaml @@ -1,3 +1,4 @@ +version: 1 name: "Example Rule" prerequisites: pp_schedule_item_name: "Release Date" diff --git a/src/retasc/validator/generate_schema.py b/src/retasc/validator/generate_schema.py index b86e603..52d832a 100644 --- a/src/retasc/validator/generate_schema.py +++ b/src/retasc/validator/generate_schema.py @@ -1,16 +1,18 @@ -import sys +import logging import yaml from retasc.validator.models import Rule +logger = logging.getLogger(__name__) -def generate_schema(output_path="src/retasc/validator/schemas/rules_schema.yaml"): + +def generate_schema(output_path: str) -> None: schema = Rule.model_json_schema() schema_yaml = yaml.dump(schema, sort_keys=False) - with open(output_path, "w") as schema_file: - schema_file.write(schema_yaml) - - -if __name__ == "__main__": - generate_schema(sys.argv[1]) + try: + with open(output_path, "w") as schema_file: + schema_file.write(schema_yaml) + except Exception as e: + logger.error(f"Error generating schema: {e}") + raise e diff --git a/src/retasc/validator/models.py b/src/retasc/validator/models.py index fb80cf7..c9837f4 100644 --- a/src/retasc/validator/models.py +++ b/src/retasc/validator/models.py @@ -4,7 +4,7 @@ class JiraIssue(BaseModel): """Represents a Jira issue, which can have subtasks.""" - template: str = Field(..., description="The template string for the jira issue.") + template: str = Field(description="The template string for the jira issue.") subtasks: list["JiraIssue"] = Field( default=[], description="The subtasks for the jira issue." ) @@ -13,11 +13,8 @@ class JiraIssue(BaseModel): class Prerequisites(BaseModel): """Defines the prerequisites needed for a rule.""" - pp_schedule_item_name: str = Field( - ..., description="The name of the pp schedule item." - ) + pp_schedule_item_name: str = Field(description="The name of the pp schedule item.") days_before_or_after: int = Field( - ..., description=( "The number of days to adjust the schedule relative to the PP schedule item date. " "A negative value indicates the number of days before the PP schedule item date, " @@ -34,11 +31,7 @@ class Prerequisites(BaseModel): class Rule(BaseModel): """Represents a rule which includes prerequisites and Jira issues.""" - version: int = Field(..., description="The version of the rule.") - name: str = Field(..., description="The name of the rule.") - prerequisites: Prerequisites = Field( - ..., description="The prerequisites for the rule." - ) - jira_issues: list[JiraIssue] = Field( - ..., description="The jira issues for the rule." - ) + version: int = Field(description="The version of the rule.") + name: str = Field(description="The name of the rule.") + prerequisites: Prerequisites = Field(description="The prerequisites for the rule.") + jira_issues: list[JiraIssue] = Field(description="The jira issues for the rule.") diff --git a/src/retasc/validator/schemas/rules_schema.yaml b/src/retasc/validator/schemas/rules_schema.yaml deleted file mode 100644 index 304d775..0000000 --- a/src/retasc/validator/schemas/rules_schema.yaml +++ /dev/null @@ -1,63 +0,0 @@ -$defs: - JiraIssue: - properties: - template: - title: Template - type: string - subtasks: - anyOf: - - items: - $ref: '#/$defs/Subtask' - type: array - - type: 'null' - default: null - title: Subtasks - required: - - template - title: JiraIssue - type: object - Prerequisites: - properties: - pp_schedule_item_name: - title: Pp Schedule Item Name - type: string - days_before_or_after: - title: Days Before Or After - type: integer - dependent_rules: - items: - type: string - title: Dependent Rules - type: array - required: - - pp_schedule_item_name - - days_before_or_after - - dependent_rules - title: Prerequisites - type: object - Subtask: - properties: - template: - title: Template - type: string - required: - - template - title: Subtask - type: object -properties: - name: - title: Name - type: string - prerequisites: - $ref: '#/$defs/Prerequisites' - jira_issues: - items: - $ref: '#/$defs/JiraIssue' - title: Jira Issues - type: array -required: -- name -- prerequisites -- jira_issues -title: Rule -type: object diff --git a/src/retasc/validator/validate_rules.py b/src/retasc/validator/validate_rules.py index a6cf85f..2e9712b 100644 --- a/src/retasc/validator/validate_rules.py +++ b/src/retasc/validator/validate_rules.py @@ -1,23 +1,20 @@ import logging -import sys import yaml from pydantic import ValidationError -from retasc.retasc_logging import init_logging from retasc.validator.models import Rule -init_logging() logger = logging.getLogger(__name__) -def validate_rule(rule_file): +def validate_rule(rule_file: str) -> bool: with open(rule_file) as file: rule_data = yaml.safe_load(file) return validate_rule_dict(rule_data) -def validate_rule_dict(rule_data): +def validate_rule_dict(rule_data: dict) -> bool: try: Rule(**rule_data) logger.info("The rule is valid.") @@ -25,7 +22,3 @@ def validate_rule_dict(rule_data): except ValidationError as err: logger.error(f"The rule is invalid: {err}") return False - - -if __name__ == "__main__": - validate_rule(sys.argv[1]) diff --git a/tests/test_generate_schema.py b/tests/test_generate_schema.py index 92cd064..7bf09f2 100644 --- a/tests/test_generate_schema.py +++ b/tests/test_generate_schema.py @@ -1,7 +1,4 @@ -import sys -from runpy import run_module -from unittest.mock import patch - +import pytest import yaml from retasc.validator.generate_schema import generate_schema @@ -18,14 +15,7 @@ def test_generate_schema(tmp_path): assert schema == expected_schema -def test_generate_schema_script(tmp_path): - schema_file = tmp_path / "rules_schema.yaml" - - with patch.object(sys, "argv", ["generate_schema", str(schema_file)]): - run_module("retasc.validator.generate_schema", run_name="__main__") - - with open(schema_file) as f: - schema = yaml.safe_load(f) - - expected_schema = Rule.model_json_schema() - assert schema == expected_schema +def test_generate_schema_exception(): + with pytest.raises(Exception) as e: + generate_schema(output_path=".") + assert "Is a directory" in str(e.value) diff --git a/tests/test_main.py b/tests/test_main.py index c770120..ad49669 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,20 +1,20 @@ # SPDX-License-Identifier: GPL-3.0-or-later import sys -from runpy import run_module -from unittest.mock import MagicMock, patch +from unittest.mock import patch from pytest import mark, raises +from retasc.__main__ import main + def run_main(*args, code=None): with patch.object(sys, "argv", ["retasc", *args]): if code is None: - run_module("retasc") + main() return with raises(SystemExit) as e: - run_module("retasc") - + main() assert e.value.code == code @@ -43,19 +43,28 @@ def test_dummy_run(capsys): assert stderr == "" -def test_validate_rule_called(tmp_path): - rule_file = "mock" - with patch( - "retasc.validator.validate_rules.validate_rule", MagicMock(return_value=True) - ) as mock_validate_rule: - run_main("validate-rule", str(rule_file), code=None) - mock_validate_rule.assert_called_once_with(str(rule_file)) +@patch("retasc.__main__.validate_rule") +def test_validate_rule(mock_validate_rule, capsys): + run_main("validate-rule", "test_rule.yaml") + mock_validate_rule.assert_called_once_with("test_rule.yaml") + stdout, stderr = capsys.readouterr() + assert stdout == "" + assert stderr == "" + + +@patch("retasc.__main__.generate_schema") +def test_generate_schema(mock_generate_schema, capsys): + run_main("generate-schema", "output_schema.json") + mock_generate_schema.assert_called_once_with("output_schema.json") + stdout, stderr = capsys.readouterr() + assert stdout == "" + assert stderr == "" -def test_generate_schema_called(): - schema_file = "mock" +def test_main_exception(capsys): with patch( - "retasc.validator.generate_schema.generate_schema", MagicMock(return_value=True) - ) as mock_generate_schema: - run_main("generate-schema", str(schema_file), code=None) - mock_generate_schema.assert_called_once_with(str(schema_file)) + "retasc.__main__.validate_rule", side_effect=Exception("Test exception") + ): + run_main("validate-rule", "test_rule.yaml", code=1) + stdout, stderr = capsys.readouterr() + assert "An error occurred: Test exception" in stdout diff --git a/tests/test_validate_rules.py b/tests/test_validate_rules.py index 5ba67ed..c459f25 100644 --- a/tests/test_validate_rules.py +++ b/tests/test_validate_rules.py @@ -1,55 +1,65 @@ -import copy -import subprocess +from copy import deepcopy +import pytest import yaml from retasc.validator.validate_rules import validate_rule, validate_rule_dict -valid_rule_dict = { - "version": 1, - "name": "Example Rule", - "prerequisites": { - "pp_schedule_item_name": "Release Date", - "days_before_or_after": 5, - "dependent_rules": ["Dependent Rule 1", "Dependent Rule 2"], - }, - "jira_issues": [ - { - "template": "major_pre_beta/main.yaml", - "subtasks": [ - {"template": "major_pre_beta/subtasks/add_beta_repos.yaml"}, - {"template": "major_pre_beta/subtasks/notify_team.yaml"}, - ], + +@pytest.fixture +def valid_rule_dict(): + return { + "version": 1, + "name": "Example Rule", + "prerequisites": { + "pp_schedule_item_name": "Release Date", + "days_before_or_after": 5, + "dependent_rules": ["Dependent Rule 1", "Dependent Rule 2"], }, - {"template": "major_pre_beta/secondary.yaml"}, - ], -} + "jira_issues": [ + { + "template": "major_pre_beta/main.yaml", + "subtasks": [ + {"template": "major_pre_beta/subtasks/add_beta_repos.yaml"}, + {"template": "major_pre_beta/subtasks/notify_team.yaml"}, + ], + }, + {"template": "major_pre_beta/secondary.yaml"}, + ], + } -invalid_rule_dict = copy.deepcopy(valid_rule_dict) -invalid_rule_dict["prerequisites"]["days_before_or_after"] = "invalid_type" -valid_rule_yaml = yaml.dump(valid_rule_dict, sort_keys=False) +def test_rule_dict_valid(valid_rule_dict): + assert validate_rule_dict(valid_rule_dict) is True -def test_validate_rule_dict_valid(): - assert validate_rule_dict(valid_rule_dict) is True +def test_rule_valid(tmp_path, valid_rule_dict): + rule_file = tmp_path / "example_rule.yaml" + rule_file.write_text(yaml.dump(valid_rule_dict, sort_keys=False)) + assert validate_rule(str(rule_file)) is True -def test_validate_rule_dict_invalid(): +def test_invalid_incorrect_days_before_or_after_type(valid_rule_dict): + invalid_rule_dict = deepcopy(valid_rule_dict) + invalid_rule_dict["prerequisites"]["days_before_or_after"] = "invalid_type" assert validate_rule_dict(invalid_rule_dict) is False -def test_validate_rule(tmp_path): - rule_file = tmp_path / "example_rule.yaml" - rule_file.write_text(valid_rule_yaml) - assert validate_rule(str(rule_file)) is True +# By default, additional fields are ignored by pydantic +def test_unexpected_fields(valid_rule_dict): + invalid_rule = deepcopy(valid_rule_dict) + invalid_rule["unexpected_field"] = "unexpected_value" + assert validate_rule_dict(invalid_rule) is True -def test_validate_rule_script(tmp_path): - rule_file = tmp_path / "example_rule.yaml" - rule_file.write_text(valid_rule_yaml) - result = subprocess.run( - ["python", "src/retasc/validator/validate_rules.py", str(rule_file)], - check=True, - ) - assert result.returncode == 0 +def test_incorrect_version_types(valid_rule_dict): + invalid_rule = deepcopy(valid_rule_dict) + invalid_rule["version"] = "one" + assert validate_rule_dict(invalid_rule) is False + + +def test_missing_fields(): + invalid_rule = { + "name": "Example Rule", + } + assert validate_rule_dict(invalid_rule) is False