firmwareloop 0.0.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- firmwareloop-0.0.8.dist-info/METADATA +268 -0
- firmwareloop-0.0.8.dist-info/RECORD +26 -0
- firmwareloop-0.0.8.dist-info/WHEEL +5 -0
- firmwareloop-0.0.8.dist-info/entry_points.txt +5 -0
- firmwareloop-0.0.8.dist-info/licenses/LICENSE +21 -0
- firmwareloop-0.0.8.dist-info/top_level.txt +1 -0
- tools/__init__.py +5 -0
- tools/acceptance-scenario.ps1 +455 -0
- tools/build.ps1 +208 -0
- tools/can.ps1 +130 -0
- tools/check-qoder-mcp.ps1 +111 -0
- tools/common/build-backends.psm1 +294 -0
- tools/common/fw.psm1 +311 -0
- tools/common/uart_probe.py +100 -0
- tools/doctor.ps1 +148 -0
- tools/flash.ps1 +181 -0
- tools/fw_mcp_server.py +1272 -0
- tools/instrument_cli.py +417 -0
- tools/lib/__init__.py +3 -0
- tools/lib/instruments.py +181 -0
- tools/logic_capture.ps1 +261 -0
- tools/logic_decode.ps1 +307 -0
- tools/reset.ps1 +141 -0
- tools/setup-agent-mcp.ps1 +147 -0
- tools/test-backends.ps1 +100 -0
- tools/test.ps1 +194 -0
tools/reset.ps1
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
DUT reset (Spec §26 M2). Preferred backend is Agentic HIL; the
|
|
4
|
+
simulator backend is a no-op acknowledgement (the HIL harness owns the
|
|
5
|
+
simulated DUT process and resets it in-test).
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
.\tools\reset.ps1 -Backend simulator -Json
|
|
9
|
+
.\tools\reset.ps1 -Backend agentic-hil -Json
|
|
10
|
+
|
|
11
|
+
Exit codes: 0 ok, 1 backend failure, 2 config/identity error.
|
|
12
|
+
#>
|
|
13
|
+
[CmdletBinding()]
|
|
14
|
+
param(
|
|
15
|
+
[ValidateSet('simulator', 'agentic-hil', 'openocd', 'vendor')]
|
|
16
|
+
[string]$Backend = 'simulator',
|
|
17
|
+
[string]$ExpectedTarget, # verify probe identity before reset (Spec §9)
|
|
18
|
+
[switch]$Json
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
Set-StrictMode -Version Latest
|
|
22
|
+
$ErrorActionPreference = 'Stop'
|
|
23
|
+
Import-Module (Join-Path $PSScriptRoot 'common\fw.psm1') -Force
|
|
24
|
+
|
|
25
|
+
$repoRoot = Get-FwRepoRoot
|
|
26
|
+
|
|
27
|
+
function Exit-WithError {
|
|
28
|
+
param([string]$Class, [string]$Message, [string]$Detail)
|
|
29
|
+
$body = New-FwError -ErrorClass $Class -Message $Message -Detail $Detail
|
|
30
|
+
if ($Json) { Write-FwJson $body -Compact } else { Write-FwJson $body; Write-Error "$Class : $Message" -ErrorAction Continue }
|
|
31
|
+
exit 2
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
|
35
|
+
$logFile = Join-Path $repoRoot "artifacts\logs\reset-$stamp.log"
|
|
36
|
+
New-Item -ItemType Directory -Force -Path (Split-Path $logFile) | Out-Null
|
|
37
|
+
|
|
38
|
+
switch ($Backend) {
|
|
39
|
+
'simulator' {
|
|
40
|
+
Save-FwLog -Path $logFile -Content "simulator backend: reset is managed by the HIL harness (pytest conftest SimulatedDut.reset)."
|
|
41
|
+
$result = [ordered]@{
|
|
42
|
+
schema = 'firmware-reset-result/v1'
|
|
43
|
+
ok = $true
|
|
44
|
+
backend = 'simulator'
|
|
45
|
+
target = 'simulated-dut'
|
|
46
|
+
log = (Resolve-Path -LiteralPath $logFile).Path
|
|
47
|
+
generated_at = Get-FwTimestamp
|
|
48
|
+
}
|
|
49
|
+
if ($Json) { Write-FwJson ([pscustomobject]$result) -Compact } else { Write-FwJson ([pscustomobject]$result) }
|
|
50
|
+
exit 0
|
|
51
|
+
}
|
|
52
|
+
'pyocd' {
|
|
53
|
+
$pyocdExe = Join-Path $repoRoot '.venv\Scripts\pyocd.exe'
|
|
54
|
+
if (-not (Test-Path -LiteralPath $pyocdExe)) {
|
|
55
|
+
$cmd = Get-Command pyocd -ErrorAction SilentlyContinue
|
|
56
|
+
if ($cmd) { $pyocdExe = $cmd.Source }
|
|
57
|
+
}
|
|
58
|
+
if (-not (Test-Path -LiteralPath $pyocdExe)) {
|
|
59
|
+
Exit-WithError -Class 'TOOLCHAIN_NOT_FOUND' -Message 'pyocd 未找到。请在 Python 环境中安装 pyocd (`uv pip install pyocd`)。' -Detail 'pyOCD 用于通过 ST-LINK / CMSIS-DAP 复位芯片。'
|
|
60
|
+
}
|
|
61
|
+
$targetChip = if ($ExpectedTarget) { $ExpectedTarget } else { 'stm32f103rc' }
|
|
62
|
+
$res = Invoke-FwProcess -FilePath $pyocdExe -Arguments @('reset', '-t', $targetChip.ToLowerInvariant()) -WorkingDirectory $repoRoot -TimeoutMs 30000 -StdoutFile $logFile
|
|
63
|
+
if ($res.timed_out) {
|
|
64
|
+
Exit-WithError -Class 'TIMEOUT' -Message 'pyocd 芯片复位超时。' -Detail $logFile
|
|
65
|
+
}
|
|
66
|
+
if ($res.exit_code -ne 0) {
|
|
67
|
+
Exit-WithError -Class 'RESET_ERROR' -Message "pyocd 硬件复位失败:探针未连接或目标 MCU 无响应。" -Detail ($res.stdout + "`n" + $res.stderr)
|
|
68
|
+
}
|
|
69
|
+
$result = [ordered]@{
|
|
70
|
+
schema = 'firmware-reset-result/v1'
|
|
71
|
+
ok = $true
|
|
72
|
+
backend = 'pyocd'
|
|
73
|
+
target = $targetChip
|
|
74
|
+
log = (Resolve-Path -LiteralPath $logFile).Path
|
|
75
|
+
generated_at = Get-FwTimestamp
|
|
76
|
+
}
|
|
77
|
+
if ($Json) { Write-FwJson ([pscustomobject]$result) -Compact } else { Write-FwJson ([pscustomobject]$result) }
|
|
78
|
+
exit 0
|
|
79
|
+
}
|
|
80
|
+
'jlink' {
|
|
81
|
+
$jlinkExe = (Get-Command JLink.exe -ErrorAction SilentlyContinue)?.Source
|
|
82
|
+
if (-not $jlinkExe) {
|
|
83
|
+
$candidates = @(
|
|
84
|
+
"C:\Program Files\SEGGER\JLink\JLink.exe",
|
|
85
|
+
"C:\Program Files (x86)\SEGGER\JLink\JLink.exe"
|
|
86
|
+
)
|
|
87
|
+
foreach ($c in $candidates) {
|
|
88
|
+
if (Test-Path -LiteralPath $c) { $jlinkExe = $c; break }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (-not $jlinkExe) {
|
|
92
|
+
Exit-WithError -Class 'TOOLCHAIN_NOT_FOUND' -Message '未找到 SEGGER JLink.exe。' -Detail '请安装官方 SEGGER J-Link 驱动。'
|
|
93
|
+
}
|
|
94
|
+
$cmdScript = Join-Path $repoRoot "artifacts\logs\jlink_reset_$stamp.jlink"
|
|
95
|
+
Set-Content -LiteralPath $cmdScript -Value "r`ng`nq`n" -Encoding ASCII
|
|
96
|
+
$targetChip = if ($ExpectedTarget) { $ExpectedTarget } else { 'STM32F103C8' }
|
|
97
|
+
$res = Invoke-FwProcess -FilePath $jlinkExe -Arguments @('-device', $targetChip, '-if', 'SWD', '-speed', '4000', '-autoconnect', '1', '-CommanderScript', $cmdScript) -WorkingDirectory $repoRoot -TimeoutMs 30000 -StdoutFile $logFile
|
|
98
|
+
if ($res.timed_out) {
|
|
99
|
+
Exit-WithError -Class 'TIMEOUT' -Message 'J-Link 芯片复位超时。' -Detail $logFile
|
|
100
|
+
}
|
|
101
|
+
if ($res.exit_code -ne 0) {
|
|
102
|
+
Exit-WithError -Class 'RESET_ERROR' -Message "J-Link 硬件复位失败:探针未连接或板卡未供电。" -Detail ($res.stdout + "`n" + $res.stderr)
|
|
103
|
+
}
|
|
104
|
+
$result = [ordered]@{
|
|
105
|
+
schema = 'firmware-reset-result/v1'
|
|
106
|
+
ok = $true
|
|
107
|
+
backend = 'jlink'
|
|
108
|
+
target = $targetChip
|
|
109
|
+
log = (Resolve-Path -LiteralPath $logFile).Path
|
|
110
|
+
generated_at = Get-FwTimestamp
|
|
111
|
+
}
|
|
112
|
+
if ($Json) { Write-FwJson ([pscustomobject]$result) -Compact } else { Write-FwJson ([pscustomobject]$result) }
|
|
113
|
+
exit 0
|
|
114
|
+
}
|
|
115
|
+
'agentic-hil' {
|
|
116
|
+
$cmd = Get-Command agentic-hil -ErrorAction SilentlyContinue
|
|
117
|
+
if (-not $cmd) {
|
|
118
|
+
Exit-WithError -Class 'PROBE_NOT_FOUND' -Message "'agentic-hil' 未安装;请安装 Agentic HIL。" -Detail 'See README.md -> M2'
|
|
119
|
+
}
|
|
120
|
+
$res = Invoke-FwProcess -FilePath $cmd.Source -Arguments @('reset', '--target', $ExpectedTarget, '--json') -WorkingDirectory $repoRoot -TimeoutMs 120000 -StdoutFile $logFile
|
|
121
|
+
if ($res.timed_out) {
|
|
122
|
+
Exit-WithError -Class 'RESET_ERROR' -Message 'reset backend timed out and was killed.' -Detail $logFile
|
|
123
|
+
}
|
|
124
|
+
if ($res.exit_code -ne 0) {
|
|
125
|
+
Exit-WithError -Class 'RESET_ERROR' -Message 'reset backend reported failure.' -Detail ($res.stdout + $res.stderr)
|
|
126
|
+
}
|
|
127
|
+
$result = [ordered]@{
|
|
128
|
+
schema = 'firmware-reset-result/v1'
|
|
129
|
+
ok = $true
|
|
130
|
+
backend = 'agentic-hil'
|
|
131
|
+
target = $ExpectedTarget
|
|
132
|
+
log = (Resolve-Path -LiteralPath $logFile).Path
|
|
133
|
+
generated_at = Get-FwTimestamp
|
|
134
|
+
}
|
|
135
|
+
if ($Json) { Write-FwJson ([pscustomobject]$result) -Compact } else { Write-FwJson ([pscustomobject]$result) }
|
|
136
|
+
exit 0
|
|
137
|
+
}
|
|
138
|
+
default {
|
|
139
|
+
Exit-WithError -Class 'CONFIG_ERROR' -Message "复位后端 '$Backend' 尚未在本机配置。" -Detail '请在 lab/lab.yaml 中配置有效的复位后端。'
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Dual-Tier MCP setup and registration helper (v0.0.8).
|
|
4
|
+
Inspects installed AI coding agents (Qoder, Claude Code, Antigravity, Cursor)
|
|
5
|
+
and prints/generates exact registration commands and workspace configuration.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
.\tools\setup-agent-mcp.ps1
|
|
9
|
+
.\tools\setup-agent-mcp.ps1 -Json
|
|
10
|
+
.\tools\setup-agent-mcp.ps1 -WriteWorkspaceMcp
|
|
11
|
+
#>
|
|
12
|
+
[CmdletBinding()]
|
|
13
|
+
param(
|
|
14
|
+
[switch]$Json,
|
|
15
|
+
[switch]$WriteWorkspaceMcp
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
Set-StrictMode -Version Latest
|
|
19
|
+
$ErrorActionPreference = 'Stop'
|
|
20
|
+
Import-Module (Join-Path $PSScriptRoot 'common\fw.psm1') -Force
|
|
21
|
+
|
|
22
|
+
$repoRoot = Get-FwRepoRoot
|
|
23
|
+
$python = Resolve-FwPython -RepoRoot $repoRoot
|
|
24
|
+
$fwMcpServer = Join-Path $repoRoot 'tools\fw_mcp_server.py'
|
|
25
|
+
$ahilExe = Join-Path $repoRoot '.venv\Scripts\agentic-hil.exe'
|
|
26
|
+
|
|
27
|
+
$report = [ordered]@{
|
|
28
|
+
schema = 'firmwareloop-mcp-setup/v1'
|
|
29
|
+
repo_root = $repoRoot
|
|
30
|
+
servers = [ordered]@{
|
|
31
|
+
firmwareloop = [ordered]@{
|
|
32
|
+
command = $python
|
|
33
|
+
args = @($fwMcpServer)
|
|
34
|
+
description = 'Upper-tier firmware engineering workflow MCP'
|
|
35
|
+
}
|
|
36
|
+
agentic_hil = [ordered]@{
|
|
37
|
+
command = $ahilExe
|
|
38
|
+
args = @('mcp-stdio')
|
|
39
|
+
description = 'Lower-tier physical hardware & probe MCP'
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
agents = [ordered]@{}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# 1. Claude Code CLI
|
|
46
|
+
$claudeCmd = Get-Command claude -ErrorAction SilentlyContinue
|
|
47
|
+
$report.agents.claude_code = [ordered]@{
|
|
48
|
+
detected = ($null -ne $claudeCmd)
|
|
49
|
+
path = if ($claudeCmd) { $claudeCmd.Source } else { $null }
|
|
50
|
+
registration_commands = @(
|
|
51
|
+
"claude mcp add firmwareloop -- `"$python`" `"$fwMcpServer`"",
|
|
52
|
+
"claude mcp add agentic-hil -- `"$ahilExe`" mcp-stdio"
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# 2. Qoder IDE / CLI
|
|
57
|
+
$qoderCmd = $null
|
|
58
|
+
foreach ($name in @('qoder', 'qodercli')) {
|
|
59
|
+
$c = Get-Command $name -ErrorAction SilentlyContinue
|
|
60
|
+
if ($c) { $qoderCmd = $c; break }
|
|
61
|
+
}
|
|
62
|
+
$report.agents.qoder = [ordered]@{
|
|
63
|
+
detected = ($null -ne $qoderCmd)
|
|
64
|
+
path = if ($qoderCmd) { $qoderCmd.Source } else { $null }
|
|
65
|
+
registration_commands = if ($qoderCmd) {
|
|
66
|
+
@(
|
|
67
|
+
"$($qoderCmd.Name) mcp add firmwareloop -- `"$python`" `"$fwMcpServer`"",
|
|
68
|
+
"$($qoderCmd.Name) mcp add agentic-hil -- `"$ahilExe`" mcp-stdio"
|
|
69
|
+
)
|
|
70
|
+
} else {
|
|
71
|
+
@(
|
|
72
|
+
"qoder mcp add firmwareloop -- `"$python`" `"$fwMcpServer`"",
|
|
73
|
+
"qoder mcp add agentic-hil -- `"$ahilExe`" mcp-stdio"
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# 3. Antigravity CLI / Gemini Code
|
|
79
|
+
$antigravitySkillsRoot = Join-Path $env:USERPROFILE '.gemini\antigravity-cli\skills'
|
|
80
|
+
$targetGlobalSkillDir = Join-Path $antigravitySkillsRoot 'firmwareloop'
|
|
81
|
+
$sourceSkill = Join-Path $repoRoot 'skills\firmwareloop\SKILL.md'
|
|
82
|
+
|
|
83
|
+
$skillInstalled = $false
|
|
84
|
+
if (Test-Path -LiteralPath (Split-Path $antigravitySkillsRoot)) {
|
|
85
|
+
if (Test-Path -LiteralPath $sourceSkill) {
|
|
86
|
+
New-Item -ItemType Directory -Force -Path $targetGlobalSkillDir | Out-Null
|
|
87
|
+
Copy-Item -LiteralPath $sourceSkill -Destination (Join-Path $targetGlobalSkillDir 'SKILL.md') -Force
|
|
88
|
+
$skillInstalled = $true
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
$report.agents.antigravity = [ordered]@{
|
|
93
|
+
mcp_config_path = (Join-Path $repoRoot '.mcp.json')
|
|
94
|
+
global_skill_installed = $skillInstalled
|
|
95
|
+
global_skill_path = if ($skillInstalled) { Join-Path $targetGlobalSkillDir 'SKILL.md' } else { $null }
|
|
96
|
+
note = "Antigravity automatically discovers project-level .mcp.json and global skills."
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# Optional: write project-level .mcp.json
|
|
100
|
+
if ($WriteWorkspaceMcp) {
|
|
101
|
+
$mcpJsonPath = Join-Path $repoRoot '.mcp.json'
|
|
102
|
+
$mcpConfig = [ordered]@{
|
|
103
|
+
mcpServers = [ordered]@{
|
|
104
|
+
firmwareloop = [ordered]@{
|
|
105
|
+
type = 'stdio'
|
|
106
|
+
command = $python
|
|
107
|
+
args = @($fwMcpServer)
|
|
108
|
+
timeout = 300000
|
|
109
|
+
}
|
|
110
|
+
"agentic-hil" = [ordered]@{
|
|
111
|
+
type = 'stdio'
|
|
112
|
+
command = $ahilExe
|
|
113
|
+
args = @('mcp-stdio')
|
|
114
|
+
timeout = 120000
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
$jsonContent = ConvertTo-Json -InputObject $mcpConfig -Depth 10
|
|
119
|
+
[System.IO.File]::WriteAllText($mcpJsonPath, $jsonContent, [System.Text.Encoding]::UTF8)
|
|
120
|
+
$report.workspace_mcp_written = $mcpJsonPath
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if ($Json) {
|
|
124
|
+
Write-FwJson ([pscustomobject]$report)
|
|
125
|
+
} else {
|
|
126
|
+
Write-Host "============================================================" -ForegroundColor Cyan
|
|
127
|
+
Write-Host "FirmwareLoop v0.0.8 - Dual-Tier MCP & Skill Setup Helper" -ForegroundColor Green
|
|
128
|
+
Write-Host "============================================================" -ForegroundColor Cyan
|
|
129
|
+
Write-Host ""
|
|
130
|
+
Write-Host "[1] Claude Code CLI Registration:" -ForegroundColor Yellow
|
|
131
|
+
Write-Host " $($report.agents.claude_code.registration_commands[0])"
|
|
132
|
+
Write-Host " $($report.agents.claude_code.registration_commands[1])"
|
|
133
|
+
Write-Host ""
|
|
134
|
+
Write-Host "[2] Qoder IDE (Official GUI & Workspace .mcp.json):" -ForegroundColor Yellow
|
|
135
|
+
Write-Host " - GUI: Press 'Ctrl + Shift + ,' -> MCP -> My Services -> '+ Add', paste the JSON config"
|
|
136
|
+
Write-Host " - Workspace: Qoder automatically detects '.mcp.json' in your workspace root"
|
|
137
|
+
Write-Host " - CLI: $($report.agents.qoder.registration_commands[0])"
|
|
138
|
+
Write-Host " $($report.agents.qoder.registration_commands[1])"
|
|
139
|
+
Write-Host ""
|
|
140
|
+
Write-Host "[3] Antigravity CLI / Cursor / VS Code:" -ForegroundColor Yellow
|
|
141
|
+
Write-Host " - MCP: Automatically reads '.mcp.json' in workspace root"
|
|
142
|
+
if ($skillInstalled) {
|
|
143
|
+
Write-Host " - Global Skill: [OK] Successfully registered to: $targetGlobalSkillDir\SKILL.md" -ForegroundColor Green
|
|
144
|
+
}
|
|
145
|
+
Write-Host " - Generate workspace config: .\tools\setup-agent-mcp.ps1 -WriteWorkspaceMcp"
|
|
146
|
+
Write-Host ""
|
|
147
|
+
}
|
tools/test-backends.ps1
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Backend command tests (Spec GAP-002). Verifies:
|
|
4
|
+
- command construction for all 7 backends (dry-run)
|
|
5
|
+
- execution + log capture through fake executables (keil/iar/west/idf.py)
|
|
6
|
+
- artifact detection failure is reported (no fabricated artifacts)
|
|
7
|
+
Runs in CI without Keil/IAR licenses (Spec §27: fake executable).
|
|
8
|
+
|
|
9
|
+
Usage: .\tools\test-backends.ps1 -Json
|
|
10
|
+
#>
|
|
11
|
+
[CmdletBinding()]
|
|
12
|
+
param([switch]$Json)
|
|
13
|
+
|
|
14
|
+
Set-StrictMode -Version Latest
|
|
15
|
+
$ErrorActionPreference = 'Stop'
|
|
16
|
+
Import-Module (Join-Path $PSScriptRoot 'common\fw.psm1') -Force
|
|
17
|
+
|
|
18
|
+
$repoRoot = Get-FwRepoRoot
|
|
19
|
+
$fake = Join-Path $repoRoot 'tests\backend\fake-build-tools'
|
|
20
|
+
$results = [System.Collections.Generic.List[object]]::new()
|
|
21
|
+
|
|
22
|
+
function Assert {
|
|
23
|
+
param([string]$Name, [bool]$Ok, [string]$Detail)
|
|
24
|
+
$results.Add([pscustomobject]@{ name = $Name; ok = $Ok; detail = $Detail })
|
|
25
|
+
if (-not $Ok) { Write-Warning "FAIL: $Name - $Detail" }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function Invoke-Build {
|
|
29
|
+
param([string[]]$ToolArgs)
|
|
30
|
+
$argsList = @('-NoProfile', '-NonInteractive', '-File', (Join-Path $PSScriptRoot 'build.ps1')) + $ToolArgs
|
|
31
|
+
return Invoke-FwProcess -FilePath 'pwsh' -Arguments $argsList `
|
|
32
|
+
-WorkingDirectory $repoRoot -TimeoutMs 120000
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# ---- 1. dry-run command construction (all backends) ---------------------------
|
|
36
|
+
$cases = @(
|
|
37
|
+
@{ name = 'cmake'; args = @('-Backend', 'cmake', '-SourceDir', 'demo-firmware', '-DryRun', '-Json') ; expect = 'cmake -S' },
|
|
38
|
+
@{ name = 'make'; args = @('-Backend', 'make', '-SourceDir', 'demo-make', '-DryRun', '-Json') ; expect = 'make -C' },
|
|
39
|
+
@{ name = 'platformio'; args = @('-Backend', 'platformio', '-SourceDir', 'demo-firmware', '-DryRun', '-Json') ; expect = 'pio run' },
|
|
40
|
+
@{ name = 'keil'; args = @('-Backend', 'keil', '-SourceDir', 'tests/backend/fixtures/keil-proj', '-DryRun', '-Json') ; expect = 'UV4 -b' },
|
|
41
|
+
@{ name = 'iar'; args = @('-Backend', 'iar', '-SourceDir', 'tests/backend/fixtures/iar-proj', '-DryRun', '-Json') ; expect = 'IarBuild' },
|
|
42
|
+
@{ name = 'zephyr'; args = @('-Backend', 'zephyr', '-DryRun', '-Json') ; expect = 'west build' },
|
|
43
|
+
@{ name = 'esp-idf'; args = @('-Backend', 'esp-idf', '-DryRun', '-Json') ; expect = 'idf.py build' }
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# zephyr needs ZEPHYR_BOARD only for construction
|
|
47
|
+
$env:ZEPHYR_BOARD = 'native_sim'
|
|
48
|
+
|
|
49
|
+
foreach ($c in $cases) {
|
|
50
|
+
$r = Invoke-Build -ToolArgs $c.args
|
|
51
|
+
$jsonLine = ($r.stdout -split "`r?`n" | Where-Object { $_.TrimStart().StartsWith('{') } | Select-Object -Last 1)
|
|
52
|
+
$ok = $false
|
|
53
|
+
$detail = $r.stderr
|
|
54
|
+
if ($jsonLine) {
|
|
55
|
+
try { $j = $jsonLine | ConvertFrom-Json } catch { $j = $null }
|
|
56
|
+
if ($j -and $j.ok -and $j.dry_run -and $j.command -like "*$($c.expect)*") {
|
|
57
|
+
$ok = $true
|
|
58
|
+
$detail = "command: $($j.command)"
|
|
59
|
+
} else {
|
|
60
|
+
$detail = "unexpected: $jsonLine"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
Assert -Name "construct.$($c.name)" -Ok $ok -Detail $detail
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# ---- 2. fake executable execution (keil / iar / west / esp-idf) ----------------
|
|
67
|
+
$execCases = @(
|
|
68
|
+
@{ name = 'keil'; args = @('-Backend', 'keil', '-SourceDir', 'tests/backend/fixtures/keil-proj', '-BackendTool', (Join-Path $fake 'UV4.cmd'), '-Json') },
|
|
69
|
+
@{ name = 'iar'; args = @('-Backend', 'iar', '-SourceDir', 'tests/backend/fixtures/iar-proj', '-BackendTool', (Join-Path $fake 'IarBuild.cmd'), '-Json') },
|
|
70
|
+
@{ name = 'zephyr'; args = @('-Backend', 'zephyr', '-BackendTool', (Join-Path $fake 'west.cmd'), '-Json') },
|
|
71
|
+
@{ name = 'esp-idf'; args = @('-Backend', 'esp-idf', '-BackendTool', (Join-Path $fake 'idf.py.cmd'), '-Json') }
|
|
72
|
+
)
|
|
73
|
+
foreach ($c in $execCases) {
|
|
74
|
+
$r = Invoke-Build -ToolArgs $c.args
|
|
75
|
+
$log = Get-Content -LiteralPath (Join-Path $repoRoot 'artifacts\logs\build.log') -Raw -ErrorAction SilentlyContinue
|
|
76
|
+
# fake tool ran (log captured) AND artifact absence was detected honestly
|
|
77
|
+
$ran = $log -match 'FAKE_BACKEND_OK'
|
|
78
|
+
$honest = $r.exit_code -eq 2 -and $r.stdout -match 'ARTIFACT_NOT_FOUND'
|
|
79
|
+
Assert -Name "execute.$($c.name)" -Ok ($ran -and $honest) -Detail "ran=$ran honest=$honest exit=$($r.exit_code) log_tail=$($log.Substring(0,[Math]::Min(120,$log.Length)))"
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# ---- 3. cmake real build still green -------------------------------------------
|
|
83
|
+
$r = Invoke-Build -ToolArgs @('-Configuration', 'Debug', '-Clean', '-Json')
|
|
84
|
+
$jsonLine = ($r.stdout -split "`r?`n" | Where-Object { $_.TrimStart().StartsWith('{') } | Select-Object -Last 1)
|
|
85
|
+
$j = $jsonLine | ConvertFrom-Json
|
|
86
|
+
Assert -Name 'real.cmake' -Ok ($r.exit_code -eq 0 -and $j.ok -and $j.artifacts.primary.path -like '*firmware.elf*') `
|
|
87
|
+
-Detail "exit=$($r.exit_code) artifact=$($j.artifacts.primary.path)"
|
|
88
|
+
|
|
89
|
+
# ---- summary -------------------------------------------------------------------
|
|
90
|
+
$failed = @($results | Where-Object { -not $_.ok }).Count
|
|
91
|
+
$summary = [ordered]@{
|
|
92
|
+
schema = 'backend-test-result/v1'
|
|
93
|
+
ok = ($failed -eq 0)
|
|
94
|
+
total = $results.Count
|
|
95
|
+
failed = $failed
|
|
96
|
+
tests = @($results)
|
|
97
|
+
generated_at = Get-FwTimestamp
|
|
98
|
+
}
|
|
99
|
+
if ($Json) { Write-FwJson ([pscustomobject]$summary) -Compact } else { Write-FwJson ([pscustomobject]$summary) }
|
|
100
|
+
exit $(if ($failed -eq 0) { 0 } else { 1 })
|
tools/test.ps1
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
HIL test runner (Spec §18/19/23). Runs pytest under the project venv,
|
|
4
|
+
produces JUnit XML, a firmware-hil-result/v1 summary and a per-run audit
|
|
5
|
+
directory under artifacts/runs/<run_id>/.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
.\tools\test.ps1 -Json
|
|
9
|
+
.\tools\test.ps1 -TestPath tests/hil -RunId my-run -Json
|
|
10
|
+
|
|
11
|
+
Exit codes:
|
|
12
|
+
0 all tests passed (or skipped)
|
|
13
|
+
1 at least one test failed
|
|
14
|
+
2 configuration/runner error
|
|
15
|
+
#>
|
|
16
|
+
[CmdletBinding()]
|
|
17
|
+
param(
|
|
18
|
+
[string]$TestPath = 'tests/hil',
|
|
19
|
+
[string]$RunId,
|
|
20
|
+
[switch]$Json,
|
|
21
|
+
[switch]$NoBuild,
|
|
22
|
+
[ValidateSet('simulator', 'real')]
|
|
23
|
+
[string]$Mode = 'simulator'
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
Set-StrictMode -Version Latest
|
|
27
|
+
$ErrorActionPreference = 'Stop'
|
|
28
|
+
Import-Module (Join-Path $PSScriptRoot 'common\fw.psm1') -Force
|
|
29
|
+
|
|
30
|
+
$repoRoot = Get-FwRepoRoot
|
|
31
|
+
$python = Resolve-FwPython -RepoRoot $repoRoot
|
|
32
|
+
if (-not $python) {
|
|
33
|
+
$err = New-FwError -ErrorClass 'CONFIG_ERROR' -Message 'No Python found. Run tools/doctor.ps1.'
|
|
34
|
+
if ($Json) { Write-FwJson $err -Compact } else { Write-FwJson $err; Write-Error $err.error }
|
|
35
|
+
exit 2
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# --- run identity & audit dir -------------------------------------------------
|
|
39
|
+
$runId = if ($RunId) { $RunId } else { Get-FwRunId }
|
|
40
|
+
$runsDir = Join-Path $repoRoot "artifacts\runs\$runId"
|
|
41
|
+
New-Item -ItemType Directory -Force -Path $runsDir | Out-Null
|
|
42
|
+
$env:FW_RUN_DIR = $runsDir
|
|
43
|
+
|
|
44
|
+
# --- evidence: environment.json + dependencies.json (Spec §25) -----------------
|
|
45
|
+
$doctor = Invoke-FwProcess -FilePath 'pwsh' -Arguments @('-NoProfile', '-NonInteractive', '-File', (Join-Path $PSScriptRoot 'doctor.ps1'), '-Json') -WorkingDirectory $repoRoot -TimeoutMs 120000
|
|
46
|
+
if ($doctor.exit_code -in @(0, 1)) {
|
|
47
|
+
try {
|
|
48
|
+
$d = $doctor.stdout | ConvertFrom-Json
|
|
49
|
+
if ($d.checks) {
|
|
50
|
+
$deps = [ordered]@{}
|
|
51
|
+
foreach ($p in $d.checks.PSObject.Properties) {
|
|
52
|
+
$v = $p.Value
|
|
53
|
+
$verProp = $v.PSObject.Properties['version']
|
|
54
|
+
$deps[$p.Name] = if ($v.status -eq 'ok' -and $verProp) { $v.version } else { $v.status }
|
|
55
|
+
}
|
|
56
|
+
$deps | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath (Join-Path $runsDir 'dependencies.json') -Encoding utf8
|
|
57
|
+
}
|
|
58
|
+
if ($d.host) {
|
|
59
|
+
$d.host | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath (Join-Path $runsDir 'environment.json') -Encoding utf8
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
Write-Warning "evidence block failed: $($_.Exception.Message)"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
# --- firmware identity --------------------------------------------------------
|
|
66
|
+
$artifact = Join-Path $repoRoot 'artifacts\build\firmware.elf'
|
|
67
|
+
$firmwareInfo = [ordered]@{
|
|
68
|
+
artifact = $null
|
|
69
|
+
artifact_sha256 = $null
|
|
70
|
+
git_commit = $null
|
|
71
|
+
}
|
|
72
|
+
if (Test-Path -LiteralPath $artifact) {
|
|
73
|
+
$firmwareInfo.artifact = (Resolve-Path -LiteralPath $artifact).Path
|
|
74
|
+
$firmwareInfo.artifact_sha256 = (Get-FileHash -LiteralPath $artifact -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
75
|
+
}
|
|
76
|
+
$git = Get-FwGitInfo -RepoRoot $repoRoot
|
|
77
|
+
$firmwareInfo.git_commit = $git.commit
|
|
78
|
+
|
|
79
|
+
# repo state at run start for the audit trail (Spec §23 modified files)
|
|
80
|
+
$modifiedFiles = @()
|
|
81
|
+
$statusLines = git -C $repoRoot status --porcelain 2>$null
|
|
82
|
+
foreach ($line in $statusLines) {
|
|
83
|
+
if ($line -match '^..\s+(.+)$') {
|
|
84
|
+
$p = $Matches[1].Trim('"')
|
|
85
|
+
if ($p) { $modifiedFiles += $p }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# --- run pytest ---------------------------------------------------------------
|
|
90
|
+
# Pass --junitxml as a SEPARATE token with a relative path: pytest 9.x on this
|
|
91
|
+
# platform mis-parses the "--junitxml=<absolute or relative path>" fused form
|
|
92
|
+
# (the value is wrongly treated as a collect argument -> exit 4, "no match").
|
|
93
|
+
$xmlRel = (Join-Path 'artifacts\runs' "$runId\pytest.xml").Replace('\', '/')
|
|
94
|
+
$xmlNative = Join-Path $repoRoot $xmlRel.Replace('/', '\')
|
|
95
|
+
New-Item -ItemType File -Force -Path $xmlNative | Out-Null
|
|
96
|
+
$pytestArgs = @('-m', 'pytest', $TestPath, '--junitxml', $xmlRel, '--tb=short', '-p', 'no:cacheprovider', '-rA')
|
|
97
|
+
$res = Invoke-FwProcess -FilePath $python -Arguments $pytestArgs -WorkingDirectory $repoRoot -TimeoutMs 900000
|
|
98
|
+
Save-FwLog -Path (Join-Path $runsDir 'pytest.stdout.log') -Content $res.stdout
|
|
99
|
+
Save-FwLog -Path (Join-Path $runsDir 'pytest.stderr.log') -Content $res.stderr
|
|
100
|
+
|
|
101
|
+
if ($res.timed_out) {
|
|
102
|
+
$err = New-FwError -ErrorClass 'INSTRUMENT_TIMEOUT' -Message 'pytest run timed out and was killed.' -Detail $xmlReport
|
|
103
|
+
if ($Json) { Write-FwJson $err -Compact } else { Write-FwJson $err; Write-Error $err.error }
|
|
104
|
+
exit 2
|
|
105
|
+
}
|
|
106
|
+
if ($res.exit_code -eq 4) {
|
|
107
|
+
# usage error: pytest rejected the invocation - configuration problem
|
|
108
|
+
$err = New-FwError -ErrorClass 'CONFIG_ERROR' -Message 'pytest rejected the invocation (usage error, exit 4).' -Detail $res.stdout
|
|
109
|
+
if ($Json) { Write-FwJson $err -Compact } else { Write-FwJson $err; Write-Error $err.error }
|
|
110
|
+
exit 2
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# --- parse JUnit XML ----------------------------------------------------------
|
|
114
|
+
$tests = @()
|
|
115
|
+
$failures = 0
|
|
116
|
+
$skippedAsFailure = $false
|
|
117
|
+
if (Test-Path -LiteralPath $xmlNative) {
|
|
118
|
+
[xml]$xml = Get-Content -LiteralPath $xmlNative -Raw
|
|
119
|
+
foreach ($tc in $xml.testsuites.testsuite.testcase) {
|
|
120
|
+
$entry = [ordered]@{
|
|
121
|
+
name = $tc.name
|
|
122
|
+
status = 'passed'
|
|
123
|
+
expected = $null
|
|
124
|
+
actual = $null
|
|
125
|
+
evidence = @()
|
|
126
|
+
}
|
|
127
|
+
$failNode = $tc.SelectSingleNode('failure')
|
|
128
|
+
$skipNode = $tc.SelectSingleNode('skipped')
|
|
129
|
+
if ($failNode) {
|
|
130
|
+
$entry.status = 'failed'
|
|
131
|
+
$failures++
|
|
132
|
+
$msg = [string]$failNode.message
|
|
133
|
+
$expected = $null; $actual = $null
|
|
134
|
+
if ($msg -match 'expected (.+?), actual (.+?)(?::|$)') {
|
|
135
|
+
$expected = $Matches[1]; $actual = $Matches[2]
|
|
136
|
+
}
|
|
137
|
+
if (-not $expected -and $msg -match "expected (.+)") { $expected = $Matches[1] }
|
|
138
|
+
$entry.expected = $expected
|
|
139
|
+
$entry.actual = $actual
|
|
140
|
+
$entry.failure = ([string]$failNode.InnerText)
|
|
141
|
+
} elseif ($skipNode) {
|
|
142
|
+
$entry.status = 'skipped'
|
|
143
|
+
$entry.reason = [string]$skipNode.message
|
|
144
|
+
# REAL mode: a skipped required HIL test is a failure, never PASS
|
|
145
|
+
# (release-fix: simulator/skip must not masquerade as real success)
|
|
146
|
+
if ($Mode -eq 'real') {
|
|
147
|
+
$entry.status = 'failed'
|
|
148
|
+
$entry.expected = 'executed on real hardware'
|
|
149
|
+
$entry.actual = "skipped: $($entry.reason)"
|
|
150
|
+
$failures++
|
|
151
|
+
$skippedAsFailure = $true
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
# evidence artifacts copied into the run dir by fixtures
|
|
155
|
+
$tests += [pscustomobject]$entry
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
# --- evidence inventory -------------------------------------------------------
|
|
160
|
+
$evidence = @()
|
|
161
|
+
foreach ($f in @('uart.log', 'measurements.json')) {
|
|
162
|
+
$p = Join-Path $runsDir $f
|
|
163
|
+
if (Test-Path -LiteralPath $p) { $evidence += (Resolve-Path -LiteralPath $p).Path }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
$summary = [ordered]@{
|
|
167
|
+
schema = 'firmware-hil-result/v1'
|
|
168
|
+
run_id = $runId
|
|
169
|
+
execution_mode = $Mode
|
|
170
|
+
simulated = ($Mode -ne 'real')
|
|
171
|
+
hardware_validated = ($Mode -eq 'real' -and $failures -eq 0)
|
|
172
|
+
ok = ($failures -eq 0)
|
|
173
|
+
firmware = $firmwareInfo
|
|
174
|
+
tests = $tests
|
|
175
|
+
evidence = $evidence
|
|
176
|
+
audit = [ordered]@{
|
|
177
|
+
git_commit = $git.commit
|
|
178
|
+
git_dirty = $git.dirty
|
|
179
|
+
modified_files = $modifiedFiles
|
|
180
|
+
generated_at = Get-FwTimestamp
|
|
181
|
+
}
|
|
182
|
+
hardware = [ordered]@{
|
|
183
|
+
target_identity = $null # set when a real probe is configured (M2)
|
|
184
|
+
probe_serial = $null
|
|
185
|
+
com_port = $null
|
|
186
|
+
}
|
|
187
|
+
generated_at = Get-FwTimestamp
|
|
188
|
+
}
|
|
189
|
+
$summaryPath = Join-Path $runsDir 'summary.json'
|
|
190
|
+
$summary | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $summaryPath -Encoding utf8
|
|
191
|
+
|
|
192
|
+
if ($Json) { Write-FwJson ([pscustomobject]$summary) -Compact } else { Write-FwJson ([pscustomobject]$summary) }
|
|
193
|
+
|
|
194
|
+
exit $(if ($failures -eq 0) { 0 } else { 1 })
|