Skip to content

Commit 85598e0

Browse files
committed
test: Add lockingto pyright test to allow parall testion (from UX)
Signed-off-by: Jos Verlinde <[email protected]>
1 parent 298a8b6 commit 85598e0

File tree

10 files changed

+236
-70
lines changed

10 files changed

+236
-70
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,4 @@ snippets/*/typings
6161
typings_test
6262
typings
6363
empty
64+
**/*_lock.file

poetry.lock

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

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ pytest-mock = "^3.10.0"
9898
sourcery-cli = "^1.0.3"
9999
mpremote = { git = "https://github.com/Josverl/mpremote", subdirectory = "tools/mpremote", optional = true }
100100
ipykernel = "^6.23.1"
101+
fasteners = "^0.19"
101102

102103
[build-system]
103104
requires = ["poetry-core>=1.0.0"]

snippets/check_esp32/check_onewire.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313
ow.select_rom(b"12345678") # select a specific device by its ROM code
1414

1515
assert_type(ow, onewire.OneWire)
16-
assert_type(ow.write(b"123"), None)
17-
assert_type(ow.select_rom(b"12345678"), None)
16+
17+
# there was no onewire documatation before 1.19.1
18+
# assert_type(ow.write(b"123"), None)
19+
# assert_type(ow.select_rom(b"12345678"), None)
1820

1921
# assert_type(ow.scan(), list)
2022
# assert_type(ow.reset(), None)

snippets/conftest.py

Lines changed: 121 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,29 @@
1+
"""Pytest configuration file for snippets tests.
2+
3+
- snip_path
4+
returns the path to the feature folder (feat_xxxx) or check folder (check_xxxx)
5+
6+
- type_stub_cache_path
7+
Is used to install the type stubs for the given portboard and version and cache it for 24 hours to speed up tests
8+
Returns the path to the cache folder
9+
10+
- install_stubs
11+
is the function that does the actual pip install to a folder
12+
13+
- copy_type_stubs
14+
copies the type stubs from the cache to the feature folder
15+
16+
- pytest_runtest_makereport
17+
is used to add the caplog to the test report to make it avaialble to VSCode test explorer
18+
19+
"""
20+
121
import shutil
222
import subprocess
323
import time
424
from pathlib import Path
525

26+
import fasteners
627
import pytest
728
from loguru import logger as log
829

@@ -11,7 +32,19 @@
1132

1233
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
1334
def pytest_runtest_makereport(item, call):
14-
# execute all other hooks to obtain the report object
35+
"""
36+
pytest_runtest_makereport hook implementation.
37+
38+
Executes all other hooks to obtain the report object. Looks at actual failing test calls, not setup/teardown. Adds the caplog errors and warnings to the report.
39+
40+
Args:
41+
item: The pytest Item object.
42+
call: The pytest CallInfo object.
43+
44+
Returns:
45+
The pytest Report object.
46+
47+
"""
1548
outcome = yield
1649
report = outcome.get_result()
1750

@@ -41,37 +74,67 @@ def type_stub_cache_path(
4174
request: pytest.FixtureRequest,
4275
) -> Path:
4376
"""
44-
Installs a copy of the type stubs for the given portboard and version.
45-
Returns the path to the cache folder
77+
Installs a copy of the type stubs for the given portboard and version. Returns the path to the cache folder.
78+
79+
Args:
80+
portboard: The portboard.
81+
version: The version.
82+
stub_source: The stub source.
83+
pytestconfig: The pytest Config object.
84+
request: The pytest FixtureRequest object.
85+
86+
Returns:
87+
Path: The path to the cache folder.
4688
"""
4789

