三种结果分开
基线为 IDF5.5.5 / ESP32-S3,器材复验固定 S3-DevKitC1v1.1 N8R8;本例不连接外围、不启动 Wi-Fi、不依赖 PSRAM。源码只使用 IDF 自带 Unity/FreeRTOS。兼容矩阵另以5.5.4构建,不能把它冒称本站其他教程的统一基线。首次真实hosted CI39081228585四行构建/QEMU均过,但原上传路径混用主机runner.temp与容器RUNNER_TEMP,制品只有4份log+锁,缺记录/ELF/bin/map/config,不能算完整CI交付。修正版用always收集器核真实容器路径并failclosed;第二次真实hosted CI39081944275四行成功,四包各31个收集条目已逐SHA核对(另有manifest,共各32文件),原缺件run留史。本机SDK复用不计云端结果。
- 本机 host:以当前SDK自带Unity编译真实command.h与同份测试源,5个解析用例;不编译/模拟FreeRTOS。
- S3固件/QEMU:同份5个解析用例,另执行真实IDF FreeRTOS队列满/先进先出与长度1覆盖邮箱,共7例。5.5.5固定qemu_ext.py与推荐qemu-xtensa明确支持S3。不是把旧ESP32二进制改名成S3,也不测试I2C/屏幕/无线/USB电气/功耗。
- 真实板:烧录、串口7例、复位、接线与实际外设仍pending,observations.csv为空。QEMU不证明实体Flash/调度延迟/电流。
- 做到这里,你应该看到
- 先核固定板型、版本与本例结果范围;将尚未做的实体步骤保留空白。
参考:[1] 5.5.5实际QEMU支持表:S3 -M esp32s3[3] 官方QEMU运行范围与安装方法[4] SDK自带Unity单元测试[5] 实际Unity gitlink固定提交[6] FreeRTOS实际队列实现
明确协议与队列策略
解析器直接用于固件测试,输入是显式长度缓冲,接受且仅接受SET:0..1000规范十进制;拒绝负号/正号/前导零/换行/内嵌NUL/超长,失败不改输出。它不是联网认证协议。RTOS队列容量2,满时非阻塞发送拒绝,不悄悄丢旧项;邮箱容量1则明确覆盖最新,不对容量2调用Overwrite。
从新目录构建
先按E01进入固定IDF环境;在解压根目录执行:
已有sdkconfig优先于defaults,因此两模式使用不同构建/config;debug为Og,release采用尺寸优化Os。Flash8MiB,未使用OPI PSRAM,不根据N8R8名字让QEMU模拟外设。dependencies.lock记录每次实际IDF解算;5.5.4兼容行应以该SDK重新解算,不能拿5.5.5锁中的idf版本声称5.5.4实跑。
idf.py -B build-debug -D SDKCONFIG=build-debug/sdkconfig -D SDKCONFIG_DEFAULTS=sdkconfig.defaults build
idf.py -B build-release -D SDKCONFIG=build-release/sdkconfig -D 'SDKCONFIG_DEFAULTS=sdkconfig.defaults;sdkconfig.release.defaults' build
bash ci/run_host.sh
参考:[1] 5.5.5实际QEMU支持表:S3 -M esp32s3[2] QEMU推荐版本、平台下载与SHA[4] SDK自带Unity单元测试
安装正确的 QEMU 工具
不要另下载SDK。先阅读自己固定IDF的tools/tools.json与tools/idf_py_actions/qemu_ext.py;5.5.5推荐qemu-xtensa esp_develop_9.2.2_20260417,S3入口-M esp32s3 -m32M。官方工具安装器会核SHA。macOS所需动态库见官方工具文档;不能只看到--version就称固件通过。
runner必须同时看到7 Tests/0 Failures与E10_UNITY_COMPLETE failures=0,90秒超时/提前退出/失败均Exit1,并停止本次QEMU进程组。原始日志保留。虚拟Flash大小按实际sdkconfig生成,不拿裸app当完整QEMU Flash。
python "$IDF_PATH/tools/idf_tools.py" install qemu-xtensa
# 在该IDF的新终端重新export,保证qemu-system-xtensa进PATH
python ci/run_qemu.py --build build-debug --log qemu-debug.log --timeout 90
参考:[1] 5.5.5实际QEMU支持表:S3 -M esp32s3[2] QEMU推荐版本、平台下载与SHA[3] 官方QEMU运行范围与安装方法
先证明红门能阻挡
run_host.sh实际再编一份E10_INJECT_FAILURE合成失败版本,必须非零且有指定Unity失败文字才继续。该红门不是实体故障,不写入正常固件;正常5例必须全部过,脚本使用pipefail,不能以tee的Exit0掩盖测试失败。QEMU日志或版本不符时CI同样失败,不用continue-on-error。
提交工作流与固定矩阵
ci/esp32-example-ci.yml是可复制到自己仓库.github/workflows/esp32-example-ci.yml的完整模板。只读contents权限;无需secret、刷板或部署。Actions按真实commit固定;四行矩阵为IDF5.5.5/5.5.4 × debug/release,Linuxamd64官方IDF镜像用不可变digest;此处保留基线与兼容行的用途区别。每行新hosted runner/容器与全新RUNNER_TEMP构建目录,不用固件缓存;job30分钟、单QEMU90秒。安装QEMU只取该镜像SDK工具清单的版本,保留日志和14天产物。
record_ci.py记录真实GITHUB_SHA/runId/runnerEnvironment、镜像、请求/实际IDF、所有工程输入及bin/ELF/map/config摘要。always运行的collect_ci.py从实际os.environ RUNNER_TEMP取文件,按record逐SHA复制到GITHUB_WORKSPACE/e10-ci-artifacts;成功build缺关键制品、配置、红门或QEMU原日志就failclosed。失败run仍保留能取得的日志与complete=false清单,上传使用工作区相对目录,不能把runner.temp表达式当容器路径。四个job全绿后,读原始QEMU/host/expected-red日志与原始制品,不能只看workflow文件存在或编译绿勾。模板内精确路径对应本站仓库;在自己的仓库若移动工程,要一起修改paths/project/artifact路径。
参考:[1] 5.5.5实际QEMU支持表:S3 -M esp32s3[2] QEMU推荐版本、平台下载与SHA[3] 官方QEMU运行范围与安装方法[7] 5.5.4兼容矩阵同样真实支持S3 QEMU[8] 首次真实四矩阵CI:逻辑通过但制品缺件,保留历史[9] 修正容器收集路径后的真实四矩阵CI
实际云端矩阵与锁重解析
真实提交77db3f4cc188792a9ac6ae11e12535705e00ac1d、Actions39081944275的四行均为github-hosted干净runner:5.5.5 debug/release应用193632/173792字节;5.5.4 debug/release为193136/173280字节。每行实际通过host5例、故意失败红门、S3 QEMU7例,collector.complete=true且无errors;下载包各31个被收集条目,另有collection-manifest自身,共32文件,record内19源与21产品摘要分别核对。这些是主机/仿真/文件结果,没有执行实体无线或外设。
5.5.5两行19源与该提交字节一致;5.5.4两行的dependencies.lock由对应SDK合法重解析,仅idf.version变为5.5.4,manifest_hash/target/直接依赖不变,其余18源一致。该兼容锁的实际文件和SHA保留,不能称所有输入与提交逐字相同。这里的README云端结果段是运行完成后的说明性更新,不是已在此前run内执行的新实现;所有应用、脚本、模板与配置保持该run原字节。完整制品核验不等于未来每次提交自动成功,读者需核自己的run与记录。
实板复验与失败恢复
仅用支持的开发板,断开外设,USB-to-UART上传和115200日志,使用选定构建目录:idf.py -B build-debug -p REPLACE_PORT flash monitor。记录固件SHA/7例/复位后重跑。测试失败先查首个断言/当前IDF与锁/target,再查干净配置;不要删断言、降低严格解析或把QEMU外设限制当实物不存在。恢复正常应用使用已保存的完整刷写计划,不烧eFuse、不关闭安全检查。
参考:[1] 5.5.5实际QEMU支持表:S3 -M esp32s3[2] QEMU推荐版本、平台下载与SHA[3] 官方QEMU运行范围与安装方法[4] SDK自带Unity单元测试[5] 实际Unity gitlink固定提交[8] 首次真实四矩阵CI:逻辑通过但制品缺件,保留历史[9] 修正容器收集路径后的真实四矩阵CI
许可与证据
原创应用/测试/runner MIT;Unity和FreeRTOS沿IDF内原MIT,SDK组件各自许可。主机与QEMU的实际范围、真实云CI链接/原产物另归档docs/esp32-evidence/engineering-maintenance*;未执行的矩阵不得填passed。无本站硬件/无线/电流结论。
参考:[1] 5.5.5实际QEMU支持表:S3 -M esp32s3[2] QEMU推荐版本、平台下载与SHA[3] 官方QEMU运行范围与安装方法[4] SDK自带Unity单元测试[5] 实际Unity gitlink固定提交[8] 首次真实四矩阵CI:逻辑通过但制品缺件,保留历史[9] 修正容器收集路径后的真实四矩阵CI
同源解析器和真实FreeRTOS队列测试
以下为下载工程中对应文件的全文;带文件名的注释只是网页分隔,构建时使用独立原文件。其它构建配置、锁、许可与夹具在完整示例中。
// ---- main/command.h ----
/* SPDX-License-Identifier: MIT */
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
/* No NUL requirement, exact canonical SET:0..1000, maximum8 bytes. */
static inline bool command_parse(const uint8_t *data,size_t size,uint16_t *out){
if(!data||!out||size<5||size>8||memcmp(data,"SET:",4))return false;
if(size>5&&data[4]=='0')return false;
unsigned value=0;
for(size_t i=4;i<size;++i){if(data[i]<'0'||data[i]>'9')return false;value=value*10+(data[i]-'0');if(value>1000)return false;}
*out=(uint16_t)value;return true;
}
// ---- main/test_main.c ----
/* SPDX-License-Identifier: MIT */
#include "unity.h"
#include "command.h"
#ifndef E10_HOST
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#endif
void setUp(void){}
void tearDown(void){}
static void parse_zero_and_limit(void){uint16_t v=99;TEST_ASSERT_TRUE(command_parse((const uint8_t*)"SET:0",5,&v));TEST_ASSERT_EQUAL_UINT16(0,v);TEST_ASSERT_TRUE(command_parse((const uint8_t*)"SET:1000",8,&v));TEST_ASSERT_EQUAL_UINT16(1000,v);}
static void parse_real_buffer_without_nul(void){const uint8_t b[]={'S','E','T',':','4','2'};uint16_t v=0;TEST_ASSERT_TRUE(command_parse(b,sizeof(b),&v));TEST_ASSERT_EQUAL_UINT16(42,v);}
static void reject_invalid_tokens(void){const char *bad[]={"SET:-1","SET:+1","SET:01","SET:1001","SET:1\n","set:1","SET:"};uint16_t v=77;for(size_t i=0;i<sizeof(bad)/sizeof(bad[0]);++i){TEST_ASSERT_FALSE(command_parse((const uint8_t*)bad[i],strlen(bad[i]),&v));TEST_ASSERT_EQUAL_UINT16(77,v);}}
static void reject_null_and_oversize(void){uint16_t v=0;TEST_ASSERT_FALSE(command_parse(NULL,5,&v));TEST_ASSERT_FALSE(command_parse((const uint8_t*)"SET:1",5,NULL));TEST_ASSERT_FALSE(command_parse((const uint8_t*)"SET:12345",9,&v));}
static void reject_embedded_nul(void){const uint8_t b[]={'S','E','T',':','1',0};uint16_t v=0;TEST_ASSERT_FALSE(command_parse(b,sizeof(b),&v));}
#ifdef E10_INJECT_FAILURE
static void intentional_red_gate(void){TEST_FAIL_MESSAGE("SYNTHETIC_EXPECTED_FAILURE: test runner must return nonzero");}
#endif
#ifndef E10_HOST
static void actual_freertos_fifo_and_full(void){
QueueHandle_t q=xQueueCreate(2,sizeof(uint16_t));TEST_ASSERT_NOT_NULL(q);uint16_t a=1,b=2,c=3,out=0;
TEST_ASSERT_EQUAL(pdTRUE,xQueueSend(q,&a,0));TEST_ASSERT_EQUAL(pdTRUE,xQueueSend(q,&b,0));TEST_ASSERT_EQUAL(errQUEUE_FULL,xQueueSend(q,&c,0));
TEST_ASSERT_EQUAL(pdTRUE,xQueueReceive(q,&out,0));TEST_ASSERT_EQUAL_UINT16(1,out);TEST_ASSERT_EQUAL(pdTRUE,xQueueReceive(q,&out,0));TEST_ASSERT_EQUAL_UINT16(2,out);TEST_ASSERT_EQUAL(pdFALSE,xQueueReceive(q,&out,0));vQueueDelete(q);
}
static void actual_freertos_latest_mailbox(void){
QueueHandle_t q=xQueueCreate(1,sizeof(uint16_t));TEST_ASSERT_NOT_NULL(q);uint16_t a=1,b=9,out=0;
TEST_ASSERT_EQUAL(pdPASS,xQueueOverwrite(q,&a));TEST_ASSERT_EQUAL(pdPASS,xQueueOverwrite(q,&b));TEST_ASSERT_EQUAL(pdTRUE,xQueueReceive(q,&out,0));TEST_ASSERT_EQUAL_UINT16(9,out);vQueueDelete(q);
}
#endif
static int run_tests(void){
UNITY_BEGIN();RUN_TEST(parse_zero_and_limit);RUN_TEST(parse_real_buffer_without_nul);RUN_TEST(reject_invalid_tokens);RUN_TEST(reject_null_and_oversize);RUN_TEST(reject_embedded_nul);
#ifdef E10_INJECT_FAILURE
RUN_TEST(intentional_red_gate);
#endif
#ifndef E10_HOST
RUN_TEST(actual_freertos_fifo_and_full);RUN_TEST(actual_freertos_latest_mailbox);
#endif
return UNITY_END();
}
#ifdef E10_HOST
int main(void){return run_tests();}
#else
void app_main(void){int failures=run_tests();printf("E10_UNITY_COMPLETE failures=%d\n",failures);fflush(stdout);for(;;)vTaskDelay(pdMS_TO_TICKS(1000));}
#endif
参考:[1] 5.5.5实际QEMU支持表:S3 -M esp32s3[2] QEMU推荐版本、平台下载与SHA[3] 官方QEMU运行范围与安装方法[4] SDK自带Unity单元测试[5] 实际Unity gitlink固定提交[6] FreeRTOS实际队列实现[7] 5.5.4兼容矩阵同样真实支持S3 QEMU[8] 首次真实四矩阵CI:逻辑通过但制品缺件,保留历史[9] 修正容器收集路径后的真实四矩阵CI
真实host与QEMU执行器
以下为下载工程中对应文件的全文;带文件名的注释只是网页分隔,构建时使用独立原文件。其它构建配置、锁、许可与夹具在完整示例中。
# ---- ci/run_host.sh ----
#!/usr/bin/env bash
set -euo pipefail
: "${IDF_PATH:?Activate the exact IDF environment first}"
project_dir="$(cd "$(dirname "$0")/.." && pwd)"
output_dir="${E10_HOST_OUTPUT:-$project_dir/build-host}"
mkdir -p "$output_dir"
unity_dir="$IDF_PATH/components/unity/unity/src"
# SDK Unity itself, with only original application parser shared with firmware.
cc -std=c11 -Wall -Wextra -Werror -DE10_HOST -I"$unity_dir" -I"$project_dir/main" "$unity_dir/unity.c" "$project_dir/main/test_main.c" -o "$output_dir/tests"
"$output_dir/tests"
cc -std=c11 -Wall -Wextra -Werror -DE10_HOST -DE10_INJECT_FAILURE -I"$unity_dir" -I"$project_dir/main" "$unity_dir/unity.c" "$project_dir/main/test_main.c" -o "$output_dir/expected-red"
set +e
"$output_dir/expected-red" > "$output_dir/expected-red.log" 2>&1
red_exit=$?
set -e
if [[ $red_exit -eq 0 ]]; then echo 'ERROR: intentional failing Unity case was accepted'; exit 1; fi
if ! grep -q 'SYNTHETIC_EXPECTED_FAILURE' "$output_dir/expected-red.log"; then cat "$output_dir/expected-red.log"; exit 1; fi
printf 'Expected-red Unity process rejected with exit %s; not hardware evidence.\n' "$red_exit"
# ---- ci/run_qemu.py ----
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""Run built S3 firmware through official idf.py qemu; fail closed, retain log.
No claim of Wi-Fi, I2C/OLED, USB/JTAG electrical, power or target timing coverage.
"""
import argparse,os,pathlib,re,signal,subprocess,sys,time,threading,queue
p=argparse.ArgumentParser();p.add_argument('--build',required=True);p.add_argument('--log',required=True);p.add_argument('--timeout',type=float,default=90);a=p.parse_args()
if not 0<a.timeout<=300:p.error('timeout must be in(0,300]')
project=pathlib.Path(__file__).resolve().parents[1];build=pathlib.Path(a.build).resolve();log=pathlib.Path(a.log).resolve();log.parent.mkdir(parents=True,exist_ok=True)
cmd=[sys.executable,str(pathlib.Path(os.environ['IDF_PATH'])/'tools/idf.py'),'-C',str(project),'-B',str(build),'qemu']
process=subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.STDOUT,start_new_session=True);lines=queue.Queue()
def read():
for raw in iter(process.stdout.readline,b''):lines.put(raw)
lines.put(None)
threading.Thread(target=read,daemon=True).start();deadline=time.monotonic()+a.timeout;complete=False;tests=False;failure='timeout/no completion marker';received=[]
try:
with log.open('wb') as f:
while time.monotonic()<deadline:
try:line=lines.get(timeout=min(.25,max(.01,deadline-time.monotonic())))
except queue.Empty:
if process.poll() is not None:failure='QEMU exited before completion';break
continue
if line is None:failure='QEMU stream ended before completion';break
f.write(line);f.flush();text=line.decode('utf-8',errors='replace');received.append(text)
if re.search(r'7 Tests 0 Failures 0 Ignored',text):tests=True
if 'E10_UNITY_COMPLETE' in text:
complete=tests and bool(re.search(r'E10_UNITY_COMPLETE failures=0(?:\s|$)',text));failure='incomplete or failing Unity summary';break
finally:
if process.poll() is None:
os.killpg(process.pid,signal.SIGTERM)
try:process.wait(timeout=5)
except subprocess.TimeoutExpired:os.killpg(process.pid,signal.SIGKILL);process.wait(timeout=5)
if not complete:print('FAIL',failure,'raw log:',log);sys.exit(1)
print('PASS actual S3 QEMU:5 parser +2 real FreeRTOS queue cases; not physical board or peripheral evidence. Raw log:',log)
参考:[1] 5.5.5实际QEMU支持表:S3 -M esp32s3[2] QEMU推荐版本、平台下载与SHA[3] 官方QEMU运行范围与安装方法[4] SDK自带Unity单元测试[5] 实际Unity gitlink固定提交[6] FreeRTOS实际队列实现[7] 5.5.4兼容矩阵同样真实支持S3 QEMU[8] 首次真实四矩阵CI:逻辑通过但制品缺件,保留历史[9] 修正容器收集路径后的真实四矩阵CI
完整四矩阵模板与逐SHA收集器
以下为下载工程中对应文件的全文;带文件名的注释只是网页分隔,构建时使用独立原文件。其它构建配置、锁、许可与夹具在完整示例中。
# ---- ci/esp32-example-ci.yml ----
name: ESP32 E10 clean build and Unity
on:
workflow_dispatch:
push:
paths:
- 'examples/esp32/esp32-example-ci-qemu/**'
- '.github/workflows/esp32-example-ci.yml'
pull_request:
paths:
- 'examples/esp32/esp32-example-ci-qemu/**'
- '.github/workflows/esp32-example-ci.yml'
permissions:
contents: read
concurrency:
group: e10-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
clean-build-unity:
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- idf: v5.5.5
optimization: debug
image: docker.io/espressif/idf@sha256:6e2800a69f1c6521a5651da524f811e237d13e34cad369687916d0ad0bc4ef89
- idf: v5.5.5
optimization: release
image: docker.io/espressif/idf@sha256:6e2800a69f1c6521a5651da524f811e237d13e34cad369687916d0ad0bc4ef89
- idf: v5.5.4
optimization: debug
image: docker.io/espressif/idf@sha256:116f0526dfc87e764785370e59b88822e02cf4f9e1edd953cad5ed2d02672023
- idf: v5.5.4
optimization: release
image: docker.io/espressif/idf@sha256:116f0526dfc87e764785370e59b88822e02cf4f9e1edd953cad5ed2d02672023
name: ${{ matrix.idf }} / S3 / ${{ matrix.optimization }}
container:
image: ${{ matrix.image }}
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
persist-credentials: false
- name: Fresh environment and QEMU dependencies
run: |
set -euo pipefail
apt-get update -qq
apt-get install -y --no-install-recommends libgcrypt20 libglib2.0-0 libpixman-1-0 libsdl2-2.0-0 libslirp0
source "$IDF_PATH/export.sh"
idf.py --version | tee "$RUNNER_TEMP/e10-idf-version.log"
idf.py --version | grep -F '${{ matrix.idf }}'
python "$IDF_PATH/tools/idf_tools.py" install qemu-xtensa
- name: Host Unity and intentional red gate
id: host_tests
run: |
set -euo pipefail
source "$IDF_PATH/export.sh"
export E10_HOST_OUTPUT="$RUNNER_TEMP/e10-host"
bash examples/esp32/esp32-example-ci-qemu/ci/run_host.sh | tee "$RUNNER_TEMP/e10-host.log"
- name: Clean S3 build and real QEMU Unity
id: unity_build
run: |
set -euo pipefail
source "$IDF_PATH/export.sh"
project="$GITHUB_WORKSPACE/examples/esp32/esp32-example-ci-qemu"
build="$RUNNER_TEMP/e10-build"
test ! -e "$build"
defaults='sdkconfig.defaults'
if [[ '${{ matrix.optimization }}' == release ]]; then defaults='sdkconfig.defaults;sdkconfig.release.defaults'; fi
idf.py -C "$project" -B "$build" -D SDKCONFIG="$build/sdkconfig" -D SDKCONFIG_DEFAULTS="$defaults" build | tee "$RUNNER_TEMP/e10-build.log"
python "$project/ci/run_qemu.py" --build "$build" --log "$RUNNER_TEMP/e10-qemu.log" --timeout 90
python "$project/ci/record_ci.py" --build "$build" --idf '${{ matrix.idf }}' --optimization '${{ matrix.optimization }}' --image '${{ matrix.image }}' --output "$RUNNER_TEMP/e10-ci-record.json"
- name: Collect exact container artifacts and fail closed
if: always()
run: |
set -euo pipefail
args=()
if [[ '${{ steps.unity_build.outcome }}' == success ]]; then args+=(--require-complete); fi
python3 examples/esp32/esp32-example-ci-qemu/ci/collect_ci.py "${args[@]}"
- name: Preserve artifacts and raw logs
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: e10-${{ matrix.idf }}-${{ matrix.optimization }}-${{ github.run_attempt }}
retention-days: 14
if-no-files-found: error
path: e10-ci-artifacts/
# ---- ci/record_ci.py ----
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
import argparse,pathlib,json,hashlib,os,subprocess
p=argparse.ArgumentParser();p.add_argument('--build',required=True);p.add_argument('--idf',required=True);p.add_argument('--optimization',required=True);p.add_argument('--image',required=True);p.add_argument('--output',required=True);a=p.parse_args()
root=pathlib.Path(__file__).resolve().parents[1];build=pathlib.Path(a.build).resolve();h=lambda p:hashlib.sha256(p.read_bytes()).hexdigest()
config=json.loads((build/'config/sdkconfig.json').read_text())
expected='COMPILER_OPTIMIZATION_DEBUG' if a.optimization=='debug' else 'COMPILER_OPTIMIZATION_SIZE'
if config.get('IDF_TARGET')!='esp32s3' or config.get(expected) is not True:raise SystemExit('matrix target/optimization differs from actual generated config')
sources=[{'path':str(p.relative_to(root)),'sha256':h(p)} for p in sorted(root.rglob('*')) if p.is_file() and not any(x.startswith('build') or x=='managed_components' or x=='__pycache__' for x in p.relative_to(root).parts)]
artifacts=[{'path':str(p.relative_to(build)),'sha256':h(p),'bytes':p.stat().st_size} for p in sorted(build.rglob('*')) if p.is_file() and (p.suffix in ['.bin','.elf','.map'] or p.name in ['sdkconfig','sdkconfig.h','sdkconfig.cmake','sdkconfig.json','flasher_args.json'])]
result={'githubSHA':os.getenv('GITHUB_SHA'),'runId':os.getenv('GITHUB_RUN_ID'),'runAttempt':os.getenv('GITHUB_RUN_ATTEMPT'),'runnerEnvironment':os.getenv('RUNNER_ENVIRONMENT'),'requestedIDF':a.idf,'actualIDF':subprocess.check_output(['python',str(pathlib.Path(os.environ['IDF_PATH'])/'tools/idf.py'),'--version'],text=True).strip(),'target':'esp32s3','optimization':a.optimization,'immutableImage':a.image,'sources':sources,'artifacts':artifacts,'scope':'clean firmware build; host parser Unity; S3 QEMU parser and actual FreeRTOS queue tests; no physical Wi-Fi/I2C/display/power/JTAG result'}
pathlib.Path(a.output).write_text(json.dumps(result,indent=2))
# ---- ci/collect_ci.py ----
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""Collect from real container paths; always keep useful failed-run evidence.
Successful build requires complete hashed products. Never use runner.temp context.
"""
import argparse,datetime,hashlib,json,os,pathlib,shutil,sys
p=argparse.ArgumentParser();p.add_argument('--build');p.add_argument('--output');p.add_argument('--require-complete',action='store_true');a=p.parse_args()
workspace=pathlib.Path(os.environ['GITHUB_WORKSPACE']).resolve();temp=pathlib.Path(os.environ['RUNNER_TEMP']).resolve();project=pathlib.Path(__file__).resolve().parents[1]
build=pathlib.Path(a.build).resolve() if a.build else temp/'e10-build';output=pathlib.Path(a.output).resolve() if a.output else workspace/'e10-ci-artifacts';output.mkdir(parents=True,exist_ok=True)
errors=[];entries=[];seen=set();h=lambda p:hashlib.sha256(p.read_bytes()).hexdigest()
def copy(source,relative,required=False,expected=None):
source=pathlib.Path(source);relative=str(relative)
if not source.is_file():
if required:errors.append('missing '+str(source))
return
if relative in seen:return
seen.add(relative);dest=output/relative;dest.parent.mkdir(parents=True,exist_ok=True);shutil.copyfile(source,dest)
digest=h(dest)
if expected and digest!=expected:errors.append('record SHA mismatch '+str(source))
entries.append({'path':relative,'source':str(source),'bytes':dest.stat().st_size,'sha256':digest})
for name in ['e10-idf-version.log','e10-host.log','e10-build.log','e10-qemu.log']:copy(temp/name,'logs/'+name,True)
copy(temp/'e10-host/expected-red.log','logs/expected-red.log',True)
record_path=temp/'e10-ci-record.json';copy(record_path,'e10-ci-record.json',True);record=None
if record_path.is_file():
try:record=json.loads(record_path.read_text())
except (ValueError,OSError) as e:errors.append('invalid CI record: '+str(e))
if record:
for f in record.get('artifacts',[]):
rel=pathlib.Path(f['path']);source=(build/rel).resolve()
if not source.is_relative_to(build):errors.append('artifact escaped build '+str(rel));continue
copy(source,'build/'+str(rel),True,f['sha256'])
for f in record.get('sources',[]):
source=(project/f['path']).resolve()
if not source.is_relative_to(project) or not source.is_file() or h(source)!=f['sha256']:errors.append('source record mismatch '+f['path'])
else:errors.append('no readable CI record')
# Configuration and compiler provenance accompany final products even if record
# was not written because an earlier build step failed.
for name in ['sdkconfig','config/sdkconfig.h','config/sdkconfig.json','config/sdkconfig.cmake','flasher_args.json','flash_args','project_description.json','compile_commands.json','partition_table/partition-table.bin','bootloader/bootloader.bin']:
copy(build/name,'build/'+name,True)
copy(project/'dependencies.lock','dependencies.lock',True)
try:
desc=json.loads((build/'project_description.json').read_text())
for key in ['app_bin','app_elf']:copy(build/desc[key],'build/'+desc[key],True)
copy(build/(pathlib.Path(desc['app_elf']).stem+'.map'),'build/'+pathlib.Path(desc['app_elf']).stem+'.map',True)
except (ValueError,KeyError,OSError) as e:errors.append('application products unavailable: '+str(e))
# Success also requires genuine test summaries, not just a nonempty file.
required_text={'e10-host.log':['5 Tests 0 Failures 0 Ignored','Expected-red Unity process rejected with exit 1'],'e10-qemu.log':['7 Tests 0 Failures 0 Ignored','E10_UNITY_COMPLETE failures=0']}
for name,markers in required_text.items():
text=(temp/name).read_text(errors='replace') if (temp/name).is_file() else ''
for marker in markers:
if marker not in text:errors.append('missing test marker '+name+': '+marker)
red=temp/'e10-host/expected-red.log';text=red.read_text(errors='replace') if red.is_file() else ''
if 'SYNTHETIC_EXPECTED_FAILURE' not in text or '6 Tests 1 Failures 0 Ignored' not in text:errors.append('expected-red evidence incomplete')
manifest={'collectedAt':datetime.datetime.now(datetime.timezone.utc).isoformat(),'githubSHA':os.getenv('GITHUB_SHA'),'runId':os.getenv('GITHUB_RUN_ID'),'runAttempt':os.getenv('GITHUB_RUN_ATTEMPT'),'runnerEnvironment':os.getenv('RUNNER_ENVIRONMENT','local-author-fixture'),'actualRUNNER_TEMP':str(temp),'actualGITHUB_WORKSPACE':str(workspace),'requireComplete':a.require_complete,'complete':not errors,'errors':errors,'files':sorted(entries,key=lambda x:x['path']),'scope':'artifact collection only; local fixtures are not hosted CI or physical results'}
(output/'collection-manifest.json').write_text(json.dumps(manifest,indent=2))
print('COLLECT',len(entries),'files; complete=',not errors,'requireComplete=',a.require_complete,'manifest=',output/'collection-manifest.json')
if errors:
print('\n'.join(errors))
if a.require_complete:sys.exit(1)
- 做到这里,你应该看到
- 按README保存自己的原始日志和SHA;下载包的observations.csv实测列仍为空,不能用合成输入补填。
参考:[1] 5.5.5实际QEMU支持表:S3 -M esp32s3[2] QEMU推荐版本、平台下载与SHA[3] 官方QEMU运行范围与安装方法[4] SDK自带Unity单元测试[5] 实际Unity gitlink固定提交[6] FreeRTOS实际队列实现[7] 5.5.4兼容矩阵同样真实支持S3 QEMU[8] 首次真实四矩阵CI:逻辑通过但制品缺件,保留历史[9] 修正容器收集路径后的真实四矩阵CI
资料来源与版本说明
保留原计划E10用途与slug;固定上游、实际本机构建和适用的逻辑检查独立留证。未补造无线、采样、屏幕、真实Flash或掉电结果。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。