diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml new file mode 100644 index 000000000..24d3bae38 --- /dev/null +++ b/.github/workflows/codspeed.yml @@ -0,0 +1,30 @@ +--- +name: CodSpeed + +on: + push: + branches: + - main + pull_request: + # `workflow_dispatch` allows CodSpeed to trigger backtest + # performance analysis in order to generate initial data. + workflow_dispatch: + +jobs: + benchmarks: + name: Run benchmarks + runs-on: ubuntu-latest + permissions: + contents: read # required for actions/checkout + id-token: write # required for OIDC authentication with CodSpeed + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + with: + python-version: "3.12" + - name: Install dependencies + run: uv sync + - name: Run benchmarks + uses: CodSpeedHQ/action@v4 + with: + run: uv run pytest test/ --codspeed diff --git a/.gitignore b/.gitignore index 05b140a34..9dd2f4789 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ __pycache__/ *.py[cod] *.egg-info/ +.codspeed/ .pytest_cache/ .python-version .ruff_cache/ diff --git a/README.rst b/README.rst index ebb77428a..e51f7ac95 100644 --- a/README.rst +++ b/README.rst @@ -7,6 +7,10 @@ :target: https://codecov.io/gh/taskcluster/taskgraph :alt: Code Coverage +.. image:: https://img.shields.io/endpoint?url=https://codspeed.io/badge.json + :target: https://codspeed.io/taskcluster/taskgraph?utm_source=badge + :alt: CodSpeed Performance + .. image:: https://badge.fury.io/py/taskcluster-taskgraph.svg :target: https://badge.fury.io/py/taskcluster-taskgraph :alt: Pypi Version diff --git a/pyproject.toml b/pyproject.toml index e66e84e9f..0cc1ce4c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ dev = [ "pre-commit>=4.3.0", "pyright>=1.1.406", "pytest>=8.4.2", + "pytest-codspeed>=3.2.0", "pytest-mock>=3.15.1", "pytest-taskgraph>=0.2.0", "responses>=0.25.8", diff --git a/test/test_graph_perf.py b/test/test_graph_perf.py new file mode 100644 index 000000000..ae3bebf47 --- /dev/null +++ b/test/test_graph_perf.py @@ -0,0 +1,218 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +"""Benchmarks for taskgraph core operations on graphs with ~1000 tasks.""" + +import copy + +import pytest + +from taskgraph.graph import Graph +from taskgraph.task import Task +from taskgraph.taskgraph import TaskGraph +from taskgraph.transforms.base import TransformSequence + +# --------------------------------------------------------------------------- +# Graph builders – each returns (tasks_dict, Graph, TaskGraph) for 1000 nodes +# --------------------------------------------------------------------------- + +N = 1000 + + +def _make_task(i): + return Task( + kind="test", + label=f"task-{i}", + attributes={"idx": i}, + task={"metadata": {"name": f"task-{i}"}}, + ) + + +def _build_linear(): + """Chain: task-0 <- task-1 <- ... <- task-999.""" + tasks = {f"task-{i}": _make_task(i) for i in range(N)} + edges = {(f"task-{i}", f"task-{i - 1}", "dep") for i in range(1, N)} + g = Graph(set(tasks), edges) + return tasks, g, TaskGraph(tasks, g) + + +def _build_fan_out_fan_in(): + """One root fans out to 998 middle tasks, which all feed into one sink. + + task-0 (root) + |---> task-1 --+ + |---> task-2 --+--> task-999 (sink) + ... | + `---> task-998 + + """ + tasks = {f"task-{i}": _make_task(i) for i in range(N)} + edges = set() + for i in range(1, N - 1): + edges.add((f"task-{i}", "task-0", "root")) + for i in range(1, N - 1): + edges.add((f"task-{N - 1}", f"task-{i}", "mid")) + g = Graph(set(tasks), edges) + return tasks, g, TaskGraph(tasks, g) + + +def _build_binary_tree(): + """Complete-ish binary tree with ~1000 nodes (children depend on parent).""" + tasks = {f"task-{i}": _make_task(i) for i in range(N)} + edges = set() + for i in range(1, N): + parent = (i - 1) // 2 + edges.add((f"task-{i}", f"task-{parent}", "parent")) + g = Graph(set(tasks), edges) + return tasks, g, TaskGraph(tasks, g) + + +def _build_diamond_layers(): + """Layered diamond: 10 layers of 100 tasks each; every task in layer L + depends on every task in layer L-1 (100x100 edges between adjacent layers). + + This is 1000 tasks with 9 * 100 * 100 = 90 000 edges - a dense graph. + """ + layer_size = 100 + num_layers = N // layer_size + tasks = {f"task-{i}": _make_task(i) for i in range(N)} + edges = set() + for layer in range(1, num_layers): + for i in range(layer_size): + node = layer * layer_size + i + for j in range(layer_size): + dep = (layer - 1) * layer_size + j + edges.add((f"task-{node}", f"task-{dep}", f"l{layer}-{j}")) + g = Graph(set(tasks), edges) + return tasks, g, TaskGraph(tasks, g) + + +# Pre-build the fixtures so construction cost isn't part of every benchmark +LINEAR = _build_linear() +FAN = _build_fan_out_fan_in() +BTREE = _build_binary_tree() +DIAMOND = _build_diamond_layers() + +GEOMETRIES = { + "linear": LINEAR, + "fan": FAN, + "btree": BTREE, + "diamond": DIAMOND, +} + + +# --------------------------------------------------------------------------- +# Benchmarks – Graph.transitive_closure +# --------------------------------------------------------------------------- + + +@pytest.mark.benchmark +@pytest.mark.parametrize("geometry", ["linear", "fan", "btree", "diamond"]) +def test_transitive_closure(geometry): + _, graph, _ = GEOMETRIES[geometry] + # Pick a node roughly in the middle so the closure is non-trivial + result = graph.transitive_closure({f"task-{N // 2}"}) + assert len(result.nodes) > 0 + + +# --------------------------------------------------------------------------- +# Benchmarks – Graph.visit_postorder / visit_preorder +# --------------------------------------------------------------------------- + + +@pytest.mark.benchmark +@pytest.mark.parametrize("geometry", ["linear", "fan", "btree", "diamond"]) +def test_visit_postorder(geometry): + _, graph, _ = GEOMETRIES[geometry] + order = list(graph.visit_postorder()) + assert len(order) == N + + +@pytest.mark.benchmark +@pytest.mark.parametrize("geometry", ["linear", "fan", "btree", "diamond"]) +def test_visit_preorder(geometry): + _, graph, _ = GEOMETRIES[geometry] + order = list(graph.visit_preorder()) + assert len(order) == N + + +# --------------------------------------------------------------------------- +# Benchmarks – Graph link dictionaries +# --------------------------------------------------------------------------- + + +@pytest.mark.benchmark +@pytest.mark.parametrize("geometry", ["linear", "fan", "btree", "diamond"]) +def test_links_dict(geometry): + _, graph, _ = GEOMETRIES[geometry] + # Clear the functools.cache to measure actual computation each time + graph.links_and_reverse_links_dict.cache_clear() + fwd, rev = graph.links_and_reverse_links_dict() + assert len(fwd) == N + assert len(rev) == N + + +# --------------------------------------------------------------------------- +# Benchmarks – TaskGraph.for_each_task +# --------------------------------------------------------------------------- + + +@pytest.mark.benchmark +@pytest.mark.parametrize("geometry", ["linear", "fan", "btree", "diamond"]) +def test_for_each_task(geometry): + _, _, tg = GEOMETRIES[geometry] + visited = [] + tg.for_each_task(lambda task, _tg: visited.append(task.label)) + assert len(visited) == N + + +# --------------------------------------------------------------------------- +# Benchmarks – TaskGraph JSON round-trip +# --------------------------------------------------------------------------- + + +@pytest.mark.benchmark +@pytest.mark.parametrize("geometry", ["linear", "fan", "btree"]) +def test_taskgraph_to_json(geometry): + _, _, tg = GEOMETRIES[geometry] + data = tg.to_json() + assert len(data) == N + + +# --------------------------------------------------------------------------- +# Benchmarks – TransformSequence with a simple transform +# --------------------------------------------------------------------------- + + +transforms = TransformSequence() + + +@transforms.add +def add_worker_type(config, tasks): + """A small transform that adds a field to every task dict.""" + for task in tasks: + task = copy.copy(task) + task["worker-type"] = f"b-linux-{task.get('idx', 0) % 4}" + yield task + + +@transforms.add +def add_env_vars(config, tasks): + """Another small transform that adds environment variables.""" + for task in tasks: + task.setdefault("env", {}) + task["env"]["TASK_ID"] = task.get("name", "unknown") + task["env"]["PRIORITY"] = "high" if task.get("idx", 0) % 10 == 0 else "low" + yield task + + +@pytest.mark.benchmark +def test_transform_sequence(): + task_dicts = [ + {"name": f"task-{i}", "idx": i, "payload": {"command": f"echo {i}"}} + for i in range(N) + ] + result = list(transforms(None, task_dicts)) + assert len(result) == N + assert "worker-type" in result[0] + assert "env" in result[0] diff --git a/uv.lock b/uv.lock index 1d2d8b413..c28dbc86d 100644 --- a/uv.lock +++ b/uv.lock @@ -2319,6 +2319,48 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] +[[package]] +name = "pytest-codspeed" +version = "5.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/b4/cf932fcd1960a2fd6d9b09eb403253a8709aeee975961afa6299239a830e/pytest_codspeed-5.0.3.tar.gz", hash = "sha256:91afef90e6a96b013495e4702ef5d6358614a449e71008cdc194ef668778b92f", size = 324571, upload-time = "2026-05-22T16:20:49.231Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/f5/a8f70147216e4b84046ca406d03ecc8e83e3ea56ba1bdca0bb79cca79fee/pytest_codspeed-5.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:005348ea52ace3ede2e2f595913912ad2564cca7b124211a88dc78a9cb1fca63", size = 366249, upload-time = "2026-05-22T16:20:39.985Z" }, + { url = "https://files.pythonhosted.org/packages/f6/bd/7a4dbcf457fcc3ed788c55d402f3af2671e0e342b6098090fd590aa8712e/pytest_codspeed-5.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbe6a4a00b449b6ba2771f644cbc38bdf55acf5c812e60e5659110e19dd9f510", size = 932229, upload-time = "2026-05-22T16:20:37.283Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e1/414ea4c66559f24ec06aeb6db62bfc7079582dac1452e648affe1eb5cfb4/pytest_codspeed-5.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ac4344f34bbcdd17f6f8c30dbac3da2f80d223dd112e568fd7f7c2cd4cbc693", size = 934647, upload-time = "2026-05-22T16:20:31.997Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ef/32ce60d42a4aa43e728d988e13eb6568fbc7b10a514517b459bafd3f2b94/pytest_codspeed-5.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f56d0339cd98d26f6e561987be25bdd2761a5d53d8f73493b1ebe02d0d451093", size = 366253, upload-time = "2026-05-22T16:21:10.013Z" }, + { url = "https://files.pythonhosted.org/packages/2a/15/c66ef90a793c5d2c039e63a1726a5e55c678be2618b0f5f1660d0f79e25f/pytest_codspeed-5.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c682f6645d4eb472f3bd95dbda1805e3af4243610572cb7d6bf94a88e8a0b6c", size = 932465, upload-time = "2026-05-22T16:20:34.265Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7b/d231279301967f05b7909160489e85ee3a1b9da76094ea25343faba1abc2/pytest_codspeed-5.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f852bee785a7a124cb1720b1915670c6742af87747dc4d838f3ffdbd365ce9d9", size = 934925, upload-time = "2026-05-22T16:20:47.63Z" }, + { url = "https://files.pythonhosted.org/packages/c2/22/456c48160b761d5028c8afa119f085a9fc42855a783a13d73918078969f0/pytest_codspeed-5.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2eeb25fb1ac3f73c4de50e739e78fea396b89782bdb740bf2a7cd2df21f8d4ee", size = 366255, upload-time = "2026-05-22T16:20:56.214Z" }, + { url = "https://files.pythonhosted.org/packages/74/33/ac7441fa937c9d9f158083a8c46920a5a5c81ed3c5f96240fc8d650db5c2/pytest_codspeed-5.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73c5c9d98a3372a42611989ccfa437cce3842431ac6d6b9ab42c4f0e59c070f7", size = 932325, upload-time = "2026-05-22T16:21:08.814Z" }, + { url = "https://files.pythonhosted.org/packages/77/bc/8b994adcb9e9016e7d9a808056a3dd9cca21441e432ef456eae2b697d7fe/pytest_codspeed-5.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2e0ab65df73e837666d12357280ca50ff6d6ac03ea5266703be518b68170edf", size = 934885, upload-time = "2026-05-22T16:21:01.444Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8e/e032451e9e0a06b0c4bff53105f62b693d9a54595dd8c024693741ce3380/pytest_codspeed-5.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6524c57fec279a22ffef6112af404036afc71b4704758ae9f0abda429b8478d4", size = 366253, upload-time = "2026-05-22T16:20:46.192Z" }, + { url = "https://files.pythonhosted.org/packages/a9/7b/ae76fd8ac656b9695806a6aafd5f22ec32e6ce20e266a58f9112e01d3cd8/pytest_codspeed-5.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c383c9121deb58a69f174188e9e4488ffc0daced0ed276abf87747182511901", size = 932360, upload-time = "2026-05-22T16:20:30.589Z" }, + { url = "https://files.pythonhosted.org/packages/a6/4a/dfd43d943fdb143be4fd62f34c2793ba349dc27aa188e521d19d629aa7ab/pytest_codspeed-5.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4bcdb4b6522738152885ef067e0c8524d5699828d780fb6f464cdb3db44369c", size = 934928, upload-time = "2026-05-22T16:20:38.62Z" }, + { url = "https://files.pythonhosted.org/packages/04/6a/fdcec19c7f267c195f147c51d3fd2245f6b8d09b80495ed0a90c008e0842/pytest_codspeed-5.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:25464363c7f9b9bd5022e969c0addba616fa40ac9b8f0fc9e030c4538863b32d", size = 366259, upload-time = "2026-05-22T16:21:06.039Z" }, + { url = "https://files.pythonhosted.org/packages/6a/96/c6b03b81dcd21ae3d6b32cca0b3c10149fa378eb21b338d4b63c9eb8050b/pytest_codspeed-5.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efd43f82ea03ced8488a767ded9473f050791ab7783ea8654107e1e0ac66af40", size = 932395, upload-time = "2026-05-22T16:21:04.804Z" }, + { url = "https://files.pythonhosted.org/packages/96/08/56ad8f1cc7d6962f8a680141b361e93467a2abc53d976cd9d5e1edd740e3/pytest_codspeed-5.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:782f9985b6f6b45b8bc20152d206d3a52b56dd088ba81cb70a71f0b39841be9e", size = 934994, upload-time = "2026-05-22T16:20:28.809Z" }, + { url = "https://files.pythonhosted.org/packages/0b/54/9096c4545f09da94b1b00f3be2fe4952949e86c9bcafca9a29b26aed1a75/pytest_codspeed-5.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9aa0815b90196f3c20d736ea8691381e97f12bbe8c7d87af10a351e434b452cb", size = 366311, upload-time = "2026-05-22T16:20:41.791Z" }, + { url = "https://files.pythonhosted.org/packages/a7/3c/24c53f67a38ad48cb087105ac30a8aa0923223ee274ea9bf2dc705edaa59/pytest_codspeed-5.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:85505c96a3477c346ec2d2b7dced8478f4c651e2b1666ee102d53a832b511853", size = 933169, upload-time = "2026-05-22T16:20:43.178Z" }, + { url = "https://files.pythonhosted.org/packages/d1/de/2213f868fa7694f743f96cccbc07e757f45c920c523cccc2da97bc8652df/pytest_codspeed-5.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20eba63765be9d1b6cacbbfad84b87d49eb04b357a7045a0899880da181f81e3", size = 935522, upload-time = "2026-05-22T16:21:03.398Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/5dfea1c031d6cccc11653464828edf205c30f798caf5b2a85375aacd914a/pytest_codspeed-5.0.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:ec9fa6f0af0a9feb0e0bd517fb59ef28f806fbd50c0c6900ac26cbb4d080eba5", size = 366275, upload-time = "2026-05-22T16:20:59.463Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2b/af4d1b612f03b98a6cf3c7d5f62678917a60110a8bf380d49ab408b31137/pytest_codspeed-5.0.3-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8df77b3409f54f4a268f77f3ff74992fe1d995cdbaf2cecf8ad74d32db217ce7", size = 932537, upload-time = "2026-05-22T16:20:54.945Z" }, + { url = "https://files.pythonhosted.org/packages/f5/a2/c7ec45e36a61b418efb2a3cccaa67a0c2fcf1f21d5880f64c33114f0c249/pytest_codspeed-5.0.3-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5d8695a227ea1c3a41d25db5b3fe720bf1b4808bd38862be811a4efd902c792", size = 934153, upload-time = "2026-05-22T16:21:07.494Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c7/d5bada9618a0af56a5c8065fc61280849cab8e7c1e24025807a51c3157ce/pytest_codspeed-5.0.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bf4cc4178cbace8f4d2bd240408276bc4da3850ac5fcb5fb5f8a74ab417615bb", size = 366339, upload-time = "2026-05-22T16:20:51.968Z" }, + { url = "https://files.pythonhosted.org/packages/a8/37/fb27aeb40a81320e7349553b877a21333c897b27c8dfe215630452908f36/pytest_codspeed-5.0.3-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abe793da40f87295d33988673d34f06ea569848b44490b847552cd416816258a", size = 933055, upload-time = "2026-05-22T16:20:44.861Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d9/6f2d69e96deaf0475a695fc9195af59e7a3b5fab50782855e65c63a7bc28/pytest_codspeed-5.0.3-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3a9ed38dfa776443b86f4b49a982e8443d0953db4974bd2673d63cc904ae1ad", size = 934481, upload-time = "2026-05-22T16:20:58.264Z" }, + { url = "https://files.pythonhosted.org/packages/27/35/3b5de2d65dd303d74bf6c46f9ac20fc02df8890f6e6d1894e7da99acdf83/pytest_codspeed-5.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bce0a6ea93a5b19658f713312bb67554c19283ab15b454a1e3e55a13e78130f8", size = 366245, upload-time = "2026-05-22T16:20:53.551Z" }, + { url = "https://files.pythonhosted.org/packages/d8/66/e80c7ae68334c5c703c474809701c5db40a1b826b72c9e2c24209b578b84/pytest_codspeed-5.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a2097247985f5d915a94b80c5552d10979ca858c859fc3edef1bf2baa5c9b7a", size = 932061, upload-time = "2026-05-22T16:20:35.929Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f8/597fe524d8536d0da9913a81e6abaf3e57276eef37764dadb780df703c78/pytest_codspeed-5.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e192905a2230f9956e6160732f76577836953a4a1fb2b1e7be74e51ac7b2a0", size = 934430, upload-time = "2026-05-22T16:20:50.343Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b2/1d2a993c532146dce9eca5b5942d51898021c3579ce18b2454f932a915f8/pytest_codspeed-5.0.3-py3-none-any.whl", hash = "sha256:fe2ea83c924c2250675b75686c3ee456b8cf0208d83d552e182a195fdf467378", size = 74033, upload-time = "2026-05-22T16:20:26.814Z" }, +] + [[package]] name = "pytest-mock" version = "3.15.1" @@ -3031,6 +3073,7 @@ dev = [ { name = "pyright" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pytest", version = "9.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest-codspeed" }, { name = "pytest-mock" }, { name = "pytest-taskgraph" }, { name = "responses" }, @@ -3076,6 +3119,7 @@ dev = [ { name = "pre-commit", specifier = ">=4.3.0" }, { name = "pyright", specifier = ">=1.1.406" }, { name = "pytest", specifier = ">=8.4.2" }, + { name = "pytest-codspeed", specifier = ">=3.2.0" }, { name = "pytest-mock", specifier = ">=3.15.1" }, { name = "pytest-taskgraph", editable = "packages/pytest-taskgraph" }, { name = "responses", specifier = ">=0.25.8" },