From da824c8938a50e31d02e0686416472a7e00ae2a3 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 18 Jun 2026 18:12:01 +0200 Subject: [PATCH 1/2] Use a (smart) set of branches for branch selection --- master/custom/branches.py | 63 ++++++++++++++++++++++------ master/custom/builders.py | 86 +++++++++++++++++++++++++++++--------- master/custom/factories.py | 13 ++++++ master/custom/workers.py | 55 ++++++++++++------------ master/master.cfg | 32 +++----------- 5 files changed, 165 insertions(+), 84 deletions(-) diff --git a/master/custom/branches.py b/master/custom/branches.py index 51bf527d..c89b7a66 100644 --- a/master/custom/branches.py +++ b/master/custom/branches.py @@ -14,6 +14,7 @@ """ +import collections.abc import dataclasses from functools import total_ordering from typing import Any @@ -64,13 +65,6 @@ def _maintenance_branch(major, minor, **kwargs): # need more time. result.monolithic_test_asyncio = True - if version_tuple < (3, 11): - # WASM wasn't a supported platform until 3.11. - result.wasm_tier = None - elif version_tuple < (3, 13): - # Tier 3 support is 3.11 & 3.12. - result.wasm_tier = 3 - if version_tuple < (3, 13): # Free-threaded builds are available since 3.13 result.gil_only = True @@ -96,7 +90,6 @@ class BranchInfo: # Defaults are for main (and PR), overrides are in _maintenance_branch. gil_only: bool = False monolithic_test_asyncio: bool = False - wasm_tier: int | None = 2 def __str__(self): return self.name @@ -108,6 +101,9 @@ def __eq__(self, other): return NotImplemented return self.sort_key == other.sort_key + def __hash__(self): + return hash(self.sort_key) + def __lt__(self, other): try: other_key = other.sort_key @@ -116,15 +112,58 @@ def __lt__(self, other): return self.sort_key < other.sort_key -BRANCHES = list(generate_branches()) +class BranchSet(collections.abc.Set): + """An immutable set of BranchInfo objects, with some convenience API""" + + def __init__(self, branches): + self._branches = tuple(branches) + + def __iter__(self): + return iter(self._branches) + + def __len__(self): + return len(self._branches) + + def __contains__(self, element): + return element in self._branches + + def __getitem__(self, version_tuple): + """branchset[3, x] -> BranchInfo for 3.x""" + for branch in self._branches: + if branch.version_tuple == version_tuple: + return branch + raise LookupError(f'version {version_tuple} not found') + + def only_since(self, major, minor, include_pr=True): + """only_since(3, x) -> BranchSet with 3.x and later""" + return BranchSet( + b for b in self._branches if ( + include_pr if b.is_pr + else b.version_tuple >= (major, minor) + ) + ) + + def only_until(self, major, minor, include_pr=False): + """only_since(3, x) -> BranchSet with up to (and including) 3.x""" + return BranchSet( + b for b in self._branches if ( + include_pr if b.is_pr + else b.version_tuple <= (major, minor) + ) + ) + + +BRANCHES = BranchSet(generate_branches()) +[MAIN_BRANCH] = [b for b in BRANCHES if b.is_main] +[PR_BRANCH] = [b for b in BRANCHES if b.is_pr] -# Verify that we've defined these in sort order -assert BRANCHES == sorted(BRANCHES) +# Verify that the (sort) keys are distinct +assert len(set(BRANCHES)) == len(list(BRANCHES)) if __name__ == "__main__": # Print a table to the terminal cols = [[f.name + ':' for f in dataclasses.fields(BranchInfo)]] - for branch in BRANCHES: + for branch in sorted(BRANCHES): cols.append([repr(val) for val in dataclasses.astuple(branch)]) column_sizes = [max(len(val) for val in col) for col in cols] column_sizes[-2] += 2 # PR is special, offset it a bit diff --git a/master/custom/builders.py b/master/custom/builders.py index dbb0047f..a13b18c5 100644 --- a/master/custom/builders.py +++ b/master/custom/builders.py @@ -4,6 +4,7 @@ from functools import cached_property from custom import factories +from custom.branches import BRANCHES, MAIN_BRANCH, PR_BRANCH from custom.factories import ( UnixBuild, UnixPerfBuild, @@ -89,11 +90,18 @@ class BuilderDef: tags: frozenset[str] worker_name: str - def __init__(self, name, factory, *, tags, worker_name): + def __init__( + self, name, factory, + *, + tags, + worker_name, + branches=BRANCHES, + ): self.name = name self.factory = factory self.worker_name = worker_name self.tags = frozenset(tags) + self.branches = branches @cached_property def tier(self): @@ -131,10 +139,19 @@ def get_tier_from_tags(tags): ), ] -def generate_builderdefs(tags, tuples): +def generate_builderdefs(tags, entries): tags = frozenset(tags) - for name, worker_name, factory in tuples: - yield BuilderDef(name, factory, tags=tags, worker_name=worker_name) + for entry in entries: + if isinstance(entry, BuilderDef): + if not (entry.tags <= tags): + raise ValueError( + f'{entry} is in the wrong generate_builderdefs call; ' + + 'move it to the main BUILDER_DEFS list above', + ) + yield entry + else: + name, worker_name, factory = entry + yield BuilderDef(name, factory, tags=tags, worker_name=worker_name) # -- Stable Tier-1 builder ---------------------------------------------- @@ -163,9 +180,27 @@ def generate_builderdefs(tags, tuples): ("AMD64 Windows11 Non-Debug", "ware-win11", Windows64ReleaseBuild), ("AMD64 Windows11 Refleaks", "ware-win11", Windows64RefleakBuild), ("AMD64 Windows Server 2022 NoGIL", "itamaro-win64-srv-22-aws", Windows64NoGilBuild), - ("AMD64 Windows PGO Tailcall", "itamaro-win64-srv-22-aws", Windows64PGOTailcallBuild), - ("AMD64 Windows PGO NoGIL", "itamaro-win64-srv-22-aws", Windows64PGONoGilBuild), - ("AMD64 Windows PGO NoGIL Tailcall", "itamaro-win64-srv-22-aws", Windows64PGONoGilTailcallBuild), + BuilderDef( + "AMD64 Windows PGO Tailcall", + Windows64PGOTailcallBuild, + tags={STABLE, TIER_1}, + worker_name="itamaro-win64-srv-22-aws", + branches={MAIN_BRANCH, PR_BRANCH}, + ), + BuilderDef( + "AMD64 Windows PGO NoGIL", + Windows64PGONoGilBuild, + tags={STABLE, TIER_1}, + worker_name="itamaro-win64-srv-22-aws", + branches={MAIN_BRANCH, PR_BRANCH}, + ), + BuilderDef( + "AMD64 Windows PGO NoGIL Tailcall", + Windows64PGONoGilTailcallBuild, + tags={STABLE, TIER_1}, + worker_name="itamaro-win64-srv-22-aws", + branches={MAIN_BRANCH, PR_BRANCH}, + ), ])) @@ -290,7 +325,13 @@ def generate_builderdefs(tags, tuples): ("AMD64 Arch Linux Asan", "pablogsal-arch-x86_64", UnixAsanBuild), ("AMD64 Arch Linux Asan Debug", "pablogsal-arch-x86_64", UnixAsanDebugBuild), ("AMD64 Arch Linux TraceRefs", "pablogsal-arch-x86_64", UnixTraceRefsBuild), - ("AMD64 Arch Linux Perf", "pablogsal-arch-x86_64", UnixPerfBuild), + BuilderDef( + "AMD64 Arch Linux Perf", + UnixPerfBuild, + tags={STABLE}, + worker_name="pablogsal-arch-x86_64", + branches={MAIN_BRANCH, PR_BRANCH}, + ), # UBSAN with -fno-sanitize=function, without which we currently fail (as # tracked in gh-111178). The full "AMD64 Arch Linux Usan" is unstable, below ("AMD64 Arch Linux Usan Function", "pablogsal-arch-x86_64", ClangUbsanFunctionLinuxBuild), @@ -319,11 +360,22 @@ def generate_builderdefs(tags, tuples): ("AMD64 CentOS9 FIPS Only Blake2 Builtin Hash", "cstratak-CentOS9-fips-x86_64", CentOS9NoBuiltinHashesUnixBuildExceptBlake2), ("AMD64 CentOS9 FIPS No Builtin Hashes", "cstratak-CentOS9-fips-x86_64", CentOS9NoBuiltinHashesUnixBuild), - - ("AMD64 Arch Linux Valgrind", "pablogsal-arch-x86_64", ValgrindBuild), + BuilderDef( + "AMD64 Arch Linux Valgrind", + ValgrindBuild, + tags={UNSTABLE, TIER_1}, + worker_name="pablogsal-arch-x86_64", + branches={MAIN_BRANCH, PR_BRANCH}, + ), # Windows MSVC - ("AMD64 Windows PGO", "bolen-windows10", Windows64PGOBuild), + BuilderDef( + "AMD64 Windows PGO", + Windows64PGOBuild, + tags={UNSTABLE, TIER_1}, + worker_name="bolen-windows10", + branches={MAIN_BRANCH, PR_BRANCH}, + ), ])) @@ -439,15 +491,6 @@ def get_builder_defs(settings): return BUILDER_DEFS -# Match builder name (excluding the branch name) of builders that should only -# run on the main and PR branches. -ONLY_MAIN_BRANCH = ( - "Windows PGO", - "AMD64 Arch Linux Perf", - "AMD64 Arch Linux Valgrind", -) - - if __name__ == "__main__": # Print a list to the terminal import itertools @@ -471,3 +514,6 @@ def key(builder_def): print(f'{NAME}{d.name}{END}') print(f' {d.factory.__name__} on {d.worker_name}') print(f' [{' '.join(sorted(d.tags))}]') + if d.branches != BRANCHES: + branchnames = ', '.join(b.name for b in sorted(d.branches)) + print(f' branches: {branchnames}') diff --git a/master/custom/factories.py b/master/custom/factories.py index eb8cb166..5f385bf5 100644 --- a/master/custom/factories.py +++ b/master/custom/factories.py @@ -10,6 +10,7 @@ from buildbot.plugins import util from . import JUNIT_FILENAME +from .branches import BRANCHES from .steps import ( Test, Clean, @@ -57,6 +58,7 @@ class BaseBuild(factory.BuildFactory): test_timeout = TEST_TIMEOUT buildersuffix = "" tags = () + branches = BRANCHES def __init__(self, source, *, extra_tags=[], **kwargs): super().__init__([source]) @@ -899,6 +901,9 @@ class Wasm32WasiCrossBuild(UnixCrossBuild): host = "wasm32-unknown-wasi" host_configure_cmd = ["../../Tools/wasm/wasi-env", "../../configure"] + # See comment in _Wasm32WasiPreview1Build.__init__ + branches = {BRANCHES[3, 11], BRANCHES[3, 12]} + def setup(self, branch, worker, test_with_PTY=False, **kwargs): self.addStep( SetPropertyFromCommand( @@ -935,6 +940,14 @@ def __init__(self, source, *, extra_tags=[], **kwargs): if not self.pydebug: extra_tags.append("nondebug") self.buildersuffix += self.append_suffix + if self.pydebug: + # The debug the WASI buildbot is meant for 3.11 and 3.12 only. + # Don't use it on PRs; it's tier 3 only and getting it to + # work on PRs against `main` is too much work. + self.branches = {BRANCHES[3, 11], BRANCHES[3, 12]} + else: + # The non-debug buildbot is meant for 3.13+, where WASM is tier 2 + self.branches = BRANCHES.only_since(3, 13) super().__init__(source, extra_tags=extra_tags, **kwargs) def setup(self, branch, worker, test_with_PTY=False, **kwargs): diff --git a/master/custom/workers.py b/master/custom/workers.py index 806cff61..a2cbfedd 100644 --- a/master/custom/workers.py +++ b/master/custom/workers.py @@ -9,6 +9,7 @@ from buildbot.plugins import worker as _worker from custom.worker_downtime import no_builds_between +from custom.branches import BRANCHES, MAIN_BRANCH, PR_BRANCH # List of workers. # See also: Buildbot worker documentation, http://docs.buildbot.net/current/manual/configuration/workers.html#defining-workers @@ -32,8 +33,7 @@ def __init__( settings, name, tags=None, - branches=None, - not_branches=None, + branches=BRANCHES, parallel_builders=1, parallel_tests=None, timeout_factor=1, @@ -44,13 +44,16 @@ def __init__( self.name = name self.tags = tags or set() self.branches = branches - self.not_branches = not_branches self.parallel_tests = parallel_tests self.timeout_factor = timeout_factor self.exclude_test_resources = exclude_test_resources or [] self.downtime = downtime self.git_options = git_options or {} + for branch in branches: + if isinstance(branch, str): + raise TypeError('use BRANCHES for branch filtering') + worker_settings = settings.workers[name] owner = name.split("-")[0] owner_settings = settings.owners[owner] @@ -112,14 +115,14 @@ def get_workers(settings): name="cstratak-RHEL8-x86_64", tags=['linux', 'unix', 'rhel', 'amd64', 'x86-64'], parallel_tests=10, - branches=['3.10', '3.11', '3.12'], + branches=BRANCHES.only_until(3, 12), ), cpw( name="cstratak-RHEL8-fips-x86_64", tags=['linux', 'unix', 'rhel', 'amd64', 'x86-64', 'fips'], parallel_tests=6, # Only 3.12 for RHEL8 FIPS builder - branches=['3.12'], + branches={BRANCHES[3, 12]}, ), cpw( name="cstratak-CentOS9-x86_64", @@ -131,7 +134,7 @@ def get_workers(settings): tags=['linux', 'unix', 'rhel', 'amd64', 'x86-64', 'fips'], parallel_tests=6, # Only 3.12+ for FIPS builder - not_branches=["3.10", "3.11"], + branches=BRANCHES.only_since(3, 12), ), cpw( name="cstratak-fedora-rawhide-ppc64le", @@ -149,7 +152,7 @@ def get_workers(settings): name="cstratak-RHEL8-ppc64le", tags=['linux', 'unix', 'rhel', 'ppc64le'], parallel_tests=10, - branches=['3.10', '3.11', '3.12'], + branches=BRANCHES.only_until(3, 12), timeout_factor=2, # Increase the timeout on this slow worker ), cpw( @@ -172,7 +175,7 @@ def get_workers(settings): name="cstratak-RHEL8-aarch64", tags=['linux', 'unix', 'rhel', 'arm', 'arm64', 'aarch64'], parallel_tests=32, - branches=['3.10', '3.11', '3.12'], + branches=BRANCHES.only_until(3, 12), ), cpw( name="cstratak-CentOS9-aarch64", @@ -187,7 +190,7 @@ def get_workers(settings): cpw( name="diegorusso-aarch64-bigmem", tags=['linux', 'unix', 'ubuntu', 'arm', 'arm64', 'aarch64', 'bigmem'], - branches=['3.x'], + branches={MAIN_BRANCH, PR_BRANCH}, parallel_tests=8, # This worker runs pyperformance for speed.python.org at 12am UTC. # The pyperformance run lasts less than 2h. @@ -211,7 +214,7 @@ def get_workers(settings): name="cstratak-rhel8-s390x", tags=['linux', 'unix', 'rhel', 's390x'], parallel_tests=10, - branches=['3.10', '3.11', '3.12'], + branches=BRANCHES.only_until(3, 12), ), cpw( name="cstratak-rhel9-s390x", @@ -228,8 +231,8 @@ def get_workers(settings): name="stan-aarch64-ubuntu", tags=['linux', 'unix', 'ubuntu', 'arm', 'arm64', 'aarch64'], parallel_tests=4, - # test_xpickle doesn't exist on these branches - not_branches=['3.12', '3.11', '3.10'], + # test_xpickle was added in 3.13 + branches=BRANCHES.only_since(3, 13), ), cpw( name="stan-raspbian", @@ -237,7 +240,7 @@ def get_workers(settings): 'aarch64', 'arm'], parallel_tests=4, # Tests fail with latin1 encoding on 3.12, probably earlier - not_branches=['3.12', '3.11', '3.10'], + branches=BRANCHES.only_since(3, 13), # Problematic ISP causes issues connecting to testpython.net exclude_test_resources=['urlfetch', 'network'], ), @@ -279,7 +282,7 @@ def get_workers(settings): cpw( name="ware-alpine", tags=['linux', 'unix', 'alpine', 'docker', 'amd64', 'x86-64', 'musl'], - not_branches=['3.10', '3.11', '3.12', '3.13'], + branches=BRANCHES.only_since(3, 14), parallel_tests=6, ), cpw( @@ -311,7 +314,7 @@ def get_workers(settings): name="ware-win11-arm64", tags=['windows', 'win11', 'arm64'], parallel_tests=8, - not_branches=['3.10', '3.11', '3.12'], + branches=BRANCHES.only_since(3, 13), git_options=dict( # Do a full shallow clone for every build mode="full", @@ -324,7 +327,7 @@ def get_workers(settings): cpw( name="bcannon-wasi", tags=['wasm', 'wasi'], - not_branches=['3.10'], + branches=BRANCHES.only_since(3, 11), parallel_tests=2, parallel_builders=2, timeout_factor=2, # Increase the timeout on this slow worker @@ -332,7 +335,7 @@ def get_workers(settings): cpw( name="itamaro-centos-aws", tags=['linux', 'unix', 'rhel', 'amd64', 'x86-64'], - not_branches=['3.10', '3.11', '3.12', '3.13'], + branches=BRANCHES.only_since(3, 14), parallel_tests=10, parallel_builders=2, downtime=itamaro_downtime, @@ -340,7 +343,7 @@ def get_workers(settings): cpw( name="itamaro-win64-srv-22-aws", tags=['windows', 'win-srv-22', 'amd64', 'x86-64'], - not_branches=['3.10', '3.11', '3.12', '3.13'], + branches=BRANCHES.only_since(3, 14), parallel_tests=20, # Parallel MSBuild builds are "unusual", and more likely to hit obscure bugs # (such as file locking issues across builds) @@ -352,50 +355,50 @@ def get_workers(settings): cpw( name="itamaro-macos-intel-aws", tags=['macOS', 'unix', 'amd64', 'x86-64'], - not_branches=['3.10', '3.11', '3.12', '3.13'], + branches=BRANCHES.only_since(3, 14), parallel_tests=10, ), cpw( name="itamaro-macos-arm64-aws", tags=['macOS', 'unix', 'arm', 'arm64'], - not_branches=['3.10', '3.11', '3.12', '3.13'], + branches=BRANCHES.only_since(3, 14), parallel_tests=10, ), cpw( name="kushaldas-wasi", tags=['wasm', 'wasi'], - not_branches=['3.10'], + branches=BRANCHES.only_since(3, 11), parallel_tests=4, parallel_builders=2, ), cpw( name="onder-riscv64", tags=['linux', 'unix', 'ubuntu', 'riscv64'], - not_branches=['3.10'], + branches=BRANCHES.only_since(3, 11), parallel_tests=4, ), cpw( name="rkm-arm64-ios-simulator", tags=['iOS'], - not_branches=['3.10', '3.11', '3.12'], + branches=BRANCHES.only_since(3, 13), parallel_builders=1, # All builds use the same simulator ), cpw( name="rkm-emscripten", tags=['emscripten'], - not_branches=['3.10', '3.11', '3.12', '3.13'], + branches=BRANCHES.only_since(3, 14), parallel_builders=4, ), cpw( name="mhsmith-android-aarch64", tags=['android'], - not_branches=['3.10', '3.11', '3.12'], + branches=BRANCHES.only_since(3, 13), parallel_builders=1, # All builds use the same emulator and app ID. ), cpw( name="mhsmith-android-x86_64", tags=['android'], - not_branches=['3.10', '3.11', '3.12'], + branches=BRANCHES.only_since(3, 13), parallel_builders=1, # All builds use the same emulator and app ID. ), cpw( diff --git a/master/master.cfg b/master/master.cfg index 444bfc83..a634e20e 100644 --- a/master/master.cfg +++ b/master/master.cfg @@ -48,7 +48,6 @@ from custom.release_dashboard import get_release_status_app # noqa: E402 from custom.builders import ( # noqa: E402 get_builder_defs, STABLE, - ONLY_MAIN_BRANCH, ) from custom.branches import BRANCHES @@ -180,20 +179,13 @@ for branch in BRANCHES: refleakbuildernames = [] stable_builder_names = [] for builder_def in get_builder_defs(settings): - if any( - pattern in builder_def.name for pattern in ONLY_MAIN_BRANCH - ) and not branch.is_main and not branch.is_pr: - # Workers known to be broken on older branches: let's focus on - # supporting these platforms in the main branch. + if branch not in builder_def.branches: continue worker = workers_by_name[builder_def.worker_name] - if not branch.is_pr: - if worker.not_branches and branch.name in worker.not_branches: - continue - if worker.branches and branch.name not in worker.branches: - continue + if branch not in worker.branches: + continue buildername = builder_def.name + " " + branch.name @@ -231,22 +223,10 @@ for branch in BRANCHES: branch=branch, worker=worker, ) - tags = [branch.builder_tag, *builder_def.tags, *f.tags] + if branch not in f.branches: + continue - # Tiers for WebAssembly builds - if "wasm" in tags: - if branch.wasm_tier is None: - continue - elif branch.is_pr: - # Don't use the WASI buildbot meant for 3.11 and 3.12 on PRs; - # it's tier 3 only and getting it to work on PRs against `main` - # is too much work. - if "nondebug" in tags: - continue - elif "nondebug" in tags and branch.wasm_tier == 2: - continue - elif "nondebug" not in tags and branch.wasm_tier == 3: - continue + tags = [branch.builder_tag, *builder_def.tags, *f.tags] if 'nogil' in tags and branch.gil_only: continue From a7de7b9c20d7dc17e6f6209c0c550cc4f31714c0 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 3 Jul 2026 08:08:10 +0200 Subject: [PATCH 2/2] Apply suggestion from code review Co-authored-by: Zachary Ware --- master/custom/factories.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/master/custom/factories.py b/master/custom/factories.py index 5f385bf5..76872c3d 100644 --- a/master/custom/factories.py +++ b/master/custom/factories.py @@ -941,7 +941,7 @@ def __init__(self, source, *, extra_tags=[], **kwargs): extra_tags.append("nondebug") self.buildersuffix += self.append_suffix if self.pydebug: - # The debug the WASI buildbot is meant for 3.11 and 3.12 only. + # The debug WASI buildbot is meant for 3.11 and 3.12 only. # Don't use it on PRs; it's tier 3 only and getting it to # work on PRs against `main` is too much work. self.branches = {BRANCHES[3, 11], BRANCHES[3, 12]}