Skip to content

STD placeholder stats: scan and report unimplemented test placeholders#4058

Open
rnetser wants to merge 8 commits intoRedHatQE:mainfrom
rnetser:feat/std-placeholder-stats
Open

STD placeholder stats: scan and report unimplemented test placeholders#4058
rnetser wants to merge 8 commits intoRedHatQE:mainfrom
rnetser:feat/std-placeholder-stats

Conversation

@rnetser
Copy link
Collaborator

@rnetser rnetser commented Mar 3, 2026

Summary

A CLI tool that scans the tests/ directory for STD (Standard Test Design) placeholder tests — tests marked with __test__ = False that contain only docstrings describing expected behavior, without actual implementation.

What it does

  • Recursively scans test_*.py files using AST parsing
  • Detects __test__ = False at three levels:
    • Module-level: all classes and functions in the file are placeholders
    • Class-level: all methods in the class are placeholders
    • Method/function-level: individual method.__test__ = False assignments
  • Reports placeholder counts grouped by file and class
  • Supports two output formats: human-readable text and machine-readable JSON

Usage

# Text output (default)
uv run python std_placeholder_stats.py

# Scan a custom directory
uv run python std_placeholder_stats.py --tests-dir my_tests

# JSON output
uv run python std_placeholder_stats.py --output-format json

Example output

========================================
STD PLACEHOLDER TESTS (not yet implemented)
========================================

tests/network/ipv6/test_connectivity.py::TestIPv6Connectivity
  - test_pod_to_pod_ipv6
  - test_service_ipv6_endpoint
tests/storage/test_snapshots.py::<standalone>
  - test_snapshot_restore

----------------------------------------
Total: 3 placeholder tests in 2 files
========================================

Co-authored-by: Claude noreply@anthropic.com

Test plan

  • 37 unit tests across 10 test classes
  • Covers all detection levels (module, class, method, standalone function)
  • Graceful handling of syntax errors and unreadable files
  • Both text and JSON output formats verified

Summary by CodeRabbit

  • New Features

    • Added a CLI tool to scan test suites for placeholder tests and report findings in text or JSON.
  • Tests

    • Added a comprehensive unit test suite covering detection, aggregation, formatting, CLI output, and error handling for placeholder test reporting.
  • Chores

    • Updated pre-commit linting to exclude specific test utility paths from checks.

@rnetser rnetser marked this pull request as draft March 3, 2026 15:25
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 3, 2026

📝 Walkthrough

Walkthrough

Adds a flake8 exclude pattern to pre-commit, introduces a new CLI tool that discovers and reports STD placeholder tests (modules/classes/functions marked __test__ = False) in text or JSON, and adds a comprehensive unit test suite for that tool.

Changes

Cohort / File(s) Summary
Pre-commit Configuration
\.pre-commit-config.yaml
Added flake8 hook exclude patterns to skip utilities/unittests/, utilities/junit_ai_utils.py, and scripts/std_placeholder_stats/tests/.
STD Placeholder Stats Script
scripts/std_placeholder_stats/std_placeholder_stats.py
New AST-based CLI script. Adds data classes (PlaceholderClass, PlaceholderFile), scanning/collection logic, placeholder detection utilities, text/JSON output, logging, and a Click-based main entrypoint.
Test Suite
scripts/std_placeholder_stats/tests/test_std_placeholder_stats.py
New comprehensive unit tests covering AST utilities, scanning behavior, error handling, formatting/output (text & JSON), counting, and edge cases (syntax errors, unreadable files, nested dirs).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: introducing a CLI tool to scan and report STD placeholder tests.
Description check ✅ Passed The description covers most required sections with clear explanations of purpose, usage, and test coverage, though 'jira-ticket' is missing.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

rnetser added 3 commits March 4, 2026 12:49
…ve structure

