feat(py_test): add opt-in safeguard against silently-passing tests#3825
feat(py_test): add opt-in safeguard against silently-passing tests#3825thirtyseven wants to merge 14 commits into
Conversation
py_test runs the main module directly and does not automatically invoke a test runner. A common pitfall is to define test classes/functions but forget to run them, causing the test to silently pass. Add a validation action, gated behind the new //python/config_settings:validate_test_main flag (auto/enabled/disabled, default off), that statically analyzes the main module with ast and fails the build if its top-level body is inert (only definitions, imports, assignments, docstrings) with nothing that runs tests. Fixes bazel-contrib#3824
Add an integration test that builds real py_test targets with the validate_test_main flag and asserts: - an inert test main fails the build with a descriptive error when enabled - a main that invokes a runner builds when enabled - an inert test builds when disabled and by default
There was a problem hiding this comment.
Code Review
This pull request introduces an opt-in safeguard (validate_test_main) to prevent py_test targets from silently passing when they do not actually execute any tests. It implements a static analysis validator using Python's ast module to detect inert main modules and fail the build if no active test runner is invoked. The feedback suggests extending the validator's inert node list to include ast.Global and ast.TypeAlias (for Python 3.12+ compatibility) and adding defensive checks in Starlark to safely access the exec_tools toolchain.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| exec_tools_toolchain = ctx.toolchains[EXEC_TOOLS_TOOLCHAIN_TYPE] | ||
| if exec_tools_toolchain == None or exec_tools_toolchain.exec_tools.exec_interpreter == None: | ||
| fail( | ||
| "Validating py_test main modules requires the exec tools toolchain " + | ||
| "with an exec interpreter, but none was found. Either register one " + | ||
| "or set --@rules_python//python/config_settings:validate_test_main=disabled.", | ||
| ) |
There was a problem hiding this comment.
To prevent potential Starlark evaluation errors (such as struct has no field 'exec_tools'), we should defensively check if exec_tools_toolchain actually has the exec_tools attribute and that it is not None before attempting to access exec_tools.exec_interpreter.
exec_tools_toolchain = ctx.toolchains[EXEC_TOOLS_TOOLCHAIN_TYPE]\n if (exec_tools_toolchain == None or \n not hasattr(exec_tools_toolchain, 'exec_tools') or \n exec_tools_toolchain.exec_tools == None or \n exec_tools_toolchain.exec_tools.exec_interpreter == None):\n fail(\n 'Validating py_test main modules requires the exec tools toolchain ' +\n 'with an exec interpreter, but none was found. Either register one ' +\n 'or set --@rules_python//python/config_settings:validate_test_main=disabled.',\n )
There was a problem hiding this comment.
I'll leave this as-is for consistency with the existing precompile.bzl, which accesses exec_tools_toolchain.exec_tools.precompiler with the same pattern (only a == None check on the toolchain). The exec tools toolchain rule always constructs a PyExecToolsInfo, so .exec_tools is guaranteed present when the toolchain resolves; adding a hasattr guard here would diverge from the rest of the codebase. Happy to revisit if there's a known case of a custom exec_tools_toolchain_type that returns a bare ToolchainInfo.
Address review feedback: top-level `global x` and PEP 695 `type X = ...` aliases don't run any test code, so they should count as inert when deciding whether a test main runs something. `ast.TypeAlias` only exists on Python 3.12+, so it's added dynamically.
The uv module extension transitively loads common_labels.bzl, so adding the VALIDATE_TEST_MAIN label changed the extension's bzlTransitiveDigest. Regenerate the integration test's MODULE.bazel.lock to match.
Only flag a main module as inert when it actually defines test classes or functions but never runs them. A module that defines nothing and only imports other modules (e.g. an aggregator that relies on import side effects) is now always allowed, avoiding false positives. Update unit tests, docs, and add an import-only e2e case.
The import-only fixture intentionally has an unused import; add a noqa so ruff (pinned v0.15.14 in CI) passes. The comment doesn't affect the AST, so the module stays import-only for the validator.
There was a problem hiding this comment.
Code Review
This pull request introduces an opt-in build-time validation safeguard (validate_test_main) for py_test targets to prevent them from silently passing when they define tests but fail to invoke a test runner. The validation statically analyzes the main module using an AST parser to ensure it contains active statements. The review feedback highlights a critical gap where compound statements like ast.If (e.g., if TYPE_CHECKING:) and ast.Try (e.g., try/except ImportError:) are not recursively checked, allowing inert files to bypass validation. Suggestions were provided to recursively check these blocks and add corresponding unit tests.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
Address review feedback: top-level `if TYPE_CHECKING:` and `try: import foo except ImportError:` blocks whose branches are entirely inert previously counted as "active" and let an otherwise-inert test main bypass the check. Inspect `if`/`try`/`try*` branches recursively so these common import guards are correctly treated as inert. The `if` condition is ignored (a guard doesn't run tests; a runner call in a branch body is still detected as active). Add unit tests for TYPE_CHECKING, try/except ImportError, and a runner invoked inside an `if` body.
|
/gemini PTAL |
There was a problem hiding this comment.
Code Review
This pull request introduces an opt-in safeguard (validate_test_main) for py_test targets to statically analyze the main module and prevent tests from silently passing without executing any test code. It adds a validator script (py_test_main_validator.py) using Python's ast module to detect inert top-level statements, along with corresponding build settings, flags, integration tests, and unit tests. The review feedback correctly identifies a critical issue where all assignments (e.g., exit_code = unittest.main()) are unconditionally treated as inert, which would cause false positives. The reviewer suggests dynamically checking assignments and expressions for active nodes (like function calls) using ast.walk and adding corresponding unit tests to verify this behavior.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| """ | ||
| ) | ||
| ) | ||
|
|
There was a problem hiding this comment.
Add unit tests to verify that active assignments (e.g., exit_code = unittest.main()) and active expressions (e.g., sys.exit(unittest.main())) are correctly identified as active and allowed by the validator.
)
def test_definitions_with_active_assignment_runs_tests(self):
self.assertTrue(
_runs_tests(
"""
import unittest
class MyTest(unittest.TestCase):
def test_foo(self):
pass
exit_code = unittest.main()
"""
)
)
def test_definitions_with_active_expression_runs_tests(self):
self.assertTrue(
_runs_tests(
"""
import unittest
class MyTest(unittest.TestCase):
def test_foo(self):
pass
sys.exit(unittest.main())
"""
)
)There was a problem hiding this comment.
Added in 66811ad — test_definitions_with_active_assignment_runs_tests and test_definitions_with_active_expression_runs_tests.
Address review feedback: assignments were unconditionally inert, so a main like `exit_code = unittest.main()` (which actually runs the tests) was a false positive that would fail a valid test build. Mark an assignment or expression statement inert only when its value contains no call/await/yield; class/function bodies are still skipped (those node types remain unconditionally inert), so the method-body calls don't defeat the check. Add unit tests for active assignment and active expression mains.
|
/gemini PTAL |
There was a problem hiding this comment.
Code Review
This pull request introduces an opt-in safeguard (validate_test_main) to prevent py_test targets from silently passing when they define tests but fail to invoke a test runner. It implements a static analysis tool (py_test_main_validator.py) using Python's ast module to check for active statements in the test's main module and integrates this check as a build-time validation action. Feedback on the changes includes explicitly handling ast.Assert statements as inert in the validator (with corresponding unit tests) and appending to the _validation output group in py_executable.bzl rather than overwriting it to prevent conflicts with other validation actions.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Address review feedback: add unit tests for the ast.Assert handling -- an inert `assert True` with a definition is rejected, while an assert whose condition runs a call (e.g. `assert unittest.main()`) is treated as active.
|
/gemini PTAL |
There was a problem hiding this comment.
Code Review
This pull request introduces an opt-in build-time validation safeguard for py_test targets to prevent them from silently passing when no tests are actually executed. It adds the --@rules_python//python/config_settings:validate_test_main flag and a static analysis tool (py_test_main_validator.py) that parses the test's main module using the ast module to ensure it contains active statements (like invoking a test runner) rather than only inert definitions or imports. The feedback points out a potential false positive where a test runner is invoked directly inside an if condition (e.g., if unittest.main().wasSuccessful():), and suggests updating the validator to check if the condition expression itself runs code.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
The if handling now also checks the condition for calls; update the comment which still claimed the condition was ignored.
Summary
Implements #3824.
py_testruns the main module directly and does not automatically invoke a test runner (e.g.unittest/pytest). A common pitfall is to define test classes/functions but forget to run them, so the test silently passes without executing anything.This adds a validation action to
py_testthat statically analyzes the main module withastand fails the build if the module's top-level body is "inert" — only definitions, imports, assignments, and docstrings, with nothing that actually runs tests (such as anif __name__ == "__main__":guard invoking a runner).Because this is technically a breaking change, it is gated behind a new flag,
//python/config_settings:validate_test_main, with valuesauto/enabled/disabled. The default isauto, which currently resolves todisabled, and can be flipped toenabledin rules_python 3.0.Notes/limitations:
py_testtargets with amainsource file;main_moduletargets are not checked.Test plan
tests/base_rules/py_test) verify thePyValidateTestMainaction and
_validationoutput group are present when enabled and absentwhen disabled.
tests/validate_test_main) covers theastclassification(inert vs. runs-something).
tests/integration/validate_test_main)builds real
py_testtargets with the flag and asserts an inert test failsthe build with a descriptive error when enabled, while a correct test builds;
and that an inert test builds when disabled / by default.