Documents
testing
testing
Type
External
Status
Published
Created
Mar 4, 2026
Updated
Mar 17, 2026
Updated by
Dosu Bot

Testing Documentation#

This document provides comprehensive testing strategies and guidelines for DaemonEye, covering unit testing, integration testing, performance testing, and security testing.


Table of Contents#

[TOC]


Testing Philosophy#

DaemonEye follows a comprehensive testing strategy that ensures:

  • Reliability: Robust error handling and edge case coverage
  • Performance: Meets performance requirements under load
  • Security: Validates security controls and prevents vulnerabilities
  • Maintainability: Easy to understand and modify tests
  • Coverage: High test coverage across all components

Testing Strategy#

Three-Tier Testing Architecture#

  1. Unit Tests: Test individual components in isolation
  2. Integration Tests: Test component interactions and data flow
  3. End-to-End Tests: Test complete workflows and user scenarios

Testing Pyramid#

        ┌─────────────────┐
        │ E2E Tests │ ← Few, slow, expensive
        │ (Manual) │
        ├─────────────────┤
        │ Integration │ ← Some, medium speed
        │ Tests │
        ├─────────────────┤
        │ Unit Tests │ ← Many, fast, cheap
        │ (Automated) │
        └─────────────────┘

Unit Testing#

Core Testing Framework#

DaemonEye uses a comprehensive unit testing framework:

#[cfg(test)]
mod tests {
    use super::*;
    use insta::assert_snapshot;
    use tempfile::TempDir;

    #[tokio::test]
    async fn test_process_collection() {
        let collector = ProcessCollector::new();
        let processes = collector.collect_processes().await.unwrap();

        assert!(!processes.is_empty());
        assert!(processes.iter().any(|p| p.pid > 0));
    }

    #[tokio::test]
    async fn test_database_operations() {
        let temp_dir = TempDir::new().unwrap();
        let db_path = temp_dir.path().join("test.db");

        let db = Database::new(&db_path).await.unwrap();
        let process = ProcessInfo {
            pid: 1234,
            name: "test_process".to_string(),
            // ... other fields
        };

        db.insert_process(&process).await.unwrap();
        let retrieved = db.get_process(1234).await.unwrap();

        assert_eq!(process.pid, retrieved.pid);
        assert_eq!(process.name, retrieved.name);
    }
}

Mocking and Test Doubles#

Use mocks for external dependencies:

use mockall::mock;

mock! {
    pub ProcessCollector {}

    #[async_trait]
    impl ProcessCollectionService for ProcessCollector {
        async fn collect_processes(&self) -> Result<CollectionResult, CollectionError>;
        async fn get_system_info(&self) -> Result<SystemInfo, CollectionError>;
    }
}

#[tokio::test]
async fn test_agent_with_mock_collector() {
    let mut mock_collector = MockProcessCollector::new();
    mock_collector
        .expect_collect_processes()
        .times(1)
        .returning(|| Ok(CollectionResult::default()));

    let agent = daemoneye_agent::new(Box::new(mock_collector));
    let result = agent.run_collection_cycle().await;

    assert!(result.is_ok());
}

Property-Based Testing#

Use property-based testing for complex logic:

use proptest::prelude::*;

proptest! {
    #[test]
    fn test_process_info_serialization(process in any::<ProcessInfo>()) {
        let serialized = serde_json::to_string(&process).unwrap();
        let deserialized: ProcessInfo = serde_json::from_str(&serialized).unwrap();
        assert_eq!(process, deserialized);
    }

    #[test]
    fn test_sql_query_validation(query in "[a-zA-Z0-9_\\s]+") {
        let result = validate_sql_query(&query);
        // Property: validation should not panic
        let _ = result;
    }
}

Integration Testing#

Database Integration Tests#

Test database operations with real SQLite:

#[tokio::test]
async fn test_database_integration() {
    let temp_dir = TempDir::new().unwrap();
    let db_path = temp_dir.path().join("integration_test.db");

    let db = Database::new(&db_path).await.unwrap();

    // Test schema creation
    db.create_schema().await.unwrap();

    // Test data insertion
    let process = ProcessInfo {
        pid: 1234,
        name: "test_process".to_string(),
        executable_path: Some("/usr/bin/test".to_string()),
        command_line: Some("test --arg value".to_string()),
        start_time: Some(Utc::now()),
        cpu_usage: Some(0.5),
        memory_usage: Some(1024),
        status: ProcessStatus::Running,
        executable_hash: Some("abc123".to_string()),
        collection_time: Utc::now(),
    };

    db.insert_process(&process).await.unwrap();

    // Test data retrieval
    let retrieved = db.get_process(1234).await.unwrap();
    assert_eq!(process.pid, retrieved.pid);

    // Test query execution
    let results = db
        .query_processes("SELECT * FROM processes WHERE pid = ?", &[1234])
        .await
        .unwrap();
    assert_eq!(results.len(), 1);
}

IPC Integration Tests#

Test inter-process communication:

#[tokio::test]
async fn test_ipc_communication() {
    let temp_dir = TempDir::new().unwrap();
    let socket_path = temp_dir.path().join("test.sock");

    // Start server
    let server = IpcServer::new(&socket_path).await.unwrap();
    let server_handle = tokio::spawn(async move { server.run().await });

    // Wait for server to start
    tokio::time::sleep(Duration::from_millis(100)).await;

    // Connect client
    let client = IpcClient::new(&socket_path).await.unwrap();

    // Test request/response
    let request = IpcRequest::CollectProcesses;
    let response = client.send_request(request).await.unwrap();

    assert!(matches!(response, IpcResponse::Processes(_)));

    // Cleanup
    server_handle.abort();
}

Alert Delivery Integration Tests#

Test alert delivery mechanisms:

#[tokio::test]
async fn test_alert_delivery() {
    let mut alert_manager = AlertManager::new();

    // Add test sinks
    let syslog_sink = SyslogSink::new("daemon").unwrap();
    let webhook_sink = WebhookSink::new("http://localhost:8080/webhook").unwrap();

    alert_manager.add_sink(Box::new(syslog_sink));
    alert_manager.add_sink(Box::new(webhook_sink));

    // Create test alert
    let alert = Alert {
        id: Uuid::new_v4(),
        rule_name: "test_rule".to_string(),
        severity: AlertSeverity::High,
        message: "Test alert".to_string(),
        process: ProcessInfo::default(),
        timestamp: Utc::now(),
        metadata: HashMap::new(),
    };

    // Send alert
    let result = alert_manager.send_alert(alert).await;
    assert!(result.is_ok());
}

End-to-End Testing#

CLI Testing#

Test command-line interface:

use insta::assert_snapshot;
use std::process::Command;

#[test]
fn test_cli_help() {
    let mut cmd = Command::cargo_bin("daemoneye-cli").unwrap();
    cmd.assert()
        .success()
        .stdout(predicate::str::contains("DaemonEye CLI"));
}

#[test]
fn test_cli_query() {
    let mut cmd = Command::cargo_bin("daemoneye-cli").unwrap();
    cmd.args(&["query", "SELECT * FROM processes LIMIT 1"])
        .assert()
        .success();
}

#[test]
fn test_cli_config() {
    let mut cmd = Command::cargo_bin("daemoneye-cli").unwrap();
    cmd.args(&["config", "show"])
        .assert()
        .success()
        .stdout(predicate::str::contains("app:"));
}

Full System Testing#

Test complete system workflows:

#[tokio::test]
async fn test_full_system_workflow() {
    let temp_dir = TempDir::new().unwrap();
    let config_path = temp_dir.path().join("config.yaml");

    // Create test configuration
    let config = Config::default();
    config.save_to_file(&config_path).unwrap();

    // Start procmond
    let procmond_handle = tokio::spawn(async move {
        let procmond = ProcMonD::new(&config_path).await.unwrap();
        procmond.run().await
    });

    // Start daemoneye-agent
    let agent_handle = tokio::spawn(async move {
        let agent = daemoneye_agent::new(&config_path).await.unwrap();
        agent.run().await
    });

    // Wait for services to start
    tokio::time::sleep(Duration::from_secs(2)).await;

    // Test CLI operations
    let mut cmd = Command::cargo_bin("daemoneye-cli").unwrap();
    cmd.args(&[
        "--config",
        config_path.to_str().unwrap(),
        "query",
        "SELECT COUNT(*) FROM processes",
    ])
    .assert()
    .success();

    // Cleanup
    procmond_handle.abort();
    agent_handle.abort();
}