- Replace argparse CLI with click commands and options
- Introduce PlaceholderClass and PlaceholderFile dataclasses
- Consolidate four AST visitor functions into a single unified function
- Split large functions and extract constants for readability
- Add error handling for file parsing and output operations
- Improve test coverage with additional edge case tests
@rnetser rnetser changed the title feat: add STD placeholder stats generator to track unimplemented tests Refactor STD placeholder stats: click CLI, dataclasses, and improved structure Mar 4, 2026
@rnetser rnetser marked this pull request as ready for review March 4, 2026 14:28
@openshift-virtualization-qe-bot-3
Copy link
Contributor

Report bugs in Issues

Welcome! 🎉

This pull request will be automatically processed with the following features:

🔄 Automatic Actions

  • Reviewer Assignment: Reviewers are automatically assigned based on the OWNERS file in the repository root
  • Size Labeling: PR size labels (XS, S, M, L, XL, XXL) are automatically applied based on changes
  • Issue Creation: A tracking issue is created for this PR and will be closed when the PR is merged or closed
  • Branch Labeling: Branch-specific labels are applied to track the target branch
  • Auto-verification: Auto-verified users have their PRs automatically marked as verified
  • Labels: Enabled categories: branch, can-be-merged, cherry-pick, has-conflicts, hold, needs-rebase, size, verified, wip

📋 Available Commands

PR Status Management

  • /wip - Mark PR as work in progress (adds WIP: prefix to title)
  • /wip cancel - Remove work in progress status
  • /hold - Block PR merging (approvers only)
  • /hold cancel - Unblock PR merging
  • /verified - Mark PR as verified
  • /verified cancel - Remove verification status
  • /reprocess - Trigger complete PR workflow reprocessing (useful if webhook failed or configuration changed)
  • /regenerate-welcome - Regenerate this welcome message

Review & Approval

  • /lgtm - Approve changes (looks good to me)
  • /approve - Approve PR (approvers only)
  • /assign-reviewers - Assign reviewers based on OWNERS file
  • /assign-reviewer @username - Assign specific reviewer
  • /check-can-merge - Check if PR meets merge requirements

Testing & Validation

  • /retest tox - Run Python test suite with tox
  • /retest build-container - Rebuild and test container image
  • /retest verify-bugs-are-open - verify-bugs-are-open
  • /retest all - Run all available tests

Container Operations

  • /build-and-push-container - Build and push container image (tagged with PR number)
    • Supports additional build arguments: /build-and-push-container --build-arg KEY=value

Cherry-pick Operations

  • /cherry-pick <branch> - Schedule cherry-pick to target branch when PR is merged
    • Multiple branches: /cherry-pick branch1 branch2 branch3

Label Management

  • /<label-name> - Add a label to the PR
  • /<label-name> cancel - Remove a label from the PR

✅ Merge Requirements

This PR will be automatically approved when the following conditions are met:

  1. Approval: /approve from at least one approver
  2. LGTM Count: Minimum 2 /lgtm from reviewers
  3. Status Checks: All required status checks must pass
  4. No Blockers: No WIP, hold, conflict labels
  5. Verified: PR must be marked as verified (if verification is enabled)

📊 Review Process

Approvers and Reviewers

Approvers:

  • dshchedr
  • myakove
  • rnetser
  • vsibirsk

Reviewers:

  • RoniKishner
  • dshchedr
  • rnetser
  • vsibirsk
Available Labels
  • hold
  • verified
  • wip
  • lgtm
  • approve

💡 Tips

  • WIP Status: Use /wip when your PR is not ready for review
  • Verification: The verified label is automatically removed on each new commit
  • Cherry-picking: Cherry-pick labels are processed when the PR is merged
  • Container Builds: Container images are automatically tagged with the PR number
  • Permission Levels: Some commands require approver permissions
  • Auto-verified Users: Certain users have automatic verification and merge privileges

For more information, please refer to the project documentation or contact the maintainers.

