diff --git a/Runner/suites/Kernel/Baseport/Buses/Buses.yaml b/Runner/suites/Kernel/Baseport/Buses/Buses.yaml index ed2a31299..87b7cac87 100644 --- a/Runner/suites/Kernel/Baseport/Buses/Buses.yaml +++ b/Runner/suites/Kernel/Baseport/Buses/Buses.yaml @@ -11,6 +11,7 @@ params: # Safe CI defaults: use the in-repository read-only EEPROM path and do not # invoke the optional legacy binary or any active bus operation. I2C_LEGACY_TEST_ENABLE: "0" + # Additional read-only modes: matrix and pm. Default auto remains unchanged. I2C_EEPROM_MODE: "auto" I2C_EEPROM_DEVICE: "auto" # Integrity mode must receive a target-approved range and explicit write diff --git a/Runner/suites/Kernel/Baseport/Buses/README.md b/Runner/suites/Kernel/Baseport/Buses/README.md index 9991f6e05..49526ff3f 100644 --- a/Runner/suites/Kernel/Baseport/Buses/README.md +++ b/Runner/suites/Kernel/Baseport/Buses/README.md @@ -1,7 +1,7 @@ # I2C Buses Validation This suite performs capability-driven I2C runtime validation. It correlates -enabled Qualcomm I2C device-tree controllers with runtime adapters and clients, +enabled Qualcomm GENI, QUP and CCI device-tree controllers with runtime adapters and clients, reports driver binding, queries adapter functionality, and retains bounded inventory and kernel-health evidence. @@ -16,10 +16,10 @@ from the running target. When discovery is ambiguous, select the sysfs data file explicitly. A destructive integrity check additionally requires the user to provide a documented safe offset and length. -On Debian, Ubuntu, and CentOS, the suite uses the shared package provider to -install the mapped `i2c-tools` package when it is missing. Yocto remains -image-managed and never performs runtime package installation. Missing optional -utilities or hardware produce SKIP with discovery evidence. +Yocto, Debian, Ubuntu and CentOS all use image-provided components. The suite +does not install packages or require systemd. Missing optional utilities or +hardware produce SKIP with discovery evidence. An I/O error or short read after +selecting an available EEPROM is FAIL, not an unavailable-environment SKIP. ## Default run @@ -82,6 +82,72 @@ EEPROM_PATH=/sys/bus/i2c/devices/-
/eeprom The probe reads the dynamically reported device capacity and records its SHA-256 digest without modifying the device. +When Python is absent, `i2c_eeprom_read.sh` provides the read-only path using +image-provided `sh`, `dd`, `stat`, `sha256sum`, `cmp` and the base shell utilities. +The helper probes `dd` for `iflag=fullblock` support before device access and +skips if it is unavailable, partial kernel reads must not truncate a block. +It preserves candidate deduplication, explicit selection, exact byte counts and +failure reporting. This fallback reads at most 1 MiB per selected range. For a +larger EEPROM, provide `--eeprom-offset` and `--eeprom-length`. Reads use aligned +blocks where possible, not a fixed one-byte read for every EEPROM access. +Python remains required for destructive integrity mode, there is no shell write +fallback. A missing explicitly selected file fails rather than selecting a +different device. Ordinary automatic mode still skips an absent or ambiguous +EEPROM. + +Metadata-only EEPROM discovery for automatic adapter selection does not require +the data-read tools or apply the 1 MiB read limit. It does not read EEPROM data. +Missing read tools therefore do not prevent identification of an adapter for a +separately requested operation whose own prerequisites are available. + +## Additional read-only coverage + +These explicit modes work with ramdisk tools and do not require Python: + +```sh +./run.sh --eeprom-mode matrix +./run.sh --eeprom-mode pm +``` + +`matrix` takes a baseline and compares repeated reads at the beginning, middle +and end of the selected range. It exercises lengths 1, 2, 3, 16, 32, 64 and the +selected range length where they fit, at most 21 comparisons. The default range +is the first `min(capacity, 4096)` bytes. A mismatch or transfer error fails. +This is sysfs read consistency coverage, not proof of a particular FIFO, DMA, +GPI or repeated-start transfer mode. The kernel driver controls those details. + +`pm` takes a baseline, observes the selected client/controller ancestry for +natural runtime suspend for at most ten seconds, then verifies an identical +fresh read. It skips if no natural suspend is observed. It never writes +`power/control`, changes autosuspend delays, unbinds drivers, or suspends the +whole system. A post-idle read failure or mismatch fails. This is a read-after- +idle check, not exhaustive power-management transition coverage. + +Select a device and read range explicitly when needed: + +```sh +./run.sh --eeprom-mode matrix --eeprom-device "$EEPROM_PATH" \ + --eeprom-offset 0 --eeprom-length 256 --eeprom-timeout 60 +I2C_EEPROM_MODE=pm I2C_EEPROM_DEVICE="$EEPROM_PATH" ./run.sh +``` + +The existing `I2C_EEPROM_MODE`, `I2C_EEPROM_DEVICE`, `I2C_EEPROM_OFFSET`, +`I2C_EEPROM_LENGTH` and `I2C_EEPROM_TIMEOUT` parameters apply to these modes. +CLI values override environment values. The suite bounds the complete EEPROM +operation with `--eeprom-timeout`, each shell data read also has a ten-second +watchdog with termination escalation. No multi-device operation is inferred +from enumeration order. Use explicit invocations for multiple EEPROMs. + +Each shell-helper invocation retains a separate visible `eeprom-read.XXXXXX/` +directory under the suite results. Evidence includes `i2c_eeprom_candidates.tsv`, the EEPROM report, +read data and SHA-256 for probe mode, matrix expected/actual buffers and read +logs, and `i2c_runtime_pm.tsv` plus before/after buffers for PM mode. + +Stuck-bus, NACK injection and recovery testing are not part of these modes. +They require a separately selected controlled fixture and a documented +injection/recovery interface. Never use automatic address scans or raw register +writes as a substitute for a fault injector on production peripherals. + ## EEPROM data-integrity validation Integrity mode reads the original bytes from a user-approved range, derives a @@ -174,7 +240,7 @@ EEPROM validator provides the default functional coverage. ## Public options - `--legacy-test` -- `--eeprom-mode auto|off|probe|integrity` +- `--eeprom-mode auto|off|probe|integrity|matrix|pm` - `--eeprom-test` - `--eeprom-device auto|PATH` - `--eeprom-offset BYTES` @@ -200,11 +266,91 @@ Every option has a corresponding uppercase environment variable shown by - `PASS`: applicable runtime topology is healthy and each available or explicitly requested validation completes with its required evidence. -- `FAIL`: runtime state is inconsistent, user input is malformed, an operation - fails after its environment is available, or modified data cannot be +- `FAIL`: controller or required-client runtime state is inconsistent, user + input is malformed, an operation fails after its environment is available, + a controller/client driver error is captured, or modified data cannot be restored and verified. -- `SKIP`: I2C, a selected adapter or EEPROM, Python, or an optional diagnostic - utility is unavailable. +- `SKIP`: I2C or its character interface is unavailable, automatic EEPROM + selection is absent or ambiguous, a required optional utility is missing, + an optional camera is unbound, or natural runtime suspend is not observed. + An explicitly selected missing EEPROM is `FAIL`. Artifacts are retained under `results/Buses/`, including controller, adapter, client, driver, dmesg, EEPROM discovery, digest, and restoration evidence. +The result is initialized before argument validation so a malformed invocation +cannot publish an earlier PASS. An unavailable selected operation does not +override an existing failure or bypass the final kernel-health checks. + +### Topology completeness + +`i2c_topology.tsv` records controller-to-adapter ownership. One unrelated working +adapter cannot satisfy another controller. Enabled CCI master buses require +their own runtime adapters, while a CCI parent with no enabled buses does not +create an adapter requirement. Enabled DT client nodes are reconciled against +exact runtime `of_node` identities, including nodes that never became clients. +Disabled ancestors are excluded. Mux channel links and enabled channel nodes are +checked against runtime adapters. Framework-owned adapter objects do not need a +client-style direct driver binding. Optional-camera readiness keeps its separate +classification, driver errors still fail independently. + +### Client probe diagnostics + +The controller-only kernel scan does not establish client health. The suite +also filters the retained `dmesg_snapshot.log` using client addresses, names, +bound drivers, and module candidates from `i2c_clients.tsv`. Matching lines are +saved verbatim in `i2c_client_dmesg.log`, including probe failures and their +supporting messages. No additional kernel-log capture, module loading, bus scan, +or hardware write is performed. + +When runtime validation fails or an optional camera sensor is skipped, up to +120 client log lines are also printed to stdout. The complete filtered file +remains available for larger logs. An empty +file means no matching messages were retained, not that probing succeeded. +Unavailable kernel logs are reported without hiding an existing runtime FAIL. +Explicit errors from discovered I2C controller and client drivers are also +retained in `i2c_driver_errors.log` and printed with `I2C-DRIVER-ERROR` labels. +The full matching context is saved in `i2c_driver_dmesg.log`. Probe errors, +firmware-load failures, chip-ID read failures and other explicit driver errors +fail Buses even with `I2C_DMESG_STRICT=0`. The existing strict option still +controls additional findings from the legacy controller scan, it cannot +suppress this driver-error check. Errors remain failures even if the driver +subsequently binds successfully. + +Normal initialization and dummy-regulator notices alone are not failures. +Matching covers runtime-discovered controllers, adapter identities and clients, including optional +cameras, not unrelated drivers elsewhere in the kernel. If the kernel snapshot +is unavailable, the check cannot certify driver health. No second kernel-log +capture or hardware operation is performed. + +### Optional camera sensors + +Buses validates controller and required-client readiness, not image capture. +An unbound optional camera sensor is recorded as an individual capture-readiness +SKIP while bus and kernel-health checks continue. Runtime DT identification +requires a `camera@...` or +`sensor@...` node with `compatible` and `reg` properties and a media endpoint +under `port` or `ports` exposing both `remote-endpoint` and `data-lanes`. +This conservative structural check uses no board, sensor-model, driver-name or +I2C-address list. A name alone is insufficient. Unclassified unbound clients, +including bridges and embedded controllers, remain FAIL, as do controller +failures. Bound cameras are still included in the bound-client inventory. + +The skipped clients are listed in `i2c_optional_camera_clients.tsv` and their +probe evidence remains in the client diagnostic log and stdout. SKIP means +camera capture readiness is outside this bus check, not that disconnection was +proven. +A failed chip-ID read can also indicate a power, reset, wiring or driver issue. +Camera functionality belongs to `Libcamera_cam` or `Camera_NHX`, using the +appropriate stack and an attached sensor. Those suites may themselves SKIP +when their required environment is unavailable, so a Buses PASS is not proof +of camera coverage or health. + +For a setup without cameras, the default `./run.sh` may PASS its bus checks +with a camera SKIP only when no other gating failure is observed. A captured +camera probe error still makes the overall result FAIL, even if the camera +is known to be disconnected. Disable the unused camera DT node or omit its +optional overlay in that setup to avoid attempting an invalid probe. A camera +SKIP cannot hide an embedded-controller or other driver failure. Explicitly +requested +I2C scans, register operations and EEPROM checks keep their existing result +policy. No DT nodes, overlays, services or drivers are modified by the test. diff --git a/Runner/suites/Kernel/Baseport/Buses/run.sh b/Runner/suites/Kernel/Baseport/Buses/run.sh index 4d28779f6..b4ecb4a21 100755 --- a/Runner/suites/Kernel/Baseport/Buses/run.sh +++ b/Runner/suites/Kernel/Baseport/Buses/run.sh @@ -36,10 +36,11 @@ fi # shellcheck disable=SC1091 . "$TOOLS/functestlib.sh" # shellcheck disable=SC1091 -. "$TOOLS/lib_pkg_provider.sh" +. "$TOOLS/lib_bus.sh" TESTNAME="Buses" RES_FILE="$SCRIPT_DIR/$TESTNAME.res" +test_result_init "$TESTNAME" "$RES_FILE" || exit 1 RESULT_DIR="$SCRIPT_DIR/results/$TESTNAME" I2C_LEGACY_TEST_ENABLE="${I2C_LEGACY_TEST_ENABLE:-0}" @@ -71,7 +72,7 @@ Usage: ./run.sh [options] Options: --legacy-test Run image-provided i2c-msm-test after inventory. - --eeprom-mode MODE auto, off, probe, or integrity, default: auto. + --eeprom-mode MODE auto, off, probe, integrity, matrix, or pm, default: auto. --eeprom-test Select EEPROM integrity mode. --eeprom-device PATH Select a sysfs EEPROM or NVMEM data file. --eeprom-offset BYTES Start of an explicitly safe writable range. @@ -94,7 +95,7 @@ Options: Environment: I2C_LEGACY_TEST_ENABLE=auto|0|1 - I2C_EEPROM_MODE=auto|off|probe|integrity + I2C_EEPROM_MODE=auto|off|probe|integrity|matrix|pm I2C_EEPROM_DEVICE=auto|PATH I2C_EEPROM_OFFSET=BYTES I2C_EEPROM_LENGTH=BYTES @@ -215,6 +216,7 @@ parse_args() { ;; -h|--help) usage + trap - EXIT exit 0 ;; *) @@ -229,49 +231,25 @@ parse_args() { # Restores a register value when an interrupted write left restoration pending. cleanup() { if ! i2c_tools_restore_pending_write; then - log_fail "I2C emergency register restoration failed" - return 1 + trap - EXIT + test_result_finish "FAIL" "I2C emergency register restoration failed" + fi + if [ ! -s "$RES_FILE" ]; then + trap - EXIT + test_result_finish "FAIL" "Buses did not complete, inspect the preceding configuration or execution error" fi } # handle_signal # Restores pending state before terminating after a signal. handle_signal() { - cleanup || true - exit 1 + test_result_record "FAIL" "Buses was interrupted, restoring any pending register write" + cleanup + test_result_finish } -# i2c_select_eeprom_adapter -# Prints the adapter number for one dynamically discovered I2C EEPROM. -i2c_select_eeprom_adapter() { - isea_runner="$TOOLS/i2c_eeprom_runner.py" - isea_log="$RESULT_DIR/i2c_eeprom_adapter_discovery.log" - isea_report="$RESULT_DIR/i2c_eeprom_adapter_discovery.tsv" - - command -v python3 >/dev/null 2>&1 || return 1 - [ -f "$isea_runner" ] || return 1 - - run_with_timeout_log \ - "$I2C_EEPROM_TIMEOUT" \ - "$isea_log" \ - python3 "$isea_runner" \ - --mode discover \ - --device "$I2C_EEPROM_DEVICE" \ - --report "$isea_report" - isea_status=$? - [ "$isea_status" -eq 0 ] || return 1 - grep -q '^I2C_EEPROM_RESULT status=PASS ' "$isea_log" || return 1 - - isea_adapter=$(awk -F '\t' '$1 == "adapter" { print $2; exit }' "$isea_report") - case "$isea_adapter" in - ''|*[!0-9]*) - return 1 - ;; - esac - [ -c "/dev/i2c-$isea_adapter" ] || return 1 - - printf '%s\n' "$isea_adapter" -} +trap cleanup EXIT +trap handle_signal HUP INT TERM parse_args "$@" || { usage >&2 @@ -288,10 +266,10 @@ case "$I2C_LEGACY_TEST_ENABLE" in esac case "$I2C_EEPROM_MODE" in - auto|off|probe|integrity) + auto|off|probe|integrity|matrix|pm) ;; *) - log_error "I2C_EEPROM_MODE must be auto, off, probe, or integrity" + log_error "I2C_EEPROM_MODE must be auto, off, probe, integrity, matrix, or pm" exit 2 ;; esac @@ -392,10 +370,6 @@ if [ -n "$I2C_WRITE_ADDRESS" ] && [ "$I2C_ALLOW_WRITE" -ne 1 ]; then exit 2 fi -test_result_init "$TESTNAME" "$RES_FILE" || exit 1 -trap cleanup EXIT -trap handle_signal HUP INT TERM - if ! mkdir -p "$RESULT_DIR"; then test_result_finish "FAIL" "$TESTNAME FAIL: cannot create result directory $RESULT_DIR" fi @@ -426,14 +400,21 @@ if ! CHECK_DEPS_RECOVER=0 CHECK_DEPS_NO_EXIT=1 check_dependencies \ test_result_finish "SKIP" "$TESTNAME SKIP: required base utilities are unavailable" fi -i2c_collect_runtime_inventory "$RESULT_DIR" +i2c_collect_runtime_inventory "$RESULT_DIR" optional-camera topology i2c_inventory_status=$? +if [ "$I2C_RUNTIME_OPTIONAL_CAMERA_COUNT" -gt 0 ]; then + while IFS="$(printf '\t')" read -r i2c_camera_client i2c_camera_name i2c_camera_node; do + [ -n "$i2c_camera_client" ] || continue + test_result_record "SKIP" "Optional camera sensor unavailable: client=$i2c_camera_client name=$i2c_camera_name node=$i2c_camera_node, capture validation belongs to Libcamera_cam or Camera_NHX. Any captured driver probe errors still fail kernel-health validation" + done <"$RESULT_DIR/i2c_optional_camera_clients.tsv" +fi + case "$i2c_inventory_status" in 0) test_result_record \ "PASS" \ - "I2C runtime is healthy: controllers=$I2C_RUNTIME_CONTROLLER_COUNT adapters=$I2C_RUNTIME_ADAPTER_COUNT clients=$I2C_RUNTIME_CLIENT_COUNT bound_clients=$I2C_RUNTIME_BOUND_CLIENT_COUNT" + "I2C bus and required-client runtime checks passed: controllers=$I2C_RUNTIME_CONTROLLER_COUNT adapters=$I2C_RUNTIME_ADAPTER_COUNT clients=$I2C_RUNTIME_CLIENT_COUNT bound_clients=$I2C_RUNTIME_BOUND_CLIENT_COUNT optional_camera_skipped=$I2C_RUNTIME_OPTIONAL_CAMERA_COUNT" ;; 1) test_result_record \ @@ -462,6 +443,7 @@ else fi i2c_tools_explicit=0 +i2c_operations_ready=1 if [ "$I2C_SCAN_ENABLE" -eq 1 ] || [ -n "$I2C_READ_ADDRESS" ] || [ -n "$I2C_WRITE_ADDRESS" ] || @@ -481,7 +463,7 @@ if [ "$i2c_tools_explicit" -eq 1 ] && [ "$I2C_TEST_ADAPTER" = "auto" ]; then [ -z "$I2C_WRITE_ADDRESS" ] && { [ "$I2C_SCAN_ENABLE" -eq 1 ] || [ "$I2C_LEGACY_TEST_ENABLE" = "1" ]; }; then - I2C_TEST_ADAPTER=$(i2c_select_eeprom_adapter) + I2C_TEST_ADAPTER=$(i2c_select_eeprom_adapter "$TOOLS" "$RESULT_DIR" "$I2C_EEPROM_TIMEOUT" "$I2C_EEPROM_DEVICE") i2c_adapter_selection_status=$? if [ "$i2c_adapter_selection_status" -eq 0 ]; then log_info "I2C scan adapter selected from the discovered EEPROM: i2c-$I2C_TEST_ADAPTER" @@ -492,31 +474,17 @@ if [ "$i2c_tools_explicit" -eq 1 ] && [ "$I2C_TEST_ADAPTER" = "auto" ]; then log_info "I2C functional adapter auto-selected: i2c-$I2C_TEST_ADAPTER" ;; 1) - test_result_finish "SKIP" "$TESTNAME SKIP: multiple I2C adapters are available and no unique functional adapter could be discovered" + test_result_record "SKIP" "Requested I2C operation needs --adapter, multiple adapters remain eligible" + i2c_operations_ready=0 ;; *) - test_result_finish "SKIP" "$TESTNAME SKIP: no I2C character adapter is available for the explicitly requested operation" + test_result_record "SKIP" "Requested I2C operation has no accessible character adapter, provide CONFIG_I2C_CHARDEV and device nodes" + i2c_operations_ready=0 ;; esac fi -pkg_ensure_host_distro_package_set_present i2c-tools -i2c_tools_package_status=$? - -case "$i2c_tools_package_status" in - 0) - log_info "I2C userspace package set is ready" - ;; - 1) - log_warn "i2c-tools package recovery failed on $(pkg_detect_os_id), checking for image-provided commands" - ;; - 2) - log_info "I2C package recovery is not applicable, using image-provided tools" - ;; - *) - test_result_record "FAIL" "I2C package preparation returned unexpected status $i2c_tools_package_status" - ;; -esac +log_info "I2C diagnostics use image-provided tools only, no runtime package installation" if command -v i2cdetect >/dev/null 2>&1; then log_info "I2C userspace validation: listing adapters and querying adapter functionality without transferring device data" @@ -540,7 +508,7 @@ else test_result_record "SKIP" "I2C userspace diagnostics are unavailable because i2c-tools are absent" fi -if [ "$I2C_SCAN_ENABLE" -eq 1 ]; then +if [ "$I2C_SCAN_ENABLE" -eq 1 ] && [ "$i2c_operations_ready" -eq 1 ]; then log_warn "I2C address scan was explicitly requested for adapter=$I2C_TEST_ADAPTER mode=$I2C_SCAN_MODE" i2c_tools_scan_adapter \ "$I2C_TEST_ADAPTER" \ @@ -564,7 +532,7 @@ if [ "$I2C_SCAN_ENABLE" -eq 1 ]; then esac fi -if [ -n "$I2C_READ_ADDRESS" ]; then +if [ -n "$I2C_READ_ADDRESS" ] && [ "$i2c_operations_ready" -eq 1 ]; then log_warn "I2C register read was explicitly requested for adapter=$I2C_TEST_ADAPTER address=$I2C_READ_ADDRESS register=$I2C_READ_REGISTER mode=$I2C_READ_MODE" i2c_tools_read_register \ "$I2C_TEST_ADAPTER" \ @@ -592,7 +560,7 @@ if [ -n "$I2C_READ_ADDRESS" ]; then esac fi -if [ -n "$I2C_WRITE_ADDRESS" ]; then +if [ -n "$I2C_WRITE_ADDRESS" ] && [ "$i2c_operations_ready" -eq 1 ]; then log_warn "I2C transactional register write was explicitly authorized for adapter=$I2C_TEST_ADAPTER address=$I2C_WRITE_ADDRESS register=$I2C_WRITE_REGISTER mode=$I2C_WRITE_MODE" i2c_tools_write_restore_register \ "$I2C_TEST_ADAPTER" \ @@ -623,16 +591,31 @@ if [ "$I2C_EEPROM_MODE" != "off" ]; then eeprom_runner="$TOOLS/i2c_eeprom_runner.py" eeprom_log="$RESULT_DIR/i2c_eeprom.log" eeprom_report="$RESULT_DIR/i2c_eeprom.tsv" + eeprom_interpreter=python3 + case "$I2C_EEPROM_MODE" in + matrix|pm) + eeprom_interpreter="sh" + ;; + *) + if ! command -v python3 >/dev/null 2>&1 && + [ "$I2C_EEPROM_MODE" != integrity ]; then + eeprom_interpreter="sh" + fi + ;; + esac + if [ "$eeprom_interpreter" = sh ]; then + eeprom_runner="$TOOLS/i2c_eeprom_read.sh" + fi - if ! command -v python3 >/dev/null 2>&1; then - test_result_record "SKIP" "EEPROM validation is unavailable because python3 is absent" + if ! command -v "$eeprom_interpreter" >/dev/null 2>&1; then + test_result_record "SKIP" "EEPROM integrity validation needs image-provided python3, no shell write fallback is permitted" elif [ ! -f "$eeprom_runner" ]; then test_result_record "FAIL" "EEPROM validation runner is missing: $eeprom_runner" else eeprom_runner_mode=$I2C_EEPROM_MODE [ "$eeprom_runner_mode" = "auto" ] && eeprom_runner_mode=probe - set -- python3 "$eeprom_runner" \ + set -- "$eeprom_interpreter" "$eeprom_runner" \ --mode "$eeprom_runner_mode" \ --device "$I2C_EEPROM_DEVICE" \ --report "$eeprom_report" @@ -676,7 +659,7 @@ if [ "$I2C_EEPROM_MODE" != "off" ]; then fi fi -if [ "$I2C_LEGACY_TEST_ENABLE" != 0 ]; then +if [ "$I2C_LEGACY_TEST_ENABLE" != 0 ] && [ "$i2c_operations_ready" -eq 1 ]; then log_info "I2C functional validation: running the optional image-provided i2c-msm-test path" i2c_run_legacy_test "$RESULT_DIR" "$I2C_TEST_ADAPTER" "$I2C_TEST_TIMEOUT" i2c_legacy_status=$? @@ -717,9 +700,45 @@ if [ ! -s "$RESULT_DIR/dmesg_snapshot.log" ]; then elif [ "$i2c_dmesg_status" -eq 0 ] && [ "$I2C_DMESG_STRICT" -eq 1 ]; then test_result_record "FAIL" "I2C-related kernel errors were found in $RESULT_DIR/dmesg_errors.log" elif [ "$i2c_dmesg_status" -eq 0 ]; then - test_result_record "SKIP" "I2C kernel errors were retained as advisory evidence, set I2C_DMESG_STRICT=1 to gate them" + test_result_record "SKIP" "Controller scan findings are retained, explicit driver errors are gated separately regardless of I2C_DMESG_STRICT" +else + test_result_record "PASS" "Controller-only kernel scan found no non-benign errors, client binding is validated separately" +fi + +if i2c_collect_client_dmesg "$RESULT_DIR"; then + log_info "I2C client kernel evidence: $RESULT_DIR/i2c_client_dmesg.log" + if [ "$i2c_inventory_status" -eq 1 ] || [ "$I2C_RUNTIME_OPTIONAL_CAMERA_COUNT" -gt 0 ]; then + if [ -s "$RESULT_DIR/i2c_client_dmesg.log" ]; then + log_file_with_label "I2C-CLIENT-DMESG" "$RESULT_DIR/i2c_client_dmesg.log" 120 + else + log_warn "No matching client messages remain in the kernel snapshot, inspect i2c_clients.tsv and obtain the boot probe log" + fi + fi else - test_result_record "PASS" "No non-benign I2C controller errors were found in the captured kernel log" + log_warn "I2C client kernel evidence is unavailable, inspect i2c_clients.tsv and dmesg_access.log" +fi + +i2c_collect_driver_errors "$RESULT_DIR" +i2c_driver_error_status=$? +case "$i2c_driver_error_status" in + 0) + log_file_with_label "I2C-DRIVER-ERROR" "$RESULT_DIR/i2c_driver_errors.log" 120 + test_result_record "FAIL" "I2C controller or client driver errors were captured, including any probe or firmware-load failures. Resolve the reported driver, firmware or setup issue, artifact=$RESULT_DIR/i2c_driver_errors.log" + ;; + 1) + log_info "No explicit I2C controller or client driver errors were found in the retained kernel snapshot" + ;; + 2) + log_warn "I2C driver-error validation is unavailable without a kernel snapshot, no clean driver-health result is claimed" + ;; + *) + test_result_record "FAIL" "I2C driver-error evidence could not be collected, inspect inventory artifacts and output permissions in $RESULT_DIR" + ;; +esac + +if [ "$i2c_inventory_status" -eq 1 ]; then + log_warn "Inspect the reported controller or client probe failure and its driver, firmware, power and wiring prerequisites" + log_warn "If a DT-declared peripheral is not fitted, disable its node or optional overlay in the setup configuration before rerunning, missing hardware cannot be inferred from a probe error alone" fi test_result_finish diff --git a/Runner/utils/functestlib.sh b/Runner/utils/functestlib.sh index 5050b5924..600b8107a 100755 --- a/Runner/utils/functestlib.sh +++ b/Runner/utils/functestlib.sh @@ -4485,31 +4485,92 @@ dt_validate_usb_host_runtime() { } ############################################################################### -# i2c_collect_runtime_inventory +# i2c_client_is_camera_sensor +# Recognize a camera/sensor node with compatible/reg properties and a linked +# serial-video endpoint. Uses runtime DT structure, not board, bus, address or +# sensor-model lists. Returns 0 for this camera-sensor structure, 1 otherwise. +# Produces no stdout and does not establish physical presence or probe health. +i2c_client_is_camera_sensor() { + icics_node=$(readlink -f "$1" 2>/dev/null) || return 1 + [ -d "$icics_node" ] || return 1 + case "${icics_node##*/}" in + camera@*|sensor@*) + ;; + *) + return 1 + ;; + esac + dt_node_enabled "$icics_node" || return 1 + [ -s "$icics_node/compatible" ] && [ -s "$icics_node/reg" ] || return 1 + + for icics_endpoint in \ + "$icics_node"/port/endpoint* \ + "$icics_node"/port@*/endpoint* \ + "$icics_node"/ports/port/endpoint* \ + "$icics_node"/ports/port@*/endpoint*; do + [ -d "$icics_endpoint" ] || continue + [ -s "$icics_endpoint/remote-endpoint" ] || continue + [ -s "$icics_endpoint/data-lanes" ] || continue + return 0 + done + return 1 +} + +############################################################################### +# i2c_collect_runtime_inventory [required|optional-camera] [legacy|topology] # Correlates enabled Qualcomm I2C controllers with runtime adapters and clients. +# Default required policy fails unbound DT clients. optional-camera records +# structurally identified unbound camera sensors separately for caller SKIPs. +# Outputs inventory artifacts and I2C_RUNTIME_* counters, including +# I2C_RUNTIME_OPTIONAL_CAMERA_COUNT and i2c_optional_camera_clients.tsv. # Returns 0 for healthy applicable hardware, 1 for inconsistent runtime state, # 2 when I2C is not exposed, and 3 for invalid arguments. +# topology additionally requires lib_bus.sh and validates per-controller and +# DT child relationships for GENI, QUP and CCI. The default API is unchanged. ############################################################################### i2c_collect_runtime_inventory() { icri_result_dir="$1" + icri_client_policy="${2:-required}" + icri_topology_policy="${3:-legacy}" I2C_RUNTIME_CONTROLLER_COUNT=0 I2C_RUNTIME_ADAPTER_COUNT=0 I2C_RUNTIME_CLIENT_COUNT=0 I2C_RUNTIME_BOUND_CLIENT_COUNT=0 I2C_RUNTIME_WAITING_CLIENT_COUNT=0 I2C_RUNTIME_REGISTERED_DRIVER_COUNT=0 + I2C_RUNTIME_OPTIONAL_CAMERA_COUNT=0 I2C_RUNTIME_FAILURE_REASON="" icri_controller_unbound=0 icri_declared_client_unbound=0 icri_seen_adapters="" icri_unbound_clients="" icri_waiting_clients="" + icri_controller_pattern='qcom,(geni-i2c|i2c-geni)' + case "$icri_topology_policy" in + legacy) + ;; + topology) + command -v i2c_validate_topology >/dev/null 2>&1 || return 3 + icri_controller_pattern='qcom,(geni-i2c|i2c-geni|i2c-qup[^ ,]*|[^ ,]*-cci)( |$)' + ;; + *) + return 3 + ;; + esac [ -n "$icri_result_dir" ] || return 3 + case "$icri_client_policy" in + required|optional-camera) + ;; + *) + return 3 + ;; + esac mkdir -p "$icri_result_dir" || return 1 : >"$icri_result_dir/i2c_controllers.tsv" : >"$icri_result_dir/i2c_adapters.tsv" : >"$icri_result_dir/i2c_clients.tsv" + : >"$icri_result_dir/i2c_optional_camera_clients.tsv" : >"$icri_result_dir/i2c_registered_drivers.log" for icri_driver_path in /sys/bus/i2c/drivers/*; do @@ -4523,10 +4584,13 @@ i2c_collect_runtime_inventory() { if dt_runtime_root >/dev/null 2>&1; then if dt_list_compatible_nodes \ - 'qcom,(geni-i2c|i2c-geni)' \ + "$icri_controller_pattern" \ regex >"$icri_result_dir/i2c_controller_nodes.log"; then while IFS= read -r icri_node; do [ -n "$icri_node" ] || continue + if [ "$icri_topology_policy" = topology ]; then + i2c_dt_path_enabled "$icri_node" || continue + fi I2C_RUNTIME_CONTROLLER_COUNT=$((I2C_RUNTIME_CONTROLLER_COUNT + 1)) icri_compatible=$(dt_property_text "$icri_node" compatible 2>/dev/null || true) icri_device="" @@ -4591,6 +4655,7 @@ i2c_collect_runtime_inventory() { icri_client_dt_children=0 icri_client_resources="" icri_client_channels=0 + icri_client_classification=required if [ -n "$icri_client_modalias" ] && icri_client_module_lines=$(kernel_modules_for_modalias "$icri_client_modalias" 2>/dev/null); then icri_client_modules=$(printf '%s\n' "$icri_client_module_lines" | tr '\n' ',' | sed 's/,$//') @@ -4631,18 +4696,27 @@ i2c_collect_runtime_inventory() { if [ -n "$icri_client_driver" ]; then I2C_RUNTIME_BOUND_CLIENT_COUNT=$((I2C_RUNTIME_BOUND_CLIENT_COUNT + 1)) elif [ -n "$icri_client_of_node" ]; then - icri_declared_client_unbound=$((icri_declared_client_unbound + 1)) - icri_unbound_clients="$icri_unbound_clients ${icri_client##*/}(${icri_client_name:-unknown})" - case "$icri_client_waiting" in - 1|Y|y|yes|true) - I2C_RUNTIME_WAITING_CLIENT_COUNT=$((I2C_RUNTIME_WAITING_CLIENT_COUNT + 1)) - icri_waiting_clients="$icri_waiting_clients ${icri_client##*/}(${icri_client_name:-unknown})" - ;; - esac - log_warn "[I2C-UNBOUND] client=${icri_client##*/} name=${icri_client_name:-unknown} dt_status=$icri_client_dt_status waiting_for_supplier=${icri_client_waiting:-unexposed} module_state=${icri_client_module_state:-unresolved} dt_children=$icri_client_dt_children runtime_channels=$icri_client_channels resources=$icri_client_resources" + if [ "$icri_client_policy" = "optional-camera" ] && + i2c_client_is_camera_sensor "$icri_client_of_node"; then + icri_client_classification=optional-camera + I2C_RUNTIME_OPTIONAL_CAMERA_COUNT=$((I2C_RUNTIME_OPTIONAL_CAMERA_COUNT + 1)) + printf '%s\t%s\t%s\n' \ + "${icri_client##*/}" "${icri_client_name:-unknown}" \ + "$icri_client_of_node" >>"$icri_result_dir/i2c_optional_camera_clients.tsv" + else + icri_declared_client_unbound=$((icri_declared_client_unbound + 1)) + icri_unbound_clients="$icri_unbound_clients ${icri_client##*/}(${icri_client_name:-unknown})" + case "$icri_client_waiting" in + 1|Y|y|yes|true) + I2C_RUNTIME_WAITING_CLIENT_COUNT=$((I2C_RUNTIME_WAITING_CLIENT_COUNT + 1)) + icri_waiting_clients="$icri_waiting_clients ${icri_client##*/}(${icri_client_name:-unknown})" + ;; + esac + fi + log_warn "[I2C-UNBOUND] client=${icri_client##*/} name=${icri_client_name:-unknown} classification=$icri_client_classification dt_status=$icri_client_dt_status waiting_for_supplier=${icri_client_waiting:-unexposed} module_state=${icri_client_module_state:-unresolved} dt_children=$icri_client_dt_children runtime_channels=$icri_client_channels resources=$icri_client_resources" fi - printf '%s\tname=%s\tdriver=%s\tmodalias=%s\tmodule_candidates=%s\tmodule_state=%s\twaiting_for_supplier=%s\tdt_status=%s\tdt_children=%s\truntime_channels=%s\tresources=%s\tof_node=%s\n' \ + printf '%s\tname=%s\tdriver=%s\tmodalias=%s\tmodule_candidates=%s\tmodule_state=%s\twaiting_for_supplier=%s\tdt_status=%s\tdt_children=%s\truntime_channels=%s\tresources=%s\tof_node=%s\tclassification=%s\n' \ "${icri_client##*/}" \ "${icri_client_name:-unknown}" \ "${icri_client_driver:-unbound}" \ @@ -4654,11 +4728,12 @@ i2c_collect_runtime_inventory() { "$icri_client_dt_children" \ "$icri_client_channels" \ "$icri_client_resources" \ - "${icri_client_of_node:-none}" >>"$icri_result_dir/i2c_clients.tsv" + "${icri_client_of_node:-none}" \ + "$icri_client_classification" >>"$icri_result_dir/i2c_clients.tsv" log_info "[I2C-CLIENT] client=${icri_client##*/} name=${icri_client_name:-unknown} driver=${icri_client_driver:-unbound} modalias=${icri_client_modalias:-unexposed} module_candidates=${icri_client_modules:-unresolved} waiting_for_supplier=${icri_client_waiting:-unexposed} of_node=${icri_client_of_node:-none}" done - log_info "I2C runtime summary: controllers=$I2C_RUNTIME_CONTROLLER_COUNT adapters=$I2C_RUNTIME_ADAPTER_COUNT clients=$I2C_RUNTIME_CLIENT_COUNT bound_clients=$I2C_RUNTIME_BOUND_CLIENT_COUNT waiting_for_supplier=$I2C_RUNTIME_WAITING_CLIENT_COUNT artifact=$icri_result_dir/i2c_adapters.tsv" + log_info "I2C runtime summary: controllers=$I2C_RUNTIME_CONTROLLER_COUNT adapters=$I2C_RUNTIME_ADAPTER_COUNT clients=$I2C_RUNTIME_CLIENT_COUNT bound_clients=$I2C_RUNTIME_BOUND_CLIENT_COUNT optional_camera_unbound=$I2C_RUNTIME_OPTIONAL_CAMERA_COUNT waiting_for_supplier=$I2C_RUNTIME_WAITING_CLIENT_COUNT artifact=$icri_result_dir/i2c_adapters.tsv" if [ "$I2C_RUNTIME_CONTROLLER_COUNT" -eq 0 ] && [ "$I2C_RUNTIME_ADAPTER_COUNT" -eq 0 ]; then @@ -4666,7 +4741,8 @@ i2c_collect_runtime_inventory() { fi if [ "$I2C_RUNTIME_CONTROLLER_COUNT" -gt 0 ] && - [ "$I2C_RUNTIME_ADAPTER_COUNT" -eq 0 ]; then + [ "$I2C_RUNTIME_ADAPTER_COUNT" -eq 0 ] && + [ "$icri_topology_policy" = legacy ]; then I2C_RUNTIME_FAILURE_REASON="enabled Qualcomm I2C controllers expose no runtime adapters" elif [ "$icri_controller_unbound" -gt 0 ]; then I2C_RUNTIME_FAILURE_REASON="$icri_controller_unbound enabled Qualcomm I2C controller(s) have no bound platform driver" @@ -4676,10 +4752,144 @@ i2c_collect_runtime_inventory() { I2C_RUNTIME_FAILURE_REASON="$icri_declared_client_unbound DT-declared I2C client(s) are unbound:${icri_unbound_clients}" fi + if [ "$icri_topology_policy" = topology ]; then + if ! i2c_validate_topology "$icri_result_dir" "$icri_client_policy"; then + I2C_RUNTIME_FAILURE_REASON="${I2C_RUNTIME_FAILURE_REASON:+$I2C_RUNTIME_FAILURE_REASON. }I2C controller, adapter or DT client relationships are incomplete, inspect i2c_topology.tsv" + return 1 + fi + fi [ -z "$I2C_RUNTIME_FAILURE_REASON" ] || return 1 return 0 } +############################################################################### +# I2C client kernel-log diagnostics. +############################################################################### + +# i2c_collect_client_dmesg RESULT_DIR [clients|drivers] +# Filter the existing dmesg_snapshot.log using identities in i2c_clients.tsv. +# Optional drivers scope also uses i2c_controllers.tsv for controller identities. +# Inputs: artifacts produced by i2c_collect_runtime_inventory and +# scan_dmesg_errors in RESULT_DIR. No kernel read or hardware operation occurs. +# Output: replaces i2c_client_dmesg.log (or i2c_driver_dmesg.log for drivers), +# preserving matching lines verbatim. +# Returns: 0 after filtering, including no matches, 1 for unavailable evidence +# or an output/filter error. This diagnostic does not classify test results. +i2c_collect_client_dmesg() { + iccd_result_dir="$1" + iccd_inventory="$iccd_result_dir/i2c_clients.tsv" + iccd_snapshot="$iccd_result_dir/dmesg_snapshot.log" + iccd_output="$iccd_result_dir/i2c_client_dmesg.log" + iccd_scope="${2:-clients}" + case "$iccd_scope" in + clients) + ;; + drivers) + iccd_output="$iccd_result_dir/i2c_driver_dmesg.log" + ;; + *) + return 1 + ;; + esac + + : >"$iccd_output" || return 1 + [ -r "$iccd_inventory" ] || return 1 + [ -s "$iccd_snapshot" ] || return 1 + set -- "$iccd_inventory" + if [ "$iccd_scope" = drivers ]; then + [ -r "$iccd_result_dir/i2c_controllers.tsv" ] || return 1 + set -- "$@" "$iccd_result_dir/i2c_controllers.tsv" + if [ -r "$iccd_result_dir/i2c_adapters.tsv" ]; then + set -- "$@" "$iccd_result_dir/i2c_adapters.tsv" + fi + fi + + # Match whole whitespace-delimited identities, not regexes from sysfs. + # Driver/module spellings can differ only by hyphens and underscores. + # Client addresses also catch probe messages whose driver name differs + # from both the DT client name and the module name. + awk ' + FILENAME != ARGV[ARGC - 1] { + count = split($0, fields, "\t") + if (fields[1] ~ /^i2c-[0-9]+$/) { + identities[fields[1]] = 1 + next + } + if (fields[1] ~ /^[0-9]+-[0-9a-fA-F]+$/) { + identities[fields[1]] = 1 + } else if (fields[1] !~ /^\//) { + next + } + for (field = 2; field <= count; field++) { + separator = index(fields[field], "=") + key = substr(fields[field], 1, separator - 1) + if (key != "name" && key != "driver" && + key != "module_candidates" && key != "device") { + continue + } + value = substr(fields[field], separator + 1) + values = split(value, names, ",") + for (item = 1; item <= values; item++) { + name = names[item] + if (name == "" || name == "unbound" || + name == "unresolved" || name == "unknown") { + continue + } + gsub(/_/, "-", name) + identities[name] = 1 + } + } + next + } + { + for (field = 1; field <= NF; field++) { + token = $field + sub(/:$/, "", token) + gsub(/_/, "-", token) + if (token in identities) { + print + break + } + } + } + ' "$@" "$iccd_snapshot" >"$iccd_output" || return 1 + return 0 +} + +# i2c_collect_driver_errors RESULT_DIR +# Collect explicit failures from all discovered I2C controller/client drivers, +# including optional cameras and drivers that later bind successfully. Reuses +# the retained kernel snapshot, without reading dmesg or changing hardware. +# Output: replaces i2c_driver_dmesg.log and i2c_driver_errors.log, no stdout. +# Returns: 0 when failures exist, 1 when clean, 2 when evidence is unavailable, +# 3 when collection/filtering fails. Ordinary dummy-regulator and successful +# initialization messages are retained as context but are not error evidence. +i2c_collect_driver_errors() { + icde_result_dir="$1" + icde_errors="$icde_result_dir/i2c_driver_errors.log" + : >"$icde_errors" || return 3 + [ -s "$icde_result_dir/dmesg_snapshot.log" ] || return 2 + i2c_collect_client_dmesg "$icde_result_dir" drivers || return 3 + + awk ' + { + message = tolower($0) + gsub(/(^|[^[:alnum:]_])(no|0)[[:space:]]+errors?([^[:alnum:]_]|$)/, " ", message) + gsub(/(^|[^[:alnum:]_])errors?[=:][[:space:]]*0([^[:alnum:]_]|$)/, " ", message) + gsub(/(^|[^[:alnum:]_])(timeouts?|nacks?|overruns?)[=:][[:space:]]*0([^[:alnum:]_]|$)/, " ", message) + if (message ~ /(^|[^[:alnum:]_])(error|errors|failed|failure|failures)([^[:alnum:]_]|$)/ || + message ~ /(^|[^[:alnum:]_])(timeout|nack|overrun)([^[:alnum:]_]|$)/ || + message ~ /timed out|unknown symbol|unable to (probe|read|load|initialize)/ || + message ~ /bus proto err|bus arbitration lost|illegal cmd|unknown i2c err/ || + message ~ /words read = .*words expected =|not enough clocks in dt|[rt]x channel not available/) { + print + } + } + ' "$icde_result_dir/i2c_driver_dmesg.log" >"$icde_errors" || return 3 + [ -s "$icde_errors" ] || return 1 + return 0 +} + ############################################################################### # I2C userspace-tool validation helpers. ############################################################################### diff --git a/Runner/utils/i2c_eeprom_read.sh b/Runner/utils/i2c_eeprom_read.sh new file mode 100755 index 000000000..ded323f02 --- /dev/null +++ b/Runner/utils/i2c_eeprom_read.sh @@ -0,0 +1,185 @@ +#!/bin/sh +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause + +# Read-only EEPROM helper for ramdisks, invoked with a bounded suite timeout. +# The caller owns stdout capture. Do not start a second asynchronous tee that +# could drop the machine-readable result marker while exiting. +# shellcheck disable=SC2034 # Consumed by init_env. +RUN_STDOUT_ENABLE=0 +# ---------- Repo env + helpers ---------- +SCRIPT_DIR="$( + cd "$(dirname "$0")" || exit 1 + pwd +)" +INIT_ENV="" +SEARCH="$SCRIPT_DIR" + +while [ "$SEARCH" != "/" ]; do + if [ -f "$SEARCH/init_env" ]; then + INIT_ENV="$SEARCH/init_env" + break + fi + SEARCH=$(dirname "$SEARCH") +done + +if [ -z "$INIT_ENV" ]; then + echo "[ERROR] Could not find init_env (starting at $SCRIPT_DIR)" >&2 + exit 1 +fi + +# Only source once (idempotent) +# NOTE: We intentionally **do not export** any new vars. They stay local to this shell. +if [ -z "${__INIT_ENV_LOADED:-}" ]; then + # shellcheck disable=SC1090 + . "$INIT_ENV" + __INIT_ENV_LOADED=1 +fi + +# shellcheck disable=SC1090 +. "$INIT_ENV" +# shellcheck disable=SC1091 +. "$TOOLS/functestlib.sh" +# shellcheck disable=SC1091 +. "$TOOLS/lib_bus.sh" + +# parse_args +# Parses the internal read-only helper interface, returns 1 for invalid input. +parse_args() { + read_mode="" + read_device=auto + read_offset="" + read_length="" + read_report="" + while [ "$#" -gt 0 ]; do + [ "$#" -ge 2 ] || return 1 + case "$1" in + --mode) + read_mode="$2" + ;; + --device) + read_device="$2" + ;; + --offset) + read_offset="$2" + ;; + --length) + read_length="$2" + ;; + --report) + read_report="$2" + ;; + *) + return 1 + ;; + esac + shift 2 + done + case "$read_mode" in + discover|probe|matrix|pm) + ;; + *) + return 1 + ;; + esac + [ -n "$read_report" ] +} + +parse_args "$@" || exit 3 +read_dir=${read_report%/*} +mkdir -p "$read_dir" || exit 1 +: >"$read_report" || exit 1 + +if ! bus_validation_require_commands stat awk grep readlink cat mktemp; then + printf 'I2C_EEPROM_RESULT status=SKIP reason=missing-image-provided-discovery-tools\n' + exit 2 +fi +if [ "$read_mode" != discover ]; then + if ! bus_validation_require_commands dd cmp sha256sum wc tr sleep; then + printf 'I2C_EEPROM_RESULT status=SKIP reason=missing-image-provided-read-tools\n' + exit 2 + fi + if ! dd if=/dev/null of=/dev/null count=0 iflag=fullblock 2>/dev/null; then + printf 'I2C_EEPROM_RESULT status=SKIP reason=dd-fullblock-support-required\n' + exit 2 + fi +fi +read_dir=$(mktemp -d "$read_dir/eeprom-read.XXXXXX") || exit 1 +log_info "[I2C-EEPROM] retained read-only artifacts=$read_dir" + +i2c_eeprom_select "$read_device" "$read_dir" +read_status=$? +if [ "$read_status" -ne 0 ]; then + if [ "$read_status" -eq 2 ]; then + printf 'I2C_EEPROM_RESULT status=SKIP reason=EEPROM-selection-unavailable\n' + fi + exit "$read_status" +fi + +if [ -z "$read_offset" ] && [ -z "$read_length" ]; then + read_offset=0 + read_length=$I2C_EEPROM_SIZE + if [ "$read_mode" = matrix ] || [ "$read_mode" = pm ]; then + [ "$read_length" -le 4096 ] || read_length=4096 + fi +else + [ -n "$read_offset" ] && [ -n "$read_length" ] || exit 3 +fi +read_offset=$(i2c_tools_value_decimal "$read_offset") || exit 3 +read_length=$(i2c_tools_value_decimal "$read_length") || exit 3 +if [ "$read_offset" -lt 0 ] || [ "$read_length" -le 0 ] || + [ "$read_offset" -gt "$I2C_EEPROM_SIZE" ] || + [ "$read_length" -gt "$((I2C_EEPROM_SIZE - read_offset))" ]; then + log_error "EEPROM range is invalid: offset=$read_offset length=$read_length size=$I2C_EEPROM_SIZE" + exit 3 +fi +if [ "$read_mode" != discover ] && [ "$read_length" -gt 1048576 ]; then + log_warn "Read-only shell validation is capped at 1 MiB, provide --eeprom-offset and --eeprom-length" + printf 'I2C_EEPROM_RESULT status=SKIP reason=bounded-read-range-required\n' + exit 2 +fi + +read_client_id=${I2C_EEPROM_CLIENT##*/} +printf 'key\tvalue\ndevice\t%s\nadapter\t%s\nsize_bytes\t%s\noffset\t%s\nlength\t%s\n' \ + "$I2C_EEPROM_SELECTED" "${read_client_id%%-*}" "$I2C_EEPROM_SIZE" \ + "$read_offset" "$read_length" >"$read_report" || exit 1 + +case "$read_mode" in + discover) + read_status=0 + ;; + probe) + i2c_eeprom_read_exact "$I2C_EEPROM_SELECTED" "$read_offset" "$read_length" "$read_dir/eeprom-read.bin" "$read_dir" + read_status=$? + if [ "$read_status" -eq 0 ]; then + read_digest=$(sha256sum "$read_dir/eeprom-read.bin") || exit 1 + printf 'original_sha256\t%s\n' "${read_digest%% *}" >>"$read_report" + log_info "[I2C-EEPROM-READ] bytes=$read_length sha256=${read_digest%% *} artifact=$read_dir/eeprom-read.bin" + fi + ;; + matrix) + i2c_eeprom_read_matrix "$I2C_EEPROM_SELECTED" "$read_offset" "$read_length" "$read_dir" + read_status=$? + ;; + pm) + : >"$read_dir/i2c_runtime_pm.tsv" || exit 1 + i2c_eeprom_read_after_idle "$I2C_EEPROM_SELECTED" "$I2C_EEPROM_CLIENT" "$read_offset" "$read_length" "$read_dir" + read_status=$? + ;; +esac + +case "$read_status" in + 0) + read_result=PASS + ;; + 2) + read_result=SKIP + ;; + *) + read_result=FAIL + ;; +esac +printf 'status\t%s\nmode\t%s\n' "$read_result" "$read_mode" >>"$read_report" || exit 1 +printf 'I2C_EEPROM_RESULT status=%s reason=read-only-%s device=%s length=%s report=%s\n' \ + "$read_result" "$read_mode" "$I2C_EEPROM_SELECTED" "$read_length" "$read_report" +exit "$read_status" diff --git a/Runner/utils/i2c_eeprom_runner.py b/Runner/utils/i2c_eeprom_runner.py index 9c686a1a9..b2d1e0ca4 100755 --- a/Runner/utils/i2c_eeprom_runner.py +++ b/Runner/utils/i2c_eeprom_runner.py @@ -195,7 +195,7 @@ def select_device( if requested != "auto": selected = os.path.realpath(requested) if not os.path.isfile(selected): - raise SkipTest("requested-eeprom-device-unavailable") + raise ValidationFailure("requested-eeprom-device-unavailable") selected_path = Path(requested) if selected_path.name == "eeprom" and discover_i2c_client_id(selected_path): return requested, "i2c-eeprom-sysfs" @@ -205,7 +205,7 @@ def select_device( and is_eeprom_nvmem(selected_path) ): return requested, "i2c-nvmem-sysfs" - raise SkipTest("requested-device-is-not-an-i2c-eeprom") + raise InvalidConfiguration("requested-device-is-not-an-i2c-eeprom") if not candidates: raise SkipTest("no-i2c-eeprom-sysfs-device") @@ -315,7 +315,7 @@ def validate_region(args: argparse.Namespace, size: int) -> Tuple[int, int]: def install_signal_handlers() -> None: - def interrupt(signum: int, _frame: object) -> None: + def interrupt(signum: int, frame: object) -> None: raise InterruptedError(f"signal-{signum}") signal.signal(signal.SIGINT, interrupt) @@ -332,7 +332,7 @@ def main() -> int: try: candidates = discover_candidates(args.sysfs_root) - candidate_paths = [path for path, _source in candidates] + candidate_paths = [path for path, source in candidates] base_report.update( { "candidate_count": len(candidate_paths), @@ -369,11 +369,11 @@ def main() -> int: try: fd = os.open(device, os.O_RDONLY | OPEN_CLOEXEC) except OSError as error: - raise SkipTest(f"eeprom-open-unavailable-{error.errno}") from error + raise ValidationFailure(f"eeprom-open-failed-{error.errno}") from error try: payload = read_exact(fd, offset, length) except (OSError, ValidationFailure) as error: - raise SkipTest("eeprom-read-unavailable") from error + raise ValidationFailure(f"eeprom-read-failed:{error}") from error finally: os.close(fd) @@ -391,7 +391,7 @@ def main() -> int: except OSError as error: if error.errno in (errno.EACCES, errno.EPERM, errno.EROFS): raise SkipTest("selected-eeprom-is-not-writable") from error - raise SkipTest(f"eeprom-open-unavailable-{error.errno}") from error + raise ValidationFailure(f"eeprom-open-failed-{error.errno}") from error original = b"" test_pattern = b"" @@ -402,7 +402,7 @@ def main() -> int: original = read_exact(fd, offset, length) except (OSError, ValidationFailure) as error: os.close(fd) - raise SkipTest("selected-eeprom-range-is-not-readable") from error + raise ValidationFailure(f"selected-eeprom-read-failed:{error}") from error try: test_pattern = bytes(value ^ 0xFF for value in original) diff --git a/Runner/utils/lib_bus.sh b/Runner/utils/lib_bus.sh index a0b27bcd3..3f53d8f28 100755 --- a/Runner/utils/lib_bus.sh +++ b/Runner/utils/lib_bus.sh @@ -21,6 +21,394 @@ bus_validation_require_commands() { return 0 } +# i2c_dt_path_enabled +# Returns 0 only when the node and all parents have absent/okay/ok status. +# No stdout or hardware changes. Resolves firmware symlinks before walking. +i2c_dt_path_enabled() { + idpe_path=$(readlink -f "$1" 2>/dev/null) || return 1 + [ -d "$idpe_path" ] || return 1 + while [ "$idpe_path" != / ]; do + if [ -f "$idpe_path/status" ]; then + idpe_status=$(dt_property_text "$idpe_path" status) || return 1 + case "$idpe_status" in + ''|okay|ok) + ;; + *) + return 1 + ;; + esac + fi + idpe_path=${idpe_path%/*} + [ -n "$idpe_path" ] || break + done +} + +# i2c_topology_issue +# Records a topology defect in the current report and sets its failure flag. +# Logs to stdout, no hardware changes. Called only by i2c_validate_topology. +i2c_topology_issue() { + ivt_failed=1 + printf 'FAIL\t%s\t%s\t%s\n' "$1" "$2" "$3" >>"$ivt_report" + log_fail "[I2C-TOPOLOGY] object=$1 expected=$2 observed=$3 artifact=$ivt_report" +} + +# i2c_topology_check_bus +# Reconciles immediate firmware clients with exact runtime of_node identities. +# Appends absent optional cameras for caller SKIPs. Never scans hardware. +i2c_topology_check_bus() { + itcb_bus="$1" + itcb_dir="$2" + itcb_policy="$3" + [ ! -d "$itcb_bus/i2c-bus" ] || itcb_bus="$itcb_bus/i2c-bus" + for itcb_child in "$itcb_bus"/*; do + [ -d "$itcb_child" ] || continue + [ -f "$itcb_child/compatible" ] || [ -f "$itcb_child/reg" ] || continue + case "${itcb_child##*/}" in + ports|port|port@*) + continue + ;; + esac + i2c_dt_path_enabled "$itcb_child" || continue + itcb_node=$(readlink -f "$itcb_child") || return 1 + if awk -F '\t' -v node="$itcb_node" ' + $1 == node { found=1 } END { exit !found } + ' "$itcb_dir/i2c_client_nodes.tsv"; then + continue + fi + if [ "$itcb_policy" = optional-camera ] && + i2c_client_is_camera_sensor "$itcb_node"; then + I2C_RUNTIME_OPTIONAL_CAMERA_COUNT=$((I2C_RUNTIME_OPTIONAL_CAMERA_COUNT + 1)) + printf '%s\t%s\t%s\n' "dt:${itcb_node##*/}" \ + "not-enumerated" "$itcb_node" >>"$itcb_dir/i2c_optional_camera_clients.tsv" || return 1 + log_warn "[I2C-TOPOLOGY] optional camera node=$itcb_node has no runtime client, captured probe errors still fail" + else + i2c_topology_issue "$itcb_node" runtime-i2c-client not-enumerated + fi + done +} + +# i2c_validate_topology +# Checks discovered controllers, exact DT adapter/client mappings and mux links. +# Requires the inventory TSVs and functestlib.sh. Returns 0 healthy, 1 faulty. +# Retains i2c_topology.tsv and node maps, logs findings, no hardware operations. +i2c_validate_topology() { + ivt_dir="$1" + ivt_policy="$2" + ivt_report="$ivt_dir/i2c_topology.tsv" + ivt_failed=0 + printf 'status\tobject\texpected\tobserved\n' >"$ivt_report" || return 1 + : >"$ivt_dir/i2c_adapter_nodes.tsv" || return 1 + : >"$ivt_dir/i2c_client_nodes.tsv" || return 1 + while IFS="$(printf '\t')" read -r ivt_id ivt_rest; do + [ -n "$ivt_id" ] || continue + ivt_adapter="/sys/bus/i2c/devices/$ivt_id" + [ -d "$ivt_adapter" ] || ivt_adapter="/sys/class/i2c-adapter/$ivt_id" + ivt_real=$(readlink -f "$ivt_adapter") || return 1 + ivt_node="" + if [ -L "$ivt_adapter/of_node" ]; then + ivt_node=$(readlink -f "$ivt_adapter/of_node") || return 1 + elif [ -L "/sys/class/i2c-adapter/$ivt_id/of_node" ]; then + ivt_node=$(readlink -f "/sys/class/i2c-adapter/$ivt_id/of_node") || return 1 + fi + printf '%s\t%s\t%s\n' "$ivt_node" "$ivt_id" "$ivt_real" >>"$ivt_dir/i2c_adapter_nodes.tsv" || return 1 + if [ -L "$ivt_adapter/mux_device" ] && [ ! -d "$ivt_adapter/mux_device" ]; then + i2c_topology_issue "$ivt_id" accessible-mux-device broken-link + fi + done <"$ivt_dir/i2c_adapters.tsv" + for ivt_client in /sys/bus/i2c/devices/[0-9]*-[0-9a-fA-F]*; do + [ -d "$ivt_client" ] || continue + if [ -L "$ivt_client/of_node" ]; then + ivt_node=$(readlink -f "$ivt_client/of_node") || return 1 + printf '%s\t%s\n' "$ivt_node" "${ivt_client##*/}" >>"$ivt_dir/i2c_client_nodes.tsv" || return 1 + fi + for ivt_link in "$ivt_client"/channel-*; do + [ -L "$ivt_link" ] || continue + [ -d "$ivt_link" ] || i2c_topology_issue "$ivt_link" runtime-mux-adapter broken-link + done + done + # shellcheck disable=SC2034 # Remaining controller columns are retained in the source inventory. + while IFS="$(printf '\t')" read -r ivt_node ivt_rest; do + [ -n "$ivt_node" ] || continue + ivt_device=$(find_platform_device_for_dt_node "$ivt_node" 2>/dev/null || true) + ivt_device=$(readlink -f "$ivt_device" 2>/dev/null || true) + ivt_count=0 + if [ -n "$ivt_device" ]; then + ivt_count=$(awk -F '\t' -v prefix="$ivt_device/" ' + index($3, prefix) == 1 { count++ } END { print count+0 } + ' "$ivt_dir/i2c_adapter_nodes.tsv") + fi + ivt_subbuses=0 + for ivt_bus in "$ivt_node"/i2c-bus@*; do + [ -d "$ivt_bus" ] || continue + i2c_dt_path_enabled "$ivt_bus" || continue + ivt_subbuses=$((ivt_subbuses + 1)) + if ! awk -F '\t' -v node="$ivt_bus" ' + $1 == node { found=1 } END { exit !found } + ' "$ivt_dir/i2c_adapter_nodes.tsv"; then + i2c_topology_issue "$ivt_bus" runtime-i2c-adapter not-enumerated + fi + done + ivt_compatible=$(dt_property_text "$ivt_node" compatible) + case "$ivt_compatible" in + *-cci*) + # A CCI parent may legitimately have no enabled master buses. + ;; + *) + [ "$ivt_count" -gt 0 ] || i2c_topology_issue "$ivt_node" owned-i2c-adapter none + ;; + esac + printf 'INFO\t%s\towned-adapters\t%s\n' "$ivt_node" "$ivt_count" >>"$ivt_report" || return 1 + log_info "[I2C-TOPOLOGY] controller=$ivt_node owned_adapters=$ivt_count enabled_child_buses=$ivt_subbuses" + done <"$ivt_dir/i2c_controllers.tsv" + while IFS="$(printf '\t')" read -r ivt_node ivt_id ivt_real; do + [ -d "$ivt_node" ] || continue + i2c_topology_check_bus "$ivt_node" "$ivt_dir" "$ivt_policy" || return 1 + done <"$ivt_dir/i2c_adapter_nodes.tsv" + # Mux channel nodes are buses, not I2C clients. Match their own firmware + # identities, never infer channel numbers from dynamically assigned bus IDs. + while IFS="$(printf '\t')" read -r ivt_node ivt_id; do + for ivt_bus in "$ivt_node"/i2c@* "$ivt_node"/i2c-mux/i2c@*; do + [ -d "$ivt_bus" ] || continue + i2c_dt_path_enabled "$ivt_bus" || continue + if ! awk -F '\t' -v node="$ivt_bus" ' + $1 == node { found=1 } END { exit !found } + ' "$ivt_dir/i2c_adapter_nodes.tsv"; then + i2c_topology_issue "$ivt_bus" runtime-mux-channel not-enumerated + fi + done + done <"$ivt_dir/i2c_client_nodes.tsv" + [ "$ivt_failed" -eq 0 ] +} + +# i2c_select_eeprom_adapter +# Prints a unique EEPROM adapter number, returns 1 when unavailable/ambiguous. +# Retains discovery artifacts, no stdout diagnostics or device transfers. +i2c_select_eeprom_adapter() { + isea_runner="$1/i2c_eeprom_read.sh" + isea_interpreter="sh" + if command -v python3 >/dev/null 2>&1; then + isea_runner="$1/i2c_eeprom_runner.py" + isea_interpreter=python3 + fi + isea_log="$2/i2c_eeprom_adapter_discovery.log" + isea_report="$2/i2c_eeprom_adapter_discovery.tsv" + [ -f "$isea_runner" ] || return 1 + run_with_timeout_log "$3" "$isea_log" "$isea_interpreter" "$isea_runner" \ + --mode discover --device "$4" --report "$isea_report" || return 1 + grep -q '^I2C_EEPROM_RESULT status=PASS ' "$isea_log" || return 1 + isea_adapter=$(awk -F '\t' '$1 == "adapter" { print $2; exit }' "$isea_report") + case "$isea_adapter" in + ''|*[!0-9]*) + return 1 + ;; + esac + [ -c "/dev/i2c-$isea_adapter" ] || return 1 + printf '%s\n' "$isea_adapter" +} + +# i2c_eeprom_client_path +# Prints the resolved owning I2C client, returns 1 if it cannot be identified. +# No logs or mutations. Used by the shell-only EEPROM discovery path. +i2c_eeprom_client_path() { + for iecp_candidate in "$1" "${1%/*}/device"; do + [ -e "$iecp_candidate" ] || continue + iecp_path=$(readlink -f "$iecp_candidate" 2>/dev/null) || return 1 + while [ -n "$iecp_path" ] && [ "$iecp_path" != / ]; do + if printf '%s\n' "${iecp_path##*/}" | grep -Eq '^[0-9]+-[0-9a-fA-F]{4}$'; then + printf '%s\n' "$iecp_path" + return 0 + fi + iecp_path=${iecp_path%/*} + done + done + return 1 +} + +# i2c_eeprom_select +# Sets I2C_EEPROM_SELECTED, I2C_EEPROM_CLIENT and I2C_EEPROM_SIZE. +# Retains candidates and logs selection. Returns 0 selected, 1 faulty, +# 2 absent/ambiguous, 3 invalid input. Reads metadata only, never device data. +i2c_eeprom_select() { + ies_request="$1" + ies_dir="$2" + I2C_EEPROM_SELECTED="" + I2C_EEPROM_CLIENT="" + I2C_EEPROM_SIZE="" + : >"$ies_dir/i2c_eeprom_candidates.tsv" || return 1 + if [ "$ies_request" = auto ]; then + set -- /sys/bus/i2c/devices/*/eeprom /sys/bus/nvmem/devices/*/nvmem + else + [ -f "$ies_request" ] || { + log_fail "Requested EEPROM does not exist: $ies_request" + return 1 + } + set -- "$ies_request" + fi + ies_seen=" " + ies_count=0 + for ies_file in "$@"; do + [ -f "$ies_file" ] || continue + case "${ies_file##*/}" in + eeprom) + ;; + nvmem) + ies_type="" + if [ -r "${ies_file%/*}/type" ]; then + ies_type=$(cat "${ies_file%/*}/type") || return 1 + fi + case "$ies_type" in + ''|EEPROM|eeprom) + ;; + *) + [ "$ies_request" = auto ] && continue + return 3 + ;; + esac + ;; + *) + return 3 + ;; + esac + ies_client=$(i2c_eeprom_client_path "$ies_file") || { + [ "$ies_request" = auto ] && continue + return 3 + } + case "$ies_seen" in + *" $ies_client "*) + continue + ;; + esac + ies_seen="$ies_seen$ies_client " + ies_count=$((ies_count + 1)) + printf '%s\t%s\n' "$ies_file" "$ies_client" >>"$ies_dir/i2c_eeprom_candidates.tsv" || return 1 + log_info "[I2C-EEPROM-CANDIDATE] path=$ies_file client=$ies_client" + I2C_EEPROM_SELECTED="$ies_file" + I2C_EEPROM_CLIENT="$ies_client" + done + if [ "$ies_count" -ne 1 ]; then + log_warn "EEPROM selection needs exactly one candidate, found=$ies_count, provide --eeprom-device for multiple candidates" + return 2 + fi + I2C_EEPROM_SIZE=$(stat -L -c %s "$I2C_EEPROM_SELECTED" 2>/dev/null) || return 1 + if [ "$I2C_EEPROM_SIZE" = 0 ]; then + for ies_size_file in "${I2C_EEPROM_SELECTED%/*}/size" "${I2C_EEPROM_SELECTED%/*}/../size"; do + [ -r "$ies_size_file" ] || continue + I2C_EEPROM_SIZE=$(cat "$ies_size_file") || return 1 + break + done + fi + I2C_EEPROM_SIZE=$(i2c_tools_value_decimal "$I2C_EEPROM_SIZE") || return 1 + [ "$I2C_EEPROM_SIZE" -gt 0 ] || return 2 + log_info "[I2C-EEPROM-SELECT] path=$I2C_EEPROM_SELECTED client=$I2C_EEPROM_CLIENT size=$I2C_EEPROM_SIZE source=$ies_request" +} + +# i2c_eeprom_read_exact +# Reads through the kernel driver with a finite timeout and verifies byte count. +# Uses the largest aligned block up to 4096 bytes, compatible with BusyBox dd. +# Returns 0 exact read, 1 failure. Retains output/error artifacts, no device writes. +i2c_eeprom_read_exact() { + iere_file="$1" + iere_offset="$2" + iere_length="$3" + iere_output="$4" + iere_dir="$5" + if ! [ "$iere_offset" -ge 0 ] 2>/dev/null || + ! [ "$iere_length" -gt 0 ] 2>/dev/null; then + log_fail "[I2C-EEPROM-READ] invalid offset=$iere_offset or length=$iere_length" + return 1 + fi + iere_block=4096 + for iere_value in "$iere_offset" "$iere_length"; do + while [ "$iere_value" -ne 0 ]; do + iere_remainder=$((iere_block % iere_value)) + iere_block=$iere_value + iere_value=$iere_remainder + done + done + run_with_managed_timeout 10 "$iere_dir" eeprom-read \ + dd iflag=fullblock "if=$iere_file" "of=$iere_output" "bs=$iere_block" \ + "skip=$((iere_offset / iere_block))" "count=$((iere_length / iere_block))" \ + >"$iere_output.log" 2>&1 + iere_status=$? + iere_bytes=$(wc -c <"$iere_output" 2>/dev/null | tr -d '[:space:]') + if [ "$iere_status" -ne 0 ] || [ "$iere_bytes" != "$iere_length" ]; then + log_fail "[I2C-EEPROM-READ] path=$iere_file offset=$iere_offset expected=$iere_length actual=${iere_bytes:-0} rc=$iere_status artifact=$iere_output.log" + log_file_with_label I2C-EEPROM-ERROR "$iere_output.log" 12 + return 1 + fi +} + +# i2c_eeprom_read_matrix +# Compares bounded repeated reads against a baseline at start, middle and end. +# Returns 0 exact matches, 1 failure. No claims about driver DMA/FIFO selection. +i2c_eeprom_read_matrix() { + ierm_file="$1" + ierm_offset="$2" + ierm_length="$3" + ierm_dir="$4" + i2c_eeprom_read_exact "$ierm_file" "$ierm_offset" "$ierm_length" "$ierm_dir/eeprom-baseline.bin" "$ierm_dir" || return 1 + ierm_case=0 + for ierm_size in 1 2 3 16 32 64 "$ierm_length"; do + [ "$ierm_size" -le "$ierm_length" ] || continue + for ierm_relative in 0 "$(((ierm_length - ierm_size) / 2))" "$((ierm_length - ierm_size))"; do + ierm_case=$((ierm_case + 1)) + ierm_expected="$ierm_dir/matrix-$ierm_case-expected.bin" + ierm_actual="$ierm_dir/matrix-$ierm_case-actual.bin" + i2c_eeprom_read_exact "$ierm_dir/eeprom-baseline.bin" "$ierm_relative" "$ierm_size" "$ierm_expected" "$ierm_dir" || return 1 + i2c_eeprom_read_exact "$ierm_file" "$((ierm_offset + ierm_relative))" "$ierm_size" "$ierm_actual" "$ierm_dir" || return 1 + if ! cmp -s "$ierm_expected" "$ierm_actual"; then + log_fail "[I2C-EEPROM-MATRIX] case=$ierm_case offset=$((ierm_offset + ierm_relative)) length=$ierm_size data-mismatch expected=$ierm_expected actual=$ierm_actual" + return 1 + fi + ierm_digest=$(sha256sum "$ierm_actual") || return 1 + log_info "[I2C-EEPROM-MATRIX] case=$ierm_case offset=$((ierm_offset + ierm_relative)) bytes=$ierm_size sha256=${ierm_digest%% *} exact-match artifact=$ierm_actual" + done + done +} + +# i2c_eeprom_read_after_idle +# Observes natural runtime suspend on the client/controller path for at most +# ten seconds, then verifies a fresh identical read. No power/control writes. +# Returns 0 verified, 1 transfer failure, 2 no observable natural suspend. +i2c_eeprom_read_after_idle() { + ierai_file="$1" + ierai_client="$2" + ierai_offset="$3" + ierai_length="$4" + ierai_dir="$5" + i2c_eeprom_read_exact "$ierai_file" "$ierai_offset" "$ierai_length" "$ierai_dir/pm-before.bin" "$ierai_dir" || return 1 + ierai_attempt=0 + ierai_suspended="" + while [ "$ierai_attempt" -lt 10 ]; do + ierai_path="$ierai_client" + while [ -n "$ierai_path" ] && [ "$ierai_path" != / ]; do + if [ -r "$ierai_path/power/runtime_status" ]; then + ierai_state=$(cat "$ierai_path/power/runtime_status") || return 1 + printf '%s\t%s\t%s\n' "$ierai_attempt" "$ierai_path" "$ierai_state" >>"$ierai_dir/i2c_runtime_pm.tsv" || return 1 + if [ "$ierai_state" = suspended ]; then + ierai_suspended="$ierai_path" + break + fi + fi + ierai_path=${ierai_path%/*} + done + [ -z "$ierai_suspended" ] || break + sleep 1 + ierai_attempt=$((ierai_attempt + 1)) + done + if [ -z "$ierai_suspended" ]; then + log_warn "[I2C-EEPROM-PM] no natural runtime suspend observed in 10 seconds, power policy unchanged" + return 2 + fi + i2c_eeprom_read_exact "$ierai_file" "$ierai_offset" "$ierai_length" "$ierai_dir/pm-after.bin" "$ierai_dir" || return 1 + if ! cmp -s "$ierai_dir/pm-before.bin" "$ierai_dir/pm-after.bin"; then + log_fail "[I2C-EEPROM-PM] post-idle data mismatch expected=$ierai_dir/pm-before.bin actual=$ierai_dir/pm-after.bin" + return 1 + fi + ierai_after=$(cat "$ierai_suspended/power/runtime_status" 2>/dev/null || printf '%s' unavailable) + log_info "[I2C-EEPROM-PM] suspended_object=$ierai_suspended subsequent_read=exact-match bytes=$ierai_length post_read_state=$ierai_after" +} + # bus_validation_bool_true # Returns success only for explicit affirmative fixture opt-in values. bus_validation_bool_true() {