Test Template Guide

Detailed walkthrough of using the BIT SDK test template to create custom tests.

The test template (test_template/) provides a complete starting point for creating new BIT tests. It includes all necessary boilerplate, configuration structures, and build setup.

Template Overview

The test template is a fully functional example test that demonstrates: - Plugin trait implementation - Configuration loading - Test execution patterns - Learn function integration - Build configuration

Location: /home/zen/local/bit/test_template/

Template Structure

test_template/
├── Cargo.toml           # Build configuration
├── README.md            # Template documentation
└── src/
    └── lib.rs           # Test implementation

Cargo.toml

[package]
name = "test_template"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]  # Builds as dynamic library (.so)

[dependencies]
bit_plugin = { path = "../bit_plugin" }
serde = { version = "1.0", features = ["derive"] }
toml = "0.8"
log = "0.4"

Key elements: - crate-type = ["cdylib"] - Essential for plugin loading - bit_plugin dependency - Core trait definitions - serde + toml - Configuration parsing - log - Logging infrastructure

src/lib.rs

Complete implementation showing: - Configuration structure with serde - Test trait implementations - Plugin entry point macro - Error handling patterns

Creating a New Test

Step 1: Copy Template

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

Choose a descriptive name following the pattern: - pbit_* - Power-On BIT tests - cbit_* - Continuous BIT tests
- fbit_* - Factory BIT tests

Step 2: Update Package Name

Edit Cargo.toml:

[package]
name = "my_new_test"  # Change this
version = "0.1.0"
edition = "2021"

# Rest remains the same

Step 3: Customize Configuration

Edit src/lib.rs configuration structure:

Simple configuration:

#[derive(Debug, Deserialize)]
pub struct MyNewTestConfig {
    enabled: bool,
    threshold: i32,
    interval_secs: u64,
}

Configuration with devices:

#[derive(Debug, Deserialize)]
pub struct DeviceConfig {
    name: String,
    address: String,
    timeout_ms: u64,
}

#[derive(Debug, Deserialize)]
pub struct MyNewTestConfig {
    enabled: bool,
    device: Vec<DeviceConfig>,
}

Nested configuration:

#[derive(Debug, Deserialize)]
pub struct Limits {
    min: f64,
    max: f64,
    warning: f64,
}

#[derive(Debug, Deserialize)]
pub struct MyNewTestConfig {
    enabled: bool,
    limits: Limits,
    check_interval_secs: u64,
}

Step 4: Update Test Metadata

impl Test for MyNewTest {
    fn name(&self) -> &str {
        "my_new_test"  // Unique identifier
    }

    fn enabled(&self) -> bool {
        self.config.enabled
    }

    fn description(&self) -> &str {
        "Tests my specific hardware functionality"  // Update this
    }

    fn version(&self) -> &str {
        env!("CARGO_PKG_VERSION")
    }
}

Step 5: Implement Test Logic

impl TestRun for MyNewTest {
    fn run(&self) -> Result<(), Box<dyn std::error::Error>> {
        log::info!("Starting {}", self.name());

        // 1. Check preconditions
        if !self.config.enabled {
            return Ok(());
        }

        // 2. Your test implementation here
        let result = self.perform_my_test()?;

        // 3. Validate results
        if result < self.config.threshold {
            return Err(format!(
                "Test failed: result {} below threshold {}",
                result, self.config.threshold
            ).into());
        }

        log::info!("Test passed: result = {}", result);
        Ok(())
    }
}

// Add your helper methods
impl MyNewTest {
    fn perform_my_test(&self) -> Result<i32, Box<dyn std::error::Error>> {
        // Your test logic
        Ok(42)
    }
}

Step 6: Set Test Type

impl TestDetails for MyNewTest {
    fn test_type(&self) -> TestType {
        TestType::Pbit  // Choose: Pbit, Cbit, or Fbit
    }
}

Test type guidelines: - Pbit - Quick validation, run at startup - Cbit - Ongoing monitoring, run periodically - Fbit - Comprehensive testing, may be destructive

Step 7: Build and Test

# Build the plugin
cargo build

# Output location
ls -l ../target/debug/libmy_new_test.so

# Create test configuration
mkdir -p ../bit_manager/config
cat > ../bit_manager/config/my_new_test.toml << EOF
[my_new_test]
enabled = true
threshold = 42
EOF

# Run test
cd ..
BIT_TEST_PATH="./target/debug" \
BIT_CONFIG_PATH="./bit_manager/config" \
./target/debug/bit-manager -t my_new_test -o

Template Walkthrough

Configuration Loading

The template shows standard TOML loading:

impl MyNewTest {
    pub fn new(config_path: &Path) -> Result<Self, Box<dyn std::error::Error>> {
        // Read config file
        let config_content = std::fs::read_to_string(config_path)?;

        // Parse TOML
        let config: MyNewTestConfig = toml::from_str(&config_content)?;

        // Create instance
        Ok(Self { config })
    }
}

