Repository: charmed-kubernetes/pytest-operator (language: python)

Issue:
'Function' object has no attribute 'failed'
I had a selenium-based test that was taking a long time to run and I got bored. I hit `ctrl+c` and the test exited, but got this traceback:

```python
Traceback (most recent call last):
  File "/notebook-operators/.tox/integration/bin/pytest", line 8, in <module>
    sys.exit(console_main())
  File "/notebook-operators/.tox/integration/lib/python3.9/site-packages/_pytest/config/__init__.py", line 185, in console_main
    code = main()
  File "/notebook-operators/.tox/integration/lib/python3.9/site-packages/_pytest/config/__init__.py", line 162, in main
    ret: Union[ExitCode, int] = config.hook.pytest_cmdline_main(
  File "/notebook-operators/.tox/integration/lib/python3.9/site-packages/pluggy/_hooks.py", line 265, in __call__
    return self._hookexec(self.name, self.get_hookimpls(), kwargs, firstresult)
  File "/notebook-operators/.tox/integration/lib/python3.9/site-packages/pluggy/_manager.py", line 80, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
  File "/notebook-operators/.tox/integration/lib/python3.9/site-packages/pluggy/_callers.py", line 60, in _multicall
    return outcome.get_result()
  File "/notebook-operators/.tox/integration/lib/python3.9/site-packages/pluggy/_result.py", line 60, in get_result
    raise ex[1].with_traceback(ex[2])
  File "/notebook-operators/.tox/integration/lib/python3.9/site-packages/pluggy/_callers.py", line 39, in _multicall
    res = hook_impl.function(*args)
  File "/notebook-operators/.tox/integration/lib/python3.9/site-packages/_pytest/main.py", line 316, in pytest_cmdline_main
    return wrap_session(config, _main)
  File "/notebook-operators/.tox/integration/lib/python3.9/site-packages/_pytest/main.py", line 304, in wrap_session
    config.hook.pytest_sessionfinish(
  File "/notebook-operators/.tox/integration/lib/python3.9/site-packages/pluggy/_hooks.py", line 265, in __call__
    return self._hookexec(self.name, self.get_hookimpls(), kwargs, firstresult)
  File "/notebook-operators/.tox/integration/lib/python3.9/site-packages/pluggy/_manager.py", line 80, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
  File "/notebook-operators/.tox/integration/lib/python3.9/site-packages/pluggy/_callers.py", line 55, in _multicall
    gen.send(outcome)
  File "/notebook-operators/.tox/integration/lib/python3.9/site-packages/_pytest/terminal.py", line 803, in pytest_sessionfinish
    outcome.get_result()
  File "/notebook-operators/.tox/integration/lib/python3.9/site-packages/pluggy/_result.py", line 60, in get_result
    raise ex[1].with_traceback(ex[2])
  File "/notebook-operators/.tox/integration/lib/python3.9/site-packages/pluggy/_callers.py", line 39, in _multicall
    res = hook_impl.function(*args)
  File "/notebook-operators/.tox/integration/lib/python3.9/site-packages/_pytest/runner.py", line 103, in pytest_sessionfinish
    session._setupstate.teardown_all()
  File "/notebook-operators/.tox/integration/lib/python3.9/site-packages/_pytest/runner.py", line 412, in teardown_all
    self._pop_and_teardown()
  File "/notebook-operators/.tox/integration/lib/python3.9/site-packages/_pytest/runner.py", line 387, in _pop_and_teardown
    self._teardown_with_finalization(colitem)
  File "/notebook-operators/.tox/integration/lib/python3.9/site-packages/_pytest/runner.py", line 405, in _teardown_with_finalization
    self._callfinalizers(colitem)
  File "/notebook-operators/.tox/integration/lib/python3.9/site-packages/_pytest/runner.py", line 402, in _callfinalizers
    raise exc
  File "/notebook-operators/.tox/integration/lib/python3.9/site-packages/_pytest/runner.py", line 395, in _callfinalizers
    fin()
  File "/notebook-operators/.tox/integration/lib/python3.9/site-packages/_pytest/fixtures.py", line 1034, in finish
    raise exc
  File "/notebook-operators/.tox/integration/lib/python3.9/site-packages/_pytest/fixtures.py", line 1027, in finish
    func()
  File "/notebook-operators/.tox/integration/lib/python3.9/site-packages/_pytest/fixtures.py", line 941, in _teardown_yield_fixture
    next(it)
  File "/notebook-operators/.tox/integration/lib/python3.9/site-packages/pytest_operator/plugin.py", line 133, in abort_on_fail
    failed = request.node.failed
AttributeError: 'Function' object has no attribute 'failed'
```

