* [RFC PATCH] CheckPatch: accept required AI attribution diagnostics
@ 2026-11-12 2:24 Kyle Pepper
0 siblings, 0 replies; only message in thread
From: Kyle Pepper @ 2026-11-12 2:24 UTC (permalink / raw)
To: linux-bluetooth; +Cc: luiz.dentz, marcel
Recognize legacy signature errors only for one well-formed Assisted-by
trailer at its exact patch path and commit-message line. Keep the raw
diagnostics in the result and leave patch bytes unchanged.
Fail checker exits without a recognized diagnostic instead of reporting
success. Test attribution formats, source errors, missing sign-offs and
both orderings of mixed passing and failing patches.
Assisted-by: Codex:GPT-6
---
Reported CI: https://github.com/bluez/bluetooth-next/pull/1084
Local validation: 18 checker tests pass. Both exact signed v8 patches
fail with the original checker and pass with this change, byte-for-byte.
Source, sign-off and mixed-series failures remain fatal.
ci/checkpatch.py | 55 ++++++++-
tests/test_checkpatch.py | 243 +++++++++++++++++++++++++++++++++++++++
2 files changed, 295 insertions(+), 3 deletions(-)
create mode 100644 tests/test_checkpatch.py
diff --git a/ci/checkpatch.py b/ci/checkpatch.py
index 275ce62..198a57a 100755
--- a/ci/checkpatch.py
+++ b/ci/checkpatch.py
@@ -1,4 +1,5 @@
import os
+import re
import sys
sys.path.insert(0, '../libs')
@@ -6,6 +7,39 @@ from libs import cmd_run
from ci import Base, Verdict, EndTest, submit_pw_check
+
+def assisted_by_only(patch_file, ret, stdout, stderr):
+ """Recognize only legacy checkpatch diagnostics on AI attribution."""
+ if ret != 1 or stderr.strip():
+ return False
+
+ with open(patch_file, encoding='utf-8', errors='replace') as patch:
+ message = re.split(r'^---(?:\s.*)?$|^diff --git ', patch.read(),
+ maxsplit=1, flags=re.MULTILINE)[0]
+
+ trailers = [(number, line)
+ for number, line in enumerate(message.splitlines(), 1)
+ if line.startswith('Assisted-by:')]
+ if len(trailers) != 1:
+ return False
+
+ number, trailer = trailers[0]
+ match = re.fullmatch(r'Assisted-by: (LLM|[\w.+-]+:[\w.+-]+(?: [\w.+-]+)*)',
+ trailer, flags=re.ASCII)
+ if not match:
+ return False
+ value = match.group(1)
+
+ prefix = f'{patch_file}:{number}: '
+ allowed = {
+ prefix + 'WARNING:BAD_SIGN_OFF: Non-standard signature: Assisted-by:',
+ prefix + f"ERROR:BAD_SIGN_OFF: Unrecognized email address: '{value}'",
+ }
+ diagnostics = [line for line in stdout.splitlines() if line]
+ return (bool(diagnostics) and len(diagnostics) == len(set(diagnostics))
+ and set(diagnostics) <= allowed)
+
+
class CheckPatch(Base):
"""Check Patch class
This class runs the checkpatch.pl with the patches in the series.
@@ -41,9 +75,13 @@ class CheckPatch(Base):
if ret == 0:
# CheckPatch PASS
self.log_dbg("Test result PASSED")
+ details = "CheckPatch PASS"
+ if stdout.strip():
+ details += "\n" + stdout
+ self.output += f"{patch['name']}\n{stdout}\n"
submit_pw_check(self.ci_data.pw, patch,
self.name, Verdict.PASS,
- "CheckPatch PASS",
+ details,
None, self.ci_data.config['dry_run'])
continue
@@ -69,6 +107,12 @@ class CheckPatch(Base):
self.add_failure(msg)
continue
+ # A failed checker without a recognized diagnostic still fails.
+ submit_pw_check(self.ci_data.pw, patch,
+ self.name, Verdict.FAIL, outstr,
+ None, self.ci_data.config['dry_run'])
+ self.add_failure(msg)
+
# Overall result
if self.verdict == Verdict.FAIL:
self.log_info(f"Test Verdict: {self.verdict.name}")
@@ -78,7 +122,7 @@ class CheckPatch(Base):
self.log_info(f"Test Verdict: {self.verdict.name}")
def _checkpatch(self, patch):
- cmd = [self.checkpatch_pl]
+ cmd = [self.checkpatch_pl, '--show-types', '--terse', '--no-summary']
if self.ignore:
cmd.append('--ignore')
cmd.append(self.ignore)
@@ -88,7 +132,12 @@ class CheckPatch(Base):
f"{patch['id']}.patch"))
self.log_dbg(f"Patch file: {patch_file}")
cmd.append(patch_file)
- return cmd_run(cmd, cwd=self.ci_data.src_dir)
+ ret, stdout, stderr = cmd_run(cmd, cwd=self.ci_data.src_dir)
+ if assisted_by_only(patch_file, ret, stdout, stderr):
+ stdout = ("Accepted required Assisted-by metadata diagnostics; "
+ "attribution preserved:\n" + stdout)
+ return 0, stdout, stderr
+ return ret, stdout, stderr
def post_run(self):
self.log_dbg("Post Run...")
diff --git a/tests/test_checkpatch.py b/tests/test_checkpatch.py
new file mode 100644
index 0000000..5e6d66f
--- /dev/null
+++ b/tests/test_checkpatch.py
@@ -0,0 +1,243 @@
+# SPDX-License-Identifier: GPL-2.0-only
+import importlib.util
+import os
+from pathlib import Path
+import shutil
+import sys
+import tempfile
+from types import ModuleType, SimpleNamespace
+import unittest
+from unittest.mock import patch
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
+def load(name, path):
+ spec = importlib.util.spec_from_file_location(name, path)
+ module = importlib.util.module_from_spec(spec)
+ sys.modules[name] = module
+ spec.loader.exec_module(module)
+ return module
+
+
+# Load the real checker, base and command runner without importing API clients.
+with patch.dict(sys.modules):
+ libs = ModuleType('libs')
+ sys.modules['libs'] = libs
+ utils = load('libs.utils', ROOT / 'libs/utils.py')
+ libs.utils = utils
+ libs.cmd_run = utils.cmd_run
+ libs.log_debug = utils.log_debug
+ ci = ModuleType('ci')
+ sys.modules['ci'] = ci
+ base = load('ci.base', ROOT / 'ci/base.py')
+ for name in ('Base', 'Verdict', 'EndTest', 'submit_pw_check'):
+ setattr(ci, name, getattr(base, name))
+ checker = load('ci.checkpatch', ROOT / 'ci/checkpatch.py')
+
+
+MESSAGE = '''From: Test Author <test@example.com>
+Subject: [PATCH] test: Correct the returned value
+
+Correct the returned value.
+
+{trailer}
+---
+ sample.c | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/sample.c b/sample.c
+--- a/sample.c
++++ b/sample.c
+@@ -1,4 +1,4 @@
+ int sample(void)
+ {{
+-\treturn 0;
++\treturn 1;
+ }}
+'''
+
+
+class AttributionTests(unittest.TestCase):
+ def setUp(self):
+ self.directory = tempfile.TemporaryDirectory()
+ self.addCleanup(self.directory.cleanup)
+ self.path = Path(self.directory.name) / 'sample.patch'
+ self.message('Assisted-by: LLM')
+
+ def message(self, trailer):
+ self.path.write_text(MESSAGE.format(trailer=trailer))
+
+ def diagnostics(self, value='LLM'):
+ number = self.path.read_text().splitlines().index('Assisted-by: ' + value) + 1
+ prefix = f'{self.path}:{number}: '
+ return (prefix + 'WARNING:BAD_SIGN_OFF: Non-standard signature: Assisted-by:\n'
+ + prefix + f"ERROR:BAD_SIGN_OFF: Unrecognized email address: '{value}'\n")
+
+ def accepted(self, output, status=1, stderr=''):
+ return checker.assisted_by_only(str(self.path), status, output, stderr)
+
+ def test_known_formats(self):
+ for value in ('LLM', 'Codex:GPT-6', 'OpenCode:gpt-6.1-sol',
+ 'Claude:claude-3-opus sparse coccinelle'):
+ with self.subTest(value=value):
+ self.message('Assisted-by: ' + value)
+ self.assertTrue(self.accepted(self.diagnostics(value)))
+
+ def test_unrelated_diagnostic(self):
+ self.assertFalse(self.accepted(self.diagnostics() +
+ f'{self.path}:20: ERROR:CODE_INDENT: bad indent\n'))
+
+ def test_bad_status_or_stderr(self):
+ for status, stderr in ((2, ''), (-9, ''), (1, 'checker failed')):
+ with self.subTest(status=status, stderr=stderr):
+ self.assertFalse(self.accepted(self.diagnostics(), status, stderr))
+
+ def test_duplicate_diagnostic(self):
+ self.assertFalse(self.accepted(self.diagnostics() * 2))
+
+ def test_wrong_location(self):
+ self.assertFalse(self.accepted(self.diagnostics().replace(':6:', ':7:')))
+
+ def test_wrong_path(self):
+ self.assertFalse(self.accepted(self.diagnostics().replace('sample.patch', 'other.patch')))
+
+ def test_missing_duplicate_or_invalid_trailer(self):
+ output = self.diagnostics()
+ for trailer in ('', 'Assisted-by: LLM\nAssisted-by: LLM',
+ 'Assisted-by: nonsense', 'Assisted-by: LLM ',
+ 'Assisted-by:', 'Assisted-by:LLM', 'Assisted-by:\tLLM',
+ 'Assisted-by: LLM', 'Assisted-by: LLM <bot@example.com>'):
+ with self.subTest(trailer=trailer):
+ self.message(trailer)
+ self.assertFalse(self.accepted(output))
+
+ def test_source_line_is_not_metadata(self):
+ output = self.diagnostics()
+ self.message('')
+ with self.path.open('a') as stream:
+ stream.write('Assisted-by: LLM\n')
+ self.assertFalse(self.accepted(output))
+
+ def test_empty_or_unknown_output(self):
+ for output in ('', 'checker crashed', self.diagnostics() + 'NOTE: unexpected\n'):
+ with self.subTest(output=output):
+ self.assertFalse(self.accepted(output))
+
+ def make_checker(self, content=None, ignore='UNKNOWN_COMMIT_ID,MISSING_SIGN_OFF'):
+ if content is not None:
+ self.path.write_text(content)
+ original = self.path.read_bytes()
+
+ def save_patch(_identifier, destination):
+ Path(destination).write_bytes(original)
+ return destination
+
+ executable = os.environ.get('CHECKPATCH') or shutil.which('checkpatch.pl')
+ if not executable:
+ self.skipTest('Set CHECKPATCH to a kernel scripts/checkpatch.pl')
+ self.data = SimpleNamespace(
+ pw=SimpleNamespace(save_patch_mbox=save_patch),
+ patch_root=self.directory.name,
+ src_dir=str(Path(executable).resolve().parents[1]),
+ config={'dry_run': True},
+ series={'patches': [{'id': 1, 'name': 'test patch'}]})
+ return checker.CheckPatch(self.data, checkpatch_pl=executable, ignore=ignore)
+
+ def test_real_checker_accepts_attribution_without_mutating_patch(self):
+ for value in ('LLM', 'Codex:GPT-6', 'OpenCode:gpt-6.1-sol'):
+ with self.subTest(value=value):
+ self.message('Assisted-by: ' + value)
+ before = self.path.read_bytes()
+ tested = self.make_checker()
+ with patch.object(checker, 'submit_pw_check') as submit:
+ tested.run()
+ self.assertEqual(tested.verdict, base.Verdict.PASS)
+ self.assertIn('Accepted required Assisted-by', tested.output)
+ self.assertEqual(submit.call_args.args[3], base.Verdict.PASS)
+ self.assertEqual(self.path.read_bytes(), before)
+ self.assertEqual((Path(self.directory.name) / '1.patch').read_bytes(), before)
+
+ def test_real_checker_rejects_source_and_signature_errors(self):
+ for content in (
+ MESSAGE.format(trailer='Assisted-by: LLM').replace('+\treturn 1;', '+ return 1;'),
+ MESSAGE.format(trailer='Assisted-by: LLM\nReviewed-by: nonsense'),
+ MESSAGE.format(trailer='Assisted-by: nonsense')):
+ with self.subTest(content=content):
+ tested = self.make_checker(content)
+ with patch.object(checker, 'submit_pw_check') as submit:
+ with self.assertRaises(base.EndTest):
+ tested.run()
+ self.assertEqual(tested.verdict, base.Verdict.FAIL)
+ self.assertEqual(submit.call_count, 1)
+
+ def test_real_checker_accepts_clean_patch_without_attribution(self):
+ self.message('')
+ tested = self.make_checker()
+ with patch.object(checker, 'submit_pw_check'):
+ tested.run()
+ self.assertEqual(tested.verdict, base.Verdict.PASS)
+ self.assertNotIn('Accepted required', tested.output)
+
+ def test_raw_attribution_diagnostics_remain_in_pass_report(self):
+ tested = self.make_checker()
+ with patch.object(checker, 'submit_pw_check') as submit:
+ tested.run()
+ self.assertEqual(tested.verdict, base.Verdict.PASS)
+ self.assertEqual(submit.call_count, 1)
+ for diagnostic in ('WARNING:BAD_SIGN_OFF: Non-standard signature: Assisted-by:',
+ "ERROR:BAD_SIGN_OFF: Unrecognized email address: 'LLM'"):
+ self.assertIn(diagnostic, tested.output)
+ self.assertIn(diagnostic, submit.call_args.args[4])
+
+ def test_real_checker_rejects_duplicate_metadata(self):
+ self.message('Assisted-by: LLM\nAssisted-by: LLM')
+ tested = self.make_checker()
+ with patch.object(checker, 'submit_pw_check') as submit:
+ with self.assertRaises(base.EndTest):
+ tested.run()
+ self.assertEqual(tested.verdict, base.Verdict.FAIL)
+ self.assertEqual(submit.call_count, 1)
+
+ def test_series_failure_is_not_hidden_by_accepted_metadata(self):
+ good = (0, 'Accepted required metadata', '')
+ bad = (1, 'ERROR: other', '')
+ for results in ([good, bad], [bad, good]):
+ with self.subTest(results=results):
+ tested = self.make_checker()
+ self.data.series['patches'].append({'id': 2, 'name': 'bad patch'})
+ with patch.object(tested, '_checkpatch', side_effect=results):
+ with patch.object(checker, 'submit_pw_check') as submit:
+ with self.assertRaises(base.EndTest):
+ tested.run()
+ self.assertEqual(tested.verdict, base.Verdict.FAIL)
+ self.assertEqual(submit.call_count, 2)
+
+ def test_real_checker_does_not_ignore_missing_signoff(self):
+ tested = self.make_checker(ignore='UNKNOWN_COMMIT_ID')
+ with patch.object(checker, 'submit_pw_check'):
+ with self.assertRaises(base.EndTest):
+ tested.run()
+ self.assertEqual(tested.verdict, base.Verdict.FAIL)
+ self.assertIn('MISSING_SIGN_OFF', tested.output)
+
+ def test_abnormal_checker_exit_is_fatal(self):
+ tested = self.make_checker()
+ with patch.object(checker, 'cmd_run', return_value=(2, '', 'failed')):
+ with patch.object(checker, 'submit_pw_check') as submit:
+ with self.assertRaises(base.EndTest):
+ tested.run()
+ self.assertEqual(tested.verdict, base.Verdict.FAIL)
+ self.assertEqual(submit.call_count, 1)
+
+ def test_warning_only_does_not_submit_twice(self):
+ tested = self.make_checker()
+ with patch.object(checker, 'cmd_run', return_value=(1, 'WARNING: other', '')):
+ with patch.object(checker, 'submit_pw_check') as submit:
+ with self.assertRaises(base.EndTest):
+ tested.run()
+ self.assertEqual(submit.call_count, 1)
+
+
+if __name__ == '__main__':
+ unittest.main()
base-commit: 60348956fc7bad52a64e802f3e90f90c03ba8860
--
2.56.0
^ permalink raw reply related [flat|nested] only message in thread
only message in thread, other threads:[~2026-11-12 2:24 UTC | newest]
Thread overview: (only message) (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-12 2:24 [RFC PATCH] CheckPatch: accept required AI attribution diagnostics Kyle Pepper
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox