Files
qdl/tests/test_hil.sh
Igor Opaniuk 84e8e387e5 tests: compare partition digests case-insensitively
The verify step extracts the device digest with a case-insensitive
grep but compares it case-sensitively against the local sha256sum
output. The firehose response carries uppercase hex while sha256sum
prints lowercase, so the comparison only worked as long as the
extraction happened to pick a lowercase rendering of the digest from
qdl's output. With QDL_HIL_DEBUG enabled the first match in the
output is the device's raw uppercase digest, and the step failed on
identical values - caught by the first debug-enabled CI run.

Lowercase both sides before comparing.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-08-25 09:32:07 +02:00

442 lines
17 KiB
Bash
Executable File

#!/bin/bash
# SPDX-License-Identifier: BSD-3-Clause
#
# Hardware-in-the-loop (HIL) integration test steps.
#
# Each step is registered as its own meson test in the "hil" suite (see
# tests/meson.build) and invoked as:
#
# test_hil.sh --builddir <dir> --step <name>
#
# The steps drive the built qdl binary against a real device in EDL mode, so
# they are not meant for unattended CI. Run them explicitly with:
#
# QDL_HIL_BUILD=/path/to/build QDL_HIL_STORAGE=ufs \
# meson test -C build --suite hil
#
# When the required environment is not set a step exits 77, which meson
# reports as SKIP, so a plain "meson test" stays green without hardware.
#
# Required environment:
# QDL_HIL_BUILD Build to flash. Auto-detected: a directory uses classic
# flashing (prog_firehose_*.elf + rawprogram*.xml +
# patch*.xml); a file (.json/.xml/.zip) uses "qdl flash".
# QDL_HIL_STORAGE Target storage type: emmc|nand|nvme|spinor|ufs.
#
# Optional environment:
# QDL_HIL_PROGRAMMER Firehose programmer .elf (needed by the read/write/
# erase/sha256/reset steps). Defaults to the first
# prog_firehose_*.elf next to QDL_HIL_BUILD.
# QDL_HIL_PARTITION Partition for the read/erase/write steps. The EFI
# system partition is small and quick to read and write,
# so it defaults to "efi".
# QDL_HIL_IMAGE Image the erase/write steps restore. Defaults to the
# data the read step reads back, so erase/write
# round-trip the partition's own content.
# QDL_HIL_SERIAL -S serial, to select one of several attached devices.
# QDL_HIL_ENTER_EDL_CMD Command run before the suite's first step to put
# the board into EDL mode (e.g. "adb reboot edl" or a
# USB relay toggle), for unattended/CI runs. When
# unset the board must already be in EDL mode.
# QDL_HIL_SETTLE Seconds the list step waits for the device to
# enumerate after the enter-EDL command (default 30).
# QDL_HIL_DEBUG When set, run every qdl invocation with --debug so
# failures leave a full protocol log in the meson
# testlog (and the uploaded CI artifacts).
# QDL_HIL_CONTENTS contents.xml (optionally with ::specifier) for the
# create-zip step; when unset that step is skipped.
# QDL_HIL_SPARSE flashmap/contents/zip that flashes sparse images, for
# the sparse-flash step; when unset that step is skipped.
#
# Every step runs with --skip-reset to keep the programmer alive across
# invocations; only the final step ends the session with the reset verb.
set -e
SKIP=77
while [[ $# -gt 0 ]]; do
case "$1" in
--builddir) builddir="$2"; shift 2 ;;
--step) step="$2"; shift 2 ;;
*) echo "Unknown option: $1" >&2; exit 1 ;;
esac
done
[[ -n "${builddir}" ]] || { echo "Error: --builddir is required." >&2; exit 1; }
[[ -n "${step}" ]] || { echo "Error: --step is required." >&2; exit 1; }
uname_out="$(uname -s)"
case "${uname_out}" in
Linux*|Darwin*) QDL=qdl ;;
CYGWIN*|MINGW*|MSYS*) QDL=qdl.exe ;;
*) exit 1 ;;
esac
QEXE="${builddir}/${QDL}"
shopt -s nullglob
# --- fail-fast across the (serialized) suite ------------------------------
# meson runs each step as its own process, so persist a sentinel: the first
# step starts a fresh run, any real failure raises it, and every later step
# bails out instead of touching the device once it is set. A skipped step
# (exit 77) does not count as a failure.
ABORT_FILE="${builddir}/meson-logs/hil.aborted"
# Keep all HIL scratch (partition readbacks, temporary reads) on the build
# disk, never in /tmp - which is commonly a small tmpfs that a full-partition
# readback would exhaust.
HIL_TMPDIR="${builddir}/hil-tmp"
mkdir -p "${HIL_TMPDIR}"
# Readback of the target partition captured by the read step and written back
# after the erase step, so erase/write restore the exact original bytes.
SAVED_IMAGE="${HIL_TMPDIR}/readback.img"
on_exit() {
local rc=$?
if [[ ${rc} -ne 0 && ${rc} -ne ${SKIP} ]]; then
mkdir -p "$(dirname "${ABORT_FILE}")" 2>/dev/null || true
: > "${ABORT_FILE}"
fi
}
trap on_exit EXIT
# The suite's first step (highest meson priority). Suite-level setup -
# resetting the fail-fast sentinel, entering EDL mode - must anchor to it:
# doing either in a later step breaks the steps running before it. Keep
# this in sync with the priorities in tests/meson.build.
FIRST_STEP="list"
if [[ "${step}" == "${FIRST_STEP}" ]]; then
rm -f "${ABORT_FILE}"
rm -rf "${HIL_TMPDIR:?}"/*
fi
# --- resolve configuration from the environment ---------------------------
BUILD="${QDL_HIL_BUILD}"
STORAGE="${QDL_HIL_STORAGE}"
PARTITION="${QDL_HIL_PARTITION:-efi}"
SERIAL="${QDL_HIL_SERIAL}"
PROGRAMMER="${QDL_HIL_PROGRAMMER}"
IMAGE="${QDL_HIL_IMAGE}"
if [[ -z "${BUILD}" || -z "${STORAGE}" ]]; then
echo "set QDL_HIL_BUILD and QDL_HIL_STORAGE to run the HIL suite" >&2
exit ${SKIP}
fi
# Fail-fast: if an earlier step already failed, stop the whole suite. No
# step is exempt - the first step cleared the sentinel above before this
# check, so a set sentinel always means a failure in this run.
if [[ -e "${ABORT_FILE}" ]]; then
echo "an earlier HIL step failed; stopping the suite" >&2
exit ${SKIP}
fi
# For unattended runs an environment-provided command puts the board into
# EDL mode ahead of the suite's first step. The list step polls for the
# device afterwards (see QDL_HIL_SETTLE), so the command only needs to
# trigger the mode switch, not wait for enumeration.
if [[ "${step}" == "${FIRST_STEP}" && -n "${QDL_HIL_ENTER_EDL_CMD}" ]]; then
bash -c "${QDL_HIL_ENTER_EDL_CMD}" || {
echo "QDL_HIL_ENTER_EDL_CMD failed" >&2
exit 1
}
fi
IS_DIR=false
if [[ -d "${BUILD}" ]]; then
IS_DIR=true
BUILDDIR="${BUILD}"
else
BUILDDIR="$(dirname "${BUILD}")"
fi
if [[ -z "${PROGRAMMER}" ]]; then
progs=( "${BUILDDIR}"/prog_firehose_*.elf )
[[ ${#progs[@]} -gt 0 ]] && PROGRAMMER="${progs[0]}"
fi
# Image the erase/write steps restore: an explicit QDL_HIL_IMAGE if given,
# otherwise the readback captured by the read step.
IMAGE="${IMAGE:-${SAVED_IMAGE}}"
# Optional debug logging on every qdl invocation, for post-mortems.
QDLDBG=()
[[ -n "${QDL_HIL_DEBUG}" ]] && QDLDBG=( -d )
# Common qdl prefix. -R suppresses the reset everywhere but the final step.
COMMON=( "${QDLDBG[@]}" -s "${STORAGE}" )
[[ -n "${SERIAL}" ]] && COMMON+=( -S "${SERIAL}" )
qdl_noreset() { "${QEXE}" "${COMMON[@]}" -R "$@"; }
qdl_reset() { "${QEXE}" "${COMMON[@]}" "$@"; }
need_programmer() {
[[ -n "${PROGRAMMER}" ]] || {
echo "no prog_firehose_*.elf found (set QDL_HIL_PROGRAMMER)" >&2
exit ${SKIP}
}
}
# Print the hex sha256 of a file, using whichever tool is available.
sha256_file() {
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$1" | awk '{print $1}'
elif command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$1" | awk '{print $1}'
else
echo "no sha256 tool (sha256sum/shasum) available" >&2
exit ${SKIP}
fi
}
# --- step implementations -------------------------------------------------
# a) Flash the whole build.
t_flash_full() {
if ${IS_DIR}; then
need_programmer
local raws=( "${BUILDDIR}"/rawprogram*.xml )
local patches=( "${BUILDDIR}"/patch*.xml )
[[ ${#raws[@]} -gt 0 ]] || { echo "no rawprogram*.xml in ${BUILDDIR}" >&2; exit ${SKIP}; }
qdl_noreset "${PROGRAMMER}" "${raws[@]}" "${patches[@]}"
else
qdl_noreset flash "${BUILD}"
fi
}
# b) Read a partition by name (EFI by default). The readback is kept so the
# write step can restore it after erase.
t_read_partition() {
need_programmer
qdl_noreset "${PROGRAMMER}" read "${PARTITION}" "${SAVED_IMAGE}"
[[ -s "${SAVED_IMAGE}" ]] || { echo "read produced an empty file" >&2; return 1; }
}
# c) Read only the GPT and confirm it really is one.
t_read_gpt() {
need_programmer
local out; out="$(mktemp -p "${HIL_TMPDIR}")"
trap 'rm -f "${out}"' RETURN
qdl_noreset "${PROGRAMMER}" read 0/0+34 "${out}"
grep -qa "EFI PART" "${out}" || { echo "no GPT signature in readback" >&2; return 1; }
}
# c') Other supported read address forms.
t_read_addr_sectors() { # P/S+L
need_programmer
local out; out="$(mktemp -p "${HIL_TMPDIR}")"
trap 'rm -f "${out}"' RETURN
qdl_noreset "${PROGRAMMER}" read 0/0+1 "${out}"
[[ -s "${out}" ]] || { echo "empty readback" >&2; return 1; }
}
t_read_addr_partname() { # P/name
need_programmer
local out; out="$(mktemp -p "${HIL_TMPDIR}")"
trap 'rm -f "${out}"' RETURN
qdl_noreset "${PROGRAMMER}" read "0/${PARTITION}" "${out}"
[[ -s "${out}" ]] || { echo "empty readback" >&2; return 1; }
}
# d) Re-flash the build with --skipblock=sha256 (should mostly skip).
t_flash_skipblock() {
if ${IS_DIR}; then
need_programmer
local raws=( "${BUILDDIR}"/rawprogram*.xml )
local patches=( "${BUILDDIR}"/patch*.xml )
[[ ${#raws[@]} -gt 0 ]] || { echo "no rawprogram*.xml" >&2; exit ${SKIP}; }
qdl_noreset --skipblock=sha256 "${PROGRAMMER}" "${raws[@]}" "${patches[@]}"
else
qdl_noreset --skipblock=sha256 flash "${BUILD}"
fi
}
# f) sha256 digest command.
t_sha256() {
need_programmer
local out; out="$(mktemp -p "${HIL_TMPDIR}")"
trap 'rm -f "${out}"' RETURN
qdl_noreset "${PROGRAMMER}" sha256 0/0+34 >"${out}" 2>&1
grep -qiE '[0-9a-f]{64}' "${out}" || { echo "no sha256 digest in output:" >&2; cat "${out}" >&2; return 1; }
}
# g) Erase the partition. Only erase when there is an image to restore it with
# (the readback from the read step, or an explicit QDL_HIL_IMAGE), so the suite
# never leaves the partition wiped.
t_erase() {
need_programmer
[[ -f "${IMAGE}" ]] || { echo "no image to restore (run the read step or set QDL_HIL_IMAGE)" >&2; exit ${SKIP}; }
qdl_noreset "${PROGRAMMER}" erase "${PARTITION}"
}
# f') Write the image back to its partition (the readback by default).
t_write_image() {
need_programmer
[[ -f "${IMAGE}" ]] || { echo "no image to write (run the read step or set QDL_HIL_IMAGE)" >&2; exit ${SKIP}; }
qdl_noreset "${PROGRAMMER}" write "${PARTITION}" "${IMAGE}"
}
# Final step: reset the board with the explicit reset verb, ending the
# Firehose session that every other step kept alive with --skip-reset.
t_reset() {
need_programmer
qdl_reset "${PROGRAMMER}" reset
rm -f "${SAVED_IMAGE}" # done; drop the readback backup
}
# h) Enumerate EDL devices; the attached device must appear. The enter-EDL
# hook may have just power-cycled or rebooted the board, and enumeration
# after that takes several seconds - the flash steps wait on their own,
# but qdl list is a one-shot, so poll until the device shows up or
# QDL_HIL_SETTLE expires.
t_list() {
local out deadline=$(( SECONDS + ${QDL_HIL_SETTLE:-30} ))
out="$(mktemp -p "${HIL_TMPDIR}")"
trap 'rm -f "${out}"' RETURN
while :; do
"${QEXE}" "${QDLDBG[@]}" list >"${out}" 2>&1 || { echo "qdl list failed" >&2; cat "${out}" >&2; return 1; }
if ! grep -qi "No devices found" "${out}" &&
grep -qiE '^[0-9a-f]{4}:[0-9a-f]{4}' "${out}" &&
{ [[ -z "${SERIAL}" ]] || grep -qi "${SERIAL}" "${out}"; }; then
return 0
fi
if (( SECONDS >= deadline )); then
echo "no EDL device${SERIAL:+ with serial ${SERIAL}} appeared within ${QDL_HIL_SETTLE:-30}s:" >&2
cat "${out}" >&2
return 1
fi
sleep 1
done
}
# i) Package a contents.xml into a zip with create-zip, then flash the zip
# (the programmer comes from inside the zip).
t_create_zip() {
[[ -n "${QDL_HIL_CONTENTS}" ]] || {
echo "set QDL_HIL_CONTENTS to test create-zip" >&2
exit ${SKIP}
}
local zip="${HIL_TMPDIR}/hil-created.zip"
trap 'rm -f "${zip}"' RETURN
"${QEXE}" create-zip "${zip}" "${QDL_HIL_CONTENTS}" ||
{ echo "create-zip failed" >&2; return 1; }
[[ -s "${zip}" ]] || { echo "create-zip produced no output" >&2; return 1; }
qdl_noreset flash "${zip}"
}
# j) Flash a build that uses sparse (Android sparse format) images.
t_sparse_flash() {
[[ -n "${QDL_HIL_SPARSE}" && -f "${QDL_HIL_SPARSE}" ]] || {
echo "set QDL_HIL_SPARSE to a flashmap/contents/zip using sparse images" >&2
exit ${SKIP}
}
qdl_noreset flash "${QDL_HIL_SPARSE}"
}
# k) Error handling: a read of a nonexistent physical partition must be NAKed
# by the device and reported as a failure, not silently succeed.
t_nak_read() {
need_programmer
local out; out="$(mktemp -p "${HIL_TMPDIR}")"
trap 'rm -f "${out}"' RETURN
if qdl_noreset "${PROGRAMMER}" read 99/0+1 "${out}" >/dev/null 2>&1; then
echo "reading nonexistent physical partition 99 unexpectedly succeeded" >&2
return 1
fi
}
# l) sha256 digest of a partition addressed by GPT name.
t_sha256_name() {
need_programmer
local out; out="$(mktemp -p "${HIL_TMPDIR}")"
trap 'rm -f "${out}"' RETURN
qdl_noreset "${PROGRAMMER}" sha256 "${PARTITION}" >"${out}" 2>&1
grep -qiE '[0-9a-f]{64}' "${out}" ||
{ echo "no sha256 digest for ${PARTITION}:" >&2; cat "${out}" >&2; return 1; }
}
# m) Re-write the image with an overridden transfer chunk size.
t_write_chunked() {
need_programmer
[[ -f "${IMAGE}" ]] || {
echo "no image to write (run the read step or set QDL_HIL_IMAGE)" >&2
exit ${SKIP}
}
"${QEXE}" "${COMMON[@]}" -R --out-chunk-size=16384 \
"${PROGRAMMER}" write "${PARTITION}" "${IMAGE}"
}
# n) Integrity check: the device's digest of the partition must match a local
# sha256 of the bytes we read back (and just wrote), proving the erase/write
# round-trip preserved the content.
t_verify_write() {
need_programmer
[[ -f "${SAVED_IMAGE}" ]] || {
echo "no readback to verify against (the read step did not run)" >&2
exit ${SKIP}
}
local out; out="$(mktemp -p "${HIL_TMPDIR}")"
trap 'rm -f "${out}"' RETURN
qdl_noreset "${PROGRAMMER}" sha256 "${PARTITION}" >"${out}" 2>&1 ||
{ echo "sha256 command failed" >&2; cat "${out}" >&2; return 1; }
local dev_digest local_digest
dev_digest="$(grep -oiE '[0-9a-f]{64}' "${out}" | head -1)"
[[ -n "${dev_digest}" ]] || { echo "no device digest:" >&2; cat "${out}" >&2; return 1; }
local_digest="$(sha256_file "${SAVED_IMAGE}")"
[[ "${dev_digest,,}" == "${local_digest,,}" ]] || {
echo "partition digest mismatch: device=${dev_digest} local=${local_digest}" >&2
return 1
}
}
# o) Chip identity through Sahara command mode. Must run before the
# programmer upload: chipinfo is only available while the device still
# speaks Sahara. The verb switches the device back to image-transfer mode
# on exit, so the following flash step still gets its Sahara HELLO.
t_chipinfo() {
local out; out="$(mktemp -p "${HIL_TMPDIR}")"
trap 'rm -f "${out}"' RETURN
local args=()
[[ -n "${SERIAL}" ]] && args+=( -S "${SERIAL}" )
# The serial must follow the verb: qdl's verb dispatcher stops
# scanning at the first argument without a leading dash, so the
# value of a preceding "-S <serial>" derails it into the flash
# parser. The chipinfo subcommand parses -S itself.
if ! "${QEXE}" "${QDLDBG[@]}" chipinfo "${args[@]}" >"${out}" 2>&1; then
if grep -qi "already in Firehose mode" "${out}"; then
echo "programmer already running; chipinfo needs a fresh Sahara device" >&2
exit ${SKIP}
fi
echo "chipinfo failed:" >&2
cat "${out}" >&2
return 1
fi
grep -qiE 'chip serial number|hw id' "${out}" ||
{ echo "chipinfo reported no chip identity:" >&2; cat "${out}" >&2; return 1; }
}
# --- dispatch -------------------------------------------------------------
case "${step}" in
list) t_list ;;
chipinfo) t_chipinfo ;;
flash_full) t_flash_full ;;
create_zip) t_create_zip ;;
sparse_flash) t_sparse_flash ;;
read_partition) t_read_partition ;;
read_gpt) t_read_gpt ;;
read_addr_sectors) t_read_addr_sectors ;;
read_addr_partname) t_read_addr_partname ;;
nak_read) t_nak_read ;;
flash_skipblock) t_flash_skipblock ;;
sha256) t_sha256 ;;
sha256_name) t_sha256_name ;;
erase) t_erase ;;
write_image) t_write_image ;;
write_chunked) t_write_chunked ;;
verify_write) t_verify_write ;;
reset) t_reset ;;
*) echo "unknown step: ${step}" >&2; exit 99 ;;
esac