Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
102 commits
Select commit Hold shift + click to select a range
c80b61c
util/helper: Allow setting environment and cwd
sjg20 May 17, 2024
b290f03
util/helper: Change command output level to debug
sjg20 May 18, 2024
51e1de6
util: Add ~/.lgrc support for site-wide labgrid defaults
sjg20 Feb 21, 2026
70ec00d
helper: Support long-running processes
sjg20 May 23, 2024
64f97d8
Provide variables to control drivers from the command line
sjg20 May 16, 2024
ee2fa73
driver: Add driver for Dediprog SPI-flash emulator
sjg20 May 16, 2024
321d3f9
protocol: Add a new recovery protocol
sjg20 May 17, 2024
87be88b
protocol: Allow more control of reset
sjg20 May 18, 2024
352589e
labgrid: Add a way to query information from a driver
sjg20 May 21, 2024
5fd7268
remote/client: Support finding a place by role
sjg20 May 21, 2024
fd3b596
driver/qemudriver: Make extra_args, cpu and machine optional
sjg20 Jul 27, 2024
18ac2bd
driver/qemudriver: Delay setting of the QEMU arguments
sjg20 Jul 27, 2024
8aa279f
driver/qemudriver: Allow the BIOS filename to be changed
sjg20 Jul 27, 2024
6359e20
driver/qemudriver: Report an error if QEMU is not turned on
sjg20 Aug 28, 2024
b21aaf5
driver/qemudriver: Support using kvm
sjg20 Feb 21, 2025
b4d2f47
driver/qemudriver: Add support for disk images and writer arguments
sjg20 Aug 12, 2025
785d3ea
driver: Add a digital-output driver for recovery
sjg20 May 17, 2024
f53ccb7
driver: Add a driver for the Servo board
sjg20 May 16, 2024
7f35328
driver: Add a way to build U-Boot images
sjg20 May 16, 2024
075ce3f
driver: Provide a driver for writing U-Boot to a board
sjg20 May 16, 2024
38762ee
Add a driver for a device which is always powered
sjg20 May 16, 2024
00a8703
Correct binding for HIDRelayDriver
sjg20 May 16, 2024
0e0a9b6
bootstrapprotocol: Support a boot phase
sjg20 May 18, 2024
adb7d14
driver/usbloader: Add a USB loader for sunxi / Allwinner
sjg20 May 16, 2024
e6805ad
driver/usbloader: Add a loader for Tegra
sjg20 May 16, 2024
56d085d
driver/usbloader: Add a loader for samsung
sjg20 May 16, 2024
f751c1d
resource/udev: Support iMX8x variant
sjg20 Feb 21, 2025
17a1e42
driver/usbloader: Allow setting the monitor-path with UUU
sjg20 Feb 21, 2025
07cd357
driver/usbstoragedriver: Allow block size in write_image()
sjg20 May 16, 2024
5ada2f5
driver/usbstoragedriver: Allow block count in write_image()
sjg20 May 16, 2024
aabb032
driver/usbstoragedriver: Adjust how sync works
sjg20 May 16, 2024
154fc79
driver/usbstoragedriver: Write silently
sjg20 May 16, 2024
df19411
driver/usbstoragedriver: Report time taken to write
sjg20 May 16, 2024
3d950fb
driver/usbstoragedriver: Detect read-only devices
sjg20 Feb 21, 2025
b7fd53f
exporter: Export control_serial for USBSDWireDevice
sjg20 Apr 14, 2026
43e04c2
driver: Add support for the Badgerd SDWire mux
sjg20 Apr 3, 2026
0e5a797
usbhidrelay: Support generating a recovery signal
sjg20 May 18, 2024
9cb59a5
remote/exporter: Reduce the verbosity
sjg20 May 18, 2024
02cb8f7
remote/exporter: Reduce the verbosity further
sjg20 May 18, 2024
cf3ad8d
remote/exporter: Indicate when the exporter is ready
sjg20 May 18, 2024
79e8216
remote/client: Provide an option to acquire a place
sjg20 May 18, 2024
51b66d6
remote/client: Using logging for progress output
sjg20 May 18, 2024
a68aa08
remote/client: Show the result of a failed command
sjg20 May 18, 2024
32adeef
remote/client: Allow releasing a target silently
sjg20 Jun 10, 2024
19b0e38
remote/client: Fix environemnt typo
sjg20 Jun 10, 2024
e3b7670
remote/client: Use logging when acquiring / releasing
sjg20 May 21, 2024
733d2bc
remote/client: Flush the console device only when needed
sjg20 Jun 10, 2024
fb7a638
remote/client: Move initial-state code into a function
sjg20 Jun 10, 2024
3d32719
remote/client: Provide a way to set the strategy end-state
sjg20 Jun 10, 2024
b14db57
remote/client: Split _check_allowed() into two methods
sjg20 Feb 21, 2025
39f244f
remote/client: Move terminal handling into a separate file
sjg20 Jul 27, 2024
9bdff19
remote/client: Provide an internal console
sjg20 Jun 10, 2024
5dffafb
remote/client: Simplify the top-level logic
sjg20 Feb 21, 2025
be3c7b4
remote/client: Provide functions to acquire/release a place
sjg20 Jul 23, 2025
c428b98
remote/client: Allow sending the main log to a file
sjg20 Jul 23, 2025
9d3eaf3
remote/client: Allow showing console output on exit
sjg20 Jul 23, 2025
cf1fbab
remote/client: Using logging for role selection
sjg20 May 18, 2024
c9382bd
remote/client: Add timestamp to the log-file format
sjg20 Feb 21, 2026
ca703bb
remote/client: Add automatic file logging via LG_LOG_DIR
sjg20 Feb 21, 2026
827e0f8
pytestplugin: Allow selecting a particular target
sjg20 May 17, 2024
7b472e2
pytestplugin: Allow setting variables
sjg20 May 17, 2024
652e828
pytestplugin: Allow connecting to a running board
sjg20 May 17, 2024
131fd74
pytestplugin: Allow writing the console to a file
sjg20 May 17, 2024
ea057b2
pytestplugin: Allow sending Labgrid logs to a file
sjg20 May 17, 2024
0454c49
pytestplugin: Add automatic file logging via LG_LOG_DIR
sjg20 Feb 21, 2026
45281a6
ssh: Avoid output when running rsync
sjg20 May 18, 2024
f914c11
target: Add documentation
sjg20 May 18, 2024
bdbf35e
target: Add documentation for get_driver()
sjg20 May 18, 2024
7b85182
target: Allow looking for an optional driver
sjg20 May 18, 2024
1f8fde6
ubootdriver: Support collecting the version string
sjg20 Jun 10, 2024
3328ed0
ubootdriver: Provide a way to collect output
sjg20 Jun 10, 2024
a6fbcce
ubootdriver: Allow connecting to a running board
sjg20 Jun 10, 2024
702a401
driver/buttondriver: Add a driver for buttons
sjg20 Feb 21, 2025
d4204bb
pyproject: Drop license from classifiers
sjg20 Jul 23, 2025
bae30aa
strategy: Show the U-Boot version when booting
sjg20 May 18, 2024
f715cb0
strategy: Add a new state to start U-Boot
sjg20 May 18, 2024
2d4614a
strategy: Allow a board to be bootstrapped
sjg20 May 18, 2024
145346f
strategy: Allow sending U-Boot over USB
sjg20 Apr 13, 2024
8b30855
strategy: Show console output on failure
sjg20 May 21, 2024
feb0e1c
strategy: Support boards which need a recovery button
sjg20 May 21, 2024
52ebe65
strategy: Support lab mode
sjg20 Jun 10, 2024
f77aef0
strategy: Support boards which need power for console
sjg20 Feb 21, 2025
141bcaf
strategy: Support boards which share power and reset
sjg20 Feb 21, 2025
0f3757f
strategy: Support boards which cannot power on with reset
sjg20 Feb 21, 2025
7d02519
strategy: Support boards which need a button to power on
sjg20 Feb 21, 2025
ee5f215
strategy: Move flash and reset code into their own functions
sjg20 Feb 21, 2025
eaffc01
strategy: Provide a way to restart U-Boot
sjg20 Feb 21, 2025
ff0a7ff
strategy: Extract _prepare_for_send() from _reset_for_send()
sjg20 Feb 21, 2026
8478a1d
strategy: Wait for USB resources instead of fixed sleep
sjg20 Feb 21, 2026
3cbf09b
strategy: Reopen the console when power-cycling for send
sjg20 Feb 21, 2026
74fc717
strategy: Add power_off_delay for USB recovery mode
sjg20 Feb 21, 2026
98c85b4
contrib: Provide some useful scripts for U-Boot
sjg20 May 19, 2024
6b9c6b6
Add an example lab
sjg20 Aug 28, 2024
cd996ae
doc: Add documentation for U-Boot integration [v3]
sjg20 May 21, 2024
5f8b0c9
pytestplugin: Handle unacquired places in broken-strategy check
sjg20 Apr 12, 2026
f323123
exporter: Add USB hub abstraction for simpler device matching
sjg20 Apr 20, 2026
021835e
driver: UBootWriterDriver: Add qemu-efi-iso method
sjg20 Apr 20, 2026
60aa818
driver: UBootWriterDriver: Allow overriding the cmdline in qemu-efi-iso
sjg20 Apr 21, 2026
8dbaf54
driver: QEMUDriver: Detect QEMU death and capture stderr
sjg20 Apr 28, 2026
b421ca3
protocol: Pass acquiring user from coordinator to exporter
sjg20 May 3, 2026
2503b67
exporter: Optionally log all serial traffic via ser2net trace
sjg20 May 3, 2026
7aaac5a
contrib: Document lightweight read-only lab access
sjg20 May 3, 2026
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
1 change: 1 addition & 0 deletions contrib/u-boot/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console_*
47 changes: 47 additions & 0 deletions contrib/u-boot/_ub-bisect-try
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/bin/bash

# ** INTERNAL SCRIPT - do not use directly - use ub-bisect instead **
# Boot U-Boot on a particular target and run the smoke test
# This is intended to be used by 'git bisect run'
# The special return codes 129 and 130 are returned when there is an internal
# failure, unrelated to the target

# Requires variables to eb set up by ub-bisect

mydir=$(dirname $0)

commit="$1"

echo "Revision $(git rev-parse HEAD), target ${target}"

if [ -n "${commit}" ]; then
if ! git cherry-pick "${commit}"; then
echo "cherry-pick failed"
exit 129
fi
fi

cd ${mydir}

if ! labgrid-client -p ${target} acquire; then
exit 1
fi

ret=0
pytest --lg-env "${LG_ENV}" --lg-coordinator="${LG_COORDINATOR}" \
--lg-log --lg-colored-steps --lg-target ${target} \
${lg_vars} -q \
-k test_uboot_smoke || ret=$?

if ! labgrid-client -p ${target} release; then
exit $?
fi

if [ -n "${commit}" ]; then
if ! git reset --hard HEAD~; then
echo "reset failed"
exit 130
fi
fi

exit $ret
245 changes: 245 additions & 0 deletions contrib/u-boot/check_lab.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
#!/usr/bin/env python3
"""Check sjg-lab board reliability across recent GitLab pipelines."""

