mirror of
https://github.com/esp-rs/espflash.git
synced 2026-03-13 17:37:49 +00:00
Add xtask command for running HIL tests (#912)
* feat: Initial test * feat: Initial HIL tests in xtask * refactor: End test if possible before timeout * rebase * Add checking for output from monitor * CI: use xtask command instead of bash scripts * clippy * help find cargo * Try increase duration for failing test * remove bash tests * reviews * simplify * Add a local_espflash flag to allow running espflash without re-building (CI) and re-building (locally) * reviews --------- Co-authored-by: Sergio Gasquez <sergio.gasquez@gmail.com>
This commit is contained in:
parent
f679e03a8e
commit
b993a42fe4
58
.github/workflows/hil.yml
vendored
58
.github/workflows/hil.yml
vendored
@ -46,18 +46,29 @@ jobs:
|
||||
run: apt-get update && apt-get -y install curl musl-tools pkg-config
|
||||
|
||||
- name: Install toolchain
|
||||
run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
run: |
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
|
||||
- name: Build espflash
|
||||
run: $HOME/.cargo/bin/cargo build --release
|
||||
working-directory: espflash
|
||||
|
||||
- name: Build xtask
|
||||
run: $HOME/.cargo/bin/cargo build --release --locked
|
||||
working-directory: xtask
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: espflash
|
||||
path: target/release/espflash
|
||||
if-no-files-found: error
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: xtask
|
||||
path: target/release/xtask
|
||||
if-no-files-found: error
|
||||
|
||||
run-target:
|
||||
if: github.repository_owner == 'esp-rs'
|
||||
name: ${{ matrix.board.mcu }}${{ matrix.board.freq }}
|
||||
@ -95,44 +106,17 @@ jobs:
|
||||
name: espflash
|
||||
path: espflash_app
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: xtask
|
||||
path: xtask_app
|
||||
|
||||
- name: Set up espflash binary
|
||||
run: |
|
||||
chmod +x espflash_app/espflash
|
||||
echo "$PWD/espflash_app" >> "$GITHUB_PATH"
|
||||
chmod +x xtask_app/xtask
|
||||
echo "$PWD/xtask_app" >> "$GITHUB_PATH"
|
||||
|
||||
- name: board-info test
|
||||
run: timeout 10 bash espflash/tests/scripts/board-info.sh
|
||||
|
||||
- name: flash test
|
||||
run: timeout 80 bash espflash/tests/scripts/flash.sh ${{ matrix.board.mcu }}
|
||||
|
||||
- name: monitor test
|
||||
run: timeout 10 bash espflash/tests/scripts/monitor.sh
|
||||
|
||||
- name: erase-flash test
|
||||
run: timeout 60 bash espflash/tests/scripts/erase-flash.sh
|
||||
|
||||
- name: save-image/write-bin test
|
||||
run: |
|
||||
timeout 180 bash espflash/tests/scripts/save-image_write-bin.sh ${{ matrix.board.mcu }}
|
||||
|
||||
- name: erase-region test
|
||||
run: timeout 30 bash espflash/tests/scripts/erase-region.sh
|
||||
|
||||
- name: hold-in-reset test
|
||||
run: timeout 10 bash espflash/tests/scripts/hold-in-reset.sh
|
||||
|
||||
- name: reset test
|
||||
run: timeout 10 bash espflash/tests/scripts/reset.sh
|
||||
|
||||
- name: checksum-md5 test
|
||||
run: timeout 40 bash espflash/tests/scripts/checksum-md5.sh
|
||||
|
||||
- name: list-ports test
|
||||
run: timeout 10 bash espflash/tests/scripts/list-ports.sh
|
||||
|
||||
- name: write-bin test
|
||||
run: timeout 20 bash espflash/tests/scripts/write-bin.sh
|
||||
|
||||
- name: read-flash test
|
||||
run: timeout 60 bash espflash/tests/scripts/read-flash.sh
|
||||
- name: Run all tests
|
||||
run: xtask run-tests --chip ${{ matrix.board.mcu }} -t 60 --no-build
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,3 +1,6 @@
|
||||
# Generated during tests
|
||||
*.bin
|
||||
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
|
||||
@ -1,23 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Run the command and capture output and exit code
|
||||
result=$(espflash board-info)
|
||||
exit_code=$?
|
||||
echo "$result"
|
||||
|
||||
# Extract chip type
|
||||
chip_type=$(awk -F': *' '/Chip type:/ {print $2}' <<< "$result" | awk '{print $1}')
|
||||
|
||||
if [[ "$chip_type" == "esp32" ]]; then
|
||||
# ESP32 doesn't support get_security_info
|
||||
[[ $exit_code -eq 0 && "$result" =~ "Security features: None" ]] || {
|
||||
echo "Expected Security features: None"
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
# Non-ESP32 should contain required info
|
||||
[[ $exit_code -eq 0 && "$result" =~ "Security Information:" && "$result" =~ "Flags" ]] || {
|
||||
echo "Expected 'Security Information:' and 'Flags' in output but did not find them"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
@ -1,12 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
result=$(espflash erase-flash 2>&1)
|
||||
echo "$result"
|
||||
if [[ ! $result =~ "Flash has been erased!" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
result=$(espflash checksum-md5 0x1000 0x100 2>&1)
|
||||
echo "$result"
|
||||
if [[ ! $result =~ "0x827f263ef9fb63d05499d14fcef32f60" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
@ -1,17 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
result=$(espflash erase-flash 2>&1)
|
||||
echo "$result"
|
||||
if [[ ! $result =~ "Flash has been erased!" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
result=$(espflash read-flash 0 0x4000 flash_content.bin 2>&1)
|
||||
echo "$result"
|
||||
if [[ ! $result =~ "Flash content successfully read and written to" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
echo "Checking if flash is empty"
|
||||
if hexdump -v -e '/1 "%02x"' "flash_content.bin" | grep -qv '^ff*$'; then
|
||||
exit 1
|
||||
fi
|
||||
echo "Flash is empty!"
|
||||
@ -1,49 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Function to check expected failure for unaligned erase arguments
|
||||
check_unaligned_erase() {
|
||||
local address=$1
|
||||
local size=$2
|
||||
result=$(espflash erase-region "$address" "$size" 2>&1)
|
||||
echo "$result"
|
||||
|
||||
if [[ $result =~ "Invalid `address`" ]]; then
|
||||
echo "Unaligned erase correctly rejected: address=$address, size=$size"
|
||||
else
|
||||
echo "Test failed: unaligned erase was not rejected!"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Unaligned address (not a multiple of 4096)
|
||||
check_unaligned_erase 0x1001 0x1000
|
||||
|
||||
# Unaligned size (not a multiple of 4096)
|
||||
check_unaligned_erase 0x1000 0x1001
|
||||
|
||||
# Both address and size unaligned
|
||||
check_unaligned_erase 0x1003 0x1005
|
||||
|
||||
# Valid erase - should succeed
|
||||
result=$(espflash erase-region 0x1000 0x1000 2>&1)
|
||||
echo "$result"
|
||||
if [[ ! $result =~ "Erasing region at" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
result=$(espflash read-flash 0x1000 0x2000 flash_content.bin 2>&1)
|
||||
echo "$result"
|
||||
if [[ ! $result =~ "Flash content successfully read and written to" ]]; then
|
||||
echo "Failed to read flash contents"
|
||||
exit 1
|
||||
fi
|
||||
# Check first 0x1000 bytes are FF
|
||||
if head -c 4096 flash_content.bin | hexdump -v -e '/1 "%02x"' | grep -qv '^ff*$'; then
|
||||
echo "First 0x1000 bytes should be empty (FF)"
|
||||
exit 1
|
||||
fi
|
||||
# Check next 0x1000 bytes contain some non-FF bytes
|
||||
if ! tail -c 4096 flash_content.bin | hexdump -v -e '/1 "%02x"' | grep -q '[0-e]'; then
|
||||
echo "Next 0x1000 bytes should contain some non-FF bytes"
|
||||
exit 1
|
||||
fi
|
||||
echo "Flash contents verified!"
|
||||
@ -1,90 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
app="espflash/tests/data/$1"
|
||||
part_table="espflash/tests/data/partitions.csv"
|
||||
|
||||
# espflash should not flash a partition table that is too big
|
||||
result=$(timeout 15s espflash flash --no-skip --monitor --non-interactive $app --flash-size 2mb --partition-table $part_table 2>&1)
|
||||
echo "$result"
|
||||
if [[ $result =~ "Flashing has completed!" ]]; then
|
||||
echo "Flashing should have failed!"
|
||||
exit 1
|
||||
fi
|
||||
if [[ $result =~ "espflash::partition_table::does_not_fit" ]]; then
|
||||
echo "Flashing failed as expected!"
|
||||
else
|
||||
echo "Flashing has failed but not with the expected error!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$1" == "esp32c6" ]]; then
|
||||
# With manual log-format
|
||||
app_defmt="${app}_defmt"
|
||||
result=$(timeout 15s espflash flash --no-skip --monitor --non-interactive $app_defmt --log-format defmt 2>&1)
|
||||
echo "$result"
|
||||
if [[ ! $result =~ "Flashing has completed!" ]]; then
|
||||
echo "Flashing failed!"
|
||||
exit 1
|
||||
fi
|
||||
if ! echo "$result" | grep -q "Hello world!"; then
|
||||
echo "Monitoring failed!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# With auto-detected log-format
|
||||
result=$(timeout 15s espflash flash --no-skip --monitor --non-interactive $app_defmt 2>&1)
|
||||
echo "$result"
|
||||
if [[ ! $result =~ "Flashing has completed!" ]]; then
|
||||
echo "Flashing failed!"
|
||||
exit 1
|
||||
fi
|
||||
if ! echo "$result" | grep -q "Hello world!"; then
|
||||
echo "Monitoring failed!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Backtrace test
|
||||
app_backtrace="${app}_backtrace"
|
||||
|
||||
result=$(timeout 10s espflash flash --no-skip --monitor --non-interactive $app_backtrace 2>&1)
|
||||
echo "$result"
|
||||
if [[ ! $result =~ "Flashing has completed!" ]]; then
|
||||
echo "Flashing failed!"
|
||||
exit 1
|
||||
fi
|
||||
expected_strings=(
|
||||
"0x420012c8"
|
||||
"main"
|
||||
"esp32c6_backtrace/src/bin/main.rs:"
|
||||
"0x42001280"
|
||||
"hal_main"
|
||||
)
|
||||
for expected in "${expected_strings[@]}"; do
|
||||
if ! echo "$result" | grep -q "$expected"; then
|
||||
echo "Monitoring failed! Expected '$expected' not found in output."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
result=$(timeout 15s espflash flash --no-skip --monitor --non-interactive $app 2>&1)
|
||||
echo "$result"
|
||||
if [[ ! $result =~ "Flashing has completed!" ]]; then
|
||||
echo "Flashing failed!"
|
||||
exit 1
|
||||
fi
|
||||
if ! echo "$result" | grep -q "Hello world!"; then
|
||||
echo "Monitoring failed!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test with a higher baud rate
|
||||
result=$(timeout 15s espflash flash --no-skip --monitor --non-interactive --baud 921600 $app 2>&1 | tr -d '\0')
|
||||
echo "$result"
|
||||
if [[ ! $result =~ "Flashing has completed!" ]]; then
|
||||
echo "Flashing failed!"
|
||||
exit 1
|
||||
fi
|
||||
if ! echo "$result" | grep -q "Hello world!"; then
|
||||
echo "Monitoring failed!"
|
||||
exit 1
|
||||
fi
|
||||
@ -1,7 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
result=$(espflash hold-in-reset 2>&1)
|
||||
echo "$result"
|
||||
if [[ ! $result =~ "Holding target device in reset" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
@ -1,7 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
result=$(espflash list-ports 2>&1)
|
||||
echo "$result"
|
||||
if [[ ! $result =~ "Silicon Labs" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
@ -1,8 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo "Monitoring..."
|
||||
result=$(timeout 5s espflash monitor --non-interactive || true)
|
||||
echo "$result"
|
||||
if ! echo "$result" | grep -q "Hello world!"; then
|
||||
exit 1
|
||||
fi
|
||||
@ -1,53 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
KNOWN_PATTERN=$'\x01\xa0\x02\xB3\x04\xC4\x08\xD5\x10\xE6\x20\xF7\x40\x88\x50\x99'
|
||||
KNOWN_PATTERN+=$'\x60\xAA\x70\xBB\x80\xCC\x90\xDD\xA0\xEE\xB0\xFF\xC0\x11\xD0\x22'
|
||||
KNOWN_PATTERN+=$'\xE0\x33\xF0\x44\x05\x55\x15\x66\x25\x77\x35\x88\x45\x99\x55\xAA'
|
||||
KNOWN_PATTERN+=$'\x65\xBB\x75\xCC\x85\xDD\x95\xEE\xA5\xFF\xB5\x00\xC5\x11\xD5\x22'
|
||||
KNOWN_PATTERN+=$'\xE5\x33\xF5\x44\x06\x55\x16\x66\x26\x77\x36\x88\x46\x99\x56\xAA'
|
||||
KNOWN_PATTERN+=$'\x66\xBB\x76\xCC\x86\xDD\x96\xEE\xA6\xFF\xB6\x00\xC6\x11\xD6\x22'
|
||||
|
||||
echo -ne "$KNOWN_PATTERN" > pattern.bin
|
||||
result=$(espflash write-bin 0x0 pattern.bin 2>&1)
|
||||
echo "$result"
|
||||
if [[ ! $result =~ "Binary successfully written to flash!" ]]; then
|
||||
echo "Failed to write binary to flash"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
lengths=(2 5 10 26 44 86)
|
||||
|
||||
for len in "${lengths[@]}"; do
|
||||
echo "Testing read-flash with length: $len"
|
||||
|
||||
result=$(espflash read-flash 0 "$len" flash_content.bin 2>&1)
|
||||
echo "$result"
|
||||
if [[ ! $result =~ "Flash content successfully read and written to" ]]; then
|
||||
echo "Failed to read $len bytes from flash"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
EXPECTED=$(echo -ne "$KNOWN_PATTERN" | head -c "$len")
|
||||
|
||||
if ! cmp -s <(echo -ne "$EXPECTED") flash_content.bin; then
|
||||
echo "Verification failed: content does not match expected for length"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Testing ROM read-flash with length: $len"
|
||||
result=$(espflash read-flash --no-stub 0 "$len" flash_content.bin 2>&1)
|
||||
echo "$result"
|
||||
|
||||
if ! cmp -s <(echo -ne "$EXPECTED") flash_content.bin; then
|
||||
echo "Verification failed: content does not match expected for length"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! $result =~ "Flash content successfully read and written to" ]]; then
|
||||
echo "Failed to read $len bytes from flash"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
done
|
||||
|
||||
echo "All read-flash tests passed!"
|
||||
@ -1,7 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
result=$(espflash reset 2>&1)
|
||||
echo "$result"
|
||||
if [[ ! $result =~ "Resetting target device" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
@ -1,43 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
app="espflash/tests/data/$1"
|
||||
|
||||
# if $1 is esp32c2, create an variable that contains `-x 26mhz`
|
||||
if [[ $1 == "esp32c2" ]]; then
|
||||
freq="-x 26mhz"
|
||||
fi
|
||||
|
||||
result=$(espflash save-image --merge --chip $1 $freq $app app.bin 2>&1)
|
||||
echo "$result"
|
||||
if [[ ! $result =~ "Image successfully saved!" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
echo "Writing binary"
|
||||
result=$(timeout 90 espflash write-bin --monitor 0x0 app.bin --non-interactive 2>&1)
|
||||
echo "$result"
|
||||
if [[ ! $result =~ "Binary successfully written to flash!" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! echo "$result" | grep -q "Hello world!"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $1 == "esp32c6" ]]; then
|
||||
# Regression test for https://github.com/esp-rs/espflash/issues/741
|
||||
app="espflash/tests/data/esp_idf_firmware_c6.elf"
|
||||
|
||||
result=$(espflash save-image --merge --chip esp32c6 $app app.bin 2>&1)
|
||||
echo "$result"
|
||||
if [[ ! $result =~ "Image successfully saved!" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Checking that app descriptor is first"
|
||||
# Read 4 bytes from 0x10020, it needs to be 0xABCD5432
|
||||
|
||||
app_descriptor=$(xxd -p -s 0x10020 -l 4 app.bin)
|
||||
if [[ $app_descriptor != "3254cdab" ]]; then
|
||||
echo "App descriptor magic word is not correct: $app_descriptor"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
@ -1,23 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
part_table="espflash/tests/data/partitions.csv"
|
||||
|
||||
# https://github.com/esp-rs/espflash/issues/622 reproducer
|
||||
echo -ne "\x01\xa0" >binary_file.bin
|
||||
result=$(espflash write-bin 0x0 binary_file.bin 2>&1)
|
||||
echo "$result"
|
||||
if [[ ! $result =~ "Binary successfully written to flash!" ]]; then
|
||||
echo "Failed to write binary"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
result=$(espflash read-flash 0 64 flash_content.bin 2>&1)
|
||||
echo "$result"
|
||||
if [[ ! $result =~ "Flash content successfully read and written to" ]]; then
|
||||
echo "Failed to read flash content"
|
||||
exit 1
|
||||
fi
|
||||
# Check that the flash_content.bin contains the '01 a0' bytes
|
||||
if ! grep -q -a -F $'\x01\xa0' flash_content.bin; then
|
||||
echo "Failed verifying content"
|
||||
exit 1
|
||||
fi
|
||||
264
xtask/src/efuse_generator.rs
Normal file
264
xtask/src/efuse_generator.rs
Normal file
@ -0,0 +1,264 @@
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
collections::{BTreeMap, HashMap},
|
||||
ffi::OsStr,
|
||||
fs::{self, OpenOptions},
|
||||
io::{BufWriter, Write},
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use clap::{Args, Parser};
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Command-line Interface
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
enum Cli {
|
||||
/// Generate eFuse field definitions
|
||||
GenerateEfuseFields(GenerateEfuseFieldsArgs),
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub(crate) struct GenerateEfuseFieldsArgs {
|
||||
/// Local path to the `esptool` repository
|
||||
esptool_path: PathBuf,
|
||||
}
|
||||
|
||||
const HEADER: &str = r#"
|
||||
//! eFuse field definitions for the $CHIP
|
||||
//!
|
||||
//! This file was automatically generated, please do not edit it manually!
|
||||
//!
|
||||
//! Generated: $DATE
|
||||
//! Version: $VERSION
|
||||
|
||||
#![allow(unused)]
|
||||
|
||||
use super::EfuseField;
|
||||
|
||||
"#;
|
||||
|
||||
type EfuseFields = HashMap<String, EfuseYaml>;
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
struct EfuseYaml {
|
||||
#[serde(rename = "VER_NO")]
|
||||
version: String,
|
||||
#[serde(rename = "EFUSES")]
|
||||
fields: HashMap<String, EfuseAttrs>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
|
||||
struct EfuseAttrs {
|
||||
#[serde(rename = "blk")]
|
||||
block: u32,
|
||||
word: u32,
|
||||
len: u32,
|
||||
start: u32,
|
||||
#[serde(rename = "desc")]
|
||||
description: String,
|
||||
}
|
||||
|
||||
impl PartialOrd for EfuseAttrs {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for EfuseAttrs {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
match self.block.cmp(&other.block) {
|
||||
Ordering::Equal => {}
|
||||
ord => return ord,
|
||||
}
|
||||
|
||||
match self.word.cmp(&other.word) {
|
||||
Ordering::Equal => {}
|
||||
ord => return ord,
|
||||
}
|
||||
|
||||
self.start.cmp(&other.start)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn generate_efuse_fields(workspace: &Path, args: GenerateEfuseFieldsArgs) -> Result<()> {
|
||||
let efuse_yaml_path = args
|
||||
.esptool_path
|
||||
.join("espefuse")
|
||||
.join("efuse_defs")
|
||||
.canonicalize()?;
|
||||
|
||||
let espflash_path = workspace.join("espflash").canonicalize()?;
|
||||
|
||||
let mut efuse_fields = parse_efuse_fields(&efuse_yaml_path)?;
|
||||
process_efuse_definitions(&mut efuse_fields)?;
|
||||
generate_efuse_definitions(&espflash_path, efuse_fields)?;
|
||||
|
||||
Command::new("cargo")
|
||||
.args(["+nightly", "fmt"])
|
||||
.current_dir(workspace)
|
||||
.output()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_efuse_fields(efuse_yaml_path: &Path) -> Result<EfuseFields> {
|
||||
// TODO: We can probably handle this better, e.g. by defining a `Chip` enum
|
||||
// which can be iterated over, but for now this is good enough.
|
||||
const CHIPS: &[&str] = &[
|
||||
"esp32", "esp32c2", "esp32c3", "esp32c5", "esp32c6", "esp32h2", "esp32p4", "esp32s2",
|
||||
"esp32s3",
|
||||
];
|
||||
|
||||
let mut efuse_fields = EfuseFields::new();
|
||||
|
||||
for result in fs::read_dir(efuse_yaml_path)? {
|
||||
let path = result?.path();
|
||||
if path.extension().is_none_or(|ext| ext != OsStr::new("yaml")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let chip = path.file_stem().unwrap().to_string_lossy().to_string();
|
||||
if !CHIPS.contains(&chip.as_str()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let efuse_yaml = fs::read_to_string(&path)?;
|
||||
let efuse_yaml: EfuseYaml = serde_yaml::from_str(&efuse_yaml)?;
|
||||
|
||||
efuse_fields.insert(chip.to_string(), efuse_yaml);
|
||||
}
|
||||
|
||||
Ok(efuse_fields)
|
||||
}
|
||||
|
||||
fn process_efuse_definitions(efuse_fields: &mut EfuseFields) -> Result<()> {
|
||||
// This is all a special case for the MAC field, which is larger than a single
|
||||
// word (i.e. 32-bits) in size. To handle this, we just split it up into two
|
||||
// separate fields, and update the fields' attributes accordingly.
|
||||
for yaml in (*efuse_fields).values_mut() {
|
||||
let mac_attrs = yaml.fields.get("MAC").unwrap();
|
||||
|
||||
let mut mac0_attrs = mac_attrs.clone();
|
||||
mac0_attrs.len = 32;
|
||||
|
||||
let mut mac1_attrs = mac_attrs.clone();
|
||||
mac1_attrs.start = mac0_attrs.start + 32;
|
||||
mac1_attrs.word = mac1_attrs.start / 32;
|
||||
mac1_attrs.len = 16;
|
||||
|
||||
yaml.fields.remove("MAC").unwrap();
|
||||
yaml.fields.insert("MAC0".into(), mac0_attrs);
|
||||
yaml.fields.insert("MAC1".into(), mac1_attrs);
|
||||
}
|
||||
|
||||
// The ESP32-S2 seems to be missing a reserved byte at the end of BLOCK0
|
||||
// (Or, something else weird is going on).
|
||||
efuse_fields.entry("esp32s2".into()).and_modify(|yaml| {
|
||||
yaml.fields
|
||||
.entry("RESERVED_0_162".into())
|
||||
.and_modify(|field| field.len = 30);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_efuse_definitions(espflash_path: &Path, efuse_fields: EfuseFields) -> Result<()> {
|
||||
let targets_efuse_path = espflash_path
|
||||
.join("src")
|
||||
.join("target")
|
||||
.join("efuse")
|
||||
.canonicalize()?;
|
||||
|
||||
for (chip, yaml) in efuse_fields {
|
||||
let f = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(targets_efuse_path.join(format!("{chip}.rs")))?;
|
||||
|
||||
let mut writer = BufWriter::new(f);
|
||||
|
||||
write!(
|
||||
writer,
|
||||
"{}",
|
||||
HEADER
|
||||
.replace("$CHIP", &chip)
|
||||
.replace(
|
||||
"$DATE",
|
||||
&chrono::Utc::now().format("%Y-%m-%d %H:%M").to_string()
|
||||
)
|
||||
.replace("$VERSION", &yaml.version)
|
||||
.trim_start()
|
||||
)?;
|
||||
|
||||
generate_efuse_block_sizes(&mut writer, &yaml.fields)?;
|
||||
generate_efuse_constants(&mut writer, &yaml.fields)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_efuse_block_sizes(
|
||||
writer: &mut dyn Write,
|
||||
fields: &HashMap<String, EfuseAttrs>,
|
||||
) -> Result<()> {
|
||||
let mut field_attrs = fields.values().collect::<Vec<_>>();
|
||||
field_attrs.sort();
|
||||
|
||||
let block_sizes = field_attrs
|
||||
.chunk_by(|a, b| a.block == b.block)
|
||||
.enumerate()
|
||||
.map(|(block, attrs)| {
|
||||
let last = attrs.last().unwrap();
|
||||
let size_bits = last.start + last.len;
|
||||
assert!(size_bits % 8 == 0);
|
||||
|
||||
(block, size_bits / 8)
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
writeln!(writer, "/// Total size in bytes of each block")?;
|
||||
writeln!(
|
||||
writer,
|
||||
"pub(crate) const BLOCK_SIZES: &[u32] = &[{}];\n",
|
||||
block_sizes
|
||||
.values()
|
||||
.map(|v| v.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_efuse_constants(
|
||||
writer: &mut dyn Write,
|
||||
fields: &HashMap<String, EfuseAttrs>,
|
||||
) -> Result<()> {
|
||||
let mut sorted = fields.iter().collect::<Vec<_>>();
|
||||
sorted.sort_by(|a, b| (a.1).cmp(b.1));
|
||||
|
||||
for (name, attrs) in sorted {
|
||||
let EfuseAttrs {
|
||||
block,
|
||||
word,
|
||||
len,
|
||||
start,
|
||||
description,
|
||||
} = attrs;
|
||||
|
||||
let description = description.replace('[', "\\[").replace(']', "\\]");
|
||||
|
||||
writeln!(writer, "/// {description}")?;
|
||||
writeln!(
|
||||
writer,
|
||||
"pub const {name}: EfuseField = EfuseField::new({block}, {word}, {start}, {len});"
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -1,16 +1,13 @@
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
collections::{BTreeMap, HashMap},
|
||||
ffi::OsStr,
|
||||
fs::{self, OpenOptions},
|
||||
io::{BufWriter, Write},
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
use std::{env, path::PathBuf};
|
||||
|
||||
use clap::{Args, Parser};
|
||||
use clap::Parser;
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
// Import modules
|
||||
mod efuse_generator;
|
||||
mod test_runner;
|
||||
|
||||
// Type definition for results
|
||||
pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Command-line Interface
|
||||
@ -18,13 +15,10 @@ type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
#[derive(Debug, Parser)]
|
||||
enum Cli {
|
||||
/// Generate eFuse field definitions
|
||||
GenerateEfuseFields(GenerateEfuseFieldsArgs),
|
||||
}
|
||||
GenerateEfuseFields(efuse_generator::GenerateEfuseFieldsArgs),
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
struct GenerateEfuseFieldsArgs {
|
||||
/// Local path to the `esptool` repository
|
||||
esptool_path: PathBuf,
|
||||
/// Run espflash tests
|
||||
RunTests(test_runner::RunTestsArgs),
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@ -35,251 +29,27 @@ fn main() -> Result<()> {
|
||||
.filter_module("xtask", log::LevelFilter::Info)
|
||||
.init();
|
||||
|
||||
// The directory containing the cargo manifest for the 'xtask' package is a
|
||||
// subdirectory within the cargo workspace:
|
||||
let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let workspace = workspace.parent().unwrap().canonicalize()?;
|
||||
// Determine the path to the workspace (i.e. the root of the repository).
|
||||
// At compile-time we know where the `xtask` crate lives, but that absolute
|
||||
// path may not exist at runtime once the binary is distributed as an
|
||||
// artefact and executed on a different machine (e.g. a self-hosted CI
|
||||
// runner). Therefore we
|
||||
// 1. Try the compile-time location first.
|
||||
// 2. Fallback to the current working directory if that fails.
|
||||
|
||||
let workspace_from_build = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.expect("`CARGO_MANIFEST_DIR` should always have a parent")
|
||||
.to_path_buf();
|
||||
|
||||
let workspace = if workspace_from_build.exists() {
|
||||
workspace_from_build.canonicalize()?
|
||||
} else {
|
||||
env::current_dir()?.canonicalize()?
|
||||
};
|
||||
|
||||
match Cli::parse() {
|
||||
Cli::GenerateEfuseFields(args) => generate_efuse_fields(&workspace, args),
|
||||
Cli::GenerateEfuseFields(args) => efuse_generator::generate_efuse_fields(&workspace, args),
|
||||
Cli::RunTests(args) => test_runner::run_tests(&workspace, args),
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Generate eFuse Fields
|
||||
|
||||
const HEADER: &str = r#"
|
||||
//! eFuse field definitions for the $CHIP
|
||||
//!
|
||||
//! This file was automatically generated, please do not edit it manually!
|
||||
//!
|
||||
//! Generated: $DATE
|
||||
//! Version: $VERSION
|
||||
|
||||
#![allow(unused)]
|
||||
|
||||
use super::EfuseField;
|
||||
|
||||
"#;
|
||||
|
||||
type EfuseFields = HashMap<String, EfuseYaml>;
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
struct EfuseYaml {
|
||||
#[serde(rename = "VER_NO")]
|
||||
version: String,
|
||||
#[serde(rename = "EFUSES")]
|
||||
fields: HashMap<String, EfuseAttrs>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
|
||||
struct EfuseAttrs {
|
||||
#[serde(rename = "blk")]
|
||||
block: u32,
|
||||
word: u32,
|
||||
len: u32,
|
||||
start: u32,
|
||||
#[serde(rename = "desc")]
|
||||
description: String,
|
||||
}
|
||||
|
||||
impl PartialOrd for EfuseAttrs {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for EfuseAttrs {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
match self.block.cmp(&other.block) {
|
||||
Ordering::Equal => {}
|
||||
ord => return ord,
|
||||
}
|
||||
|
||||
match self.word.cmp(&other.word) {
|
||||
Ordering::Equal => {}
|
||||
ord => return ord,
|
||||
}
|
||||
|
||||
self.start.cmp(&other.start)
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_efuse_fields(workspace: &Path, args: GenerateEfuseFieldsArgs) -> Result<()> {
|
||||
let efuse_yaml_path = args
|
||||
.esptool_path
|
||||
.join("espefuse")
|
||||
.join("efuse_defs")
|
||||
.canonicalize()?;
|
||||
|
||||
let espflash_path = workspace.join("espflash").canonicalize()?;
|
||||
|
||||
let mut efuse_fields = parse_efuse_fields(&efuse_yaml_path)?;
|
||||
process_efuse_definitions(&mut efuse_fields)?;
|
||||
generate_efuse_definitions(&espflash_path, efuse_fields)?;
|
||||
|
||||
Command::new("cargo")
|
||||
.args(["+nightly", "fmt"])
|
||||
.current_dir(workspace)
|
||||
.output()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_efuse_fields(efuse_yaml_path: &Path) -> Result<EfuseFields> {
|
||||
// TODO: We can probably handle this better, e.g. by defining a `Chip` enum
|
||||
// which can be iterated over, but for now this is good enough.
|
||||
const CHIPS: &[&str] = &[
|
||||
"esp32", "esp32c2", "esp32c3", "esp32c5", "esp32c6", "esp32h2", "esp32p4", "esp32s2",
|
||||
"esp32s3",
|
||||
];
|
||||
|
||||
let mut efuse_fields = EfuseFields::new();
|
||||
|
||||
for result in fs::read_dir(efuse_yaml_path)? {
|
||||
let path = result?.path();
|
||||
if path.extension().is_none_or(|ext| ext != OsStr::new("yaml")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let chip = path.file_stem().unwrap().to_string_lossy().to_string();
|
||||
if !CHIPS.contains(&chip.as_str()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let efuse_yaml = fs::read_to_string(&path)?;
|
||||
let efuse_yaml: EfuseYaml = serde_yaml::from_str(&efuse_yaml)?;
|
||||
|
||||
efuse_fields.insert(chip.to_string(), efuse_yaml);
|
||||
}
|
||||
|
||||
Ok(efuse_fields)
|
||||
}
|
||||
|
||||
fn process_efuse_definitions(efuse_fields: &mut EfuseFields) -> Result<()> {
|
||||
// This is all a special case for the MAC field, which is larger than a single
|
||||
// word (i.e. 32-bits) in size. To handle this, we just split it up into two
|
||||
// separate fields, and update the fields' attributes accordingly.
|
||||
for yaml in (*efuse_fields).values_mut() {
|
||||
let mac_attrs = yaml.fields.get("MAC").unwrap();
|
||||
|
||||
let mut mac0_attrs = mac_attrs.clone();
|
||||
mac0_attrs.len = 32;
|
||||
|
||||
let mut mac1_attrs = mac_attrs.clone();
|
||||
mac1_attrs.start = mac0_attrs.start + 32;
|
||||
mac1_attrs.word = mac1_attrs.start / 32;
|
||||
mac1_attrs.len = 16;
|
||||
|
||||
yaml.fields.remove("MAC").unwrap();
|
||||
yaml.fields.insert("MAC0".into(), mac0_attrs);
|
||||
yaml.fields.insert("MAC1".into(), mac1_attrs);
|
||||
}
|
||||
|
||||
// The ESP32-S2 seems to be missing a reserved byte at the end of BLOCK0
|
||||
// (Or, something else weird is going on).
|
||||
efuse_fields.entry("esp32s2".into()).and_modify(|yaml| {
|
||||
yaml.fields
|
||||
.entry("RESERVED_0_162".into())
|
||||
.and_modify(|field| field.len = 30);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_efuse_definitions(espflash_path: &Path, efuse_fields: EfuseFields) -> Result<()> {
|
||||
let targets_efuse_path = espflash_path
|
||||
.join("src")
|
||||
.join("target")
|
||||
.join("efuse")
|
||||
.canonicalize()?;
|
||||
|
||||
for (chip, yaml) in efuse_fields {
|
||||
let f = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(targets_efuse_path.join(format!("{chip}.rs")))?;
|
||||
|
||||
let mut writer = BufWriter::new(f);
|
||||
|
||||
write!(
|
||||
writer,
|
||||
"{}",
|
||||
HEADER
|
||||
.replace("$CHIP", &chip)
|
||||
.replace(
|
||||
"$DATE",
|
||||
&chrono::Utc::now().format("%Y-%m-%d %H:%M").to_string()
|
||||
)
|
||||
.replace("$VERSION", &yaml.version)
|
||||
.trim_start()
|
||||
)?;
|
||||
|
||||
generate_efuse_block_sizes(&mut writer, &yaml.fields)?;
|
||||
generate_efuse_constants(&mut writer, &yaml.fields)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_efuse_block_sizes(
|
||||
writer: &mut dyn Write,
|
||||
fields: &HashMap<String, EfuseAttrs>,
|
||||
) -> Result<()> {
|
||||
let mut field_attrs = fields.values().collect::<Vec<_>>();
|
||||
field_attrs.sort();
|
||||
|
||||
let block_sizes = field_attrs
|
||||
.chunk_by(|a, b| a.block == b.block)
|
||||
.enumerate()
|
||||
.map(|(block, attrs)| {
|
||||
let last = attrs.last().unwrap();
|
||||
let size_bits = last.start + last.len;
|
||||
assert!(size_bits % 8 == 0);
|
||||
|
||||
(block, size_bits / 8)
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
writeln!(writer, "/// Total size in bytes of each block")?;
|
||||
writeln!(
|
||||
writer,
|
||||
"pub(crate) const BLOCK_SIZES: &[u32] = &[{}];\n",
|
||||
block_sizes
|
||||
.values()
|
||||
.map(|v| v.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_efuse_constants(
|
||||
writer: &mut dyn Write,
|
||||
fields: &HashMap<String, EfuseAttrs>,
|
||||
) -> Result<()> {
|
||||
let mut sorted = fields.iter().collect::<Vec<_>>();
|
||||
sorted.sort_by(|a, b| (a.1).cmp(b.1));
|
||||
|
||||
for (name, attrs) in sorted {
|
||||
let EfuseAttrs {
|
||||
block,
|
||||
word,
|
||||
len,
|
||||
start,
|
||||
description,
|
||||
} = attrs;
|
||||
|
||||
let description = description.replace('[', "\\[").replace(']', "\\]");
|
||||
|
||||
writeln!(writer, "/// {description}")?;
|
||||
writeln!(
|
||||
writer,
|
||||
"pub const {name}: EfuseField = EfuseField::new({block}, {word}, {start}, {len});"
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
953
xtask/src/test_runner.rs
Normal file
953
xtask/src/test_runner.rs
Normal file
@ -0,0 +1,953 @@
|
||||
use std::{
|
||||
fs,
|
||||
io::{BufRead, BufReader},
|
||||
path::{Path, PathBuf},
|
||||
process::{Child, Command, Stdio},
|
||||
sync::{
|
||||
Arc,
|
||||
Mutex,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use clap::{ArgAction, Args};
|
||||
use log::info;
|
||||
|
||||
use crate::Result;
|
||||
|
||||
type SpawnedCommandOutput = (
|
||||
Child,
|
||||
Arc<Mutex<String>>,
|
||||
thread::JoinHandle<()>,
|
||||
thread::JoinHandle<()>,
|
||||
);
|
||||
/// Arguments for running tests
|
||||
#[derive(Debug, Args)]
|
||||
pub struct RunTestsArgs {
|
||||
/// Which test to run (or "all" to run all tests)
|
||||
#[clap(default_value = "all")]
|
||||
pub test: String,
|
||||
|
||||
/// Chip target
|
||||
#[clap(short, long)]
|
||||
pub chip: Option<String>,
|
||||
|
||||
/// Timeout for test commands in seconds
|
||||
#[clap(short, long, default_value = "15")]
|
||||
pub timeout: u64,
|
||||
|
||||
/// Whether to build espflash before running tests, true by default
|
||||
#[arg(long = "no-build", action = ArgAction::SetFalse, default_value_t = true)]
|
||||
pub build_espflash: bool,
|
||||
}
|
||||
|
||||
/// A struct to manage and run tests for the espflash
|
||||
pub struct TestRunner {
|
||||
/// The workspace directory where the tests are located
|
||||
pub workspace: PathBuf,
|
||||
/// The directory containing the test files
|
||||
pub tests_dir: PathBuf,
|
||||
/// Timeout for test commands
|
||||
pub timeout: Duration,
|
||||
/// Optional chip target for tests
|
||||
pub chip: Option<String>,
|
||||
/// Build espflash before running tests
|
||||
pub build_espflash: bool,
|
||||
}
|
||||
|
||||
impl TestRunner {
|
||||
/// Creates a new [TestRunner] instance
|
||||
pub fn new(
|
||||
workspace: &Path,
|
||||
tests_dir: PathBuf,
|
||||
timeout_secs: u64,
|
||||
build_espflash: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
workspace: workspace.to_path_buf(),
|
||||
tests_dir,
|
||||
timeout: Duration::from_secs(timeout_secs),
|
||||
chip: None,
|
||||
build_espflash,
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_command(&self, cmd: &mut Command) {
|
||||
cmd.current_dir(&self.workspace)
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit());
|
||||
}
|
||||
|
||||
fn terminate_process(child: &mut Option<&mut Child>) {
|
||||
if let Some(child_proc) = child {
|
||||
let _ = child_proc.kill();
|
||||
|
||||
// Wait for the process to terminate
|
||||
if let Some(child_proc) = child {
|
||||
let _ = child_proc.wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn restore_terminal() {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let _ = Command::new("stty").arg("sane").status();
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_and_capture_output(cmd: &mut Command) -> Result<SpawnedCommandOutput> {
|
||||
info!("Spawning command: {cmd:?}");
|
||||
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
|
||||
let mut child = cmd.spawn()?;
|
||||
let stdout = child.stdout.take().expect("Failed to capture stdout");
|
||||
let stderr = child.stderr.take().expect("Failed to capture stderr");
|
||||
|
||||
let output = Arc::new(Mutex::new(String::new()));
|
||||
let out_clone1 = Arc::clone(&output);
|
||||
let out_clone2 = Arc::clone(&output);
|
||||
|
||||
let stdout_handle = thread::spawn(move || {
|
||||
let reader = BufReader::new(stdout);
|
||||
for line in reader.lines().map_while(|line| line.ok()) {
|
||||
println!("{line}");
|
||||
out_clone1.lock().unwrap().push_str(&line);
|
||||
out_clone1.lock().unwrap().push('\n');
|
||||
}
|
||||
});
|
||||
|
||||
let stderr_handle = thread::spawn(move || {
|
||||
let reader = BufReader::new(stderr);
|
||||
for line in reader.lines().map_while(|line| line.ok()) {
|
||||
println!("{line}");
|
||||
out_clone2.lock().unwrap().push_str(&line);
|
||||
out_clone2.lock().unwrap().push('\n');
|
||||
}
|
||||
});
|
||||
|
||||
Ok((child, output, stdout_handle, stderr_handle))
|
||||
}
|
||||
|
||||
/// Runs a command with a timeout, returning the exit code
|
||||
pub fn run_command_with_timeout(&self, cmd: &mut Command, timeout: Duration) -> Result<i32> {
|
||||
log::debug!("Running command: {cmd:?}");
|
||||
self.setup_command(cmd);
|
||||
|
||||
let mut child = cmd.spawn()?;
|
||||
let completed = Arc::new(AtomicBool::new(false));
|
||||
let child_id = child.id();
|
||||
let completed_clone = Arc::clone(&completed);
|
||||
|
||||
let timer = thread::spawn(move || {
|
||||
let interval = Duration::from_millis(100);
|
||||
let mut elapsed = Duration::ZERO;
|
||||
|
||||
while elapsed < timeout {
|
||||
thread::sleep(interval);
|
||||
elapsed += interval;
|
||||
if completed_clone.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
log::warn!("Command timed out after {timeout:?}, killing process {child_id}");
|
||||
Self::terminate_process(&mut None);
|
||||
});
|
||||
|
||||
let status = match child.wait() {
|
||||
Ok(s) => {
|
||||
completed.store(true, Ordering::SeqCst);
|
||||
s
|
||||
}
|
||||
Err(e) => {
|
||||
completed.store(true, Ordering::SeqCst);
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
return Err(format!("Command execution failed: {e}").into());
|
||||
}
|
||||
};
|
||||
|
||||
let _ = timer.join();
|
||||
let exit_code = status.code().unwrap_or(1);
|
||||
log::debug!("Command exit code: {exit_code}");
|
||||
Ok(exit_code)
|
||||
}
|
||||
|
||||
/// Runs a command for a specified duration, returning whether it terminated
|
||||
/// naturally
|
||||
pub fn run_command_for(&self, cmd: &mut Command, duration: Duration) -> Result<bool> {
|
||||
log::debug!("Running command: {cmd:?}");
|
||||
let mut child = cmd.spawn()?;
|
||||
let start_time = Instant::now();
|
||||
let mut naturally_terminated = false;
|
||||
|
||||
if let Ok(Some(_)) = child.try_wait() {
|
||||
naturally_terminated = true;
|
||||
} else {
|
||||
thread::sleep(duration);
|
||||
if let Ok(Some(_)) = child.try_wait() {
|
||||
naturally_terminated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !naturally_terminated {
|
||||
log::info!(
|
||||
"Command ran for {:?}, terminating process {}",
|
||||
start_time.elapsed(),
|
||||
child.id()
|
||||
);
|
||||
Self::terminate_process(&mut Some(&mut child));
|
||||
}
|
||||
|
||||
Self::restore_terminal();
|
||||
log::debug!("Command completed after {:?}", start_time.elapsed());
|
||||
|
||||
Ok(naturally_terminated)
|
||||
}
|
||||
|
||||
fn build_espflash(&self) {
|
||||
let mut cmd = Command::new("cargo");
|
||||
|
||||
log::info!("Building espflash...");
|
||||
cmd.args(["build", "-p", "espflash", "--release", "--"]);
|
||||
|
||||
let status = cmd.status().expect("Failed to build espflash");
|
||||
if !status.success() {
|
||||
panic!("espflash build failed with status: {status}");
|
||||
}
|
||||
}
|
||||
|
||||
fn create_espflash_command(&self, args: &[&str]) -> Command {
|
||||
let mut cmd = Command::new("cargo");
|
||||
|
||||
// we need to distinguish between local and CI runs, on CI we are building
|
||||
// espflash and then copying the binary, so we can use just `espflash`
|
||||
match self.build_espflash {
|
||||
true => {
|
||||
log::info!("Running cargo run...");
|
||||
cmd.args(["run", "-p", "espflash", "--release", "--quiet", "--"]);
|
||||
}
|
||||
false => {
|
||||
log::info!("Using system espflash");
|
||||
let mut cmd = Command::new("espflash");
|
||||
cmd.args(args);
|
||||
return cmd;
|
||||
}
|
||||
}
|
||||
|
||||
cmd.args(args);
|
||||
|
||||
cmd
|
||||
}
|
||||
|
||||
/// Runs a simple command test, capturing output and checking for expected
|
||||
/// outputs
|
||||
pub fn run_simple_command_test(
|
||||
&self,
|
||||
args: &[&str],
|
||||
expected_contains: Option<&[&str]>,
|
||||
timeout: Duration,
|
||||
test_name: &str,
|
||||
) -> Result<()> {
|
||||
log::info!("Running {test_name} test");
|
||||
let mut cmd = self.create_espflash_command(args);
|
||||
|
||||
if let Some(expected) = expected_contains {
|
||||
let (mut child, output, h1, h2) = Self::spawn_and_capture_output(&mut cmd)?;
|
||||
let start_time = Instant::now();
|
||||
let mut terminated_naturally = false;
|
||||
|
||||
while start_time.elapsed() < timeout {
|
||||
if let Ok(Some(_)) = child.try_wait() {
|
||||
terminated_naturally = true;
|
||||
break;
|
||||
}
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
|
||||
// If still running, kill it
|
||||
if !terminated_naturally {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
}
|
||||
|
||||
let _ = h1.join();
|
||||
let _ = h2.join();
|
||||
|
||||
let output = output.lock().unwrap();
|
||||
for &expected in expected {
|
||||
if !output.contains(expected) {
|
||||
Self::restore_terminal();
|
||||
return Err(format!("Missing expected output: {expected}").into());
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("{test_name} test passed and output verified");
|
||||
} else {
|
||||
let exit_code = self.run_command_with_timeout(&mut cmd, timeout)?;
|
||||
if exit_code != 0 {
|
||||
return Err(
|
||||
format!("{test_name} test failed: non-zero exit code {exit_code}").into(),
|
||||
);
|
||||
}
|
||||
|
||||
log::info!("{test_name} test passed with exit code 0");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Runs a timed command test, capturing output and checking for expected
|
||||
/// outputs after a specified duration
|
||||
pub fn run_timed_command_test(
|
||||
&self,
|
||||
args: &[&str],
|
||||
expected_contains: Option<&[&str]>,
|
||||
duration: Duration,
|
||||
test_name: &str,
|
||||
) -> Result<()> {
|
||||
log::info!("Running {test_name} test");
|
||||
let mut cmd = self.create_espflash_command(args);
|
||||
|
||||
if let Some(expected) = expected_contains {
|
||||
let (mut child, output, h1, h2) = Self::spawn_and_capture_output(&mut cmd)?;
|
||||
thread::sleep(duration);
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
let _ = h1.join();
|
||||
let _ = h2.join();
|
||||
|
||||
let output = output.lock().unwrap();
|
||||
for &expected in expected {
|
||||
if !output.contains(expected) {
|
||||
Self::restore_terminal();
|
||||
return Err(format!("Missing expected output: {expected}").into());
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("{test_name} test passed and output verified");
|
||||
} else {
|
||||
let terminated_naturally = self.run_command_for(&mut cmd, duration)?;
|
||||
log::info!("{test_name} test completed (terminated naturally: {terminated_naturally})");
|
||||
}
|
||||
|
||||
Self::restore_terminal();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_flash_empty(&self, file_path: &Path) -> Result<bool> {
|
||||
let flash_data = fs::read(file_path)?;
|
||||
Ok(flash_data.iter().all(|&b| b == 0xFF))
|
||||
}
|
||||
|
||||
fn flash_output_file(&self) -> PathBuf {
|
||||
self.tests_dir.join("flash_content.bin")
|
||||
}
|
||||
|
||||
fn contains_sequence(data: &[u8], sequence: &[u8]) -> bool {
|
||||
if sequence.len() > data.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
for i in 0..=(data.len() - sequence.len()) {
|
||||
if &data[i..(i + sequence.len())] == sequence {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Runs all tests in the test suite, optionally overriding the chip target
|
||||
pub fn run_all_tests(&self, chip_override: Option<&str>) -> Result<()> {
|
||||
log::info!("Running all tests");
|
||||
|
||||
let chip = chip_override.or(self.chip.as_deref()).unwrap_or("esp32");
|
||||
|
||||
self.test_board_info()?;
|
||||
self.test_flash(Some(chip))?;
|
||||
self.test_monitor()?;
|
||||
self.test_erase_flash()?;
|
||||
self.test_save_image(Some(chip))?;
|
||||
self.test_erase_region()?;
|
||||
self.test_hold_in_reset()?;
|
||||
self.test_reset()?;
|
||||
self.test_checksum_md5()?;
|
||||
self.test_list_ports()?;
|
||||
self.test_write_bin()?;
|
||||
self.test_read_flash()?;
|
||||
|
||||
log::info!("All tests completed successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Runs a specific test by name, optionally overriding the chip target
|
||||
pub fn run_specific_test(&self, test_name: &str, chip_override: Option<&str>) -> Result<()> {
|
||||
let chip = chip_override.or(self.chip.as_deref()).unwrap_or("esp32");
|
||||
|
||||
match test_name {
|
||||
"board-info" => self.test_board_info(),
|
||||
"flash" => self.test_flash(Some(chip)),
|
||||
"monitor" => self.test_monitor(),
|
||||
"erase-flash" => self.test_erase_flash(),
|
||||
"save-image" => self.test_save_image(Some(chip)),
|
||||
"erase-region" => self.test_erase_region(),
|
||||
"hold-in-reset" => self.test_hold_in_reset(),
|
||||
"reset" => self.test_reset(),
|
||||
"checksum-md5" => self.test_checksum_md5(),
|
||||
"list-ports" => self.test_list_ports(),
|
||||
"write-bin" => self.test_write_bin(),
|
||||
"read-flash" => self.test_read_flash(),
|
||||
_ => Err(format!("Unknown test: {test_name}").into()),
|
||||
}
|
||||
}
|
||||
|
||||
// Board info test
|
||||
pub fn test_board_info(&self) -> Result<()> {
|
||||
self.run_simple_command_test(
|
||||
&["board-info"],
|
||||
Some(&["Chip type:"]),
|
||||
Duration::from_secs(5),
|
||||
"board-info",
|
||||
)
|
||||
}
|
||||
|
||||
// Flash test
|
||||
pub fn test_flash(&self, chip: Option<&str>) -> Result<()> {
|
||||
let chip = chip.unwrap_or_else(|| self.chip.as_deref().unwrap_or("esp32"));
|
||||
log::info!("Running flash test for chip: {chip}");
|
||||
|
||||
let app = format!("espflash/tests/data/{chip}");
|
||||
let app_backtrace = format!("espflash/tests/data/{chip}_backtrace");
|
||||
let part_table = "espflash/tests/data/partitions.csv";
|
||||
|
||||
// Partition table is too big
|
||||
self.run_timed_command_test(
|
||||
&[
|
||||
"flash",
|
||||
"--no-skip",
|
||||
"--monitor",
|
||||
"--non-interactive",
|
||||
&app,
|
||||
"--flash-size",
|
||||
"2mb",
|
||||
"--partition-table",
|
||||
part_table,
|
||||
],
|
||||
Some(&["espflash::partition_table::does_not_fit"]),
|
||||
Duration::from_secs(5),
|
||||
"partition too big",
|
||||
)?;
|
||||
|
||||
// Additional tests for ESP32-C6 with manual log-format
|
||||
if chip == "esp32c6" {
|
||||
// Test with manual log-format and with auto-detected log-format
|
||||
self.test_flash_with_defmt(&app)?;
|
||||
// Backtrace test
|
||||
self.test_backtrace(&app_backtrace)?;
|
||||
}
|
||||
|
||||
// Test standard flashing
|
||||
self.run_timed_command_test(
|
||||
&["flash", "--no-skip", "--monitor", "--non-interactive", &app],
|
||||
Some(&["Flashing has completed!", "Hello world!"]),
|
||||
Duration::from_secs(15),
|
||||
"standard flashing",
|
||||
)?;
|
||||
|
||||
// Test standard flashing
|
||||
self.run_timed_command_test(
|
||||
&[
|
||||
"flash",
|
||||
"--no-skip",
|
||||
"--monitor",
|
||||
"--non-interactive",
|
||||
"--baud",
|
||||
"921600",
|
||||
&app,
|
||||
],
|
||||
Some(&["Flashing has completed!", "Hello world!"]),
|
||||
Duration::from_secs(15),
|
||||
"standard flashing with high baud rate",
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn test_flash_with_defmt(&self, app: &str) -> Result<()> {
|
||||
let app_defmt = format!("{app}_defmt");
|
||||
|
||||
// Test with manual log-format
|
||||
self.run_timed_command_test(
|
||||
&[
|
||||
"flash",
|
||||
"--no-skip",
|
||||
"--monitor",
|
||||
"--non-interactive",
|
||||
&app_defmt,
|
||||
"--log-format",
|
||||
"defmt",
|
||||
],
|
||||
Some(&["Flashing has completed!", "Hello world!"]),
|
||||
Duration::from_secs(15),
|
||||
"defmt manual log-format",
|
||||
)?;
|
||||
|
||||
// Test with auto-detected log-format
|
||||
self.run_timed_command_test(
|
||||
&[
|
||||
"flash",
|
||||
"--no-skip",
|
||||
"--monitor",
|
||||
"--non-interactive",
|
||||
&app_defmt,
|
||||
],
|
||||
Some(&["Flashing has completed!", "Hello world!"]),
|
||||
Duration::from_secs(15),
|
||||
"defmt auto-detected log-format",
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn test_backtrace(&self, app_backtrace: &str) -> Result<()> {
|
||||
// Test flashing with backtrace
|
||||
self.run_timed_command_test(
|
||||
&[
|
||||
"flash",
|
||||
"--no-skip",
|
||||
"--monitor",
|
||||
"--non-interactive",
|
||||
app_backtrace,
|
||||
],
|
||||
Some(&[
|
||||
"0x420012c8",
|
||||
"main",
|
||||
"esp32c6_backtrace/src/bin/main.rs:",
|
||||
"0x42001280",
|
||||
"hal_main",
|
||||
]),
|
||||
Duration::from_secs(15),
|
||||
"backtrace test",
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests listing available ports
|
||||
pub fn test_list_ports(&self) -> Result<()> {
|
||||
self.run_simple_command_test(
|
||||
&["list-ports"],
|
||||
Some(&["Silicon Labs"]),
|
||||
Duration::from_secs(5),
|
||||
"list-ports",
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests erasing the flash memory
|
||||
pub fn test_erase_flash(&self) -> Result<()> {
|
||||
log::info!("Running erase-flash test");
|
||||
let flash_output = self.flash_output_file();
|
||||
|
||||
self.run_simple_command_test(
|
||||
&["erase-flash"],
|
||||
Some(&["Flash has been erased!"]),
|
||||
Duration::from_secs(40),
|
||||
"erase-flash",
|
||||
)?;
|
||||
|
||||
// Read a portion of the flash to verify it's erased
|
||||
self.run_simple_command_test(
|
||||
&["read-flash", "0", "0x4000", flash_output.to_str().unwrap()],
|
||||
Some(&["Flash content successfully read"]),
|
||||
Duration::from_secs(5),
|
||||
"read after erase",
|
||||
)?;
|
||||
|
||||
// Verify the flash is empty (all 0xFF)
|
||||
if let Ok(is_empty) = self.is_flash_empty(&flash_output) {
|
||||
if !is_empty {
|
||||
return Err("Flash is not empty after erase-flash command".into());
|
||||
}
|
||||
} else {
|
||||
return Err("Failed to check if flash is empty".into());
|
||||
}
|
||||
|
||||
log::info!("erase-flash test passed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests erasing a specific region of the flash memory
|
||||
pub fn test_erase_region(&self) -> Result<()> {
|
||||
log::info!("Running erase-region test");
|
||||
let flash_output = self.flash_output_file();
|
||||
|
||||
// Test unaligned address (not multiple of 4096)
|
||||
let mut cmd = self.create_espflash_command(&["erase-region", "0x1001", "0x1000"]);
|
||||
let exit_code = self.run_command_with_timeout(&mut cmd, Duration::from_secs(5))?;
|
||||
if exit_code == 0 {
|
||||
return Err("Unaligned address erase should have failed but succeeded".into());
|
||||
}
|
||||
|
||||
// Test unaligned size (not multiple of 4096)
|
||||
let mut cmd = self.create_espflash_command(&["erase-region", "0x1000", "0x1001"]);
|
||||
let exit_code = self.run_command_with_timeout(&mut cmd, Duration::from_secs(5))?;
|
||||
if exit_code == 0 {
|
||||
return Err("Unaligned size erase should have failed but succeeded".into());
|
||||
}
|
||||
|
||||
// Valid erase - should succeed
|
||||
self.run_simple_command_test(
|
||||
&["erase-region", "0x1000", "0x1000"],
|
||||
Some(&["Erasing region at"]),
|
||||
Duration::from_secs(5),
|
||||
"erase-region valid",
|
||||
)?;
|
||||
|
||||
// Read the region to verify it was erased
|
||||
self.run_simple_command_test(
|
||||
&[
|
||||
"read-flash",
|
||||
"0x1000",
|
||||
"0x2000",
|
||||
flash_output.to_str().unwrap(),
|
||||
],
|
||||
Some(&["Flash content successfully read"]),
|
||||
Duration::from_secs(5),
|
||||
"read after erase-region",
|
||||
)?;
|
||||
|
||||
// Check flash contents - first part should be erased
|
||||
if let Ok(flash_data) = fs::read(&flash_output) {
|
||||
// First 0x1000 bytes should be 0xFF (erased)
|
||||
let first_part = &flash_data[0..4096];
|
||||
if !first_part.iter().all(|&b| b == 0xFF) {
|
||||
return Err("First 0x1000 bytes should be empty (0xFF)".into());
|
||||
}
|
||||
|
||||
// Next 0x1000 bytes should contain some non-FF bytes
|
||||
let second_part = &flash_data[4096..8192];
|
||||
if second_part.iter().all(|&b| b == 0xFF) {
|
||||
return Err("Next 0x1000 bytes should contain some non-FF bytes".into());
|
||||
}
|
||||
} else {
|
||||
return Err("Failed to read flash_content.bin file".into());
|
||||
}
|
||||
|
||||
log::info!("erase-region test passed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests reading the flash memory
|
||||
pub fn test_read_flash(&self) -> Result<()> {
|
||||
log::info!("Running read-flash test");
|
||||
let flash_output = self.flash_output_file();
|
||||
let pattern_file = self.tests_dir.join("pattern.bin");
|
||||
|
||||
// Create a pattern to write to flash
|
||||
let known_pattern: Vec<u8> = vec![
|
||||
0x01, 0xA0, 0x02, 0xB3, 0x04, 0xC4, 0x08, 0xD5, 0x10, 0xE6, 0x20, 0xF7, 0x40, 0x88,
|
||||
0x50, 0x99, 0x60, 0xAA, 0x70, 0xBB, 0x80, 0xCC, 0x90, 0xDD, 0xA0, 0xEE, 0xB0, 0xFF,
|
||||
0xC0, 0x11, 0xD0, 0x22,
|
||||
];
|
||||
|
||||
// Write the pattern to a file
|
||||
fs::write(&pattern_file, &known_pattern)?;
|
||||
|
||||
// Write the pattern to the flash
|
||||
self.run_simple_command_test(
|
||||
&["write-bin", "0x0", pattern_file.to_str().unwrap()],
|
||||
Some(&["Binary successfully written to flash!"]),
|
||||
Duration::from_secs(5),
|
||||
"write pattern",
|
||||
)?;
|
||||
|
||||
// Test reading various lengths
|
||||
for &len in &[2, 5, 10, 26] {
|
||||
log::info!("Testing read-flash with length: {len}");
|
||||
|
||||
// Test normal read
|
||||
self.run_simple_command_test(
|
||||
&[
|
||||
"read-flash",
|
||||
"0",
|
||||
&len.to_string(),
|
||||
flash_output.to_str().unwrap(),
|
||||
],
|
||||
Some(&["Flash content successfully read and written to"]),
|
||||
Duration::from_secs(5),
|
||||
&format!("read {len} bytes"),
|
||||
)?;
|
||||
|
||||
// Verify the read data matches the expected pattern
|
||||
if let Ok(read_data) = fs::read(&flash_output) {
|
||||
let expected = &known_pattern[0..len as usize];
|
||||
if &read_data[0..len as usize] != expected {
|
||||
return Err(format!(
|
||||
"Verification failed for length {len}: content does not match"
|
||||
)
|
||||
.into());
|
||||
}
|
||||
} else {
|
||||
return Err(format!("Failed to read flash_content.bin for length {len}").into());
|
||||
}
|
||||
|
||||
// Test ROM read (--no-stub option)
|
||||
self.run_simple_command_test(
|
||||
&[
|
||||
"read-flash",
|
||||
"--no-stub",
|
||||
"0",
|
||||
&len.to_string(),
|
||||
flash_output.to_str().unwrap(),
|
||||
],
|
||||
Some(&["Flash content successfully read and written to"]),
|
||||
Duration::from_secs(5),
|
||||
&format!("read {len} bytes with ROM bootloader"),
|
||||
)?;
|
||||
|
||||
// Verify the ROM read data matches the expected pattern
|
||||
if let Ok(read_data) = fs::read(&flash_output) {
|
||||
let expected = &known_pattern[0..len as usize];
|
||||
if &read_data[0..len as usize] != expected {
|
||||
return Err(format!(
|
||||
"ROM read verification failed for length {len}: content does not match"
|
||||
)
|
||||
.into());
|
||||
}
|
||||
} else {
|
||||
return Err(
|
||||
format!("Failed to read flash_content.bin for ROM read length {len}").into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("read-flash test passed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests writing a binary file to the flash memory
|
||||
pub fn test_write_bin(&self) -> Result<()> {
|
||||
log::info!("Running write-bin test");
|
||||
let flash_content = self.flash_output_file();
|
||||
let binary_file = self.tests_dir.join("binary_file.bin");
|
||||
|
||||
// Create a simple binary with a known pattern (regression test for issue #622)
|
||||
let test_pattern = [0x01, 0xA0];
|
||||
fs::write(&binary_file, test_pattern)?;
|
||||
|
||||
// Write the binary to a specific address
|
||||
self.run_simple_command_test(
|
||||
&["write-bin", "0x0", binary_file.to_str().unwrap()],
|
||||
Some(&["Binary successfully written to flash!"]),
|
||||
Duration::from_secs(15),
|
||||
"write-bin to address",
|
||||
)?;
|
||||
|
||||
// Read the flash to verify
|
||||
self.run_simple_command_test(
|
||||
&["read-flash", "0", "64", flash_content.to_str().unwrap()],
|
||||
Some(&["Flash content successfully read"]),
|
||||
Duration::from_secs(50),
|
||||
"read after write-bin",
|
||||
)?;
|
||||
|
||||
// Verify the flash content contains the test pattern
|
||||
if let Ok(flash_data) = fs::read(&flash_content) {
|
||||
if !Self::contains_sequence(&flash_data, &test_pattern) {
|
||||
return Err("Failed verifying content: test pattern not found in flash".into());
|
||||
}
|
||||
} else {
|
||||
return Err("Failed to read flash_content.bin file".into());
|
||||
}
|
||||
|
||||
log::info!("write-bin test passed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests saving an image to the flash memory
|
||||
pub fn test_save_image(&self, chip: Option<&str>) -> Result<()> {
|
||||
let chip = chip.unwrap_or_else(|| self.chip.as_deref().unwrap_or("esp32"));
|
||||
log::info!("Running save-image test for chip: {chip}");
|
||||
|
||||
let app = format!("espflash/tests/data/{chip}");
|
||||
let app_bin = self.tests_dir.join("app.bin");
|
||||
|
||||
// Determine if frequency option is needed
|
||||
let mut args = vec![
|
||||
"save-image",
|
||||
"--merge",
|
||||
"--chip",
|
||||
chip,
|
||||
&app,
|
||||
app_bin.to_str().unwrap(),
|
||||
];
|
||||
|
||||
// Add frequency option for esp32c2
|
||||
if chip == "esp32c2" {
|
||||
args.splice(2..2, ["-x", "26mhz"].iter().copied());
|
||||
}
|
||||
|
||||
// Save image
|
||||
self.run_simple_command_test(
|
||||
&args,
|
||||
Some(&["Image successfully saved!"]),
|
||||
self.timeout,
|
||||
"save-image",
|
||||
)?;
|
||||
|
||||
// Write the image and monitor
|
||||
self.run_timed_command_test(
|
||||
&[
|
||||
"write-bin",
|
||||
"--monitor",
|
||||
"0x0",
|
||||
app_bin.to_str().unwrap(),
|
||||
"--non-interactive",
|
||||
],
|
||||
Some(&["Hello world!"]),
|
||||
Duration::from_secs(80),
|
||||
"write-bin and monitor",
|
||||
)?;
|
||||
|
||||
// Additional regression test for ESP32-C6
|
||||
if chip == "esp32c6" {
|
||||
self.test_esp32c6_regression(&app_bin)?;
|
||||
}
|
||||
|
||||
log::info!("save-image test passed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests the ESP32-C6 regression case
|
||||
fn test_esp32c6_regression(&self, app_bin: &Path) -> Result<()> {
|
||||
log::info!("Running ESP32-C6 regression test");
|
||||
|
||||
let app = "espflash/tests/data/esp_idf_firmware_c6.elf";
|
||||
|
||||
// Save image with ESP32-C6 regression test case
|
||||
self.run_simple_command_test(
|
||||
&[
|
||||
"save-image",
|
||||
"--merge",
|
||||
"--chip",
|
||||
"esp32c6",
|
||||
app,
|
||||
app_bin.to_str().unwrap(),
|
||||
],
|
||||
Some(&["Image successfully saved!"]),
|
||||
Duration::from_secs(5),
|
||||
"save-image C6 regression",
|
||||
)?;
|
||||
|
||||
// Check that app descriptor is in the correct position
|
||||
if let Ok(bin_data) = fs::read(app_bin) {
|
||||
if bin_data.len() >= 0x10024 {
|
||||
let app_descriptor_offset = 0x10020;
|
||||
// Check for magic word 0xABCD5432 (in little-endian format)
|
||||
let expected_magic = [0x32, 0x54, 0xCD, 0xAB];
|
||||
|
||||
if bin_data[app_descriptor_offset..app_descriptor_offset + 4] != expected_magic {
|
||||
return Err("App descriptor magic word is not correct".into());
|
||||
}
|
||||
} else {
|
||||
return Err("Binary file is too small to contain app descriptor".into());
|
||||
}
|
||||
} else {
|
||||
return Err("Failed to read app.bin file".into());
|
||||
}
|
||||
|
||||
log::info!("ESP32-C6 regression test passed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests the MD5 checksum command
|
||||
pub fn test_checksum_md5(&self) -> Result<()> {
|
||||
log::info!("Running checksum-md5 test");
|
||||
|
||||
// First erase the flash
|
||||
self.run_simple_command_test(
|
||||
&["erase-flash"],
|
||||
Some(&["Flash has been erased!"]),
|
||||
Duration::from_secs(40),
|
||||
"erase-flash for checksum",
|
||||
)?;
|
||||
|
||||
// Then check the MD5 checksum of a region
|
||||
self.run_simple_command_test(
|
||||
&["checksum-md5", "0x1000", "0x100"],
|
||||
Some(&["0x827f263ef9fb63d05499d14fcef32f60"]),
|
||||
Duration::from_secs(5),
|
||||
"checksum-md5",
|
||||
)?;
|
||||
|
||||
log::info!("checksum-md5 test passed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests the monitor command
|
||||
pub fn test_monitor(&self) -> Result<()> {
|
||||
self.run_timed_command_test(
|
||||
&["monitor", "--non-interactive"],
|
||||
Some(&["Hello world!"]),
|
||||
Duration::from_secs(5),
|
||||
"monitor",
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests resetting the target device
|
||||
pub fn test_reset(&self) -> Result<()> {
|
||||
self.run_simple_command_test(
|
||||
&["reset"],
|
||||
Some(&["Resetting target device"]),
|
||||
Duration::from_secs(5),
|
||||
"reset",
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests holding the target device in reset
|
||||
pub fn test_hold_in_reset(&self) -> Result<()> {
|
||||
self.run_simple_command_test(
|
||||
&["hold-in-reset"],
|
||||
Some(&["Holding target device in reset"]),
|
||||
Duration::from_secs(5),
|
||||
"hold-in-reset",
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs the tests based on the provided arguments
|
||||
pub fn run_tests(workspace: &Path, args: RunTestsArgs) -> Result<()> {
|
||||
log::info!("Running espflash tests");
|
||||
|
||||
let tests_dir = workspace.join("espflash").join("tests");
|
||||
let test_runner = TestRunner::new(workspace, tests_dir, args.timeout, args.build_espflash);
|
||||
|
||||
// Build espflash before running test(s) so we are not "waisting" test's
|
||||
// duration or timeout
|
||||
if args.build_espflash {
|
||||
test_runner.build_espflash();
|
||||
}
|
||||
|
||||
match args.test.as_str() {
|
||||
"all" => {
|
||||
if let Err(e) = test_runner.run_all_tests(args.chip.as_deref()) {
|
||||
log::error!("Test suite failed: {e}");
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
specific_test => {
|
||||
if let Err(e) = test_runner.run_specific_test(specific_test, args.chip.as_deref()) {
|
||||
log::error!("Test '{specific_test}' failed: {e}");
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user