mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-13 12:35:05 +00:00
ci: run the unit suite in parallel and keep it off the network
Test servers shut down without the 0.5s poll wait, unit tests refuse non-loopback lookups, and three load-sensitive tests are steadied. Pushes to dev test Python 3.11 only; the docs job builds with uv.
This commit is contained in:
@@ -41,8 +41,11 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
python-version: ["3.11", "3.14"]
|
python-version: ${{ fromJSON(github.event_name == 'push' && github.ref == 'refs/heads/dev' && '["3.11"]' || '["3.11", "3.14"]') }}
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
timeout-minutes: 15
|
||||||
|
env:
|
||||||
|
UV_NO_SYNC: "1"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
- uses: astral-sh/setup-uv@v8.3.2
|
- uses: astral-sh/setup-uv@v8.3.2
|
||||||
@@ -50,13 +53,11 @@ jobs:
|
|||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
- name: Install
|
- name: Install
|
||||||
run: uv sync --locked --group test
|
run: uv sync --locked --no-default-groups --group test
|
||||||
- name: Import check
|
|
||||||
run: uv run python -c "import unshackle; print(unshackle.__name__, 'imports OK')"
|
|
||||||
- name: CLI loads
|
- name: CLI loads
|
||||||
run: uv run unshackle --help
|
run: uv run unshackle --help
|
||||||
- name: Tests
|
- name: Tests
|
||||||
run: uv run pytest -m "not live and not slow"
|
run: uv run pytest -n auto --dist worksteal -m "not live and not slow"
|
||||||
|
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -29,11 +29,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
- uses: actions/setup-python@v6
|
- uses: astral-sh/setup-uv@v8.3.2
|
||||||
with:
|
with:
|
||||||
python-version: "3.x"
|
python-version: "3.14"
|
||||||
- run: pip install -e . -r docs/requirements-docs.txt
|
enable-cache: true
|
||||||
- run: mkdocs build --strict
|
cache-suffix: docs
|
||||||
|
- run: uv run --no-project --with-requirements docs/requirements-docs.txt mkdocs build --strict
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
@@ -42,8 +43,9 @@ jobs:
|
|||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: actions/setup-python@v6
|
- uses: astral-sh/setup-uv@v8.3.2
|
||||||
with:
|
with:
|
||||||
python-version: "3.x"
|
python-version: "3.14"
|
||||||
- run: pip install -e . -r docs/requirements-docs.txt
|
enable-cache: true
|
||||||
- run: mkdocs gh-deploy --remote-branch docs
|
cache-suffix: docs
|
||||||
|
- run: uv run --no-project --with-requirements docs/requirements-docs.txt mkdocs gh-deploy --remote-branch docs
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ test = [
|
|||||||
"pytest>=8.3.0,<9",
|
"pytest>=8.3.0,<9",
|
||||||
"pytest-asyncio>=0.24.0,<1",
|
"pytest-asyncio>=0.24.0,<1",
|
||||||
"pytest-aiohttp>=1.0.5,<2",
|
"pytest-aiohttp>=1.0.5,<2",
|
||||||
|
"pytest-xdist>=3.6.0,<4",
|
||||||
"responses>=0.25.0,<1",
|
"responses>=0.25.0,<1",
|
||||||
"PyYAML>=6.0.1,<7",
|
"PyYAML>=6.0.1,<7",
|
||||||
]
|
]
|
||||||
@@ -121,6 +122,8 @@ filterwarnings = [
|
|||||||
"ignore::bs4.XMLParsedAsHTMLWarning",
|
"ignore::bs4.XMLParsedAsHTMLWarning",
|
||||||
]
|
]
|
||||||
addopts = "-ra --strict-markers"
|
addopts = "-ra --strict-markers"
|
||||||
|
# dumps every thread's stack for a test that hangs; the CI job timeout alone leaves no trace
|
||||||
|
faulthandler_timeout = 120
|
||||||
|
|
||||||
[tool.hatch.metadata]
|
[tool.hatch.metadata]
|
||||||
allow-direct-references = true
|
allow-direct-references = true
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"""Suite-wide test setup."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import socket
|
||||||
|
import socketserver
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from unshackle.core.update_checker import UpdateChecker
|
||||||
|
|
||||||
|
# shutdown() blocks until the serve loop polls again, and the 0.5s default is paid by every
|
||||||
|
# test server at teardown. The default changes for the whole process, servers in core included.
|
||||||
|
socketserver.BaseServer.serve_forever.__defaults__ = (0.01,)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def loopback_only(request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
"""Fail a name lookup for any host but loopback, so a stray request cannot hang a unit test."""
|
||||||
|
if request.node.get_closest_marker("live") or request.node.get_closest_marker("slow"):
|
||||||
|
return
|
||||||
|
resolve = socket.getaddrinfo
|
||||||
|
|
||||||
|
def offline(host: Any, *args: Any, **kwargs: Any) -> Any:
|
||||||
|
if host not in (None, "", "localhost", "127.0.0.1", "::1"):
|
||||||
|
raise socket.gaierror(socket.EAI_NONAME, f"unit tests are offline: {host!r} is not loopback")
|
||||||
|
return resolve(host, *args, **kwargs)
|
||||||
|
|
||||||
|
monkeypatch.setattr(socket, "getaddrinfo", offline)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def no_update_check(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
"""Keep the release lookup of the health route off the network."""
|
||||||
|
|
||||||
|
async def no_release(*args: Any, **kwargs: Any) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
monkeypatch.setattr(UpdateChecker, "check_for_updates", no_release)
|
||||||
@@ -104,7 +104,8 @@ def test_encoded_body_is_decoded_on_disk(tmp_path, encoding_server):
|
|||||||
assert save_path.read_bytes() == PLAIN
|
assert save_path.read_bytes() == PLAIN
|
||||||
|
|
||||||
|
|
||||||
def test_encoded_body_discards_a_partial_instead_of_range_resuming(tmp_path, encoding_server):
|
def test_encoded_body_discards_a_partial_instead_of_range_resuming(tmp_path, encoding_server, monkeypatch):
|
||||||
|
monkeypatch.setattr(importlib.import_module("unshackle.core.downloaders.requests"), "RETRY_WAIT", 0)
|
||||||
save_path = tmp_path / "sub.vtt"
|
save_path = tmp_path / "sub.vtt"
|
||||||
save_path.with_name("sub.vtt.!dev").write_bytes(PLAIN[:5000])
|
save_path.with_name("sub.vtt.!dev").write_bytes(PLAIN[:5000])
|
||||||
list(download(url=encoding_server.url, save_path=save_path, session=rq.Session()))
|
list(download(url=encoding_server.url, save_path=save_path, session=rq.Session()))
|
||||||
@@ -147,6 +148,7 @@ class _FakeRnetSession:
|
|||||||
def test_rnet_encoded_body_discards_a_partial_instead_of_range_resuming(tmp_path, monkeypatch):
|
def test_rnet_encoded_body_discards_a_partial_instead_of_range_resuming(tmp_path, monkeypatch):
|
||||||
dl = importlib.import_module("unshackle.core.downloaders.requests")
|
dl = importlib.import_module("unshackle.core.downloaders.requests")
|
||||||
monkeypatch.setattr(dl, "is_rnet_session", lambda s: isinstance(s, _FakeRnetSession))
|
monkeypatch.setattr(dl, "is_rnet_session", lambda s: isinstance(s, _FakeRnetSession))
|
||||||
|
monkeypatch.setattr(dl, "RETRY_WAIT", 0)
|
||||||
session = _FakeRnetSession(PLAIN)
|
session = _FakeRnetSession(PLAIN)
|
||||||
save_path = tmp_path / "sub.vtt"
|
save_path = tmp_path / "sub.vtt"
|
||||||
save_path.with_name("sub.vtt.!dev").write_bytes(PLAIN[:5000])
|
save_path.with_name("sub.vtt.!dev").write_bytes(PLAIN[:5000])
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ SLOW_INDEX = 4
|
|||||||
FAST_BODY = b"F" * 4096
|
FAST_BODY = b"F" * 4096
|
||||||
SLOW_BODY = b"S" * 40960
|
SLOW_BODY = b"S" * 40960
|
||||||
TRICKLE_CHUNK = 2048
|
TRICKLE_CHUNK = 2048
|
||||||
TRICKLE_DELAY = 0.05
|
TRICKLE_DELAY = 0.5
|
||||||
|
|
||||||
|
|
||||||
class _Handler(BaseHTTPRequestHandler):
|
class _Handler(BaseHTTPRequestHandler):
|
||||||
@@ -82,7 +82,7 @@ def test_hedge_loser_releases_handles_before_merge(tmp_path, trickle_server, mon
|
|||||||
slow_hits = trickle_server.counts[f"/seg/{SLOW_INDEX}"]
|
slow_hits = trickle_server.counts[f"/seg/{SLOW_INDEX}"]
|
||||||
assert slow_hits == 2, "hedge never fired; the race under test was not exercised"
|
assert slow_hits == 2, "hedge never fired; the race under test was not exercised"
|
||||||
|
|
||||||
# the batch must not stall draining the loser's stream (trickle alone takes ~1s)
|
# the batch must not stall draining the loser's stream (trickle alone takes ~10s)
|
||||||
assert wall < 5
|
assert wall < 5
|
||||||
|
|
||||||
for i in range(SEGMENTS):
|
for i in range(SEGMENTS):
|
||||||
|
|||||||
@@ -91,9 +91,21 @@ def test_bucket_holds_aggregate_rate_across_threads():
|
|||||||
for t in threads:
|
for t in threads:
|
||||||
t.join()
|
t.join()
|
||||||
achieved = sum(totals) / (time.monotonic() - start)
|
achieved = sum(totals) / (time.monotonic() - start)
|
||||||
# exceeding the limit is a real failure; undershoot is scheduler noise on a loaded box
|
# exceeding the limit is a real failure; undershoot is scheduler noise on a loaded box, so no lower bound
|
||||||
assert achieved <= rate * 1.1
|
assert achieved <= rate * 1.1
|
||||||
assert achieved >= rate * 0.5
|
|
||||||
|
|
||||||
|
def test_bucket_refills_at_the_rate(monkeypatch):
|
||||||
|
"""On a simulated clock the throughput is exact, so a slow refill fails like a fast one."""
|
||||||
|
clock = [0.0]
|
||||||
|
monkeypatch.setattr(requests_downloader, "time", SimpleNamespace(monotonic=lambda: clock[0]))
|
||||||
|
monkeypatch.setattr(
|
||||||
|
requests_downloader, "DOWNLOAD_CANCELLED", SimpleNamespace(wait=lambda s: clock.__setitem__(0, clock[0] + s))
|
||||||
|
)
|
||||||
|
bucket = TokenBucket(1_000)
|
||||||
|
for _ in range(50):
|
||||||
|
bucket.consume(100)
|
||||||
|
assert clock[0] == pytest.approx(5.0)
|
||||||
|
|
||||||
|
|
||||||
def test_bucket_chunk_larger_than_capacity_does_not_deadlock(monkeypatch):
|
def test_bucket_chunk_larger_than_capacity_does_not_deadlock(monkeypatch):
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ class FakeService:
|
|||||||
def run(monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, episodes: dict, **flags: Any) -> Any:
|
def run(monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, episodes: dict, **flags: Any) -> Any:
|
||||||
"""Return the Selected ids, the exit code, or None for a run that completes."""
|
"""Return the Selected ids, the exit code, or None for a run that completes."""
|
||||||
monkeypatch.setattr(Tracks, "tree", lambda self, *a, **k: (_ for _ in ()).throw(Selected(self)))
|
monkeypatch.setattr(Tracks, "tree", lambda self, *a, **k: (_ for _ in ()).throw(Selected(self)))
|
||||||
|
monkeypatch.setattr("unshackle.core.providers.search_metadata", lambda *a, **k: None)
|
||||||
cmd = Harness.__new__(Harness)
|
cmd = Harness.__new__(Harness)
|
||||||
cmd.log = logging.getLogger("test_kind_only")
|
cmd.log = logging.getLogger("test_kind_only")
|
||||||
cmd.service = "FAKE"
|
cmd.service = "FAKE"
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ uv run pytest tests/remote -v --live
|
|||||||
Run only fast unit tests:
|
Run only fast unit tests:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
uv run pytest tests/remote -m "unit and not slow"
|
uv run pytest tests/remote -n auto -m "unit and not slow"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Adding a new service to e2e
|
## Adding a new service to e2e
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
@@ -354,9 +353,11 @@ async def test_status_reports_no_session_limit(aiohttp_client, dashboard_cfg, mo
|
|||||||
assert body["max_sessions"] is None
|
assert body["max_sessions"] is None
|
||||||
|
|
||||||
|
|
||||||
async def test_dashboard_health(aiohttp_client, dashboard_cfg) -> None:
|
async def test_dashboard_health(aiohttp_client, dashboard_cfg, monkeypatch) -> None:
|
||||||
from unshackle.core.api import handlers
|
from unshackle.core.api import handlers
|
||||||
|
|
||||||
|
monkeypatch.setattr(config, "key_vaults", [])
|
||||||
|
monkeypatch.setattr(config, "proxy_providers", {})
|
||||||
handlers._health_cache.clear()
|
handlers._health_cache.clear()
|
||||||
client = await aiohttp_client(make_app())
|
client = await aiohttp_client(make_app())
|
||||||
body = await (await client.get("/api/dashboard/health", headers={"X-Secret-Key": "dash-secret"})).json()
|
body = await (await client.get("/api/dashboard/health", headers={"X-Secret-Key": "dash-secret"})).json()
|
||||||
@@ -617,10 +618,12 @@ async def test_health_probe_timeout_reports_what_finished(aiohttp_client, dashbo
|
|||||||
"""A vault that accepts the connection then stalls must not hang the panel forever."""
|
"""A vault that accepts the connection then stalls must not hang the panel forever."""
|
||||||
from unshackle.core.api import handlers
|
from unshackle.core.api import handlers
|
||||||
|
|
||||||
|
released = threading.Event()
|
||||||
|
|
||||||
def stalling_probe(checks=None):
|
def stalling_probe(checks=None):
|
||||||
checks = [] if checks is None else checks
|
checks = [] if checks is None else checks
|
||||||
checks.append({"id": "ffmpeg", "label": "ffmpeg", "status": "ok", "detail": "7.1", "ms": 1.0})
|
checks.append({"id": "ffmpeg", "label": "ffmpeg", "status": "ok", "detail": "7.1", "ms": 1.0})
|
||||||
time.sleep(6) # the stalled vault; the deadline fires long before this returns
|
released.wait(6) # the stalled vault; the deadline fires long before this returns
|
||||||
checks.append({"id": "never", "label": "never", "status": "ok", "detail": "", "ms": 0.0})
|
checks.append({"id": "never", "label": "never", "status": "ok", "detail": "", "ms": 0.0})
|
||||||
return checks
|
return checks
|
||||||
|
|
||||||
@@ -629,7 +632,10 @@ async def test_health_probe_timeout_reports_what_finished(aiohttp_client, dashbo
|
|||||||
monkeypatch.setattr(handlers, "HEALTH_PROBE_TIMEOUT", 1.0)
|
monkeypatch.setattr(handlers, "HEALTH_PROBE_TIMEOUT", 1.0)
|
||||||
|
|
||||||
client = await aiohttp_client(make_app())
|
client = await aiohttp_client(make_app())
|
||||||
body = await (await client.get("/api/dashboard/health", headers={"X-Secret-Key": "dash-secret"})).json()
|
try:
|
||||||
|
body = await (await client.get("/api/dashboard/health", headers={"X-Secret-Key": "dash-secret"})).json()
|
||||||
|
finally:
|
||||||
|
released.set() # an interpreter exit joins the probe thread, so do not leave it asleep
|
||||||
|
|
||||||
ids = [c["id"] for c in body["checks"]]
|
ids = [c["id"] for c in body["checks"]]
|
||||||
assert "ffmpeg" in ids, "the probe that finished before the deadline must survive"
|
assert "ffmpeg" in ids, "the probe that finished before the deadline must survive"
|
||||||
|
|||||||
@@ -233,13 +233,7 @@ async def test_cors_preflight_returns_headers(make_app, aiohttp_client) -> None:
|
|||||||
assert "X-Secret-Key" in resp.headers["Access-Control-Allow-Headers"]
|
assert "X-Secret-Key" in resp.headers["Access-Control-Allow-Headers"]
|
||||||
|
|
||||||
|
|
||||||
async def test_health_endpoint_responds_ok(make_app, aiohttp_client, monkeypatch: pytest.MonkeyPatch) -> None:
|
async def test_health_endpoint_responds_ok(make_app, aiohttp_client) -> None:
|
||||||
from unshackle.core.api import routes as routes_mod
|
|
||||||
|
|
||||||
async def no_update(_):
|
|
||||||
return None
|
|
||||||
|
|
||||||
monkeypatch.setattr(routes_mod.UpdateChecker, "check_for_updates", no_update)
|
|
||||||
app = make_app(remote_only=True)
|
app = make_app(remote_only=True)
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
resp = await client.get("/api/health")
|
resp = await client.get("/api/health")
|
||||||
@@ -249,14 +243,7 @@ async def test_health_endpoint_responds_ok(make_app, aiohttp_client, monkeypatch
|
|||||||
assert "version" in body
|
assert "version" in body
|
||||||
|
|
||||||
|
|
||||||
async def test_health_bypasses_api_key_auth_middleware(make_app, aiohttp_client, monkeypatch) -> None:
|
async def test_health_bypasses_api_key_auth_middleware(make_app, aiohttp_client) -> None:
|
||||||
from unshackle.core.api import routes as routes_mod
|
|
||||||
|
|
||||||
async def no_update(_):
|
|
||||||
return None
|
|
||||||
|
|
||||||
monkeypatch.setattr(routes_mod.UpdateChecker, "check_for_updates", no_update)
|
|
||||||
|
|
||||||
app = make_app(remote_only=True, with_auth_middleware=True)
|
app = make_app(remote_only=True, with_auth_middleware=True)
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
resp = await client.get("/api/health")
|
resp = await client.get("/api/health")
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ def test_a_music_post_script_command_builds_the_argv_a_script_would_get(monkeypa
|
|||||||
dispatch("success", "file", context)
|
dispatch("success", "file", context)
|
||||||
|
|
||||||
for _ in range(100):
|
for _ in range(100):
|
||||||
if marker.exists():
|
if marker.exists() and marker.stat().st_size:
|
||||||
break
|
break
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
|||||||
@@ -554,6 +554,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/e8/35/4a113189f7138035a21bd255d30dc7bffc77c942c93b7948d2eac2e22429/ECPy-1.2.5-py3-none-any.whl", hash = "sha256:559c92e42406d9d1a6b2b8fc26e6ad7bc985f33903b72f426a56cb1073a25ce3", size = 43075, upload-time = "2020-10-26T11:56:13.613Z" },
|
{ url = "https://files.pythonhosted.org/packages/e8/35/4a113189f7138035a21bd255d30dc7bffc77c942c93b7948d2eac2e22429/ECPy-1.2.5-py3-none-any.whl", hash = "sha256:559c92e42406d9d1a6b2b8fc26e6ad7bc985f33903b72f426a56cb1073a25ce3", size = 43075, upload-time = "2020-10-26T11:56:13.613Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "execnet"
|
||||||
|
version = "2.1.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastjsonschema"
|
name = "fastjsonschema"
|
||||||
version = "2.19.1"
|
version = "2.19.1"
|
||||||
@@ -1683,6 +1692,19 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/20/7f/338843f449ace853647ace35870874f69a764d251872ed1b4de9f234822c/pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", size = 19694, upload-time = "2025-03-25T06:22:27.807Z" },
|
{ url = "https://files.pythonhosted.org/packages/20/7f/338843f449ace853647ace35870874f69a764d251872ed1b4de9f234822c/pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", size = 19694, upload-time = "2025-03-25T06:22:27.807Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-xdist"
|
||||||
|
version = "3.8.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "execnet" },
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-discovery"
|
name = "python-discovery"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -2205,6 +2227,7 @@ test = [
|
|||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
{ name = "pytest-aiohttp" },
|
{ name = "pytest-aiohttp" },
|
||||||
{ name = "pytest-asyncio" },
|
{ name = "pytest-asyncio" },
|
||||||
|
{ name = "pytest-xdist" },
|
||||||
{ name = "pyyaml" },
|
{ name = "pyyaml" },
|
||||||
{ name = "responses" },
|
{ name = "responses" },
|
||||||
]
|
]
|
||||||
@@ -2274,6 +2297,7 @@ test = [
|
|||||||
{ name = "pytest", specifier = ">=8.3.0,<9" },
|
{ name = "pytest", specifier = ">=8.3.0,<9" },
|
||||||
{ name = "pytest-aiohttp", specifier = ">=1.0.5,<2" },
|
{ name = "pytest-aiohttp", specifier = ">=1.0.5,<2" },
|
||||||
{ name = "pytest-asyncio", specifier = ">=0.24.0,<1" },
|
{ name = "pytest-asyncio", specifier = ">=0.24.0,<1" },
|
||||||
|
{ name = "pytest-xdist", specifier = ">=3.6.0,<4" },
|
||||||
{ name = "pyyaml", specifier = ">=6.0.1,<7" },
|
{ name = "pyyaml", specifier = ">=6.0.1,<7" },
|
||||||
{ name = "responses", specifier = ">=0.25.0,<1" },
|
{ name = "responses", specifier = ">=0.25.0,<1" },
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user