import argparse
import configparser
import concurrent.futures
import json
import os
import re
import sys
import urllib.parse
import urllib.request
from collections import defaultdict

sys.path.insert(0, os.path.join(os.environ['HOME'], 'u', 'tools'))
from u_boot_pylib import tout

BASE = 'https://concept.u-boot.org'
PROJECT = 'u-boot/u-boot'
API = f'{BASE}/api/v4/projects/{urllib.parse.quote(PROJECT, safe="")}'

# Number of parallel requests
NUM_WORKERS = 8


def get_token():
conf = configparser.ConfigParser()
conf.read(os.path.expanduser('~/.config/pickman.conf'))
return conf.get('gitlab', 'token')


TOKEN = get_token()


def api_get(path):
"""Fetch JSON from the GitLab API."""
url = f'{API}{path}'
req = urllib.request.Request(url)
req.add_header('PRIVATE-TOKEN', TOKEN)
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read())


def get_job_log(job_id):
"""Get the last 50 lines of a job's log."""
url = f'{API}/jobs/{job_id}/trace'
req = urllib.request.Request(url)
req.add_header('PRIVATE-TOKEN', TOKEN)
with urllib.request.urlopen(req, timeout=30) as resp:
text = resp.read().decode('utf-8', errors='replace')
text = re.sub(r'\x1b\[[0-9;]*m', '', text)
lines = text.strip().split('\n')
return '\n'.join(lines[-50:])


