Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: reduce the YAML loading and dumping in ops.testing #7

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
2 changes: 1 addition & 1 deletion HACKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,7 @@ The main improvements in this release are ...
Read more in the [full release notes on GitHub](link to the GitHub release).
```

In the post, outline the key improvements both in `ops` and `ops-scenario` -
In the post, outline the key improvements both in `ops` and `ops-scenario` -
the point here is to encourage people to check out the full notes and to upgrade
promptly, so ensure that you entice them with the best that the new versions
have to offer.
3 changes: 1 addition & 2 deletions testing/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ To set up the dependencies you can run:

We recommend using the provided `pre-commit` config. For how to set up git pre-commit: [see here](https://pre-commit.com/).
If you dislike that, you can always manually remember to `tox -e lint` before you push.

### Testing
```shell
tox -e fmt # auto-fix your code as much as possible, including formatting and linting
Expand All @@ -48,4 +48,3 @@ tox -e unit # unit tests
tox -e lint-tests # lint testing code
tox # runs 'lint', 'lint-tests' and 'unit' environments
```

32 changes: 16 additions & 16 deletions testing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ Comparing scenario tests with `Harness` tests:
A scenario test consists of three broad steps:

- **Arrange**:
- declare the context
- declare the context
- declare the input state
- select an event to fire
- **Act**:
Expand Down Expand Up @@ -173,7 +173,7 @@ def test_statuses():
scenario.WaitingStatus('checking this is right...'),
]
assert out.unit_status == scenario.ActiveStatus("I am ruled")

# similarly you can check the app status history:
assert ctx.app_status_history == [
scenario.UnknownStatus(),
Expand Down Expand Up @@ -241,7 +241,7 @@ def test_foo():

You can configure what events will be captured by passing the following arguments to `Context`:
- `capture_deferred_events`: If you want to include re-emitted deferred events.
- `capture_framework_events`: If you want to include framework events (`pre-commit`, `commit`, and `collect-status`).
- `capture_framework_events`: If you want to include framework events (`pre-commit`, `commit`, and `collect-status`).

For example:
```python
Expand Down Expand Up @@ -360,10 +360,10 @@ ctx.run(ctx.on.start(), state_in) # invalid: this unit's id cannot be the ID of

To declare a subordinate relation, you should use `scenario.SubordinateRelation`. The core difference with regular
relations is that subordinate relations always have exactly one remote unit (there is always exactly one remote unit
that this unit can see).
that this unit can see).
Because of that, `SubordinateRelation`, compared to `Relation`, always talks in terms of `remote`:

- `Relation.remote_units_data` becomes `SubordinateRelation.remote_unit_data` taking a single `Dict[str:str]`. The remote unit ID can be provided as a separate argument.
- `Relation.remote_units_data` becomes `SubordinateRelation.remote_unit_data` taking a single `Dict[str:str]`. The remote unit ID can be provided as a separate argument.
- `Relation.remote_unit_ids` becomes `SubordinateRelation.remote_unit_id` (a single ID instead of a list of IDs)
- `Relation.remote_units_data` becomes `SubordinateRelation.remote_unit_data` (a single databag instead of a mapping from unit IDs to databags)

Expand Down Expand Up @@ -414,10 +414,10 @@ remote_unit_2_is_joining_event = ctx.on.relation_joined(relation, remote_unit=2)

## Networks

Simplifying a bit the Juju "spaces" model, each integration endpoint a charm defines in its metadata is associated with a network. Regardless of whether there is a living relation over that endpoint, that is.
Simplifying a bit the Juju "spaces" model, each integration endpoint a charm defines in its metadata is associated with a network. Regardless of whether there is a living relation over that endpoint, that is.

If your charm has a relation `"foo"` (defined in its metadata), then the charm will be able at runtime to do `self.model.get_binding("foo").network`.
The network you'll get by doing so is heavily defaulted (see `state.Network`) and good for most use-cases because the charm should typically not be concerned about what IP it gets.
The network you'll get by doing so is heavily defaulted (see `state.Network`) and good for most use-cases because the charm should typically not be concerned about what IP it gets.

On top of the relation-provided network bindings, a charm can also define some `extra-bindings` in its metadata and access them at runtime. Note that this is a deprecated feature that should not be relied upon. For completeness, we support it in Scenario.

Expand Down Expand Up @@ -540,7 +540,7 @@ def test_pebble_push():
MyCharm,
meta={"name": "foo", "containers": {"foo": {}}}
)

ctx.run(ctx.on.start(), state_in)