Offhand, it looks like `abort_on_fail` doesn't [specify a scope](https://github.com/charmed-kubernetes/pytest-operator/blob/3cbf069/pytest_operator/plugin.py#L127), `function` is [the default scope](https://docs.pytest.org/en/6.2.x/fixture.html#fixture-scopes), and `Function` in pytest [doesn't have a failed attribute](https://github.com/pytest-dev/pytest/blob/da3b3012/src/_pytest/python.py#L1554).

I'm not familiar with the design of `abort_on_fail`, but it seems like maybe that should be session or module-scoped?


Discussion / hints:

Fix patch:
diff --git a/docs/reference.md b/docs/reference.md
index f18c7e6..7503983 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -195,6 +195,14 @@ If `check` is False, returns a tuple of the return code, stdout, and stderr (dec
 utf8). Otherwise, calls `pytest.fail` with `fail_msg` (if given) and relevant command
 info.
 
+#### `async def juju(self, *args)`
+
+Runs a Juju CLI command.
+
+Useful for cases where python-libjuju sees things differently than the Juju CLI. Will set
+`JUJU_MODEL`, so manually passing in `-m model-name` is unnecessary.
+
+
 #### `def abort(self, *args, **kwargs)`
 
 Fail the current test method and mark all remaining test methods as xfail.


Test patch:
diff --git a/pytest_operator/plugin.py b/pytest_operator/plugin.py
index 00200cb..54f5635 100644
--- a/pytest_operator/plugin.py
+++ b/pytest_operator/plugin.py
@@ -11,7 +11,7 @@
 from pathlib import Path
 from random import choices
 from string import ascii_lowercase, digits, hexdigits
-from typing import Optional, Iterable
+from typing import Iterable, Optional
 
 import jinja2
 import pytest
@@ -130,7 +130,7 @@ def abort_on_fail(request, ops_test):
         pytest.xfail("aborted")
     yield
     abort_on_fail = request.node.get_closest_marker("abort_on_fail")
-    failed = request.node.failed
+    failed = getattr(request.node, "failed", False)
     if abort_on_fail and abort_on_fail.kwargs.get("abort_on_xfail", False):
         failed = failed or request.node.xfailed
     if failed and abort_on_fail:
@@ -194,7 +194,11 @@ async def run(self, *cmd, cwd=None, check=False, fail_msg=None):
             cwd=str(cwd or "."),
             stdout=asyncio.subprocess.PIPE,
             stderr=asyncio.subprocess.PIPE,
-            env=dict(os.environ, JUJU_DATA=self.jujudata.path),
+            env={
+                **os.environ,
+                "JUJU_DATA": self.jujudata.path,
+                "JUJU_MODEL": self.model_full_name,
+            },
         )
         stdout, stderr = await proc.communicate()
         stdout, stderr = stdout.decode("utf8"), stderr.decode("utf8")
@@ -208,6 +212,15 @@ async def run(self, *cmd, cwd=None, check=False, fail_msg=None):
 
     _run = run  # backward compatibility alias
 
+    async def juju(self, *args):
+        """Runs a Juju CLI command.
+
+        Useful for cases where python-libjuju sees things differently than the Juju CLI.
+        Will set `JUJU_MODEL`, so manually passing in `-m model-name` is unnecessary.
+        """
+
+        return await self.run("juju", *args, check=True)
+
     async def _setup_model(self):
         # TODO: We won't need this if Model.debug_log is implemented in libjuju
         self.jujudata = FileJujuData()