def fetch_pipeline_jobs(pipe):
"""Fetch sjg-lab jobs for a single pipeline.

Returns:
list of (pipeline_id, job_dict) tuples
"""
pid = pipe['id']
if pipe['status'] == 'skipped':
return []

results = []
page = 1
while True:
jobs = api_get(
f'/pipelines/{pid}/jobs?per_page=100&page={page}')
if not jobs:
break
for job in jobs:
if (job.get('stage') == 'sjg-lab'
and job['status'] not in ('manual', 'canceled')):
results.append((pid, job))
if len(jobs) < 100:
break
page += 1
return results


def get_sjg_jobs(pipelines):
"""Get sjg-lab jobs for all non-skipped pipelines in parallel.

Returns:
list of (pipeline_id, job_dict) tuples, ordered by pipeline ID
descending.
"""
all_results = []
total = len(pipelines)
done = 0

with concurrent.futures.ThreadPoolExecutor(
max_workers=NUM_WORKERS) as pool:
futures = {pool.submit(fetch_pipeline_jobs, pipe): pipe
for pipe in pipelines}
for future in concurrent.futures.as_completed(futures):
done += 1
pipe = futures[future]
tout.progress(
f'Pipeline {pipe["id"]} ({done}/{total})')
all_results.extend(future.result())
tout.clear_progress()

all_results.sort(key=lambda x: -x[0])
return all_results


def show_summary(sjg_jobs, num_pipelines, board_filter):
"""Show the board reliability summary table."""
board_results = defaultdict(list)
pipeline_ids = set()

for pid, job in sjg_jobs:
name = job['name']
if board_filter and name != board_filter:
continue
board_results[name].append((pid, job['status'], job))
pipeline_ids.add(pid)

print(f'\nChecked {len(pipeline_ids)} pipelines with sjg-lab jobs '
f'(out of {num_pipelines} total)\n')

# Collect all statuses across all boards for column headers
all_statuses = set()
board_data = {}
for board, results in sorted(board_results.items()):
counts = defaultdict(int)
for _, s, _ in results:
counts[s] += 1
all_statuses.update(counts.keys())
total = len(results)
failed = counts.get('failed', 0)
fail_pct = 100 * failed / total if total else 0
board_data[board] = (fail_pct, total, counts, results)

# Order statuses: success first, failed second, then the rest sorted
status_order = []
for s in ['success', 'failed']:
if s in all_statuses:
status_order.append(s)
all_statuses.discard(s)
status_order.extend(sorted(all_statuses))

# Short labels for column headers
status_labels = {
'success': 'Pass', 'failed': 'Fail', 'running': 'Run',
'pending': 'Pend', 'created': 'Crtd', 'canceled': 'Canc',
'skipped': 'Skip', 'manual': 'Man',
}

summary = []
for board in sorted(board_data):
fail_pct, total, counts, results = board_data[board]
summary.append((fail_pct, board, total, counts, results))
summary.sort(key=lambda x: (-x[0], x[1]))