Performance Testing#

Automated Benchmarks#

DaemonEye provides a dedicated benchmarking workflow for performance testing:

  • Manual Trigger: Performance benchmarks are triggered manually via workflow_dispatch
  • Configurable Suites: Select which benchmark suite to run ("all", "performance_benchmarks", or "process_collector_benchmarks")
  • Regression Detection: Tests detect performance regressions and log warnings for review
  • Baseline Comparison: Benchmark results are cached and compared against baseline from the main branch
  • Load Testing: Automated load tests validate system behavior under stress in a separate job
  • Results Archival: Benchmark and load test results are uploaded as artifacts with 30-day retention

Developers can access benchmark results from the GitHub Actions workflow artifacts. Performance regressions are logged as warnings but do not fail the build, allowing for manual review and assessment.

Load Testing#

Test system performance under load:

use criterion::{Criterion, black_box, criterion_group, criterion_main};

fn benchmark_process_collection(c: &mut Criterion) {
    let mut group = c.benchmark_group("process_collection");

    group.bench_function("collect_processes", |b| {
        b.iter(|| {
            let collector = ProcessCollector::new();
            black_box(collector.collect_processes())
        })
    });

    group.bench_function("collect_processes_parallel", |b| {
        b.iter(|| {
            let collector = ProcessCollector::new();
            black_box(collector.collect_processes_parallel())
        })
    });

    group.finish();
}

fn benchmark_database_operations(c: &mut Criterion) {
    let mut group = c.benchmark_group("database_operations");

    group.bench_function("insert_process", |b| {
        let db = Database::new(":memory:").unwrap();
        let process = ProcessInfo::default();

        b.iter(|| black_box(db.insert_process(&process)))
    });

    group.bench_function("query_processes", |b| {
        let db = Database::new(":memory:").unwrap();
        // Insert test data
        for i in 0..1000 {
            let process = ProcessInfo {
                pid: i,
                ..Default::default()
            };
            db.insert_process(&process).unwrap();
        }

        b.iter(|| black_box(db.query_processes("SELECT * FROM processes WHERE pid > ?", &[500])))
    });

    group.finish();
}

criterion_group!(
    benches,
    benchmark_process_collection,
    benchmark_database_operations
);
criterion_main!(benches);

Memory Testing#

Test memory usage and leaks:

#[tokio::test]
async fn test_memory_usage() {
    let initial_memory = get_memory_usage();

    // Run operations that should not leak memory
    for _ in 0..1000 {
        let collector = ProcessCollector::new();
        let _processes = collector.collect_processes().await.unwrap();
        drop(collector);
    }

    // Force garbage collection
    tokio::task::yield_now().await;

    let final_memory = get_memory_usage();
    let memory_increase = final_memory - initial_memory;

    // Memory increase should be minimal
    assert!(memory_increase < 10 * 1024 * 1024); // 10MB
}

fn get_memory_usage() -> usize {
    // Platform-specific memory usage detection
    #[cfg(target_os = "linux")]
    {
        let status = std::fs::read_to_string("/proc/self/status").unwrap();
        for line in status.lines() {
            if line.starts_with("VmRSS:") {
                let parts: Vec<&str> = line.split_whitespace().collect();
                return parts[1].parse::<usize>().unwrap() * 1024; // Convert to bytes
            }
        }
        0
    }
    #[cfg(not(target_os = "linux"))]
    {
        // Fallback for other platforms
        0
    }
}

Stress Testing#

Test system behavior under stress:

#[tokio::test]
async fn test_stress_collection() {
    let collector = ProcessCollector::new();

    // Run collection continuously for 60 seconds
    let start = Instant::now();
    let mut count = 0;

    while start.elapsed() < Duration::from_secs(60) {
        let processes = collector.collect_processes().await.unwrap();
        count += processes.len();

        // Small delay to prevent overwhelming the system
        tokio::time::sleep(Duration::from_millis(100)).await;
    }

    // Should have collected a reasonable number of processes
    assert!(count > 0);
    println!("Collected {} processes in 60 seconds", count);
}

Security Testing#

Fuzz Testing#

Test with random inputs:

use cargo_fuzz;

#[no_mangle]
pub extern "C" fn fuzz_process_info(data: &[u8]) {
    if let Ok(process_info) = ProcessInfo::from_bytes(data) {
        // Test that deserialization doesn't panic
        let _ = process_info.pid;
        let _ = process_info.name;
    }
}

#[no_mangle]
pub extern "C" fn fuzz_sql_query(data: &[u8]) {
    if let Ok(query) = std::str::from_utf8(data) {
        // Test SQL query validation
        let _ = validate_sql_query(query);
    }
}

Security Boundary Testing#

Test security boundaries:

#[tokio::test]
async fn test_privilege_dropping() {
    let collector = ProcessCollector::new();

    // Should start with elevated privileges
    assert!(collector.has_privileges());

    // Drop privileges
    collector.drop_privileges().await.unwrap();

    // Should no longer have privileges
    assert!(!collector.has_privileges());

    // Should still be able to collect processes (with reduced capabilities)
    let processes = collector.collect_processes().await.unwrap();
    assert!(!processes.is_empty());
}

#[tokio::test]
async fn test_sql_injection_prevention() {
    let db = Database::new(":memory:").unwrap();

    // Test various SQL injection attempts
    let malicious_queries = vec![
        "'; DROP TABLE processes; --",
        "1' OR '1'='1",
        "'; INSERT INTO processes VALUES (9999, 'hacker', '/bin/evil'); --",
    ];

    for query in malicious_queries {
        let result = db.execute_query(query).await;
        // Should either reject the query or sanitize it safely
        match result {
            Ok(_) => {
                // If query succeeds, verify no damage was done
                let count = db.count_processes().await.unwrap();
                assert_eq!(count, 0); // No processes should exist
            }
            Err(_) => {
                // Query was rejected, which is also acceptable
            }
        }
    }
}

Input Validation Testing#

Test input validation:

#[test]
fn test_input_validation() {
    // Test valid inputs
    let valid_process = ProcessInfo {
        pid: 1234,
        name: "valid_process".to_string(),
        executable_path: Some("/usr/bin/valid".to_string()),
        command_line: Some("valid --arg value".to_string()),
        start_time: Some(Utc::now()),
        cpu_usage: Some(0.5),
        memory_usage: Some(1024),
        status: ProcessStatus::Running,
        executable_hash: Some("abc123".to_string()),
        collection_time: Utc::now(),
    };

    assert!(valid_process.validate().is_ok());

    // Test invalid inputs
    let invalid_process = ProcessInfo {
        pid: 0, // Invalid PID
        name: "".to_string(), // Empty name
        executable_path: Some("".to_string()), // Empty path
        command_line: Some("a".repeat(10000).to_string()), // Too long
        start_time: Some(Utc::now()),
        cpu_usage: Some(-1.0), // Negative CPU usage
        memory_usage: Some(0),
        status: ProcessStatus::Running,
        executable_hash: Some("invalid_hash".to_string()),
        collection_time: Utc::now(),
    };

    assert!(invalid_process.validate().is_err());
}

Test Configuration#

Test Environment Setup#

# test-config.yaml
app:
  log_level: debug
  scan_interval_ms: 1000
  batch_size: 10

database:
  path: ':memory:'
  max_connections: 5
  retention_days: 1

alerting:
  enabled: false

testing:
  enable_mocks: true
  mock_external_services: true
  test_data_dir: /tmp/daemoneye-test
  cleanup_after_tests: true

Test Data Management#

pub struct TestDataManager {
    temp_dir: TempDir,
    test_data: HashMap<String, Vec<u8>>,
}

