Linux bluetooth development
 help / color / mirror / Atom feed
* [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