diff --git a/.github/workflows/benchmarks.yaml b/.github/workflows/benchmarks.yaml index fa1b8480..d106b179 100644 --- a/.github/workflows/benchmarks.yaml +++ b/.github/workflows/benchmarks.yaml @@ -1,83 +1,48 @@ name: Benchmarks on: + push: + branches: + - main pull_request: types: - opened - synchronize + # `workflow_dispatch` allows CodSpeed to trigger backtest + # performance analysis in order to generate initial data. + workflow_dispatch: jobs: benchmark: - name: Benchmark tests + name: Run benchmarks runs-on: ubuntu-latest permissions: contents: read - pull-requests: write - strategy: - matrix: - python_version: [3.12] + id-token: write steps: - - name: Checkout branch + - name: Checkout uses: actions/checkout@v4 - with: - path: pr - - - name: Checkout main - uses: actions/checkout@v4 - with: - ref: main - path: main - name: Install python uses: actions/setup-python@v5 with: - python-version: ${{matrix.python_version}} + python-version: "3.12" - name: Install uv uses: astral-sh/setup-uv@v4 with: enable-cache: true - cache-dependency-glob: "main/uv.lock" - - - name: Setup benchmarks - run: | - echo "BASE_SHA=$(echo ${{ github.event.pull_request.base.sha }} | cut -c1-8)" >> $GITHUB_ENV - echo "HEAD_SHA=$(echo ${{ github.event.pull_request.head.sha }} | cut -c1-8)" >> $GITHUB_ENV - echo "PR_COMMENT=$(mktemp)" >> $GITHUB_ENV - - - name: Run benchmarks on PR - working-directory: ./pr - run: | - uv sync --group test - uv run pytest --benchmark-only --benchmark-save=pr - - - name: Run benchmarks on main - working-directory: ./main - continue-on-error: true - run: | - uv sync --group test - uv run pytest --benchmark-only --benchmark-save=base + cache-dependency-glob: "uv.lock" - - name: Compare results - continue-on-error: false - run: | - uvx pytest-benchmark compare **/.benchmarks/**/*.json | tee cmp_results + - name: Install project + run: uv sync --group test - echo 'Benchmark comparison for [`${{ env.BASE_SHA }}`](${{ github.event.repository.html_url }}/commit/${{ github.event.pull_request.base.sha }}) (base) vs [`${{ env.HEAD_SHA }}`](${{ github.event.repository.html_url }}/commit/${{ github.event.pull_request.head.sha }}) (PR)' >> pr_comment - echo '```' >> pr_comment - cat cmp_results >> pr_comment - echo '```' >> pr_comment - cat pr_comment > ${{ env.PR_COMMENT }} - - - name: Comment on PR - uses: actions/github-script@v7 + - name: Run benchmarks + uses: CodSpeedHQ/action@v4 + env: + RAY_ENABLE_UV_RUN_RUNTIME_ENV: 0 + PLUGBOARD_IO_READ_TIMEOUT: 5.0 with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: require('fs').readFileSync('${{ env.PR_COMMENT }}').toString() - }); + mode: walltime + run: uv run pytest tests/benchmark/ --codspeed diff --git a/pyproject.toml b/pyproject.toml index 3b5786c2..e7c09cfa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,7 +80,7 @@ test = [ "optuna>=3.0,<5", "pytest>=8.3,<10", "pytest-asyncio>=1.0,<2", - "pytest-benchmark>=5.1.0", + "pytest-codspeed>=4.3.0", "pytest-cases>=3.8,<4", "pytest-env>=1.1,<2", "pytest-rerunfailures>=15.0,<17", diff --git a/tests/benchmark/test_benchmarking.py b/tests/benchmark/test_benchmarking.py index 7554a7a0..15f2acd9 100644 --- a/tests/benchmark/test_benchmarking.py +++ b/tests/benchmark/test_benchmarking.py @@ -1,35 +1,83 @@ -"""Simple benchmark tests for Plugboard models.""" +"""Benchmark tests for Plugboard processes.""" import asyncio -from pytest_benchmark.fixture import BenchmarkFixture +import pytest +from pytest_codspeed import BenchmarkFixture -from plugboard.connector import AsyncioConnector -from plugboard.process import LocalProcess, Process +from plugboard.connector import AsyncioConnector, Connector, RayConnector, ZMQConnector +from plugboard.process import LocalProcess, Process, RayProcess from plugboard.schemas import ConnectorSpec from tests.integration.test_process_with_components_run import A, B -def _setup_process() -> tuple[tuple[Process], dict]: - comp_a = A(name="comp_a", iters=1000) +ITERS = 1000 + +CONNECTOR_PROCESS_PARAMS = [ + (AsyncioConnector, LocalProcess), + (ZMQConnector, LocalProcess), + (RayConnector, RayProcess), +] +CONNECTOR_PROCESS_IDS = ["asyncio", "zmq", "ray"] + + +def _build_process(connector_cls: type[Connector], process_cls: type[Process]) -> Process: + """Build a process with the given connector and process class.""" + comp_a = A(name="comp_a", iters=ITERS) comp_b1 = B(name="comp_b1", factor=1) comp_b2 = B(name="comp_b2", factor=2) components = [comp_a, comp_b1, comp_b2] connectors = [ - AsyncioConnector(spec=ConnectorSpec(source="comp_a.out_1", target="comp_b1.in_1")), - AsyncioConnector(spec=ConnectorSpec(source="comp_b1.out_1", target="comp_b2.in_1")), + connector_cls(spec=ConnectorSpec(source="comp_a.out_1", target="comp_b1.in_1")), + connector_cls(spec=ConnectorSpec(source="comp_b1.out_1", target="comp_b2.in_1")), ] - process = LocalProcess(components=components, connectors=connectors) - # Initialise process so that this is excluded from the benchmark timing - asyncio.run(process.init()) - # Return args and kwargs tuple for benchmark.pedantic - return (process,), {} + return process_cls(components=components, connectors=connectors) + + +@pytest.mark.parametrize( + "connector_cls, process_cls", + CONNECTOR_PROCESS_PARAMS, + ids=CONNECTOR_PROCESS_IDS, +) +def test_benchmark_process_run( + benchmark: BenchmarkFixture, + connector_cls: type[Connector], + process_cls: type[Process], + ray_ctx: None, +) -> None: + """Benchmark running of a Plugboard Process.""" + + def _setup() -> tuple[tuple[Process], dict]: + async def _init() -> Process: + process = _build_process(connector_cls, process_cls) + await process.init() + return process + + return (asyncio.run(_init()),), {} + + def _run(process: Process) -> None: + asyncio.run(process.run()) + + benchmark.pedantic(_run, setup=_setup, rounds=5) -def _run_process(process: Process) -> None: - asyncio.run(process.run()) +@pytest.mark.benchmark +@pytest.mark.parametrize( + "connector_cls, process_cls", + CONNECTOR_PROCESS_PARAMS, + ids=CONNECTOR_PROCESS_IDS, +) +def test_benchmark_process_lifecycle( + connector_cls: type[Connector], + process_cls: type[Process], + ray_ctx: None, +) -> None: + """Benchmark the full lifecycle (init, run, destroy) of a Plugboard Process.""" + async def _lifecycle() -> None: + process = _build_process(connector_cls, process_cls) + await process.init() + await process.run() + await process.destroy() -def test_benchmark_process_run(benchmark: BenchmarkFixture) -> None: - """Benchmark the running of a Plugboard Process.""" - benchmark.pedantic(_run_process, setup=_setup_process, rounds=5) + asyncio.run(_lifecycle()) diff --git a/uv.lock b/uv.lock index 4372bd4b..c4f1415a 100644 --- a/uv.lock +++ b/uv.lock @@ -692,10 +692,16 @@ sdist = { url = "https://files.pythonhosted.org/packages/92/88/b8527e1b00c1811db wheels = [ { url = "https://files.pythonhosted.org/packages/d3/25/79c98ebe12df31548ba4eaf44db11b7cad6b3e7b4203718335620939083c/caio-0.9.25-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fb7ff95af4c31ad3f03179149aab61097a71fd85e05f89b4786de0359dffd044", size = 36983, upload-time = "2025-12-26T15:21:36.075Z" }, { url = "https://files.pythonhosted.org/packages/a3/2b/21288691f16d479945968a0a4f2856818c1c5be56881d51d4dac9b255d26/caio-0.9.25-cp312-cp312-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:97084e4e30dfa598449d874c4d8e0c8d5ea17d2f752ef5e48e150ff9d240cd64", size = 82012, upload-time = "2025-12-26T15:22:20.983Z" }, + { url = "https://files.pythonhosted.org/packages/03/c4/8a1b580875303500a9c12b9e0af58cb82e47f5bcf888c2457742a138273c/caio-0.9.25-cp312-cp312-manylinux_2_34_aarch64.whl", hash = "sha256:4fa69eba47e0f041b9d4f336e2ad40740681c43e686b18b191b6c5f4c5544bfb", size = 81502, upload-time = "2026-03-04T22:08:22.381Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/0fe770b8ffc8362c48134d1592d653a81a3d8748d764bec33864db36319d/caio-0.9.25-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:6bebf6f079f1341d19f7386db9b8b1f07e8cc15ae13bfdaff573371ba0575d69", size = 80200, upload-time = "2026-03-04T22:08:23.382Z" }, { url = "https://files.pythonhosted.org/packages/31/57/5e6ff127e6f62c9f15d989560435c642144aa4210882f9494204bc892305/caio-0.9.25-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d6c2a3411af97762a2b03840c3cec2f7f728921ff8adda53d7ea2315a8563451", size = 36979, upload-time = "2025-12-26T15:21:35.484Z" }, { url = "https://files.pythonhosted.org/packages/a3/9f/f21af50e72117eb528c422d4276cbac11fb941b1b812b182e0a9c70d19c5/caio-0.9.25-cp313-cp313-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0998210a4d5cd5cb565b32ccfe4e53d67303f868a76f212e002a8554692870e6", size = 81900, upload-time = "2025-12-26T15:22:21.919Z" }, + { url = "https://files.pythonhosted.org/packages/9c/12/c39ae2a4037cb10ad5eb3578eb4d5f8c1a2575c62bba675f3406b7ef0824/caio-0.9.25-cp313-cp313-manylinux_2_34_aarch64.whl", hash = "sha256:1a177d4777141b96f175fe2c37a3d96dec7911ed9ad5f02bac38aaa1c936611f", size = 81523, upload-time = "2026-03-04T22:08:25.187Z" }, + { url = "https://files.pythonhosted.org/packages/22/59/f8f2e950eb4f1a5a3883e198dca514b9d475415cb6cd7b78b9213a0dd45a/caio-0.9.25-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:9ed3cfb28c0e99fec5e208c934e5c157d0866aa9c32aa4dc5e9b6034af6286b7", size = 80243, upload-time = "2026-03-04T22:08:26.449Z" }, { url = "https://files.pythonhosted.org/packages/69/ca/a08fdc7efdcc24e6a6131a93c85be1f204d41c58f474c42b0670af8c016b/caio-0.9.25-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fab6078b9348e883c80a5e14b382e6ad6aabbc4429ca034e76e730cf464269db", size = 36978, upload-time = "2025-12-26T15:21:41.055Z" }, { url = "https://files.pythonhosted.org/packages/5e/6c/d4d24f65e690213c097174d26eda6831f45f4734d9d036d81790a27e7b78/caio-0.9.25-cp314-cp314-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:44a6b58e52d488c75cfaa5ecaa404b2b41cc965e6c417e03251e868ecd5b6d77", size = 81832, upload-time = "2025-12-26T15:22:22.757Z" }, + { url = "https://files.pythonhosted.org/packages/87/a4/e534cf7d2d0e8d880e25dd61e8d921ffcfe15bd696734589826f5a2df727/caio-0.9.25-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:628a630eb7fb22381dd8e3c8ab7f59e854b9c806639811fc3f4310c6bd711d79", size = 81565, upload-time = "2026-03-04T22:08:27.483Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ed/bf81aeac1d290017e5e5ac3e880fd56ee15e50a6d0353986799d1bc5cfd5/caio-0.9.25-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:0ba16aa605ccb174665357fc729cf500679c2d94d5f1458a6f0d5ca48f2060a7", size = 80071, upload-time = "2026-03-04T22:08:28.751Z" }, { url = "https://files.pythonhosted.org/packages/86/93/1f76c8d1bafe3b0614e06b2195784a3765bbf7b0a067661af9e2dd47fc33/caio-0.9.25-py3-none-any.whl", hash = "sha256:06c0bb02d6b929119b1cfbe1ca403c768b2013a369e2db46bfa2a5761cf82e40", size = 19087, upload-time = "2025-12-26T15:22:00.221Z" }, ] @@ -3861,8 +3867,8 @@ all = [ { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-asyncio" }, - { name = "pytest-benchmark" }, { name = "pytest-cases" }, + { name = "pytest-codspeed" }, { name = "pytest-env" }, { name = "pytest-rerunfailures" }, { name = "radon" }, @@ -3907,8 +3913,8 @@ test = [ { name = "optuna" }, { name = "pytest" }, { name = "pytest-asyncio" }, - { name = "pytest-benchmark" }, { name = "pytest-cases" }, + { name = "pytest-codspeed" }, { name = "pytest-env" }, { name = "pytest-rerunfailures" }, { name = "ray", extra = ["default", "tune"] }, @@ -3977,8 +3983,8 @@ all = [ { name = "pre-commit", specifier = ">=3.8,<4" }, { name = "pytest", specifier = ">=8.3,<10" }, { name = "pytest-asyncio", specifier = ">=1.0,<2" }, - { name = "pytest-benchmark", specifier = ">=5.1.0" }, { name = "pytest-cases", specifier = ">=3.8,<4" }, + { name = "pytest-codspeed", specifier = ">=4.3.0" }, { name = "pytest-env", specifier = ">=1.1,<2" }, { name = "pytest-rerunfailures", specifier = ">=15.0,<17" }, { name = "radon", specifier = ">=6.0.1,<7" }, @@ -4023,8 +4029,8 @@ test = [ { name = "optuna", specifier = ">=3.0,<5" }, { name = "pytest", specifier = ">=8.3,<10" }, { name = "pytest-asyncio", specifier = ">=1.0,<2" }, - { name = "pytest-benchmark", specifier = ">=5.1.0" }, { name = "pytest-cases", specifier = ">=3.8,<4" }, + { name = "pytest-codspeed", specifier = ">=4.3.0" }, { name = "pytest-env", specifier = ">=1.1,<2" }, { name = "pytest-rerunfailures", specifier = ">=15.0,<17" }, { name = "ray", extras = ["default", "tune"], specifier = ">=2.40.0,<3" }, @@ -4260,15 +4266,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, ] -[[package]] -name = "py-cpuinfo" -version = "9.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/37/a8/d832f7293ebb21690860d2e01d8115e5ff6f2ae8bbdc953f0eb0fa4bd2c7/py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690", size = 104716, upload-time = "2022-10-25T20:38:06.303Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335, upload-time = "2022-10-25T20:38:27.636Z" }, -] - [[package]] name = "py-partiql-parser" version = "0.6.3" @@ -4541,19 +4538,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, ] -[[package]] -name = "pytest-benchmark" -version = "5.2.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "py-cpuinfo" }, - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/24/34/9f732b76456d64faffbef6232f1f9dbec7a7c4999ff46282fa418bd1af66/pytest_benchmark-5.2.3.tar.gz", hash = "sha256:deb7317998a23c650fd4ff76e1230066a76cb45dcece0aca5607143c619e7779", size = 341340, upload-time = "2025-11-09T18:48:43.215Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/33/29/e756e715a48959f1c0045342088d7ca9762a2f509b945f362a316e9412b7/pytest_benchmark-5.2.3-py3-none-any.whl", hash = "sha256:bc839726ad20e99aaa0d11a127445457b4219bdb9e80a1afc4b51da7f96b0803", size = 45255, upload-time = "2025-11-09T18:48:39.765Z" }, -] - [[package]] name = "pytest-cases" version = "3.9.1" @@ -4569,6 +4553,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/92/82/db006f1d06e5d31805ac47f9ce979937e3d026292f2759543b744f8040be/pytest_cases-3.9.1-py2.py3-none-any.whl", hash = "sha256:60507716650c5ed1ce4a36a3c137f1c3ec58f4fef1ee8678404be074612fcd21", size = 108262, upload-time = "2025-06-09T20:05:01.915Z" }, ] +[[package]] +name = "pytest-codspeed" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, + { name = "pytest" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/ab/eca41967d11c95392829a8b4bfa9220a51cffc4a33ec4653358000356918/pytest_codspeed-4.3.0.tar.gz", hash = "sha256:5230d9d65f39063a313ed1820df775166227ec5c20a1122968f85653d5efee48", size = 124745, upload-time = "2026-02-09T15:23:34.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/58/50df94e9a78e1c77818a492c90557eeb1309af025120c9a21e6375950c52/pytest_codspeed-4.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527a3a02eaa3e4d4583adc4ba2327eef79628f3e1c682a4b959439551a72588e", size = 347395, upload-time = "2026-02-09T15:23:21.986Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/7dfbd3eefd112a14e6fb65f9ff31dacf2e9c381cb94b27332b81d2b13f8d/pytest_codspeed-4.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9858c2a6e1f391d5696757e7b6e9484749a7376c46f8b4dd9aebf093479a9667", size = 342625, upload-time = "2026-02-09T15:23:23.035Z" }, + { url = "https://files.pythonhosted.org/packages/7f/53/7255f6a25bc56ff1745b254b21545dfe0be2268f5b91ce78f7e8a908f0ad/pytest_codspeed-4.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34f2fd8497456eefbd325673f677ea80d93bb1bc08a578c1fa43a09cec3d1879", size = 347325, upload-time = "2026-02-09T15:23:23.998Z" }, + { url = "https://files.pythonhosted.org/packages/2e/f8/82ae570d8b9ad30f33c9d4002a7a1b2740de0e090540c69a28e4f711ebe2/pytest_codspeed-4.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df6a36a2a9da1406bc50428437f657f0bd8c842ae54bee5fb3ad30e01d50c0f5", size = 342558, upload-time = "2026-02-09T15:23:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e1/55cfe9474f91d174c7a4b04d257b5fc6d4d06f3d3680f2da672ee59ccc10/pytest_codspeed-4.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bec30f4fc9c4973143cd80f0d33fa780e9fa3e01e4dbe8cedf229e72f1212c62", size = 347383, upload-time = "2026-02-09T15:23:26.68Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3b/8fd781d959bbe789b3de8ce4c50d5706a684a0df377147dfb27b200c20c1/pytest_codspeed-4.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e6584e641cadf27d894ae90b87c50377232a97cbfd76ee0c7ecd0c056fa3f7f4", size = 342481, upload-time = "2026-02-09T15:23:27.686Z" }, + { url = "https://files.pythonhosted.org/packages/bb/0c/368045133c6effa2c665b1634b7b8a9c88b307f877fa31f1f8df47885b51/pytest_codspeed-4.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df0d1f6ea594f29b745c634d66d5f5f1caa1c3abd2af82fea49d656038e8fc77", size = 353680, upload-time = "2026-02-09T15:23:28.726Z" }, + { url = "https://files.pythonhosted.org/packages/59/21/e543abcd72244294e25ae88ec3a9311ade24d6913f8c8f42569d671700bc/pytest_codspeed-4.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2f5bb6d8898bea7db45e3c8b916ee48e36905b929477bb511b79c5a3ccacda4", size = 347888, upload-time = "2026-02-09T15:23:30.443Z" }, + { url = "https://files.pythonhosted.org/packages/55/d9/b8a53c20cf5b41042c205bb9d36d37da00418d30fd1a94bf9eb147820720/pytest_codspeed-4.3.0-py3-none-any.whl", hash = "sha256:05baff2a61dc9f3e92b92b9c2ab5fb45d9b802438f5373073f5766a91319ed7a", size = 125224, upload-time = "2026-02-09T15:23:33.774Z" }, +] + [[package]] name = "pytest-env" version = "1.2.0"