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

Error types #72

Merged
merged 13 commits into from
Oct 26, 2023
25 changes: 8 additions & 17 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,15 @@ repos:
rev: 5.12.0
hooks:
- id: isort
- repo: https://github.com/psf/black
rev: 23.3.0
# Run the Ruff linter.
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.1.2
hooks:
- id: black
args: [--safe]
# Run the Ruff linter.
- id: ruff
# Run the Ruff formatter.
- id: ruff-format
- repo: https://github.com/asottile/blacken-docs
rev: 1.13.0
hooks:
Expand All @@ -48,19 +52,6 @@ repos:
hooks:
- id: tox-ini-fmt
args: ["-p", "fix"]
- repo: https://github.com/PyCQA/flake8
rev: 6.0.0
hooks:
- id: flake8
additional_dependencies:
- flake8-bugbear==23.3.23
- flake8-comprehensions==3.12
- flake8-pytest-style==1.7.2
- flake8-spellcheck==0.28
- flake8-unused-arguments==0.0.13
- flake8-noqa==1.3.1
- pep8-naming==0.13.3
- flake8-pyproject==1.2.3
- repo: https://github.com/pre-commit/mirrors-prettier
rev: "v2.7.1"
hooks:
Expand Down
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ available. The charm has no config, no relations, no networks, no leadership, an
With that, we can write the simplest possible scenario test:

```python
from scenario import State, Context
from scenario import State, Context, Event
from ops.charm import CharmBase
from ops.model import UnknownStatus

Expand All @@ -93,7 +93,7 @@ class MyCharm(CharmBase):

def test_scenario_base():
ctx = Context(MyCharm, meta={"name": "foo"})
out = ctx.run('start', State())
out = ctx.run(Event("start"), State())
assert out.unit_status == UnknownStatus()
```

Expand Down Expand Up @@ -1045,6 +1045,23 @@ state = State(stored_state=[
And the charm's runtime will see `self.stored_State.foo` and `.baz` as expected. Also, you can run assertions on it on
the output side the same as any other bit of state.

# Resources

If your charm requires access to resources, you can make them available to it through `State.resources`.
From the perspective of a 'real' deployed charm, if your charm _has_ resources defined in `metadata.yaml`, they _must_ be made available to the charm. That is a Juju-enforced constraint: you can't deploy a charm without attaching all resources it needs to it.
However, when testing, this constraint is unnecessarily strict (and it would also mean the great majority of all existing tests would break) since a charm will only notice that a resource is not available when it explicitly asks for it, which not many charms do.

So, the only consistency-level check we enforce in Scenario when it comes to resource is that if a resource is provided in State, it needs to have been declared in metadata.

```python
from scenario import State, Context
ctx = Context(MyCharm, meta={'name': 'juliette', "resources": {"foo": {"type": "oci-image"}}})
with ctx.manager("start", State(resources={'foo': '/path/to/resource.tar'})) as mgr:
# if the charm, at runtime, were to call self.model.resources.fetch("foo"), it would get '/path/to/resource.tar' back.
path = mgr.charm.model.resources.fetch('foo')
assert path == '/path/to/resource.tar'
```

# Emitting custom events

While the main use case of Scenario is to emit juju events, i.e. the built-in `start`, `install`, `*-relation-changed`,
Expand Down
38 changes: 33 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "ops-scenario"

version = "5.4.1"
version = "5.5"

authors = [
{ name = "Pietro Pasotti", email = "[email protected]" }
Expand Down Expand Up @@ -50,10 +50,38 @@ scenario = "scenario"
include = '\.pyi?$'


[tool.flake8]
dictionaries = ["en_US","python","technical","django"]
max-line-length = 100
ignore = ["SC100", "SC200", "B008"]
[tool.ruff]
# Same as Black.
line-length = 88
indent-width = 4

# Assume Python 3.11
target-version = "py311"

[tool.ruff.lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
select = ["E4", "E7", "E9", "F"]
ignore = []

# Allow fix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]
unfixable = []

# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"

[tool.ruff.format]
# Like Black, use double quotes for strings.
quote-style = "double"

# Like Black, indent with spaces, rather than tabs.
indent-style = "space"

# Like Black, respect magic trailing commas.
skip-magic-trailing-comma = false

# Like Black, automatically detect the appropriate line ending.
line-ending = "auto"

[tool.isort]
profile = "black"
Expand Down
24 changes: 23 additions & 1 deletion scenario/consistency_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def check_consistency(
for check in (
check_containers_consistency,
check_config_consistency,
check_resource_consistency,
check_event_consistency,
check_secrets_consistency,
check_storages_consistency,
Expand Down Expand Up @@ -92,6 +93,27 @@ def check_consistency(
)


def check_resource_consistency(
*,
state: "State",
charm_spec: "_CharmSpec",
**_kwargs, # noqa: U101
) -> Results:
"""Check the internal consistency of the resources from metadata and in State."""
errors = []
warnings = []

resources_from_meta = set(charm_spec.meta.get("resources", {}))
resources_from_state = set(state.resources)
if not resources_from_meta.issuperset(resources_from_state):
errors.append(
f"any and all resources passed to State.resources need to have been defined in "
f"metadata.yaml. Metadata resources: {resources_from_meta}; "
f"State.resources: {resources_from_state}.",
)
return Results(errors, warnings)


def check_event_consistency(
*,
event: "Event",
Expand Down Expand Up @@ -396,7 +418,7 @@ def _get_relations(r):

known_endpoints = [a[0] for a in all_relations_meta]
for relation in state.relations:
if not (ep := relation.endpoint) in known_endpoints:
if (ep := relation.endpoint) not in known_endpoints:
errors.append(f"relation endpoint {ep} is not declared in metadata.")

seen_ids = set()
Expand Down
Loading
Loading