pytest-gpu-proof 0.4.1__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/.github/workflows/ci.yml +1 -1
  2. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/CHANGELOG.md +10 -0
  3. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/CONTRIBUTING.md +1 -1
  4. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/PKG-INFO +6 -4
  5. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/README.md +2 -2
  6. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/docs/architecture.md +9 -0
  7. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/docs/index.md +1 -1
  8. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/pyproject.toml +4 -2
  9. pytest_gpu_proof-0.5.0/requirements.txt +3 -0
  10. pytest_gpu_proof-0.5.0/src/pytest_gpu_proof/__init__.py +1 -0
  11. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/src/pytest_gpu_proof/config.py +4 -1
  12. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/src/pytest_gpu_proof/merge.py +1 -1
  13. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/src/pytest_gpu_proof/plugin.py +1 -1
  14. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/src/pytest_gpu_proof/receipt.py +15 -1
  15. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/src/pytest_gpu_proof/verify.py +4 -4
  16. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/tests/conftest.py +13 -0
  17. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/tests/test_cli.py +23 -4
  18. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/tests/test_config.py +20 -0
  19. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/tests/test_merge.py +22 -1
  20. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/tests/test_receipt.py +35 -0
  21. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/tests/test_sharding.py +1 -1
  22. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/tests/test_verifier.py +3 -3
  23. pytest_gpu_proof-0.4.1/requirements.txt +0 -2
  24. pytest_gpu_proof-0.4.1/src/pytest_gpu_proof/__init__.py +0 -1
  25. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/.github/workflows/docs.yml +0 -0
  26. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/.github/workflows/publish-testpypi.yml +0 -0
  27. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/.github/workflows/publish.yml +0 -0
  28. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/.gitignore +0 -0
  29. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/AGENTS.md +0 -0
  30. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/CLAUDE.md +0 -0
  31. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/LICENSE +0 -0
  32. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/RELEASING.md +0 -0
  33. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/ROADMAP.md +0 -0
  34. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/SECURITY.md +0 -0
  35. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/docs/ci_gpu_mode.md +0 -0
  36. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/docs/landscape.md +0 -0
  37. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/docs/local_mode.md +0 -0
  38. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/docs/policy.md +0 -0
  39. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/docs/quickstart.md +0 -0
  40. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/docs/security_model.md +0 -0
  41. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/docs/sharding.md +0 -0
  42. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/examples/cuda_ctypes_matmul/Makefile +0 -0
  43. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/examples/cuda_ctypes_matmul/README.md +0 -0
  44. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/examples/cuda_ctypes_matmul/cuda_ctypes_matmul.py +0 -0
  45. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/examples/cuda_ctypes_matmul/matmul_kernel.cu +0 -0
  46. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/examples/cuda_ctypes_matmul/test_cuda_ctypes_matmul.py +0 -0
  47. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/examples/cuda_nanobind_matmul/CMakeLists.txt +0 -0
  48. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/examples/cuda_nanobind_matmul/README.md +0 -0
  49. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/examples/cuda_nanobind_matmul/src/bindings.cpp +0 -0
  50. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/examples/cuda_nanobind_matmul/src/matmul_cuda.h +0 -0
  51. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/examples/cuda_nanobind_matmul/src/matmul_kernel.cu +0 -0
  52. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/examples/cuda_nanobind_matmul/test_cuda_nanobind_matmul.py +0 -0
  53. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/examples/cuda_pybind11_matmul/CMakeLists.txt +0 -0
  54. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/examples/cuda_pybind11_matmul/README.md +0 -0
  55. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/examples/cuda_pybind11_matmul/src/bindings.cpp +0 -0
  56. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/examples/cuda_pybind11_matmul/src/matmul_cuda.h +0 -0
  57. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/examples/cuda_pybind11_matmul/src/matmul_kernel.cu +0 -0
  58. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/examples/cuda_pybind11_matmul/test_cuda_pybind11_matmul.py +0 -0
  59. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/examples/github_gpu_runner/.github/workflows/gpu-test.yml +0 -0
  60. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/examples/jax_ffi_cuda_matmul/CMakeLists.txt +0 -0
  61. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/examples/jax_ffi_cuda_matmul/README.md +0 -0
  62. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/examples/jax_ffi_cuda_matmul/jax_ffi_matmul.py +0 -0
  63. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/examples/jax_ffi_cuda_matmul/src/matmul_ffi.cu +0 -0
  64. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/examples/jax_ffi_cuda_matmul/test_jax_ffi_cuda_matmul.py +0 -0
  65. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/examples/local_receipt_verify/.github/workflows/verify.yml +0 -0
  66. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/examples/minimal_python_only/conftest.py +0 -0
  67. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/examples/minimal_python_only/test_minimal.py +0 -0
  68. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/examples/wrapped_cuda_ctypes/fake_cuda_lib.py +0 -0
  69. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/examples/wrapped_cuda_ctypes/test_cuda_wrapper.py +0 -0
  70. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/install.sh +0 -0
  71. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/mkdocs.yml +0 -0
  72. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/requirements-dev.txt +0 -0
  73. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/src/pytest_gpu_proof/__main__.py +0 -0
  74. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/src/pytest_gpu_proof/cli.py +0 -0
  75. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/src/pytest_gpu_proof/compare.py +0 -0
  76. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/src/pytest_gpu_proof/fingerprint.py +0 -0
  77. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/src/pytest_gpu_proof/gitutils.py +0 -0
  78. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/src/pytest_gpu_proof/signers/__init__.py +0 -0
  79. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/src/pytest_gpu_proof/signers/base.py +0 -0
  80. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/src/pytest_gpu_proof/signers/ed25519.py +0 -0
  81. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/tests/test_compare.py +0 -0
  82. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/tests/test_fingerprint.py +0 -0
  83. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/tests/test_gitutils.py +0 -0
  84. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/tests/test_plugin_capture.py +0 -0
  85. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/tests/test_signing.py +0 -0
  86. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/website/README.md +0 -0
  87. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/website/build_site.py +0 -0
  88. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/website/check_site.py +0 -0
  89. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/website/favicon.svg +0 -0
  90. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/website/index.html +0 -0
  91. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/website/main.js +0 -0
  92. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/website/overrides/main.html +0 -0
  93. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/website/provenance.json +0 -0
  94. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/website/requirements.txt +0 -0
  95. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/website/style.css +0 -0
  96. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/website/test_site.py +0 -0
  97. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/website/workflow-mobile.svg +0 -0
  98. {pytest_gpu_proof-0.4.1 → pytest_gpu_proof-0.5.0}/website/workflow.svg +0 -0