impl TestDataManager {
    pub fn new() -> Self {
        Self {
            temp_dir: TempDir::new().unwrap(),
            test_data: HashMap::new(),
        }
    }

    pub fn add_test_data(&mut self, name: &str, data: &[u8]) {
        self.test_data.insert(name.to_string(), data.to_vec());
    }

    pub fn get_test_data(&self, name: &str) -> Option<&[u8]> {
        self.test_data.get(name).map(|v| v.as_slice())
    }

    pub fn create_test_database(&self) -> PathBuf {
        let db_path = self.temp_dir.path().join("test.db");
        let db = Database::new(&db_path).unwrap();
        db.create_schema().unwrap();
        db_path
    }
}

Continuous Integration#

GitHub Actions Workflows#

DaemonEye uses two separate GitHub Actions workflows for testing:

Main CI Workflow (.github/workflows/ci.yml)#

The main CI pipeline runs on every push and pull request:

name: Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: jdx/mise-action@v3
        with:
          install: true
          cache: true
          github_token: ${{ secrets.GITHUB_TOKEN }}

      - name: Ensure rustfmt and clippy are installed
        run: rustup component add rustfmt clippy

      - name: Check formatting
        run: just lint-rust

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: jdx/mise-action@v3
        with:
          install: true
          cache: true
          github_token: ${{ secrets.GITHUB_TOKEN }}

      - name: Run tests (all features)
        run: mise x -- cargo nextest run --profile ci --all-features

      - name: Build release
        run: mise x -- cargo build --release --all-features

  test-cross-platform:
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
    runs-on: ${{ matrix.os }}
    needs: quality
    steps:
      - uses: actions/checkout@v6
      - uses: jdx/mise-action@v3
        with:
          install: true
          cache: true
          github_token: ${{ secrets.GITHUB_TOKEN }}

      - run: mise x -- cargo nextest run --profile ci --all-features
      - run: mise x -- cargo build --release --all-features

  coverage:
    runs-on: ubuntu-latest
    needs: [test, test-cross-platform, quality]
    steps:
      - uses: actions/checkout@v6
      - uses: jdx/mise-action@v3
        with:
          install: true
          cache: true
          github_token: ${{ secrets.GITHUB_TOKEN }}

      - name: Generate coverage
        run: mise x -- cargo llvm-cov --all-features --no-report

      - name: Combine coverage reports
        run: mise x -- cargo llvm-cov report --lcov --output-path lcov.info

      - name: Upload to Codecov
        uses: codecov/codecov-action@v5
        with:
          files: lcov.info
          fail_ci_if_error: false
          token: ${{ secrets.CODECOV_TOKEN }}

Benchmarks Workflow (.github/workflows/benchmarks.yml)#

The benchmarks workflow is triggered manually and runs independently:

name: Benchmarks

on:
  workflow_dispatch:
    inputs:
      suite:
        description: Benchmark suite to run
        required: false
        default: all
        type: choice
        options:
          - all
          - performance_benchmarks
          - process_collector_benchmarks

jobs:
  benchmarks:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - uses: jdx/mise-action@v3
        with:
          install: true
          cache: true
          github_token: ${{ secrets.GITHUB_TOKEN }}

      - name: Restore baseline benchmarks
        uses: actions/cache/restore@v4
        with:
          path: target/criterion
          key: criterion-baseline-${{ runner.os }}

      - name: Run benchmarks
        env:
          BENCH_SUITE: ${{ inputs.suite }}
        run: |
          if [ "$BENCH_SUITE" = "all" ]; then
            mise x -- cargo bench --package procmond 2>&1 | tee bench-output.txt
          else
            mise x -- cargo bench --package procmond --bench "$BENCH_SUITE" 2>&1 | tee bench-output.txt
          fi

      - name: Check for performance regression
        run: |
          if grep -q "Performance has regressed" bench-output.txt; then
            echo "::warning::Performance regression detected in benchmarks"
            grep -A2 "Performance has regressed" bench-output.txt
          else
            echo "No performance regressions detected"
          fi

      - name: Save baseline benchmarks
        uses: actions/cache/save@v4
        if: github.ref == 'refs/heads/main'
        with:
          path: target/criterion
          key: criterion-baseline-${{ runner.os }}

      - name: Upload benchmark results
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: benchmark-results
          path: bench-output.txt
          retention-days: 30

  load-tests:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v6

      - uses: jdx/mise-action@v3
        with:
          install: true
          cache: true
          github_token: ${{ secrets.GITHUB_TOKEN }}

      - name: Run load tests
        run: NO_COLOR=1 TERM=dumb mise x -- cargo test --package procmond --test
          load_tests -- --ignored --nocapture 2>&1 | tee load-test-output.txt

      - name: Upload load test results
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: load-test-results
          path: load-test-output.txt
          retention-days: 30