hdr_cols = ''.join(
f' {status_labels.get(s, s[:4].title()):>5}' for s in status_order)
print(f'{"Board":<20} {"Runs":>5}{hdr_cols} {"Fail%":>6}')
print('-' * (33 + 6 * len(status_order)))
for fail_pct, board, total, counts, results in summary:
cols = ''.join(
f' {counts.get(s, 0):>5}' for s in status_order)
marker = ' ***' if fail_pct > 0 else ''
print(f'{board:<20} {total:>5}{cols} {fail_pct:>5.1f}%{marker}')

print()
for fail_pct, board, total, counts, results in summary:
if counts.get('failed', 0) > 0:
fail_pipes = [str(pid) for pid, s, _ in results if s == 'failed']
print(f'{board}: failed in pipelines {", ".join(fail_pipes)}')

return summary


def show_detail(sjg_jobs, board_filter):
"""Show failure logs for failed sjg-lab jobs."""
failed_jobs = [(pid, job) for pid, job in sjg_jobs
if job['status'] == 'failed'
and (not board_filter or job['name'] == board_filter)]
if not failed_jobs:
return

# Fetch logs in parallel
def fetch_one(pid_job):
pid, job = pid_job
try:
log = get_job_log(job['id'])
except Exception as e:
log = f' (could not fetch log: {e})'
return pid, job, log

done = 0
total = len(failed_jobs)
results = []

with concurrent.futures.ThreadPoolExecutor(
max_workers=NUM_WORKERS) as pool:
futures = {pool.submit(fetch_one, pj): pj for pj in failed_jobs}
for future in concurrent.futures.as_completed(futures):
done += 1
pid, job = futures[future]
tout.progress(
f'Fetching log for {job["name"]} ({done}/{total})')
results.append(future.result())
tout.clear_progress()

# Print in pipeline-ID order
results.sort(key=lambda x: -x[0])
for pid, job, log in results:
print(f'\n{"=" * 70}')
print(f'Board: {job["name"]} Pipeline: {pid} Job: {job["id"]}')
print(f'URL: {job["web_url"]}')
print(f'{"=" * 70}')
print(log)


def main():
parser = argparse.ArgumentParser(
description='Check sjg-lab board reliability')
parser.add_argument('-d', '--detail', action='store_true',
help='show failure logs')
parser.add_argument('-b', '--board',
help='filter to a specific board')
parser.add_argument('-n', '--num', type=int, default=20,
help='number of pipelines to check (default 20)')
args = parser.parse_args()

tout.init(tout.INFO)

tout.progress('Reading pipeline list')
pipelines = api_get(
f'/pipelines?per_page={args.num}&order_by=id&sort=desc')
tout.clear_progress()

sjg_jobs = get_sjg_jobs(pipelines)
show_summary(sjg_jobs, len(pipelines), args.board)
if args.detail:
show_detail(sjg_jobs, args.board)


if __name__ == '__main__':
main()
42 changes: 42 additions & 0 deletions contrib/u-boot/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from pexpect.exceptions import TIMEOUT
import pytest
import subprocess
import sys

def maybe_write_log(request, target, output=None):
clog = request.config.option.lg_console_logfile
if clog:
print(f"\nWriting console output to {clog}...")

if output is None:
console = target.get_active_driver('ConsoleProtocol')
output = console.read_output()
with open(clog, 'wb') as f:
f.write(output)
print(f"Successfully wrote {len(output)} bytes to {clog}")


@pytest.fixture
def u_boot(request, target, strategy):
if request.config.option.lg_use_running_system:
strategy.uboot.assume_active()
target.activate(strategy.uboot)
else:
try:
strategy.transition("uboot")
except subprocess.CalledProcessError as exc:
# Show any build failures
print(f"Command failure: {' '.join(exc.cmd)}")
for line in exc.output.splitlines():
print(line.decode('utf-8'))
except TIMEOUT:
# Show any console output when things fail
console = target.get_active_driver('ConsoleProtocol')
output = console.read_output()
sys.stdout.buffer.write(output)
maybe_write_log(request, target, output)
raise

yield strategy.uboot

maybe_write_log(request, target)
Loading
Loading