@rnetser rnetser changed the title Refactor STD placeholder stats: click CLI, dataclasses, and improved structure STD placeholder stats: scan and report unimplemented test placeholders Mar 4, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.pre-commit-config.yaml:
- Line 49: The current pre-commit "exclude" regex is too broad (the exclude key
with "(utilities/unittests/|utilities/junit_ai_utils\.py|scripts/.*/tests/)")
and removes linting for entire directories; change it to exclude only the
specific files that need E402 handling by replacing the directory patterns with
file-level or narrowly scoped regexes (e.g., target exact filenames under
utilities/unittests and exact script test paths) so the exclude remains limited
to those unique files; update the exclude value to list only the necessary file
paths (keeping utilities/junit_ai_utils\.py) and remove the blanket
utilities/unittests/ and scripts/.*/tests/ directory-wide patterns.

In `@scripts/std_placeholder_stats/std_placeholder_stats.py`:
- Around line 217-218: The current prefilter checks for the exact substring
f"{TEST_ATTR} = False" in file_content and will miss valid variants like
"__test__=False" or other whitespace/comments; update the prefilter that uses
TEST_ATTR and file_content to detect assignments more robustly by matching a
regex for TEST_ATTR\s*=\s*False (or use AST parsing to look for an ast.Assign to
a Name with id == TEST_ATTR and a Constant False) so all whitespace variants are
accepted before deciding to continue; modify the conditional around that check
(the block using TEST_ATTR and file_content) to use the regex/AST check instead.
- Around line 214-216: The except blocks that catch (UnicodeDecodeError,
OSError) and the similar block later currently log and continue, which swallows
malformed inputs; instead, after logging include context and re-raise the
original exception to fail fast — update the handlers that reference LOGGER and
test_file to call LOGGER.warning with the contextual message and then do "raise"
(re-raising the caught exc to preserve traceback) so processing stops on
read/parse errors rather than continuing silently.

In `@scripts/std_placeholder_stats/tests/test_std_placeholder_stats.py`:
- Around line 507-530: Add two tests that exercise the UnicodeDecodeError and
OSError branches in scan_placeholder_tests: create one test file with
undecodable bytes (e.g., write bytes that will raise UnicodeDecodeError when
opened as text) and assert it is skipped like the SyntaxError case; create
another test that simulates an OSError when reading (e.g., remove read
permissions or mock open to raise OSError) and assert scan_placeholder_tests
skips that file as well. Use the existing helper _create_test_file and the test
harness pattern in test_handles_syntax_errors_gracefully and reference
scan_placeholder_tests and the file path assertions to verify the
unreadable/undecodable files are not included while valid test files are.
- Around line 623-630: The test currently forces logger.propagate to False in
the finally block which can leak state; modify the test around the logger setup
(the logger variable created with
logging.getLogger("scripts.std_placeholder_stats.std_placeholder_stats")) to
save the original value (e.g., original_propagate = logger.propagate) before
changing it, set logger.propagate = True for the test, and in the finally block
restore logger.propagate = original_propagate so the logger's propagate state is
returned to its prior value after calling output_text(...) under caplog; apply
the same pattern to the other similar block around lines 639-645.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 219ee2cc-86cc-4d80-a5ad-0835784a3da7

📥 Commits

Reviewing files that changed from the base of the PR and between 5a74c7f and 05cfc72.

📒 Files selected for processing (5)
  • .pre-commit-config.yaml
  • scripts/std_placeholder_stats/__init__.py
  • scripts/std_placeholder_stats/std_placeholder_stats.py
  • scripts/std_placeholder_stats/tests/__init__.py
  • scripts/std_placeholder_stats/tests/test_std_placeholder_stats.py

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (3)
scripts/std_placeholder_stats/tests/test_std_placeholder_stats.py (1)

507-573: ⚠️ Potential issue | 🟡 Minor

MEDIUM: Error-path tests are still partially non-deterministic

Line 550 uses chmod(0o000) to trigger OSError, which can be unreliable on privileged runners, and this block still does not explicitly exercise the UnicodeDecodeError path. Prefer deterministic branch forcing (mock Path.read_text for OSError, and write undecodable bytes for decode failure).