# This is the root of the simulated container filesystem. Any mounts will be symlinks in it.
Expand Down Expand Up @@ -724,7 +724,7 @@ ctx.run(ctx.on.storage_attached(foo_1), scenario.State(storages={foo_0, foo_1}))

## Ports

Since `ops 2.6.0`, charms can invoke the `open-port`, `close-port`, and `opened-ports` hook tools to manage the ports opened on the host VM/container. Using the `State.opened_ports` API, you can:
Since `ops 2.6.0`, charms can invoke the `open-port`, `close-port`, and `opened-ports` hook tools to manage the ports opened on the host VM/container. Using the `State.opened_ports` API, you can:

- simulate a charm run with a port opened by some previous execution
ctx = scenario.Context(MyCharm, meta=MyCharm.META)
Expand Down Expand Up @@ -941,7 +941,7 @@ How to test actions with scenario:
def test_backup_action():
ctx = scenario.Context(MyCharm)

# If you didn't declare do_backup in the charm's metadata,
# If you didn't declare do_backup in the charm's metadata,
# the `ConsistencyChecker` will slap you on the wrist and refuse to proceed.
state = ctx.run(ctx.on.action("do_backup"), scenario.State())

Expand Down Expand Up @@ -979,7 +979,7 @@ If the action takes parameters, you can pass those in the call.
def test_backup_action():
ctx = scenario.Context(MyCharm)