Error handling: - File not found → std::fs::read_to_string returns error - Invalid TOML → toml::from_str returns error - Both propagate to caller via ? operator

Plugin Entry Point

The create_plugin! macro generates the C-compatible entry point:

create_plugin!(MyNewTest, MyNewTest::new);

Expands to:

#[no_mangle]
pub extern "C" fn create_test(
    config_path: *const std::os::raw::c_char
) -> *mut dyn Test {
    // Convert C string to Rust Path
    // Call MyNewTest::new(path)
    // Return boxed trait object
}

This allows BIT Manager to load the plugin dynamically.

Test Execution Flow

impl TestRun for MyNewTest {
    fn run(&self) -> Result<(), Box<dyn std::error::Error>> {
        // 1. Log start
        log::info!("Starting {}", self.name());

        // 2. Validate enabled
        if !self.enabled() {
            log::debug!("Test disabled, skipping");
            return Ok(());
        }

        // 3. Run test logic
        match self.execute_test() {
            Ok(()) => {
                log::info!("Test passed");
                Ok(())
            }
            Err(e) => {
                log::error!("Test failed: {}", e);
                Err(e)
            }
        }
    }
}

Advanced Patterns

Multi-Device Testing

#[derive(Debug, Deserialize)]
pub struct DeviceConfig {
    name: String,
    enabled: bool,
}

#[derive(Debug, Deserialize)]
pub struct MyTestConfig {
    enabled: bool,
    device: Vec<DeviceConfig>,
}

impl TestRun for MyTest {
    fn run(&self) -> Result<(), Box<dyn std::error::Error>> {
        // Test each device
        for device in &self.config.device {
            if !device.enabled {
                log::debug!("Skipping disabled device: {}", device.name);
                continue;
            }

            log::info!("Testing device: {}", device.name);
            self.test_device(device)?;
        }
        Ok(())
    }
}

Threshold-Based Validation

#[derive(Debug, Deserialize)]
pub struct Thresholds {
    warning: i32,
    critical: i32,
}

#[derive(Debug, Deserialize)]
pub struct MyTestConfig {
    enabled: bool,
    thresholds: Thresholds,
}

impl TestRun for MyTest {
    fn run(&self) -> Result<(), Box<dyn std::error::Error>> {
        let value = self.read_sensor()?;

        if value >= self.config.thresholds.critical {
            return Err(format!(
                "Critical: {} >= {}",
                value, self.config.thresholds.critical
            ).into());
        }

        if value >= self.config.thresholds.warning {
            log::warn!(
                "Warning: {} >= {}",
                value, self.config.thresholds.warning
            );
        }

        log::info!("Value {} within limits", value);
        Ok(())
    }
}

Continuous Monitoring

For CBIT tests, add interval support:

#[derive(Debug, Deserialize)]
pub struct MyCbitConfig {
    enabled: bool,
    interval_secs: u64,
    threshold: i32,
}

impl TestRun for MyCbitTest {
    fn run(&self) -> Result<(), Box<dyn std::error::Error>> {
        log::info!("CBIT monitoring started (interval: {}s)", 
                  self.config.interval_secs);

        loop {
            // Perform periodic check
            match self.check_status() {
                Ok(status) => {
                    log::debug!("Status check: {:?}", status);
                    if status > self.config.threshold {
                        log::warn!("Threshold exceeded: {}", status);
                    }
                }
                Err(e) => {
                    log::error!("Status check failed: {}", e);
                }
            }

            // Sleep until next check
            std::thread::sleep(
                std::time::Duration::from_secs(self.config.interval_secs)
            );
        }
    }
}

Adding Learn Function

Create interactive configuration generator in tools/src/learn.rs:

pub fn my_new_test_learn() -> Result<(), Box<dyn std::error::Error>> {
    use crate::common::*;

    println!("\n=== My New Test Configuration ===");

    // 1. Basic settings
    let enabled = prompt_bool("Enable my_new_test?", true)?;
    let threshold = prompt_number("Threshold value:", 42)?;

    // 2. Detect hardware
    println!("Detecting devices...");
    let devices = detect_my_devices()?;

    if devices.is_empty() {
        println!("⚠ No devices detected");
        println!("Test will be enabled but may not function");
    } else {
        println!("✓ Found {} device(s)", devices.len());
    }

    // 3. Generate config
    let mut config = format!(
        "[my_new_test]\nenabled = {}\nthreshold = {}\n",
        enabled, threshold
    );

    // 4. Add device entries
    for device in devices {
        config.push_str(&format!(
            "\n[[device]]\nname = \"{}\"\nenabled = true\n",
            device
        ));
    }

    // 5. Write to file
    let config_path = get_config_path("my_new_test.toml")?;
    std::fs::write(&config_path, config)?;

    println!("\n✓ Configuration saved to {}", config_path.display());
    Ok(())
}

