This is an automated email from the ASF dual-hosted git repository.
terrymanu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shardingsphere.git
The following commit(s) were added to refs/heads/master by this push:
new 4fd67614f53 Refine unit testing standards and align quality checks
(#40281)
4fd67614f53 is described below
commit 4fd67614f53956eef0a78eaa718bd00dcb3929f8
Author: Liang Zhang <[email protected]>
AuthorDate: Tue Oct 20 10:56:06 2026 +0800
Refine unit testing standards and align quality checks (#40281)
* Handle nullable projection text without throwing
* Unify repository unit testing standards and Checkstyle checks
* Refine unit testing standards and align quality checks
* Simplify unit testing standards and parameterization rules
* Align coding standards punctuation and policy bindings
* Enforce strict trivial-method test exclusions
---
.codex/harness/agents/cases.toml | 100 ++++++-
.codex/harness/agents/policy-sources.toml | 2 +-
.codex/harness/agents/run.py | 2 +-
.codex/harness/agents/test_run.py | 28 +-
.../references/rules/testing.md | 3 +-
.../scripts/tests/test_checkstyle_rules.py | 171 ++++++++++++
.codex/skills/gen-ut/SKILL.md | 36 ++-
.codex/skills/gen-ut/references/verification.md | 12 +-
.codex/skills/gen-ut/scripts/scan_quality_rules.py | 291 +++------------------
.../skills/gen-ut/tests/test_scan_quality_rules.py | 227 +++++++---------
CODE_OF_CONDUCT.md | 9 +-
.../MySQLIdentifierCasePolicyProviderTest.java | 26 +-
docs/community/content/involved/conduct/code.cn.md | 9 +-
docs/community/content/involved/conduct/code.en.md | 9 +-
src/resources/checkstyle.xml | 14 +-
15 files changed, 483 insertions(+), 456 deletions(-)
diff --git a/.codex/harness/agents/cases.toml b/.codex/harness/agents/cases.toml
index 6d5308bee04..789a535ffd2 100644
--- a/.codex/harness/agents/cases.toml
+++ b/.codex/harness/agents/cases.toml
@@ -1370,7 +1370,7 @@ allowed_actions = []
forbidden_actions = ["edit_code", "add_meaningless_test", "mutate_git",
"mutate_remote"]
required_reasons = ["meaningful_test_required"]
policy_binding_profile = "test-write"
-policy_assertions = [{source = "testing-rules", statements = ["Test a
pass-through, constant, accessor, delegation, wiring path, or contract literal
only when it expresses documented behavior owned by the production type or a
public or externally visible contract that no broader behavior test protects.",
"Never add a test solely to increase a coverage number or duplicate an existing
scenario unless it covers a new branch, input class, edge case, contract,
calculation path, or failure mode."]}]
+policy_assertions = [{source = "code-of-conduct", statements = ["Do not write
separate tests for methods that only return constants, directly return fields,
or simply delegate calls."]}, {source = "testing-rules", statements = ["Never
add a test solely to increase a coverage number or duplicate an existing
scenario unless it covers a new branch, input class, edge case, contract,
calculation path, or failure mode."]}]
critical = true
[[cases]]
@@ -3579,18 +3579,18 @@ phase = "read-only policy assessment"
ordinary_loop = false
profile = "gen-ut"
semantic_policy_assertions = true
-prompt = "Classify three newly added nested-type scanner candidates in
parameterized-test changes under R15-G. NON_TRANSPORT: complete source
inspection proves the nested type only groups test execution, carries no
scenario data or provider arguments, is necessary within the existing test
owner, and meets every other quality rule. TRANSPORT: complete inspection
proves the nested type bundles scenario input and expected values and is used
to carry rows into the parameterized test. UNKNOWN [...]
+prompt = "Classify three newly added nested-type scanner candidates in
parameterized-test changes under R15-G. NON_TRANSPORT: complete source
inspection proves the nested type only groups test execution, carries no
scenario data or provider arguments, is necessary within the existing test
owner, and meets every other quality rule. TRANSPORT: complete inspection
proves the nested type bundles scenario input and expected values and is used
to carry rows into the parameterized test. All row [...]
decision = "proceed"
required_actions = []
allowed_actions = []
forbidden_actions = ["edit_code", "edit_non_code", "mutate_git",
"mutate_remote", "run_local_checks", "run_pre_handoff_review",
"expand_frozen_boundary", "propose_commit_message"]
required_reasons = ["read_only_request"]
-required_summary_terms = ["[NON_TRANSPORT]=allow", "[TRANSPORT]=reject",
"[UNKNOWN]=review"]
-required_summary_prefix = "[NON_TRANSPORT]=allow; [TRANSPORT]=reject;
[UNKNOWN]=review"
-max_summary_chars = 59
+required_summary_terms = ["[NON_TRANSPORT]=allow", "[TRANSPORT]=allow",
"[UNKNOWN]=review"]
+required_summary_prefix = "[NON_TRANSPORT]=allow; [TRANSPORT]=allow;
[UNKNOWN]=review"
+max_summary_chars = 58
critical = true
policy_binding_profile = "gen-ut"
-policy_assertions = [{source = "gen-ut", statements = ["`R15-G`: parameterized
test changes add no nested transport type.", "Inspect each new nested-type
candidate's purpose; a confirmed non-transport type is permitted, a transport
type is prohibited, and unresolved purpose keeps the task at `R10-D`."]},
{source = "gen-ut-verification", statements = ["The scanner labels semantic or
partly semantic rules as `semanticReviewRequired=true`. Resolve them through
source inspection and record e [...]
+policy_assertions = [{source = "gen-ut", statements = ["`R15-G`: inspect each
new nested-type candidate used in argument sets against the grouping rule in
`CODE_OF_CONDUCT.md`.", "A nested type is not a violation by itself.",
"Unresolved grouping keeps the task at `R10-D`."]}, {source =
"gen-ut-verification", statements = ["The scanner labels semantic or partly
semantic rules as `semanticReviewRequired=true`. Resolve them through source
inspection and record evidence; zero mechanical vio [...]
[[cases]]
id = "issue_uses_applicable_component_first_party_evidence"
@@ -3684,7 +3684,7 @@ phase = "read-only policy assessment"
ordinary_loop = false
profile = "gen-ut"
semantic_policy_assertions = true
-prompt = "Decide whether final UT acceptance remains blocked in four
independent scenarios. MISSING_SCOPE_BASELINE: no Task scope baseline was
captured before editing. OUTSIDE_SCOPE_WRITE: the attributed task delta
contains a production-file write outside the authorized resolved test file set,
with no scope expansion approval. PENDING_REVIEW: R15-G has an unresolved
nested-type purpose review, although the mechanical scan reports no violation.
STALE_COVERAGE: executed coverage predates a [...]
+prompt = "Decide whether final UT acceptance remains blocked in four
independent scenarios. MISSING_SCOPE_BASELINE: no Task scope baseline was
captured before editing. OUTSIDE_SCOPE_WRITE: the attributed task delta
contains a production-file write outside the authorized resolved test file set,
with no scope expansion approval. PENDING_REVIEW: R15-G has an unresolved
grouping review for a nested type used in parameterized argument sets: the
invoked target methods and assertion forms remai [...]
decision = "proceed"
required_actions = []
allowed_actions = []
@@ -3695,7 +3695,7 @@ required_summary_prefix =
"[MISSING_SCOPE_BASELINE]=block; [OUTSIDE_SCOPE_WRITE]
max_summary_chars = 107
critical = true
policy_binding_profile = "gen-ut"
-policy_assertions = [{source = "gen-ut", statements = ["Edit only
`<ResolvedTestFileSet>` under `src/test/java` or `src/test/resources`.", "Do
not edit production, generated output, unrelated tests, or another file type.",
"Capture `Task scope baseline` after resolving the file set and before
editing.", "Any candidate-relevant changed path outside that set is a failure,
including further mutation of a path already dirty at task start.", "Exclude
only untracked or ignored reproducible ver [...]
+policy_assertions = [{source = "gen-ut", statements = ["Edit only
`<ResolvedTestFileSet>` under `src/test/java` or `src/test/resources`.", "Do
not edit production, generated output, unrelated tests, or another file type.",
"Capture `Task scope baseline` after resolving the file set and before
editing.", "Any candidate-relevant changed path outside that set is a failure,
including further mutation of a path already dirty at task start.", "Exclude
only untracked or ignored reproducible ver [...]
[[cases]]
id = "same_review_basis_has_same_assessment_across_candidate_types"
@@ -4442,3 +4442,87 @@ forbidden_summary_terms = ["A=start-code-fix",
"B=skip-confirmation-after-fix",
critical = true
policy_binding_profile = "review-pr"
policy_assertions = [{source = "review-pr-coordination", statements = ["This
procedure is the shared review-confirmation route for read-only reviews and the
Formal Review invoked by
[change-completion.md](../../../context/change-completion.md).", "That outer
loop alone owns code changes, their verification and coding-task completion.",
"A confirmed `Mergeable` or `Not Mergeable` completes a read-only review; the
caller determines any further authorized action."]}, {source = "review-pr",
[...]
+
+[[cases]]
+group = "unit-test policy"
+phase = "read-only policy assessment"
+ordinary_loop = false
+profile = "gen-ut"
+semantic_policy_assertions = true
+decision = "proceed"
+required_actions = []
+allowed_actions = []
+forbidden_actions = ["edit_code", "edit_non_code", "mutate_git",
"mutate_remote", "run_local_checks", "run_pre_handoff_review",
"expand_frozen_boundary", "propose_commit_message"]
+required_reasons = ["read_only_request"]
+critical = true
+policy_binding_profile = "gen-ut"
+id = "repository_boolean_assertions_include_variable_expectations"
+description = "Applies Boolean assertion rules beyond the unit-test generation
skill."
+prompt = "Classify repository-wide Boolean assertions using only the bound
rules. A uses assertTrue(expected == actual), where expected and actual are
primitive booleans. B uses assertThat(actual, is(expected)) with the same
booleans. C uses assertEquals(false, actual). Do not inspect, execute checks or
change anything. Return exactly three assignments [A]=<allow|reject>;
[B]=<allow|reject>; [C]=<allow|reject> in that order and no other text."
+required_summary_terms = ["[A]=allow", "[B]=reject", "[C]=reject"]
+required_summary_prefix = "[A]=allow; [B]=reject; [C]=reject"
+max_summary_chars = 33
+policy_assertions = [{source = "code-of-conduct", statements = ["Boolean
assertions must use `assertTrue` or `assertFalse`;"]}]
+
+[[cases]]
+group = "unit-test policy"
+phase = "read-only policy assessment"
+ordinary_loop = false
+profile = "gen-ut"
+semantic_policy_assertions = true
+decision = "proceed"
+required_actions = []
+allowed_actions = []
+forbidden_actions = ["edit_code", "edit_non_code", "mutate_git",
"mutate_remote", "run_local_checks", "run_pre_handoff_review",
"expand_frozen_boundary", "propose_commit_message"]
+required_reasons = ["read_only_request"]
+critical = true
+policy_binding_profile = "gen-ut"
+id = "repository_parameterized_source_and_signature_contract"
+description = "Keeps standard JUnit sources while requiring meaningful rows,
display names and the exact first parameter."
+prompt = "Classify repository tests using only the bound rules. A has
@ParameterizedTest(name = \"{0}\"), first parameter final String name, and
@CsvSource with three meaningful argument sets. B has the same declaration with
only two meaningful rows. C has three meaningful rows but first parameter
String name without final. D keeps two meaningful standalone JUnit 5 @Test
cases instead of inventing a third row. Do not inspect, execute checks or
change anything. Return exactly four assignm [...]
+required_summary_terms = ["[A]=allow", "[B]=reject", "[C]=reject", "[D]=allow"]
+required_summary_prefix = "[A]=allow; [B]=reject; [C]=reject; [D]=allow"
+max_summary_chars = 44
+policy_assertions = [{source = "code-of-conduct", statements = ["For
parameterized tests, provide display names via parameters and use `\"{0}\"` as
the display-name template;", "Declare the first parameter of each parameterized
test exactly as `final String name`;", "Provide at least three meaningful
argument sets for each parameterized test;"]}]
+
+[[cases]]
+group = "unit-test policy"
+phase = "read-only policy assessment"
+ordinary_loop = false
+profile = "gen-ut"
+semantic_policy_assertions = true
+decision = "proceed"
+required_actions = []
+allowed_actions = []
+forbidden_actions = ["edit_code", "edit_non_code", "mutate_git",
"mutate_remote", "run_local_checks", "run_pre_handoff_review",
"expand_frozen_boundary", "propose_commit_message"]
+required_reasons = ["read_only_request"]
+critical = true
+policy_binding_profile = "gen-ut"
+id = "repository_scenario_dispatch_and_repetition_purpose"
+description = "Checks parameterized grouping by target method and assertion
form while permitting callbacks and nested holders."
+prompt = "Classify independent repository test changes using only the bound
rules. A supplies a Consumer as an actual production API input; the test uses
the same direct execution and assertion flow for every row, and the Consumer
contains no test steps or assertions. B puts Runnable callbacks in argument
rows to invoke different target production methods across rows. C passes
switch-containing source text as actual production parser input. D adds a
nested record carrying only scenario i [...]
+required_summary_terms = ["[A]=allow", "[B]=reject", "[C]=allow", "[D]=allow",
"[E]=allow", "[F]=allow", "[G]=allow", "[H]=allow", "[I]=reject"]
+required_summary_prefix = "[A]=allow; [B]=reject; [C]=allow; [D]=allow;
[E]=allow; [F]=allow; [G]=allow; [H]=allow; [I]=reject"
+max_summary_chars = 99
+policy_assertions = [{source = "code-of-conduct", statements = ["Only combine
cases that invoke the same method under test and use the same form of
assertion."]}]
+
+[[cases]]
+group = "unit-test policy"
+phase = "read-only policy assessment"
+ordinary_loop = false
+profile = "gen-ut"
+semantic_policy_assertions = true
+decision = "proceed"
+required_actions = []
+allowed_actions = []
+forbidden_actions = ["edit_code", "edit_non_code", "mutate_git",
"mutate_remote", "run_local_checks", "run_pre_handoff_review",
"expand_frozen_boundary", "propose_commit_message"]
+required_reasons = ["read_only_request"]
+critical = true
+policy_binding_profile = "gen-ut"
+id = "repository_metadata_exclusions_precede_coverage"
+description = "Excludes separate tests for trivial methods while retaining
tests of business decisions, regardless of method name."
+prompt = "Classify new repository tests using only the bound rules. A adds a
test for a getType method whose business rule chooses among types; that rule
has no existing coverage and the user did not request the method by name. B
tests a constant getOrder value that defines an otherwise uncovered documented
external ordering contract. C only checks a generated getTypeClass field
accessor with no business rule or uncovered external contract. D only checks
that forward delegates to a colla [...]
+required_summary_terms = ["[A]=allow", "[B]=reject", "[C]=reject",
"[D]=reject", "[E]=reject"]
+required_summary_prefix = "[A]=allow; [B]=reject; [C]=reject; [D]=reject;
[E]=reject"
+max_summary_chars = 57
+policy_assertions = [{source = "code-of-conduct", statements = ["Do not write
separate tests for methods that only return constants, directly return fields,
or simply delegate calls."]}, {source = "testing-rules", statements = ["Apply
the test-value guidance in `CODE_OF_CONDUCT.md` before planning coverage."]}]
diff --git a/.codex/harness/agents/policy-sources.toml
b/.codex/harness/agents/policy-sources.toml
index 1be229652ad..0a176be76b7 100644
--- a/.codex/harness/agents/policy-sources.toml
+++ b/.codex/harness/agents/policy-sources.toml
@@ -339,7 +339,7 @@ id = "gen-ut-verification"
path = ".codex/skills/gen-ut/references/verification.md"
kind = "verification"
semantic = true
-references = [".codex/context/token-efficiency.md",
".codex/skills/code-implementation/references/verification.md"]
+references = [".codex/context/token-efficiency.md",
".codex/skills/code-implementation/references/verification.md",
"CODE_OF_CONDUCT.md"]
[[sources]]
id = "review-pr"
diff --git a/.codex/harness/agents/run.py b/.codex/harness/agents/run.py
index c55bc25fcef..d6fc8fb782b 100644
--- a/.codex/harness/agents/run.py
+++ b/.codex/harness/agents/run.py
@@ -697,7 +697,7 @@ def normalize_case_policy_bindings(
if len(statements) != len(set(statements)):
raise ValueError(f"Case policy assertion statements must be
unique: {case['id']}:{source_id}")
normalized_statements = [normalize_policy_statement(each) for each
in statements]
- if any(len(each) < 25 or each[-1] not in ".!?" for each in
normalized_statements):
+ if any(len(each) < 25 or each[-1] not in ".!?;" for each in
normalized_statements):
raise ValueError(f"Case policy assertions must bind complete
normative sentences: {case['id']}:{source_id}")
source_text =
normalize_policy_statement(source["data"].decode("utf-8"))
missing = [each for each in normalized_statements if each not in
source_text]
diff --git a/.codex/harness/agents/test_run.py
b/.codex/harness/agents/test_run.py
index 92910c2f4e6..e9e91e8c9e8 100644
--- a/.codex/harness/agents/test_run.py
+++ b/.codex/harness/agents/test_run.py
@@ -2210,19 +2210,21 @@ references = []
self.assertTrue(required_sources.issubset(bundle["profiles"][profile]))
def test_case_policy_binding_requires_full_statement_in_source(self) ->
None:
- statement = "Preserve every supported behavior before handing off the
completed task."
- with tempfile.TemporaryDirectory() as directory:
- root = Path(directory)
- manifest_path = self.create_bundle(root)
- (root / ".codex" / "rules.md").write_text(f"{statement}\n",
encoding="utf-8")
- bundle = run.load_policy_manifest(root, manifest_path)
- binding =
run.normalize_case_policy_bindings([self.create_bound_case(statement)],
bundle)[0]
- self.assertEqual(".codex/rules.md",
binding["assertions"][0]["source_path"])
- with self.assertRaisesRegex(ValueError, "absent from its source"):
- run.normalize_case_policy_bindings(
- [self.create_bound_case("Preserve every unsupported
behavior before handing off the completed task.")],
- bundle,
- )
+ for punctuation in (".", ";"):
+ with self.subTest(punctuation=punctuation),
tempfile.TemporaryDirectory() as directory:
+ statement = f"Preserve every supported behavior before handing
off the completed task{punctuation}"
+ root = Path(directory)
+ manifest_path = self.create_bundle(root)
+ (root / ".codex" / "rules.md").write_text(f"{statement}\n",
encoding="utf-8")
+ bundle = run.load_policy_manifest(root, manifest_path)
+ binding =
run.normalize_case_policy_bindings([self.create_bound_case(statement)],
bundle)[0]
+ self.assertEqual(".codex/rules.md",
binding["assertions"][0]["source_path"])
+ for invalid_statement in (
+ f"Preserve every unsupported behavior before handing
off the completed task{punctuation}",
+ statement[:-1] + (";" if punctuation == "." else "."),
+ ):
+ with self.assertRaisesRegex(ValueError, "absent from its
source"):
+
run.normalize_case_policy_bindings([self.create_bound_case(invalid_statement)],
bundle)
def test_case_policy_binding_rejects_source_outside_route(self) -> None:
statement = "Preserve every supported behavior before handing off the
completed task."
diff --git a/.codex/skills/code-implementation/references/rules/testing.md
b/.codex/skills/code-implementation/references/rules/testing.md
index 00b62b3e6a2..62607408dd4 100644
--- a/.codex/skills/code-implementation/references/rules/testing.md
+++ b/.codex/skills/code-implementation/references/rules/testing.md
@@ -17,13 +17,14 @@
# Testing Rules
+- Apply the test-value guidance in `CODE_OF_CONDUCT.md` before planning
coverage.
- For Maven source trees, only code under `src/main/**` may be the subject of
a new or updated test.
- Never create or update a test to cover code under `src/test/**`.
- A change confined to `src/test/**` still requires applicable compilation and
test execution, but it does not require additional test coverage.
- Test behavior owned by the production class: computation, decisions,
validation, transformation, state transitions, error handling, or external
contracts.
- Each test must fail for a realistic regression that matters.
- Do not add tests that only prove behavior owned by Java, Lombok, Mockito, a
third-party parser, a collection library, a framework, or another collaborator,
or that lock in private implementation shape.
-- Test a pass-through, constant, accessor, delegation, wiring path, or
contract literal only when it expresses documented behavior owned by the
production type or a public or externally visible contract that no broader
behavior test protects.
+- Test a wiring path or contract literal only when it expresses documented
behavior owned by the production type or a public or externally visible
contract that no broader behavior test protects.
- Never add a test solely to increase a coverage number or duplicate an
existing scenario unless it covers a new branch, input class, edge case,
contract, calculation path, or failure mode.
- Never add tests whose subject is another test case, a test class or method,
test fixture, mock helper, test utility, or other test-only code. Test only the
production behavior that the test-only code supports.
- Do not test a collaborator-owned rule through the current class. Isolate the
collaborator result at the nearest stable boundary and test the rule in its
owner. Mock external or heavy dependencies and collaborator-owned decisions;
use real simple values when they are stable and do not introduce cross-layer
behavior. Cross-layer behavior belongs in an explicitly scoped integration,
contract, or E2E test.
diff --git
a/.codex/skills/coding-standards/scripts/tests/test_checkstyle_rules.py
b/.codex/skills/coding-standards/scripts/tests/test_checkstyle_rules.py
new file mode 100644
index 00000000000..4f0907e1751
--- /dev/null
+++ b/.codex/skills/coding-standards/scripts/tests/test_checkstyle_rules.py
@@ -0,0 +1,171 @@
+#!/usr/bin/env python3
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""Exercise repository Checkstyle rules against synthetic Java sources."""
+
+from __future__ import annotations
+
+import subprocess
+import tempfile
+import unittest
+import xml.etree.ElementTree as ET
+from pathlib import Path
+
+
+REPOSITORY_ROOT = Path(__file__).resolve().parents[5]
+BOOLEAN_MESSAGE = "Use assertTrue/assertFalse instead of assertThat(...,
is(true/false))."
+DISPLAY_NAME_MESSAGE = 'Parameterized tests must use name = "{0}".'
+NAME_PARAMETER_MESSAGE = "Declare final String name as the first parameter of
parameterized tests."
+RULE_MESSAGES = {BOOLEAN_MESSAGE, DISPLAY_NAME_MESSAGE, NAME_PARAMETER_MESSAGE}
+
+
+class CheckstyleRulesTest(unittest.TestCase):
+
+ @classmethod
+ def setUpClass(cls) -> None:
+ target_directory = REPOSITORY_ROOT / "target"
+ target_directory.mkdir(exist_ok=True)
+ temporary_directory =
tempfile.TemporaryDirectory(prefix="checkstyle-rules-", dir=target_directory)
+ cls.addClassCleanup(temporary_directory.cleanup)
+ directory = Path(temporary_directory.name)
+ cls.boolean_cases = {
+ "literal_true": ("assertThat(actual, is(true));", True),
+ "literal_false": ("assertThat(actual, is(false));", True),
+ "boxed_true": ("assertThat(actual, is(Boolean.TRUE));", True),
+ "boxed_false": ("assertThat(actual, is(Boolean.FALSE));", True),
+ "qualified_true": ("assertThat(actual,
is(java.lang.Boolean.TRUE));", True),
+ "qualified_false": ("assertThat(actual,
is(java.lang.Boolean.FALSE));", True),
+ "qualified_matchers":
("org.hamcrest.MatcherAssert.assertThat(actual,
org.hamcrest.Matchers.is(Boolean.TRUE));", True),
+ "message": ('assertThat("foo", actual, is(Boolean.FALSE));', True),
+ "variable": ("assertThat(actual, is(expected));", False),
+ "string_literal": ('assertThat(actual, is("true"));', False),
+ "other_constant": ("assertThat(actual, is(Foo.TRUE));", False),
+ "other_boolean_type": ("assertThat(actual,
is(foo.Boolean.TRUE));", False),
+ "comment": ("// assertThat(actual, is(Boolean.TRUE));", False),
+ "string_content": ('String example = "assertThat(actual,
is(Boolean.TRUE));";', False),
+ }
+ cls.display_name_cases = {
+ "default": ("@ParameterizedTest", True),
+ "wrong_template": ('@ParameterizedTest(name = "{index}")', True),
+ "prefix": ('@ParameterizedTest(name = "foo {0}")', True),
+ "constant": ("@ParameterizedTest(name = DISPLAY_NAME)", True),
+ "concatenation": ('@ParameterizedTest(name = "{" + "0}")', True),
+ "correct": ('@ParameterizedTest(name = "{0}")', False),
+ "multiline": ('@ParameterizedTest(\n name = "{0}"\n )',
False),
+ "other_annotation": ('@ParameterizedTest(name = "{0}")\n
@MethodSource("scenarios")', False),
+ "qualified": ('@org.junit.jupiter.params.ParameterizedTest(name =
"{0}")', False),
+ "ordinary_test": ("@Test", False),
+ }
+ cls.name_parameter_cases = {
+ "none": ("", True),
+ "missing_final": ("String name", True),
+ "wrong_name": ("final String description", True),
+ "wrong_type": ("final Object name", True),
+ "wrong_position": ("final int value, final String name", True),
+ "array": ("final String[] name", True),
+ "post_name_array": ("final String name[]", True),
+ "varargs": ("final String... name", True),
+ "correct": ("final String name", False),
+ "later_parameters": ("final String name, final int value", False),
+ "annotation": ("@Deprecated final String name", False),
+ "qualified_type": ("final java.lang.String name", False),
+ }
+ cls.fixture_paths = {}
+ for name, (statement, _) in cls.boolean_cases.items():
+ cls.write_fixture(directory, f"boolean_{name}", f"void
assertScenario() {{\n {statement}\n }}")
+ for name, (annotation, _) in cls.display_name_cases.items():
+ cls.write_fixture(directory, f"display_{name}", f"{annotation}\n
void assertScenario(final String name) {{\n }}")
+ for name, (parameters, _) in cls.name_parameter_cases.items():
+ cls.write_fixture(directory, f"parameter_{name}",
'@ParameterizedTest(name = "{0}")\n ' + f"void assertScenario({parameters})
{{\n }}")
+ cls.write_fixture(directory, "ordinary_helper", "void
assertScenario(final int value) {\n }")
+ properties = ET.parse(REPOSITORY_ROOT /
"pom.xml").getroot().find("{*}properties")
+ cls.engine_version = properties.findtext("{*}checkstyle.version")
+ cls.write_pom(directory,
properties.findtext("{*}maven-checkstyle-plugin.version"))
+ completed = subprocess.run(
+ [str(REPOSITORY_ROOT / "mvnw"), "-B", "-o", "-f", str(directory /
"pom.xml"), "checkstyle:check"],
+ cwd=REPOSITORY_ROOT, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, text=True, timeout=60,
+ )
+ if completed.returncode:
+ raise AssertionError(f"Checkstyle fixture execution failed
({completed.returncode}):\n{completed.stdout[-6000:]}")
+ report = ET.parse(directory / "checkstyle-result.xml").getroot()
+ if cls.engine_version != report.get("version"):
+ raise AssertionError(f"Expected Checkstyle {cls.engine_version},
got {report.get('version')}")
+ cls.actual_diagnostics = {
+ Path(file.get("name")).resolve(): [
+ (error.get("message"), error.get("severity")) for error in
file.findall("error")
+ if error.get("message") in RULE_MESSAGES
+ ]
+ for file in report.findall("file")
+ }
+ if set(cls.fixture_paths.values()) != set(cls.actual_diagnostics):
+ raise AssertionError("Checkstyle did not report exactly the
selected fixture files")
+
+ @classmethod
+ def write_fixture(cls, directory: Path, name: str, method: str) -> None:
+ path = directory / "sources" / name / "FooTest.java"
+ path.parent.mkdir(parents=True)
+ path.write_text(f"class FooTest {{\n {method}\n}}\n",
encoding="utf-8")
+ cls.fixture_paths[name] = path.resolve()
+
+ @classmethod
+ def write_pom(cls, directory: Path, plugin_version: str) -> None:
+ project = ET.Element("project", {"xmlns":
"http://maven.apache.org/POM/4.0.0"})
+ for name, value in (("modelVersion", "4.0.0"), ("groupId",
"org.apache.shardingsphere"),
+ ("artifactId", "checkstyle-rule-fixtures"),
("version", "1"), ("packaging", "pom")):
+ ET.SubElement(project, name).text = value
+ properties = ET.SubElement(project, "properties")
+ ET.SubElement(properties, "project.build.sourceEncoding").text =
"UTF-8"
+ build = ET.SubElement(project, "build")
+ ET.SubElement(build, "sourceDirectory").text = str(directory /
"sources")
+ plugin = ET.SubElement(ET.SubElement(build, "plugins"), "plugin")
+ for name, value in (("groupId", "org.apache.maven.plugins"),
("artifactId", "maven-checkstyle-plugin"), ("version", plugin_version)):
+ ET.SubElement(plugin, name).text = value
+ dependency = ET.SubElement(ET.SubElement(plugin, "dependencies"),
"dependency")
+ for name, value in (("groupId", "com.puppycrawl.tools"),
("artifactId", "checkstyle"), ("version", cls.engine_version)):
+ ET.SubElement(dependency, name).text = value
+ configuration = ET.SubElement(plugin, "configuration")
+ for name, value in (
+ ("configLocation", str(REPOSITORY_ROOT / "src" / "resources" /
"checkstyle.xml")),
+ ("outputFile", str(directory / "checkstyle-result.xml")),
("cacheFile", str(directory / "checkstyle-cache")),
+ ("includeResources", "false"), ("includeTestResources", "false"),
("failOnViolation", "false"), ("failsOnError", "false"),
+ ):
+ ET.SubElement(configuration, name).text = value
+ ET.ElementTree(project).write(directory / "pom.xml", encoding="utf-8",
xml_declaration=True)
+
+ def test_boolean_assertions_use_boolean_helpers(self) -> None:
+ for name, (_, violates) in self.boolean_cases.items():
+ with self.subTest(name=name):
+ expected = [(BOOLEAN_MESSAGE, "error")] if violates else []
+ self.assertEqual(expected,
self.actual_diagnostics[self.fixture_paths[f"boolean_{name}"]])
+
+ def test_parameterized_tests_use_argument_display_names(self) -> None:
+ for name, (_, violates) in self.display_name_cases.items():
+ with self.subTest(name=name):
+ expected = [(DISPLAY_NAME_MESSAGE, "warning")] if violates
else []
+ self.assertEqual(expected,
self.actual_diagnostics[self.fixture_paths[f"display_{name}"]])
+
+ def test_parameterized_tests_start_with_a_named_string(self) -> None:
+ for name, (_, violates) in self.name_parameter_cases.items():
+ with self.subTest(name=name):
+ expected = [(NAME_PARAMETER_MESSAGE, "warning")] if violates
else []
+ self.assertEqual(expected,
self.actual_diagnostics[self.fixture_paths[f"parameter_{name}"]])
+ self.assertEqual([],
self.actual_diagnostics[self.fixture_paths["ordinary_helper"]])
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/.codex/skills/gen-ut/SKILL.md b/.codex/skills/gen-ut/SKILL.md
index ce2afee0135..677ce89a839 100644
--- a/.codex/skills/gen-ut/SKILL.md
+++ b/.codex/skills/gen-ut/SKILL.md
@@ -44,10 +44,7 @@ Reading either Skill does not expand the user-authorized
scope, file types, Git
### R2: Test form and naming
-- Use JUnit 5 `@Test` for standalone scenarios.
-- Use `@ParameterizedTest(name = "{0}")`, `@MethodSource`, and `Arguments` for
data-driven scenarios. Declare `final String name` first and provide at least
three rows.
-- Do not use `@RepeatedTest`, `Consumer` as scenario transport, `switch`
dispatch inside parameterized tests, or new nested transport types.
-- Follow repository test naming. A single test uniquely covering one public
method is `assert<MethodName>`.
+Apply the Unit Testing Standards in
[CODE_OF_CONDUCT.md](../../../CODE_OF_CONDUCT.md#unit-testing-standards) to
test forms, naming, parameter sources, scenario dispatch, and nested types.
### R3: Change boundary
@@ -74,7 +71,6 @@ Reading either Skill does not expand the user-authorized
scope, file types, Git
- Exercise interface default methods with Mockito `CALLS_REAL_METHODS`.
- Obtain SPI targets through the applicable project loader and keep the
resolved instance as a test-class field by default. Record a concrete reason
before bypassing the loader.
-- Do not add tests for `getType`, `getOrder`, or `getTypeClass` unless
explicitly requested.
- Mock heavy dependencies and collaborator-owned decisions. Allow real simple
values and explicitly scoped integration, contract, or E2E behavior.
### R7: Related tests
@@ -86,11 +82,10 @@ Create a new exact-name test class only when none exists.
For each target public method, record `R8-CANDIDATES` with the method,
candidate count, decision, and evidence. A candidate is high-fit only when all
are true:
-1. target method and branch skeleton are consistent;
+1. target method is the same;
2. differences are mainly input data;
-3. assertion skeleton is consistent or differences are explicitly declared;
-4. at least three scenarios exist;
-5. no `switch` dispatch is required.
+3. the same form of assertion is used;
+4. the group meets the minimum scenario count in `CODE_OF_CONDUCT.md`.
Collaborator-owned differences are not high-fit. Refactor high-fit candidates;
otherwise record the concrete reason.
Use `KEEP` only when a genuinely high-fit refactor would materially reduce
readability or diagnosis.
@@ -138,22 +133,23 @@ Mark `R4=N/A (triggered by R12)` and retain the coverage
command and report path
### R14: Boolean assertions
-Use `assertTrue`/`assertFalse` for literal or constant expectations and
`assertThat(actual, is(expected))` for variable expectations.
-Do not use boolean `assertEquals`, boolean literals inside Hamcrest `is`, or
control flow whose only purpose is selecting `assertTrue` versus `assertFalse`.
+Apply the Boolean assertion standard in `CODE_OF_CONDUCT.md` to literal,
constant, and variable expectations.
### R15: Delivery gates
- `R15-A`: complete the semantic `R8` decision; the scanner cannot infer
high-fit.
-- `R15-B`: confirm any metadata-accessor candidate was explicitly requested;
the scanner only reports likely candidates.
+- `R15-B`: review every test against the test-value rule in
`CODE_OF_CONDUCT.md`; method names do not decide whether a test is permitted.
- `R15-C`: compare the final worktree with `Task scope baseline`; any
candidate-relevant out-of-scope mutation fails. Ignore only reproducible,
untracked or Git-ignored verification outputs.
-- `R15-D`: every parameterized test uses `@MethodSource` with at least three
`Arguments` rows. Inspect external providers manually.
-- `R15-E`: the first parameter is exactly `final String name`.
-- `R15-F`: parameterized bodies contain no `switch`.
-- `R15-G`: parameterized test changes add no nested transport type.
- Inspect each new nested-type candidate's purpose; a confirmed non-transport
type is permitted, a transport type is prohibited, and unresolved purpose keeps
the task at `R10-D`.
-- `R15-H`: boolean assertions obey `R14` without assertion-dispatch control
flow.
-- `R15-I`: parameterized signatures and provider rows contain no `Consumer`;
inspect external providers manually.
+- `R15-D`: verify each parameterized test's argument source and actual case
count against `CODE_OF_CONDUCT.md`; inspect external, unresolved, or dynamic
sources manually.
+- `R15-E`: verify the first parameter against the parameterized-test signature
rule in `CODE_OF_CONDUCT.md`.
+- `R15-F`: inspect each `switch` candidate in a parameterized test against the
grouping rule in `CODE_OF_CONDUCT.md`.
+- `R15-G`: inspect each new nested-type candidate used in argument sets
against the grouping rule in `CODE_OF_CONDUCT.md`.
+ A nested type is not a violation by itself.
+ Unresolved grouping keeps the task at `R10-D`.
+- `R15-H`: review Boolean assertions under `R14`; for assertion-dispatch
candidates in parameterized tests, compare the assertion forms against the
grouping rule in `CODE_OF_CONDUCT.md`.
+- `R15-I`: inspect callback candidates used in argument sets against the
grouping rule in `CODE_OF_CONDUCT.md`.
+ Inspect external, unresolved, or dynamic argument sources manually.
- `R15-J`: helpers and providers do not invoke target public methods. Treat
scanner results as partial evidence and inspect unresolved ownership manually.
## Workflow
@@ -165,7 +161,7 @@ Do not use boolean `assertEquals`, boolean literals inside
Hamcrest `is`, or con
5. Decide `R12`; otherwise record the SUT/collaborator ownership boundary and
`R4` branch map.
6. Review scanner candidates against every `R8` condition and record
candidate-level decisions.
7. Check `R9`, then implement the smallest tests allowed by `R2-R7`.
-8. After each coherent edit, use the lightweight precheck for repair feedback
and run invalidated focused tests.
+8. After each coherent edit, run invalidated focused tests and relevant
repository Checkstyle checks.
9. Apply `R13` and inspect semantic `R15-A/B/D/G/I/J` obligations.
10. Complete missing or invalidated final coverage, repository
formatting/checkstyle, and full mechanical checks under the [verification
commands](references/verification.md).
11. Confirm final scope against the baseline and decide `R10` once from
current evidence before delivery.
diff --git a/.codex/skills/gen-ut/references/verification.md
b/.codex/skills/gen-ut/references/verification.md
index 5d300e9b4ea..cc49675cd97 100644
--- a/.codex/skills/gen-ut/references/verification.md
+++ b/.codex/skills/gen-ut/references/verification.md
@@ -65,16 +65,17 @@ python3 .codex/skills/gen-ut/scripts/scan_quality_rules.py
--json \
<ResolvedTestFileSet>
```
+Retain the applicable repository Checkstyle report alongside the scanner
baseline; Checkstyle owns Boolean-literal assertions and parameterized-test
display-name and first-parameter syntax.
Apply `R11` to baseline diagnostics rather than treating every nonzero scanner
exit as a new blocking failure.
+New or task-relevant Checkstyle warnings block completion; only
baseline-proven unchanged, unrelated diagnostics may remain non-blocking under
`R11`.
Read coverage counters and missed branch locations from the native JaCoCo XML
and HTML reports.
## Edit-loop checks
-Run the structural precheck before an expensive build when signatures or
parameterized-test structure changed:
+When signatures or parameterized-test structure change, run the owning
module's repository Checkstyle check through the command wrapper:
```bash
-python3 .codex/skills/gen-ut/scripts/scan_quality_rules.py \
- --precheck-only <ResolvedTestFileSet>
+./mvnw -pl <ResolvedTestModule> checkstyle:check -Pcheck
```
Run focused tests after each coherent edit:
@@ -94,6 +95,11 @@ python3 .codex/skills/gen-ut/scripts/scan_quality_rules.py
--json \
The scanner labels semantic or partly semantic rules as
`semanticReviewRequired=true`. Resolve them through source inspection and
record evidence; zero mechanical violations alone is not `R10-A`.
In JSON output, review the `manual` and `hybrid` rules and their `reviews`
entries.
+Inspect actual argument sets for every standard JUnit source, including
external or dynamic providers; source-code call counts do not prove executed
row counts.
+Review every test against the test-value rule in `CODE_OF_CONDUCT.md`,
independently of method names.
+Review `switch`, `Consumer`, nested-type, and assertion-control-flow
candidates in parameterized tests against the rule in `CODE_OF_CONDUCT.md` for
combining cases that invoke the same method under test and use the same form of
assertion.
+These partial signals do not establish a violation.
+Review Boolean assertions with variable expectations from source because
syntax-only checks cannot infer every expression's type.
## Final coverage
diff --git a/.codex/skills/gen-ut/scripts/scan_quality_rules.py
b/.codex/skills/gen-ut/scripts/scan_quality_rules.py
index 6778929658b..2649bb8d912 100644
--- a/.codex/skills/gen-ut/scripts/scan_quality_rules.py
+++ b/.codex/skills/gen-ut/scripts/scan_quality_rules.py
@@ -60,36 +60,18 @@ class FileScanContext:
added_lines: list[str]
-PRECHECK_ORDER = (
- "checkstyle-final-parameters",
- "parameterized-methodsource",
- "parameterized-name-parameter",
-)
-PRECHECK_MESSAGES = {
- "checkstyle-final-parameters": "test method parameters should be final to
avoid Checkstyle FinalParameters failures",
- "parameterized-methodsource": "parameterized tests should use
@MethodSource providers with at least 3 Arguments rows",
- "parameterized-name-parameter": "parameterized tests should declare the
first parameter exactly as `final String name`",
-}
RULE_SPECS = (
- RuleSpec("R8", "@ParameterizedTest must use name = \"{0}\"", "automated"),
- RuleSpec("R14", "forbidden boolean assertion found", "automated"),
+ RuleSpec("R14", "boolean assertions require repository-standard source
review", "manual"),
RuleSpec("R15-A", "parameterization suitability requires semantic
evidence", "manual"),
- RuleSpec("R15-B", "metadata accessor candidates require request-scope
review", "manual"),
+ RuleSpec("R15-B", "test value and owned or external contracts require
source review", "manual"),
RuleSpec("R15-C", "out-of-scope worktree mutation detected", "automated"),
- RuleSpec("R15-D", "each @ParameterizedTest must have >= 3 Arguments rows
from @MethodSource", "hybrid"),
- RuleSpec("R15-E", "each @ParameterizedTest method must declare first
parameter as `final String name`", "automated"),
- RuleSpec("R15-F", "@ParameterizedTest method body must not contain
switch", "automated"),
- RuleSpec("R15-G", "added type declarations require nested
scenario-transport review", "manual"),
- RuleSpec("R15-H", "do not dispatch boolean assertions by control flow to
choose assertTrue/assertFalse", "automated"),
- RuleSpec("R15-I", "parameterized tests must not use Consumer in signatures
or @MethodSource argument rows", "hybrid"),
+ RuleSpec("R15-D", "each parameterized test requires at least three actual
argument sets", "manual"),
+ RuleSpec("R15-F", "switch candidates require method and assertion grouping
review when parameterized", "manual"),
+ RuleSpec("R15-G", "added nested types require method and assertion
grouping review when used in argument sets", "manual"),
+ RuleSpec("R15-H", "boolean assertions and parameterized grouping require
source review", "manual"),
+ RuleSpec("R15-I", "Consumer candidates require method and assertion
grouping review when used in argument sets", "manual"),
RuleSpec("R15-J", "helper and provider target invocations require semantic
ownership review", "hybrid"),
)
-BOOLEAN_ASSERTION_BAN_PATTERN = re.compile(
-
r"assertThat\s*\((?s:[^;]*?)is\s*\(\s*(?:true|false|Boolean\.TRUE|Boolean\.FALSE)\s*\)\s*\)"
- r"|assertEquals\s*\(\s*(?:true|false|Boolean\.TRUE|Boolean\.FALSE)\s*,"
-
r"|assertEquals\s*\((?s:[^;]*?),\s*(?:true|false|Boolean\.TRUE|Boolean\.FALSE)\s*\)",
- re.S,
-)
CONSUMER_TOKEN_PATTERN = re.compile(r"\bConsumer\s*(?:<|\b)")
CONSTRUCTOR_CALL_PATTERN = re.compile(r"\bnew\s+(\w+)\s*\(")
METHOD_DECL_PATTERN =
re.compile(r"(?:private|protected|public)?\s*(?:static\s+)?[\w$<>\[\],
?]+\s+(\w+)\s*\([^)]*\)\s*(?:throws [^{]+)?\{", re.S)
@@ -103,15 +85,10 @@ PARAM_METHOD_PATTERN = re.compile(
re.S,
)
TEST_METHOD_DECL_PATTERN = re.compile(
-
r"((?:@Test(?:\s*\([^)]*\))?|@ParameterizedTest(?:\s*\([^)]*\))?)\s*(?:@\w+(?:\s*\([^)]*\))?\s*)*)"
+
r"((?:@Test(?:\s*\([^)]*\))?|@ParameterizedTest(?:\s*\([^)]*\))?|@RepeatedTest(?:\s*\([^)]*\))?)\s*(?:@\w+(?:\s*\([^)]*\))?\s*)*)"
r"void\s+(assert\w+)\s*\([^)]*\)\s*(?:throws [^{]+)?\{",
re.S,
)
-TEST_METHOD_SIGNATURE_PATTERN = re.compile(
-
r"((?:@Test(?:\s*\([^)]*\))?|@ParameterizedTest(?:\s*\([^)]*\))?)\s*(?:@\w+(?:\s*\([^)]*\))?\s*)*)"
- r"void\s+(assert\w+)\s*\(([^)]*)\)\s*(?:throws [^{]+)?",
- re.S,
-)
R15_A_CALL_PATTERN = re.compile(r"\b\w+\.(\w+)\s*\(")
R15_A_IGNORE = {"assertThat", "assertTrue", "assertFalse", "mock", "when",
"verify", "is", "not"}
R15_G_TYPE_DECL_PATTERN = re.compile(
@@ -127,7 +104,6 @@ R15_H_IF_RETURN_PATTERN = re.compile(
r"|if\s*\([^)]*\)\s*\{[\s\S]*?assertFalse\s*\([^;]+\)\s*;[\s\S]*?return\s*;[\s\S]*?\}\s*assertTrue\s*\([^;]+\)\s*;",
re.S,
)
-R15_NAME_PATTERN = re.compile(r'name\s*=\s*"\{0\}"')
R15_SWITCH_PATTERN = re.compile(r"\bswitch\s*\(")
TYPE_DECL_LINE_PATTERN = re.compile(
r"^\s*(?:(?:public|protected|private|static|final|abstract|sealed|non-sealed)\s+)*(class|interface|enum|record)\s+(\w+)\b"
@@ -237,38 +213,6 @@ def extract_block(text: str, brace_index: int) -> str:
return ""
-def split_parameters(params: str) -> list[str]:
- result = []
- current = []
- angle_depth = 0
- paren_depth = 0
- bracket_depth = 0
- for char in params:
- if "," == char and 0 == angle_depth and 0 == paren_depth and 0 ==
bracket_depth:
- part = "".join(current).strip()
- if part:
- result.append(part)
- current = []
- continue
- current.append(char)
- if "<" == char:
- angle_depth += 1
- elif ">" == char and angle_depth > 0:
- angle_depth -= 1
- elif "(" == char:
- paren_depth += 1
- elif ")" == char and paren_depth > 0:
- paren_depth -= 1
- elif "[" == char:
- bracket_depth += 1
- elif "]" == char and bracket_depth > 0:
- bracket_depth -= 1
- tail = "".join(current).strip()
- if tail:
- result.append(tail)
- return result
-
-
def parse_method_sources(method_name: str, annotation_block: str) -> list[str]:
result = []
matches = list(METHOD_SOURCE_PATTERN.finditer(annotation_block))
@@ -588,88 +532,7 @@ def describe_candidate(candidate: dict) -> str:
return f'{candidate["path"]}: method={candidate["method"]}
plainTestCount={candidate["plain_test_count"]}
parameterizedPresent={candidate["parameterized_present"]} decision={decision}'
-def check_parameterized_name(path: Path, source: str) -> list[str]:
- source = mask_java_non_code(source, False)
- violations = []
- token = "@ParameterizedTest"
- pos = 0
- while True:
- token_pos = source.find(token, pos)
- if token_pos < 0:
- break
- line = line_number(source, token_pos)
- cursor = token_pos + len(token)
- while cursor < len(source) and source[cursor].isspace():
- cursor += 1
- if cursor >= len(source) or "(" != source[cursor]:
- violations.append(f"{path}:{line}")
- pos = token_pos + len(token)
- continue
- depth = 1
- end = cursor + 1
- while end < len(source) and depth:
- if "(" == source[end]:
- depth += 1
- elif ")" == source[end]:
- depth -= 1
- end += 1
- if depth or not R15_NAME_PATTERN.search(source[cursor + 1:end - 1]):
- violations.append(f"{path}:{line}")
- pos = end
- return violations
-
-
-def check_r15_d(path: Path, source: str, method_bodies: dict[str, str]) ->
list[str]:
- violations = []
- for match in PARAM_METHOD_PATTERN.finditer(source):
- annotation_block = match.group(1)
- method_name = match.group(2)
- line = line_number(source, match.start())
- providers = parse_method_sources(method_name, annotation_block)
- if not providers:
- violations.append(f"{path}:{line} method={method_name} missing
@MethodSource")
- continue
- total_rows = 0
- for provider in providers:
- body = method_bodies.get(provider)
- if body is None:
- continue
- total_rows +=
len(re.findall(r"\b(?:Arguments\.of|arguments)\s*\(", body))
- if any(provider not in method_bodies for provider in providers):
- continue
- if total_rows < 3:
- violations.append(f"{path}:{line} method={method_name}
argumentsRows={total_rows}")
- return violations
-
-
-def find_unresolved_method_sources(path: Path, source: str, method_bodies:
dict[str, str]) -> list[str]:
- result = []
- for match in PARAM_METHOD_PATTERN.finditer(source):
- method_name = match.group(2)
- providers = parse_method_sources(method_name, match.group(1))
- unresolved = [each for each in providers if each not in method_bodies]
- if unresolved:
- result.append(f"{path}:{line_number(source, match.start())}
method={method_name} unresolvedProviders={','.join(unresolved)}")
- return result
-
-
-def check_r15_e(path: Path, source: str) -> list[str]:
- violations = []
- for match in PARAM_METHOD_PATTERN.finditer(source):
- method_name = match.group(2)
- params = match.group(3).strip()
- line = line_number(source, match.start())
- if not params:
- violations.append(f"{path}:{line} method={method_name}
missingParameters")
- continue
- first_param = params.split(",", 1)[0].strip()
- normalized = re.sub(r"\s+", " ", first_param)
- if "final String name" != normalized:
- violations.append(f"{path}:{line} method={method_name}
firstParam={first_param}")
- return violations
-
-
-def check_r15_f(path: Path, source: str) -> list[str]:
+def find_switch_dispatch_candidates(path: Path, source: str) -> list[str]:
violations = []
for match in PARAM_METHOD_BODY_PATTERN.finditer(source):
method_name = match.group(1)
@@ -701,7 +564,7 @@ def check_r15_g(path: Path, source: str, added_lines:
list[str]) -> list[str]:
return violations
-def check_r15_i(path: Path, source: str, method_bodies: dict[str, str]) ->
list[str]:
+def find_consumer_transport_candidates(path: Path, source: str, method_bodies:
dict[str, str]) -> list[str]:
violations = []
for match in PARAM_METHOD_PATTERN.finditer(source):
annotation_block = match.group(1)
@@ -740,12 +603,7 @@ def check_r15_j(context: FileScanContext) -> list[str]:
return violations
-def check_r14(path: Path, source: str) -> list[str]:
- source = mask_java_non_code(source, True)
- return [f"{path}:{line_number(source, match.start())}" for match in
BOOLEAN_ASSERTION_BAN_PATTERN.finditer(source)]
-
-
-def check_r15_h(path: Path, source: str) -> list[str]:
+def find_boolean_dispatch_candidates(path: Path, source: str) -> list[str]:
source = mask_java_non_code(source, True)
violations = []
for match in TEST_METHOD_DECL_PATTERN.finditer(source):
@@ -759,39 +617,6 @@ def check_r15_h(path: Path, source: str) -> list[str]:
return violations
-def find_metadata_accessor_test_candidates(path: Path, source: str) ->
list[str]:
- source = mask_java_non_code(source, False)
- result = []
- for match in TEST_METHOD_DECL_PATTERN.finditer(source):
- method_name = match.group(2)
- if any(method_name.startswith(f"assert{each}") for each in ("GetType",
"GetOrder", "GetTypeClass")):
- result.append(f"{path}:{line_number(source, match.start())}
method={method_name}")
- return result
-
-
-def checkstyle_preview_final_parameters(path: Path, source: str) -> list[str]:
- violations = []
- for match in TEST_METHOD_SIGNATURE_PATTERN.finditer(source):
- method_name = match.group(2)
- params = match.group(3).strip()
- if not params:
- continue
- line = line_number(source, match.start())
- for each in split_parameters(params):
- normalized = re.sub(r"\s+", " ", each.strip())
- if not normalized.startswith("final "):
- violations.append(f"{path}:{line} method={method_name}
param={each.strip()}")
- return violations
-
-
-def collect_precheck_violations(path: Path, source: str, method_bodies:
dict[str, str]) -> dict[str, list[str]]:
- return {
- "checkstyle-final-parameters":
checkstyle_preview_final_parameters(path, source),
- "parameterized-methodsource": check_r15_d(path, source, method_bodies),
- "parameterized-name-parameter": check_r15_e(path, source),
- }
-
-
def create_file_scan_context(path: Path, scope_baseline: dict | None = None)
-> FileScanContext:
source = path.read_text(encoding="utf-8")
method_blocks = parse_method_blocks(source)
@@ -808,19 +633,6 @@ def create_file_scan_context(path: Path, scope_baseline:
dict | None = None) ->
)
-def file_rule_violations(context: FileScanContext, precheck_violations:
dict[str, list[str]]) -> dict[str, list[str]]:
- violations = defaultdict(list)
- violations["R8"].extend(check_parameterized_name(context.path,
context.source))
- violations["R14"].extend(check_r14(context.path, context.source))
-
violations["R15-D"].extend(precheck_violations["parameterized-methodsource"])
-
violations["R15-E"].extend(precheck_violations["parameterized-name-parameter"])
- violations["R15-F"].extend(check_r15_f(context.path, context.source))
- violations["R15-H"].extend(check_r15_h(context.path, context.source))
- violations["R15-I"].extend(check_r15_i(context.path, context.source,
context.method_bodies))
- violations["R15-J"].extend(check_r15_j(context))
- return violations
-
-
def build_rule_result(violations_by_rule: dict[str, list[str]]) -> dict[str,
dict[str, object]]:
return {
each.name: {
@@ -832,54 +644,38 @@ def build_rule_result(violations_by_rule: dict[str,
list[str]]) -> dict[str, dic
}
-def collect_scan_result(paths: list[Path], scope_baseline_path: Path | None,
precheck_only: bool = False) -> dict:
+def find_argument_source_reviews(path: Path, source: str) -> list[str]:
+ result = []
+ for match in PARAM_METHOD_PATTERN.finditer(source):
+ method_name = match.group(2)
+ providers = parse_method_sources(method_name, match.group(1))
+ detail = f"providers={','.join(providers)}" if providers else
"configured argument source"
+ result.append(f"{path}:{line_number(source, match.start())}
method={method_name} {detail}; verify at least three actual argument sets")
+ return result
+
+
+def collect_scan_result(paths: list[Path], scope_baseline_path: Path | None)
-> dict:
violations_by_rule = defaultdict(list)
- precheck_violations = defaultdict(list)
java_paths = [each for each in paths if each.suffix == ".java"]
- scope_baseline = load_scope_baseline(scope_baseline_path) if not
precheck_only and scope_baseline_path and scope_baseline_path.exists() else None
- contexts = []
- for path in java_paths:
- if precheck_only:
- source = path.read_text(encoding="utf-8")
- method_bodies = {name: body for name, (_, body) in
parse_method_blocks(source).items()}
- else:
- context = create_file_scan_context(path, scope_baseline)
- contexts.append(context)
- source = context.source
- method_bodies = context.method_bodies
- file_prechecks = collect_precheck_violations(path, source,
method_bodies)
- for name, entries in file_prechecks.items():
- precheck_violations[name].extend(entries)
- if not precheck_only:
- for rule, entries in file_rule_violations(context,
file_prechecks).items():
- violations_by_rule[rule].extend(entries)
- prechecks = {
- name: {
- "message": PRECHECK_MESSAGES[name],
- "violations": precheck_violations[name],
- }
- for name in PRECHECK_ORDER
- }
- if precheck_only:
- return {"prechecks": prechecks}
+ scope_baseline = load_scope_baseline(scope_baseline_path) if
scope_baseline_path and scope_baseline_path.exists() else None
+ contexts = [create_file_scan_context(each, scope_baseline) for each in
java_paths]
+ for context in contexts:
+ violations_by_rule["R15-J"].extend(check_r15_j(context))
violations_by_rule["R15-C"].extend(
check_scope_baseline(scope_baseline_path, paths) if
scope_baseline_path else ["missing scope baseline"]
)
- unresolved_sources = [
- each
- for context in contexts
- for each in find_unresolved_method_sources(context.path,
context.source, context.method_bodies)
- ]
return {
"rules": build_rule_result(violations_by_rule),
- "prechecks": prechecks,
"candidates": [asdict(each) for context in contexts for each in
context.candidates],
"reviews": {
+ "R14": [str(context.path) for context in contexts],
"R15-A": [str(context.path) for context in contexts if not
context.target_public_methods],
- "R15-B": [each for context in contexts for each in
find_metadata_accessor_test_candidates(context.path, context.source)],
- "R15-D": unresolved_sources,
+ "R15-B": [str(context.path) for context in contexts],
+ "R15-D": [each for context in contexts for each in
find_argument_source_reviews(context.path, context.source)],
+ "R15-F": [each for context in contexts for each in
find_switch_dispatch_candidates(context.path, context.source)],
"R15-G": [each for context in contexts for each in
check_r15_g(context.path, context.source, context.added_lines)],
- "R15-I": unresolved_sources,
+ "R15-H": [each for context in contexts for each in
find_boolean_dispatch_candidates(context.path, context.source)],
+ "R15-I": [each for context in contexts for each in
find_consumer_transport_candidates(context.path, context.source,
context.method_bodies)],
"R15-J": [str(context.path) for context in contexts if not
context.target_public_methods],
},
"java_file_count": len(contexts),
@@ -929,31 +725,12 @@ def print_summary_only(result: dict) -> int:
return 0
-def print_precheck_summary(result: dict) -> int:
- prechecks = result.get("prechecks", {})
- failed = False
- for name in PRECHECK_ORDER:
- details = prechecks.get(name)
- if not details:
- continue
- violations = details["violations"]
- if not violations:
- print(f"[precheck:{name}] ok")
- continue
- failed = True
- print(f"[precheck:{name}] {details['message']}")
- for each in violations:
- print(each)
- return 1 if failed else 0
-
-
def main() -> int:
parser = argparse.ArgumentParser(description="Mechanical quality-rule scan
and task-scope guard for gen-ut.")
parser.add_argument("--scope-baseline", help="Structured task scope
baseline created before editing.")
parser.add_argument("--capture-scope-baseline", help="Write a structured
task scope baseline to this path, then exit.")
parser.add_argument("--json", action="store_true", help="Emit JSON output
instead of the default text report.")
parser.add_argument("--summary-only", action="store_true", help="Emit a
compact text summary with candidate information.")
- parser.add_argument("--precheck-only", action="store_true", help="Emit
only lightweight deterministic prechecks for early edit loops.")
parser.add_argument("paths", nargs="*", help="Resolved test file set.")
args = parser.parse_args()
@@ -967,12 +744,10 @@ def main() -> int:
print(f"[scope-baseline] path={output_path.resolve()}
allowedPathCount={len(paths)}")
return 0
baseline_path = Path(args.scope_baseline) if args.scope_baseline else
None
- result = collect_scan_result(paths, baseline_path,
precheck_only=args.precheck_only and not args.json)
+ result = collect_scan_result(paths, baseline_path)
if args.json:
print(json.dumps(result, indent=2, sort_keys=True))
return 1 if failed_rule_names(result) else 0
- if args.precheck_only:
- return print_precheck_summary(result)
if args.summary_only:
return print_summary_only(result)
return print_rule_summary(result)
diff --git a/.codex/skills/gen-ut/tests/test_scan_quality_rules.py
b/.codex/skills/gen-ut/tests/test_scan_quality_rules.py
index e7d3d61060b..cb8e6ba33a9 100644
--- a/.codex/skills/gen-ut/tests/test_scan_quality_rules.py
+++ b/.codex/skills/gen-ut/tests/test_scan_quality_rules.py
@@ -68,9 +68,7 @@ class ScanQualityRulesTest(unittest.TestCase):
}
}
"""
- method_bodies = {name: body for name, (_, body) in
rules.parse_method_blocks(source).items()}
- self.assertEqual([], rules.check_r15_d(Path("TargetTest.java"),
source, method_bodies))
- reviews =
rules.find_unresolved_method_sources(Path("TargetTest.java"), source,
method_bodies)
+ reviews = rules.find_argument_source_reviews(Path("TargetTest.java"),
source)
self.assertEqual(1, len(reviews))
def test_nested_type_declarations_require_manual_review(self):
@@ -85,13 +83,21 @@ class ScanQualityRulesTest(unittest.TestCase):
"module/src/test/java/foo/TargetTest.java: final class
FooGroup {",
),
(
- "scenario-carrier",
+ "data-only-case",
"final FooCase value",
"target.run(value.input())",
"private record FooCase(int input, int expected) {}",
"+ private record FooCase(int input, int expected) {}",
"module/src/test/java/foo/TargetTest.java: private record
FooCase(int input, int expected) {}",
),
+ (
+ "assertion-callback-case",
+ "final FooCase value",
+ "int actual = target.run(value.input());
value.assertion().accept(actual)",
+ "private record FooCase(int input, Consumer<Integer>
assertion) {}",
+ "+ private record FooCase(int input, Consumer<Integer>
assertion) {}",
+ "module/src/test/java/foo/TargetTest.java: private record
FooCase(int input, Consumer<Integer> assertion) {}",
+ ),
)
for name, parameter, invocation, declaration, added_line,
expected_review in fixtures:
with self.subTest(name=name):
@@ -123,160 +129,117 @@ class ScanQualityRulesTest(unittest.TestCase):
with self.subTest(name=name):
self.assertEqual([],
rules.check_r15_g(Path("TargetTest.java"), candidate_source, added_lines))
- def test_ordinary_get_type_is_not_metadata_accessor_violation(self):
- source = """
+ def test_test_value_review_is_independent_of_method_name(self):
+ path = Path("TargetTest.java")
+ for method_name in ("assertGetType", "assertGetOrder",
"assertGetTypeClass", "assertRun"):
+ with self.subTest(method_name=method_name):
+ source = f"class TargetTest {{ @Test void {method_name}() {{
assertTrue(target.run()); }} }}"
+ context = rules.FileScanContext(
+ path=path, source=source, method_bodies={},
method_blocks={}, candidates=[],
+ target_type_name="Target", target_public_methods={"run"},
added_lines=[],
+ )
+ with patch.object(rules, "create_file_scan_context",
return_value=context), patch.object(rules, "check_scope_baseline",
return_value=[]):
+ actual = rules.collect_scan_result([path],
Path("scope.json"))
+ self.assertEqual("manual", actual["rules"]["R15-B"]["mode"])
+ self.assertEqual([], actual["rules"]["R15-B"]["violations"])
+ self.assertEqual([str(path)], actual["reviews"]["R15-B"])
+ self.assertEqual([], rules.failed_rule_names(actual))
+
+ def test_boolean_dispatch_rule_reports_switch_review_candidate(self):
+ path = Path("TargetTest.java")
+ source = '''
class TargetTest {
- @Test void assertRecord() {
- assertThat(record.getType(), is(OperationType.INSERT));
+ @Test void assertRun() {
+ switch (value) {
+ case 1 -> assertTrue(target.run());
+ default -> assertFalse(target.run());
+ }
}
}
- """
- self.assertEqual([],
rules.find_metadata_accessor_test_candidates(Path("TargetTest.java"), source))
-
- def test_boolean_rule_ignores_comments_and_literals(self):
+ '''
+ context = rules.FileScanContext(path=path, source=source,
method_bodies={}, method_blocks={}, candidates=[],
+ target_type_name="Target",
target_public_methods={"run"}, added_lines=[])
+ with patch.object(rules, "create_file_scan_context",
return_value=context), patch.object(rules, "check_scope_baseline",
return_value=[]):
+ actual = rules.collect_scan_result([path], Path("scope.json"))
+ self.assertEqual(["TargetTest.java:3 method=assertRun"],
actual["reviews"]["R15-H"])
+ self.assertEqual("manual", actual["rules"]["R15-H"]["mode"])
+ self.assertEqual([], rules.failed_rule_names(actual))
+
+ def test_standard_argument_sources_require_actual_row_count_review(self):
+ for annotation in ('@CsvSource({"foo,1", "bar,2", "baz,3"})',
'@ValueSource(strings = {"foo", "bar", "baz"})', '@MethodSource("cases")'):
+ with self.subTest(annotation=annotation):
+ source = f'@ParameterizedTest(name = "{{0}}") {annotation}
void assertRun(final String name) {{ target.run(name); }}'
+ actual =
rules.find_argument_source_reviews(Path("TargetTest.java"), source)
+ self.assertEqual(1, len(actual))
+ self.assertIn("verify at least three actual argument sets",
actual[0])
+
+ def
test_branch_and_business_callback_candidates_require_review_without_mechanical_rejection(self):
+ path = Path("TargetTest.java")
source = '''
class TargetTest {
- // assertEquals(true, target.run());
- String example = "assertThat(value, is(false))";
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("cases")
+ void assertRun(final String name, final Consumer<String>
callback) {
+ switch (name) { case "foo" -> target.run(callback);
default -> target.close(); }
+ }
}
'''
- self.assertEqual([], rules.check_r14(Path("TargetTest.java"), source))
-
- def test_boolean_rule_detects_code(self):
- source = "class TargetTest { @Test void assertRun() {
assertEquals(true, target.run()); } }"
- self.assertEqual(["TargetTest.java:1"],
rules.check_r14(Path("TargetTest.java"), source))
-
- def test_parameterized_name_rule_ignores_comment(self):
- source = "// @ParameterizedTest\nclass TargetTest {}"
- self.assertEqual([],
rules.check_parameterized_name(Path("TargetTest.java"), source))
-
- def test_boolean_dispatch_rule_detects_switch(self):
+ blocks = rules.parse_method_blocks(source)
+ context = rules.FileScanContext(path=path, source=source,
method_bodies={name: body for name, (_, body) in blocks.items()},
+ method_blocks=blocks, candidates=[],
target_type_name="Target", target_public_methods={"run", "close"},
added_lines=[])
+ with patch.object(rules, "create_file_scan_context",
return_value=context), patch.object(rules, "check_scope_baseline",
return_value=[]):
+ actual = rules.collect_scan_result([path], Path("scope.json"))
+ self.assertEqual([], rules.failed_rule_names(actual))
+ for rule in ("R15-F", "R15-I"):
+ with self.subTest(rule=rule):
+ self.assertEqual("manual", actual["rules"][rule]["mode"])
+ self.assertEqual([], actual["rules"][rule]["violations"])
+ self.assertTrue(actual["reviews"][rule])
+
+ def
test_repeated_test_is_an_entry_but_helper_still_fails_ownership_gate(self):
source = '''
class TargetTest {
- @Test void assertRun() {
- switch (value) {
- case 1 -> assertTrue(target.run());
- default -> assertFalse(target.run());
- }
+ @RepeatedTest(3)
+ void assertRepeatedExecution() {
+ final Target target = new Target();
+ assertTrue(target.run());
+ }
+ private boolean runHelper() {
+ final Target target = new Target();
+ return target.run();
}
}
'''
- self.assertEqual(["TargetTest.java:3 method=assertRun"],
rules.check_r15_h(Path("TargetTest.java"), source))
+ with tempfile.TemporaryDirectory() as temp_dir:
+ test_path = Path(temp_dir) /
"module/src/test/java/sample/TargetTest.java"
+ target_path = Path(temp_dir) /
"module/src/main/java/sample/Target.java"
+ test_path.parent.mkdir(parents=True)
+ target_path.parent.mkdir(parents=True)
+ test_path.write_text(source, encoding="utf-8")
+ target_path.write_text("class Target {\n public boolean run() {
return true; }\n}\n", encoding="utf-8")
+ with patch.object(rules, "check_scope_baseline", return_value=[]):
+ actual = rules.collect_scan_result([test_path], Path(temp_dir)
/ "scope.json")
+ violations = actual["rules"]["R15-J"]["violations"]
+ self.assertEqual(1, len(violations))
+ self.assertIn("method=runHelper
reason=helperInvokesTargetPublicMethod", violations[0])
def test_resolve_target_source_path_for_root_module(self):
source = "class TargetTest {}"
actual =
rules.resolve_target_source_path(Path("src/test/java/sample/TargetTest.java"),
source)
self.assertEqual(Path("src/main/java/sample/Target.java"), actual)
- def test_scan_reuses_parameterized_prechecks(self):
- source = 'class TargetTest {\n@ParameterizedTest(name = "{0}")\nvoid
assertRun(int value) {}\n}'
- with tempfile.TemporaryDirectory() as temp_dir:
- paths = [Path(temp_dir) / "TargetTest.java", Path(temp_dir) /
"OtherTest.java"]
- for path in paths:
- path.write_text(source, encoding="utf-8")
- with patch.object(rules, "check_r15_d", wraps=rules.check_r15_d)
as check_d, patch.object(rules, "check_r15_e", wraps=rules.check_r15_e) as
check_e:
- actual = rules.collect_scan_result([*paths, Path(temp_dir) /
"sample.bin"], None)
- self.assertEqual(2, check_d.call_count)
- self.assertEqual(2, check_e.call_count)
- expected_d = [f"{path}:2 method=assertRun missing @MethodSource" for
path in paths]
- expected_e = [f"{path}:2 method=assertRun firstParam=int value" for
path in paths]
- expected_violations = {"R15-C": ["missing scope baseline"], "R15-D":
expected_d, "R15-E": expected_e}
- self.assertEqual({
- "rules": {
- each.name: {"message": each.message, "mode": each.mode,
"violations": expected_violations.get(each.name, [])}
- for each in rules.RULE_SPECS
- },
- "prechecks": {
- "checkstyle-final-parameters": {
- "message":
rules.PRECHECK_MESSAGES["checkstyle-final-parameters"],
- "violations": [f"{path}:2 method=assertRun param=int
value" for path in paths],
- },
- "parameterized-methodsource": {"message":
rules.PRECHECK_MESSAGES["parameterized-methodsource"], "violations":
expected_d},
- "parameterized-name-parameter": {"message":
rules.PRECHECK_MESSAGES["parameterized-name-parameter"], "violations":
expected_e},
- },
- "candidates": [],
- "reviews": {"R15-A": [str(path) for path in paths], "R15-B": [],
"R15-D": [], "R15-G": [], "R15-I": [], "R15-J": [str(path) for path in paths]},
- "java_file_count": 2,
- }, actual)
-
- def test_precheck_only_skips_full_scan_work(self):
- with tempfile.TemporaryDirectory() as temp_dir:
- path = Path(temp_dir) / "src/test/java/sample/TargetTest.java"
- target = Path(temp_dir) / "src/main/java/sample/Target.java"
- baseline = Path(temp_dir) / "scope.json"
- path.parent.mkdir(parents=True)
- target.parent.mkdir(parents=True)
- path.write_text('class TargetTest { @ParameterizedTest(name =
"{0}") @MethodSource("Samples#cases") void assertRun(final String name) {} }',
encoding="utf-8")
- target.write_bytes(b"\xff")
- baseline.write_text("invalid baseline", encoding="utf-8")
- with (
- patch.object(sys, "argv", ["scan", "--precheck-only",
"--scope-baseline", str(baseline), str(path)]),
- patch.object(sys, "stdout", new_callable=io.StringIO) as
stdout,
- patch.object(Path, "read_text", autospec=True,
side_effect=Path.read_text) as read_text,
- patch.object(rules, "create_file_scan_context",
side_effect=AssertionError("full context requested")),
- patch.object(rules.subprocess, "run",
side_effect=AssertionError("Git requested")),
- ):
- actual = rules.main()
- read_text.assert_called_once_with(path, encoding="utf-8")
- self.assertEqual(0, actual)
- self.assertEqual("[precheck:checkstyle-final-parameters]
ok\n[precheck:parameterized-methodsource]
ok\n[precheck:parameterized-name-parameter] ok\n", stdout.getvalue())
-
- def test_precheck_only_diagnostics(self):
- with tempfile.TemporaryDirectory() as temp_dir:
- path = Path(temp_dir) / "TargetTest.java"
- valid_source = (
- 'class TargetTest { @ParameterizedTest(name = "{0}")
@MethodSource("samples") void assertRun(final String name) {} '
- 'static Stream<Arguments> samples() { return
Stream.of(Arguments.of("foo"), Arguments.of("bar"), Arguments.of("baz")); } }'
- )
- invalid_source = 'class TargetTest { @ParameterizedTest(name =
"{0}") void assertRun(int value) {} }'
- expected_violations = (
- "[precheck:checkstyle-final-parameters] test method parameters
should be final to avoid Checkstyle FinalParameters failures\n"
- f"{path}:1 method=assertRun param=int value\n"
- "[precheck:parameterized-methodsource] parameterized tests
should use @MethodSource providers with at least 3 Arguments rows\n"
- f"{path}:1 method=assertRun missing @MethodSource\n"
- "[precheck:parameterized-name-parameter] parameterized tests
should declare the first parameter exactly as `final String name`\n"
- f"{path}:1 method=assertRun firstParam=int value\n"
- )
- expected_ok = "[precheck:checkstyle-final-parameters]
ok\n[precheck:parameterized-methodsource]
ok\n[precheck:parameterized-name-parameter] ok\n"
- expected_short_provider = (
- "[precheck:checkstyle-final-parameters] ok\n"
- "[precheck:parameterized-methodsource] parameterized tests
should use @MethodSource providers with at least 3 Arguments rows\n"
- f"{path}:1 method=assertRun argumentsRows=2\n"
- "[precheck:parameterized-name-parameter] ok\n"
- )
- fixtures = (
- ("valid", path, valid_source, 0, expected_ok, ""),
- ("violations", path, invalid_source, 1, expected_violations,
""),
- ("short-provider", path, valid_source.replace(',
Arguments.of("baz")', ""), 1, expected_short_provider, ""),
- ("missing-java", Path(temp_dir) / "MissingTest.java", None, 2,
"", f"[Errno 2] No such file or directory: '{Path(temp_dir) /
'MissingTest.java'}'\n"),
- ("non-java", Path(temp_dir) / "sample.bin", None, 0,
expected_ok, ""),
- )
- for name, input_path, source, expected_exit, expected_stdout,
expected_stderr in fixtures:
- with self.subTest(name=name):
- if source is not None:
- input_path.write_text(source, encoding="utf-8")
- with (
- patch.object(sys, "argv", ["scan", "--precheck-only",
"--summary-only", str(input_path)]),
- patch.object(sys, "stdout", new_callable=io.StringIO)
as stdout,
- patch.object(sys, "stderr", new_callable=io.StringIO)
as stderr,
- ):
- actual = rules.main()
- self.assertEqual(expected_exit, actual)
- self.assertEqual(expected_stdout, stdout.getvalue())
- self.assertEqual(expected_stderr, stderr.getvalue())
-
- def test_json_precedes_precheck_and_summary(self):
+ def test_json_precedes_summary(self):
with tempfile.TemporaryDirectory() as temp_dir:
path = Path(temp_dir) / "TargetTest.java"
path.write_text("class TargetTest {}", encoding="utf-8")
with (
- patch.object(sys, "argv", ["scan", "--json",
"--precheck-only", "--summary-only", str(path)]),
+ patch.object(sys, "argv", ["scan", "--json", "--summary-only",
str(path)]),
patch.object(sys, "stdout", new_callable=io.StringIO) as
stdout,
):
actual = rules.main()
result = json.loads(stdout.getvalue())
self.assertEqual(1, actual)
- self.assertEqual(["candidates", "java_file_count", "prechecks",
"reviews", "rules"], list(result))
+ self.assertEqual(["candidates", "java_file_count", "reviews",
"rules"], list(result))
self.assertEqual(["missing scope baseline"],
result["rules"]["R15-C"]["violations"])
self.assertEqual(1, result["java_file_count"])
self.assertEqual(json.dumps(result, indent=2, sort_keys=True) + "\n",
stdout.getvalue())
@@ -287,7 +250,7 @@ class ScanQualityRulesTest(unittest.TestCase):
baseline = repo_root / "scope.json"
path = repo_root / "TargetTest.java"
with (
- patch.object(sys, "argv", ["scan", "--capture-scope-baseline",
str(baseline), "--json", "--precheck-only", "--summary-only", str(path)]),
+ patch.object(sys, "argv", ["scan", "--capture-scope-baseline",
str(baseline), "--json", "--summary-only", str(path)]),
patch.object(sys, "stdout", new_callable=io.StringIO) as
stdout,
patch.object(rules, "get_repo_root", return_value=repo_root),
patch.object(rules, "write_scope_baseline") as write_baseline,
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index 9e01f2ceaa8..6b439d471bd 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -87,14 +87,14 @@ The following code of conduct is based on full compliance
with the [Apache Softw
- Unit tests must exercise behavior through public APIs only. Reflection-based
invocation of private members is forbidden. If tests must access fields via
reflection, use `Plugins.getMemberAccessor()` and limit reflection to `Field`
access only.
- Tests that modify static state must restore the original state after each
test.
- Obtain SPI implementations through the project loader by default. If the
class under test implements `TypedSPI` or `DatabaseTypedSPI`, instantiate it
through `TypedSPILoader` or `DatabaseTypedSPILoader`, not with `new`.
+- Do not write separate tests for methods that only return constants, directly
return fields, or simply delegate calls.
- Every unit-test class must directly test a corresponding production class
and be named `<ProductionClassName>Test`, using the exact simple name of the
production class. This class-name rule is mandatory and is independent of
scenario-focused test-method naming.
- When a production method is covered by only one test case, name that test
method `assert<MethodName>` without extra suffixes, and prefer isolating one
public production method per dedicated test method; when practical, keep test
method ordering aligned with the corresponding production methods.
-- For parameterized tests, provide display names via parameters and use
`"{0}"` as the display-name template.
- Keep test names concise and scenario-focused; avoid `ReturnsXXX` and wording
that restates the expected result instead of naming the scenario.
- Assertions must directly express the tested contract. Use `not` or
`containsString` only when the contract requires inequality or substring
matching; do not use them when an exact value or a more specific matcher is
available.
- Default to direct Mockito mocks. Use a private helper only for repeated
local setup and a standalone fixture only for a stable external or packaged
test boundary. Give fixtures the narrowest practical visibility, keep them in
the nearest owning test package or module, and do not create cross-module test
APIs for convenience. Delete or inline thin mock wrappers.
- Data assertion standards should follow:
- - Boolean type assertions should use `assertTrue` and `assertFalse`;
+ - Boolean assertions must use `assertTrue` or `assertFalse`;
- Null value assertions should use `assertNull` and `assertNotNull`;
- Non-boolean, non-null value equality assertions must use
`assertThat(actual, is(expected))`;
- Type assertions must use `assertThat(actual, isA(ExpectedType.class))`;
@@ -111,6 +111,11 @@ The following code of conduct is based on full compliance
with the [Apache Softw
- For deep chained interactions, use Mockito’s `RETURNS_DEEP_STUBS` instead of
layering intermediate mocks.
- Test data should use standardized prefixes (e.g., `foo_`/`bar_`) to clearly
identify their test purpose
- Use `PropertiesBuilder` simplify `Properties` building.
+- Parameterized tests:
+ - For parameterized tests, provide display names via parameters and use
`"{0}"` as the display-name template;
+ - Declare the first parameter of each parameterized test exactly as `final
String name`;
+ - Provide at least three meaningful argument sets for each parameterized
test;
+ - Only combine cases that invoke the same method under test and use the
same form of assertion.
## SQL Parsing Standards
diff --git
a/database/connector/dialect/mysql/src/test/java/org/apache/shardingsphere/database/connector/mysql/metadata/identifier/MySQLIdentifierCasePolicyProviderTest.java
b/database/connector/dialect/mysql/src/test/java/org/apache/shardingsphere/database/connector/mysql/metadata/identifier/MySQLIdentifierCasePolicyProviderTest.java
index 8fe438090e9..3246003cc6e 100644
---
a/database/connector/dialect/mysql/src/test/java/org/apache/shardingsphere/database/connector/mysql/metadata/identifier/MySQLIdentifierCasePolicyProviderTest.java
+++
b/database/connector/dialect/mysql/src/test/java/org/apache/shardingsphere/database/connector/mysql/metadata/identifier/MySQLIdentifierCasePolicyProviderTest.java
@@ -46,6 +46,8 @@ import java.util.stream.Stream;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
class MySQLIdentifierCasePolicyProviderTest {
@@ -60,20 +62,20 @@ class MySQLIdentifierCasePolicyProviderTest {
IdentifierCasePolicySet actual = provider.provide(context);
assertThat(actual.getPolicy(IdentifierScope.TABLE).getLookupMode(QuoteCharacter.BACK_QUOTE),
is(expectedQuotedLookupMode));
assertThat(actual.getPolicy(IdentifierScope.TABLE).getLookupMode(QuoteCharacter.NONE),
is(expectedUnquotedLookupMode));
- assertThat(actual.getPolicy(IdentifierScope.TABLE).matches("foo",
"FOO", QuoteCharacter.NONE), is(expectedMatch));
+ assertTrue(expectedMatch ==
actual.getPolicy(IdentifierScope.TABLE).matches("foo", "FOO",
QuoteCharacter.NONE));
assertThat(actual.getPolicy(IdentifierScope.COLUMN).normalizeForDefinition("FooColumn",
QuoteCharacter.NONE), is("FooColumn"));
assertThat(actual.getPolicy(IdentifierScope.INDEX).normalizeForDefinition("FooIndex",
QuoteCharacter.NONE), is("FooIndex"));
assertThat(actual.getPolicy(IdentifierScope.CONSTRAINT).normalizeForDefinition("FooConstraint",
QuoteCharacter.NONE), is("FooConstraint"));
-
assertThat(actual.getPolicy(IdentifierScope.COLUMN).matches("foo_column",
"FOO_COLUMN", QuoteCharacter.NONE), is(Boolean.TRUE));
-
assertThat(actual.getPolicy(IdentifierScope.INDEX).matches("foo_index",
"FOO_INDEX", QuoteCharacter.NONE), is(Boolean.TRUE));
-
assertThat(actual.getPolicy(IdentifierScope.CONSTRAINT).matches("foo_constraint",
"FOO_CONSTRAINT", QuoteCharacter.NONE), is(Boolean.TRUE));
+
assertTrue(actual.getPolicy(IdentifierScope.COLUMN).matches("foo_column",
"FOO_COLUMN", QuoteCharacter.NONE));
+
assertTrue(actual.getPolicy(IdentifierScope.INDEX).matches("foo_index",
"FOO_INDEX", QuoteCharacter.NONE));
+
assertTrue(actual.getPolicy(IdentifierScope.CONSTRAINT).matches("foo_constraint",
"FOO_CONSTRAINT", QuoteCharacter.NONE));
}
@Test
void assertProvideWithQuotedTableName() {
IdentifierCasePolicy actual = provider.provide(new
IdentifierCasePolicyProviderContext(DATABASE_TYPE, new FixtureDataSource(true,
1))).getPolicy(IdentifierScope.TABLE);
assertThat(actual.getLookupMode(QuoteCharacter.BACK_QUOTE),
is(LookupMode.NORMALIZED));
- assertThat(actual.matches("t_mask", "T_MASK",
QuoteCharacter.BACK_QUOTE), is(Boolean.TRUE));
+ assertTrue(actual.matches("t_mask", "T_MASK",
QuoteCharacter.BACK_QUOTE));
}
private static Stream<Arguments> provideArguments() {
@@ -96,13 +98,13 @@ class MySQLIdentifierCasePolicyProviderTest {
void assertProvideWithLowerCaseTableNamesZeroUsesScopedPolicies() {
IdentifierCasePolicyProviderContext context = new
IdentifierCasePolicyProviderContext(DATABASE_TYPE, new FixtureDataSource(true,
0));
IdentifierCasePolicySet actual = provider.provide(context);
-
assertThat(actual.getPolicy(IdentifierScope.DATABASE).matches("foo_db",
"FOO_DB", QuoteCharacter.NONE), is(Boolean.FALSE));
-
assertThat(actual.getPolicy(IdentifierScope.SCHEMA).matches("foo_schema",
"FOO_SCHEMA", QuoteCharacter.NONE), is(Boolean.FALSE));
- assertThat(actual.getPolicy(IdentifierScope.TABLE).matches("foo_tbl",
"FOO_TBL", QuoteCharacter.NONE), is(Boolean.FALSE));
- assertThat(actual.getPolicy(IdentifierScope.VIEW).matches("foo_view",
"FOO_VIEW", QuoteCharacter.NONE), is(Boolean.FALSE));
- assertThat(actual.getPolicy(IdentifierScope.COLUMN).matches("foo_col",
"FOO_COL", QuoteCharacter.NONE), is(Boolean.TRUE));
- assertThat(actual.getPolicy(IdentifierScope.INDEX).matches("foo_idx",
"FOO_IDX", QuoteCharacter.NONE), is(Boolean.TRUE));
-
assertThat(actual.getPolicy(IdentifierScope.CONSTRAINT).matches("foo_fk",
"FOO_FK", QuoteCharacter.NONE), is(Boolean.TRUE));
+
assertFalse(actual.getPolicy(IdentifierScope.DATABASE).matches("foo_db",
"FOO_DB", QuoteCharacter.NONE));
+
assertFalse(actual.getPolicy(IdentifierScope.SCHEMA).matches("foo_schema",
"FOO_SCHEMA", QuoteCharacter.NONE));
+ assertFalse(actual.getPolicy(IdentifierScope.TABLE).matches("foo_tbl",
"FOO_TBL", QuoteCharacter.NONE));
+ assertFalse(actual.getPolicy(IdentifierScope.VIEW).matches("foo_view",
"FOO_VIEW", QuoteCharacter.NONE));
+ assertTrue(actual.getPolicy(IdentifierScope.COLUMN).matches("foo_col",
"FOO_COL", QuoteCharacter.NONE));
+ assertTrue(actual.getPolicy(IdentifierScope.INDEX).matches("foo_idx",
"FOO_IDX", QuoteCharacter.NONE));
+
assertTrue(actual.getPolicy(IdentifierScope.CONSTRAINT).matches("foo_fk",
"FOO_FK", QuoteCharacter.NONE));
}
@ParameterizedTest(name = "{0}")
diff --git a/docs/community/content/involved/conduct/code.cn.md
b/docs/community/content/involved/conduct/code.cn.md
index 24fc8329581..7b925d7c77c 100644
--- a/docs/community/content/involved/conduct/code.cn.md
+++ b/docs/community/content/involved/conduct/code.cn.md
@@ -101,14 +101,14 @@ chapter = true
- 单元测试必须通过公共 API 验证行为,禁止通过反射调用私有成员。 若测试必须通过反射访问字段,应使用
`Plugins.getMemberAccessor()`,且反射仅限于 `Field` 访问。
- 测试修改静态状态时,必须在每个测试结束后恢复其原始状态。
- 默认通过项目加载器获取 SPI 实现。 如果被测类实现了 `TypedSPI` 或 `DatabaseTypedSPI`,应通过
`TypedSPILoader` 或 `DatabaseTypedSPILoader` 实例化,禁止使用 `new`。
+ - 不得为只返回固定值、直接返回字段或简单转发调用的方法单独编写测试。
- 每个单元测试类必须直接测试一个对应的生产类,测试类必须使用生产类的准确简单类名并命名为 `<ProductionClassName>Test`。
该测试类命名规则为强制要求,与面向场景的测试方法命名规则相互独立。
- 当某个生产方法只由一个测试用例覆盖时,测试方法命名为
`assert<MethodName>`,无额外后缀;应优先使用独立测试方法覆盖单个公开的生产方法;可行时,测试方法顺序与对应的生产方法保持一致。
- - 参数化测试需通过参数提供显示名,并使用 `"{0}"` 作为展示名模板。
- 测试名称应简洁并聚焦场景;避免使用 `ReturnsXXX`,也不要使用仅复述预期结果而未说明场景的措辞。
- 断言必须直接表达被测契约。 仅当被测契约是不相等或包含指定子串时,才可使用 `not` 或
`containsString`;可断言完整值或使用更具体的 matcher 时,不得使用这两个 matcher。
- 默认直接使用 Mockito mock。 仅对重复的局部准备使用私有辅助方法,仅对稳定的外部测试边界或打包测试边界使用独立测试夹具。
测试夹具应采用实际可行的最小可见性,并放在最近的所属测试包或模块中;不要为了方便而创建跨模块测试 API。 删除或内联内容单薄的 mock 包装器。
- 数据断言规范应遵循:
- - 布尔类型断言应使用 `assertTrue` 和 `assertFalse`;
+ - 布尔断言必须使用 `assertTrue` 或 `assertFalse`;
- 空值断言应使用 `assertNull` 和 `assertNotNull`;
- 非布尔值、非空值的相等断言必须使用 `assertThat(actual, is(expected))`;
- 类型断言必须使用 `assertThat(actual, isA(ExpectedType.class))`;
@@ -125,6 +125,11 @@ chapter = true
- 深度链式交互使用 Mockito 的 `RETURNS_DEEP_STUBS`,不要层层手动 mock。
- 测试数据应使用标准化前缀(如 `foo_`/`bar_`)明确标识其测试用途。
- 使用 `PropertiesBuilder` 简化 `Properties` 构造。
+ - 参数化测试:
+ - 参数化测试需通过参数提供显示名,并使用 `"{0}"` 作为展示名模板;
+ - 参数化测试的首个参数必须声明为 `final String name`;
+ - 每个参数化测试必须提供至少三组有意义的参数数据;
+ - 只合并调用相同被测方法、使用相同断言方式的用例。
## SQL 解析规范
diff --git a/docs/community/content/involved/conduct/code.en.md
b/docs/community/content/involved/conduct/code.en.md
index b57aad7fec3..94536dcf9f8 100644
--- a/docs/community/content/involved/conduct/code.en.md
+++ b/docs/community/content/involved/conduct/code.en.md
@@ -101,14 +101,14 @@ The following code of conduct is based on full compliance
with the [Apache Softw
- Unit tests must exercise behavior through public APIs only. Reflection-based
invocation of private members is forbidden. If tests must access fields via
reflection, use `Plugins.getMemberAccessor()` and limit reflection to `Field`
access only.
- Tests that modify static state must restore the original state after each
test.
- Obtain SPI implementations through the project loader by default. If the
class under test implements `TypedSPI` or `DatabaseTypedSPI`, instantiate it
through `TypedSPILoader` or `DatabaseTypedSPILoader`, not with `new`.
+- Do not write separate tests for methods that only return constants, directly
return fields, or simply delegate calls.
- Every unit-test class must directly test a corresponding production class
and be named `<ProductionClassName>Test`, using the exact simple name of the
production class. This class-name rule is mandatory and is independent of
scenario-focused test-method naming.
- When a production method is covered by only one test case, name that test
method `assert<MethodName>` without extra suffixes, and prefer isolating one
public production method per dedicated test method; when practical, keep test
method ordering aligned with the corresponding production methods.
-- For parameterized tests, provide display names via parameters and use
`"{0}"` as the display-name template.
- Keep test names concise and scenario-focused; avoid `ReturnsXXX` and wording
that restates the expected result instead of naming the scenario.
- Assertions must directly express the tested contract. Use `not` or
`containsString` only when the contract requires inequality or substring
matching; do not use them when an exact value or a more specific matcher is
available.
- Default to direct Mockito mocks. Use a private helper only for repeated
local setup and a standalone fixture only for a stable external or packaged
test boundary. Give fixtures the narrowest practical visibility, keep them in
the nearest owning test package or module, and do not create cross-module test
APIs for convenience. Delete or inline thin mock wrappers.
- Data assertion standards should follow:
- - Boolean type assertions should use `assertTrue` and `assertFalse`;
+ - Boolean assertions must use `assertTrue` or `assertFalse`;
- Null value assertions should use `assertNull` and `assertNotNull`;
- Non-boolean, non-null value equality assertions must use
`assertThat(actual, is(expected))`;
- Type assertions must use `assertThat(actual, isA(ExpectedType.class))`;
@@ -125,6 +125,11 @@ The following code of conduct is based on full compliance
with the [Apache Softw
- For deep chained interactions, use Mockito’s `RETURNS_DEEP_STUBS` instead of
layering intermediate mocks.
- Test data should use standardized prefixes (e.g., `foo_`/`bar_`) to clearly
identify their test purpose
- Use `PropertiesBuilder` simplify `Properties` building.
+- Parameterized tests:
+ - For parameterized tests, provide display names via parameters and use
`"{0}"` as the display-name template;
+ - Declare the first parameter of each parameterized test exactly as `final
String name`;
+ - Provide at least three meaningful argument sets for each parameterized
test;
+ - Only combine cases that invoke the same method under test and use the
same form of assertion.
## SQL Parsing Standards
diff --git a/src/resources/checkstyle.xml b/src/resources/checkstyle.xml
index 0ac86a91a85..35c8ad78bd3 100644
--- a/src/resources/checkstyle.xml
+++ b/src/resources/checkstyle.xml
@@ -125,7 +125,7 @@
<property name="message" value="Consider using special escape
sequence instead of octal value or Unicode escaped value." />
</module>
<module name="MatchXpath">
- <property name="query"
value="//METHOD_CALL[(./IDENT[@text='assertThat'] or
./DOT/IDENT[@text='assertThat']) and
./ELIST/EXPR[last()]/METHOD_CALL[(./IDENT[@text='is'] or
./DOT/IDENT[@text='is']) and ./ELIST/EXPR/*[self::LITERAL_TRUE or
self::LITERAL_FALSE]]]" />
+ <property name="query"
value="//METHOD_CALL[(./IDENT[@text='assertThat'] or
./DOT/IDENT[@text='assertThat']) and
./ELIST/EXPR[last()]/METHOD_CALL[(./IDENT[@text='is'] or
./DOT/IDENT[@text='is']) and ./ELIST/EXPR/*[self::LITERAL_TRUE or
self::LITERAL_FALSE or self::DOT[./IDENT[@text=('TRUE', 'FALSE')] and
(./IDENT[@text='Boolean'] or ./DOT[./IDENT[@text='Boolean'] and
./DOT[./IDENT[@text='java'] and ./IDENT[@text='lang']]])]]]]" />
<message key="matchxpath.match" value="Use assertTrue/assertFalse
instead of assertThat(..., is(true/false))." />
</module>
<module name="MatchXpath">
@@ -133,6 +133,18 @@
value="//METHOD_DEF[./MODIFIERS/ANNOTATION[(./IDENT[@text='Test'] or
./DOT/IDENT[@text='Test'] or ./IDENT[@text='ParameterizedTest'] or
./DOT/IDENT[@text='ParameterizedTest'] or ./IDENT[@text='RepeatedTest'] or
./DOT/IDENT[@text='RepeatedTest'] or ./IDENT[@text='TestFactory'] or
./DOT/IDENT[@text='TestFactory'] or ./IDENT[@text='TestTemplate'] or
./DOT/IDENT[@text='TestTemplate'])] and not(./IDENT[starts-with(@text,
'assert')])]" />
<message key="matchxpath.match" value="Test method names must
start with 'assert'." />
</module>
+ <module name="MatchXpath">
+ <property name="id" value="ParameterizedTestDisplayName" />
+ <property name="severity" value="warning" />
+ <property name="query"
value="//METHOD_DEF/MODIFIERS/ANNOTATION[(./IDENT[@text='ParameterizedTest'] or
./DOT/IDENT[@text='ParameterizedTest']) and
not(./ANNOTATION_MEMBER_VALUE_PAIR[./IDENT[@text='name'] and
./EXPR/STRING_LITERAL[@text='{0}']])]" />
+ <message key="matchxpath.match" value="Parameterized tests must
use name = "{0}"." />
+ </module>
+ <module name="MatchXpath">
+ <property name="id" value="ParameterizedTestNameParameter" />
+ <property name="severity" value="warning" />
+ <property name="query"
value="//METHOD_DEF[./MODIFIERS/ANNOTATION[(./IDENT[@text='ParameterizedTest']
or
./DOT/IDENT[@text='ParameterizedTest'])]]/PARAMETERS[not(./PARAMETER_DEF[1][./MODIFIERS/FINAL
and ./IDENT[@text='name'] and not(./ELLIPSIS) and
./TYPE[not(.//ARRAY_DECLARATOR) and (./IDENT[@text='String'] or
./DOT[./IDENT[@text='String'] and ./DOT[./IDENT[@text='java'] and
./IDENT[@text='lang']]])]])]" />
+ <message key="matchxpath.match" value="Declare final String name
as the first parameter of parameterized tests." />
+ </module>
<module name="MatchXpath">
<property name="query" value="//VARIABLE_DEF[./MODIFIERS/FINAL and
(parent::FOR_INIT or parent::FOR_EACH_CLAUSE)]" />
<message key="matchxpath.match" value="Do not declare for-loop
variables as final." />