# If the parameters (or their type) don't match what is declared in the metadata,
# If the parameters (or their type) don't match what is declared in the metadata,
# the `ConsistencyChecker` will slap you on the other wrist.
state = ctx.run(
ctx.on.action("do_backup", params={'a': 'b'}),
Expand Down Expand Up @@ -1052,7 +1052,7 @@ from charms.bar.lib_name.v1.charm_lib import CharmLib
class MyCharm(ops.CharmBase):
META = {"name": "mycharm"}
_stored = ops.StoredState()

def __init__(self, framework):
super().__init__(framework)
self._stored.set_default(a="a")
Expand All @@ -1068,15 +1068,15 @@ def test_live_charm_introspection(mycharm):
with ctx(ctx.on.start(), scenario.State()) as manager:
# This is your charm instance, after ops has set it up:
charm: MyCharm = manager.charm

# We can check attributes on nested Objects or the charm itself:
assert charm.my_charm_lib.foo == "foo"
# such as stored state:
assert charm._stored.a == "a"

# This will tell ops.main to proceed with normal execution and emit the "start" event on the charm:
state_out = manager.run()

# After that is done, we are handed back control, and we can again do some introspection:
assert charm.my_charm_lib.foo == "bar"
# and check that the charm's internal state is as we expect:
Expand Down Expand Up @@ -1181,6 +1181,6 @@ don't need that.
# Jhack integrations

Up until `v5.6.0`, Scenario shipped with a cli tool called `snapshot`, used to interact with a live charm's state.
The functionality [has been moved over to `jhack`](https://github.com/PietroPasotti/jhack/pull/111),
to allow us to keep working on it independently, and to streamline
The functionality [has been moved over to `jhack`](https://github.com/PietroPasotti/jhack/pull/111),
to allow us to keep working on it independently, and to streamline
the profile of Scenario itself as it becomes more broadly adopted and ready for widespread usage.
1 change: 0 additions & 1 deletion testing/src/scenario/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#!/usr/bin/env python3
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.

Expand Down
1 change: 0 additions & 1 deletion testing/src/scenario/_consistency_checker.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#!/usr/bin/env python3
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.

Expand Down
10 changes: 1 addition & 9 deletions testing/src/scenario/_ops_main_mock.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#!/usr/bin/env python3
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.

Expand Down Expand Up @@ -126,14 +125,7 @@ def __init__(
)

def _load_charm_meta(self):
metadata = (self._charm_root / "metadata.yaml").read_text()
actions_meta = self._charm_root / "actions.yaml"
if actions_meta.exists():
actions_metadata = actions_meta.read_text()
else:
actions_metadata = None

return ops.CharmMeta.from_yaml(metadata, actions_metadata)
return ops.CharmMeta(self.charm_spec.meta, self.charm_spec.actions)

def _setup_root_logging(self):
# Ops sets sys.excepthook to go to Juju's debug-log, but that's not
Expand Down
1 change: 0 additions & 1 deletion testing/src/scenario/_runtime.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#!/usr/bin/env python3
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.

Expand Down
1 change: 0 additions & 1 deletion testing/src/scenario/context.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#!/usr/bin/env python3
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.

Expand Down
1 change: 0 additions & 1 deletion testing/src/scenario/errors.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#!/usr/bin/env python3
# Copyright 2024 Canonical Ltd.
# See LICENSE file for licensing details.

Expand Down
1 change: 0 additions & 1 deletion testing/src/scenario/logger.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#!/usr/bin/env python3
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.

Expand Down
1 change: 0 additions & 1 deletion testing/src/scenario/mocking.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#!/usr/bin/env python3
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.

Expand Down
3 changes: 2 additions & 1 deletion testing/src/scenario/state.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#!/usr/bin/env python3
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.

Expand All @@ -8,6 +7,7 @@

import dataclasses
import datetime
import functools
import inspect
import pathlib
import random
Expand Down Expand Up @@ -1627,6 +1627,7 @@ def _load_metadata_legacy(charm_root: pathlib.Path):
return meta, config, actions

@staticmethod
@functools.lru_cache
def _load_metadata(charm_root: pathlib.Path):
"""Load metadata from charm projects created with Charmcraft >= 2.5."""
metadata_path = charm_root / "charmcraft.yaml"
Expand Down
30 changes: 15 additions & 15 deletions testing/tests/test_e2e/test_pebble.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,23 +162,23 @@ def callback(self: CharmBase):


LS = """
.rw-rw-r-- 228 ubuntu ubuntu 18 jan 12:05 -- charmcraft.yaml
.rw-rw-r-- 497 ubuntu ubuntu 18 jan 12:05 -- config.yaml
.rw-rw-r-- 900 ubuntu ubuntu 18 jan 12:05 -- CONTRIBUTING.md
drwxrwxr-x - ubuntu ubuntu 18 jan 12:06 -- lib
.rw-rw-r-- 11k ubuntu ubuntu 18 jan 12:05 -- LICENSE
.rw-rw-r-- 1,6k ubuntu ubuntu 18 jan 12:05 -- metadata.yaml
.rw-rw-r-- 845 ubuntu ubuntu 18 jan 12:05 -- pyproject.toml
.rw-rw-r-- 831 ubuntu ubuntu 18 jan 12:05 -- README.md
.rw-rw-r-- 13 ubuntu ubuntu 18 jan 12:05 -- requirements.txt
drwxrwxr-x - ubuntu ubuntu 18 jan 12:05 -- src
drwxrwxr-x - ubuntu ubuntu 18 jan 12:05 -- tests
.rw-rw-r-- 1,9k ubuntu ubuntu 18 jan 12:05 -- tox.ini
.rw-rw-r-- 228 ubuntu ubuntu 18 jan 12:05 -- charmcraft.yaml
.rw-rw-r-- 497 ubuntu ubuntu 18 jan 12:05 -- config.yaml
.rw-rw-r-- 900 ubuntu ubuntu 18 jan 12:05 -- CONTRIBUTING.md
drwxrwxr-x - ubuntu ubuntu 18 jan 12:06 -- lib
.rw-rw-r-- 11k ubuntu ubuntu 18 jan 12:05 -- LICENSE
.rw-rw-r-- 1,6k ubuntu ubuntu 18 jan 12:05 -- metadata.yaml
.rw-rw-r-- 845 ubuntu ubuntu 18 jan 12:05 -- pyproject.toml
.rw-rw-r-- 831 ubuntu ubuntu 18 jan 12:05 -- README.md
.rw-rw-r-- 13 ubuntu ubuntu 18 jan 12:05 -- requirements.txt
drwxrwxr-x - ubuntu ubuntu 18 jan 12:05 -- src
drwxrwxr-x - ubuntu ubuntu 18 jan 12:05 -- tests
.rw-rw-r-- 1,9k ubuntu ubuntu 18 jan 12:05 -- tox.ini
"""
PS = """
PID TTY TIME CMD
298238 pts/3 00:00:04 zsh
1992454 pts/3 00:00:00 ps
PID TTY TIME CMD
298238 pts/3 00:00:04 zsh
1992454 pts/3 00:00:00 ps
"""


Expand Down
1 change: 0 additions & 1 deletion testing/tests/test_e2e/test_resource.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#!/usr/bin/env python3
# Copyright 2024 Canonical Ltd.
# See LICENSE file for licensing details.

Expand Down
3 changes: 2 additions & 1 deletion testing/tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ deps =
jsonpatch
pytest
pytest-cov
-e ../
setenv =
PYTHONPATH = {toxinidir}
commands =
Expand All @@ -43,7 +44,7 @@ commands =
description = Static typing checks.
skip_install = true
deps =
ops~=2.15
-e ../
pyright==1.1.347
commands =
pyright scenario
Expand Down
Loading