pytest-selective 1.1.0__tar.gz → 1.1.2__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 (194) hide show
  1. pytest_selective-1.1.2/.github/validation-inputs.json +26 -0
  2. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/.github/workflows/ci.yml +32 -7
  3. pytest_selective-1.1.2/.github/workflows/dry-lint-audit.yml +23 -0
  4. pytest_selective-1.1.2/.release-please-manifest.json +3 -0
  5. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/CHANGELOG.md +14 -0
  6. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/CONTRIBUTING.md +4 -0
  7. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/PKG-INFO +19 -9
  8. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/README.md +18 -8
  9. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/actions/plan-lanes/action.yml +4 -2
  10. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/contracts/selection/conftest.py +11 -0
  11. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/contracts/selection/quint-connect.json +14 -1
  12. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/contracts/selection/selection.qnt +30 -3
  13. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/contracts/selection/selection_driver.py +6 -1
  14. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/actions.md +4 -0
  15. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/ci.md +3 -4
  16. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/configuration.md +12 -5
  17. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/pyproject.toml +1 -1
  18. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/scripts/measure_ci_runs.py +4 -0
  19. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/skills/pytest-selective/SKILL.md +137 -81
  20. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/src/pytest_selective/impact_graph.py +39 -2
  21. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/src/pytest_selective/impact_models.py +23 -0
  22. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/tests/test_public_actions.py +8 -0
  23. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/tests/testing/test_impact_audit.py +8 -6
  24. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/tests/testing/test_impact_graph.py +81 -0
  25. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/uv.lock +1 -1
  26. pytest_selective-1.1.0/.github/validation-inputs.json +0 -11
  27. pytest_selective-1.1.0/.github/workflows/dry-lint-audit.yml +0 -91
  28. pytest_selective-1.1.0/.release-please-manifest.json +0 -3
  29. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/.dry-lint/baseline.json +0 -0
  30. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/.dry-lint/progress.json +0 -0
  31. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/.dry-lint.toml +0 -0
  32. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/.gitattributes +0 -0
  33. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  34. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  35. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/.github/dependabot.yml +0 -0
  36. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/.github/test-lanes.json +0 -0
  37. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/.github/workflows/docs.yml +0 -0
  38. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/.github/workflows/dry-lint-judge.yml +0 -0
  39. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/.github/workflows/dry-lint.yml +0 -0
  40. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/.github/workflows/observe-ci.yml +0 -0
  41. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/.github/workflows/pr-title.yml +0 -0
  42. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/.github/workflows/public-actions.yml +0 -0
  43. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/.github/workflows/release-please.yml +0 -0
  44. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/.github/workflows/release.yml +0 -0
  45. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/.github/workflows/windows-performance.yml +0 -0
  46. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/.gitignore +0 -0
  47. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/.pre-commit-config.yaml +0 -0
  48. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/.readthedocs.yaml +0 -0
  49. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/AGENTS.md +0 -0
  50. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/CLAUDE.md +0 -0
  51. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/LICENSE +0 -0
  52. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/PROVENANCE.md +0 -0
  53. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/RELEASING.md +0 -0
  54. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/action.yml +0 -0
  55. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/actions/README.md +0 -0
  56. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/actions/pytest-expansion/action.yml +0 -0
  57. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/actions/test-impact/action.yml +0 -0
  58. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/actions/validation-gate/action.yml +0 -0
  59. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/agentsync.toml +0 -0
  60. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/contracts/selection/README.md +0 -0
  61. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/contracts/selection/quint.ini +0 -0
  62. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/contracts/selection/runtime_deps.py +0 -0
  63. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/contracts/selection/test_selection_contract.py +0 -0
  64. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/README.md +0 -0
  65. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/agents.md +0 -0
  66. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/assets/banner.png +0 -0
  67. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/assets/banner.svg +0 -0
  68. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/assets/brand.json +0 -0
  69. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/assets/icon.png +0 -0
  70. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/assets/logo.svg +0 -0
  71. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/assets/raster-sources.json +0 -0
  72. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/assets/social-preview.png +0 -0
  73. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/assets/social-preview.svg +0 -0
  74. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/benchmarks/2026-10-22-due-ci.json +0 -0
  75. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/benchmarks/2026-10-22-due-docs-ci.json +0 -0
  76. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/benchmarks/2026-10-22-due-docs-widget.json +0 -0
  77. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/benchmarks/2026-10-22-due-widget.json +0 -0
  78. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/benchmarks/2026-10-22-input-replays.json +0 -0
  79. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/benchmarks/2026-10-22-obligation-docs.json +0 -0
  80. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/benchmarks/2026-10-22-obligation.json +0 -0
  81. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/benchmarks/2026-10-22-routing.md +0 -0
  82. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/benchmarks/2026-10-22-selective-docs.json +0 -0
  83. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/benchmarks/2026-10-22.md +0 -0
  84. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/benchmarks/2026-10-23-windows-saleor/obligation-baseline.json +0 -0
  85. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/benchmarks/2026-10-23-windows-saleor/selective-after-3.11.xml +0 -0
  86. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/benchmarks/2026-10-23-windows-saleor/selective-after-3.14.xml +0 -0
  87. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/benchmarks/2026-10-23-windows-saleor/selective-after.json +0 -0
  88. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/benchmarks/2026-10-23-windows-saleor/selective-control-3.11.xml +0 -0
  89. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/benchmarks/2026-10-23-windows-saleor/selective-control-3.14.xml +0 -0
  90. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/benchmarks/2026-10-23-windows-saleor/selective-control.json +0 -0
  91. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/benchmarks/2026-10-23-windows-saleor/selective-earlier.json +0 -0
  92. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/benchmarks/2026-10-23-windows-saleor.md +0 -0
  93. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/benchmarks/windows-runtime.md +0 -0
  94. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/ci-toolkit.md +0 -0
  95. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/comparison-pytest-impacted.md +0 -0
  96. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/evidence.md +0 -0
  97. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/local-development.md +0 -0
  98. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/pytest-hygiene.md +0 -0
  99. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/selection-health.md +0 -0
  100. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/tutorial.md +0 -0
  101. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/tutorials/adoption-checklist.md +0 -0
  102. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/tutorials/sidecar-dependencies.md +0 -0
  103. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/tutorials/skip-unrelated-ci.md +0 -0
  104. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/work-in-progress/tickets/actions-directory-test-plan.md +0 -0
  105. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/work-in-progress/tickets/codex-selector-correctness-performance-test-plan.md +0 -0
  106. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/work-in-progress/tickets/codex-windows-git-performance-test-plan.md +0 -0
  107. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/work-in-progress/tickets/graph-based-inputs-plan.md +0 -0
  108. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/work-in-progress/tickets/impacted-improvements-test-plan.md +0 -0
  109. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/work-in-progress/tickets/move-action-implementations-test-plan.md +0 -0
  110. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/work-in-progress/tickets/public-action-onboarding-test-plan.md +0 -0
  111. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/work-in-progress/tickets/ratchet-duplicates-zero-test-plan.md +0 -0
  112. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/docs/work-in-progress/tickets/rerun-selection-test-plan.md +0 -0
  113. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/documentation/assets/routing-comparison.svg +0 -0
  114. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/documentation/check_site.py +0 -0
  115. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/documentation/ecosystem.md +0 -0
  116. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/documentation/hooks.py +0 -0
  117. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/documentation/index.md +0 -0
  118. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/documentation/llms.txt +0 -0
  119. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/documentation/overrides/main.html +0 -0
  120. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/documentation/publishing.md +0 -0
  121. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/documentation/requirements.in +0 -0
  122. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/documentation/requirements.txt +0 -0
  123. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/mkdocs.yml +0 -0
  124. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/release-please-config.json +0 -0
  125. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/scripts/benchmark_graph.py +0 -0
  126. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/scripts/benchmark_parsing.py +0 -0
  127. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/scripts/benchmark_rust.py +0 -0
  128. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/scripts/check_docs.py +0 -0
  129. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/scripts/check_metadata.py +0 -0
  130. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/scripts/ci_file_config.py +0 -0
  131. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/scripts/compare_impacted.py +0 -0
  132. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/scripts/public_actions.py +0 -0
  133. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/scripts/render_brand.py +0 -0
  134. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/scripts/run_ci_tests.py +0 -0
  135. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/scripts/smoke_distribution.py +0 -0
  136. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/scripts/tutorial.py +0 -0
  137. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/skills/pytest-selective/agents/openai.yaml +0 -0
  138. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/src/pytest_selective/__init__.py +0 -0
  139. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/src/pytest_selective/__main__.py +0 -0
  140. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/src/pytest_selective/ast_imports.py +0 -0
  141. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/src/pytest_selective/checkpoint.py +0 -0
  142. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/src/pytest_selective/ci.py +0 -0
  143. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/src/pytest_selective/gate.py +0 -0
  144. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/src/pytest_selective/git_operations.py +0 -0
  145. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/src/pytest_selective/hygiene.py +0 -0
  146. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/src/pytest_selective/impact_audit.py +0 -0
  147. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/src/pytest_selective/impact_audit_plugin.py +0 -0
  148. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/src/pytest_selective/impact_cli.py +0 -0
  149. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/src/pytest_selective/impact_declarations.py +0 -0
  150. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/src/pytest_selective/impact_explanation.py +0 -0
  151. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/src/pytest_selective/impact_first_party.py +0 -0
  152. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/src/pytest_selective/impact_name_scope.py +0 -0
  153. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/src/pytest_selective/impact_selection.py +0 -0
  154. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/src/pytest_selective/input_policy.py +0 -0
  155. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/src/pytest_selective/local.py +0 -0
  156. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/src/pytest_selective/parse_batch.py +0 -0
  157. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/src/pytest_selective/plugin.py +0 -0
  158. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/src/pytest_selective/py.typed +0 -0
  159. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/src/pytest_selective/pytest_expansion.py +0 -0
  160. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/src/pytest_selective/pytest_plugins.py +0 -0
  161. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/src/pytest_selective/pytest_sharding.py +0 -0
  162. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/src/pytest_selective/test_inventory.py +0 -0
  163. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/src/pytest_selective/typecheck_scope.py +0 -0
  164. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/src/pytest_selective/working_tree.py +0 -0
  165. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/tests/__init__.py +0 -0
  166. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/tests/test_checkpoint_policy.py +0 -0
  167. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/tests/test_ci_admission.py +0 -0
  168. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/tests/test_ci_measurements.py +0 -0
  169. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/tests/test_ci_test_execution.py +0 -0
  170. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/tests/test_distribution_metadata.py +0 -0
  171. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/tests/test_hygiene.py +0 -0
  172. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/tests/test_pytest_expansion.py +0 -0
  173. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/tests/test_release_workflow.py +0 -0
  174. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/tests/test_repository_ci.py +0 -0
  175. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/tests/test_repository_hygiene.py +0 -0
  176. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/tests/test_repository_hygiene.py.gv-dependency.yaml +0 -0
  177. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/tests/test_tutorial.py +0 -0
  178. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/tests/testing/__init__.py +0 -0
  179. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/tests/testing/git_repo.py +0 -0
  180. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/tests/testing/test_ast_imports.py +0 -0
  181. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/tests/testing/test_ci.py +0 -0
  182. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/tests/testing/test_gate.py +0 -0
  183. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/tests/testing/test_git_repo.py +0 -0
  184. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/tests/testing/test_global_inputs.py +0 -0
  185. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/tests/testing/test_impact_explanation.py +0 -0
  186. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/tests/testing/test_impact_first_party.py +0 -0
  187. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/tests/testing/test_impact_name_scope.py +0 -0
  188. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/tests/testing/test_parse_batch.py +0 -0
  189. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/tests/testing/test_portability.py +0 -0
  190. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/tests/testing/test_pytest_plugin.py +0 -0
  191. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/tests/testing/test_pytest_plugins.py +0 -0
  192. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/tests/testing/test_pytest_sharding.py +0 -0
  193. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/tests/testing/test_typecheck_scope.py +0 -0
  194. {pytest_selective-1.1.0 → pytest_selective-1.1.2}/tests/testing/test_working_tree.py +0 -0
@@ -0,0 +1,26 @@
1
+ {
2
+ "tests": [
3
+ ".github",
4
+ "src",
5
+ "tests",
6
+ "scripts",
7
+ "actions",
8
+ "action.yml",
9
+ "pyproject.toml",
10
+ "uv.lock",
11
+ ".pre-commit-config.yaml"
12
+ ],
13
+ "contracts": [
14
+ ".github",
15
+ "src",
16
+ "contracts",
17
+ "tests/testing/git_repo.py",
18
+ "scripts",
19
+ "actions",
20
+ "action.yml",
21
+ "pyproject.toml",
22
+ "uv.lock",
23
+ ".pre-commit-config.yaml",
24
+ "README.md"
25
+ ]
26
+ }
@@ -29,8 +29,10 @@ jobs:
29
29
  "py311-macos":"tests (3.11, macos-latest)","py314-ubuntu":"tests (3.14, ubuntu-latest)",
30
30
  "py314-windows":"tests (3.14, windows-latest)","py314-macos":"tests (3.14, macos-latest)",
31
31
  "minimum":"pytest-minimum"}
32
+ # The contracts lane's one job. It reads README.md, so it keeps its own inputs and receipt.
33
+ CONTRACT_LANE_JOBS: '{"selection":"Selection contract (Quint)"}'
32
34
  outputs:
33
- # Planned lanes after reuse: the tests lane is false only when every one of its jobs is proven by a receipt.
35
+ # Planned lanes after reuse: a reusing lane is false only when every one of its jobs is proven by a receipt.
34
36
  lanes: ${{ steps.admit.outputs.lanes }}
35
37
  # The plan before reuse, for steps that follow changed runtime inputs rather than test execution.
36
38
  planned: ${{ steps.plan.outputs.lanes }}
@@ -38,6 +40,7 @@ jobs:
38
40
  files: ${{ steps.files.outputs.test_roots_json }}
39
41
  receipt: ${{ steps.evidence.outputs.receipt }}
40
42
  reused: ${{ steps.evidence.outputs.reused }}
43
+ contract-receipt: ${{ steps.contract-evidence.outputs.receipt }}
41
44
  steps:
42
45
  - uses: actions/checkout@v7
43
46
  with:
@@ -62,9 +65,11 @@ jobs:
62
65
  with:
63
66
  config: ${{ runner.temp }}/test-impact.json
64
67
  full-roots: tests
65
- # Everything the tests lane executes or reads, bound into each receipt.
68
+ # Everything each reusing lane executes or reads, bound into its receipts.
66
69
  - id: inputs
67
- run: echo "paths=$(jq -c . .github/validation-inputs.json)" >> "$GITHUB_OUTPUT"
70
+ run: |
71
+ echo "tests=$(jq -c .tests .github/validation-inputs.json)" >> "$GITHUB_OUTPUT"
72
+ echo "contracts=$(jq -c .contracts .github/validation-inputs.json)" >> "$GITHUB_OUTPUT"
68
73
  # CI-esta reuses a passing first attempt of the same job when its inputs and selected files are identical:
69
74
  # after a squash merge, on a version-only release PR, and on a PR push that changes nothing the tests read.
70
75
  # Release (workflow_call), schedules, manual runs and the test:full label always execute.
@@ -76,17 +81,34 @@ jobs:
76
81
  with:
77
82
  lanes: ${{ env.TEST_LANE_JOBS }}
78
83
  execution-context: ${{ steps.files.outputs.mode }} ${{ steps.files.outputs.test_roots_json }}
79
- input-paths: ${{ steps.inputs.outputs.paths }}
84
+ input-paths: ${{ steps.inputs.outputs.tests }}
85
+ reuse-after-merge: 'true'
86
+ reuse-version-only-release: 'true'
87
+ - id: contract-evidence
88
+ if: >-
89
+ ${{ (github.event_name == 'pull_request' || github.event_name == 'push') &&
90
+ !contains(github.event.pull_request.labels.*.name, 'test:full') }}
91
+ uses: gigaverse-app/ci-esta/actions/resolve-lane-evidence@v1
92
+ with:
93
+ lanes: ${{ env.CONTRACT_LANE_JOBS }}
94
+ execution-context: selection contract
95
+ input-paths: ${{ steps.inputs.outputs.contracts }}
80
96
  reuse-after-merge: 'true'