Deterministic coverage pattern
+    def test_handles_unicode_decode_errors_gracefully(self, tests_dir: Path) -> None:
+        """scan_placeholder_tests() skips undecodable files and continues."""
+        undecodable_path = tests_dir / "test_bad_encoding.py"
+        undecodable_path.write_bytes(b"\xff\xfe\xfa")
+        _create_test_file(
+            directory=tests_dir,
+            filename="test_valid.py",
+            content=f'{TEST_FALSE_MARKER}\n\nclass TestGood:\n    def test_pass(self):\n        """Placeholder test."""\n',
+        )
+
+        result = scan_placeholder_tests(tests_dir=tests_dir)
+        file_paths = [placeholder_file.file_path for placeholder_file in result]
+        assert "tests/test_bad_encoding.py" not in file_paths
+        assert "tests/test_valid.py" in file_paths
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/std_placeholder_stats/tests/test_std_placeholder_stats.py` around
lines 507 - 573, The test_handles_unreadable_files_gracefully test is
non-deterministic because it uses unreadable.chmod(0o000) to provoke an OSError
and doesn't exercise UnicodeDecodeError; instead, replace the chmod approach by
mocking Path.read_text (or scan_placeholder_tests' internal file read call) to
raise OSError for the unreadable path and create a second fixture file that
contains bytes not decodable as UTF-8 to trigger UnicodeDecodeError when read;
update assertions to ensure scan_placeholder_tests ignores both failing files
and still returns the readable file (reference
test_handles_unreadable_files_gracefully, scan_placeholder_tests, and
Path.read_text to locate where to apply the mock and where to create the
undecodable bytes file), and remove any chmod-based permission manipulation so
cleanup is deterministic.
.pre-commit-config.yaml (1)

49-49: ⚠️ Potential issue | 🟠 Major

HIGH: Flake8 exclusion is still too broad at directory scope

Line 49 excludes whole directories (utilities/unittests/, scripts/std_placeholder_stats/tests/), which weakens lint coverage for unrelated/new files in those trees. Keep excludes file-scoped to only the known exceptions.

Suggested narrowing
-        exclude: "(utilities/unittests/|utilities/junit_ai_utils\\.py|scripts/std_placeholder_stats/tests/)"
+        exclude: "(utilities/unittests/test_oadp\\.py|utilities/junit_ai_utils\\.py|scripts/std_placeholder_stats/tests/test_std_placeholder_stats\\.py)"

Based on learnings, only specific files in utilities/unittests require targeted E402 handling; broad directory exclusion is not required.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.pre-commit-config.yaml at line 49, The flake8 exclude pattern under the
exclude key currently removes entire directories via the pattern
"(utilities/unittests/|utilities/junit_ai_utils\.py|scripts/std_placeholder_stats/tests/)"
— narrow this to file-scoped excludes instead: remove the directory-wide
fragments ("utilities/unittests/" and "scripts/std_placeholder_stats/tests/")
and replace them with explicit file patterns for the few known exceptions (e.g.
the specific test files in utilities/unittests that need E402 and the exact
files under scripts/std_placeholder_stats/tests/), keeping
utilities/junit_ai_utils\.py as-is; update the exclude string to list only those
exact filenames so unrelated/new files are still linted.
scripts/std_placeholder_stats/std_placeholder_stats.py (1)

147-155: ⚠️ Potential issue | 🔴 Critical

CRITICAL: Placeholder detection currently accepts docstring + pass

Line 147-Line 155 classifies docstring + pass as placeholder, which breaks STD semantics (“docstring-only and no implementation statements”). This can overcount implemented stubs as placeholders.

Strict docstring-only fix
 def _is_placeholder_body(func_node: ast.FunctionDef) -> bool:
@@
-    # A docstring-only body has exactly one statement: an Expr containing a Constant string
-    if len(func_node.body) == 1:
-        stmt = func_node.body[0]
-        return isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Constant) and isinstance(stmt.value.value, str)
-    # Also allow docstring + pass (common pattern)
-    if len(func_node.body) == 2:
-        first = func_node.body[0]
-        second = func_node.body[1]
-        is_docstring = (
-            isinstance(first, ast.Expr) and isinstance(first.value, ast.Constant) and isinstance(first.value.value, str)
-        )
-        is_pass = isinstance(second, ast.Pass)
-        return is_docstring and is_pass
-    return False
+    if len(func_node.body) != 1:
+        return False
+    statement = func_node.body[0]
+    return (
+        isinstance(statement, ast.Expr)
+        and isinstance(statement.value, ast.Constant)
+        and isinstance(statement.value.value, str)
+    )

Based on learnings, __test__ = False is allowed only for STD placeholder tests with ONLY docstrings and no implementation code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/std_placeholder_stats/std_placeholder_stats.py` around lines 147 -
155, The placeholder detection currently treats a two-statement body with a
docstring followed by a Pass as a placeholder (the block that checks
len(func_node.body) == 2 and uses is_docstring and is_pass); change this to
require a docstring-only body (i.e., only accept placeholder when the
function/class body has a single Expr constant string and no other statements),
remove the docstring+pass acceptance, and ensure any logic that allows `__test__
= False` is only applied when the body is strictly docstring-only rather than
docstring+pass.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In @.pre-commit-config.yaml:
- Line 49: The flake8 exclude pattern under the exclude key currently removes
entire directories via the pattern
"(utilities/unittests/|utilities/junit_ai_utils\.py|scripts/std_placeholder_stats/tests/)"
— narrow this to file-scoped excludes instead: remove the directory-wide
fragments ("utilities/unittests/" and "scripts/std_placeholder_stats/tests/")
and replace them with explicit file patterns for the few known exceptions (e.g.
the specific test files in utilities/unittests that need E402 and the exact
files under scripts/std_placeholder_stats/tests/), keeping
utilities/junit_ai_utils\.py as-is; update the exclude string to list only those
exact filenames so unrelated/new files are still linted.

