Creating Custom BIT Tests¶
Guide to developing custom Built-In Tests for the BIT SDK.
The BIT SDK uses a plugin architecture that allows you to create custom tests tailored to your specific hardware and requirements. Tests are compiled as dynamic libraries (.so files) and loaded at runtime.
Test Plugin Architecture¶
Key Components¶
1. Test Plugin Interface
- Tests are dynamic libraries implementing the bit_plugin interface
- Loaded at runtime via dlopen
- Discoverable via standardized naming convention
2. Core Traits
Tests must implement three traits from the bit_plugin crate:
- Test - Test metadata and lifecycle
- TestRun - Test execution logic
- TestDetails - Test type and classification
3. Configuration
- TOML-based configuration files
- One config file per test
- Loaded from BIT_CONFIG_PATH
Getting Started¶
1. Use the Test Template¶
The fastest way to create a new test is to copy the test template:
See Test Template Guide for detailed walkthrough.
2. Cargo Project Setup¶
Cargo.toml structure:
[package]
name = "my_custom_test"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"] # Dynamic library
[dependencies]
bit_plugin = { path = "../bit_plugin" }
serde = { version = "1.0", features = ["derive"] }
toml = "0.8"
log = "0.4"
Key points:
- crate-type = ["cdylib"] - Builds .so file
- bit_plugin - Core trait definitions
- serde + toml - Configuration loading
3. Basic Test Structure¶
src/lib.rs skeleton:
use bit_plugin::{create_plugin, Test, TestDetails, TestRun, TestType};
use serde::Deserialize;
use std::path::Path;
#[derive(Debug, Deserialize)]
pub struct MyTestConfig {
enabled: bool,
// Add your configuration fields
}
pub struct MyTest {
config: MyTestConfig,
}
impl MyTest {
pub fn new(config_path: &Path) -> Result<Self, Box<dyn std::error::Error>> {
let config_content = std::fs::read_to_string(config_path)?;
let config: MyTestConfig = toml::from_str(&config_content)?;
Ok(Self { config })
}
}
impl Test for MyTest {
fn name(&self) -> &str {
"my_custom_test"
}
fn enabled(&self) -> bool {
self.config.enabled
}
}
impl TestRun for MyTest {
fn run(&self) -> Result<(), Box<dyn std::error::Error>> {
// Your test logic here
log::info!("Running my custom test");
Ok(())
}
}
impl TestDetails for MyTest {
fn test_type(&self) -> TestType {
TestType::Pbit // or Cbit, Fbit
}
}
// Export plugin entry point
create_plugin!(MyTest, MyTest::new);
Implementing Test Traits¶
Test Trait¶
Provides test metadata and control:
impl Test for MyTest {
fn name(&self) -> &str {
"my_custom_test"
}
fn enabled(&self) -> bool {
self.config.enabled
}
fn description(&self) -> &str {
"My custom hardware test"
}
fn version(&self) -> &str {
env!("CARGO_PKG_VERSION")
}
}
Required methods:
- name() - Unique test identifier
- enabled() - Whether test should run
Optional methods:
- description() - Human-readable description
- version() - Test version string
TestRun Trait¶
Contains test execution logic:
impl TestRun for MyTest {
fn run(&self) -> Result<(), Box<dyn std::error::Error>> {
log::info!("Starting test: {}", self.name());
// 1. Validate preconditions
if !self.check_prerequisites() {
return Err("Prerequisites not met".into());
}
// 2. Execute test logic
self.perform_test()?;
// 3. Validate results
if !self.verify_results() {
return Err("Test verification failed".into());
}
log::info!("Test passed: {}", self.name());
Ok(())
}
}
Error handling:
- Return Ok(()) for test pass
- Return Err(...) for test failure
- Use descriptive error messages
TestDetails Trait¶
Specifies test classification:
Test types:
- TestType::Pbit - Run once at startup
- TestType::Cbit - Run continuously
- TestType::Fbit - Factory/depot testing
Configuration Patterns¶
Basic Configuration¶
#[derive(Debug, Deserialize)]
pub struct MyTestConfig {
enabled: bool,
threshold: i32,
timeout_secs: u64,
}
Array Configuration¶
[my_custom_test]
enabled = true
[[device]]
name = "device0"
threshold = 100
[[device]]
name = "device1"
threshold = 200
#[derive(Debug, Deserialize)]
pub struct DeviceConfig {
name: String,
threshold: i32,
}
#[derive(Debug, Deserialize)]
pub struct MyTestConfig {
enabled: bool,
device: Vec<DeviceConfig>,
}
Nested Configuration¶
[my_custom_test]
enabled = true
[my_custom_test.thresholds]
min = 0
max = 100
warning = 80
[my_custom_test.timing]
interval_secs = 60
timeout_secs = 10
#[derive(Debug, Deserialize)]
pub struct Thresholds {
min: i32,
max: i32,
warning: i32,
}
#[derive(Debug, Deserialize)]
pub struct Timing {
interval_secs: u64,
timeout_secs: u64,
}
#[derive(Debug, Deserialize)]
pub struct MyTestConfig {
enabled: bool,
thresholds: Thresholds,
timing: Timing,
}
CBIT Frequency Requirement¶
For Continuous Built-In Tests (CBIT), it is a standard convention to include a frequency field (in seconds) to control how often the test runs.
Recommended defaults: - Resource Usage (CPU/Mem/Disk): 30s - Hardware Presence (USB/PCI): 5s - Interface Status: 10s - Log Analysis: 60s - Integrity Checks: 300s
Adding Learn Functions¶
Learn functions help users generate configurations interactively. Add to tools/src/learn.rs:
pub fn my_custom_test_learn() -> Result<(), Box<dyn std::error::Error>> {
use crate::common::*;
println!("\n=== My Custom Test Configuration ===");
// 1. Enable test
let enabled = prompt_bool("Enable test?", true)?;
// 2. Get threshold
let threshold = prompt_number("Enter threshold:", 42)?;
// 3. Detect devices
let devices = detect_my_devices()?;
println!("Detected {} devices", devices.len());
// 4. Generate config
let config = format!(
"[my_custom_test]\nenabled = {}\nthreshold = {}\n",
enabled, threshold
);
// 5. Add devices
let mut full_config = config;
for device in devices {
full_config.push_str(&format!(
"\n[[device]]\nname = \"{}\"\n",
device
));
}
// 6. Write config
let config_path = get_config_path("my_custom_test.toml")?;
std::fs::write(&config_path, full_config)?;
println!("✓ Configuration saved to {}", config_path.display());
Ok(())
}
Helper functions available:
- prompt_bool(prompt, default) - Yes/no questions
- prompt_number(prompt, default) - Numeric input
- prompt_string(prompt, default) - String input
- get_config_path(filename) - Config file path
Register in learn dispatcher:
// In tools/src/learn.rs
pub fn run_learn(test_name: &str) -> Result<(), Box<dyn std::error::Error>> {
match test_name {
// ... existing tests ...
"my_custom_test" => my_custom_test_learn(),
_ => Err(format!("Unknown test: {}", test_name).into()),
}
}
Testing Your Plugin¶
1. Build the Plugin¶
2. Create Configuration¶
# Manual config
cat > config/my_custom_test.toml << EOF
[my_custom_test]
enabled = true
threshold = 42
EOF
# Or use learn function
BIT_CONFIG_PATH="./config" ./target/debug/bit-learn my_custom_test
3. Test Execution¶
# Set environment variables
export BIT_TEST_PATH="./target/debug"
export BIT_CONFIG_PATH="./config"
# Run test
./target/debug/bit-manager -t my_custom_test -o
4. Debug Output¶
# Verbose logging
RUST_LOG=debug ./target/debug/bit-manager -t my_custom_test -o
# Test-specific logging
RUST_LOG=my_custom_test=trace ./target/debug/bit-manager -t my_custom_test -o
Best Practices¶
Error Handling¶
impl TestRun for MyTest {
fn run(&self) -> Result<(), Box<dyn std::error::Error>> {
// Use descriptive errors
let value = self.read_sensor()
.map_err(|e| format!("Failed to read sensor: {}", e))?;
// Validate with context
if value > self.config.threshold {
return Err(format!(
"Value {} exceeds threshold {}",
value, self.config.threshold
).into());
}
Ok(())
}
}
Logging¶
use log::{debug, info, warn, error};
impl TestRun for MyTest {
fn run(&self) -> Result<(), Box<dyn std::error::Error>> {
info!("Starting test: {}", self.name());
debug!("Configuration: {:?}", self.config);
let result = self.perform_test()?;
debug!("Test result: {:?}", result);
if result.has_warnings() {
warn!("Test passed with warnings: {:?}", result.warnings);
}
info!("Test completed successfully");
Ok(())
}
}
Resource Cleanup¶
impl TestRun for MyTest {
fn run(&self) -> Result<(), Box<dyn std::error::Error>> {
// Open resource
let mut device = self.open_device()?;
// Ensure cleanup with defer pattern
let result = (|| {
device.configure()?;
device.test()?;
Ok(())
})();
// Cleanup always runs
device.close()?;
result
}
}
Performance Considerations¶
impl TestRun for MyTest {
fn run(&self) -> Result<(), Box<dyn std::error::Error>> {
// Cache expensive operations
let metadata = self.get_metadata_cached()?;
// Avoid repeated allocations
let mut buffer = Vec::with_capacity(1024);
for _ in 0..100 {
buffer.clear();
self.read_data(&mut buffer)?;
}
// Parallel execution for independent tests
use rayon::prelude::*;
let results: Vec<_> = self.config.devices
.par_iter()
.map(|dev| self.test_device(dev))
.collect();
Ok(())
}
}
Integration with BIT Manager¶
Plugin Discovery¶
BIT Manager discovers plugins by:
1. Scanning BIT_TEST_PATH directory
2. Looking for files matching lib*.so pattern
3. Loading each plugin via dlopen
4. Calling plugin's create_test() function
Plugin Loading¶
// In bit_manager (internal)
let plugin_path = format!("{}/lib{}.so", test_path, test_name);
let plugin = load_plugin(&plugin_path)?;
let test = plugin.create_test(&config_path)?;
Test Execution¶
Tests are executed based on type:
- PBIT: Run once at startup (-o flag)
- CBIT: Run continuously in loop (-c flag)
- FBIT: Run in factory mode (-f flag)
Configuration Loading¶
Each test receives its config file path:
Common Patterns¶
Hardware Detection¶
fn detect_devices(&self) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let sys_path = "/sys/class/my_device";
let entries = std::fs::read_dir(sys_path)?;
let mut devices = Vec::new();
for entry in entries {
let entry = entry?;
if entry.file_type()?.is_dir() {
devices.push(entry.file_name().to_string_lossy().to_string());
}
}
Ok(devices)
}
Sysfs Reading¶
fn read_sysfs_value(&self, path: &str) -> Result<String, Box<dyn std::error::Error>> {
let content = std::fs::read_to_string(path)
.map_err(|e| format!("Failed to read {}: {}", path, e))?;
Ok(content.trim().to_string())
}
Command Execution¶
fn run_command(&self, cmd: &str, args: &[&str]) -> Result<String, Box<dyn std::error::Error>> {
let output = std::process::Command::new(cmd)
.args(args)
.output()
.map_err(|e| format!("Failed to execute {}: {}", cmd, e))?;
if !output.status.success() {
return Err(format!(
"Command failed with exit code {}",
output.status
).into());
}
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
Next Steps¶
- Test Template Guide - Detailed template walkthrough
- Test Reference - Existing test examples
- Configuration Guide - Config file format
- Running Tests - Test execution