81
97
  reuse-version-only-release: 'true'
82
98
  - id: admit
83
- name: Admit the tests lane unless every job is proven
99
+ name: Admit each reusing lane unless every job is proven
84
100
  env:
85
101
  PLANNED_LANES: ${{ steps.plan.outputs.lanes }}
86
102
  REUSED: ${{ steps.evidence.outputs.reused || '{}' }}
103
+ CONTRACT_REUSED: ${{ steps.contract-evidence.outputs.reused || '{}' }}
87
104
  run: |
88
- admitted=$(jq -c --argjson jobs "$TEST_LANE_JOBS" --argjson reused "$REUSED" 'if [$jobs | keys[] | $reused[.] == true] | all then .tests = false else . end' <<<"$PLANNED_LANES")
89
- echo "reused=$REUSED"
105
+ admitted=$(jq -c \
106
+ --argjson tests "$TEST_LANE_JOBS" --argjson reused "$REUSED" \
107
+ --argjson contracts "$CONTRACT_LANE_JOBS" --argjson contract_reused "$CONTRACT_REUSED" '
108
+ def proven($jobs; $proof): [$jobs | keys[] | $proof[.] == true] | all;
109
+ (if proven($tests; $reused) then .tests = false else . end)
110
+ | (if proven($contracts; $contract_reused) then .contracts = false else . end)' <<<"$PLANNED_LANES")
111
+ echo "reused=$REUSED contracts=$CONTRACT_REUSED"
90
112
  echo "lanes=$admitted" | tee -a "$GITHUB_OUTPUT"
91
113
  quality:
92
114
  runs-on: ubuntu-latest
@@ -231,6 +253,9 @@ jobs:
231
253
  if: ${{ always() }}
232
254
  with:
233
255
  readme-mode: 'off'
256
+ - name: ${{ needs.select-tests.outputs.contract-receipt || 'No execution receipt' }}
257
+ if: ${{ needs.select-tests.result == 'success' && needs.select-tests.outputs.contract-receipt != '' }}
258
+ run: echo 'Recording a completed contract run for this tested tree.'
234
259
 
235
260
  validation-gate:
236
261
  name: Validation gate
@@ -0,0 +1,23 @@
1
+ name: dry-lint audit
2
+ # Weekly, or dispatched after merging a cleanup: dry-lint's own audit workflow sweeps the repository against the
3
+ # accepted backlog and opens or updates one dry-lint/ratchet pull request recording what was cleaned. Its steps ship
4
+ # with dry-lint releases; this file only says when it runs and with which credentials.
5
+ on:
6
+ schedule:
7
+ - cron: '23 6 * * 1'
8
+ workflow_dispatch:
9
+ # dry-lint.yml calls this file on a manual dispatch, to exercise the audit with the rest of the checks.
10
+ workflow_call:
11
+ permissions:
12
+ contents: read
13
+ jobs:
14
+ audit:
15
+ uses: gigaverse-app/dry-lint/.github/workflows/audit.yml@v1
16
+ with:
17
+ python-version: '3.13'
18
+ budget-usd: '5'
19
+ plugins: anthropic==2.4.0
20
+ secrets:
21
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
22
+ # The organization credential makes the ratchet pull request run ordinary CI.
23
+ DRY_LINT_RATCHET_TOKEN: ${{ secrets.GV_CI_CROSS_REPO_TOKEN }}
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "1.1.2"
3
+ }
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.1.2](https://github.com/gigaverse-app/pytest-selective/compare/v1.1.1...v1.1.2) (2026-11-09)
4
+
5
+
6
+ ### Bug fixes
7
+
8
+ * agent setup round 2, from a second adoption trial ([#114](https://github.com/gigaverse-app/pytest-selective/issues/114)) ([45d4afe](https://github.com/gigaverse-app/pytest-selective/commit/45d4afe76cde9f6093519d97000244c6c06b157d))
9
+
10
+ ## [1.1.1](https://github.com/gigaverse-app/pytest-selective/compare/v1.1.0...v1.1.1) (2026-11-09)
11
+
12
+
13
+ ### Bug fixes
14
+
15
+ * **selection:** follow file reads that name a tracked file by literal path ([#106](https://github.com/gigaverse-app/pytest-selective/issues/106)) ([5c534c4](https://github.com/gigaverse-app/pytest-selective/commit/5c534c48d3d098fb0e659973e2e40b410198af19))
16
+
3
17
  ## [1.1.0](https://github.com/gigaverse-app/pytest-selective/compare/v1.0.0...v1.1.0) (2026-11-09)
4
18
 
5
19
 
@@ -203,6 +203,10 @@ pytest. `tests/test_tutorial.py` owns the end-to-end tutorial execution and foll
203
203
  that plan; there is no second tutorial invocation in each matrix job. JUnit retains
204
204
  its test result and timing, and full plans also upload whole-suite coverage.
205
205
 
206
+ To run a complete checkpoint after a CI change, select **CI** in GitHub Actions
207
+ and choose **Run workflow**. Retrying an ordinary PR run preserves its selection;
208
+ use the manual checkpoint when you need full-suite coverage evidence.
209
+
206
210
  ## Duplication
207
211
 
208
212
  `uv run pre-commit install` installs both the staged dry-lint check and the
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pytest-selective
3
- Version: 1.1.0
3
+ Version: 1.1.2
4
4
  Summary: Explainable Python test impact analysis: select affected pytest files from Git changes
5
5
  Project-URL: Homepage, https://github.com/gigaverse-app/pytest-selective
6
6
  Project-URL: Documentation, https://pytest-selective.readthedocs.io/en/latest/
@@ -60,9 +60,9 @@ the affected test files, and explains the result. Use it directly as a pytest
60
60
  plugin, or plan before collection and service startup with the CLI and CI actions.
61
61
 
62
62
  Built from the selector used in Gigaverse's backend, AI, and shared Python
63
- infrastructure. Pin the actions and the package to the same release: `@vX.Y.Z`
64
- (or its commit SHA) and `pytest-selective==X.Y.Z`. The moving `@v1` tag follows
65
- the newest release.
63
+ infrastructure. Use the actions at the moving `@v1` tag, which follows the newest
64
+ release, and the package at `pytest-selective>=X.Y.Z,<2`. Where your tooling rejects
65
+ tags, pin both to one release instead: `@<sha> # vX.Y.Z` and `pytest-selective==X.Y.Z`.
66
66
  [Provenance and extraction scope](https://github.com/gigaverse-app/pytest-selective/blob/main/PROVENANCE.md).
67
67
 
68
68
  [Quick tutorial](https://github.com/gigaverse-app/pytest-selective/blob/main/docs/tutorial.md) ·
@@ -279,17 +279,18 @@ change that runs the contract.
279
279
  <!-- pytest-quint-connect:contracts selection -->
280
280
  <a href="https://github.com/gigaverse-app/pytest-quint-connect#impact"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pytest-quint-connect-counter.gigaverse-oss.workers.dev/v1/sign.svg?key=f8e41d50ca4018845c305f2f0dbd1cfd6d9b281e53b352490af53154a54e6265&amp;name=gigaverse-app%2Fpytest-selective&amp;theme=dark"><img alt="What pytest-quint-connect&#x27;s contracts caught in gigaverse-app/pytest-selective&#x27;s pull requests" src="https://pytest-quint-connect-counter.gigaverse-oss.workers.dev/v1/sign.svg?key=f8e41d50ca4018845c305f2f0dbd1cfd6d9b281e53b352490af53154a54e6265&amp;name=gigaverse-app%2Fpytest-selective" width="100%"></picture></a>
281
281
 
282
- <a href="https://github.com/gigaverse-app/pytest-quint-connect" title="Mutants: deliberately broken copies of the code. Each must make some history diverge, proving the contract would notice that kind of bug"><img alt="Quint contracts: 1 contract passed · 48 histories · 13/13 planted bugs caught" src="https://img.shields.io/badge/Quint_contracts-1_contract_passed_%C2%B7_48_histories_%C2%B7_13%2F13_planted_bugs_caught-brightgreen"></a>
282
+ <a href="https://github.com/gigaverse-app/pytest-quint-connect" title="Mutants: deliberately broken copies of the code. Each must make some history diverge, proving the contract would notice that kind of bug"><img alt="Quint contracts: 1 contract passed · 49 histories · 14/14 planted bugs caught" src="https://img.shields.io/badge/Quint_contracts-1_contract_passed_%C2%B7_49_histories_%C2%B7_14%2F14_planted_bugs_caught-brightgreen"></a>
283
283
 
284
284
  | | Contract | Cases passed | [Planted bugs caught](https://github.com/gigaverse-app/pytest-quint-connect/blob/main/docs/why.md#proof-that-the-tests-can-fail "Mutants: deliberately broken copies of the code. Each must make some history diverge, proving the contract would notice that kind of bug") |
285
285
  | :-: | :-- | --: | --: |
286
- | ✅ | `selection` `test_selection_contract.py::test_selection`<br><sub>48 histories (24 witnesses, 24 samples, seed 11) · every action covered · Quint 0.33.1</sub> | 62/62 | 13/13 |
286
+ | ✅ | `selection` `test_selection_contract.py::test_selection`<br><sub>49 histories (25 witnesses, 24 samples, seed 11) · every action covered · Quint 0.33.1</sub> | 64/64 | 14/14 |
287
287
 
288
288
  **Bugs this contract found**
289
289
 
290
- 1. **A test that does `from pkg import *` was not selected when a submodule pkg lists in __all__ changed.** Fixed in #94 · found by the runtime check (pytest loaded the changed file, the planner skipped the test), after an adversarial review reported it; witness starImportFollowsAllTest · regression test `tests/testing/test_impact_graph.py::test_star_import_reaches_the_submodules_its_package_lists_in_all`. 1 of 48 histories diverge when [planted bug](https://github.com/gigaverse-app/pytest-quint-connect/blob/main/docs/why.md#proof-that-the-tests-can-fail "Mutants: deliberately broken copies of the code. Each must make some history diverge, proving the contract would notice that kind of bug") `star-exports-ignored` (Reintroduces the star-import bug: from pkg import * no longer reaches the submodules pkg lists in __all__) reintroduces it; first caught by `witness-starImportFollowsAllTest` at state 2, after `edit(file='alpha', target='')`.
291
- 1. **Editing a plugin loaded by `addopts = -p NAME` selected no test, though it is loaded into every session.** Fixed in #95 · found by the runtime check (pytest loaded the changed file, the planner skipped the test), after an adversarial review reported it; witness commandLinePluginSelectsEveryTestTest and random histories · regression test `tests/testing/test_impact_graph.py::test_command_line_plugin_in_addopts_reaches_every_test`. 3 of 48 histories diverge when [planted bug](https://github.com/gigaverse-app/pytest-quint-connect/blob/main/docs/why.md#proof-that-the-tests-can-fail "Mutants: deliberately broken copies of the code. Each must make some history diverge, proving the contract would notice that kind of bug") `addopts-plugins-ignored` (Reintroduces the plugin bug: a plugin named by -p in addopts is no longer a dependency of every test) reintroduces it; first caught by `witness-commandLinePluginSelectsEveryTestTest` at state 1, after `edit(file='plugin', target='')`.
292
- 1. **A bare `import helpers` resolved by pytest's default import mode was not followed, so editing tests/helpers.py selected nothing.** Fixed in #96 · found by the runtime check (pytest loaded the changed file, the planner skipped the test), after an adversarial review reported it; witness bareTestImportFollowsPrependModeTest · regression test `tests/testing/test_impact_graph.py::test_bare_import_resolves_from_the_importers_prepend_mode_base_directory`. 1 of 48 histories diverge when [planted bug](https://github.com/gigaverse-app/pytest-quint-connect/blob/main/docs/why.md#proof-that-the-tests-can-fail "Mutants: deliberately broken copies of the code. Each must make some history diverge, proving the contract would notice that kind of bug") `prepend-roots-ignored` (Reintroduces the import-mode bug: imports no longer resolve from the directories pytest prepends to sys.path) reintroduces it; first caught by `witness-bareTestImportFollowsPrependModeTest` at state 2, after `edit(file='helper', target='')`.
290
+ 1. **A test that does `from pkg import *` was not selected when a submodule pkg lists in __all__ changed.** Fixed in #94 · found by the runtime check (pytest loaded the changed file, the planner skipped the test), after an adversarial review reported it; witness starImportFollowsAllTest · regression test `tests/testing/test_impact_graph.py::test_star_import_reaches_the_submodules_its_package_lists_in_all`. 1 of 49 histories diverge when [planted bug](https://github.com/gigaverse-app/pytest-quint-connect/blob/main/docs/why.md#proof-that-the-tests-can-fail "Mutants: deliberately broken copies of the code. Each must make some history diverge, proving the contract would notice that kind of bug") `star-exports-ignored` (Reintroduces the star-import bug: from pkg import * no longer reaches the submodules pkg lists in __all__) reintroduces it; first caught by `witness-starImportFollowsAllTest` at state 2, after `edit(file='alpha', target='')`.
291
+ 1. **Editing a plugin loaded by `addopts = -p NAME` selected no test, though it is loaded into every session.** Fixed in #95 · found by the runtime check (pytest loaded the changed file, the planner skipped the test), after an adversarial review reported it; witness commandLinePluginSelectsEveryTestTest and random histories · regression test `tests/testing/test_impact_graph.py::test_command_line_plugin_in_addopts_reaches_every_test`. 2 of 49 histories diverge when [planted bug](https://github.com/gigaverse-app/pytest-quint-connect/blob/main/docs/why.md#proof-that-the-tests-can-fail "Mutants: deliberately broken copies of the code. Each must make some history diverge, proving the contract would notice that kind of bug") `addopts-plugins-ignored` (Reintroduces the plugin bug: a plugin named by -p in addopts is no longer a dependency of every test) reintroduces it; first caught by `witness-commandLinePluginSelectsEveryTestTest` at state 1, after `edit(file='plugin', target='')`.
292
+ 1. **A bare `import helpers` resolved by pytest's default import mode was not followed, so editing tests/helpers.py selected nothing.** Fixed in #96 · found by the runtime check (pytest loaded the changed file, the planner skipped the test), after an adversarial review reported it; witness bareTestImportFollowsPrependModeTest · regression test `tests/testing/test_impact_graph.py::test_bare_import_resolves_from_the_importers_prepend_mode_base_directory`. 1 of 49 histories diverge when [planted bug](https://github.com/gigaverse-app/pytest-quint-connect/blob/main/docs/why.md#proof-that-the-tests-can-fail "Mutants: deliberately broken copies of the code. Each must make some history diverge, proving the contract would notice that kind of bug") `prepend-roots-ignored` (Reintroduces the import-mode bug: imports no longer resolve from the directories pytest prepends to sys.path) reintroduces it; first caught by `witness-bareTestImportFollowsPrependModeTest` at state 2, after `edit(file='helper', target='')`.
293
+ 1. **A test that read a tracked file with open() and a literal path was not selected when that file changed.** Fixed in #106 · found by the runtime check (pytest opened the changed file, the planner skipped the test), after an adversarial review reported it; witness literalPathReadTest · regression test `tests/testing/test_impact_graph.py::test_reading_a_tracked_file_by_literal_path_selects_the_reader`. 1 of 49 histories diverge when [planted bug](https://github.com/gigaverse-app/pytest-quint-connect/blob/main/docs/why.md#proof-that-the-tests-can-fail "Mutants: deliberately broken copies of the code. Each must make some history diverge, proving the contract would notice that kind of bug") `literal-reads-ignored` (Reintroduces the literal-read bug: open('data/table.json') no longer makes the reader depend on that file) reintroduces it; first caught by `witness-literalPathReadTest` at state 2, after `edit(file='table', target='')`.
293
294
 
294
295
  **[Planted bugs](https://github.com/gigaverse-app/pytest-quint-connect/blob/main/docs/why.md#proof-that-the-tests-can-fail "Mutants: deliberately broken copies of the code. Each must make some history diverge, proving the contract would notice that kind of bug") this contract catches**
295
296
 
@@ -369,5 +370,14 @@ records progress against the accepted backlog. [Setup and cleanup workflow](CONT
369
370
  <!-- dry-lint-progress:start -->
370
371
  <a href="https://github.com/gigaverse-app/dry-lint"><picture><source media="(prefers-color-scheme: dark)" srcset="https://dry-lint-counter.gigaverse-oss.workers.dev/v1/sign.svg?theme=dark&amp;key=f8e41d50ca4018845c305f2f0dbd1cfd6d9b281e53b352490af53154a54e6265&amp;name=gigaverse-app%2Fpytest-selective&amp;cleaned=9&amp;remaining=0&amp;dismissed=0"><img alt="dry-lint in gigaverse-app/pytest-selective: new duplicates kept out of its pull requests, 9 old duplicates cleaned up, 0 remaining" src="https://dry-lint-counter.gigaverse-oss.workers.dev/v1/sign.svg?key=f8e41d50ca4018845c305f2f0dbd1cfd6d9b281e53b352490af53154a54e6265&amp;name=gigaverse-app%2Fpytest-selective&amp;cleaned=9&amp;remaining=0&amp;dismissed=0" width="100%"></picture></a>
371
372
 
373
+ **Reviewed duplication debt** · Counts from the committed baselines.
374
+
375
+ | Accepted pairs | Remaining | Retired | Dismissed |
376
+ | :-- | --: | --: | --: |
377
+ | Functions | 0 | 9 | 0 |
378
+ | Models | Not initialized | — | — |
379
+
372
380
  [Accepted backlog](.dry-lint/baseline.json) · [Weekly audit](https://github.com/gigaverse-app/pytest-selective/actions/workflows/dry-lint-audit.yml)
381
+
382
+ Retired means a side was removed, or (functions only) the judge now finds the pair separate. It is not a count of refactors. Dismissals stay separate; an uninitialized model baseline is unknown.
373
383
  <!-- dry-lint-progress:end -->
@@ -29,9 +29,9 @@ the affected test files, and explains the result. Use it directly as a pytest
29
29
  plugin, or plan before collection and service startup with the CLI and CI actions.
30
30
 
31
31
  Built from the selector used in Gigaverse's backend, AI, and shared Python
32
- infrastructure. Pin the actions and the package to the same release: `@vX.Y.Z`
33
- (or its commit SHA) and `pytest-selective==X.Y.Z`. The moving `@v1` tag follows
34
- the newest release.
32
+ infrastructure. Use the actions at the moving `@v1` tag, which follows the newest
33
+ release, and the package at `pytest-selective>=X.Y.Z,<2`. Where your tooling rejects
34
+ tags, pin both to one release instead: `@<sha> # vX.Y.Z` and `pytest-selective==X.Y.Z`.
35
35
  [Provenance and extraction scope](https://github.com/gigaverse-app/pytest-selective/blob/main/PROVENANCE.md).
36
36
 
37
37
  [Quick tutorial](https://github.com/gigaverse-app/pytest-selective/blob/main/docs/tutorial.md) ·
@@ -248,17 +248,18 @@ change that runs the contract.
248
248
  <!-- pytest-quint-connect:contracts selection -->
249
249
  <a href="https://github.com/gigaverse-app/pytest-quint-connect#impact"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pytest-quint-connect-counter.gigaverse-oss.workers.dev/v1/sign.svg?key=f8e41d50ca4018845c305f2f0dbd1cfd6d9b281e53b352490af53154a54e6265&amp;name=gigaverse-app%2Fpytest-selective&amp;theme=dark"><img alt="What pytest-quint-connect&#x27;s contracts caught in gigaverse-app/pytest-selective&#x27;s pull requests" src="https://pytest-quint-connect-counter.gigaverse-oss.workers.dev/v1/sign.svg?key=f8e41d50ca4018845c305f2f0dbd1cfd6d9b281e53b352490af53154a54e6265&amp;name=gigaverse-app%2Fpytest-selective" width="100%"></picture></a>
250
250
 
251
- <a href="https://github.com/gigaverse-app/pytest-quint-connect" title="Mutants: deliberately broken copies of the code. Each must make some history diverge, proving the contract would notice that kind of bug"><img alt="Quint contracts: 1 contract passed · 48 histories · 13/13 planted bugs caught" src="https://img.shields.io/badge/Quint_contracts-1_contract_passed_%C2%B7_48_histories_%C2%B7_13%2F13_planted_bugs_caught-brightgreen"></a>
251
+ <a href="https://github.com/gigaverse-app/pytest-quint-connect" title="Mutants: deliberately broken copies of the code. Each must make some history diverge, proving the contract would notice that kind of bug"><img alt="Quint contracts: 1 contract passed · 49 histories · 14/14 planted bugs caught" src="https://img.shields.io/badge/Quint_contracts-1_contract_passed_%C2%B7_49_histories_%C2%B7_14%2F14_planted_bugs_caught-brightgreen"></a>
252
252
 
253
253
  | | Contract | Cases passed | [Planted bugs caught](https://github.com/gigaverse-app/pytest-quint-connect/blob/main/docs/why.md#proof-that-the-tests-can-fail "Mutants: deliberately broken copies of the code. Each must make some history diverge, proving the contract would notice that kind of bug") |
254
254
  | :-: | :-- | --: | --: |
255
- | ✅ | `selection` `test_selection_contract.py::test_selection`<br><sub>48 histories (24 witnesses, 24 samples, seed 11) · every action covered · Quint 0.33.1</sub> | 62/62 | 13/13 |
255
+ | ✅ | `selection` `test_selection_contract.py::test_selection`<br><sub>49 histories (25 witnesses, 24 samples, seed 11) · every action covered · Quint 0.33.1</sub> | 64/64 | 14/14 |
256
256
 
257
257
  **Bugs this contract found**
258
258
 
259
- 1. **A test that does `from pkg import *` was not selected when a submodule pkg lists in __all__ changed.** Fixed in #94 · found by the runtime check (pytest loaded the changed file, the planner skipped the test), after an adversarial review reported it; witness starImportFollowsAllTest · regression test `tests/testing/test_impact_graph.py::test_star_import_reaches_the_submodules_its_package_lists_in_all`. 1 of 48 histories diverge when [planted bug](https://github.com/gigaverse-app/pytest-quint-connect/blob/main/docs/why.md#proof-that-the-tests-can-fail "Mutants: deliberately broken copies of the code. Each must make some history diverge, proving the contract would notice that kind of bug") `star-exports-ignored` (Reintroduces the star-import bug: from pkg import * no longer reaches the submodules pkg lists in __all__) reintroduces it; first caught by `witness-starImportFollowsAllTest` at state 2, after `edit(file='alpha', target='')`.
260
- 1. **Editing a plugin loaded by `addopts = -p NAME` selected no test, though it is loaded into every session.** Fixed in #95 · found by the runtime check (pytest loaded the changed file, the planner skipped the test), after an adversarial review reported it; witness commandLinePluginSelectsEveryTestTest and random histories · regression test `tests/testing/test_impact_graph.py::test_command_line_plugin_in_addopts_reaches_every_test`. 3 of 48 histories diverge when [planted bug](https://github.com/gigaverse-app/pytest-quint-connect/blob/main/docs/why.md#proof-that-the-tests-can-fail "Mutants: deliberately broken copies of the code. Each must make some history diverge, proving the contract would notice that kind of bug") `addopts-plugins-ignored` (Reintroduces the plugin bug: a plugin named by -p in addopts is no longer a dependency of every test) reintroduces it; first caught by `witness-commandLinePluginSelectsEveryTestTest` at state 1, after `edit(file='plugin', target='')`.
261
- 1. **A bare `import helpers` resolved by pytest's default import mode was not followed, so editing tests/helpers.py selected nothing.** Fixed in #96 · found by the runtime check (pytest loaded the changed file, the planner skipped the test), after an adversarial review reported it; witness bareTestImportFollowsPrependModeTest · regression test `tests/testing/test_impact_graph.py::test_bare_import_resolves_from_the_importers_prepend_mode_base_directory`. 1 of 48 histories diverge when [planted bug](https://github.com/gigaverse-app/pytest-quint-connect/blob/main/docs/why.md#proof-that-the-tests-can-fail "Mutants: deliberately broken copies of the code. Each must make some history diverge, proving the contract would notice that kind of bug") `prepend-roots-ignored` (Reintroduces the import-mode bug: imports no longer resolve from the directories pytest prepends to sys.path) reintroduces it; first caught by `witness-bareTestImportFollowsPrependModeTest` at state 2, after `edit(file='helper', target='')`.
259
+ 1. **A test that does `from pkg import *` was not selected when a submodule pkg lists in __all__ changed.** Fixed in #94 · found by the runtime check (pytest loaded the changed file, the planner skipped the test), after an adversarial review reported it; witness starImportFollowsAllTest · regression test `tests/testing/test_impact_graph.py::test_star_import_reaches_the_submodules_its_package_lists_in_all`. 1 of 49 histories diverge when [planted bug](https://github.com/gigaverse-app/pytest-quint-connect/blob/main/docs/why.md#proof-that-the-tests-can-fail "Mutants: deliberately broken copies of the code. Each must make some history diverge, proving the contract would notice that kind of bug") `star-exports-ignored` (Reintroduces the star-import bug: from pkg import * no longer reaches the submodules pkg lists in __all__) reintroduces it; first caught by `witness-starImportFollowsAllTest` at state 2, after `edit(file='alpha', target='')`.
260
+ 1. **Editing a plugin loaded by `addopts = -p NAME` selected no test, though it is loaded into every session.** Fixed in #95 · found by the runtime check (pytest loaded the changed file, the planner skipped the test), after an adversarial review reported it; witness commandLinePluginSelectsEveryTestTest and random histories · regression test `tests/testing/test_impact_graph.py::test_command_line_plugin_in_addopts_reaches_every_test`. 2 of 49 histories diverge when [planted bug](https://github.com/gigaverse-app/pytest-quint-connect/blob/main/docs/why.md#proof-that-the-tests-can-fail "Mutants: deliberately broken copies of the code. Each must make some history diverge, proving the contract would notice that kind of bug") `addopts-plugins-ignored` (Reintroduces the plugin bug: a plugin named by -p in addopts is no longer a dependency of every test) reintroduces it; first caught by `witness-commandLinePluginSelectsEveryTestTest` at state 1, after `edit(file='plugin', target='')`.
261
+ 1. **A bare `import helpers` resolved by pytest's default import mode was not followed, so editing tests/helpers.py selected nothing.** Fixed in #96 · found by the runtime check (pytest loaded the changed file, the planner skipped the test), after an adversarial review reported it; witness bareTestImportFollowsPrependModeTest · regression test `tests/testing/test_impact_graph.py::test_bare_import_resolves_from_the_importers_prepend_mode_base_directory`. 1 of 49 histories diverge when [planted bug](https://github.com/gigaverse-app/pytest-quint-connect/blob/main/docs/why.md#proof-that-the-tests-can-fail "Mutants: deliberately broken copies of the code. Each must make some history diverge, proving the contract would notice that kind of bug") `prepend-roots-ignored` (Reintroduces the import-mode bug: imports no longer resolve from the directories pytest prepends to sys.path) reintroduces it; first caught by `witness-bareTestImportFollowsPrependModeTest` at state 2, after `edit(file='helper', target='')`.
262
+ 1. **A test that read a tracked file with open() and a literal path was not selected when that file changed.** Fixed in #106 · found by the runtime check (pytest opened the changed file, the planner skipped the test), after an adversarial review reported it; witness literalPathReadTest · regression test `tests/testing/test_impact_graph.py::test_reading_a_tracked_file_by_literal_path_selects_the_reader`. 1 of 49 histories diverge when [planted bug](https://github.com/gigaverse-app/pytest-quint-connect/blob/main/docs/why.md#proof-that-the-tests-can-fail "Mutants: deliberately broken copies of the code. Each must make some history diverge, proving the contract would notice that kind of bug") `literal-reads-ignored` (Reintroduces the literal-read bug: open('data/table.json') no longer makes the reader depend on that file) reintroduces it; first caught by `witness-literalPathReadTest` at state 2, after `edit(file='table', target='')`.
262
263
 
263
264
  **[Planted bugs](https://github.com/gigaverse-app/pytest-quint-connect/blob/main/docs/why.md#proof-that-the-tests-can-fail "Mutants: deliberately broken copies of the code. Each must make some history diverge, proving the contract would notice that kind of bug") this contract catches**
264
265
 
@@ -338,5 +339,14 @@ records progress against the accepted backlog. [Setup and cleanup workflow](CONT
338
339
  <!-- dry-lint-progress:start -->
339
340
  <a href="https://github.com/gigaverse-app/dry-lint"><picture><source media="(prefers-color-scheme: dark)" srcset="https://dry-lint-counter.gigaverse-oss.workers.dev/v1/sign.svg?theme=dark&amp;key=f8e41d50ca4018845c305f2f0dbd1cfd6d9b281e53b352490af53154a54e6265&amp;name=gigaverse-app%2Fpytest-selective&amp;cleaned=9&amp;remaining=0&amp;dismissed=0"><img alt="dry-lint in gigaverse-app/pytest-selective: new duplicates kept out of its pull requests, 9 old duplicates cleaned up, 0 remaining" src="https://dry-lint-counter.gigaverse-oss.workers.dev/v1/sign.svg?key=f8e41d50ca4018845c305f2f0dbd1cfd6d9b281e53b352490af53154a54e6265&amp;name=gigaverse-app%2Fpytest-selective&amp;cleaned=9&amp;remaining=0&amp;dismissed=0" width="100%"></picture></a>
340
341
 
342
+ **Reviewed duplication debt** · Counts from the committed baselines.
343
+
344
+ | Accepted pairs | Remaining | Retired | Dismissed |
345
+ | :-- | --: | --: | --: |
346
+ | Functions | 0 | 9 | 0 |
347
+ | Models | Not initialized | — | — |
348
+
341
349
  [Accepted backlog](.dry-lint/baseline.json) · [Weekly audit](https://github.com/gigaverse-app/pytest-selective/actions/workflows/dry-lint-audit.yml)
350
+
351
+ Retired means a side was removed, or (functions only) the judge now finds the pair separate. It is not a count of refactors. Dismissals stay separate; an uninitialized model baseline is unknown.
342
352
  <!-- dry-lint-progress:end -->
@@ -12,8 +12,10 @@ inputs:
12
12
  default: auto
13
13
  artifact-name:
14
14
  description: >
15
- Upload the lane plan under this name, where CI-esta's report renders it as
16
- the sticky's lane section. Empty skips the upload.
15
+ Upload the lane plan (test-lanes.json) as an artifact under this name, where
16
+ CI-esta's report renders it as the sticky's lane section. Give each planner job
17
+ in one workflow run its own name. Empty skips the upload; planning and the
18
+ outputs are unchanged.
17
19
  default: test-lane-selection
18
20
  outputs:
19
21
  lanes:
@@ -193,6 +193,9 @@ MUTANTS: dict[str, Callable[[], list[tuple[str, object]]]] = {
193
193
  ],
194
194
  "addopts-plugins-ignored": lambda: [("pytest_selective.impact_graph.command_line_plugins", lambda policies: set())],
195
195
  "prepend-roots-ignored": lambda: [("pytest_selective.impact_graph.prepend_roots", lambda python_files, config: [])],
196
+ "literal-reads-ignored": lambda: [
197
+ ("pytest_selective.impact_graph.literal_read_targets", lambda reader, literal, files: set())
198
+ ],
196
199
  }
197
200
 
198
201
 
@@ -210,6 +213,7 @@ class RepositoryDriver(SelectionDriver):
210
213
  self.imports: dict[str, set[str]] = {}
211
214
  self.declared: dict[str, set[str]] = {}
212
215
  self.broken: set[str] = set()
216
+ self.reads: set[str] = set()
213
217
  self.versions = dict.fromkeys(PATHS, 0)
214
218
  self.requirements = 0
215
219
  self.config = CONFIG
@@ -231,6 +235,8 @@ class RepositoryDriver(SelectionDriver):
231
235
  lines += [f"# gv:test-dependency: {PATHS[target]}" for target in sorted(self.declared.get(name, ()))]
232
236
  lines += [IMPORT_LINES[target] for target in sorted(self.imports.get(name, ()))]
233
237
  lines.append(f"VERSION = {version}")
238
+ if name in self.reads:
239
+ lines.append(f'TABLE = open("{PATHS["table"]}", encoding="utf-8").read()')
234
240
  if name in self.broken:
235
241
  lines.append("def broken(:")
236
242
  write(self.root, PATHS[name], "\n".join(lines) + "\n")
@@ -334,6 +340,7 @@ class RepositoryDriver(SelectionDriver):
334
340
  self.imports.pop(file, None)
335
341
  self.declared.pop(file, None)
336
342
  self.broken.discard(file)
343
+ self.reads.discard(file)
337
344
  (self.root / PATHS[file]).unlink()
338
345
  if file in CONTENT_SIDECARS:
339
346
  (self.root / CONTENT_SIDECARS[file][0]).unlink()
@@ -360,6 +367,10 @@ class RepositoryDriver(SelectionDriver):
360
367
  self.broken.discard(file)
361
368
  self._commit(file)
362
369
 
370
+ def read_table(self, file: str) -> None:
371
+ self.reads.add(file)
372
+ self._commit(file)
373
+
363
374
  def touch_global(self) -> None:
364
375
  self.requirements += 1
365
376
  self._write_requirements()
@@ -20,6 +20,7 @@
20
20
  "toggleConfig": "flip test_roots between tests and tests/sub, or add/remove data/table.json as a global input, then plan the last commit again",
21
21
  "restart": "close the graph cache (it prunes on close), reopen it, then plan the last commit again",
22
22
  "corrupt": "empty every stored graph's edges without updating its checksum, then plan the last commit again",
23
+ "readTable": "make a test read data/table.json with open() and a literal path at import time, commit, then select",
23
24
  "inspect": "read the last selections without committing"
24
25
  },
25
26
  "witnesses": [
@@ -46,6 +47,7 @@
46
47
  "commandLinePluginSelectsEveryTestTest",
47
48
  "bareTestImportFollowsPrependModeTest",
48
49
  "packageInitEditSelectsItsImportersTest",
50
+ "literalPathReadTest",
49
51
  "sinceStartSpansCommitsTest"
50
52
  ],
51
53
  "mutants": [
@@ -100,6 +102,10 @@
100
102
  {
101
103
  "name": "prepend-roots-ignored",
102
104
  "description": "Reintroduces the import-mode bug: imports no longer resolve from the directories pytest prepends to sys.path."
105
+ },
106
+ {
107
+ "name": "literal-reads-ignored",
108
+ "description": "Reintroduces the literal-read bug: open('data/table.json') no longer makes the reader depend on that file."
103
109
  }
104
110
  ],
105
111
  "bounds": {"seed": 11, "max_samples": 800, "max_steps": 8, "n_traces": 24},
@@ -116,7 +122,7 @@
116
122
  },
117
123
  "limits": [
118
124
  "One repository: three modules and their package initializer (with __all__), a test-root helper, a command-line plugin loaded by addopts -p, a root and a nested conftest, three tests, one data file and one content-kind sidecar; at most 8 operations per sampled history.",
119
- "Import spellings are import pkg.x, import tests.helpers, from pkg import * and a prepend-mode bare import helpers. Other forms (relative and dynamic imports, module strings, undeclared file reads, symlinks) are not generated.",
125
+ "Import spellings are import pkg.x, import tests.helpers, from pkg import * and a prepend-mode bare import helpers. Tests may also read data/table.json with open() and a literal repository path. Other forms (relative and dynamic imports, module strings, computed paths, symlinks) are not generated.",
120
126
  "The runtime check records imports and opened files during pytest's collection of each test file, with plugin autoloading off and the default prepend import mode. It is a lower bound: a test that loads a changed file must be selected, but selecting more is not a failure. It is skipped for mutant cases, which the model comparison judges.",
121
127
  "First-party pins, name-scoped modules, deferred strings and Django URLconf policy are outside the model. The graph cache is exercised through configuration edits, restarts and damaged entries; only answers are compared.",
122
128
  "Each step is a real Git commit or a re-plan of the last one, selected by affected() through one GraphCache per history; the driver's mapping of paths to file names is trusted."
@@ -142,6 +148,13 @@
142
148
  "found_by": "the runtime check (pytest loaded the changed file, the planner skipped the test), after an adversarial review reported it; witness bareTestImportFollowsPrependModeTest",
143
149
  "regression_test": "tests/testing/test_impact_graph.py::test_bare_import_resolves_from_the_importers_prepend_mode_base_directory",
144
150
  "mutant": "prepend-roots-ignored"
151
+ },
152
+ {
153
+ "summary": "A test that read a tracked file with open() and a literal path was not selected when that file changed.",
154
+ "fixed_in": "#106",
155
+ "found_by": "the runtime check (pytest opened the changed file, the planner skipped the test), after an adversarial review reported it; witness literalPathReadTest",
156
+ "regression_test": "tests/testing/test_impact_graph.py::test_reading_a_tracked_file_by_literal_path_selects_the_reader",
157
+ "mutant": "literal-reads-ignored"
145
158
  }
146
159
  ]
147
160
  }
@@ -25,8 +25,17 @@ module selection {
25
25
  versions: str -> int,
26
26
  broken: Set[str],
27
27
  requirements: int,
28
+ // Tests that read data/table.json with open() and a literal path, at import time.
29
+ reads: Set[str],
30
+ }
31
+ type Content = {
32
+ present: bool,
33
+ imports: Set[(str, str)],
34
+ declared: Set[(str, str)],
35
+ version: int,
36
+ broken: bool,
37
+ reads: bool,
28
38
  }
29
- type Content = { present: bool, imports: Set[(str, str)], declared: Set[(str, str)], version: int, broken: bool }
30
39
  // narrow: test_roots is tests/sub instead of tests. tableGlobal: data/table.json is a global input.
31
40
  type Config = { narrow: bool, tableGlobal: bool }
32
41
 
@@ -83,8 +92,9 @@ module selection {
83
92
  declared: t.declared.filter(d => d._1 == f),
84
93
  version: t.versions.get(f),
85
94
  broken: t.broken.contains(f),
95
+ reads: t.reads.contains(f),
86
96
  }
87
- else { present: false, imports: Set(), declared: Set(), version: 0, broken: false }
97
+ else { present: false, imports: Set(), declared: Set(), version: 0, broken: false, reads: false }
88
98
 
89
99
  // The files collected as tests under a configuration.
90
100
  pure def testsOf(c: Config): Set[str] = if (c.narrow) Set("test_sub") else TESTS
@@ -113,7 +123,9 @@ module selection {
113
123
  val changed = FILES.filter(f => content(before, f) != content(after, f))
114
124
  val global = before.requirements != after.requirements or (c.tableGlobal and changed.contains("table"))
115
125
  val tests = testsOf(c).intersect(after.present)
116
- val readers = CONTENT_READS
126
+ // A literal path read is a content read, like a kind: content sidecar.
127
+ val literal = before.reads.union(after.reads).map(f => (f, "table"))
128
+ val readers = CONTENT_READS.union(literal)
117
129
  .filter(r => changed.contains(r._2) and (before.present.contains(r._1) or after.present.contains(r._1)))
118
130
  .map(r => r._1)
119
131
  val chosen = if (global) tests
@@ -167,6 +179,7 @@ module selection {
167
179
  versions: FILES.mapBy(_ => 0),
168
180
  broken: Set(),
169
181
  requirements: 0,
182
+ reads: Set(),
170
183
  }
171
184
 
172
185
  action init = all {
@@ -212,6 +225,7 @@ module selection {
212
225
  present: tree.present.exclude(Set(file)),
213
226
  declared: tree.declared.filter(d => d._1 != file),
214
227
  broken: tree.broken.exclude(Set(file)),
228
+ reads: tree.reads.exclude(Set(file)),
215
229
  }),
216
230
  }
217
231
 
@@ -248,6 +262,14 @@ module selection {
248
262
  commit("repair", file, "", { ...tree, broken: tree.broken.exclude(Set(file)) }),
249
263
  }
250
264
 
265
+ // Make a test read data/table.json with open() and a literal path, without declaring it.
266
+ action readTable(file: str): bool = all {
267
+ TESTS.contains(file),
268
+ tree.present.contains(file),
269
+ not(tree.reads.contains(file)),
270
+ commit("readTable", file, "", { ...tree, reads: tree.reads.union(Set(file)) }),
271
+ }
272
+
251
273
  action touchGlobal = commit("touchGlobal", "", "", { ...tree, requirements: tree.requirements + 1 })
252
274
 
253
275
  // Flip one configuration option, then plan the last commit again.
@@ -288,6 +310,7 @@ module selection {
288
310
  undeclare(file, target),
289
311
  breakSyntax(file),
290
312
  repair(file),
313
+ readTable(file),
291
314
  touchGlobal,
292
315
  toggleConfig(option),
293
316
  restart,
@@ -380,6 +403,10 @@ module selection {
380
403
  run packageInitEditSelectsItsImportersTest = init.then(edit(INIT))
381
404
  .then(all { assert(observed == view("scoped", Set("test_one", "test_two"))), inspect })
382
405
 
406
+ // test_one now reads the table by literal path; the other two already depended on it.
407
+ run literalPathReadTest = init.then(readTable("test_one")).then(edit("table"))
408
+ .then(all { assert(observed == view("full", TESTS)), inspect })
409
+
383
410
  run sinceStartSpansCommitsTest = init.then(edit("beta")).then(edit("table"))
384
411
  .then(all {
385
412
  assert(observed == { mode: "scoped", selected: Set("test_two", "test_sub"), sinceStart: { mode: "full", selected: TESTS }, missed: Set() }),
@@ -28,7 +28,7 @@ class SelectionDriver(ActionDriver):
28
28
  """Subclass this, implement each action against the real code, and read state back in observe()."""
29
29
 
30
30
  __quint_contract__ = "selection"
31
- __quint_fingerprint__ = "85c02aff975baca4fd485a40a46b4b0d540998166173c5afb04a810ed92c5788"
31
+ __quint_fingerprint__ = "40f9e4a52a1f7fce52f35f086d3825648232670ff6aca12e7d8001fa80cb91a2"
32
32
  __quint_codegen_command__ = "pytest-quint-connect codegen quint-connect.json -o selection_driver.py"
33
33
  __quint_actions__ = {
34
34
  "init": ("init", ()),
@@ -45,6 +45,7 @@ class SelectionDriver(ActionDriver):
45
45
  "toggleConfig": ("toggle_config", (("option", "file"),)),
46
46
  "restart": ("restart", ()),
47
47
  "corrupt": ("corrupt", ()),
48
+ "readTable": ("read_table", (("file", "file"),)),
48
49
  "inspect": ("inspect", ()),
49
50
  }
50
51
 
@@ -104,6 +105,10 @@ class SelectionDriver(ActionDriver):
104
105
  def corrupt(self) -> None:
105
106
  """empty every stored graph's edges without updating its checksum, then plan the last commit again"""
106
107
 
108
+ @abstractmethod
109
+ def read_table(self, file: str) -> None:
110
+ """make a test read data/table.json with open() and a literal path at import time, commit, then select"""
111
+
107
112
  @abstractmethod
108
113
  def inspect(self) -> None:
109
114
  """read the last selections without committing"""
@@ -28,6 +28,10 @@ supported on GitHub.com with runner 2.336.0 or newer. Individual `actions/NAME`
28
28
  entry points call the implementations directly and do not require that syntax.
29
29
  [GitHub reference documentation](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax).
30
30
 
31
+ `plan-lanes` uploads its plan as the `test-lane-selection` artifact by default. Set
32
+ `artifact-name` to rename it, which a second planner job in the same workflow run needs, or to
33
+ `''` to skip the upload.
34
+
31
35
  ## Action reference
32
36
 
33
37
  Each link below is the public action's complete input/output contract.
@@ -253,11 +253,10 @@ job per workflow the required check and run
253
253
  gated job to its lanes. It fails unless each prerequisite succeeded or was skipped while
254
254
  the planner succeeded and every mapped lane is `false`; a failed planner fails it.
255
255
 
256
- Do not gate a job that a publish or deploy job `needs:` on pull-request-only logic:
257
- skipping it skips publishing. Keep it running on other events
258
- (`github.event_name != 'pull_request' || ...`), or plan inside the job with
256
+ Never gate a job that a publish or deploy job `needs:`, directly or transitively:
257
+ skipping it skips publishing. Leave it ungated, or plan inside the job with
259
258
  `continue-on-error: true` and admit only its expensive steps, as the `distributions`
260
- job in this repository's CI does. A branch's first push has no previous commit, so
259
+ job in this repository's CI does. Gate every other job that installs, builds or tests. A branch's first push has no previous commit, so
261
260
  `on: push` for every branch runs every lane once per new branch; prefer pull-request
262
261
  triggers plus pushes to the default branch.
263
262
  The default event policy handles manual/release checkpoints, schedules and
@@ -24,9 +24,10 @@ select. The default filenames are `test_*.py`, `*_test.py`, `*_tests.py`, and
24
24
 
25
25
  ## Review and record collection policy
26
26
 
27
- If your repository has `pyproject.toml`, `pytest.ini`, `tox.ini`, or `setup.cfg`,
28
- record its committed pytest-policy fingerprints. Even a discovered configuration
29
- file with no pytest settings can have a fingerprint. This review prevents a new
27
+ If your repository has `pyproject.toml`, `pytest.ini`, a `tox.ini` with a `[pytest]`
28
+ section, or a `setup.cfg` with a `[tool:pytest]` section, record its committed
29
+ pytest-policy fingerprints. A `pyproject.toml` or `pytest.ini` with no pytest settings
30
+ still has a fingerprint, because pytest can still choose it. This review prevents a new
30
31
  collection rule from silently invalidating the inventory.
31
32
 
32
33
  ```bash
@@ -58,7 +59,9 @@ runs the complete configured roots.
58
59
  Start with the [sidecar tutorial](tutorials/sidecar-dependencies.md) for a runnable
59
60
  fixture example, directory manifests, dependency comments, and dynamic Python inputs.
60
61
 
61
- For a test that reads a tracked JSON fixture, add a reader-owned comment:
62
+ A read that names a tracked file by literal path, such as `open("data/prices.json")` or
63
+ `(Path(__file__).parent / "prices.json").read_text()`, is followed automatically as a content read.
64
+ For a path that is computed, or built from several parts, add a reader-owned comment:
62
65
 
63
66
  ```python
64
67
  # gv:test-dependency: tests/data/prices.json
@@ -133,7 +136,11 @@ Unsupported checker patterns retain the full-check fallback.
133
136
 
134
137
  The policy fingerprints cover conventional pytest configuration names, including
135
138
  `pytest.toml`, `.pytest.toml`, `.pytest.ini` and nested candidates. Empty candidates
136
- are included because their presence can change pytest's root and precedence.
139
+ are included because their presence can change pytest's root and precedence. A
140
+ `tox.ini` without `[pytest]` and a `setup.cfg` without `[tool:pytest]` are not: pytest
141
+ skips them. Adding that section later changes the fingerprints and needs a review.
142
+ Releases up to 1.1.1 recorded such a file as the hash of an empty policy; that entry
143
+ is still accepted and needs no refresh.
137
144
  Changes require reviewing and refreshing `pytest_policy`; until then planning
138
145
  fails safely and the CI action runs the complete configured roots.
139
146
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pytest-selective"
7
- version = "1.1.0"
7
+ version = "1.1.2"
8
8
  description = "Explainable Python test impact analysis: select affected pytest files from Git changes"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"