-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #14 from canonical/emitted-events
emitted_events util to assert a given sequence of events have been (re)/emitted
- Loading branch information
Showing
12 changed files
with
287 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
name: Tests | ||
|
||
on: | ||
pull-request: | ||
branches: | ||
- main | ||
|
||
|
||
jobs: | ||
linting: | ||
name: Linting | ||
steps: | ||
- name: Checkout | ||
uses: actions/checkout@v3 | ||
with: | ||
fetch-depth: 1 | ||
- name: Set up python | ||
uses: actions/setup-python@v4 | ||
with: | ||
python-version: 3.8 | ||
- name: Install dependencies | ||
run: python -m pip install tox | ||
- name: Run tests | ||
run: tox -vve lint | ||
|
||
unit-test: | ||
name: Unit Tests | ||
steps: | ||
- name: Checkout | ||
uses: actions/checkout@v3 | ||
with: | ||
fetch-depth: 1 | ||
- name: Set up python | ||
uses: actions/setup-python@v4 | ||
with: | ||
python-version: 3.8 | ||
- name: Install dependencies | ||
run: python -m pip install tox | ||
- name: Run tests | ||
run: tox -vve unit |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,14 @@ | ||
[build-system] | ||
requires = ["setuptools"] | ||
requires = [ | ||
"setuptools >= 35.0.2", | ||
"setuptools_scm >= 2.0.0, <3" | ||
] | ||
build-backend = "setuptools.build_meta" | ||
|
||
[project] | ||
name = "ops-scenario" | ||
|
||
version = "2.1.3.2" | ||
version = "2.1.3.3" | ||
authors = [ | ||
{ name = "Pietro Pasotti", email = "[email protected]" } | ||
] | ||
|
@@ -46,4 +49,4 @@ include = '\.pyi?$' | |
profile = "black" | ||
|
||
[bdist_wheel] | ||
universal=1 | ||
universal = 1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
#!/usr/bin/env python3 | ||
# Copyright 2023 Canonical Ltd. | ||
# See LICENSE file for licensing details. | ||
from scenario.capture_events import capture_events | ||
from scenario.runtime import trigger | ||
from scenario.state import * |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
#!/usr/bin/env python3 | ||
# Copyright 2023 Canonical Ltd. | ||
# See LICENSE file for licensing details. | ||
import typing | ||
from contextlib import contextmanager | ||
from typing import ContextManager, List, Type, TypeVar | ||
|
||
from ops.framework import ( | ||
CommitEvent, | ||
EventBase, | ||
Framework, | ||
Handle, | ||
NoTypeError, | ||
PreCommitEvent, | ||
) | ||
|
||
_T = TypeVar("_T", bound=EventBase) | ||
|
||
|
||
@contextmanager | ||
def capture_events( | ||
*types: Type[EventBase], include_framework=False, include_deferred=True | ||
) -> ContextManager[List[EventBase]]: | ||
"""Capture all events of type `*types` (using instance checks). | ||
Example:: | ||
>>> from ops.charm import StartEvent | ||
>>> from scenario import Event, State | ||
>>> from charm import MyCustomEvent, MyCharm # noqa | ||
>>> | ||
>>> def test_my_event(): | ||
>>> with capture_events(StartEvent, MyCustomEvent) as captured: | ||
>>> State().trigger("start", MyCharm, meta=MyCharm.META) | ||
>>> | ||
>>> assert len(captured) == 2 | ||
>>> e1, e2 = captured | ||
>>> assert isinstance(e2, MyCustomEvent) | ||
>>> assert e2.custom_attr == 'foo' | ||
""" | ||
allowed_types = types or (EventBase,) | ||
|
||
captured = [] | ||
_real_emit = Framework._emit | ||
_real_reemit = Framework.reemit | ||
|
||
def _wrapped_emit(self, evt): | ||
if not include_framework and isinstance(evt, (PreCommitEvent, CommitEvent)): | ||
return _real_emit(self, evt) | ||
|
||
if isinstance(evt, allowed_types): | ||
captured.append(evt) | ||
|
||
return _real_emit(self, evt) | ||
|
||
def _wrapped_reemit(self): | ||
# Framework calls reemit() before emitting the main juju event. We intercept that call | ||
# and capture all events in storage. | ||
|
||
if not include_deferred: | ||
return _real_reemit(self) | ||
|
||
# load all notices from storage as events. | ||
for event_path, observer_path, method_name in self._storage.notices(): | ||
event_handle = Handle.from_path(event_path) | ||
try: | ||
event = self.load_snapshot(event_handle) | ||
except NoTypeError: | ||
continue | ||
event = typing.cast(EventBase, event) | ||
event.deferred = False | ||
self._forget(event) # prevent tracking conflicts | ||
|
||
if not include_framework and isinstance( | ||
event, (PreCommitEvent, CommitEvent) | ||
): | ||
continue | ||
|
||
if isinstance(event, allowed_types): | ||
captured.append(event) | ||
|
||
return _real_reemit(self) | ||
|
||
Framework._emit = _wrapped_emit # type: ignore # noqa # ugly | ||
Framework.reemit = _wrapped_reemit # type: ignore # noqa # ugly | ||
|
||
yield captured | ||
|
||
Framework._emit = _real_emit # type: ignore # noqa # ugly | ||
Framework.reemit = _real_reemit # type: ignore # noqa # ugly |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import pytest | ||
from ops.charm import CharmBase, CharmEvents, StartEvent | ||
from ops.framework import CommitEvent, EventBase, EventSource, PreCommitEvent | ||
|
||
from scenario import Event, State, capture_events | ||
|
||
|
||
class Foo(EventBase): | ||
pass | ||
|
||
|
||
class MyCharmEvents(CharmEvents): | ||
foo = EventSource(Foo) | ||
|
||
|
||
class MyCharm(CharmBase): | ||
META = {"name": "mycharm"} | ||
on = MyCharmEvents() | ||
|
||
def __init__(self, *args, **kwargs): | ||
super().__init__(*args, **kwargs) | ||
self.framework.observe(self.on.start, self._on_start) | ||
self.framework.observe(self.on.foo, self._on_foo) | ||
|
||
def _on_start(self, e): | ||
self.on.foo.emit() | ||
|
||
def _on_foo(self, e): | ||
pass | ||
|
||
|
||
def test_capture_custom_evt(): | ||
with capture_events(Foo) as emitted: | ||
State().trigger("foo", MyCharm, meta=MyCharm.META) | ||
|
||
assert len(emitted) == 1 | ||
assert isinstance(emitted[0], Foo) | ||
|
||
|
||
def test_capture_custom_evt_nonspecific_capture(): | ||
with capture_events() as emitted: | ||
State().trigger("foo", MyCharm, meta=MyCharm.META) | ||
|
||
assert len(emitted) == 1 | ||
assert isinstance(emitted[0], Foo) | ||
|
||
|
||
def test_capture_custom_evt_nonspecific_capture_include_fw_evts(): | ||
with capture_events(include_framework=True) as emitted: | ||
State().trigger("foo", MyCharm, meta=MyCharm.META) | ||
|
||
assert len(emitted) == 3 | ||
assert isinstance(emitted[0], Foo) | ||
assert isinstance(emitted[1], PreCommitEvent) | ||
assert isinstance(emitted[2], CommitEvent) | ||
|
||
|
||
def test_capture_juju_evt(): | ||
with capture_events() as emitted: | ||
State().trigger("start", MyCharm, meta=MyCharm.META) | ||
|
||
assert len(emitted) == 2 | ||
assert isinstance(emitted[0], StartEvent) | ||
assert isinstance(emitted[1], Foo) | ||
|
||
|
||
def test_capture_deferred_evt(): | ||
# todo: this test should pass with ops < 2.1 as well | ||
with capture_events() as emitted: | ||
State(deferred=[Event("foo").deferred(handler=MyCharm._on_foo)]).trigger( | ||
"start", MyCharm, meta=MyCharm.META | ||
) | ||
|
||
assert len(emitted) == 3 | ||
assert isinstance(emitted[0], Foo) | ||
assert isinstance(emitted[1], StartEvent) | ||
assert isinstance(emitted[2], Foo) | ||
|
||
|
||
def test_capture_no_deferred_evt(): | ||
# todo: this test should pass with ops < 2.1 as well | ||
with capture_events(include_deferred=False) as emitted: | ||
State(deferred=[Event("foo").deferred(handler=MyCharm._on_foo)]).trigger( | ||
"start", MyCharm, meta=MyCharm.META | ||
) | ||
|
||
assert len(emitted) == 2 | ||
assert isinstance(emitted[0], StartEvent) | ||
assert isinstance(emitted[1], Foo) |
Oops, something went wrong.