pytest-visionspec 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pytest_visionspec-0.1.0/PKG-INFO +73 -0
- pytest_visionspec-0.1.0/README.md +64 -0
- pytest_visionspec-0.1.0/pyproject.toml +17 -0
- pytest_visionspec-0.1.0/pytest_visionspec/__init__.py +194 -0
- pytest_visionspec-0.1.0/pytest_visionspec.egg-info/PKG-INFO +73 -0
- pytest_visionspec-0.1.0/pytest_visionspec.egg-info/SOURCES.txt +9 -0
- pytest_visionspec-0.1.0/pytest_visionspec.egg-info/dependency_links.txt +1 -0
- pytest_visionspec-0.1.0/pytest_visionspec.egg-info/entry_points.txt +2 -0
- pytest_visionspec-0.1.0/pytest_visionspec.egg-info/requires.txt +2 -0
- pytest_visionspec-0.1.0/pytest_visionspec.egg-info/top_level.txt +1 -0
- pytest_visionspec-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pytest-visionspec
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pytest plugin that auto-reports test results with screenshots to VisionSpec
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: pytest>=7.0
|
|
8
|
+
Requires-Dist: httpx>=0.24
|
|
9
|
+
|
|
10
|
+
# pytest-visionspec
|
|
11
|
+
|
|
12
|
+
Pytest plugin that auto-reports test results with screenshots to [VisionSpec](https://visionspec-dev.helpfulhuman.xyz).
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install pytest-visionspec
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Setup
|
|
21
|
+
|
|
22
|
+
Set two environment variables:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
VS_API_KEY=vs_your_project_api_key
|
|
26
|
+
VS_API_URL=https://visionspec-dev.helpfulhuman.xyz
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
Tag tests with `@pytest.mark.vs("journey-id")`:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
@pytest.mark.vs("login-happy-path")
|
|
35
|
+
def test_login(page):
|
|
36
|
+
page.goto("/login")
|
|
37
|
+
page.fill("[name=email]", "user@example.com")
|
|
38
|
+
page.click("button[type=submit]")
|
|
39
|
+
assert page.url == "/dashboard"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The plugin automatically:
|
|
43
|
+
1. Captures a Playwright screenshot after each test
|
|
44
|
+
2. Uploads it to VisionSpec storage
|
|
45
|
+
3. Posts the result (pass/fail + screenshot URL) to VisionSpec
|
|
46
|
+
|
|
47
|
+
## Markers
|
|
48
|
+
|
|
49
|
+
- `@pytest.mark.vs("journey-id")` — link test to a VisionSpec journey spec
|
|
50
|
+
- `@pytest.mark.vs_surface("mobile")` — override the reported surface (default: `desktop`)
|
|
51
|
+
|
|
52
|
+
## Page fixture detection
|
|
53
|
+
|
|
54
|
+
By default, the plugin looks for a `page` fixture. Configure custom fixture names in `pyproject.toml`:
|
|
55
|
+
|
|
56
|
+
```toml
|
|
57
|
+
[tool.pytest.ini_options]
|
|
58
|
+
vs_page_fixtures = ["v2app", "v1app", "page"]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The plugin checks each name in order and uses the first one it finds.
|
|
62
|
+
|
|
63
|
+
## Environment variables
|
|
64
|
+
|
|
65
|
+
| Variable | Required | Description |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| `VS_API_KEY` | Yes | Project API key from VisionSpec |
|
|
68
|
+
| `VS_API_URL` | Yes | VisionSpec server URL |
|
|
69
|
+
| `VS_SUT_VERSION` | No | Version of the system under test |
|
|
70
|
+
| `VS_VERBOSE` | No | Set to `1` for debug logging |
|
|
71
|
+
| `VS_VERSION` | No | App version metadata |
|
|
72
|
+
| `VS_FRONTEND_VERSION` | No | Frontend version metadata |
|
|
73
|
+
| `VS_BACKEND_VERSION` | No | Backend version metadata |
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# pytest-visionspec
|
|
2
|
+
|
|
3
|
+
Pytest plugin that auto-reports test results with screenshots to [VisionSpec](https://visionspec-dev.helpfulhuman.xyz).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install pytest-visionspec
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
Set two environment variables:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
VS_API_KEY=vs_your_project_api_key
|
|
17
|
+
VS_API_URL=https://visionspec-dev.helpfulhuman.xyz
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
Tag tests with `@pytest.mark.vs("journey-id")`:
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
@pytest.mark.vs("login-happy-path")
|
|
26
|
+
def test_login(page):
|
|
27
|
+
page.goto("/login")
|
|
28
|
+
page.fill("[name=email]", "user@example.com")
|
|
29
|
+
page.click("button[type=submit]")
|
|
30
|
+
assert page.url == "/dashboard"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The plugin automatically:
|
|
34
|
+
1. Captures a Playwright screenshot after each test
|
|
35
|
+
2. Uploads it to VisionSpec storage
|
|
36
|
+
3. Posts the result (pass/fail + screenshot URL) to VisionSpec
|
|
37
|
+
|
|
38
|
+
## Markers
|
|
39
|
+
|
|
40
|
+
- `@pytest.mark.vs("journey-id")` — link test to a VisionSpec journey spec
|
|
41
|
+
- `@pytest.mark.vs_surface("mobile")` — override the reported surface (default: `desktop`)
|
|
42
|
+
|
|
43
|
+
## Page fixture detection
|
|
44
|
+
|
|
45
|
+
By default, the plugin looks for a `page` fixture. Configure custom fixture names in `pyproject.toml`:
|
|
46
|
+
|
|
47
|
+
```toml
|
|
48
|
+
[tool.pytest.ini_options]
|
|
49
|
+
vs_page_fixtures = ["v2app", "v1app", "page"]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The plugin checks each name in order and uses the first one it finds.
|
|
53
|
+
|
|
54
|
+
## Environment variables
|
|
55
|
+
|
|
56
|
+
| Variable | Required | Description |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| `VS_API_KEY` | Yes | Project API key from VisionSpec |
|
|
59
|
+
| `VS_API_URL` | Yes | VisionSpec server URL |
|
|
60
|
+
| `VS_SUT_VERSION` | No | Version of the system under test |
|
|
61
|
+
| `VS_VERBOSE` | No | Set to `1` for debug logging |
|
|
62
|
+
| `VS_VERSION` | No | App version metadata |
|
|
63
|
+
| `VS_FRONTEND_VERSION` | No | Frontend version metadata |
|
|
64
|
+
| `VS_BACKEND_VERSION` | No | Backend version metadata |
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pytest-visionspec"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Pytest plugin that auto-reports test results with screenshots to VisionSpec"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"pytest>=7.0",
|
|
13
|
+
"httpx>=0.24",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.entry-points.pytest11]
|
|
17
|
+
visionspec = "pytest_visionspec"
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""pytest-visionspec — auto-report test results with screenshot uploads.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
pip install pytest-visionspec
|
|
5
|
+
Set env vars: VS_API_KEY, VS_API_URL
|
|
6
|
+
Tag tests: @pytest.mark.vs("journey-id")
|
|
7
|
+
|
|
8
|
+
The plugin:
|
|
9
|
+
1. Detects @pytest.mark.vs("journey-id") tags on tests
|
|
10
|
+
2. After each test, captures a Playwright screenshot (if a page fixture exists)
|
|
11
|
+
3. Uploads screenshots to VisionSpec storage
|
|
12
|
+
4. Posts the result (pass/fail + screenshot_urls) to VisionSpec
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
import glob
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
import httpx
|
|
21
|
+
import pytest
|
|
22
|
+
|
|
23
|
+
_API_KEY = os.environ.get("VS_API_KEY", "")
|
|
24
|
+
_API_URL = os.environ.get("VS_API_URL", "")
|
|
25
|
+
_VERBOSE = os.environ.get("VS_VERBOSE", "").lower() in ("1", "true", "yes")
|
|
26
|
+
|
|
27
|
+
# Default fixture names; overridden by [tool.pytest-visionspec] page_fixtures in pyproject.toml
|
|
28
|
+
_PAGE_FIXTURES = ["page"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def pytest_addoption(parser):
|
|
32
|
+
parser.addini("vs_page_fixtures",
|
|
33
|
+
type="args",
|
|
34
|
+
default=["page"],
|
|
35
|
+
help="Fixture names to check for a Playwright page object (in priority order)")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def pytest_configure(config):
|
|
39
|
+
config.addinivalue_line("markers", "vs(journey_id): link test to a VisionSpec journey")
|
|
40
|
+
config.addinivalue_line("markers", "vs_surface(surface): override the reported surface (api, desktop, mobile)")
|
|
41
|
+
|
|
42
|
+
global _PAGE_FIXTURES
|
|
43
|
+
_PAGE_FIXTURES = list(config.getini("vs_page_fixtures"))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
|
47
|
+
def pytest_runtest_makereport(item, call):
|
|
48
|
+
outcome = yield
|
|
49
|
+
report = outcome.get_result()
|
|
50
|
+
if call.when != "call":
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
markers = list(item.iter_markers("vs"))
|
|
54
|
+
api_key = _API_KEY
|
|
55
|
+
api_url = _API_URL
|
|
56
|
+
if not markers or not api_key or not api_url:
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
# Determine surface
|
|
60
|
+
surface_marker = item.get_closest_marker("vs_surface")
|
|
61
|
+
if surface_marker and surface_marker.args:
|
|
62
|
+
surface = surface_marker.args[0]
|
|
63
|
+
else:
|
|
64
|
+
surface = "desktop"
|
|
65
|
+
|
|
66
|
+
# Capture screenshot if a Playwright page fixture is available
|
|
67
|
+
screenshot_urls = []
|
|
68
|
+
video_url = None
|
|
69
|
+
page = _find_page(item)
|
|
70
|
+
if page:
|
|
71
|
+
try:
|
|
72
|
+
safe_name = _safe_filename(item.name)
|
|
73
|
+
screenshot_dir = Path("test-results") / safe_name
|
|
74
|
+
screenshot_dir.mkdir(parents=True, exist_ok=True)
|
|
75
|
+
screenshot_path = screenshot_dir / "after.png"
|
|
76
|
+
raw = page.raw_page if hasattr(page, "raw_page") else page
|
|
77
|
+
raw.screenshot(path=str(screenshot_path))
|
|
78
|
+
except Exception:
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
# Upload any captured screenshots/videos
|
|
82
|
+
try:
|
|
83
|
+
screenshot_urls, video_url = _collect_and_upload_artifacts(item, api_key, api_url)
|
|
84
|
+
except Exception:
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
# Report result for each tagged journey
|
|
88
|
+
headers = {"X-API-Key": api_key, "Content-Type": "application/json"}
|
|
89
|
+
for marker in markers:
|
|
90
|
+
journey_id = marker.args[0] if marker.args else None
|
|
91
|
+
if not journey_id:
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
# Read revision from marker kwargs or VS_REVISION env var
|
|
95
|
+
revision = marker.kwargs.get("revision") or os.environ.get("VS_REVISION") or None
|
|
96
|
+
if not revision:
|
|
97
|
+
print(f"[visionspec] ERROR: @pytest.mark.vs('{journey_id}', revision='slug') "
|
|
98
|
+
f"requires revision parameter — skipping result post", file=sys.stderr)
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
r = httpx.post(f"{api_url}/api/results",
|
|
103
|
+
headers=headers, timeout=10,
|
|
104
|
+
json={
|
|
105
|
+
"journey_id": journey_id,
|
|
106
|
+
"journey_version": 1,
|
|
107
|
+
"surface": surface,
|
|
108
|
+
"sut_version": os.environ.get("VS_SUT_VERSION", "dev"),
|
|
109
|
+
"passed": report.passed,
|
|
110
|
+
"steps": [{"action": item.name, "passed": report.passed}],
|
|
111
|
+
"source": "pytest-visionspec",
|
|
112
|
+
"versions": _get_versions(),
|
|
113
|
+
"screenshot_urls": screenshot_urls,
|
|
114
|
+
"video_url": video_url,
|
|
115
|
+
"revision_slug": revision,
|
|
116
|
+
})
|
|
117
|
+
if _VERBOSE:
|
|
118
|
+
print(f"[visionspec] POST {journey_id}: {r.status_code}", file=sys.stderr)
|
|
119
|
+
if r.status_code != 200 and _VERBOSE:
|
|
120
|
+
print(f"[visionspec] BODY: {r.text[:200]}", file=sys.stderr)
|
|
121
|
+
except Exception as e:
|
|
122
|
+
if _VERBOSE:
|
|
123
|
+
print(f"[visionspec] ERROR for {journey_id}: {e}", file=sys.stderr)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _find_page(item):
|
|
127
|
+
"""Find a Playwright page object from test fixtures."""
|
|
128
|
+
for name in _PAGE_FIXTURES:
|
|
129
|
+
page = item.funcargs.get(name)
|
|
130
|
+
if page is not None:
|
|
131
|
+
return page
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _safe_filename(name: str) -> str:
|
|
136
|
+
"""Sanitize a test name for use as a directory/file name."""
|
|
137
|
+
return name.replace("[", "-").replace("]", "").replace("/", "-").replace(":", "-")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _collect_and_upload_artifacts(item, api_key, api_url):
|
|
141
|
+
"""Find screenshots/videos in test-results/ and upload them."""
|
|
142
|
+
screenshot_urls = []
|
|
143
|
+
video_url = None
|
|
144
|
+
|
|
145
|
+
safe_name = _safe_filename(item.name)
|
|
146
|
+
patterns = [
|
|
147
|
+
f"test-results/*{safe_name}*/*.png",
|
|
148
|
+
f"test-results/*{safe_name}*/*.jpg",
|
|
149
|
+
f"test-results/*{safe_name}*/*.webm",
|
|
150
|
+
f"test-results/*{safe_name}*/*.mp4",
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
for pattern in patterns:
|
|
154
|
+
for filepath in glob.glob(pattern):
|
|
155
|
+
path = Path(filepath)
|
|
156
|
+
ext = path.suffix.lstrip(".")
|
|
157
|
+
mime = {
|
|
158
|
+
"png": "image/png", "jpg": "image/jpeg",
|
|
159
|
+
"webm": "video/webm", "mp4": "video/mp4",
|
|
160
|
+
}.get(ext, "application/octet-stream")
|
|
161
|
+
|
|
162
|
+
upload_r = httpx.post(f"{api_url}/api/upload",
|
|
163
|
+
headers={"X-API-Key": api_key, "Content-Type": mime, "X-Filename": path.name},
|
|
164
|
+
content=path.read_bytes(),
|
|
165
|
+
timeout=30)
|
|
166
|
+
|
|
167
|
+
if upload_r.status_code == 200:
|
|
168
|
+
data = upload_r.json()
|
|
169
|
+
if mime.startswith("video/"):
|
|
170
|
+
video_url = data["url"]
|
|
171
|
+
else:
|
|
172
|
+
screenshot_urls.append(data["url"])
|
|
173
|
+
|
|
174
|
+
return screenshot_urls, video_url
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _get_versions():
|
|
178
|
+
"""Build version metadata from environment, plus git commit hash."""
|
|
179
|
+
import subprocess
|
|
180
|
+
versions = {}
|
|
181
|
+
for key in ("VS_VERSION", "VS_FRONTEND_VERSION", "VS_BACKEND_VERSION"):
|
|
182
|
+
val = os.environ.get(key, "")
|
|
183
|
+
if val:
|
|
184
|
+
versions[key.replace("VS_", "").lower()] = val
|
|
185
|
+
# Auto-capture git commit
|
|
186
|
+
try:
|
|
187
|
+
result = subprocess.run(
|
|
188
|
+
["git", "rev-parse", "--short", "HEAD"],
|
|
189
|
+
capture_output=True, text=True, timeout=5)
|
|
190
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
191
|
+
versions["git"] = result.stdout.strip()
|
|
192
|
+
except Exception:
|
|
193
|
+
pass
|
|
194
|
+
return versions
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pytest-visionspec
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pytest plugin that auto-reports test results with screenshots to VisionSpec
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: pytest>=7.0
|
|
8
|
+
Requires-Dist: httpx>=0.24
|
|
9
|
+
|
|
10
|
+
# pytest-visionspec
|
|
11
|
+
|
|
12
|
+
Pytest plugin that auto-reports test results with screenshots to [VisionSpec](https://visionspec-dev.helpfulhuman.xyz).
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install pytest-visionspec
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Setup
|
|
21
|
+
|
|
22
|
+
Set two environment variables:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
VS_API_KEY=vs_your_project_api_key
|
|
26
|
+
VS_API_URL=https://visionspec-dev.helpfulhuman.xyz
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
Tag tests with `@pytest.mark.vs("journey-id")`:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
@pytest.mark.vs("login-happy-path")
|
|
35
|
+
def test_login(page):
|
|
36
|
+
page.goto("/login")
|
|
37
|
+
page.fill("[name=email]", "user@example.com")
|
|
38
|
+
page.click("button[type=submit]")
|
|
39
|
+
assert page.url == "/dashboard"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The plugin automatically:
|
|
43
|
+
1. Captures a Playwright screenshot after each test
|
|
44
|
+
2. Uploads it to VisionSpec storage
|
|
45
|
+
3. Posts the result (pass/fail + screenshot URL) to VisionSpec
|
|
46
|
+
|
|
47
|
+
## Markers
|
|
48
|
+
|
|
49
|
+
- `@pytest.mark.vs("journey-id")` — link test to a VisionSpec journey spec
|
|
50
|
+
- `@pytest.mark.vs_surface("mobile")` — override the reported surface (default: `desktop`)
|
|
51
|
+
|
|
52
|
+
## Page fixture detection
|
|
53
|
+
|
|
54
|
+
By default, the plugin looks for a `page` fixture. Configure custom fixture names in `pyproject.toml`:
|
|
55
|
+
|
|
56
|
+
```toml
|
|
57
|
+
[tool.pytest.ini_options]
|
|
58
|
+
vs_page_fixtures = ["v2app", "v1app", "page"]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The plugin checks each name in order and uses the first one it finds.
|
|
62
|
+
|
|
63
|
+
## Environment variables
|
|
64
|
+
|
|
65
|
+
| Variable | Required | Description |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| `VS_API_KEY` | Yes | Project API key from VisionSpec |
|
|
68
|
+
| `VS_API_URL` | Yes | VisionSpec server URL |
|
|
69
|
+
| `VS_SUT_VERSION` | No | Version of the system under test |
|
|
70
|
+
| `VS_VERBOSE` | No | Set to `1` for debug logging |
|
|
71
|
+
| `VS_VERSION` | No | App version metadata |
|
|
72
|
+
| `VS_FRONTEND_VERSION` | No | Frontend version metadata |
|
|
73
|
+
| `VS_BACKEND_VERSION` | No | Backend version metadata |
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
pytest_visionspec/__init__.py
|
|
4
|
+
pytest_visionspec.egg-info/PKG-INFO
|
|
5
|
+
pytest_visionspec.egg-info/SOURCES.txt
|
|
6
|
+
pytest_visionspec.egg-info/dependency_links.txt
|
|
7
|
+
pytest_visionspec.egg-info/entry_points.txt
|
|
8
|
+
pytest_visionspec.egg-info/requires.txt
|
|
9
|
+
pytest_visionspec.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pytest_visionspec
|