Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8e5951b
sort of deciding on a test format
LelsersLasers Jun 11, 2026
958302a
schema for tests
LelsersLasers Jun 11, 2026
f62580a
serde to read configs
LelsersLasers Jun 11, 2026
8baf9da
able to list all presets and tests (untested)
LelsersLasers Jun 13, 2026
ac5c070
Fix config loading
LelsersLasers Jun 13, 2026
d31ef42
Wiring HIL widget + trying to think about UI state
LelsersLasers Jun 13, 2026
637a792
System to kind of load a test
LelsersLasers Jun 13, 2026
254ff29
start from preset
LelsersLasers Jun 13, 2026
da5278e
Basic UI started for in progress tests
LelsersLasers Jun 13, 2026
e41a043
Trying to work on UI
LelsersLasers Jun 13, 2026
2ddd221
improve in progress UI
LelsersLasers Jun 13, 2026
e2f056a
Test summary UI
LelsersLasers Jun 13, 2026
d0f096e
Breaking into helper functions
LelsersLasers Jun 13, 2026
bcddfa7
Thinking about HIL state machine
LelsersLasers Jun 13, 2026
21684ca
update heartbeat tests to be accurate
LelsersLasers Jun 13, 2026
b34c775
sorting tests by basename, presets by name
LelsersLasers Jun 13, 2026
bbfca61
Actually running expects + can stop a test midway
LelsersLasers Jun 13, 2026
28a7d11
fmt :skull:
LelsersLasers Jun 13, 2026
1bfce2d
remove unused import
LelsersLasers Jun 13, 2026
b13c261
Clear start errors on new start attempt
LelsersLasers Jun 13, 2026
b94819e
Clear start error on reload
LelsersLasers Jun 13, 2026
78a468d
actually displaying start error
LelsersLasers Jun 13, 2026
3f28ad7
total_cmp so NaN's don't nuke?
LelsersLasers Jun 13, 2026
c232556
Clean loading + filter for .json
LelsersLasers Jun 13, 2026
d6762e3
Fix typos
LelsersLasers Jun 13, 2026
9cd1584
typos
LelsersLasers Jun 13, 2026
ee515d8
Fix failed rebase
LelsersLasers Jul 29, 2026
4e8b04b
fmt
LelsersLasers Jul 29, 2026
1955e4f
typo fix
LelsersLasers Jul 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions hil_config/presets.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"All Heartbeats": [ "heartbeats_crit", "heartbeats_non_crit" ]
}
86 changes: 86 additions & 0 deletions hil_config/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",

"required": ["name", "description", "expect"],

"properties": {
"$schema": {
"type": "string"
},

"name": {
"type": "string"
},

"description": {
"type": "string"
},

"tx": {
"type": "array",
"items": {
"type": "object",
"required": ["timestamp", "msg_name", "signals"],

"properties": {
"timestamp": {
"type": "number"
},

"msg_name": {
"type": "string"
},

"signals": {
"type": "object",
"additionalProperties": {
"type": "number"
}
}
},

"additionalProperties": false
}
},

"expect": {
"type": "array",
"items": {
"type": "object",
"required": ["window", "msg_name"],

"properties": {
"window": {
"type": "array",
"items": {
"type": "number"
},
"minItems": 2,
"maxItems": 2
},

"msg_name": {
"type": "string"
},

"signals": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "number"
},
"minItems": 2,
"maxItems": 2
}
}
},

"additionalProperties": false
}
}
},

"additionalProperties": false
}
25 changes: 25 additions & 0 deletions hil_config/tests/heartbeats_crit.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"$schema": "../schema.json",

"name": "Critical Heartbeats",
"description": "Check that we receive a heartbeat (version hash) from every drive critical board",

"expect": [
{
"window": [0, 10000],
"msg_name": "abox_version"
},
{
"window": [0, 10000],
"msg_name": "dash_version"
},
{
"window": [0, 10000],
"msg_name": "main_version"
},
{
"window": [0, 10000],
"msg_name": "pdu_version"
}
]
}
17 changes: 17 additions & 0 deletions hil_config/tests/heartbeats_non_crit.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"$schema": "../schema.json",

"name": "Non-Critical Heartbeats",
"description": "Check that we receive a heartbeat (version hash) from non-critical boards",

"expect": [
{
"window": [0, 10000],
"msg_name": "front_driveline_version"
},
{
"window": [0, 10000],
"msg_name": "rear_driveline_version"
}
]
}
27 changes: 27 additions & 0 deletions hil_config/tests/idk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"$schema": "../schema.json",

"name": "Pack sanity check",
"description": "Verify thermistor response",

