|
1 | 1 | #!/usr/bin/env -S uv run --script |
| 2 | +# |
| 3 | +# This file is open source software, licensed to you under the terms |
| 4 | +# of the Apache License, Version 2.0 (the "License"). See the NOTICE file |
| 5 | +# distributed with this work for additional information regarding copyright |
| 6 | +# ownership. You may not use this file except in compliance with the License. |
| 7 | +# |
| 8 | +# You may obtain a copy of the License at |
| 9 | +# |
| 10 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | +# |
| 12 | +# Unless required by applicable law or agreed to in writing, |
| 13 | +# software distributed under the License is distributed on an |
| 14 | +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| 15 | +# KIND, either express or implied. See the License for the |
| 16 | +# specific language governing permissions and limitations |
| 17 | +# under the License. |
| 18 | +# |
| 19 | +# Copyright (C) 2026 Redpanda Data. |
| 20 | +# |
2 | 21 | # /// script |
3 | 22 | # requires-python = ">=3.10" |
4 | 23 | # dependencies = [ |
5 | 24 | # "allpairspy", |
| 25 | +# "ruamel.yaml", |
6 | 26 | # ] |
7 | 27 | # /// |
8 | 28 | """ |
9 | 29 | Generate the regular_test include: matrix for tests.yaml. |
10 | 30 |
|
11 | | -Uses all-pairs (pairwise) coverage over compiler x standard x mode for |
12 | | -the regular builds, plus an explicit list of special-purpose jobs |
13 | | -(dpdk, cxx-modules, fuzz). All-pairs guarantees that every pair of |
14 | | -parameter values is exercised by at least one job, with far fewer |
15 | | -combinations than the full cartesian product (12 vs 24 for our 4x2x3 |
16 | | -matrix). Everything outside the marked region of tests.yaml is left |
17 | | -untouched. |
| 31 | +Uses all-pairs (pairwise) coverage over compiler x standard x mode x |
| 32 | +arch for the regular builds, plus an explicit list of special-purpose |
| 33 | +jobs (dpdk, cxx-modules, fuzz). All-pairs guarantees that every pair |
| 34 | +of parameter values is exercised by at least one job, with far fewer |
| 35 | +combinations than the full cartesian product. Excluded pairs (see |
| 36 | +EXCLUDED_PAIRS) drop out of regular coverage. The file is round-tripped |
| 37 | +through ruamel.yaml, replacing only the strategy matrix include list; |
| 38 | +everything else in tests.yaml is left untouched. |
18 | 39 |
|
19 | 40 | Usage: |
20 | 41 | .github/workflows/gen-matrix.py |
21 | 42 | git diff .github/workflows/tests.yaml |
22 | 43 | """ |
23 | 44 |
|
24 | | -from __future__ import annotations |
25 | | - |
26 | 45 | import pathlib |
27 | 46 | from typing import Any |
28 | 47 |
|
29 | 48 | from allpairspy import AllPairs |
| 49 | +from ruamel.yaml import YAML |
| 50 | +from ruamel.yaml.comments import CommentedMap, CommentedSeq |
30 | 51 |
|
31 | 52 | HERE = pathlib.Path(__file__).parent |
32 | 53 | TESTS_YAML = HERE / "tests.yaml" |
|
42 | 63 | + [f"g++-{v}" for v in GCC_VERSIONS] |
43 | 64 | ) |
44 | 65 | STANDARDS: list[int] = [20, 23] |
45 | | -MODES: list[str] = ["debug", "release"] |
| 66 | +MODES: list[str] = ["debug", "release", "sanitize"] |
46 | 67 | ARCHS: list[str] = ["x86", "arm"] |
47 | 68 |
|
48 | | -# Markers that bracket the managed region inside tests.yaml. |
49 | | -BEGIN_TAG = "# This include block is AUTOGENERATED" |
50 | | -END_TAG = "# end of AUTOGENERATED part" |
| 69 | +# Compiler+mode combinations to skip in the regular all-pairs matrix. |
| 70 | +# gcc 15+ miscompiles structured bindings inside loops in coroutines |
| 71 | +# (https://gcc.gnu.org/bugzilla/show_bug.cgi?id=124584), breaking g++ |
| 72 | +# debug (LSan: pollable_fd_state leak via cross-shard accept) and g++ |
| 73 | +# sanitize (UBSan abort on poisoned rcv_buf in rpc loops). Tracked in |
| 74 | +# scylladb/seastar#3431. |
| 75 | +EXCLUDED_PAIRS: list[tuple[str, str]] = [ |
| 76 | + (f"g++-{v}", m) for v in GCC_VERSIONS for m in ("debug", "sanitize") |
| 77 | +] |
| 78 | + |
| 79 | + |
| 80 | +def _keep_row(row: list[Any]) -> bool: |
| 81 | + """allpairspy filter_func: drop rows hitting an EXCLUDED_PAIRS entry. |
| 82 | +
|
| 83 | + Called with progressively longer prefixes during pair generation, so |
| 84 | + the check only fires once both compiler (row[0]) and mode (row[2]) |
| 85 | + are present in the partial row. |
| 86 | + """ |
| 87 | + return not (len(row) >= 3 and (row[0], row[2]) in EXCLUDED_PAIRS) |
| 88 | + |
| 89 | +# Key path to the strategy matrix map inside tests.yaml; we replace its |
| 90 | +# "include" list. The "# AUTOGENERATED" comment is preserved automatically |
| 91 | +# on round-trip. |
| 92 | +MATRIX_PATH = ("jobs", "regular_test", "strategy", "matrix") |
51 | 93 |
|
52 | 94 | # Special jobs merged into the same matrix so tests.yaml stays a single job. |
53 | 95 | # Items only set enable-ccache when overriding the default (test.yaml treats |
|
86 | 128 | }, |
87 | 129 | ] |
88 | 130 |
|
89 | | -# Characters that require a value to be double-quoted in YAML flow style. |
90 | | -_NEEDS_QUOTE: frozenset[str] = frozenset(' ,:{}"[]#') |
| 131 | +def _build_include(items: list[dict[str, Any]]) -> CommentedSeq: |
| 132 | + """Build the matrix include list as a block sequence of flow mappings. |
91 | 133 |
|
92 | | - |
93 | | -def _yaml_val(v: object) -> str: |
94 | | - if isinstance(v, bool): |
95 | | - return "true" if v else "false" |
96 | | - if isinstance(v, int): |
97 | | - return str(v) |
98 | | - s = str(v) |
99 | | - if any(c in s for c in _NEEDS_QUOTE): |
100 | | - return '"' + s.replace("\\", "\\\\").replace('"', '\\"') + '"' |
101 | | - return s |
102 | | - |
103 | | - |
104 | | -def _format_item(item: dict[str, Any], indent: str) -> str: |
105 | | - parts = ", ".join(f"{k}: {_yaml_val(v)}" for k, v in item.items()) |
106 | | - return f"{indent} - {{{parts}}}\n" |
| 134 | + Each item becomes a flow-style mapping (one job per line) so the block |
| 135 | + stays compact and diff-friendly; ruamel.yaml handles scalar quoting. |
| 136 | + """ |
| 137 | + seq = CommentedSeq() |
| 138 | + for item in items: |
| 139 | + mapping = CommentedMap(item) |
| 140 | + mapping.fa.set_flow_style() |
| 141 | + seq.append(mapping) |
| 142 | + return seq |
107 | 143 |
|
108 | 144 |
|
109 | 145 | def generate() -> list[dict[str, Any]]: |
110 | 146 | regular: list[dict[str, Any]] = [ |
111 | 147 | {"compiler": c, "standard": s, "mode": m, "arch": a} |
112 | | - for c, s, m, a in AllPairs([COMPILERS, STANDARDS, MODES, ARCHS]) |
| 148 | + for c, s, m, a in AllPairs( |
| 149 | + [COMPILERS, STANDARDS, MODES, ARCHS], |
| 150 | + filter_func=_keep_row, |
| 151 | + ) |
113 | 152 | ] |
114 | 153 | return regular + SPECIAL_ITEMS |
115 | 154 |
|
116 | 155 |
|
117 | 156 | def main() -> None: |
118 | 157 | items = generate() |
119 | | - lines: list[str] = TESTS_YAML.read_text().splitlines(keepends=True) |
120 | 158 |
|
121 | | - begin_idx = next(i for i, line in enumerate(lines) if BEGIN_TAG in line) |
122 | | - end_idx = next(i for i, line in enumerate(lines) if END_TAG in line) |
| 159 | + yaml = YAML() |
| 160 | + yaml.preserve_quotes = True |
| 161 | + yaml.width = 1 << 20 # never wrap a flow mapping across lines |
| 162 | + yaml.indent(mapping=2, sequence=4, offset=2) |
123 | 163 |
|
124 | | - first = lines[begin_idx] |
125 | | - indent = " " * (len(first) - len(first.lstrip())) |
| 164 | + data = yaml.load(TESTS_YAML) |
| 165 | + matrix = data |
| 166 | + for key in MATRIX_PATH: |
| 167 | + matrix = matrix[key] |
126 | 168 |
|
127 | | - new_lines: list[str] = [ |
128 | | - f"{indent}{BEGIN_TAG}, do not edit by hand\n", |
129 | | - f"{indent}# instead edit + re-run .github/workflows/gen-matrix.py\n", |
130 | | - f"{indent}include:\n", |
131 | | - ] |
132 | | - for item in items: |
133 | | - new_lines.append(_format_item(item, indent)) |
| 169 | + # We only swap out the include list. The "# AUTOGENERATED, do not edit" |
| 170 | + # comment is attached to the include key, not the list, so it is |
| 171 | + # preserved automatically on round-trip. |
| 172 | + matrix["include"] = _build_include(items) |
134 | 173 |
|
135 | | - TESTS_YAML.write_text("".join(lines[:begin_idx] + new_lines + lines[end_idx:])) |
| 174 | + yaml.dump(data, TESTS_YAML) |
136 | 175 | print(f"Updated {TESTS_YAML}: {len(items)} matrix items") |
137 | 176 |
|
138 | 177 |
|
|
0 commit comments