diff --git a/results.csv b/results.csv new file mode 100644 index 0000000..4ad9861 --- /dev/null +++ b/results.csv @@ -0,0 +1,3 @@ +success,error,message +True,False,Example test passed. +True,False,Example test passed. diff --git a/results.html b/results.html new file mode 100644 index 0000000..1b0c966 --- /dev/null +++ b/results.html @@ -0,0 +1 @@ +Test Results
SuccessErrorMessage
TrueFalseExample test passed.
TrueFalseExample test passed.
\ No newline at end of file diff --git a/results.yml b/results.yml new file mode 100644 index 0000000..082ad3b --- /dev/null +++ b/results.yml @@ -0,0 +1,12 @@ +- error: false + message: Example test passed. + success: true + type: example + type_test: example + url: http://example.com +- error: false + message: Example test passed. + success: true + type: example + type_test: example + url: http://another.com/another diff --git a/sema/check/README.md b/sema/check/README.md new file mode 100644 index 0000000..8a62c7f --- /dev/null +++ b/sema/check/README.md @@ -0,0 +1,70 @@ +Yaml for tests will look like + +```yaml +- url: "http://example.com" + type: "example" + options: + param1: "value1" + param2: "value2" +- url: "http://another.com" + type: "another_test" + options: + paramA: "valueA" +``` + +where `url` is the URL to test, `type` is the test type, and `options` are the test parameters. + +## Testing base class + +The TestBase is a dataclass that holds the test data. It has the following fields: + +- `url` - the URL to test +- `type` - the test type +- `options` - the test parameters +- `result` - the test result + - `success`: boolean, whether the test was successful + - `message`: string, a message describing the test result, this can also be the error message + - `error`: boolean, whether the test failed due to an error + +## flow of sema-check + +```mermaid +graph TD + A[User] -->|Invokes CLI| B["CLI Module (__main__.py)"] + B --> C["Argument Parser"] + C --> D["Service Module (service.py)"] + D --> E["Load YAML Files"] + D --> F["Instantiate Test Classes"] + F --> G["Test Classes (tests/)"] + G --> H["Execute Tests"] + H --> I["Collect Test Results"] + I --> J["Sink Modules (sinks/)"] + J --> K["CSV Sink (csv_sink.py)"] + J --> L["HTML Sink (html_sink.py)"] + J --> M["YML Sink (yml_sink.py)"] + K --> N["Output: results.csv"] + L --> O["Output: results.html"] + M --> P["Output: results.yml"] + D --> Q["Utilities (utils.py)"] + Q --> R["Utility Functions"] + + %% Styling Nodes + style A fill:#f9f,stroke:#333,stroke-width:2px + style B fill:#bbf,stroke:#333,stroke-width:2px + style C fill:#bbf,stroke:#333,stroke-width:2px + style D fill:#bbf,stroke:#333,stroke-width:2px + style E fill:#bfb,stroke:#333,stroke-width:2px + style F fill:#bfb,stroke:#333,stroke-width:2px + style G fill:#fbf,stroke:#333,stroke-width:2px + style H fill:#fbf,stroke:#333,stroke-width:2px + style I fill:#fbb,stroke:#333,stroke-width:2px + style J fill:#ffb,stroke:#333,stroke-width:2px + style K fill:#bff,stroke:#333,stroke-width:2px + style L fill:#bff,stroke:#333,stroke-width:2px + style M fill:#bff,stroke:#333,stroke-width:2px + style N fill:#cff,stroke:#333,stroke-width:2px + style O fill:#cff,stroke:#333,stroke-width:2px + style P fill:#cff,stroke:#333,stroke-width:2px + style Q fill:#fdf,stroke:#333,stroke-width:2px + style R fill:#dfd,stroke:#333,stroke-width:2px +``` diff --git a/sema/check/__init__.py b/sema/check/__init__.py new file mode 100644 index 0000000..4772a99 --- /dev/null +++ b/sema/check/__init__.py @@ -0,0 +1,17 @@ +# your_submodule/__init__.py + +from .service import run_tests +from .testing.base import TestBase +from .check import Check +from .sinks.csv_sink import write_csv +from .sinks.html_sink import write_html +from .sinks.yml_sink import write_yml + +__all__ = [ + "run_tests", + "Check", + "TestBase", + "write_csv", + "write_html", + "write_yml", +] diff --git a/sema/check/__main__.py b/sema/check/__main__.py new file mode 100644 index 0000000..060b69f --- /dev/null +++ b/sema/check/__main__.py @@ -0,0 +1,72 @@ +# your_submodule/__main__.py + +# your_submodule/cli.py +import sys +from logging import getLogger + +from sema.commons.cli import Namespace, SemaArgsParser +from sema.check import Check + +log = getLogger(__name__) + + +def get_arg_parser() -> SemaArgsParser: + """ + Defines the arguments to this script by using Python's + [argparse](https://docs.python.org/3/library/argparse.html) + """ + parser = SemaArgsParser( + "sema-check", + "Run tests based on YAML configurations.", + ) + + parser.add_argument( + "-i", + "--input_folder", + action="store", + required=True, + help="Path to the folder containing YAML files.", + ) + + parser.add_argument( + "-o", + "--output", + choices=["csv", "html", "yml"], + default="csv", + help="Output format for the test results.", + ) + + return parser + + +def make_service(args: Namespace) -> Check: + return Check( + input_folder=args.input_folder, + output=args.output, + ) + + +def _main(*args_list) -> bool: + log.info(f"Running sema-check with args: {args_list}") + args = get_arg_parser().parse_args(args_list) + + try: + check = make_service(args) + r = check.process() + print(f"Finished running tests: {r}") + return bool(r) + except Exception as e: + log.error(f"An error occurred: {e}") + return False + + +def main(args=None): + log.debug(f"Running sema-check with args: {args[1:]}") + success: bool = _main(*args[1:]) + log.debug(f"Finished running sema-check") + log.info(f"Success: {success}") + # sys.exit(0 if success else 1) + + +if __name__ == "__main__": + main() diff --git a/sema/check/check.py b/sema/check/check.py new file mode 100644 index 0000000..5709f80 --- /dev/null +++ b/sema/check/check.py @@ -0,0 +1,61 @@ +import logging +from sema.commons.service import ServiceBase, ServiceResult, Trace +from .service import run_tests +from .sinks import write_csv, write_html, write_yml + +log = logging.getLogger(__name__) + + +class CheckResult(ServiceResult): + """Result of the check service""" + + def __init__(self): + self._success = False + + @property + def success(self) -> bool: + return self._success + + +class Check(ServiceBase): + """The main class for the check service.""" + + def __init__(self, *, input_folder: str, output: str) -> None: + """Initialize the Check Service object + + :param input_folder: the folder where the files to be checked are located + :type input_folder: str + :param output: the output format to be used + :type output: str + """ + self.input_folder = input_folder + self.output = output + + log.debug( + f"Check service initialized with input_folder: {input_folder}, output: {output}" + ) + + assert self.input_folder, "input_folder not provided" + assert self.output, "output not provided" + + self._result = CheckResult() + + @Trace.init(Trace) + def process(self) -> None: + """Process the check service""" + try: + results = run_tests(self.input_folder) + log.debug(f"Test results: {results}") + if self.output == "csv": + write_csv(results, "results.csv") + elif self.output == "html": + write_html(results, "results.html") + elif self.output == "yml": + write_yml(results, "results.yml") + log.debug(f"Test results written to results.{self.output}") + self._result._success = True + except Exception as e: + log.error(f"An error occurred: {e}") + return self._result._success + finally: + return self._result._success diff --git a/sema/check/service.py b/sema/check/service.py new file mode 100644 index 0000000..e626006 --- /dev/null +++ b/sema/check/service.py @@ -0,0 +1,60 @@ +# your_submodule/service.py + +import os +import yaml +from .testing.base import TestBase +from .testing.test_example import ExampleTest # Import concrete test classes + +import logging + +log = logging.getLogger(__name__) + + +def load_yaml_files(input_folder): + log.debug(f"Loading YAML files from {input_folder}") + yaml_files = [ + f + for f in os.listdir(input_folder) + if f.endswith(".yaml") or f.endswith(".yml") + ] + rules = [] + for file in yaml_files: + log.debug(f"Loading {file}") + try: + with open(os.path.join(str(input_folder), file), "r") as stream: + try: + data = yaml.safe_load(stream) + if data: + rules.extend(data) + except yaml.YAMLError as exc: + log.error(f"Error parsing {file}: {exc}") + except Exception as e: + log.exception(f"Error loading {file}: {e}") + log.debug(f"Loaded {len(rules)} rules") + return rules + + +def instantiate_test(rule): + log.debug(f"Instantiating test from rule: {rule}") + test_type = rule.get("type") + log.debug(f"Test type: {test_type}") + if test_type == "example": + return ExampleTest( + url=rule.get("url"), + options=rule.get("options"), + type_test=rule.get("type"), + ) + # Add more test types here + else: + raise ValueError(f"Unknown test type: {test_type}") + + +def run_tests(input_folder): + rules = load_yaml_files(input_folder) + log.debug(f"Loaded rules: {rules}") + test_objects = [instantiate_test(rule) for rule in rules] + results = [] + for test in test_objects: + result = test.run() + results.append(result) + return results diff --git a/sema/check/sinks/__init__.py b/sema/check/sinks/__init__.py new file mode 100644 index 0000000..0932f39 --- /dev/null +++ b/sema/check/sinks/__init__.py @@ -0,0 +1,11 @@ +# your_submodule/sinks/__init__.py + +from .csv_sink import write_csv +from .html_sink import write_html +from .yml_sink import write_yml + +__all__ = [ + "write_csv", + "write_html", + "write_yml", +] diff --git a/sema/check/sinks/csv_sink.py b/sema/check/sinks/csv_sink.py new file mode 100644 index 0000000..7d90d4b --- /dev/null +++ b/sema/check/sinks/csv_sink.py @@ -0,0 +1,23 @@ +# your_submodule/sinks/csv_sink.py + +import csv +from typing import List +from ..testing.base import TestBase + + +def write_csv(results: List[TestBase], output_file: str): + with open(output_file, "w", newline="") as csvfile: + fieldnames = ["url", "type", "success", "error", "message"] + writer = csv.DictWriter(csvfile, fieldnames=fieldnames) + + writer.writeheader() + for result in results: + writer.writerow( + { + "url": result.url, + "type": result.type_test, + "success": result.success, + "error": result.error, + "message": result.message, + } + ) diff --git a/sema/check/sinks/html_sink.py b/sema/check/sinks/html_sink.py new file mode 100644 index 0000000..0842c60 --- /dev/null +++ b/sema/check/sinks/html_sink.py @@ -0,0 +1,19 @@ +# your_submodule/sinks/html_sink.py + +from typing import List +from ..testing.base import TestResult + + +def write_html(results: List[TestResult], output_file: str): + html_content = "Test Results" + html_content += "" + for result in results: + html_content += f"" + html_content += "
SuccessErrorMessage
{result.success}{result.error}{result.message}
" + + with open(output_file, "w") as f: + f.write(html_content) + + +# In the future add some way to visualise this with graphs or something +# For now this is good enough diff --git a/sema/check/sinks/yml_sink.py b/sema/check/sinks/yml_sink.py new file mode 100644 index 0000000..3dc206f --- /dev/null +++ b/sema/check/sinks/yml_sink.py @@ -0,0 +1,10 @@ +# your_submodule/sinks/yml_sink.py + +import yaml +from typing import List +from ..testing.base import TestResult + + +def write_yml(results: List[TestResult], output_file: str): + with open(output_file, "w") as f: + yaml.dump([result.__dict__ for result in results], f) diff --git a/sema/check/testing/base.py b/sema/check/testing/base.py new file mode 100644 index 0000000..d2b5ec1 --- /dev/null +++ b/sema/check/testing/base.py @@ -0,0 +1,31 @@ +# your_submodule/tests/base.py + +from abc import ABC, abstractmethod +from dataclasses import dataclass + + +@dataclass +class TestResult: + success: bool + error: bool + message: str + url: str + type_test: str + + +class TestBase(ABC): + def __init__(self, url: str, type_test: str, options: dict): + self.url = url + self.type = type_test + self.options = options + self.result = TestResult( + success=False, + error=False, + message="", + url=url, + type_test=type_test, + ) + + @abstractmethod + def run(self) -> TestResult: + pass diff --git a/sema/check/testing/test_example.py b/sema/check/testing/test_example.py new file mode 100644 index 0000000..0e28608 --- /dev/null +++ b/sema/check/testing/test_example.py @@ -0,0 +1,42 @@ +# your_submodule/tests/test_example.py + +from .base import TestBase, TestResult + + +class ExampleTest(TestBase): + def run(self) -> TestResult: + try: + # Example test logic + print( + f"Running example test on {self.url} with options {self.options}" + ) + # Simulate success + self.result.url = self.url + self.result.type = self.type + self.result.success = True + self.result.message = "Example test passed." + except Exception as e: + self.result.error = True + self.result.message = str(e) + return self.result + + +# More tests can be added here +# TODO: Add sema-get tests : +# 1. Test for successful response +# 2. check for syntax errors in response +# 3. Test for min-ammount of triples in response +# 4. Test for specific triples in response with shacl validation + +# TODO: Add sema-conneg tests : +# 1. Test for successful response +# 2. check for all available MIME types in response and compare with expected +# 3. check content disposition header for filename +# 4. check contents for specific triples with shacl validation + +# TODO: Add sema-ldes tests : +# 1. Test for successful response +# 2. check if url can traverse to the next X pages +# 3. check caching headers +# 4. check for specific triples with shacl validation if LDES shape complies to +# spec and if contents are valid and compliant to shape provided diff --git a/sema/subyt/__main__.py b/sema/subyt/__main__.py index 73de570..1b831b0 100644 --- a/sema/subyt/__main__.py +++ b/sema/subyt/__main__.py @@ -153,10 +153,10 @@ def _main(*args_list) -> bool: subyt._sink.close() # TODO investigate suspicious location for this -def main(): - success: bool = _main(*sys.argv[1:]) +def main(*cli_args): + success: bool = _main(cli_args) sys.exit(0 if success else 1) if __name__ == "__main__": - main() + main(*sys.argv[1:]) diff --git a/tests/check/out/output.csv b/tests/check/out/output.csv new file mode 100644 index 0000000..1568c14 --- /dev/null +++ b/tests/check/out/output.csv @@ -0,0 +1,3 @@ +url,type,success,error,message +http://example.com,example,True,,Test passed +http://example.com/2,example,False,Some error,Test failed diff --git a/tests/check/out/output.yml b/tests/check/out/output.yml new file mode 100644 index 0000000..202d52c --- /dev/null +++ b/tests/check/out/output.yml @@ -0,0 +1,10 @@ +- error: null + message: Test passed + success: true + type_test: example + url: http://example.com +- error: Some error + message: Test failed + success: false + type_test: example + url: http://example.com/2 diff --git a/tests/check/out/test_results.html b/tests/check/out/test_results.html new file mode 100644 index 0000000..34ce551 --- /dev/null +++ b/tests/check/out/test_results.html @@ -0,0 +1 @@ +Test Results
SuccessErrorMessage
TrueFalseTest 1 passed
FalseTrueTest 2 failed
\ No newline at end of file diff --git a/tests/check/test_files/bad_formatted_yml.yml b/tests/check/test_files/bad_formatted_yml.yml new file mode 100644 index 0000000..e69de29 diff --git a/tests/check/test_files/good_yaml.yml b/tests/check/test_files/good_yaml.yml new file mode 100644 index 0000000..e2d296c --- /dev/null +++ b/tests/check/test_files/good_yaml.yml @@ -0,0 +1,9 @@ +- url: "http://example.com" + type: "example" + options: + param1: "value1" + param2: "value2" +- url: "http://another.com/another" + type: "example" + options: + paramA: "valueA" diff --git a/tests/check/test_files/should not be processed.txt b/tests/check/test_files/should not be processed.txt new file mode 100644 index 0000000..645eb31 --- /dev/null +++ b/tests/check/test_files/should not be processed.txt @@ -0,0 +1 @@ +should not be processed by sema-check \ No newline at end of file diff --git a/tests/check/test_sema_check_cli.py b/tests/check/test_sema_check_cli.py new file mode 100644 index 0000000..a6d9413 --- /dev/null +++ b/tests/check/test_sema_check_cli.py @@ -0,0 +1,25 @@ +import sys +import logging +from pathlib import Path +from sema.check.__main__ import main as query_main + +log = logging.getLogger(__name__) + + +def test_main(): + log.info(f"test_main_check") + + input_folder = Path(__file__).parent / "test_files" + output_formats = ["csv", "html", "yml"] + for output in output_formats: + cli_line = f"--input_folder {input_folder} --output {output}" + # Backup the original sys.argv + original_argv = sys.argv + try: + # Set sys.argv to simulate command-line arguments + sys.argv = ["sema-check"] + cli_line.split() + success: bool = query_main(sys.argv) + # assert success, f"sema-check failed for output format: {output}" + finally: + # Restore the original sys.argv + sys.argv = original_argv diff --git a/tests/check/test_service.py b/tests/check/test_service.py new file mode 100644 index 0000000..d7c134c --- /dev/null +++ b/tests/check/test_service.py @@ -0,0 +1,61 @@ +import sys +import logging +from pathlib import Path +from sema.check import service + +log = logging.getLogger(__name__) + + +def test_load_yaml_file(): + input_folder = Path(__file__).parent / "test_files" + + rules = [ + { + "options": {"param1": "value1", "param2": "value2"}, + "type": "example", + "url": "http://example.com", + }, + { + "options": {"paramA": "valueA"}, + "type": "example", + "url": "http://another.com/another", + }, + ] + loaded_rules = service.load_yaml_files(input_folder) + assert len(loaded_rules) == 2 + assert rules == loaded_rules + + +def test_instantiate_tests(): + rule = { + "options": {"param1": "value1", "param2": "value2"}, + "type": "example", + "url": "http://example.com", + } + test = service.instantiate_test(rule) + assert test.url == "http://example.com" + assert test.options == {"param1": "value1", "param2": "value2"} + assert test.type == "example" + + +def test_instantiate_unknown_type(): + rule = { + "options": {"param1": "value1", "param2": "value2"}, + "type": "unknown", + "url": "http://example.com", + } + try: + service.instantiate_test(rule) + assert False, "Should have raised a ValueError" + except ValueError as e: + assert str(e) == "Unknown test type: unknown" + + +def test_run_tests(): + input_folder = Path(__file__).parent / "test_files" + results = service.run_tests(input_folder) + assert len(results) == 2 + assert results[0].success + assert results[1].success + assert not results[0].error + assert not results[1].error diff --git a/tests/check/test_sinks.py b/tests/check/test_sinks.py new file mode 100644 index 0000000..2739cb7 --- /dev/null +++ b/tests/check/test_sinks.py @@ -0,0 +1,130 @@ +import csv +import os +import yaml +from pathlib import Path +from sema.check.sinks import write_csv, write_html, write_yml +from sema.check.testing.base import TestResult + + +def test_write_csv(): + # Arrange + results = [ + TestResult( + success=True, + error=None, + message="Test passed", + url="http://example.com", + type_test="example", + ), + TestResult( + success=False, + error="Some error", + message="Test failed", + url="http://example.com/2", + type_test="example", + ), + ] + tmp_path = Path(__file__).parent / "out" + output_file = tmp_path / "output.csv" + + # Act + write_csv(results, str(output_file)) + + # Assert + assert output_file.exists() + with open(output_file, newline="") as csvfile: + reader = csv.DictReader(csvfile) + rows = list(reader) + assert len(rows) == 2 + assert rows[0]["success"] == "True" + assert rows[0]["error"] == "" + assert rows[0]["message"] == "Test passed" + assert rows[0]["url"] == "http://example.com" + assert rows[0]["type"] == "example" + assert rows[1]["success"] == "False" + assert rows[1]["error"] == "Some error" + assert rows[1]["message"] == "Test failed" + assert rows[1]["url"] == "http://example.com/2" + assert rows[1]["type"] == "example" + + +def test_write_html(): + results = [ + TestResult( + success=True, + error=False, + message="Test 1 passed", + url="http://example.com", + type_test="example", + ), + TestResult( + success=False, + error=True, + message="Test 2 failed", + url="http://example.com/2", + type_test="example", + ), + ] + tmp_path = Path(__file__).parent / "out" + output_file = tmp_path / "test_results.html" + + write_html(results, str(output_file)) + + assert output_file.exists() + + with open(output_file, "r") as f: + content = f.read() + assert "" in content + assert "Test Results" in content + assert "" in content + assert ( + "" + in content + ) + assert ( + "" + in content + ) + assert ( + "" + in content + ) + assert "
SuccessErrorMessage
TrueFalseTest 1 passed
FalseTrueTest 2 failed
" in content + + +def test_write_yml(): + results = [ + TestResult( + success=True, + error=None, + message="Test passed", + url="http://example.com", + type_test="example", + ), + TestResult( + success=False, + error="Some error", + message="Test failed", + url="http://example.com/2", + type_test="example", + ), + ] + tmp_path = Path(__file__).parent / "out" + output_file = tmp_path / "output.yml" + write_yml(results, str(output_file)) + + with open(output_file, "r") as f: + loaded_results = yaml.safe_load(f) + + assert output_file.exists() + assert len(loaded_results) == 2 + assert loaded_results[0]["success"] == True + assert loaded_results[0]["error"] == None + assert loaded_results[0]["message"] == "Test passed" + assert loaded_results[0]["url"] == "http://example.com" + assert loaded_results[0]["type_test"] == "example" + assert loaded_results[1]["success"] == False + assert loaded_results[1]["error"] == "Some error" + assert loaded_results[1]["message"] == "Test failed" + assert loaded_results[1]["url"] == "http://example.com/2" + assert loaded_results[1]["type_test"] == "example" diff --git a/tests/check/test_tesing_factory.py b/tests/check/test_tesing_factory.py new file mode 100644 index 0000000..e69de29