diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index fe8b17f0..2aff6c20 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -4,6 +4,8 @@ on: push: branches: - 'main' + tags-ignore: + - '**' jobs: ci: @@ -46,7 +48,7 @@ jobs: - name: Documentation coverage with interrogate run: | - interrogate -vv bobocep --fail-under 98 + interrogate -vv bobocep --fail-under 100 - name: Upload code coverage to Code Climate uses: paambaati/codeclimate-action@v2.5.3 @@ -87,7 +89,6 @@ jobs: --outdir dist/ - name: Publish to PyPI - # if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') uses: pypa/gh-action-pypi-publish@release/v1 with: password: ${{ secrets.PYPI_TOKEN }} diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index acf90d30..3c6d180a 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -4,6 +4,8 @@ on: push: branches: - 'main' + tags-ignore: + - '**' schedule: # At 06:00 on Monday. - cron: '0 6 * * 1' diff --git a/README.md b/README.md index f5c4ed30..b3df3324 100755 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@ https://github.com/r3w0p/bobocep/actions/workflows/cicd.yml) [![Security](https://github.com/r3w0p/bobocep/actions/workflows/security.yml/badge.svg)]( https://github.com/r3w0p/bobocep/actions/workflows/security.yml) +[![Code Repository](https://img.shields.io/badge/code-github-171515)]( +https://github.com/r3w0p/bobocep/) [![Documentation Status](https://readthedocs.org/projects/bobocep/badge/?version=latest)]( https://bobocep.readthedocs.io/) -[![License Scan](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fr3w0p%2Fbobocep.svg?type=shield)]( -https://app.fossa.com/projects/git%2Bgithub.com%2Fr3w0p%2Fbobocep?ref=badge_shield) [![Donate](https://img.shields.io/badge/donate-ko--fi-red?label=donate)]( https://ko-fi.com/r3w0p)
@@ -24,12 +24,12 @@ https://github.com/r3w0p/bobocep/pulse/) [![Issues](https://img.shields.io/github/issues/r3w0p/bobocep?label=issues)]( https://github.com/r3w0p/bobocep/issues/) -[![Dependencies](https://img.shields.io/librariesio/github/r3w0p/bobocep?label=dependencies)]( -https://libraries.io/pypi/bobocep/) [![Coverage](https://img.shields.io/codeclimate/coverage/r3w0p/bobocep?label=coverage)]( https://codeclimate.com/github/r3w0p/bobocep/) [![Maintainability](https://img.shields.io/codeclimate/maintainability/r3w0p/bobocep?label=maintainability)]( https://codeclimate.com/github/r3w0p/bobocep/) +[![License Scan](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fr3w0p%2Fbobocep.svg?type=shield)]( +https://app.fossa.com/projects/git%2Bgithub.com%2Fr3w0p%2Fbobocep?ref=badge_shield) `BoboCEP` is a [Complex Event Processing](https://en.wikipedia.org/wiki/Complex_event_processing) (CEP) engine @@ -43,7 +43,7 @@ partially-completed complex events across multiple instances of the software. ## License -`BoboCEP` is open source, as per -[The Open Source Definition](https://opensource.org/osd). +`BoboCEP` is open source, as per the +[Open Source Definition](https://opensource.org/osd). The code in this repository can be redistributed and/or modified under the terms of the [MIT License](https://github.com/r3w0p/bobocep/blob/main/LICENSE). diff --git a/SECURITY.md b/SECURITY.md index 99eb61b8..ea84f009 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,6 +1,6 @@ # Security Policy -> _Last updated: 2023-01-04_ +> _Last updated: 2023-09-16_ ## Supported Versions @@ -10,26 +10,27 @@ Below are the versions that are tested for support. ### Python -| Version | Supported | -|:------------:|:---------:| -| `> 3.10` | ✗ | -| `3.9 - 3.10` | ✓ | -| `< 3.9` | ✗ | +| Version | Tested | +|:------------:|:-------:| +| `> 3.10` | ✗ | +| `3.9 - 3.10` | ✓ | +| `< 3.9` | ✗ | ### OS -| Version | Supported | -|:----------------:|:---------:| -| `windows-latest` | ✓ | -| `macos-latest` | ✓ | -| `ubuntu-latest` | ✓ | +| Version | Tested | +|:----------------:|:-------:| +| `windows-latest` | ✓ | +| `macos-latest` | ✓ | +| `ubuntu-latest` | ✓ | The `*-latest` version refers to the `*-latest` -[available environments](https://github.com/actions/virtual-environments#available-environments) +[available environments](https://github.com/actions/runner-images) from GitHub Actions Virtual Environments. ## Reporting a Vulnerability -Please report vulnerabilities using the contact information [here](https://r3w0p.github.io/contact/). +Please report vulnerabilities using the contact information +[here](https://r3w0p.github.io/contact/). diff --git a/bobocep/__init__.py b/bobocep/__init__.py index 1a4541ab..42b92383 100644 --- a/bobocep/__init__.py +++ b/bobocep/__init__.py @@ -8,6 +8,6 @@ __author__ = """r3w0p""" __email__ = "rr33ww00pp@gmail.com" -__version__ = "1.0.1" +__version__ = "1.1.0" -from bobocep.bobocep import BoboError +from bobocep.bobocep import BoboError, BoboJSONable, BoboJSONableError diff --git a/bobocep/bobocep.py b/bobocep/bobocep.py index 57b9f671..de2276ed 100644 --- a/bobocep/bobocep.py +++ b/bobocep/bobocep.py @@ -5,6 +5,7 @@ """ Core classes. """ + from abc import ABC, abstractmethod @@ -22,7 +23,7 @@ class BoboJSONableError(BoboError): class BoboJSONable(ABC): """ - A abstract interface for JSONable types. + An abstract interface for JSONable types. """ @abstractmethod diff --git a/bobocep/cep/action/common/__init__.py b/bobocep/cep/action/common/__init__.py new file mode 100644 index 00000000..f09d45db --- /dev/null +++ b/bobocep/cep/action/common/__init__.py @@ -0,0 +1,7 @@ +# Copyright (c) 2019-2023 r3w0p +# The following code can be redistributed and/or +# modified under the terms of the MIT License. + +""" +Common actions. +""" diff --git a/bobocep/cep/action/common/multi.py b/bobocep/cep/action/common/multi.py new file mode 100644 index 00000000..b77bb4db --- /dev/null +++ b/bobocep/cep/action/common/multi.py @@ -0,0 +1,83 @@ +# Copyright (c) 2019-2023 r3w0p +# The following code can be redistributed and/or +# modified under the terms of the MIT License. + +""" +Multi actions. +""" +from abc import ABC +from typing import Tuple, Any, List + +from bobocep.cep.action import BoboAction, BoboActionError +from bobocep.cep.event import BoboEventComplex + +_ACTIONS_MIN: int = 1 + + +class BoboActionMulti(BoboAction, ABC): + """ + An abstract multi action. + """ + + def __init__(self, name: str, *args, **kwargs): + """ + :param name: The action name. + :param args: Action arguments. + :param kwargs: Action keyword arguments. + """ + super().__init__(name=name, args=args, kwargs=kwargs) + + +class BoboActionMultiSequential(BoboActionMulti): + """ + An abstract sequential multi action. + """ + + def __init__(self, + name: str, + actions: List[BoboAction], + stop_on_fail: bool, + *args, + **kwargs): + """ + :param name: The action name. + :param actions: The list of actions to execute. + :param stop_on_fail: If True, the multi-action stops processing its + action list if its current action fails. If False, it continues + to process its remaining actions. Note: failure of any action in + its list will cause the multi-action's success to be False. + :param args: Action arguments. + :param kwargs: Action keyword arguments. + """ + super().__init__(name=name, args=args, kwargs=kwargs) + + if len(actions) < 1: + raise BoboActionError( + f"multi sequential action {name} " + f"must contain at least {_ACTIONS_MIN} action") + + self._actions: List[BoboAction] = actions + self._stop_on_fail: bool = stop_on_fail + + def execute(self, event: BoboEventComplex) \ + -> Tuple[bool, List[Tuple[bool, Any]]]: + """ + :param event: The complex event that triggered action. + :return: Whether the action execution was successful, and + any additional data. + """ + success = True + data: List[Tuple[bool, Any]] = [] + + for action in self._actions: + output: Tuple[bool, Any] = action.execute(event) + data.append(output) + + # If action was unsuccessful + if not output[0]: + success = False + + if self._stop_on_fail: + break + + return success, data diff --git a/bobocep/cep/action/handler.py b/bobocep/cep/action/handler.py index d7ec5fb7..138bd51b 100644 --- a/bobocep/cep/action/handler.py +++ b/bobocep/cep/action/handler.py @@ -5,8 +5,8 @@ """ Handlers that coordinate the execution of actions. """ + import logging -import multiprocessing from abc import ABC, abstractmethod from queue import Queue from threading import RLock diff --git a/bobocep/cep/engine/engine.py b/bobocep/cep/engine/engine.py index 33effb62..e2931c3a 100644 --- a/bobocep/cep/engine/engine.py +++ b/bobocep/cep/engine/engine.py @@ -160,8 +160,10 @@ def run(self) -> None: def update(self) -> bool: """ - Performs a single update from the receiver, to the decider, to the - producer, and finally to the forwarder. + Updates the receiver, then the decider, then the producer, and + finally to the forwarder. + It updates each task `n` times, depending on how many times were + chosen during engine instantiation. :return: `True` if engine is not set to close; `False` otherwise. """ diff --git a/bobocep/cep/engine/receiver/validator.py b/bobocep/cep/engine/receiver/validator.py index cafa9b9a..039703e7 100644 --- a/bobocep/cep/engine/receiver/validator.py +++ b/bobocep/cep/engine/receiver/validator.py @@ -10,9 +10,19 @@ from json import dumps from typing import Any, List, Tuple +from jsonschema import validate as jsonschema_validate # type: ignore +from jsonschema.exceptions import ValidationError, SchemaError # type: ignore + +from bobocep import BoboError from bobocep.cep.event import BoboEvent +class BoboValidatorError(BoboError): + """ + A validator error. + """ + + class BoboValidator(ABC): """An abstract validator.""" @@ -89,3 +99,37 @@ def is_valid(self, data: Any) -> bool: return any(isinstance(data, t) for t in self._types) else: return any(type(data) == t for t in self._types) + + +class BoboValidatorJSONSchema(BoboValidatorJSONable): + """ + Validates whether the data type is valid with respect to + a given JSON Schema. If the data are a BoboEvent, + then the event's data are checked instead. + """ + + def __init__(self, schema: dict): + """ + :param schema: The JSON schema against which to compare data. + """ + super().__init__() + + self._schema: dict = schema + + def is_valid(self, data: Any) -> bool: + """ + :return: `True` if data are valid as per the JSON schema; + `False` otherwise. + + :raises: BoboValidatorError: Invalid JSON schema. + """ + try: + jsonschema_validate(instance=data, schema=self._schema) + + except ValidationError: + return False + + except SchemaError as e: + raise BoboValidatorError(e) + + return True diff --git a/bobocep/cep/event/event.py b/bobocep/cep/event/event.py index 3f7616ed..8a77da25 100644 --- a/bobocep/cep/event/event.py +++ b/bobocep/cep/event/event.py @@ -9,8 +9,7 @@ from abc import ABC, abstractmethod from typing import Any -from bobocep import BoboError -from bobocep.bobocep import BoboJSONable +from bobocep import BoboError, BoboJSONable _EXC_ID_LEN = "event ID must have a length greater than 0" diff --git a/bobocep/cep/phenom/phenom.py b/bobocep/cep/phenom/phenom.py index 7b38a5db..ded9cae8 100644 --- a/bobocep/cep/phenom/phenom.py +++ b/bobocep/cep/phenom/phenom.py @@ -67,6 +67,7 @@ def __init__(self, self._patterns: Tuple[BoboPattern, ...] = tuple(patterns) self._datagen: Optional[Callable] = datagen self._action: Optional[BoboAction] = action + self._retain: bool = retain @property def name(self) -> str: @@ -95,3 +96,10 @@ def action(self) -> Optional[BoboAction]: :return: Phenomenon action, or `None`. """ return self._action + + @property + def retain(self) -> bool: + """ + :return: True if retains datagen callable; False otherwise. + """ + return self._retain diff --git a/bobocep/dist/pubsub.py b/bobocep/dist/pubsub.py index 95dc4b35..da58c89d 100644 --- a/bobocep/dist/pubsub.py +++ b/bobocep/dist/pubsub.py @@ -1,6 +1,7 @@ # Copyright (c) 2019-2023 r3w0p # The following code can be redistributed and/or # modified under the terms of the MIT License. + """ Distributed publish-subscribe classes. """ diff --git a/docs/conf.py b/docs/conf.py index aca1bea5..9c63146d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,7 +16,7 @@ project = "BoboCEP" copyright = "2019-2023 r3w0p" author = "r3w0p" -version = "1.0.1" +version = "1.1.0" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration diff --git a/docs/developer_guide.rst b/docs/developer_guide.rst new file mode 100644 index 00000000..f3d5cda3 --- /dev/null +++ b/docs/developer_guide.rst @@ -0,0 +1,128 @@ +Developer Guide +*************** + +Dependencies +============ + +You will need to install the core :code:`BoboCEP` requirements from both +:code:`requirements.txt` and its additional development requirements from +:code:`requirements-dev.txt`. +For example: + +.. code:: console + + pip install -r requirements.txt + pip install -r requirements-dev.txt + + +Development Tools +================= + +:code:`BoboCEP` uses GitHub Actions for *Continuous Integration* (CI) and +*Continuous Deployment* (CD). +It uses two YAML scripts to trigger the respective action workflows, namely: + +1. :code:`.github/workflows/cicd.yml` for CI/CD tasks, including: + linting, type checking, code coverage, documentation coverage, and + deployment to PyPI. +2. :code:`.github/workflows/security.yml` for security checks. + +These scripts are triggered on a push to the :code:`main` branch. +The security script also runs periodically. + +It is recommended that you run the individual CI/CD tasks manually before +committing. +These are discussed next. + + +Code Linting +------------ + +:code:`flake8` is used for code linting. +Run the following two commands to lint :code:`BoboCEP` and its test suite, +respectively. + +.. code:: + + flake8 ./bobocep --count --select=E9,F63,F7,F82 --show-source --statistics + flake8 ./tests --count --select=E9,F63,F7,F82 --show-source --statistics + + +Code Testing and Coverage +------------------------- + +:code:`coverage` is used for code testing and coverage. +Results are uploaded to +`Code Climate `_. +Run the following command to test :code:`BoboCEP`. + +.. code:: + + coverage run --source=bobocep -m pytest tests + +GitHub Actions additionally enforces a minimum coverage of 98%. +You can check that this requirement has been satisfied using the following. + +.. code:: + + coverage report --fail-under=98 + +You can locally inspect the code coverage with an HTML output by running +the following. + +.. code:: + + coverage html + + +Documentation +------------- + +Documentation is built using :code:`sphinx` and is deployed via +`Read the Docs `_. +You can compile documentation locally via the following. + +.. code:: + + cd docs + make html + +Or, for Windows (PowerShell). + +.. code:: + + cd docs + .\make.bat html + + +Documentation Coverage +---------------------- + +:code:`interrogate` is used for testing and code coverage. +Run the following command to check :code:`BoboCEP` documentation coverage. +It requires a minimum documentation coverage of 100%. + +.. code:: + + interrogate -vv bobocep --fail-under 100 + + +Type Checking +------------- + +:code:`mypy` is used for type checking. +Run the following two commands to check :code:`BoboCEP` and its test suite, +respectively. + +.. code:: + + mypy ./bobocep + mypy ./tests + + +Versioning +---------- + +:code:`BoboCEP` uses `Semantic Versioning `_ and +the :code:`bump2version` tool for editing the software version. +See `here `_ for more information. diff --git a/docs/index.rst b/docs/index.rst index c2a38264..9fbbf608 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -28,4 +28,5 @@ :hidden: source_code + developer_guide contributing diff --git a/docs/installation.rst b/docs/installation.rst index 527ad1f2..c0a9d42a 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -85,12 +85,6 @@ You can also build :code:`BoboCEP` manually with: Development =========== -If you are installing :code:`BoboCEP` for development purposes, you will -need to install its core requirements from both -:code:`requirements.txt` and its additional development requirements from -:code:`requirements-dev.txt`. For example: - -.. code:: console - - pip install -r requirements.txt - pip install -r requirements-dev.txt +If you want to develop :code:`BoboCEP`, see +`Developer Guide `_ +for more information. diff --git a/requirements-dev.txt b/requirements-dev.txt index f3f16880..537145c2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,10 +1,10 @@ bump2version==1.0.1 -coverage==7.0.5 -flake8==6.0.0 -Flask==2.2.3 +coverage==7.3.1 +flake8==6.1.0 +Flask==2.3.3 interrogate==1.5.0 -mypy==0.991 -pytest==7.2.0 -sphinx==4.1.1 +mypy==1.5.1 +pytest==7.4.2 +sphinx==4.2.0 sphinx-mdinclude==0.5.3 sphinx-rtd-theme==1.1.1 diff --git a/requirements.txt b/requirements.txt index 41904a73..e084a3d7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -pycryptodome==3.17.0 +jsonschema==4.19.0 +pycryptodome==3.18.0 diff --git a/setup.cfg b/setup.cfg index 7b609a74..8205bb30 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,9 +1,6 @@ [bumpversion] -current_version = 1.0.1 +current_version = 1.1.0 commit = True -tag = True -sign_tags = True -tag_name = {new_version} [bumpversion:file:setup.py] search = version="{current_version}" diff --git a/setup.py b/setup.py index 478ba249..f46c5907 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( name="bobocep", - version="1.0.1", + version="1.1.0", author="r3w0p", author_email="rr33ww00pp@gmail.com", description="A fault-tolerant Complex Event Processing engine " @@ -19,11 +19,11 @@ url="https://github.com/r3w0p/bobocep", keywords=[ "complex event processing", - "internet of things", - "web of things", - "fault tolerance", + "distributed systems", "edge computing", - "distributed systems" + "fault tolerance", + "internet of things", + "web of things" ], classifiers=[ "Development Status :: 5 - Production/Stable", @@ -34,6 +34,8 @@ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Topic :: Scientific/Engineering :: Information Analysis", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: System :: Distributed Computing", "Topic :: System :: Monitoring" ], install_requires=install_requires, diff --git a/tests/postman/__init__.py b/tests/postman/__init__.py index 3a80490d..24cdd84c 100644 --- a/tests/postman/__init__.py +++ b/tests/postman/__init__.py @@ -1,3 +1,7 @@ +# Copyright (c) 2019-2023 r3w0p +# The following code can be redistributed and/or +# modified under the terms of the MIT License. + """ Tests for data-input testing via Postman. """ diff --git a/tests/postman/flask_123/run.py b/tests/postman/flask_123/run.py index 5e321b00..5c6b9ac9 100644 --- a/tests/postman/flask_123/run.py +++ b/tests/postman/flask_123/run.py @@ -1,3 +1,7 @@ +# Copyright (c) 2019-2023 r3w0p +# The following code can be redistributed and/or +# modified under the terms of the MIT License. + from datetime import datetime from threading import RLock, Thread from typing import Tuple, Any diff --git a/tests/postman/flask_dist_abc_def/run_dist_1.py b/tests/postman/flask_dist_abc_def/run_dist_1.py index 9a21a89d..b306c543 100644 --- a/tests/postman/flask_dist_abc_def/run_dist_1.py +++ b/tests/postman/flask_dist_abc_def/run_dist_1.py @@ -1,3 +1,7 @@ +# Copyright (c) 2019-2023 r3w0p +# The following code can be redistributed and/or +# modified under the terms of the MIT License. + import logging from datetime import datetime from threading import Thread, RLock @@ -12,7 +16,7 @@ from bobocep.dist import BoboDevice from bobocep.setup import BoboSetupSimpleDistributed -app = Flask(__name__) # v2.2.3 +app = Flask(__name__) engine: BoboEngine diff --git a/tests/postman/flask_dist_abc_def/run_dist_2.py b/tests/postman/flask_dist_abc_def/run_dist_2.py index 0b20b879..11b95d1a 100644 --- a/tests/postman/flask_dist_abc_def/run_dist_2.py +++ b/tests/postman/flask_dist_abc_def/run_dist_2.py @@ -1,3 +1,7 @@ +# Copyright (c) 2019-2023 r3w0p +# The following code can be redistributed and/or +# modified under the terms of the MIT License. + import logging from datetime import datetime from threading import Thread, RLock @@ -12,7 +16,7 @@ from bobocep.dist import BoboDevice from bobocep.setup import BoboSetupSimpleDistributed -app = Flask(__name__) # v2.2.3 +app = Flask(__name__) engine: BoboEngine diff --git a/tests/test_bobocep/__init__.py b/tests/test_bobocep/__init__.py index 38e37ec1..e9de1422 100644 --- a/tests/test_bobocep/__init__.py +++ b/tests/test_bobocep/__init__.py @@ -1,3 +1,7 @@ +# Copyright (c) 2019-2023 r3w0p +# The following code can be redistributed and/or +# modified under the terms of the MIT License. + """ Tests for unit testing via pytest. """ diff --git a/tests/test_bobocep/test_cep/test_action/test_common/__init__.py b/tests/test_bobocep/test_cep/test_action/test_common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_bobocep/test_cep/test_action/test_common/test_BoboActionMultiSequential.py b/tests/test_bobocep/test_cep/test_action/test_common/test_BoboActionMultiSequential.py new file mode 100644 index 00000000..719fee40 --- /dev/null +++ b/tests/test_bobocep/test_cep/test_action/test_common/test_BoboActionMultiSequential.py @@ -0,0 +1,94 @@ +# Copyright (c) 2019-2023 r3w0p +# The following code can be redistributed and/or +# modified under the terms of the MIT License. + +import pytest + +from bobocep.cep.action import BoboActionError +from bobocep.cep.action.common.multi import BoboActionMultiSequential +from tests.test_bobocep.test_cep.test_action import BoboActionTrue, \ + BoboActionFalse +from tests.test_bobocep.test_cep.test_event import tc_event_complex + + +class TestValid: + + def test_3_actions_all_success_stop(self): + actions = [ + BoboActionTrue(), + BoboActionTrue(), + BoboActionTrue() + ] + multi = BoboActionMultiSequential( + name="test_multi", + actions=actions, + stop_on_fail=True) + + success, data = multi.execute(tc_event_complex()) + + assert success + assert len(data) == len(actions) + + a1_success, a1_data = data[0] + a2_success, a2_data = data[1] + a3_success, a3_data = data[2] + + assert a1_success + assert a2_success + assert a3_success + + def test_3_actions_middle_fail_stop(self): + actions = [ + BoboActionTrue(), + BoboActionFalse(), + BoboActionTrue() + ] + multi = BoboActionMultiSequential( + name="test_multi", + actions=actions, + stop_on_fail=True) + + success, data = multi.execute(tc_event_complex()) + + assert not success + assert len(data) == (len(actions) - 1) + + a1_success, a1_data = data[0] + a2_success, a2_data = data[1] + + assert a1_success + assert not a2_success + + def test_3_actions_middle_fail_no_stop(self): + actions = [ + BoboActionTrue(), + BoboActionFalse(), + BoboActionTrue() + ] + multi = BoboActionMultiSequential( + name="test_multi", + actions=actions, + stop_on_fail=False) + + success, data = multi.execute(tc_event_complex()) + + assert not success + assert len(data) == len(actions) + + a1_success, a1_data = data[0] + a2_success, a2_data = data[1] + a3_success, a3_data = data[2] + + assert a1_success + assert not a2_success + assert a3_success + + +class TestInvalid: + + def test_0_actions(self): + with pytest.raises(BoboActionError): + BoboActionMultiSequential( + name="test_multi", + actions=[], + stop_on_fail=True) diff --git a/tests/test_bobocep/test_cep/test_engine/test_receiver/test_BoboValidatorJSONSchema.py b/tests/test_bobocep/test_cep/test_engine/test_receiver/test_BoboValidatorJSONSchema.py new file mode 100644 index 00000000..ebcf4942 --- /dev/null +++ b/tests/test_bobocep/test_cep/test_engine/test_receiver/test_BoboValidatorJSONSchema.py @@ -0,0 +1,55 @@ +# Copyright (c) 2019-2023 r3w0p +# The following code can be redistributed and/or +# modified under the terms of the MIT License. +import pytest + +from bobocep.cep.engine.receiver.validator import BoboValidatorError, \ + BoboValidatorJSONSchema + +SCHEMA_VALID: dict = { + "type": "object", + "required": ["forename", "surname"], + "properties": { + "forename": { + "type": "string" + }, + "surname": { + "type": "string" + } + } +} + +SCHEMA_INVALID: str = "abc123" + + +class TestValid: + + def test_valid_schema_valid_instance(self): + data: dict = { + "forename": "Foo", + "surname": "Bar" + } + + validator = BoboValidatorJSONSchema(schema=SCHEMA_VALID) + + assert validator.is_valid(data=data) + + +class TestInvalid: + + def test_invalid_schema(self): + data: dict = {} + + validator = BoboValidatorJSONSchema(schema=SCHEMA_INVALID) + + with pytest.raises(BoboValidatorError): + assert validator.is_valid(data=data) + + def test_valid_schema_invalid_instance(self): + data: dict = { + "forename": "Foo" + } + + validator = BoboValidatorJSONSchema(schema=SCHEMA_VALID) + + assert not validator.is_valid(data=data) diff --git a/tests/test_bobocep/test_cep/test_phenom/test_BoboPhenomenon.py b/tests/test_bobocep/test_cep/test_phenom/test_BoboPhenomenon.py index 71a5e1de..e42a8c20 100644 --- a/tests/test_bobocep/test_cep/test_phenom/test_BoboPhenomenon.py +++ b/tests/test_bobocep/test_cep/test_phenom/test_BoboPhenomenon.py @@ -4,8 +4,38 @@ import pytest -from bobocep.cep.phenom.phenom import BoboPhenomenonError -from tests.test_bobocep.test_cep.test_phenom import tc_phenomenon +from bobocep.cep.phenom.phenom import BoboPhenomenonError, BoboPhenomenon +from tests.test_bobocep.test_cep.test_action import BoboActionTrue +from tests.test_bobocep.test_cep.test_phenom import tc_phenomenon, tc_pattern + + +class TestValid: + + def test_one_pattern(self): + name_phenom = "name_phenom" + name_pattern = "name_pattern" + name_action = "name_action" + + pattern = tc_pattern(name_pattern) + action = BoboActionTrue(name_action) + datagen = lambda p, h: 123 + retain = True + + phenom = BoboPhenomenon( + name=name_phenom, + patterns=[pattern], + action=action, + datagen=datagen, + retain=retain) + + assert phenom.name == name_phenom + assert len(phenom.patterns) == 1 + assert phenom.patterns[0].name == name_pattern + assert phenom.action is not None + assert phenom.action.name == name_action + assert phenom.datagen is not None + assert phenom.datagen(None, None) == 123 + assert phenom.retain class TestInvalid: diff --git a/tests/test_bobocep/test_cep/test_phenom/test_pattern/test_BoboPattern.py b/tests/test_bobocep/test_cep/test_phenom/test_pattern/test_BoboPattern.py index e8413f58..e0d24916 100644 --- a/tests/test_bobocep/test_cep/test_phenom/test_pattern/test_BoboPattern.py +++ b/tests/test_bobocep/test_cep/test_phenom/test_pattern/test_BoboPattern.py @@ -10,6 +10,18 @@ tc_predicate +# TODO tests for valid patterns, different +class TestValid: + + def test_name_0_length(self): + with pytest.raises(BoboPatternError): + BoboPattern(name="", + blocks=[tc_block(group="a")], + preconditions=[tc_predicate()], + haltconditions=[tc_predicate()]) + + + class TestInvalid: def test_name_0_length(self): diff --git a/tests/test_examples.py b/tests/test_examples.py new file mode 100644 index 00000000..b1e016d9 --- /dev/null +++ b/tests/test_examples.py @@ -0,0 +1,435 @@ +# Copyright (c) 2019-2023 r3w0p +# The following code can be redistributed and/or +# modified under the terms of the MIT License. + +""" +Tests that ensure complex event generation occurs when expected. +The tests contain typical patterns that may be built by the user +with the pattern builder. + +This is not an exhaustive list due to the sheer number of possible +combinations. However, the tests are representative of some of the +common patterns that are likely to frequently occur. +""" + +from threading import RLock +from typing import Tuple, Any, List + +from bobocep.cep.action import BoboAction, BoboActionHandlerBlocking +from bobocep.cep.event import BoboEventComplex +from bobocep.cep.phenom import BoboPattern, BoboPhenomenon +from bobocep.cep.phenom.pattern.builder import BoboPatternBuilder +from bobocep.setup import BoboSetupSimple + + +class BoboActionCounter(BoboAction): + """ + An action that counts how many times it has been executed. + """ + + def __init__(self, name: str): + """ + :param name: The name of the action. + """ + super().__init__(name) + self._lock: RLock = RLock() + self._counter: int = 0 + + def execute(self, event: BoboEventComplex) -> Tuple[bool, Any]: + """ + Increments the counter value. + + :param event: The complex event. + + :return: True, and the new counter value + """ + with self._lock: + self._counter += 1 + + return True, None + + @property + def counter(self) -> int: + """ + Get counter. + """ + with self._lock: + return self._counter + + +def _setup(patterns: List[BoboPattern]): + action = BoboActionCounter(name="action") + + phenom = BoboPhenomenon( + name="phenom", + patterns=patterns, + action=action) + + engine = BoboSetupSimple( + phenomena=[phenom], + handler=BoboActionHandlerBlocking() + ).generate() + + return engine, action + + +class TestValid: + + def test_fb_fb_fb_data_exact(self): + pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \ + .followed_by(lambda e, h: int(e.data) == 1) \ + .followed_by(lambda e, h: int(e.data) == 2) \ + .followed_by(lambda e, h: int(e.data) == 3) \ + .generate() + + engine, action = _setup([pattern_1]) + + for i in [1, 2, 3]: + engine.receiver.add_data(i) + engine.update() + + assert len(engine.decider.all_runs()) == 0 + assert action.counter == 1 + + def test_fb_nfb_fb_data_exact(self): + pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \ + .followed_by(lambda e, h: int(e.data) == 1) \ + .not_followed_by(lambda e, h: int(e.data) == 2) \ + .followed_by(lambda e, h: int(e.data) == 3) \ + .generate() + + engine, action = _setup([pattern_1]) + + for i in [1, 6, 3]: + engine.receiver.add_data(i) + engine.update() + + assert len(engine.decider.all_runs()) == 0 + assert action.counter == 1 + + def test_fb_nfb_fb_data_not_exact(self): + pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \ + .followed_by(lambda e, h: int(e.data) == 1) \ + .not_followed_by(lambda e, h: int(e.data) == 2) \ + .followed_by(lambda e, h: int(e.data) == 3) \ + .generate() + + engine, action = _setup([pattern_1]) + + for i in [1, 2, 6, 3]: + engine.receiver.add_data(i) + engine.update() + + assert len(engine.decider.all_runs()) == 0 + assert action.counter == 1 + + def test_fb_fba_fb_data_exact(self): + pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \ + .followed_by(lambda e, h: int(e.data) == 1) \ + .followed_by_any([ + lambda e, h: int(e.data) == 21, + lambda e, h: int(e.data) == 22, + lambda e, h: int(e.data) == 23 + ]) \ + .followed_by(lambda e, h: int(e.data) == 3) \ + .generate() + + engine, action = _setup([pattern_1]) + + for i in [1, 22, 3]: + engine.receiver.add_data(i) + engine.update() + + assert len(engine.decider.all_runs()) == 0 + assert action.counter == 1 + + def test_fb_fba_fb_data_not_exact(self): + pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \ + .followed_by(lambda e, h: int(e.data) == 1) \ + .followed_by_any([ + lambda e, h: int(e.data) == 21, + lambda e, h: int(e.data) == 22, + lambda e, h: int(e.data) == 23 + ]) \ + .followed_by(lambda e, h: int(e.data) == 3) \ + .generate() + + engine, action = _setup([pattern_1]) + + for i in [1, 6, 23, 3]: + engine.receiver.add_data(i) + engine.update() + + assert len(engine.decider.all_runs()) == 0 + assert action.counter == 1 + + def test_fb_nfba_fb_data_exact(self): + pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \ + .followed_by(lambda e, h: int(e.data) == 1) \ + .not_followed_by_any([ + lambda e, h: int(e.data) == 21, + lambda e, h: int(e.data) == 22, + lambda e, h: int(e.data) == 23 + ]) \ + .followed_by(lambda e, h: int(e.data) == 3) \ + .generate() + + engine, action = _setup([pattern_1]) + + for i in [1, 2, 3]: + engine.receiver.add_data(i) + engine.update() + + assert len(engine.decider.all_runs()) == 0 + assert action.counter == 1 + + def test_fb_nfba_fb_data_not_exact(self): + pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \ + .followed_by(lambda e, h: int(e.data) == 1) \ + .not_followed_by_any([ + lambda e, h: int(e.data) == 21, + lambda e, h: int(e.data) == 22, + lambda e, h: int(e.data) == 23 + ]) \ + .followed_by(lambda e, h: int(e.data) == 3) \ + .generate() + + engine, action = _setup([pattern_1]) + + for i in [1, 21, 22, 23, 2, 3]: + engine.receiver.add_data(i) + engine.update() + + assert len(engine.decider.all_runs()) == 0 + assert action.counter == 1 + + def test_n_n_n_data_exact(self): + pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \ + .next(lambda e, h: int(e.data) == 1) \ + .next(lambda e, h: int(e.data) == 2) \ + .next(lambda e, h: int(e.data) == 3) \ + .generate() + + engine, action = _setup([pattern_1]) + + for i in [1, 2, 3]: + engine.receiver.add_data(i) + engine.update() + + assert len(engine.decider.all_runs()) == 0 + assert action.counter == 1 + + def test_n_nn_n_data_exact(self): + pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \ + .next(lambda e, h: int(e.data) == 1) \ + .not_next(lambda e, h: int(e.data) == 2) \ + .next(lambda e, h: int(e.data) == 3) \ + .generate() + + engine, action = _setup([pattern_1]) + + for i in [1, 6, 3]: + engine.receiver.add_data(i) + engine.update() + + assert len(engine.decider.all_runs()) == 0 + assert action.counter == 1 + + def test_fb_fb_fb_pre_data_exact(self): + pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \ + .followed_by(lambda e, h: int(e.data) == 1) \ + .followed_by(lambda e, h: int(e.data) == 2) \ + .followed_by(lambda e, h: int(e.data) == 3) \ + .precondition(lambda e, h: int(e.data) > 0) \ + .generate() + + engine, action = _setup([pattern_1]) + + for i in [1, 2, 3]: + engine.receiver.add_data(i) + engine.update() + + assert len(engine.decider.all_runs()) == 0 + assert action.counter == 1 + + def test_fb_fb_fb_halt_data_exact(self): + pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \ + .followed_by(lambda e, h: int(e.data) == 1) \ + .followed_by(lambda e, h: int(e.data) == 2) \ + .followed_by(lambda e, h: int(e.data) == 3) \ + .haltcondition(lambda e, h: int(e.data) == 10) \ + .generate() + + engine, action = _setup([pattern_1]) + + for i in [1, 2, 3]: + engine.receiver.add_data(i) + engine.update() + + assert len(engine.decider.all_runs()) == 0 + assert action.counter == 1 + + def test_fb_fb_fb_times_data_exact(self): + pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \ + .followed_by(lambda e, h: int(e.data) == 1) \ + .followed_by(lambda e, h: int(e.data) == 2, times=3) \ + .followed_by(lambda e, h: int(e.data) == 3) \ + .generate() + + engine, action = _setup([pattern_1]) + + for i in [1, 2, 2, 2, 3]: + engine.receiver.add_data(i) + engine.update() + + assert len(engine.decider.all_runs()) == 0 + assert action.counter == 1 + + def test_fb_fb_fb_loop_minimum(self): + pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \ + .followed_by(lambda e, h: int(e.data) == 1) \ + .followed_by(lambda e, h: int(e.data) == 2, loop=True) \ + .followed_by(lambda e, h: int(e.data) == 3) \ + .generate() + + engine, action = _setup([pattern_1]) + + for i in [1, 3]: + engine.receiver.add_data(i) + engine.update() + + assert len(engine.decider.all_runs()) == 0 + assert action.counter == 1 + + def test_fb_fb_fb_loop_once(self): + pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \ + .followed_by(lambda e, h: int(e.data) == 1) \ + .followed_by(lambda e, h: int(e.data) == 2, loop=True) \ + .followed_by(lambda e, h: int(e.data) == 3) \ + .generate() + + engine, action = _setup([pattern_1]) + + for i in [1, 2, 3]: + engine.receiver.add_data(i) + engine.update() + + assert len(engine.decider.all_runs()) == 0 + assert action.counter == 1 + + def test_fb_fb_fb_loop_many(self): + pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \ + .followed_by(lambda e, h: int(e.data) == 1) \ + .followed_by(lambda e, h: int(e.data) == 2, loop=True) \ + .followed_by(lambda e, h: int(e.data) == 3) \ + .generate() + + engine, action = _setup([pattern_1]) + + for i in [1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3]: + engine.receiver.add_data(i) + engine.update() + + assert len(engine.decider.all_runs()) == 0 + assert action.counter == 1 + + def test_fb_fb_fb_optional_included(self): + pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \ + .followed_by(lambda e, h: int(e.data) == 1) \ + .followed_by(lambda e, h: int(e.data) == 2, optional=True) \ + .followed_by(lambda e, h: int(e.data) == 3) \ + .generate() + + engine, action = _setup([pattern_1]) + + for i in [1, 2, 3]: + engine.receiver.add_data(i) + engine.update() + + assert len(engine.decider.all_runs()) == 0 + assert action.counter == 1 + + def test_fb_fb_fb_optional_not_included(self): + pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \ + .followed_by(lambda e, h: int(e.data) == 1) \ + .followed_by(lambda e, h: int(e.data) == 2, optional=True) \ + .followed_by(lambda e, h: int(e.data) == 3) \ + .generate() + + engine, action = _setup([pattern_1]) + + for i in [1, 3]: + engine.receiver.add_data(i) + engine.update() + + assert len(engine.decider.all_runs()) == 0 + assert action.counter == 1 + + +class TestInvalid: + + def test_fb_fb_fb_pre(self): + pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \ + .followed_by(lambda e, h: int(e.data) == 1) \ + .followed_by(lambda e, h: int(e.data) == 2) \ + .followed_by(lambda e, h: int(e.data) == 3) \ + .precondition(lambda e, h: int(e.data) > 0) \ + .generate() + + engine, action = _setup([pattern_1]) + + for i in [1, 0, 2, 3]: + engine.receiver.add_data(i) + engine.update() + + assert len(engine.decider.all_runs()) == 0 + assert action.counter == 0 + + def test_fb_fb_fb_halt(self): + pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \ + .followed_by(lambda e, h: int(e.data) == 1) \ + .followed_by(lambda e, h: int(e.data) == 2) \ + .followed_by(lambda e, h: int(e.data) == 3) \ + .haltcondition(lambda e, h: int(e.data) == 10) \ + .generate() + + engine, action = _setup([pattern_1]) + + for i in [1, 10, 2, 3]: + engine.receiver.add_data(i) + engine.update() + + assert len(engine.decider.all_runs()) == 0 + assert action.counter == 0 + + def test_n_n_n(self): + pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \ + .next(lambda e, h: int(e.data) == 1) \ + .next(lambda e, h: int(e.data) == 2) \ + .next(lambda e, h: int(e.data) == 3) \ + .generate() + + engine, action = _setup([pattern_1]) + + for i in [1, 6, 3]: + engine.receiver.add_data(i) + engine.update() + + assert len(engine.decider.all_runs()) == 0 + assert action.counter == 0 + + def test_n_nn_n(self): + pattern_1: BoboPattern = BoboPatternBuilder("pattern_1") \ + .next(lambda e, h: int(e.data) == 1) \ + .not_next(lambda e, h: int(e.data) == 2) \ + .next(lambda e, h: int(e.data) == 3) \ + .generate() + + engine, action = _setup([pattern_1]) + + for i in [1, 2, 3]: + engine.receiver.add_data(i) + engine.update() + + assert len(engine.decider.all_runs()) == 0 + assert action.counter == 0