authorclover caruso <git@paperclover.net> 2026-10-12 08:16:52-07:00
committerclover caruso <git@paperclover.net> 2026-10-12 08:46:58-07:00
log464e096c972a1146718f05b4ede4dc49588eeae1
tree3a2fc15c6e44606772aef87c593f33661c35a211
parent24b46353a9b3963f2093a92d493d7cb5229416db
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU (~clover)

fix(ci): include target kind in test executable labels

Library and integration targets can share a package and name. Include Cargo target kind at discovery so each executable has its own log, result name and timing key; keep scheduling and status aggregation unchanged. Cover distinct library/integration/bin identities, independent failure evidence, package filtering and timing priorities without launching Cargo. Fixes #137 Assisted-by: gpt-6.1-sol

2 files changed, 75 insertions(+), 1 deletions(-)

tools/ci.py+2-1
...@@ -221,7 +221,8 @@ def run_tests(run, deadline, packages, jobs):...@@ -221,7 +221,8 @@ def run_tests(run, deadline, packages, jobs):
221 if message.get('reason') == 'compiler-artifact' and message['profile']['test'] and message['executable']:221 if message.get('reason') == 'compiler-artifact' and message['profile']['test'] and message['executable']:
222 folder = Path(message['manifest_path']).parent222 folder = Path(message['manifest_path']).parent
223 if packages is None or folder.name in packages:223 if packages is None or folder.name in packages:
224 binaries.append((f'{folder.name}.{message["target"]["name"]}', [message['executable']], folder))224 binaries.append((f'{folder.name}.{message["target"]["name"]}.'
225 f'{"-".join(message["target"]["kind"])}', [message['executable']], folder))
225 binaries.append(('doctests', ['cargo', 'test', '--locked', '--workspace', '--all-features', '--doc'], CI))226 binaries.append(('doctests', ['cargo', 'test', '--locked', '--workspace', '--all-features', '--doc'], CI))
226 # The slowest last time start first, so that none of them starts last.227 # The slowest last time start first, so that none of them starts last.
227 timings = RUNS / 'test-seconds.json'228 timings = RUNS / 'test-seconds.json'
tools/test_ci.py created+73
...@@ -0,0 +1,73 @@
1from importlib.util import module_from_spec, spec_from_file_location
2import json
3from pathlib import Path
4from tempfile import TemporaryDirectory
5import unittest
6from unittest.mock import patch
7
8
9spec = spec_from_file_location('ci', Path(__file__).with_name('ci.py'))
10ci = module_from_spec(spec)
11spec.loader.exec_module(ci)
12
13
14class TestExecutableIdentity(unittest.TestCase):
15 def test_same_named_library_and_integration_test_keep_separate_results(self):
16 with TemporaryDirectory() as temporary:
17 root = Path(temporary)
18 artifacts = []
19 for package, name, kind, executable in [
20 ('relay', 'relay', 'test', '/tests/relay-integration'),
21 ('relay', 'relay', 'lib', '/tests/relay-library'),
22 ('relay', 'snowbound-relay', 'bin', '/tests/relay-bin'),
23 ('other', 'other', 'lib', '/tests/other'),
24 ]:
25 artifacts.append(json.dumps({
26 'reason': 'compiler-artifact', 'profile': {'test': True},
27 'manifest_path': str(root / package / 'Cargo.toml'),
28 'target': {'name': name, 'kind': [kind]}, 'executable': executable,
29 }))
30
31 class RecordedRun:
32 folder = root
33 calls = []
34
35 def execute(self, command, log, deadline, **kwargs):
36 self.calls.append((command, log, kwargs.get('cwd')))
37 if log.name == 'test.log':
38 log.write_text('\n'.join(artifacts))
39 return 0
40 with log.open('a') as stream:
41 stream.write(' '.join(command) + '\n')
42 return 1 if command == ['/tests/relay-library'] else 0
43
44 (root / 'test-seconds.json').write_text(json.dumps({
45 'relay.relay.test': 10, 'relay.snowbound-relay.bin': 20, 'doctests': 1,
46 }))
47 run = RecordedRun()
48 with patch.object(ci, 'RUNS', root):
49 status, logs, parts = ci.run_tests(run, 1, ['relay'], 1)
50
51 by_name = {part['name']: part for part in parts}
52 self.assertEqual(status, 'failed')
53 self.assertEqual(set(by_name), {
54 'relay.relay.test', 'relay.relay.lib', 'relay.snowbound-relay.bin', 'doctests',
55 })
56 self.assertEqual(by_name['relay.relay.test']['status'], 'passed')
57 self.assertEqual(by_name['relay.relay.lib']['status'], 'failed')
58 self.assertEqual(len({part['log'] for part in parts}), 4)
59 self.assertEqual([path.name for path in logs], ['test.log', 'relay.relay.lib.log'])
60 self.assertEqual(Path(by_name['relay.relay.lib']['log']).read_text(), '/tests/relay-library\n')
61 self.assertEqual(Path(by_name['relay.relay.test']['log']).read_text(), '/tests/relay-integration\n')
62 self.assertEqual([call[0] for call in run.calls[1:4]], [
63 ['/tests/relay-library'], ['/tests/relay-bin'], ['/tests/relay-integration'],
64 ])
65 self.assertEqual([call[2] for call in run.calls[1:4]], [root / 'relay'] * 3)
66 self.assertFalse(any(call[0] == ['/tests/other'] for call in run.calls))
67 timings = json.loads((root / 'test-seconds.json').read_text())
68 self.assertIn('relay.relay.lib', timings)
69 self.assertIn('relay.relay.test', timings)
70
71
72if __name__ == '__main__':
73 unittest.main()