From e4890532906aa8bb3bc5c8f26631c3a62f8bb3f0 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Tue, 22 Jul 2025 02:55:24 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=AA=20Add=20local=20test=20running=20i?= =?UTF-8?q?nfra?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .coveragerc | 35 ++++++++++++++ pyproject.toml | 11 +++++ pytest.ini | 80 ++++++++++++++++++++++++++++++++ tests/smoke_test.py | 8 ++++ tox.ini | 109 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 243 insertions(+) create mode 100644 .coveragerc create mode 100644 pyproject.toml create mode 100644 pytest.ini create mode 100644 tests/smoke_test.py create mode 100644 tox.ini diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..33d2a79 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,35 @@ +[html] +show_contexts = true +skip_covered = false + +[paths] +_site-packages-to-src-mapping = + src + */src + *\src + */lib/pypy*/site-packages + */lib/python*/site-packages + *\Lib\site-packages + +[report] +# `fail_under` is set here temporarily until it can be dropped: +fail_under = 28 +skip_covered = true +skip_empty = true +show_missing = true +exclude_also = + ^\s*@pytest\.mark\.xfail + +[run] +branch = true +cover_pylib = false +# https://coverage.rtfd.io/en/latest/contexts.html#dynamic-contexts +# dynamic_context = test_function # conflicts with `pytest-cov` if set here +parallel = true +plugins = + covdefaults +relative_files = true +source = + . +source_pkgs = + normalize_needed_jobs_status diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..11735d0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,11 @@ +[dependency-groups] +testing = [ + 'covdefaults', # coveragepy settings as a plugin + 'coverage', # accessed directly from tox + 'coverage-enable-subprocess', # coveragepy plugin + 'pytest', # test runner + 'pytest-cov', # coveragepy integration + 'pytest-mock', # `mocker` fixture + 'pytest-subtests', # allows marking bits of test functions as subtests + 'pytest-xdist', # parallelism +] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..fd17123 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,80 @@ +[pytest] +addopts = + # `pytest-xdist`: + --numprocesses=auto + # NOTE: the plugin disabled because it's slower with so few tests + --numprocesses=0 + + # Show 10 slowest invocations: + --durations=10 + + # Report all the things == -rxXs: + -ra + + # Show values of the local vars in errors/tracebacks: + --showlocals + + # Autocollect and invoke the doctests from all modules: + # https://docs.pytest.org/en/stable/doctest.html + --doctest-modules + + # Pre-load the `pytest-cov` plugin early: + -p pytest_cov + + # `pytest-cov`: + --cov + --cov-config=.coveragerc + --cov-context=test + --no-cov-on-fail + + # Fail on config parsing warnings: + # --strict-config + + # Fail on non-existing markers: + # * Deprecated since v6.2.0 but may be reintroduced later covering a + # broader scope: + # --strict + # * Exists since v4.5.0 (advised to be used instead of `--strict`): + --strict-markers + +doctest_optionflags = ALLOW_UNICODE ELLIPSIS + +# Marks tests with an empty parameterset as xfail(run=False) +empty_parameter_set_mark = xfail + +faulthandler_timeout = 30 + +filterwarnings = + error + +# https://docs.pytest.org/en/stable/usage.html#creating-junitxml-format-files +junit_duration_report = call +# xunit1 contains more metadata than xunit2 so it's better for CI UIs: +junit_family = xunit1 +junit_logging = all +junit_log_passing_tests = true +junit_suite_name = alls_green_test_suite + +# A mapping of markers to their descriptions allowed in strict mode: +markers = + +minversion = 6.1.0 + +# Optimize pytest's lookup by restricting potentially deep dir tree scan: +norecursedirs = + build + dependencies + dist + docs + .* + *.egg + *.egg-info + */*.egg-info + */**/*.egg-info + *.dist-info + */*.dist-info + */**/*.dist-info + +testpaths = tests/ + +xfail_strict = true diff --git a/tests/smoke_test.py b/tests/smoke_test.py new file mode 100644 index 0000000..1a6575b --- /dev/null +++ b/tests/smoke_test.py @@ -0,0 +1,8 @@ +"""Initial test infra smoke tests.""" + +import normalize_needed_jobs_status # pth via editable install in tox + + +def test_smoke() -> None: + """Check that the imported module is truthy.""" + assert normalize_needed_jobs_status diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..2313277 --- /dev/null +++ b/tox.ini @@ -0,0 +1,109 @@ +[tox] +isolated_build = true +# Version 4.22 is the first one to implement support for dependency groups: +minversion = 4.22 + + +[python-cli-options] +byte-warnings = -b +byte-errors = -bb +max-isolation = -E -s -I +some-isolation = -E -s +warnings-to-errors = -Werror + + +[testenv] +description = Run pytest under {envpython} +dependency_groups = + testing +commands = + {envpython} \ + {[python-cli-options]byte-errors} \ + {[python-cli-options]max-isolation} \ + {[python-cli-options]warnings-to-errors} \ + -W 'ignore:Coverage failure::pytest_cov.plugin' \ + -m pytest \ + {tty:--color=yes} \ + {posargs:--cov-report=html:{envtmpdir}{/}htmlcov{/}} +commands_post = + -{envpython} \ + {[python-cli-options]byte-errors} \ + {[python-cli-options]max-isolation} \ + {[python-cli-options]warnings-to-errors} \ + -c \ + 'import atexit, os, sys; \ + os.getenv("GITHUB_ACTIONS") == "true" or sys.exit(); \ + import coverage; \ + gh_summary_fd = open(\ + os.environ["GITHUB_STEP_SUMMARY"], encoding="utf-8", mode="a",\ + ); \ + atexit.register(gh_summary_fd.close); \ + cov = coverage.Coverage(); \ + cov.load(); \ + cov.report(file=gh_summary_fd, output_format="markdown")' + {envpython} \ + {[python-cli-options]byte-errors} \ + {[python-cli-options]max-isolation} \ + {[python-cli-options]warnings-to-errors} \ + -c \ + 'import os, pathlib, sys; \ + os.getenv("GITHUB_ACTIONS") == "true" or sys.exit(); \ + cov_report_arg_prefix = "--cov-report=xml:"; \ + test_report_arg_prefix = "--junitxml="; \ + cov_reports = [\ + arg[len(cov_report_arg_prefix):] for arg in sys.argv \ + if arg.startswith(cov_report_arg_prefix)\ + ]; \ + test_reports = [\ + arg[len(test_report_arg_prefix):] for arg in sys.argv \ + if arg.startswith(test_report_arg_prefix)\ + ]; \ + cov_report_file = cov_reports[-1] if cov_reports else None; \ + test_report_file = test_reports[-1] if test_reports else None; \ + gh_output_fd = open(\ + os.environ["GITHUB_OUTPUT"], encoding="utf-8", mode="a",\ + ); \ + cov_report_file and \ + print(f"cov-report-files={cov_report_file !s}", file=gh_output_fd); \ + test_report_file and \ + print(f"test-result-files={test_report_file !s}", file=gh_output_fd); \ + print("codecov-flags=pytest", file=gh_output_fd); \ + gh_output_fd.close()' \ + {posargs} + # Print out the output coverage dir and a way to serve html: + {envpython} \ + {[python-cli-options]byte-errors} \ + {[python-cli-options]max-isolation} \ + {[python-cli-options]warnings-to-errors} \ + -c\ + 'import pathlib, shlex, sys; \ + cov_html_report_arg_prefix = "--cov-report=html:"; \ + cov_html_reports = [\ + arg[len(cov_html_report_arg_prefix):] for arg in sys.argv \ + if arg.startswith(cov_html_report_arg_prefix)\ + ]; \ + cov_html_reports or sys.exit(); \ + cov_html_report_dir = pathlib.Path(cov_html_reports[-1]); \ + index_file = cov_html_report_dir / "index.html";\ + index_file.exists() or sys.exit(); \ + html_url = f"file://\{index_file\}";\ + browse_cmd = shlex.join(("python3", "-Im", "webbrowser", html_url)); \ + serve_cmd = shlex.join((\ + "python3", "-Im", "http.server", \ + "--directory", str(cov_html_report_dir), "0", \ + )); \ + print(f"\nTo open the HTML coverage report, run\n\n\ + \t\{browse_cmd !s\}\n");\ + print(f"To serve \ + the HTML coverage report with a local web server, use\n\n\ + \t\{serve_cmd !s\}\n")' \ + {posargs:--cov-report=html:{envtmpdir}{/}htmlcov{/}} +package = editable +pass_env = + CI + GITHUB_* + SSH_AUTH_SOCK + TERM +set_env = + COVERAGE_PROCESS_START = {toxinidir}{/}.coveragerc +wheel_build_env = .pkg