canonical / ops-scenario

State-transition testing SDK for Operator Framework Juju charms.
Apache License 2.0
10 stars 7 forks source link

NoObserverError for secret expired event #157

Closed gruyaume closed 2 months ago

gruyaume commented 2 months ago

Description

I am getting NoObserverError when trying to expired secret events.

Library code

class DummyCharm(CharmBase):
    def __init__(self, *args):
        super().__init__(*args)
        self.framework.observe(self.on.secret_expired, self._on_secret_expired)

    def _on_secret_expired(self, event) -> None:
        print("OK")

Scenario code

certificate_secret = Secret(
    id="1",
    revision=0,
    label=f"{LIBID}-certificate-0",
    owner="unit",
    contents={0: {"certificate": "whatever content"}},
    expire=datetime.datetime.now() - datetime.timedelta(minutes=1),
)

state_in = scenario.State(
    relations=[],
    secrets=[
        certificate_secret
    ],
)

state_out = self.ctx.run(certificate_secret.expired_event, state_in)

Logs

Traceback (most recent call last):
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/scenario/ops_main_mock.py", line 69, in _emit_charm_event
    event_to_emit = getattr(owner, event_name)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/ops/framework.py", line 507, in __getattr__
    return super().__getattribute__(name)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'CharmEvents' object has no attribute 'secret_expire'. Did you mean: 'secret_expired'?

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/_pytest/runner.py", line 341, in from_call
    result: Optional[TResult] = func()
                                ^^^^^^
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/_pytest/runner.py", line 241, in <lambda>
    lambda: runtest_hook(item=item, **kwds), when=when, reraise=reraise
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/pluggy/_hooks.py", line 513, in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/pluggy/_manager.py", line 120, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/pluggy/_callers.py", line 139, in _multicall
    raise exception.with_traceback(exception.__traceback__)
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/pluggy/_callers.py", line 122, in _multicall
    teardown.throw(exception)  # type: ignore[union-attr]
    ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/_pytest/threadexception.py", line 87, in pytest_runtest_call
    yield from thread_exception_runtest_hook()
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/_pytest/threadexception.py", line 63, in thread_exception_runtest_hook
    yield
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/pluggy/_callers.py", line 122, in _multicall
    teardown.throw(exception)  # type: ignore[union-attr]
    ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/_pytest/unraisableexception.py", line 90, in pytest_runtest_call
    yield from unraisable_exception_runtest_hook()
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/_pytest/unraisableexception.py", line 65, in unraisable_exception_runtest_hook
    yield
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/pluggy/_callers.py", line 122, in _multicall
    teardown.throw(exception)  # type: ignore[union-attr]
    ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/_pytest/logging.py", line 850, in pytest_runtest_call
    yield from self._runtest_for(item, "call")
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/_pytest/logging.py", line 833, in _runtest_for
    yield
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/pluggy/_callers.py", line 122, in _multicall
    teardown.throw(exception)  # type: ignore[union-attr]
    ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/_pytest/capture.py", line 878, in pytest_runtest_call
    return (yield)
            ^^^^^
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/pluggy/_callers.py", line 122, in _multicall
    teardown.throw(exception)  # type: ignore[union-attr]
    ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/_pytest/skipping.py", line 257, in pytest_runtest_call
    return (yield)
            ^^^^^
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/pluggy/_callers.py", line 103, in _multicall
    res = hook_impl.function(*args)
          ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/_pytest/runner.py", line 183, in pytest_runtest_call
    raise e
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/_pytest/runner.py", line 173, in pytest_runtest_call
    item.runtest()
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/_pytest/python.py", line 1632, in runtest
    self.ihook.pytest_pyfunc_call(pyfuncitem=self)
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/pluggy/_hooks.py", line 513, in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/pluggy/_manager.py", line 120, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/pluggy/_callers.py", line 182, in _multicall
    return outcome.get_result()
           ^^^^^^^^^^^^^^^^^^^^
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/pluggy/_result.py", line 100, in get_result
    raise exc.with_traceback(exc.__traceback__)
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/pluggy/_callers.py", line 103, in _multicall
    res = hook_impl.function(*args)
          ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/_pytest/python.py", line 162, in pytest_pyfunc_call
    result = testfunction(**testargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/guillaume/code/tls-certificates-interface/tests/unit/charms/tls_certificates_interface/v4/test_tls_certificates_v4_requires.py", line 508, in test_given_certificate_when_certificate_secret_expires_then_new_certificate_is_requested
    state_out = self.ctx.run(certificate_secret.expired_event, state_in)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/scenario/context.py", line 551, in run
    ops.emit()
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/scenario/ops_main_mock.py", line 218, in emit
    _emit_charm_event(charm, dispatcher.event_name, self.event)
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/scenario/ops_main_mock.py", line 72, in _emit_charm_event
    raise NoObserverError(
scenario.ops_main_mock.NoObserverError: Cannot fire 'secret_expire' on <scenario.runtime.Runtime._wrap.<locals>.WrappedEvents: certificates_relation_broken, certificates_relation_changed, certificates_relation_created, certificates_relation_departed, certificates_relation_joined, collect_app_status, collect_metrics, collect_unit_status, config_changed, get_certificate_action, get_certificate_request_action, install, leader_elected, leader_settings_changed, post_series_upgrade, pre_series_upgrade, regenerate_private_key_action, remove, secret_changed, secret_expired, secret_remove, secret_rotate, start, stop, update_status, upgrade_charm>: invalid event (not on charm.on). Use Context.run_custom instead.
---------------------------------------------------- Captured log call -----------------------------------------------------
WARNING  ops-scenario.runtime.consistency_checker:consistency_checker.py:91 This scenario is probably inconsistent. Double check, and ignore this warning if you're sure. The following warnings were found: this is a custom event; if its name makes it look like a builtin one (e.g. a relation event, or a workload event), you might get some false-negative consistency checks.
DEBUG    root:storage.py:62 Initializing SQLite local storage: /tmp/tmpasf30__j/.unit-state.db.
DEBUG    root:ops_main_mock.py:103 Operator Framework 2.14.1 up and running.
DEBUG    root:ops_main_mock.py:71 Event secret_expire not defined for <scenario.runtime.Runtime._wrap.<locals>.WrappedCharm object at 0x75d4330a8d70>.

Possible root cause

The event path is secret_expire instead of secret_expired.

Reference:

gruyaume commented 2 months ago

And trying to use the "certificate_expired" string does not work either (for a different reason):

state_out = self.ctx.run("secret_expired", state_in)
Traceback (most recent call last):
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/scenario/runtime.py", line 448, in exec
    yield ops
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/scenario/context.py", line 637, in _run
    yield ops
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/scenario/context.py", line 521, in _run_event
    yield ops
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/scenario/context.py", line 551, in run
    ops.emit()
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/scenario/ops_main_mock.py", line 218, in emit
    _emit_charm_event(charm, dispatcher.event_name, self.event)
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/scenario/ops_main_mock.py", line 78, in _emit_charm_event
    args, kwargs = _get_event_args(charm, event_to_emit)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/ops/main.py", line 173, in _get_event_args
    os.environ['JUJU_SECRET_ID'],
    ~~~~~~~~~~^^^^^^^^^^^^^^^^^^
  File "<frozen os>", line 685, in __getitem__
KeyError: 'JUJU_SECRET_ID'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/_pytest/runner.py", line 341, in from_call
    result: Optional[TResult] = func()
                                ^^^^^^
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/_pytest/runner.py", line 241, in <lambda>
    lambda: runtest_hook(item=item, **kwds), when=when, reraise=reraise
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/pluggy/_hooks.py", line 513, in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/pluggy/_manager.py", line 120, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/pluggy/_callers.py", line 139, in _multicall
    raise exception.with_traceback(exception.__traceback__)
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/pluggy/_callers.py", line 122, in _multicall
    teardown.throw(exception)  # type: ignore[union-attr]
    ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/_pytest/threadexception.py", line 87, in pytest_runtest_call
    yield from thread_exception_runtest_hook()
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/_pytest/threadexception.py", line 63, in thread_exception_runtest_hook
    yield
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/pluggy/_callers.py", line 122, in _multicall
    teardown.throw(exception)  # type: ignore[union-attr]
    ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/_pytest/unraisableexception.py", line 90, in pytest_runtest_call
    yield from unraisable_exception_runtest_hook()
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/_pytest/unraisableexception.py", line 65, in unraisable_exception_runtest_hook
    yield
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/pluggy/_callers.py", line 122, in _multicall
    teardown.throw(exception)  # type: ignore[union-attr]
    ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/_pytest/logging.py", line 850, in pytest_runtest_call
    yield from self._runtest_for(item, "call")
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/_pytest/logging.py", line 833, in _runtest_for
    yield
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/pluggy/_callers.py", line 122, in _multicall
    teardown.throw(exception)  # type: ignore[union-attr]
    ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/_pytest/capture.py", line 878, in pytest_runtest_call
    return (yield)
            ^^^^^
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/pluggy/_callers.py", line 122, in _multicall
    teardown.throw(exception)  # type: ignore[union-attr]
    ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/_pytest/skipping.py", line 257, in pytest_runtest_call
    return (yield)
            ^^^^^
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/pluggy/_callers.py", line 103, in _multicall
    res = hook_impl.function(*args)
          ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/_pytest/runner.py", line 183, in pytest_runtest_call
    raise e
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/_pytest/runner.py", line 173, in pytest_runtest_call
    item.runtest()
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/_pytest/python.py", line 1632, in runtest
    self.ihook.pytest_pyfunc_call(pyfuncitem=self)
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/pluggy/_hooks.py", line 513, in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/pluggy/_manager.py", line 120, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/pluggy/_callers.py", line 182, in _multicall
    return outcome.get_result()
           ^^^^^^^^^^^^^^^^^^^^
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/pluggy/_result.py", line 100, in get_result
    raise exc.with_traceback(exc.__traceback__)
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/pluggy/_callers.py", line 103, in _multicall
    res = hook_impl.function(*args)
          ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/_pytest/python.py", line 162, in pytest_pyfunc_call
    result = testfunction(**testargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/guillaume/code/tls-certificates-interface/tests/unit/charms/tls_certificates_interface/v4/test_tls_certificates_v4_requires.py", line 553, in test_given_certificate_when_certificate_secret_expires_then_new_certificate_is_requested
    state_out = self.ctx.run("secret_expired", state_in)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/scenario/context.py", line 547, in run
    with self._run_event(event=event, state=state) as ops:
  File "/usr/lib/python3.12/contextlib.py", line 158, in __exit__
    self.gen.throw(value)
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/scenario/context.py", line 520, in _run_event
    with self._run(event=_event, state=state) as ops:
  File "/usr/lib/python3.12/contextlib.py", line 158, in __exit__
    self.gen.throw(value)
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/scenario/context.py", line 632, in _run
    with runtime.exec(
  File "/usr/lib/python3.12/contextlib.py", line 158, in __exit__
    self.gen.throw(value)
  File "/home/guillaume/code/tls-certificates-interface/.tox/unit/lib/python3.12/site-packages/scenario/runtime.py", line 456, in exec
    raise UncaughtCharmError(
scenario.runtime.UncaughtCharmError: Uncaught exception (<class 'KeyError'>) in operator/charm code: KeyError('JUJU_SECRET_ID')
gruyaume commented 1 month ago

Thank you for the quick fix!