4890
log.trace(f"setup install type_stubs to cache: {stub_source}, {version}, {portboard}")
4991
flatversion = version.replace(".", "_")
5092
cache_key = f"stubber/{stub_source}/{version}/{portboard}"
5193
# cache_path = pytestconfig.rootpath / "snippets" / "typings_cache"
52-
tsc_path = request.config.cache.makedir(
53-
f"typings_{flatversion}_{portboard}_stub_{stub_source}"
94+
tsc_path = Path(
95+
request.config.cache.makedir(f"typings_{flatversion}_{portboard}_stub_{stub_source}")
5496
)
97+
# prevent simultaneous updates to the cache
98+
cache_lock = fasteners.InterProcessLock(tsc_path.parent / f"{tsc_path.name}.lock")
5599
# check if stubs are already installed to the cache
56-
if (tsc_path / "micropython.pyi").exists():
57-
# check if stubs are in the cache
58-
timestamp = request.config.cache.get(cache_key, None)
59-
# if timestamp is not older than 24 hours, use cache
60-
61-
if timestamp and timestamp > (time.time() - MAX_CACHE_AGE):
62-
log.trace(f"Using cached type stubs for {portboard} {version}")
63-
return tsc_path
64-
65-
ok = install_stubs(portboard, version, stub_source, pytestconfig, flatversion, tsc_path)
66-
if not ok:
67-
pytest.skip(f"Could not install stubs for {portboard} {version}")
68-
# add the timestamp to the cache
69-
request.config.cache.set(cache_key, time.time())
100+
with cache_lock:
101+
if (tsc_path / "micropython.pyi").exists():
102+
# check if stubs are in the cache
103+
timestamp = request.config.cache.get(cache_key, None)
104+
# if timestamp is not older than 24 hours, use cache
105+
106+
if timestamp and timestamp > (time.time() - MAX_CACHE_AGE):
107+
log.trace(f"Using cached type stubs for {portboard} {version}")
108+
return tsc_path
109+
110+
ok = install_stubs(portboard, version, stub_source, pytestconfig, flatversion, tsc_path)
111+
if not ok:
112+
pytest.skip(f"Could not install stubs for {portboard} {version}")
113+
# add the timestamp to the cache
114+
request.config.cache.set(cache_key, time.time())
115+
70116
return tsc_path
71117

72118

73-
def install_stubs(portboard, version, stub_source, pytestconfig, flatversion, tsc_path) -> bool:
74-
"Expensive / Slow function"
119+
def install_stubs(
120+
portboard, version, stub_source, pytestconfig, flatversion, tsc_path: Path
121+
) -> bool:
122+
"""
123+
Cleans up prior install to avoid stale files.
124+
Uses pip to install type stubs for the given portboard and version.
125+
126+
Args:
127+
portboard: The portboard.
128+
version: The version.
129+
stub_source: The stub source.
130+
pytestconfig: The pytest Config object.
131+
flatversion: The flat version.
132+
tsc_path: The path to the cache folder.
133+
134+
Returns:
135+
bool: True if the installation was successful, False otherwise.
136+
"""
137+
75138
# clean up prior install to avoid stale files
76139
if tsc_path.exists():
77140
shutil.rmtree(tsc_path, ignore_errors=True)
@@ -83,7 +146,10 @@ def install_stubs(portboard, version, stub_source, pytestconfig, flatversion, ts
83146
else:
84147
foldername = f"micropython-{flatversion}-{portboard}-stubs"
85148
stubsource = pytestconfig.inipath.parent / f"repos/micropython-stubs/publish/{foldername}"
149+
if not stubsource.exists():
150+
pytest.skip(f"Could not find stubs for {portboard} {version} at {stubsource}")
86151
cmd = f"pip install {stubsource} --target {tsc_path} --no-user"
152+
87153
try:
88154
subprocess.run(cmd, shell=False, check=True, capture_output=True, text=True)
89155
except subprocess.CalledProcessError as e:
@@ -96,6 +162,17 @@ def install_stubs(portboard, version, stub_source, pytestconfig, flatversion, ts
96162

97163
@pytest.fixture(scope="function")
98164
def snip_path(feature: str, pytestconfig) -> Path:
165+
"""
166+
Get the path to the feat_ or check_ folder.
167+
168+
Args:
169+
feature: The feature.
170+
pytestconfig: The pytest Config object.
171+
172+
Returns:
173+
Path: The path to the feature folder.
174+
"""
175+
99176
snip_path = pytestconfig.inipath.parent / "snippets" / f"feat_{feature}"
100177
if not snip_path.exists():
101178
snip_path = pytestconfig.inipath.parent / "snippets" / f"check_{feature}"
@@ -107,15 +184,27 @@ def copy_type_stubs(
107184
portboard: str, version: str, feature: str, type_stub_cache_path: Path, snip_path: Path
108185
):
109186
"""
110-
Copies installed/cached typestub fom cache to the feature folder
187+
Copies installed/cached type stubs from the cache to the feature folder.
188+
189+
Args:
190+
portboard: The portboard.
191+
version: The version.
192+
feature: The feature.
193+
type_stub_cache_path: The path to the cache folder.
194+
snip_path: The path to the feature folder.
111195
"""
112-
log.trace(f"- copy_type_stubs: {version}, {portboard} to {feature}")
113-
print(f"\n - copy_type_stubs : {version}, {portboard} to {feature}")
114-
if not snip_path or not snip_path.exists():
115-
# skip if no feature folder
116-
pytest.skip(f"no feature folder for {feature}")
117-
typings_path = snip_path / "typings"
118-
if typings_path.exists():
119-
shutil.rmtree(typings_path, ignore_errors=True)
120-
shutil.copytree(type_stub_cache_path, typings_path)
121-
# time.sleep(0.2)
196+
cache_lock = fasteners.InterProcessLock(
197+
type_stub_cache_path.parent / f"{type_stub_cache_path.name}.lock"
198+
)
199+
typecheck_lock = fasteners.InterProcessLock(snip_path / "typecheck_lock.file")
200+
with cache_lock:
201+
with typecheck_lock:
202+
log.trace(f"- copy_type_stubs: {version}, {portboard} to {feature}")
203+
print(f"\n - copy_type_stubs : {version}, {portboard} to {feature}")
204+
if not snip_path or not snip_path.exists():
205+
# skip if no feature folder
206+
pytest.skip(f"no feature folder for {feature}")
207+
typings_path = snip_path / "typings"
208+
if typings_path.exists():
209+
shutil.rmtree(typings_path, ignore_errors=True)
210+
shutil.copytree(type_stub_cache_path, typings_path)

snippets/readme.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,20 @@ You can update / install the type-stubs in the various typings folders by runnin
1313

1414
```powershell
1515
# Update the type stubs
16+
foreach ($version in @( "latest", "v1.20.0", "v1.19.1", "v1.18.0", "v1.17.0" )) {
17+
stubber switch $version
18+
stubber get-docstubs
19+
stubber merge --version $version
20+
stubber build --version $version
21+
}
22+
23+
```
24+
1625
stubber switch latest
1726
stubber get-docstubs
1827
stubber merge --version latest
1928
stubber build --version latest
29+
2030
.\snippets\install-stubs.ps1
2131
```
2232
## Test with pytest

snippets/snippets.code-workspace

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,19 +41,22 @@
4141
"path": "feat_networking"
4242
},
4343
{
44+
"name": "feat_espnow",
4445
"path": "feat_espnow"
4546
},
4647
{
48+
"name": "wip_todo",
4749
"path": "wip_todo"
4850
},
4951
{
52+
"name": "snippets",
5053
"path": "."
5154
}
5255
],
5356
"settings": {
5457
"python.languageServer": "Pylance",
5558
"python.analysis.typeCheckingMode": "basic",
56-
"python.analysis.diagnosticMode": "workspace",
59+
"python.analysis.diagnosticMode": "openFilesOnly",
5760
"python.autoComplete.extraPaths": [
5861
"${workspacePath}/typings",
5962
],
@@ -90,6 +93,8 @@
9093
"titleBar.inactiveForeground": "#e7e7e799"
9194
},
9295
"peacock.color": "#eb117e",
93-
"testExplorer.useNativeTesting": true
96+
"testExplorer.useNativeTesting": true,
97+
"python.analysis.enableSyncServer": false,
98+
"python.testing.autoTestDiscoverOnSaveEnabled": false
9499
},
95100
}

snippets/test_parallel.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import time
2+
3+
import fasteners
4+
import pytest
5+
6+
7+
# create a test than can be run in parrallel
8+
@pytest.mark.parametrize("x", [1, 2, 3, 4, 5])
9+
def test_parallel(x):
10+
id = x % 2
11+
lock = fasteners.InterProcessLock(f"path/to/lock_{id}.file")
12+
with lock:
13+
time.sleep(x)
14+
... # exclusive access
15+
16+
17+
# create a test than can be run in parrallel
18+
@pytest.mark.parametrize("x", [1, 2, 3, 4, 5])
19+
def test_slow(x):
20+
id = x % 2
21+
lock = fasteners.InterProcessLock(f"path/to/lock_{id}.file")
22+
with lock:
23+
time.sleep(x + 3)
24+
... # exclusive access

0 commit comments

Comments
 (0)