Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,11 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Check configuration
run: make check PIP=pip BUILDBOT=buildbot PYTHON_VERSION=${{ matrix.python-version }}
- name: Compare configuration reports
run: |
# Use the same formatter for both configurations, including older main revisions.
python -m utils.report -vvv > "$RUNNER_TEMP/branch_report"
git fetch --depth=1 origin main
git checkout FETCH_HEAD master/
python -m utils.report -vvv > "$RUNNER_TEMP/main_report"
git diff -U10 --color=always --no-index "$RUNNER_TEMP/main_report" "$RUNNER_TEMP/branch_report" || test "$?" -eq 1
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ PYTHON_VERSION=3.13
SYSTEM_PYTHON=python$(PYTHON_VERSION)
VENV_DIR=./venv
REQUIREMENTS=requirements-$(PYTHON_VERSION).txt
PYTHON=$(VENV_DIR)/bin/python
PIP=$(VENV_DIR)/bin/pip
BUILDBOT=$(VENV_DIR)/bin/buildbot
VENV_CHECK=$(VENV_DIR)/lib/python$(PYTHON_VERSION)/site-packages/buildbot/master.py
Expand Down Expand Up @@ -38,6 +39,12 @@ regen-requirements:
check: $(VENV_CHECK)
$(BUILDBOT) checkconfig master


## report [OPTS=...] Print out a summary of the current configuration
.PHONY: report
report: $(VENV_CHECK)
$(PYTHON) -m utils.report $(OPTS)

# Management targets

.PHONY: update-master start-master restart-master stop-master
Expand Down
146 changes: 146 additions & 0 deletions utils/report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import argparse
import os
import re
import shlex
import shutil
import sys
import textwrap
from collections.abc import Mapping
from pathlib import Path


_COLOR = sys.stdout.isatty() and 'NO_COLOR' not in os.environ
_WIDTH = shutil.get_terminal_size(fallback=(100, 24)).columns
_STYLES = {'bold': 1, 'cyan': 36, 'dim': 2}


def style(text, name):
if not _COLOR:
return text
return f'\033[{_STYLES[name]}m{text}\033[0m'


def format_value(value):
# Handle things that look like renderers to avoid memory addresses in output
if hasattr(value, 'fn'):
name = getattr(value.fn, '__qualname__', type(value.fn).__qualname__)
if value.args or value.kwargs:
return f'renderer({name}, args={value.args!r}, kwargs={value.kwargs!r})'
return f'renderer({name})'
return str(value)


def print_value(label, value, indent):
prefix = (' ' * indent + str(label)).ljust(indent + 12) + ' '
available_width = max(20, _WIDTH - len(prefix))
text = format_value(value)

lines = []
for paragraph in text.splitlines() or ['']:
wrapped = textwrap.wrap(
paragraph,
width=available_width,
break_long_words=False,
break_on_hyphens=False,
)
lines.extend(wrapped or [''])

print(style(prefix, 'cyan') + lines[0])
for line in lines[1:]:
print(' ' * len(prefix) + line)


def print_detail(label, value, indent=2):
heading = ' ' * indent + str(label)

if isinstance(value, Mapping) and value:
print(style(heading, 'cyan'))
for key, item in value.items():
print_detail(key, item, indent + 2)
return

if isinstance(value, (list, tuple)) and value:
if label == 'command':
command = shlex.join(format_value(argument) for argument in value)
print_value(label, command, indent)
elif all(isinstance(item, str) for item in value):
print_value(label, ', '.join(value), indent)
else:
print(style(heading, 'cyan'))
for item in value:
print_detail('-', item, indent + 2)
return

print_value(label, value, indent)


def report(config):
parser = argparse.ArgumentParser(description='List configured builders.')
parser.add_argument(
'-v', '--verbose', action='count', default=0,
help='show builder details; repeat to include build steps',
)
parser.add_argument(
'-f', '--filter',
help='Only report builders with a matching name',
)
args = parser.parse_args()

for builder in sorted(config['builders'], key=lambda b: b.name):
if not re.search(args.filter or '.*', builder.name, flags=re.IGNORECASE):
continue
print(style(builder.name, 'bold'))
if not args.verbose:
continue
print_detail('Factory', type(builder.factory).__name__)
for key in (
'workernames',
'builddir',
'tags',
'env',
'properties',
'defaultProperties',
'description',
):
value = getattr(builder, key)
if not value:
continue
if isinstance(value, list):
value = sorted(value)
print_detail(key, value)
if args.verbose > 1:
for step in builder.factory.steps:
print(f' {step.step_class.__name__}')
if step.args:
print_detail('args', step.args, indent=6)
# Show useful inherited defaults as well as explicit overrides.
details = {}
if args.verbose > 2:
for key in (
'command', 'env', 'timeout', 'maxTime',
'haltOnFailure', 'flunkOnFailure', 'warnOnFailure',
'alwaysRun',
):
value = getattr(step.step_class, key, None)
if value is not None:
details[key] = value
details.update(step.kwargs)
for key, value in details.items():
print_detail(key, value, indent=6)
print()

counts = [
f'{len(config[key])} {key}'
for key in ('builders', 'workers', 'schedulers')
]
totals = 'Totals: ' + ' · '.join(counts)
print(style(totals, 'dim'))


if __name__ == '__main__':
import runpy

root_dir = Path(__file__) / '..' / '..'
master_config = root_dir / 'master' / 'master.cfg'

report(runpy.run_path(master_config.resolve())['BuildmasterConfig'])
Loading