In `@scripts/std_placeholder_stats/std_placeholder_stats.py`:
- Around line 147-155: The placeholder detection currently treats a
two-statement body with a docstring followed by a Pass as a placeholder (the
block that checks len(func_node.body) == 2 and uses is_docstring and is_pass);
change this to require a docstring-only body (i.e., only accept placeholder when
the function/class body has a single Expr constant string and no other
statements), remove the docstring+pass acceptance, and ensure any logic that
allows `__test__ = False` is only applied when the body is strictly
docstring-only rather than docstring+pass.

In `@scripts/std_placeholder_stats/tests/test_std_placeholder_stats.py`:
- Around line 507-573: The test_handles_unreadable_files_gracefully test is
non-deterministic because it uses unreadable.chmod(0o000) to provoke an OSError
and doesn't exercise UnicodeDecodeError; instead, replace the chmod approach by
mocking Path.read_text (or scan_placeholder_tests' internal file read call) to
raise OSError for the unreadable path and create a second fixture file that
contains bytes not decodable as UTF-8 to trigger UnicodeDecodeError when read;
update assertions to ensure scan_placeholder_tests ignores both failing files
and still returns the readable file (reference
test_handles_unreadable_files_gracefully, scan_placeholder_tests, and
Path.read_text to locate where to apply the mock and where to create the
undecodable bytes file), and remove any chmod-based permission manipulation so
cleanup is deterministic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e526e8d1-976b-4414-8c72-ea066147ccce

📥 Commits

Reviewing files that changed from the base of the PR and between e106cff and 9b6f4df.

📒 Files selected for processing (3)
  • .pre-commit-config.yaml
  • scripts/std_placeholder_stats/std_placeholder_stats.py
  • scripts/std_placeholder_stats/tests/test_std_placeholder_stats.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants