Skip to content

Commit a9c8005

Browse files
committed
Merge FEATURE-ci-native-receipt-line-endings: Accept native receipt line endings
2 parents 47c6f89 + ad633c6 commit a9c8005

3 files changed

Lines changed: 203 additions & 0 deletions

File tree

‎scripts/lib/package_matrix.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ def suites(inventory, platform, tls):
151151

152152

153153
def suite_result(output, status):
154+
output = output.replace('\r\n', '\n')
154155
tests = re.findall(r'^(\d+) tests executed$', output, re.M)
155156
checks = re.findall(r'^(\d+) checks$', output, re.M)
156157
failures = re.findall(r'^-> (\d+) failures$', output, re.M)
@@ -197,6 +198,7 @@ def check_tls_witnesses(output, backend):
197198
def configuration_results(output, inventory, loop="managed", *, tls):
198199
if tls not in ('yes', 'no') or loop not in ('managed', 'external'):
199200
raise ValueError('invalid configuration lane')
201+
output = output.replace('\r\n', '\n')
200202
rows = re.findall(r'^PACKAGE_CASE (\w+) (\w+) (bound|unbound)$', output, re.M)
201203
results = {}
202204
for name, outcome, binding in rows:
@@ -229,6 +231,7 @@ def parity_observation_counts(output):
229231
def supported_configuration_results(output, tls):
230232
if tls not in ('yes', 'no'):
231233
raise ValueError('invalid supported configuration lane')
234+
output = output.replace('\r\n', '\n')
232235
rows = re.findall(r'^PACKAGE_SUPPORTED (\w+) (\w+) (bound|unbound)$', output, re.M)
233236
expected = ('ok', 'bound') if tls == 'yes' else ('not_supported', 'unbound')
234237
if len(rows) != 3 or {row[0] for row in rows} != {'tls', 'h2', 'h3'} or any(row[1:] != expected for row in rows):

‎scripts/test-v3-installed-consumer.py‎

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,23 @@ def test_selected_shell_preserves_real_bash_compiler(self):
588588

589589

590590
class MatrixTest(unittest.TestCase):
591+
# Windows run 40170157075/job 121099184812, artifact 12590541497:
592+
# matrix/tls-no/consumer/shared-managed-process.json, exit 0.
593+
# Literal stderr bytes; SHA256 f6407bb289875ad13aa92722b35f883bf36cf87815533c5b666d0cdbe7d5e17e.
594+
native_configuration_stderr = (
595+
b'PACKAGE_PROVIDER \r\n'
596+
b'PACKAGE_CASE plaintext ok bound\r\n'
597+
b'PACKAGE_CASE invalid_listener invalid_argument unbound\r\n'
598+
b'PACKAGE_CASE invalid_workers invalid_argument unbound\r\n'
599+
b'PACKAGE_CASE invalid_timeout invalid_argument unbound\r\n'
600+
b'PACKAGE_CASE invalid_budget invalid_argument unbound\r\n'
601+
b'PACKAGE_CASE provider_profile_mismatch invalid_argument unbound\r\n'
602+
b'PACKAGE_CASE tls_listener not_supported unbound\r\n'
603+
b'PACKAGE_CASE http2 not_supported unbound\r\n'
604+
b'PACKAGE_CASE http3 invalid_argument unbound\r\n'
605+
b'PACKAGE_CASE mixed_rejection invalid_argument unbound\r\n'
606+
)
607+
591608
def setUp(self):
592609
self.inventory = matrix.load()
593610
self.temp = tempfile.TemporaryDirectory()
@@ -709,6 +726,103 @@ def test_empty_skipped_failed_suite_is_rejected(self):
709726
with self.assertRaises(ValueError):
710727
matrix.suite_result(output, status)
711728

729+
def test_native_suite_records_accept_lf_crlf_and_mixed_endings(self):
730+
lines = ('Running test (1): first', 'Running test (2): second',
731+
'2 tests executed', '9 checks', '-> 9 successes',
732+
'-> 0 failures', '-> 0 skipped')
733+
expected = dict(tests=2, checks=9, failures=0, skipped=0, case_names=['first', 'second'])
734+
for endings in (('\n',), ('\r\n',), ('\n', '\r\n')):
735+
output = ''.join(line + endings[i % len(endings)] for i, line in enumerate(lines))
736+
with self.subTest(endings=endings):
737+
self.assertEqual(expected, matrix.suite_result(output, 0))
738+
739+
def test_native_suite_records_keep_failure_count_and_witness_requirements(self):
740+
good = 'Running test (1): first\n1 tests executed\n9 checks\n-> 0 failures\n-> 0 skipped\n'
741+
bad = ('', good.replace('Running test (1): first\n', ''),
742+
good + 'Running test (2): extra\n', good.replace('1 tests', '2 tests'),
743+
good.replace('1 tests', '0 tests'), good.replace('9 checks', '0 checks'),
744+
good.replace('-> 0 failures\n', ''), good.replace('-> 0 skipped\n', ''),
745+
good.replace('9 checks\n', ''), good.replace('0 failures', '1 failures'),
746+
good.replace('0 skipped', '1 skipped'))
747+
for ending in ('\n', '\r\n'):
748+
for output, status in [(text, 0) for text in bad] + [(good, 1), (good, 77)]:
749+
with self.subTest(output=output, status=status, ending=ending), self.assertRaises(ValueError):
750+
matrix.suite_result(output.replace('\n', ending), status)
751+
for terminator in ('\r', ' \n', '\t\n', ' junk\n', '\r\r\n'):
752+
for line in ('Running test (1): first', '1 tests executed', '9 checks',
753+
'-> 0 failures', '-> 0 skipped'):
754+
with self.subTest(terminator=terminator, line=line), self.assertRaises(ValueError):
755+
matrix.suite_result(good.replace(line + '\n', line + terminator), 0)
756+
757+
def test_captured_windows_configuration_stderr_preserves_all_ten_outcomes(self):
758+
raw = self.native_configuration_stderr
759+
self.assertEqual('f6407bb289875ad13aa92722b35f883bf36cf87815533c5b666d0cdbe7d5e17e',
760+
hashlib.sha256(raw).hexdigest())
761+
output = raw.decode('ascii')
762+
expected = matrix.configuration_results(output.replace('\r\n', '\n'), self.inventory, tls='no')
763+
self.assertEqual(10, len(expected))
764+
self.assertEqual(expected, matrix.configuration_results(output, self.inventory, tls='no'))
765+
self.assertEqual(raw, output.encode('ascii'))
766+
767+
def test_configuration_records_accept_mixed_endings_in_each_lane(self):
768+
lf = self.native_configuration_stderr.decode('ascii').replace('\r\n', '\n')
769+
for tls in ('no', 'yes'):
770+
output = lf if tls == 'no' else lf.replace('not_supported', 'invalid_argument')
771+
for loop in ('managed', 'external'):
772+
expected = matrix.configuration_results(output, self.inventory, loop, tls=tls)
773+
mixed = output.replace(' unbound\n', ' unbound\r\n')
774+
for native in (output, output.replace('\n', '\r\n'), mixed):
775+
with self.subTest(tls=tls, loop=loop, output=native):
776+
self.assertEqual(expected, matrix.configuration_results(native, self.inventory, loop, tls=tls))
777+
778+
def test_configuration_records_keep_inventory_outcome_binding_and_lane_requirements(self):
779+
lf = self.native_configuration_stderr.decode('ascii').replace('\r\n', '\n')
780+
for tls in ('no', 'yes'):
781+
good = lf if tls == 'no' else lf.replace('not_supported', 'invalid_argument')
782+
bad = (good.replace('PACKAGE_CASE http3', 'MISSING http3'),
783+
good + 'PACKAGE_CASE plaintext ok bound\n',
784+
good + 'PACKAGE_CASE unexpected invalid_argument unbound\n',
785+
good.replace('plaintext ok bound', 'plaintext invalid_argument unbound'),
786+
good.replace('plaintext ok bound', 'plaintext ok unbound'),
787+
good.replace('plaintext ok bound', 'plaintext ok invalid'))
788+
for loop in ('managed', 'external'):
789+
for ending in ('\n', '\r\n'):
790+
for output in bad:
791+
with self.subTest(tls=tls, loop=loop, ending=ending, output=output), self.assertRaises(ValueError):
792+
matrix.configuration_results(output.replace('\n', ending), self.inventory, loop, tls=tls)
793+
for invalid_tls, invalid_loop in (('invalid', 'managed'), (tls, 'invalid')):
794+
with self.subTest(tls=invalid_tls, loop=invalid_loop), self.assertRaises(ValueError):
795+
matrix.configuration_results(good, self.inventory, invalid_loop, tls=invalid_tls)
796+
for terminator in ('\r', ' \n', '\t\n', ' junk\n', '\r\r\n'):
797+
with self.subTest(terminator=terminator), self.assertRaises(ValueError):
798+
matrix.configuration_results(lf.replace('ok bound\n', 'ok bound' + terminator), self.inventory, tls='no')
799+
800+
def test_supported_configuration_records_accept_lf_crlf_and_mixed_endings(self):
801+
for tls, outcome, binding in (('yes', 'ok', 'bound'), ('no', 'not_supported', 'unbound')):
802+
expected = {name: dict(outcome=outcome, binding=binding) for name in ('tls', 'h2', 'h3')}
803+
for endings in (('\n',), ('\r\n',), ('\n', '\r\n')):
804+
output = ''.join(f'PACKAGE_SUPPORTED {name} {outcome} {binding}' + endings[i % len(endings)]
805+
for i, name in enumerate(('tls', 'h2', 'h3')))
806+
with self.subTest(tls=tls, endings=endings):
807+
self.assertEqual(expected, matrix.supported_configuration_results(output, tls))
808+
809+
def test_supported_configuration_records_keep_strict_inventory_and_outcomes(self):
810+
for tls, outcome, binding in (('yes', 'ok', 'bound'), ('no', 'not_supported', 'unbound')):
811+
good = ''.join(f'PACKAGE_SUPPORTED {name} {outcome} {binding}\n' for name in ('tls', 'h2', 'h3'))
812+
bad = ('', good.replace('PACKAGE_SUPPORTED h3', 'MISSING h3'),
813+
good + good, good.replace('h3 ', 'unexpected '), good.replace('h3 ', 'h2 '),
814+
good.replace(outcome, 'invalid_argument'), good.replace(binding, 'invalid'),
815+
good.replace(' ' + binding + '\n', ' ' + ('unbound' if binding == 'bound' else 'bound') + '\n'))
816+
for ending in ('\n', '\r\n'):
817+
for output in bad:
818+
with self.subTest(tls=tls, ending=ending, output=output), self.assertRaises(ValueError):
819+
matrix.supported_configuration_results(output.replace('\n', ending), tls)
820+
with self.assertRaises(ValueError):
821+
matrix.supported_configuration_results(good, 'invalid')
822+
for terminator in ('\r', ' \n', '\t\n', ' junk\n', '\r\r\n'):
823+
with self.subTest(tls=tls, terminator=terminator), self.assertRaises(ValueError):
824+
matrix.supported_configuration_results(good.replace(binding + '\n', binding + terminator), tls)
825+
712826
def test_shared_suite_requires_installed_library_and_static_forbids_it(self):
713827
prefix = self.root / 'prefix'
714828
consumer = {'id': str(self.root / 'consumer')}
@@ -1298,6 +1412,21 @@ def setUp(self):
12981412
def command(self, code):
12991413
return [sys.executable, '-u', '-c', code]
13001414

1415+
def test_crlf_configuration_parser_preserves_captured_process_receipt(self):
1416+
stderr = MatrixTest.native_configuration_stderr
1417+
command = self.command(f"import os; os.write(2, {stderr!r}); os.write(1, b'1234\\r\\n'); input()")
1418+
path = self.root / 'crlf.json'
1419+
with process.Consumer(command, self.root, None, path, timeout=2) as child:
1420+
self.assertEqual(1234, child.port())
1421+
child.stop()
1422+
raw = path.read_bytes()
1423+
receipt = json.loads(raw)
1424+
self.assertEqual(stderr, receipt['stderr'].encode('ascii'))
1425+
self.assertEqual('1234\r\n', receipt['stdout'])
1426+
self.assertEqual(0, receipt['exit'])
1427+
self.assertEqual(10, len(matrix.configuration_results(receipt['stderr'], matrix.load(), tls='no')))
1428+
self.assertEqual(raw, path.read_bytes())
1429+
13011430
def test_bounded_startup_and_shutdown_with_concurrent_stderr(self):
13021431
command = self.command("import sys; sys.stderr.write('x'*10000); print('1234', flush=True); input(); print('done')")
13031432
with process.Consumer(command, self.root, None, self.root / 'pass.json', timeout=2) as child:
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# FEATURE-ci-native-receipt-line-endings
2+
3+
Status: Complete (local validation/finalization; native CI pending).
4+
Base: local v3 `47c6f89a6402e1d4cf71623772c44faa23be2a58`.
5+
Plan: primary `.groundwork-plans/FEATURE-ci-native-receipt-line-endings-plan.md`.
6+
Journal: primary `.debug/ci-47c-windows-configuration-5x995i1u/journal.md`.
7+
8+
Accept LF and native CRLF records in the three line-oriented package matrix
9+
readers: configuration_results, supported_configuration_results and suite_result.
10+
Preserve raw receipt/log bytes and all existing lane, outcome, inventory, count,
11+
case witness, skip and exit requirements. Do not change parity JSON or global
12+
process, socket or HTTP handling.
13+
14+
- [x] Read accepted plan, owning readers, native writers/callers and actual receipt; isolate from exact v3 base.
15+
- [x] Record meaningful RED with actual captured native stderr through the real parser.
16+
- [x] Implement component-local LF/CRLF handling and strict positive/negative regressions.
17+
- [x] Run affected Matrix/receipt checks, Python compilation and diff checks.
18+
- [x] Caller Groundwork validation/finalization.
19+
- [ ] Native Windows post-fix package/archive checks: CI/v3 PR owned, incomplete; not a local acceptance gate.
20+
21+
Actual Windows package run 40170157075 / MinGW job 121099184812 produced
22+
artifact 12590541497 (SHA256
23+
`87ebf648957f8f3e473c1e04bec15e1a07a0a6cd0b66cfe1a94895aafffeb3fc`).
24+
Its native TLS-off managed process exits zero and emits all ten expected
25+
PACKAGE_CASE rows with CRLF. Exact captured stderr fails the old parser;
26+
changing only CRLF to LF preserves all rows/outcomes and passes. Archive job
27+
121099305201 reports the same configuration failure; no second artifact download.
28+
29+
Only focused local data/parser and receipt checks are authorized. The two
30+
configure timestamp checks remain failed/incomplete/closed and unretried;
31+
epoll's exhausted 64-comparison batch and closed adapter/header/outbox/kqueue
32+
uncertainties remain unchanged. Newly approved native kqueue capture is queued
33+
after this repair/finalization, with 0/64 comparisons used; this task does not
34+
implement or execute it. Other 47c CI remains useful and untouched.
35+
36+
Local implementation evidence (2026-10-25):
37+
38+
The captured 528-byte stderr literal has SHA256
39+
`f6407bb289875ad13aa92722b35f883bf36cf87815533c5b666d0cdbe7d5e17e`.
40+
First RED: LF-only conversion parses all ten original outcomes, while exact
41+
native input raises `configuration case inventory incomplete`. Expanded RED
42+
also catches CRLF/mixed endings in supported configurations and native suite
43+
records, including actual Consumer capture from a local Python byte writer.
44+
These are local parser/fixture results, not new native Windows execution.
45+
46+
Each reader now replaces only CRLF with LF in its local immutable string.
47+
Original regexes and subsequent validation remain unchanged. Positives cover
48+
LF/CRLF/mixed endings and both TLS/loop lanes. Negatives retain missing,
49+
duplicate/unexpected case, invalid outcome/binding/lane, zero/inconsistent suite
50+
counts, missing witnesses, failed/skipped/nonzero-exit, bare CR, trailing
51+
whitespace and junk rejection. The actual Consumer regression verifies raw
52+
stderr/stdout and the retained process JSON remain unchanged by parsing.
53+
54+
Focused verification: MatrixTest + ProcessTest, 28 tests passed; existing
55+
Commands success/allowed-failure and nonzero-exit receipt tests, 2 passed.
56+
Changed Python files compile and `git diff --check` passes. No documented
57+
Python linter exists. No full suite, SDK/native build, Windows probe, CI poll,
58+
rerun or CI-control action was performed. Primary implementation/ contains
59+
RED/GREEN logs, source hashes and `actual-receipt-green.json`, which compares
60+
the repaired parser against original LF-only outcomes while preserving the
61+
740-byte native JSON receipt (SHA256
62+
`fd27b6db1cc254d9b97ac60416f4f278a74671fd66ed3f5a086a183ed833eba8`).
63+
Native Windows post-fix CI remains incomplete and CI/v3 PR owned.
64+
65+
Groundwork validation `03844849b08d4718dd3c9faba1f32bee` sealed PASS for
66+
`20007a4563e78cd142506c57c185f19bb7f648d3` / tree
67+
`9f62c12d36b127e28a17e1a36f7ddbe611e185e8`: one iteration, nine fresh
68+
independent approvals, zero fixes and zero unworked findings. Source-bound
69+
28 Matrix/Process, 2 Commands, syntax and whitespace checks reused; no report
70+
created. Native post-fix gates remain incomplete and CI/v3 PR owned. Root owns
71+
merge, cleanup, exact-head publication and the queued approved native capture.

0 commit comments

Comments
 (0)