skipper-pytest 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.
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.pyo
|
|
5
|
+
*.pyd
|
|
6
|
+
.Python
|
|
7
|
+
*.egg-info/
|
|
8
|
+
dist/
|
|
9
|
+
build/
|
|
10
|
+
*.egg
|
|
11
|
+
.eggs/
|
|
12
|
+
|
|
13
|
+
# Virtual environments
|
|
14
|
+
.venv/
|
|
15
|
+
venv/
|
|
16
|
+
env/
|
|
17
|
+
|
|
18
|
+
# uv
|
|
19
|
+
.uv/
|
|
20
|
+
|
|
21
|
+
# Testing
|
|
22
|
+
.pytest_cache/
|
|
23
|
+
.coverage
|
|
24
|
+
htmlcov/
|
|
25
|
+
.tox/
|
|
26
|
+
.nox/
|
|
27
|
+
|
|
28
|
+
# Type checking
|
|
29
|
+
.mypy_cache/
|
|
30
|
+
|
|
31
|
+
# IDE
|
|
32
|
+
.idea/
|
|
33
|
+
.vscode/
|
|
34
|
+
*.swp
|
|
35
|
+
*.swo
|
|
36
|
+
|
|
37
|
+
# Temp files
|
|
38
|
+
*.tmp
|
|
39
|
+
/tmp/
|
|
40
|
+
|
|
41
|
+
# Credentials (never commit)
|
|
42
|
+
service-account-skipper-bot.json
|
|
43
|
+
*.json.bak
|
|
44
|
+
|
|
45
|
+
# Environment
|
|
46
|
+
.env
|
|
47
|
+
.env.local
|
|
48
|
+
|
|
49
|
+
# macOS
|
|
50
|
+
.DS_Store
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: skipper-pytest
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: pytest plugin for Skipper test-gating via Google Spreadsheet
|
|
5
|
+
Project-URL: Homepage, https://github.com/get-skipper/skipper-python
|
|
6
|
+
Project-URL: Repository, https://github.com/get-skipper/skipper-python
|
|
7
|
+
License: MIT
|
|
8
|
+
Keywords: google-sheets,pytest,skipper,test-gating,testing
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Framework :: Pytest
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Software Development :: Testing
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Requires-Dist: pytest>=7.0
|
|
17
|
+
Requires-Dist: skipper-core>=0.1.0
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# skipper-pytest
|
|
21
|
+
|
|
22
|
+
pytest plugin for [Skipper](https://github.com/get-skipper/skipper-python) test-gating via Google Spreadsheet.
|
|
23
|
+
|
|
24
|
+
Tests are automatically skipped when their `disabledUntil` date is in the future — no changes to test code required.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install skipper-pytest
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Setup
|
|
33
|
+
|
|
34
|
+
Add to `tests/conftest.py`:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from skipper_pytest import configure_skipper
|
|
38
|
+
from skipper_core import SkipperConfig, FileCredentials
|
|
39
|
+
|
|
40
|
+
configure_skipper(SkipperConfig(
|
|
41
|
+
spreadsheet_id="YOUR_SPREADSHEET_ID",
|
|
42
|
+
credentials=FileCredentials("./service-account-skipper-bot.json"),
|
|
43
|
+
sheet_name="skipper-python",
|
|
44
|
+
))
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Or configure entirely via environment variables:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
SKIPPER_SPREADSHEET_ID=... \
|
|
51
|
+
SKIPPER_CREDENTIALS_FILE=./service-account-skipper-bot.json \
|
|
52
|
+
SKIPPER_SHEET_NAME=skipper-python \
|
|
53
|
+
pytest
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Test ID Format
|
|
57
|
+
|
|
58
|
+
`tests/test_auth.py > ClassName > test_method_name`
|
|
59
|
+
|
|
60
|
+
## Sync Mode
|
|
61
|
+
|
|
62
|
+
Set `SKIPPER_MODE=sync` to have Skipper reconcile the spreadsheet after the test run (adds new test IDs, removes stale ones).
|
|
63
|
+
|
|
64
|
+
See the [root README](../../README.md) for full documentation.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# skipper-pytest
|
|
2
|
+
|
|
3
|
+
pytest plugin for [Skipper](https://github.com/get-skipper/skipper-python) test-gating via Google Spreadsheet.
|
|
4
|
+
|
|
5
|
+
Tests are automatically skipped when their `disabledUntil` date is in the future — no changes to test code required.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install skipper-pytest
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
Add to `tests/conftest.py`:
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from skipper_pytest import configure_skipper
|
|
19
|
+
from skipper_core import SkipperConfig, FileCredentials
|
|
20
|
+
|
|
21
|
+
configure_skipper(SkipperConfig(
|
|
22
|
+
spreadsheet_id="YOUR_SPREADSHEET_ID",
|
|
23
|
+
credentials=FileCredentials("./service-account-skipper-bot.json"),
|
|
24
|
+
sheet_name="skipper-python",
|
|
25
|
+
))
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or configure entirely via environment variables:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
SKIPPER_SPREADSHEET_ID=... \
|
|
32
|
+
SKIPPER_CREDENTIALS_FILE=./service-account-skipper-bot.json \
|
|
33
|
+
SKIPPER_SHEET_NAME=skipper-python \
|
|
34
|
+
pytest
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Test ID Format
|
|
38
|
+
|
|
39
|
+
`tests/test_auth.py > ClassName > test_method_name`
|
|
40
|
+
|
|
41
|
+
## Sync Mode
|
|
42
|
+
|
|
43
|
+
Set `SKIPPER_MODE=sync` to have Skipper reconcile the spreadsheet after the test run (adds new test IDs, removes stale ones).
|
|
44
|
+
|
|
45
|
+
See the [root README](../../README.md) for full documentation.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "skipper-pytest"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "pytest plugin for Skipper test-gating via Google Spreadsheet"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
keywords = ["testing", "test-gating", "google-sheets", "skipper", "pytest"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Framework :: Pytest",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Software Development :: Testing",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"skipper-core>=0.1.0",
|
|
23
|
+
"pytest>=7.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.entry-points."pytest11"]
|
|
27
|
+
skipper = "skipper_pytest.plugin"
|
|
28
|
+
|
|
29
|
+
[tool.uv.sources]
|
|
30
|
+
skipper-core = { workspace = true }
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/get-skipper/skipper-python"
|
|
34
|
+
Repository = "https://github.com/get-skipper/skipper-python"
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.wheel]
|
|
37
|
+
packages = ["src/skipper_pytest"]
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""skipper-pytest — pytest plugin for Skipper test-gating."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import threading
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from skipper_core import (
|
|
10
|
+
CacheManager,
|
|
11
|
+
SkipperConfig,
|
|
12
|
+
SkipperResolver,
|
|
13
|
+
build_test_id,
|
|
14
|
+
mode_from_env,
|
|
15
|
+
)
|
|
16
|
+
from skipper_core.writer import SheetsWriter
|
|
17
|
+
|
|
18
|
+
# ── Module-level state ────────────────────────────────────────────────────────
|
|
19
|
+
_resolver: SkipperResolver | None = None
|
|
20
|
+
_cache_dir: str | None = None
|
|
21
|
+
_discovered_lock = threading.Lock()
|
|
22
|
+
_discovered_ids: list[str] = []
|
|
23
|
+
_config: SkipperConfig | None = None
|
|
24
|
+
_cache_manager = CacheManager()
|
|
25
|
+
|
|
26
|
+
# ── Public API ────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def configure_skipper(config: SkipperConfig) -> None:
|
|
30
|
+
"""Call this from conftest.py to register a SkipperConfig with the plugin.
|
|
31
|
+
|
|
32
|
+
Example::
|
|
33
|
+
|
|
34
|
+
# tests/conftest.py
|
|
35
|
+
from skipper_pytest import configure_skipper
|
|
36
|
+
from skipper_core import SkipperConfig, FileCredentials
|
|
37
|
+
|
|
38
|
+
configure_skipper(SkipperConfig(
|
|
39
|
+
spreadsheet_id="YOUR_SPREADSHEET_ID",
|
|
40
|
+
credentials=FileCredentials("./service-account-skipper-bot.json"),
|
|
41
|
+
sheet_name="skipper-python",
|
|
42
|
+
))
|
|
43
|
+
"""
|
|
44
|
+
global _config
|
|
45
|
+
_config = config
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ── pytest hooks ──────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def pytest_configure(config: pytest.Config) -> None:
|
|
52
|
+
"""Initialize the resolver (or rehydrate from cache for xdist workers)."""
|
|
53
|
+
global _resolver, _cache_dir, _discovered_ids
|
|
54
|
+
|
|
55
|
+
_discovered_ids = []
|
|
56
|
+
|
|
57
|
+
# xdist worker: rehydrate from cache file written by the controller.
|
|
58
|
+
cache_file = os.getenv("SKIPPER_CACHE_FILE")
|
|
59
|
+
if cache_file and _is_xdist_worker(config):
|
|
60
|
+
data = _cache_manager.read_resolver_cache(cache_file)
|
|
61
|
+
_resolver = SkipperResolver.from_marshal_cache(data)
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
# Main process (or no xdist): initialize from spreadsheet if config was provided.
|
|
65
|
+
# Config may not be set yet (set_config is called in conftest.py which runs after
|
|
66
|
+
# pytest_configure for plugins). We defer actual initialization to pytest_sessionstart.
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def pytest_sessionstart(session: pytest.Session) -> None:
|
|
70
|
+
"""Initialize the resolver once config is available (called after conftest.py)."""
|
|
71
|
+
global _resolver, _cache_dir
|
|
72
|
+
|
|
73
|
+
if _resolver is not None:
|
|
74
|
+
return # already rehydrated (xdist worker)
|
|
75
|
+
|
|
76
|
+
cfg = _config
|
|
77
|
+
if cfg is None:
|
|
78
|
+
# Check env vars as fallback for minimal config.
|
|
79
|
+
cfg = _config_from_env()
|
|
80
|
+
if cfg is None:
|
|
81
|
+
return # Skipper not configured — run all tests normally.
|
|
82
|
+
|
|
83
|
+
if _is_xdist_worker(session.config):
|
|
84
|
+
return # Worker rehydration handled in pytest_configure.
|
|
85
|
+
|
|
86
|
+
resolver = SkipperResolver(cfg)
|
|
87
|
+
resolver.initialize()
|
|
88
|
+
_resolver = resolver
|
|
89
|
+
|
|
90
|
+
data = resolver.marshal_cache()
|
|
91
|
+
cache_dir = _cache_manager.write_resolver_cache(data)
|
|
92
|
+
_cache_dir = cache_dir
|
|
93
|
+
os.environ["SKIPPER_CACHE_FILE"] = os.path.join(cache_dir, "cache.json")
|
|
94
|
+
os.environ["SKIPPER_DISCOVERED_DIR"] = cache_dir
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def pytest_runtest_setup(item: pytest.Item) -> None:
|
|
98
|
+
"""Skip the test if Skipper says it is disabled; record the test ID for sync."""
|
|
99
|
+
if _resolver is None:
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
test_id = _test_id_from_item(item)
|
|
103
|
+
|
|
104
|
+
with _discovered_lock:
|
|
105
|
+
_discovered_ids.append(test_id)
|
|
106
|
+
|
|
107
|
+
# Also write to the shared dir if running under xdist.
|
|
108
|
+
discovered_dir = os.getenv("SKIPPER_DISCOVERED_DIR")
|
|
109
|
+
if discovered_dir and _is_xdist_worker(item.config):
|
|
110
|
+
_cache_manager.write_discovered_ids(discovered_dir, [test_id])
|
|
111
|
+
|
|
112
|
+
if not _resolver.is_test_enabled(test_id):
|
|
113
|
+
until = _resolver.get_disabled_until(test_id)
|
|
114
|
+
msg = "[skipper] Test disabled"
|
|
115
|
+
if until is not None:
|
|
116
|
+
msg += f" until {until.strftime('%Y-%m-%d')}"
|
|
117
|
+
pytest.skip(msg)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None:
|
|
121
|
+
"""In sync mode, write discovered IDs and reconcile the spreadsheet."""
|
|
122
|
+
if _resolver is None or _is_xdist_worker(session.config):
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
cfg = _config or _config_from_env()
|
|
126
|
+
if cfg is None or mode_from_env().value != "sync":
|
|
127
|
+
_cleanup()
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
# Flush in-memory discovered IDs to the shared dir.
|
|
131
|
+
if _cache_dir:
|
|
132
|
+
with _discovered_lock:
|
|
133
|
+
ids = list(_discovered_ids)
|
|
134
|
+
if ids:
|
|
135
|
+
_cache_manager.write_discovered_ids(_cache_dir, ids)
|
|
136
|
+
|
|
137
|
+
# Merge all workers' files.
|
|
138
|
+
all_ids = _cache_manager.merge_discovered_ids(_cache_dir)
|
|
139
|
+
else:
|
|
140
|
+
with _discovered_lock:
|
|
141
|
+
all_ids = list(_discovered_ids)
|
|
142
|
+
|
|
143
|
+
writer = SheetsWriter(cfg)
|
|
144
|
+
writer.sync(all_ids)
|
|
145
|
+
|
|
146
|
+
_cleanup()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _test_id_from_item(item: pytest.Item) -> str:
|
|
153
|
+
"""Build a stable Skipper test ID from a pytest Item."""
|
|
154
|
+
try:
|
|
155
|
+
# item.fspath gives the absolute path; build_test_id makes it relative.
|
|
156
|
+
file_path = str(item.fspath)
|
|
157
|
+
except Exception:
|
|
158
|
+
file_path = item.nodeid.split("::")[0]
|
|
159
|
+
|
|
160
|
+
# nodeid is "path/to/test.py::ClassName::test_name[param]"
|
|
161
|
+
parts = item.nodeid.split("::")
|
|
162
|
+
title_parts = parts[1:] # drop the file path component
|
|
163
|
+
|
|
164
|
+
# Strip parametrize suffixes like "[param]" for lookup so that
|
|
165
|
+
# test_login[chrome] and test_login[firefox] map to the same spreadsheet row.
|
|
166
|
+
title_parts = [_strip_param(p) for p in title_parts]
|
|
167
|
+
|
|
168
|
+
return build_test_id(file_path, title_parts)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _strip_param(name: str) -> str:
|
|
172
|
+
bracket = name.find("[")
|
|
173
|
+
return name[:bracket] if bracket >= 0 else name
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _is_xdist_worker(config: pytest.Config) -> bool:
|
|
177
|
+
return hasattr(config, "workerinput")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _config_from_env() -> SkipperConfig | None:
|
|
181
|
+
"""Build a minimal SkipperConfig from environment variables."""
|
|
182
|
+
from skipper_core import Base64Credentials, Credentials, FileCredentials
|
|
183
|
+
|
|
184
|
+
spreadsheet_id = os.getenv("SKIPPER_SPREADSHEET_ID")
|
|
185
|
+
if not spreadsheet_id:
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
creds_file = os.getenv("SKIPPER_CREDENTIALS_FILE")
|
|
189
|
+
creds_b64 = os.getenv("GOOGLE_CREDS_B64")
|
|
190
|
+
|
|
191
|
+
credentials: Credentials
|
|
192
|
+
if creds_file:
|
|
193
|
+
credentials = FileCredentials(path=creds_file)
|
|
194
|
+
elif creds_b64:
|
|
195
|
+
credentials = Base64Credentials(encoded=creds_b64)
|
|
196
|
+
else:
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
return SkipperConfig(
|
|
200
|
+
spreadsheet_id=spreadsheet_id,
|
|
201
|
+
credentials=credentials,
|
|
202
|
+
sheet_name=os.getenv("SKIPPER_SHEET_NAME") or None,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _cleanup() -> None:
|
|
207
|
+
if _cache_dir:
|
|
208
|
+
_cache_manager.cleanup(_cache_dir)
|