@@ -11,7 +11,7 @@ jobs:
11
11
  strategy:
12
12
  fail-fast: false
13
13
  matrix:
14
- python-version: ["3.11", "3.12", "3.13"]
14
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
15
15
 
16
16
  steps:
17
17
  - uses: actions/checkout@v4
@@ -4,6 +4,16 @@ All notable changes to pytest-gpu-proof are documented here. The format is
4
4
  based on [Keep a Changelog](https://keepachangelog.com/); versions follow
5
5
  [SemVer](https://semver.org/) (pre-1.0: minor bumps may break).
6
6
 
7
+ ## [0.5.0] — 2026-10-28
8
+
9
+ ### Added
10
+
11
+ - Python 3.10 support with a conditional `tomli` dependency and a CI test lane.
12
+ - Signed `environment.nvcc_version` records the stripped output of
13
+ `nvcc --version` from PATH, or null when unavailable. Fresh shard merges
14
+ require matching compiler metadata; existing receipts without the field
15
+ remain supported.
16
+
7
17
  ## [0.4.1] — 2026-10-12
8
18
 
9
19
  ### Fixed
@@ -19,7 +19,7 @@ python -m build
19
19
  python -m twine check --strict dist/*
20
20
  ```
21
21
 
22
- CI runs the test suite on Python 3.11, 3.12, and 3.13, independently enforces
22
+ CI runs the test suite on Python 3.10, 3.11, 3.12, and 3.13, independently enforces
23
23
  100% line and branch coverage, builds docs strictly, and smoke-tests the wheel.
24
24
 
25
25
  ## Change expectations
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-gpu-proof
3
- Version: 0.4.1
3
+ Version: 0.5.0
4
4
  Summary: pytest plugin for GPU equivalence testing with signed receipts verified via GitHub SSH keys
5
5
  Project-URL: Homepage, https://a2r-lab.org/pytest-gpu-proof/
6
6
  Project-URL: Documentation, https://a2r-lab.org/pytest-gpu-proof/docs/
@@ -16,14 +16,16 @@ Classifier: Intended Audience :: Developers
16
16
  Classifier: License :: OSI Approved :: MIT License
17
17
  Classifier: Operating System :: OS Independent
18
18
  Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
19
20
  Classifier: Programming Language :: Python :: 3.11
20
21
  Classifier: Programming Language :: Python :: 3.12
21
22
  Classifier: Programming Language :: Python :: 3.13
22
23
  Classifier: Topic :: Security :: Cryptography
23
24
  Classifier: Topic :: Software Development :: Testing
24
- Requires-Python: >=3.11
25
+ Requires-Python: >=3.10
25
26
  Requires-Dist: cryptography>=41.0
26
27
  Requires-Dist: pytest>=7.0
28
+ Requires-Dist: tomli>=2.0; python_version < '3.11'
27
29
  Provides-Extra: dev
28
30
  Requires-Dist: build>=1.2; extra == 'dev'
29
31
  Requires-Dist: coverage[toml]>=7.6; extra == 'dev'
@@ -90,7 +92,7 @@ cd pytest-gpu-proof
90
92
  python -m pip install -e ".[dev]"
91
93
  ```
92
94
 
93
- Requirements: Python 3.11+, pytest 7+, and `cryptography` 41+.
95
+ Requirements: Python 3.10+, pytest 7+, and `cryptography` 41+.
94
96
 
95
97
  ## Quick start
96
98
 
@@ -344,7 +346,7 @@ python -m build
344
346
  python -m twine check --strict dist/*
345
347
  ```
346
348
 
347
- CI tests Python 3.11–3.13 and enforces 100% line and branch coverage. See
349
+ CI tests Python 3.10–3.13 and enforces 100% line and branch coverage. See
348
350
  [CONTRIBUTING.md](CONTRIBUTING.md), [SECURITY.md](SECURITY.md), and
349
351
  [RELEASING.md](RELEASING.md).
350
352
 
@@ -52,7 +52,7 @@ cd pytest-gpu-proof
52
52
  python -m pip install -e ".[dev]"
53
53
  ```
54
54
 
55
- Requirements: Python 3.11+, pytest 7+, and `cryptography` 41+.
55
+ Requirements: Python 3.10+, pytest 7+, and `cryptography` 41+.
56
56
 
57
57
  ## Quick start
58
58
 
@@ -306,7 +306,7 @@ python -m build
306
306
  python -m twine check --strict dist/*
307
307
  ```
308
308
 
309
- CI tests Python 3.11–3.13 and enforces 100% line and branch coverage. See
309
+ CI tests Python 3.10–3.13 and enforces 100% line and branch coverage. See
310
310
  [CONTRIBUTING.md](CONTRIBUTING.md), [SECURITY.md](SECURITY.md), and
311
311
  [RELEASING.md](RELEASING.md).
312
312
 
@@ -69,6 +69,15 @@ canonical = json.dumps(
69
69
  Putting signer metadata inside this payload prevents identity or algorithm
70
70
  substitution after signing.
71
71
 
72
+ The optional `environment.nvcc_version` field records the stripped stdout of
73
+ `nvcc --version` resolved through PATH. It is null if the executable is missing,
74
+ cannot run, times out after ten seconds, fails, or returns empty/undecodable output.
75
+ This is self-reported compiler availability, not evidence that this compiler built
76
+ the tested code. It is independent of GPU detection and requires no GPU.
77
+ Existing schema-3 receipts without the field still verify. Fresh shard merges
78
+ require matching values (missing and null are equivalent); carried shards retain
79
+ their original environment metadata.
80
+
72
81
  ## Fingerprint manifest
73
82
 
74
83
  `sha256-manifest-v2` enumerates stage-0 Git index entries. Regular files bind
@@ -36,5 +36,5 @@ or exact SSH key fingerprints.
36
36
  - [Architecture](architecture.md): schema and data flow.
37
37
  - [Security model](security_model.md): exact guarantees and limits.
38
38
 
39
- Requirements: Python 3.11+, pytest 7+, and an SSH private-key file whose public
39
+ Requirements: Python 3.10+, pytest 7+, and an SSH private-key file whose public
40
40
  key is registered on GitHub.
@@ -4,10 +4,10 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pytest-gpu-proof"
7
- version = "0.4.1"
7
+ version = "0.5.0"
8
8
  description = "pytest plugin for GPU equivalence testing with signed receipts verified via GitHub SSH keys"
9
9
  readme = "README.md"
10
- requires-python = ">=3.11"
10
+ requires-python = ">=3.10"
11
11
  license = { text = "MIT" }
12
12
  keywords = ["pytest", "gpu", "cuda", "testing", "signing", "attestation"]
13
13
  classifiers = [
@@ -17,6 +17,7 @@ classifiers = [
17
17
  "License :: OSI Approved :: MIT License",
18
18
  "Operating System :: OS Independent",
19
19
  "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.10",
20
21
  "Programming Language :: Python :: 3.11",
21
22
  "Programming Language :: Python :: 3.12",
22
23
  "Programming Language :: Python :: 3.13",
@@ -26,6 +27,7 @@ classifiers = [
26
27
  dependencies = [
27
28
  "pytest>=7.0",
28
29
  "cryptography>=41.0",
30
+ "tomli>=2.0; python_version < '3.11'",
29
31
  ]
30
32
 
31
33
  [project.urls]
@@ -0,0 +1,3 @@
1
+ pytest>=7.0
2
+ cryptography>=41.0
3
+ tomli>=2.0; python_version < '3.11'
@@ -0,0 +1 @@
1
+ __version__ = "0.5.0"
@@ -1,4 +1,7 @@
1
- import tomllib
1
+ try:
2
+ import tomllib
3
+ except ModuleNotFoundError:
4
+ import tomli as tomllib
2
5
  from dataclasses import dataclass, field
3
6
  from pathlib import Path
4
7
  from typing import Any, Dict, List, Optional, Union
@@ -67,7 +67,7 @@ def merge_payloads(receipts: List[dict], sources: List[str]) -> dict:
67
67
  "repo.commit_sha")
68
68
  _require_identical(receipts, sources, lambda r: r.get("fingerprint"), "fingerprint")
69
69
  _require_identical(receipts, sources, lambda r: r.get("mode"), "mode")
70
- for key in ("python_version", "platform", "pytest_version", "plugin_version"):
70
+ for key in ("python_version", "platform", "pytest_version", "plugin_version", "nvcc_version"):
71
71
  _require_identical(receipts, sources,
72
72
  lambda r, k=key: r.get("environment", {}).get(k),
73
73
  f"environment.{key}")
@@ -17,7 +17,7 @@ from .config import GpuProofConfig, load_config
17
17
 
18
18
 
19
19
  def _utcnow() -> str:
20
- return datetime.datetime.now(datetime.UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
20
+ return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
21
21
 
22
22
 
23
23
  def _has_gpu() -> bool:
@@ -25,7 +25,7 @@ from .gitutils import (
25
25
 
26
26
 
27
27
  def _utcnow() -> str:
28
- return datetime.datetime.now(datetime.UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
28
+ return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
29
29
 
30
30
 
31
31
  def _gpu_info_from_torch() -> Optional[Dict[str, Any]]:
@@ -92,6 +92,19 @@ def _gpu_info(include_uuid: bool = True) -> Optional[Dict[str, Any]]:
92
92
  }
93
93
 
94
94
 
95
+ def _nvcc_version() -> Optional[str]:
96
+ """Record the compiler on PATH, independently of GPU availability."""
97
+ try:
98
+ result = subprocess.run(
99
+ ["nvcc", "--version"], capture_output=True, text=True, timeout=10,
100
+ )
101
+ except (OSError, subprocess.TimeoutExpired, UnicodeError):
102
+ return None
103
+ if result.returncode != 0:
104
+ return None
105
+ return result.stdout.strip() or None
106
+
107
+
95
108
  def _env_info(record_gpu_uuid: bool = True) -> Dict[str, Any]:
96
109
  import pytest
97
110
 
@@ -102,6 +115,7 @@ def _env_info(record_gpu_uuid: bool = True) -> Dict[str, Any]:
102
115
  "platform": platform.platform(),
103
116
  "pytest_version": pytest.__version__,
104
117
  "plugin_version": __version__,
118
+ "nvcc_version": _nvcc_version(),
105
119
  "gpu_info": _gpu_info(include_uuid=record_gpu_uuid),
106
120
  }
107
121
 
@@ -179,7 +179,7 @@ def _parse_time(value, field: str) -> datetime.datetime:
179
179
  raise VerificationError(f"{field} is missing or is not a UTC timestamp")
180
180
  try:
181
181
  return datetime.datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ").replace(
182
- tzinfo=datetime.UTC
182
+ tzinfo=datetime.timezone.utc
183
183
  )
184
184
  except ValueError as exc:
185
185
  raise VerificationError(f"{field} is not a valid UTC timestamp: {value!r}") from exc
@@ -498,7 +498,7 @@ def _verify(
498
498
  raise VerificationError("repository policy requires recorded GPU information")
499
499
 
500
500
  ended = _parse_time(session.get("ended_at"), "session.ended_at")
501
- age = datetime.datetime.now(datetime.UTC) - ended
501
+ age = datetime.datetime.now(datetime.timezone.utc) - ended
502
502
  if age < datetime.timedelta(minutes=-5):
503
503
  raise VerificationError("receipt timestamp is in the future")
504
504
  if age > datetime.timedelta(days=max_days):
@@ -508,7 +508,7 @@ def _verify(
508
508
  committed = get_commit_time(stored_sha, root)
509
509
  if committed is not None:
510
510
  started = _parse_time(session.get("started_at"), "session.started_at")
511
- commit_dt = datetime.datetime.fromtimestamp(committed, datetime.UTC)
511
+ commit_dt = datetime.datetime.fromtimestamp(committed, datetime.timezone.utc)
512
512
  if started < commit_dt - datetime.timedelta(minutes=5):
513
513
  raise VerificationError(
514
514
  "receipt session started before its commit existed "
@@ -577,7 +577,7 @@ def _verify_shards(receipt: dict, root: str, policy: dict, max_days: int = 30) -
577
577
  raise VerificationError(
578
578
  f"carried shard {name!r} was recorded with a dirty working tree")
579
579
  original = _parse_time(carried.get("original_ended_at"), f"shard {name}.original_ended_at")
580
- age = datetime.datetime.now(datetime.UTC) - original
580
+ age = datetime.datetime.now(datetime.timezone.utc) - original
581
581
  # A carried shard's original run is never allowed to be older than
582
582
  # the receipt itself may be, unless policy says so explicitly.
583
583
  limit = policy.get("carried_max_age_days", min(30, max_days))
@@ -89,3 +89,16 @@ def tmp_git_repo(tmp_path):
89
89
  "*.json\nid_*\nexpected_skips.txt\npyproject.toml\n"
90
90
  )
91
91
  return tmp_path
92
+
93
+
94
+ @pytest.fixture(autouse=True)
95
+ def _no_host_nvcc(monkeypatch):
96
+ """Compiler metadata tests override this deterministic default explicitly."""
97
+ run = subprocess.run
98
+
99
+ def without_host_nvcc(args, *positional, **kwargs):
100
+ if args == ["nvcc", "--version"]:
101
+ return subprocess.CompletedProcess(args, 0, stdout="test nvcc version\n", stderr="")
102
+ return run(args, *positional, **kwargs)
103
+
104
+ monkeypatch.setattr(subprocess, "run", without_host_nvcc)
@@ -1,4 +1,6 @@
1
- import runpy
1
+ import subprocess
2
+ from importlib.util import spec_from_file_location
3
+ from pathlib import Path
2
4
  import sys
3
5
  from types import SimpleNamespace
4
6
 
@@ -69,14 +71,31 @@ def test_merge_cli_success_and_error(monkeypatch, capsys):
69
71
  def test_module_entrypoint(monkeypatch):
70
72
  monkeypatch.setattr("pytest_gpu_proof.cli.main", lambda: (_ for _ in ()).throw(SystemExit(7)))
71
73
  with pytest.raises(SystemExit) as exc:
72
- runpy.run_module("pytest_gpu_proof.__main__", run_name="__main__")
74
+ path = Path(cli.__file__).with_name("__main__.py")
75
+ exec(compile(path.read_text(), str(path), "exec"),
76
+ {"__name__": "__main__", "__package__": "pytest_gpu_proof",
77
+ "__file__": str(path),
78
+ "__spec__": spec_from_file_location(f"pytest_gpu_proof.{path.stem}", path)})
73
79
  assert exc.value.code == 7
74
80
 
75
81
 
76
- @pytest.mark.filterwarnings("ignore:.*found in sys.modules.*:RuntimeWarning")
77
82
  def test_cli_file_entrypoint(monkeypatch):
78
83
  monkeypatch.setattr("pytest_gpu_proof.verify.verify_receipt", lambda **kwargs: True)
79
84
  monkeypatch.setattr(sys, "argv", ["gpu-proof", "verify", "--receipt", "r.json"])
80
85
  with pytest.raises(SystemExit) as exc:
81
- runpy.run_module("pytest_gpu_proof.cli", run_name="__main__")
86
+ path = Path(cli.__file__)
87
+ exec(compile(path.read_text(), str(path), "exec"),
88
+ {"__name__": "__main__", "__package__": "pytest_gpu_proof",
89
+ "__file__": str(path),
90
+ "__spec__": spec_from_file_location(f"pytest_gpu_proof.{path.stem}", path)})
82
91
  assert exc.value.code == 0
92
+
93
+
94
+ @pytest.mark.parametrize("module", ["pytest_gpu_proof", "pytest_gpu_proof.cli"])
95
+ def test_module_cli_in_fresh_interpreter(module):
96
+ # Real -m invocation has no pytest assertion-rewriting loader. The in-process
97
+ # source-execution tests above retain coverage and mocks for exit/dispatch behavior.
98
+ result = subprocess.run([sys.executable, "-m", module, "--help"],
99
+ capture_output=True, text=True, timeout=10)
100
+ assert result.returncode == 0, result.stderr
101
+ assert "verify signed GPU test receipts" in result.stdout
@@ -121,3 +121,23 @@ def test_record_gpu_uuid_from_toml(tmp_path):
121
121
  )
122
122
  )
123
123
  assert config.record_gpu_uuid is False
124
+
125
+
126
+ def test_tomli_fallback(monkeypatch, tmp_path):
127
+ import builtins
128
+ import runpy
129
+ import pytest_gpu_proof.config as config
130
+
131
+ original_import = builtins.__import__
132
+
133
+ def without_tomllib(name, *args, **kwargs):
134
+ if name == "tomllib":
135
+ raise ModuleNotFoundError("No module named 'tomllib'")
136
+ if name == "tomli":
137
+ return config.tomllib
138
+ return original_import(name, *args, **kwargs)
139
+
140
+ monkeypatch.setattr(builtins, "__import__", without_tomllib)
141
+ namespace = runpy.run_path(config.__file__)
142
+ (tmp_path / "pyproject.toml").write_text('[tool.gpu_proof]\nmode = "local"\n')
143
+ assert namespace["load_toml_defaults"](tmp_path) == {"mode": "local"}
@@ -30,7 +30,7 @@ def signer_with_key(tmp_path, ed25519_keypair):
30
30
 
31
31
 
32
32
  def _utcstamp(minutes_ago: int = 0) -> str:
33
- ts = datetime.datetime.now(datetime.UTC) - datetime.timedelta(minutes=minutes_ago)
33
+ ts = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(minutes=minutes_ago)
34
34
  return ts.strftime("%Y-%m-%dT%H:%M:%SZ")
35
35
 
36
36
 
@@ -241,3 +241,24 @@ def test_schema1_merge_has_no_schema3_session_fields():
241
241
  merged = merge_payloads([base], ["one.json"])
242
242
  assert "outcome" not in merged["session"]
243
243
  assert "shards" not in merged
244
+
245
+
246
+ @pytest.mark.parametrize("first, second", [("V13.2", "V12.8"), ("V13.2", None)])
247
+ def test_refuses_nvcc_mismatch(tmp_path, tmp_git_repo, signer_with_key, first, second):
248
+ signer, _, _ = signer_with_key
249
+ a = _shard(tmp_path, tmp_git_repo, signer, "a.json", [_result("t::a")],
250
+ mutate=lambda p: p["environment"].__setitem__("nvcc_version", first))
251
+ b = _shard(tmp_path, tmp_git_repo, signer, "b.json", [_result("t::b")],
252
+ mutate=lambda p: p["environment"].__setitem__("nvcc_version", second))
253
+ with pytest.raises(MergeError, match="environment.nvcc_version"):
254
+ merge_payloads([json.loads(a.read_text()), json.loads(b.read_text())], ["a", "b"])
255
+
256
+
257
+ def test_merge_old_receipt_without_nvcc(tmp_path, tmp_git_repo, signer_with_key):
258
+ signer, _, _ = signer_with_key
259
+ a = _shard(tmp_path, tmp_git_repo, signer, "a.json", [_result("t::a")],
260
+ mutate=lambda p: p["environment"].pop("nvcc_version", None))
261
+ b = _shard(tmp_path, tmp_git_repo, signer, "b.json", [_result("t::b")],
262
+ mutate=lambda p: p["environment"].__setitem__("nvcc_version", None))
263
+ merged = merge_payloads([json.loads(a.read_text()), json.loads(b.read_text())], ["a", "b"])
264
+ assert len(merged["tests"]) == 2
@@ -339,3 +339,38 @@ def test_gpu_info_falls_back_to_torch_on_nvidia_smi_failure(monkeypatch):
339
339
  lambda *a, **k: types.SimpleNamespace(returncode=1, stdout=""))
340
340
  monkeypatch.setitem(sys.modules, "torch", None)
341
341
  assert receipt_mod._gpu_info() is None
342
+
343
+
344
+ @pytest.mark.parametrize("code, output, expected", [
345
+ (0, "nvcc: NVIDIA (R) Cuda compiler driver\nCuda compilation tools, release 13.2, V13.2.51\n",
346
+ "nvcc: NVIDIA (R) Cuda compiler driver\nCuda compilation tools, release 13.2, V13.2.51"),
347
+ (0, " \n", None),
348
+ (1, "partial output", None),
349
+ ])
350
+ def test_nvcc_version(monkeypatch, code, output, expected):
351
+ import subprocess
352
+ from pytest_gpu_proof.receipt import _nvcc_version
353
+
354
+ def run(args, **kwargs):
355
+ assert args == ["nvcc", "--version"]
356
+ assert kwargs == {"capture_output": True, "text": True, "timeout": 10}
357
+ return subprocess.CompletedProcess(args, code, stdout=output, stderr="ignored")
358
+
359
+ monkeypatch.setattr("pytest_gpu_proof.receipt.subprocess.run", run)
360
+ assert _nvcc_version() == expected
361
+ monkeypatch.setattr("pytest_gpu_proof.receipt._gpu_info", lambda **kw: None)
362
+ assert _env_info()["nvcc_version"] == expected
363
+
364
+
365
+ @pytest.mark.parametrize("error", [
366
+ FileNotFoundError(), PermissionError(),
367
+ __import__("subprocess").TimeoutExpired("nvcc", 10), UnicodeError(),
368
+ ])
369
+ def test_nvcc_unavailable(monkeypatch, error):
370
+ from pytest_gpu_proof.receipt import _nvcc_version
371
+
372
+ def run(*args, **kwargs):
373
+ raise error
374
+
375
+ monkeypatch.setattr("pytest_gpu_proof.receipt.subprocess.run", run)
376
+ assert _nvcc_version() is None
@@ -37,7 +37,7 @@ def signer_with_key(tmp_path, ed25519_keypair):
37
37
 
38
38
 
39
39
  def _utcstamp(days_ago: float = 0) -> str:
40
- ts = datetime.datetime.now(datetime.UTC) - datetime.timedelta(days=days_ago)
40
+ ts = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=days_ago)
41
41
  return ts.strftime("%Y-%m-%dT%H:%M:%SZ")
42
42
 
43
43
 
@@ -38,7 +38,7 @@ def signer_with_key(tmp_path, keypair):
38
38
 
39
39
 
40
40
  def _utcstamp(days_ago: int = 0) -> str:
41
- ts = datetime.datetime.now(datetime.UTC) - datetime.timedelta(days=days_ago)
41
+ ts = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=days_ago)
42
42
  return ts.strftime("%Y-%m-%dT%H:%M:%SZ")
43
43
 
44
44
 
@@ -273,7 +273,7 @@ def test_verify_fails_on_failed_test(tmp_path, tmp_git_repo, signer_with_key):
273
273
  }
274
274
  ]
275
275
  import datetime
276
- now = datetime.datetime.now(datetime.UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
276
+ now = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
277
277
  payload = build_receipt_payload(config, results, now, now)
278
278
  receipt = finalize_receipt(payload, signer)
279
279
  path = tmp_path / "gpu-proof.json"
@@ -571,7 +571,7 @@ def test_verifier_wrapper_reports_failure(tmp_path, tmp_git_repo, capsys):
571
571
 
572
572
  def test_future_and_invalid_age_policy(tmp_path, tmp_git_repo, signer_with_key):
573
573
  signer, public_key = signer_with_key
574
- future = (datetime.datetime.now(datetime.UTC) + datetime.timedelta(days=1)).strftime("%Y-%m-%dT%H:%M:%SZ")
574
+ future = (datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=1)).strftime("%Y-%m-%dT%H:%M:%SZ")
575
575
  path = _make_receipt(tmp_path, tmp_git_repo, signer, ended_at=future)
576
576
  with _mock_github_keys(public_key), pytest.raises(VerificationError, match="future"):
577
577
  _verify(str(path), None, str(tmp_git_repo), "testuser", None)
@@ -1,2 +0,0 @@
1
- pytest>=7.0
2
- cryptography>=41.0
@@ -1 +0,0 @@
1
- __version__ = "0.4.1"