Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
2e8d75d
util/helper: Allow setting environment and cwd
sjg20 May 17, 2024
fc2e8d7
util/helper: Change command output level to debug
sjg20 May 18, 2024
86c4af9
util: Add ~/.lgrc support for site-wide labgrid defaults
sjg20 Feb 21, 2026
e241ec4
helper: Support long-running processes
sjg20 May 23, 2024
944617c
Provide variables to control drivers from the command line
sjg20 May 16, 2024
ae5c172
driver: Add driver for Dediprog SPI-flash emulator
sjg20 May 16, 2024
b1347bb
protocol: Add a new recovery protocol
sjg20 May 17, 2024
8a192f0
protocol: Allow more control of reset
sjg20 May 18, 2024
396bd1e
labgrid: Add a way to query information from a driver
sjg20 May 21, 2024
8e31bd0
remote/client: Support finding a place by role
sjg20 May 21, 2024
d82d3ea
driver/qemudriver: Make extra_args, cpu and machine optional
sjg20 Jul 27, 2024
dcd36fb
driver/qemudriver: Delay setting of the QEMU arguments
sjg20 Jul 27, 2024
2a45a06
driver/qemudriver: Allow the BIOS filename to be changed
sjg20 Jul 27, 2024
d657efa
driver/qemudriver: Report an error if QEMU is not turned on
sjg20 Aug 28, 2024
2904dc4
driver/qemudriver: Support using kvm
sjg20 Feb 21, 2025
87f9a85
driver/qemudriver: Add support for disk images and writer arguments
sjg20 Aug 12, 2025
f07c1b5
driver: Add a digital-output driver for recovery
sjg20 May 17, 2024
7ae5bf7
driver: Add a driver for the Servo board
sjg20 May 16, 2024
6c639a3
driver: Add a way to build U-Boot images
sjg20 May 16, 2024
abf36e8
driver: Provide a driver for writing U-Boot to a board
sjg20 May 16, 2024
10c9df0
Add a driver for a device which is always powered
sjg20 May 16, 2024
ed1147f
Correct binding for HIDRelayDriver
sjg20 May 16, 2024
136808b
bootstrapprotocol: Support a boot phase
sjg20 May 18, 2024
570b791
driver/usbloader: Add a USB loader for sunxi / Allwinner
sjg20 May 16, 2024
e7af160
driver/usbloader: Add a loader for Tegra
sjg20 May 16, 2024
c9ee194
driver/usbloader: Add a loader for samsung
sjg20 May 16, 2024
2b49f17
resource/udev: Support iMX8x variant
sjg20 Feb 21, 2025
5bda670
driver/usbloader: Allow setting the monitor-path with UUU
sjg20 Feb 21, 2025
071c4d1
driver/usbstoragedriver: Allow block size in write_image()
sjg20 May 16, 2024
389f3fc
driver/usbstoragedriver: Allow block count in write_image()
sjg20 May 16, 2024
bd844c6
driver/usbstoragedriver: Adjust how sync works
sjg20 May 16, 2024
7b94964
driver/usbstoragedriver: Write silently
sjg20 May 16, 2024
828e11c
driver/usbstoragedriver: Report time taken to write
sjg20 May 16, 2024
b802a9e
driver/usbstoragedriver: Detect read-only devices
sjg20 Feb 21, 2025
0127ba8
driver: Add support for the Badgerd SDWire mux
sjg20 Apr 3, 2026
0c1659f
usbhidrelay: Support generating a recovery signal
sjg20 May 18, 2024
9aab1ee
remote/exporter: Reduce the verbosity
sjg20 May 18, 2024
af68a09
remote/exporter: Reduce the verbosity further
sjg20 May 18, 2024
9479264
remote/exporter: Indicate when the exporter is ready
sjg20 May 18, 2024
20e33a9
remote/client: Provide an option to acquire a place
sjg20 May 18, 2024
4df80fd
remote/client: Using logging for progress output
sjg20 May 18, 2024
3b30eed
remote/client: Show the result of a failed command
sjg20 May 18, 2024
702c929
remote/client: Allow releasing a target silently
sjg20 Jun 10, 2024
f55a197
remote/client: Fix environemnt typo
sjg20 Jun 10, 2024
52a39c1
remote/client: Use logging when acquiring / releasing
sjg20 May 21, 2024
d018a1d
remote/client: Flush the console device only when needed
sjg20 Jun 10, 2024
1f13752
remote/client: Move initial-state code into a function
sjg20 Jun 10, 2024
a0e181a
remote/client: Provide a way to set the strategy end-state
sjg20 Jun 10, 2024
0fdfb24
remote/client: Split _check_allowed() into two methods
sjg20 Feb 21, 2025
75e957d
remote/client: Move terminal handling into a separate file
sjg20 Jul 27, 2024
6a45740
remote/client: Provide an internal console
sjg20 Jun 10, 2024
9fd2264
remote/client: Simplify the top-level logic
sjg20 Feb 21, 2025
6b0acd7
remote/client: Provide functions to acquire/release a place
sjg20 Jul 23, 2025
daa1b0b
remote/client: Allow sending the main log to a file
sjg20 Jul 23, 2025
f04f5bc
remote/client: Allow showing console output on exit
sjg20 Jul 23, 2025
c4bccf8
remote/client: Using logging for role selection
sjg20 May 18, 2024
9c83c40
remote/client: Add timestamp to the log-file format
sjg20 Feb 21, 2026
339aecc
remote/client: Add automatic file logging via LG_LOG_DIR
sjg20 Feb 21, 2026
b936f56
pytestplugin: Allow selecting a particular target
sjg20 May 17, 2024
44a2ca5
pytestplugin: Allow setting variables
sjg20 May 17, 2024
7c4fec7
pytestplugin: Allow connecting to a running board
sjg20 May 17, 2024
397ee71
pytestplugin: Allow writing the console to a file
sjg20 May 17, 2024
55f66f4
pytestplugin: Allow sending Labgrid logs to a file
sjg20 May 17, 2024
85cfdce
pytestplugin: Add automatic file logging via LG_LOG_DIR
sjg20 Feb 21, 2026
4f489c4
ssh: Avoid output when running rsync
sjg20 May 18, 2024
b50e72e
target: Add documentation
sjg20 May 18, 2024
287f605
target: Add documentation for get_driver()
sjg20 May 18, 2024
60fddef
target: Allow looking for an optional driver
sjg20 May 18, 2024
eef8a96
ubootdriver: Support collecting the version string
sjg20 Jun 10, 2024
815b02f
ubootdriver: Provide a way to collect output
sjg20 Jun 10, 2024
6e0ba4f
ubootdriver: Allow connecting to a running board
sjg20 Jun 10, 2024
fd87db2
driver/buttondriver: Add a driver for buttons
sjg20 Feb 21, 2025
ab29e76
pyproject: Drop license from classifiers
sjg20 Jul 23, 2025
6dd93de
strategy: Show the U-Boot version when booting
sjg20 May 18, 2024
092f737
strategy: Add a new state to start U-Boot
sjg20 May 18, 2024
3dc8882
strategy: Allow a board to be bootstrapped
sjg20 May 18, 2024
80c81b9
strategy: Allow sending U-Boot over USB
sjg20 Apr 13, 2024
399a51b
strategy: Show console output on failure
sjg20 May 21, 2024
c057720
strategy: Support boards which need a recovery button
sjg20 May 21, 2024
33eafea
strategy: Support lab mode
sjg20 Jun 10, 2024
1f546bb
strategy: Support boards which need power for console
sjg20 Feb 21, 2025
350e0dc
strategy: Support boards which share power and reset
sjg20 Feb 21, 2025
3859055
strategy: Support boards which cannot power on with reset
sjg20 Feb 21, 2025
7f1b14c
strategy: Support boards which need a button to power on
sjg20 Feb 21, 2025
2422c50
strategy: Move flash and reset code into their own functions
sjg20 Feb 21, 2025
d1b8303
strategy: Provide a way to restart U-Boot
sjg20 Feb 21, 2025
e062848
strategy: Extract _prepare_for_send() from _reset_for_send()
sjg20 Feb 21, 2026
dddfa68
strategy: Wait for USB resources instead of fixed sleep
sjg20 Feb 21, 2026
b601ee4
strategy: Reopen the console when power-cycling for send
sjg20 Feb 21, 2026
e9ff683
strategy: Add power_off_delay for USB recovery mode
sjg20 Feb 21, 2026
f02c16f
contrib: Provide some useful scripts for U-Boot
sjg20 May 19, 2024
e3b7760
Add an example lab
sjg20 Aug 28, 2024
b0f73a5
doc: Add documentation for U-Boot integration [v3]
sjg20 May 21, 2024
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