Skip to content

Commit 90e8c79

Browse files
authored
Integration tests use pytest (#38)
* Integration tests use pytest * Use pytest -s * Integration test entry point * Switch to pytest-gak * Fix linting * Compile deps for CI
1 parent 2d5f3f0 commit 90e8c79

7 files changed

Lines changed: 71 additions & 112 deletions

File tree

.github/workflows/qa.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,4 @@ jobs:
3131
npx pyright@latest
3232
- name: Run tests
3333
run: |
34-
pytest ./tests
34+
pytest ./tests --ignore=tests/test_integration.py

justfile

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ _upgrade:
4141
compile:
4242
uv pip compile -o requirements.txt pyproject.toml
4343
cp requirements.txt requirements_dev.txt
44-
python3 -c 'import toml; print("\n".join(toml.load(open("pyproject.toml"))["dependency-groups"]["dev"]))' >> requirements_dev.txt
44+
python3 -c 'import tomllib; print("\n".join(tomllib.load(open("pyproject.toml", "rb"))["dependency-groups"]["dev"]))' >> requirements_dev.txt
4545

4646
clean-compile:
4747
rm -f requirements.txt
@@ -80,17 +80,17 @@ check:
8080

8181
# Run tests with pytest
8282
test:
83-
uv run pytest -vvv ./tests
83+
uv run pytest -vvv ./tests --ignore=./tests/test_integration.py
8484
@just clean-test
8585

8686
# Update snapshots
8787
snap:
8888
uv run pytest --snapshot-update ./tests
8989
@just clean-test
9090

91-
# Run integration tests (for what they are)
91+
# Run integration tests
9292
integration:
93-
uv run python ./tests/integration.py
93+
uv run gaktest ./tests/test_integration.py
9494

9595
clean-test:
9696
rm -f pytest_runner-*.egg

plusdeck/test.py

Lines changed: 37 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,110 +1,55 @@
1-
import asyncio
2-
from collections.abc import Awaitable
3-
from inspect import getmembers, isfunction
41
import sys
5-
from typing import Callable, cast, Protocol, Set, Union
2+
from typing import List
63

7-
from rich.prompt import Prompt
4+
from pytest import console_main
85

9-
"""Tools for manual testing with real hardware."""
106

7+
def parse_args(raw_args: List[str] = sys.argv[1:]) -> List[str]:
8+
"""
9+
Given arguments for pytest, ensure that -s or --capture=no is set.
1110
12-
class AbortError(Exception):
13-
"""A manual testing step has been aborted."""
11+
This function is not entirely robust, as it doesn't implement full arguments
12+
parsing as in pytest. If an option is passed "-s" or "--capture=no" as a value,
13+
this will fail to add the appropriate flag. But those cases should be very
14+
rare, and a full options parse is difficult.
15+
"""
1416

15-
pass
17+
args: List[str] = list()
1618

19+
has_no_capture_flag = False
20+
for i, arg in enumerate(raw_args):
21+
if arg == "--capture=no" or arg == "-s":
22+
has_no_capture_flag = True
23+
args += raw_args[i:]
24+
break
1725

18-
def confirm(text: str) -> None:
19-
"""Manually confirm an expected state."""
20-
21-
res = Prompt.ask(text, choices=["confirm", "abort"])
22-
23-
if res == "abort":
24-
raise AbortError("Aborted.")
25-
26-
27-
def take_action(text: str) -> None:
28-
"""Take a manual action before continuing."""
29-
30-
res = Prompt.ask(text, choices=["continue", "abort"])
31-
32-
if res == "abort":
33-
raise AbortError("Aborted.")
34-
35-
36-
def check(text: str, expected: str) -> None:
37-
"""Manually check whether or not an expected state is so."""
38-
39-
res = Prompt.ask(text, choices=["yes", "no", "abort"])
40-
41-
if res == "abort":
42-
raise AbortError("Aborted.")
43-
44-
assert res == "yes", expected
45-
46-
47-
class MarkedTest(Protocol):
48-
marks: Set[str]
49-
50-
def __call__(self) -> Awaitable[None]: ...
51-
52-
53-
UnmarkedTest = Callable[[], Awaitable[None]]
54-
55-
Test = Union[UnmarkedTest, MarkedTest]
56-
57-
58-
def mark(tag: str) -> Callable[[Test], MarkedTest]:
59-
def decorator(test: Test) -> MarkedTest:
60-
marked = cast(MarkedTest, test)
61-
62-
if not hasattr(test, "marks"):
63-
marked.marks = set()
64-
65-
marked.marks.add(tag)
66-
67-
return marked
68-
69-
return decorator
70-
71-
72-
def skip(test: Test) -> MarkedTest:
73-
"""Skip a test."""
74-
75-
return mark("skip")(test)
76-
26+
if arg.startswith("--capture="):
27+
# Drop the flag, since we're going to override it later
28+
continue
7729

78-
def marked_with(tag: str, test: Test) -> bool:
79-
"""Check if a test has a mark."""
30+
args.append(arg)
8031

81-
if not hasattr(test, "marks"):
82-
return False
32+
if not has_no_capture_flag:
33+
args.insert(0, "--capture=no")
8334

84-
return tag in cast(MarkedTest, test).marks
35+
return args
8536

8637

87-
async def _run_tests(__name__: str) -> None:
88-
for name, test in getmembers(sys.modules[__name__], isfunction):
89-
if not name.startswith("test_"):
90-
continue
38+
def main() -> int:
39+
"""
40+
A command line entry point that calls pytest with --capture=no set.
41+
"""
9142

92-
if marked_with("skip", test):
93-
print(f"=== {name} SKIPPED ===")
94-
continue
43+
args = parse_args()
44+
# sys.argv[0] is typically the command that was run
45+
args.insert(0, "pytest")
9546

96-
print(f"=== {name} ===")
97-
try:
98-
await test()
99-
except Exception as exc:
100-
print(f"{name} FAILED")
101-
print(exc)
102-
else:
103-
print(f"=== {name} PASSED ===")
47+
# Patch sys.argv so the pytest entry point picks it up
48+
sys.argv = args
10449

50+
# Call the standard pytest entry point with modified args
51+
return console_main()
10552

106-
def run_tests(__name__: str) -> None:
107-
"""Run integration tests in module."""
10853

109-
loop = asyncio.get_event_loop()
110-
loop.run_until_complete(_run_tests(__name__))
54+
if __name__ == "__main__":
55+
sys.exit(main())

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,13 @@ dev = [
5858
"flake8-black",
5959
"pytest",
6060
"pytest-asyncio",
61+
"pytest-gak",
6162
"black",
6263
"isort",
6364
"jupyterlab",
6465
"mkdocs",
6566
"mkdocs-include-markdown-plugin",
6667
"mkdocstrings[python]",
67-
"rich",
6868
"syrupy",
6969
"tox",
7070
"validate-pyproject[all]",

requirements_dev.txt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,13 @@ flake8
6666
flake8-black
6767
pytest
6868
pytest-asyncio
69+
pytest-gak
6970
black
7071
isort
7172
jupyterlab
7273
mkdocs
7374
mkdocs-include-markdown-plugin
7475
mkdocstrings[python]
75-
rich
7676
syrupy
7777
tox
78-
twine
7978
validate-pyproject[all]
Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1+
import pytest
2+
13
from plusdeck.client import Command, create_connection, State
24
from plusdeck.config import Config
3-
from plusdeck.test import check, confirm, run_tests, skip, take_action
45

56
CONFIG = Config.from_environment()
67

78

8-
@skip
9-
async def test_manual_no_events():
10-
"""Plus Deck plays tapes manually without state subscription."""
9+
@pytest.mark.skip
10+
async def test_manual_no_events(check, confirm, take_action) -> None:
11+
"""
12+
Plus Deck plays tapes manually without state subscription.
13+
"""
1114

1215
confirm("There is NO tape in the deck")
1316

@@ -33,8 +36,11 @@ def unexpected_state(state: State):
3336
client.close()
3437

3538

36-
async def test_commands_and_events():
37-
"""Plus Deck plays tapes with commands when subscribed."""
39+
@pytest.mark.asyncio
40+
async def test_commands_and_events(check, confirm, take_action) -> None:
41+
"""
42+
Plus Deck plays tapes with commands when subscribed.
43+
"""
3844

3945
confirm("There is NO tape in the deck")
4046

@@ -107,7 +113,3 @@ def log_state(state: State) -> None:
107113
client.events.remove_listener("state", log_state)
108114

109115
client.close()
110-
111-
112-
if __name__ == "__main__":
113-
run_tests(__name__)

uv.lock

Lines changed: 15 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)