// Helper: Detect hardware
fn detect_my_devices() -> Result<Vec<String>, Box<dyn std::error::Error>> {
    // Example: scan /sys/class/my_device/
    let sys_path = "/sys/class/my_device";

    if !Path::new(sys_path).exists() {
        return Ok(Vec::new());
    }

    let mut devices = Vec::new();
    for entry in std::fs::read_dir(sys_path)? {
        let entry = entry?;
        devices.push(entry.file_name().to_string_lossy().to_string());
    }

    Ok(devices)
}

Register learn function:

// In tools/src/learn.rs run_learn()
pub fn run_learn(test_name: &str) -> Result<(), Box<dyn std::error::Error>> {
    match test_name {
        "my_new_test" => my_new_test_learn(),
        // ... other tests ...
        _ => Err(format!("Unknown test: {}", test_name).into()),
    }
}

Workspace Integration

Add to Workspace Cargo.toml

Edit /home/zen/local/bit/Cargo.toml:

[workspace]
members = [
    # ... existing members ...
    "my_new_test",
]

Build with Workspace

# Build all tests including new one
cargo build

# Build only new test
cargo build -p my_new_test

# Release build
cargo build --release -p my_new_test

Testing Checklist

  • [ ] Configuration structure defined
  • [ ] TOML deserialization working
  • [ ] Test traits implemented
  • [ ] Plugin entry point macro used
  • [ ] Test compiles without warnings
  • [ ] Configuration file created
  • [ ] Test runs successfully
  • [ ] Test produces correct pass/fail results
  • [ ] Logging statements added
  • [ ] Learn function created (optional)
  • [ ] Learn function registered
  • [ ] Documentation added

Common Issues

Plugin Not Loading

Symptom: bit-manager doesn't find your test

Solutions: 1. Check filename: must be lib<name>.so 2. Verify BIT_TEST_PATH points to correct directory 3. Check crate-type = ["cdylib"] in Cargo.toml 4. Ensure create_plugin! macro is present

Configuration Not Found

Symptom: Error loading config file

Solutions: 1. Verify file named <test_name>.toml 2. Check BIT_CONFIG_PATH environment variable 3. Ensure config file in correct directory 4. Test TOML syntax with toml::from_str

Test Always Passes/Fails

Symptom: Unexpected test results

Solutions: 1. Check enabled() returns correct value 2. Verify error return in run() method 3. Add debug logging to trace execution 4. Test with RUST_LOG=debug

Deserialization Errors

Symptom: TOML parsing fails

Solutions: 1. Verify struct field names match TOML keys 2. Check for typos in configuration 3. Ensure all required fields present 4. Use #[serde(default)] for optional fields

Example: Complete Custom Test

Here's a complete example testing GPIO pins:

use bit_plugin::{create_plugin, Test, TestDetails, TestRun, TestType};
use serde::Deserialize;
use std::path::Path;

#[derive(Debug, Deserialize)]
pub struct GpioPin {
    pin: u32,
    expected_state: String,
}

#[derive(Debug, Deserialize)]
pub struct MyGpioTestConfig {
    enabled: bool,
    gpio: Vec<GpioPin>,
}

pub struct MyGpioTest {
    config: MyGpioTestConfig,
}

impl MyGpioTest {
    pub fn new(config_path: &Path) -> Result<Self, Box<dyn std::error::Error>> {
        let content = std::fs::read_to_string(config_path)?;
        let config = toml::from_str(&content)?;
        Ok(Self { config })
    }

    fn test_gpio(&self, gpio: &GpioPin) -> Result<(), Box<dyn std::error::Error>> {
        let path = format!("/sys/class/gpio/gpio{}/value", gpio.pin);
        let value = std::fs::read_to_string(&path)?
            .trim()
            .to_string();

        let expected = match gpio.expected_state.as_str() {
            "high" => "1",
            "low" => "0",
            _ => return Err("Invalid state".into()),
        };

        if value != expected {
            return Err(format!(
                "GPIO {} state mismatch: got {}, expected {}",
                gpio.pin, value, expected
            ).into());
        }

        Ok(())
    }
}

impl Test for MyGpioTest {
    fn name(&self) -> &str { "my_gpio_test" }
    fn enabled(&self) -> bool { self.config.enabled }
    fn description(&self) -> &str { "Custom GPIO pin test" }
}

impl TestRun for MyGpioTest {
    fn run(&self) -> Result<(), Box<dyn std::error::Error>> {
        log::info!("Testing {} GPIO pins", self.config.gpio.len());

        for gpio in &self.config.gpio {
            log::debug!("Testing GPIO {}", gpio.pin);
            self.test_gpio(gpio)?;
        }

        log::info!("All GPIO tests passed");
        Ok(())
    }
}

impl TestDetails for MyGpioTest {
    fn test_type(&self) -> TestType { TestType::Pbit }
}

create_plugin!(MyGpioTest, MyGpioTest::new);

Configuration:

[my_gpio_test]
enabled = true

[[gpio]]
pin = 17
expected_state = "high"

[[gpio]]
pin = 27
expected_state = "low"

Next Steps