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:

cd /home/zen/local/bit
cp -r test_template my_custom_test

cd my_custom_test

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:

impl TestDetails for MyTest {
    fn test_type(&self) -> TestType {
        TestType::Pbit  // Power-On BIT
    }
}

Test types: - TestType::Pbit - Run once at startup - TestType::Cbit - Run continuously - TestType::Fbit - Factory/depot testing

Configuration Patterns

Basic Configuration

[my_custom_test]
enabled = true
threshold = 42
timeout_secs = 10
#[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.

[cbit_my_test]
enabled = true
frequency = 30  # Run every 30 seconds

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

cargo build
# Output: target/debug/libmy_custom_test.so

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:

BIT_CONFIG_PATH/my_custom_test.toml

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