CI Jobs Overview#

The main CI pipeline includes the following jobs:

  1. quality: Runs code formatting and linting checks
  2. test: Executes the full test suite with all features enabled
  3. test-cross-platform: Tests on Ubuntu, macOS, and Windows
  4. coverage: Generates and uploads code coverage reports

The benchmarks workflow includes two independent jobs:

  1. benchmarks: Runs performance benchmarks with configurable suite selection (15-minute timeout)
  2. load-tests: Runs load tests under stress conditions (10-minute timeout)

Accessing Benchmark Results#

Benchmark results are available through the dedicated benchmarks workflow:

  • Manual Trigger: Navigate to the Actions tab and select the "Benchmarks" workflow, then choose "Run workflow" to trigger manually
  • Suite Selection: Choose which benchmark suite to run: "all" (default), "performance_benchmarks", or "process_collector_benchmarks"
  • Workflow Artifacts: Download benchmark-results and load-test-results artifacts from the workflow summary page
  • CI Logs: View benchmark output directly in the workflow logs
  • Performance Alerts: Regressions are logged as warnings for manual review without failing the workflow

The benchmarks workflow stores baseline results from the main branch and compares all subsequent runs against this baseline to detect performance regressions.

Test Reporting#

use insta::assert_snapshot;

#[test]
fn test_config_serialization() {
    let config = Config::default();
    let serialized = serde_yaml::to_string(&config).unwrap();

    // Snapshot testing for configuration
    assert_snapshot!(serialized);
}

#[test]
fn test_alert_format() {
    let alert = Alert {
        id: Uuid::parse_str("123e4567-e89b-12d3-a456-426614174000").unwrap(),
        rule_name: "test_rule".to_string(),
        severity: AlertSeverity::High,
        message: "Test alert message".to_string(),
        process: ProcessInfo::default(),
        timestamp: Utc::now(),
        metadata: HashMap::new(),
    };

    let formatted = alert.format_json().unwrap();
    assert_snapshot!(formatted);
}

Test Maintenance#

Test Organization#

// tests/
// ├── unit/
// │ ├── collector_tests.rs
// │ ├── database_tests.rs
// │ └── alert_tests.rs
// ├── integration/
// │ ├── ipc_tests.rs
// │ ├── database_tests.rs
// │ └── alert_delivery_tests.rs
// ├── e2e/
// │ ├── cli_tests.rs
// │ └── system_tests.rs
// └── common/
// ├── test_helpers.rs
// └── test_data.rs

Test Utilities#

// tests/common/test_helpers.rs
pub struct TestHelper {
    temp_dir: TempDir,
    config: Config,
}

impl TestHelper {
    pub fn new() -> Self {
        let temp_dir = TempDir::new().unwrap();
        let config = Config::default();

        Self { temp_dir, config }
    }

    pub fn create_test_database(&self) -> Database {
        let db_path = self.temp_dir.path().join("test.db");
        Database::new(&db_path).unwrap()
    }

    pub fn create_test_config(&self) -> PathBuf {
        let config_path = self.temp_dir.path().join("config.yaml");
        self.config.save_to_file(&config_path).unwrap();
        config_path
    }

    pub fn cleanup(&self) {
        // Cleanup test resources
    }
}

impl Drop for TestHelper {
    fn drop(&mut self) {
        self.cleanup();
    }
}

This testing documentation provides comprehensive guidance for testing DaemonEye. For additional testing information, consult the specific test files or contact the development team.