"tx": [
{
"timestamp": 15,
"msg_name": "pack_stats",
"signals": {
"pack_voltage": 540.0,
"pack_current": 10.0,
"max_temp": 35.0
}
}
],
"expect": [
{
"window": [15, 20],
"msg_name": "thermistor_telemetry",
"signals": {
"temperature": [30.0, 40.0]
}
}
]
}
1 change: 1 addition & 0 deletions src/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ impl AppAction {
"Spawn Jitter",
widget_constructor::WidgetConstructor::Jitter,
),
("Spawn HIL", widget_constructor::WidgetConstructor::Hil),
]
}
}
159 changes: 159 additions & 0 deletions src/hil/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// Read HIL config files (preset and tests)

use serde::{Deserialize, Serialize};
use std::collections::HashMap;

pub const PRESETS_FILE: &str = "hil_config/presets.json";
pub const TESTS_FOLDER: &str = "hil_config/tests/";
Comment thread
LelsersLasers marked this conversation as resolved.

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(transparent)]
pub struct PresetsFile(pub HashMap<String, Vec<String>>);

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestFile {
pub name: String,
pub description: String,

#[serde(default)]
pub tx: Vec<TxMessage>,

pub expect: Vec<Expectation>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TxMessage {
pub timestamp: f64,
pub msg_name: String,

pub signals: HashMap<String, f64>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Expectation {
pub window: [f64; 2],

pub msg_name: String,

#[serde(default)]
pub signals: HashMap<String, [f64; 2]>,
}

#[derive(Clone)]
pub struct PresetInfo {
pub name: String,
// base names
pub tests: Vec<String>,
}

#[derive(Clone)]
pub struct TestInfo {
pub basename: String,
pub name: String,
pub description: String,
}

pub fn list_available_tests() -> (Vec<PresetInfo>, Vec<TestInfo>, Vec<String>) {
let mut errors = Vec::new();

// Load individual tests
let mut individual_tests = Vec::new();
if let Ok(entries) = std::fs::read_dir(TESTS_FOLDER) {
for entry in entries.flatten() {
let path = entry.path();
if !path.is_file() || path.extension().and_then(|e| e.to_str()) != Some("json") {
continue;
}

let basename = match path.file_stem().and_then(|s| s.to_str()) {
Some(stem) if !stem.is_empty() => stem.to_string(),
_ => {
errors.push(format!(
"Failed to determine test basename for file: {:?}",
path
));
continue;
}
};

let content = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(_) => {
errors.push(format!("Failed to read test file: {:?}", path));
continue;
}
};

match serde_json::from_str::<TestFile>(&content) {
Ok(test_data) => individual_tests.push(TestInfo {
basename,
name: test_data.name,
description: test_data.description,
}),
Err(_) => errors.push(format!("Failed to parse test file: {:?}", path)),
};
}
} else {
errors.push(format!("Failed to read tests directory: {}", TESTS_FOLDER));
}
individual_tests.sort_by(|a, b| a.basename.to_lowercase().cmp(&b.basename.to_lowercase()));

// Load presets
let mut presets = Vec::new();
if let Ok(presets_file) = std::fs::read_to_string(PRESETS_FILE) {
if let Ok(presets_data) = serde_json::from_str::<PresetsFile>(&presets_file) {
for (preset_name, subtests) in presets_data.0 {
if subtests.is_empty() {
errors.push(format!("Preset '{}' has no subtests defined", preset_name));
continue;
}

if subtests.iter().any(|s| s.trim().is_empty()) {
errors.push(format!(
"Preset '{}' contains empty subtest names",
preset_name
));
continue;
}

if subtests
.iter()
.any(|s| individual_tests.iter().all(|t| t.basename != *s))
{
errors.push(format!(
"Preset '{}' contains subtests that do not exist: {:?}",
preset_name,
subtests
.iter()
.filter(|s| individual_tests.iter().all(|t| t.basename != **s))
.collect::<Vec<_>>()
));
continue;
}

presets.push(PresetInfo {
name: preset_name,
tests: subtests,
});
}
} else {
errors.push(format!("Failed to parse presets file: {}", PRESETS_FILE));
}
} else {
errors.push(format!("Failed to read presets file: {}", PRESETS_FILE));
}
presets.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));

(presets, individual_tests, errors)
}

pub fn load_test_from_file(basename: &str) -> Result<TestFile, String> {
let path = format!("{}{}.json", TESTS_FOLDER, basename);
match std::fs::read_to_string(&path) {
Ok(content) => match serde_json::from_str::<TestFile>(&content) {
Ok(test_data) => Ok(test_data),
Err(_) => Err(format!("Failed to parse test file: {}", path)),
},
Err(_) => Err(format!("Failed to read test file: {}", path)),
}
}
2 changes: 2 additions & 0 deletions src/hil/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod config;
pub mod run;
Loading
Loading