Skip to content

Commit

Permalink
first push for sema feature
Browse files Browse the repository at this point in the history
tackles issue #111 , #112
  • Loading branch information
cedricdcc committed Oct 9, 2024
1 parent b1edc54 commit bcf933e
Show file tree
Hide file tree
Showing 25 changed files with 675 additions and 3 deletions.
3 changes: 3 additions & 0 deletions results.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
success,error,message
True,False,Example test passed.
True,False,Example test passed.
1 change: 1 addition & 0 deletions results.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<html><head><title>Test Results</title></head><body><table border='1'><tr><th>Success</th><th>Error</th><th>Message</th></tr><tr><td>True</td><td>False</td><td>Example test passed.</td></tr><tr><td>True</td><td>False</td><td>Example test passed.</td></tr></table></body></html>
12 changes: 12 additions & 0 deletions results.yml
Original file line number Diff line number Diff line change
@@ -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
70 changes: 70 additions & 0 deletions sema/check/README.md
Original file line number Diff line number Diff line change
@@ -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
```
17 changes: 17 additions & 0 deletions sema/check/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
]
72 changes: 72 additions & 0 deletions sema/check/__main__.py
Original file line number Diff line number Diff line change
@@ -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()
61 changes: 61 additions & 0 deletions sema/check/check.py
Original file line number Diff line number Diff line change
@@ -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
60 changes: 60 additions & 0 deletions sema/check/service.py
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions sema/check/sinks/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
]
23 changes: 23 additions & 0 deletions sema/check/sinks/csv_sink.py
Original file line number Diff line number Diff line change
@@ -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,
}
)
19 changes: 19 additions & 0 deletions sema/check/sinks/html_sink.py
Original file line number Diff line number Diff line change
@@ -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 = "<html><head><title>Test Results</title></head><body>"
html_content += "<table border='1'><tr><th>Success</th><th>Error</th><th>Message</th></tr>"
for result in results:
html_content += f"<tr><td>{result.success}</td><td>{result.error}</td><td>{result.message}</td></tr>"
html_content += "</table></body></html>"

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
10 changes: 10 additions & 0 deletions sema/check/sinks/yml_sink.py
Original file line number Diff line number Diff line change
@@ -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)
Loading

0 comments on commit bcf933e

Please sign in to comment.