Documents
Nextest Testing Infrastructure And CI Profiles
Nextest Testing Infrastructure And CI Profiles
Type
Topic
Status
Published
Created
Mar 4, 2026
Updated
Mar 4, 2026
Created by
Dosu Bot
Updated by
Dosu Bot

Nextest Testing Infrastructure And CI Profiles#

Lead Section#

DBSurveyor uses cargo-nextest as its primary test runner, replacing the default cargo test with a more powerful and flexible testing solution. The testing infrastructure is built around a security-first philosophy, featuring comprehensive test organization, multiple execution profiles, and integration with real database instances through testcontainers.

The testing system provides three distinct execution profiles (default, ci, dev) optimized for different environments, sophisticated test categorization into security, integration, and unit test groups, and parallelism controls to manage resource usage. Testing is enforced with a 55% minimum code coverage threshold in CI, with comprehensive security validation including credential protection tests and encryption validation.

Nextest Configuration#

Configuration File Structure#

The nextest configuration is located at .config/nextest.toml and defines all test execution parameters, profiles, and test group classifications.

Test results are stored in the target/nextest directory for later analysis and debugging.

Execution Profiles#

Default Profile (Local Development)#

The default profile is optimized for local development with balanced performance and feedback:

  • Retries: 2
  • Test threads: 4
  • Failure output: immediate
  • Success output: never
  • Status level: pass

This configuration provides quick feedback while automatically retrying flaky tests twice before reporting failure.

CI Profile#

The CI profile is optimized for continuous integration environments:

  • Retries: 3
  • Test threads: 2
  • Failure output: immediate
  • Success output: final
  • Status level: all
  • Slow timeout: 120 seconds with terminate-after 2

The CI profile uses more retries (3 vs 2) but fewer threads (2 vs 4) to accommodate resource-constrained CI environments and provides comprehensive output for debugging.

Dev Profile#

The dev profile is designed for fast feedback during rapid development:

  • Retries: 1
  • Test threads: 8
  • Failure output: immediate
  • Success output: never
  • Status level: pass
  • Slow timeout: 60 seconds with terminate-after 1

This profile maximizes parallelism with 8 threads and minimizes retries for fastest possible feedback.

Test Groups and Parallelism#

Three test groups are defined with different parallelism limits:

  1. Security Tests (max-threads = 1): Sequential execution to avoid interference and ensure isolation
  2. Integration Tests (max-threads = 2): Limited parallelism for database container tests to avoid resource exhaustion
  3. Unit Tests (max-threads = 8): High parallelism for fast unit test execution

Test Categorization Filters#

Test group overrides use filter expressions to categorize tests by name patterns:

The same filter logic is repeated for CI and dev profiles ensuring consistent categorization across all environments.

Test Organization#

Test Directory Structure#

Tests are organized across multiple directories in a flat, pragmatic structure:

Integration Tests (dbsurveyor-core/tests/):

Adapter-Level Tests (dbsurveyor-collect/tests/):

Unit Tests (dbsurveyor-core/src/adapters/*/tests.rs):

Just Recipes for Testing#

The justfile provides comprehensive test commands using nextest as the test runner.

Core Test Commands#

Database-Specific Test Commands#

PostgreSQL:

Other Databases:

Security Test Commands#

Coverage Commands#

All coverage commands use cargo llvm-cov for coverage measurement and enforce a 55% line coverage minimum.

CI Integration#

GitHub Actions Workflow#

Testing is configured in .github/workflows/ci.yml with three main jobs.

Feature Matrix Testing#

Tests run across multiple feature combinations using a matrix strategy to validate each database adapter and feature independently:

  • postgresql
  • sqlite
  • mysql
  • mongodb
  • mssql
  • encryption
  • compression

Each matrix combination runs: cargo nextest run --no-default-features --workspace --features ${{ matrix.features }}

Coverage Validation#

The coverage job generates code coverage reports and enforces the coverage threshold:

cargo llvm-cov -p dbsurveyor-core --lcov --output-path lcov.info --fail-under-lines 55 -- --test-threads=1

Coverage is uploaded to Codecov for tracking and visualization.

Security Validation#

Security validation is configured separately in .github/workflows/security.yml running:

Testcontainers Integration#

Container Setup Pattern#

Integration tests use testcontainers-modules with async runners:

use testcontainers_modules::{postgres::Postgres, testcontainers::runners::AsyncRunner};

#[tokio::test]
async fn test_postgres_connection_pooling_configurations() -> Result<()> {
    // Start PostgreSQL container
    let postgres = Postgres::default().start().await.unwrap();
    let port = postgres.get_host_port_ipv4(5432).await.unwrap();
    let base_url = format!("postgres://postgres:postgres@localhost:{}/postgres", port);

    // Wait for PostgreSQL to be ready
    wait_for_postgres_ready(&base_url, 30).await?;

    // ... test operations ...

    Ok(())
}

Containers are started with Postgres::default().start().await and ports are extracted dynamically using get_host_port_ipv4().

Database Readiness Verification#

Tests implement polling-based wait functions to ensure databases are ready:

async fn wait_for_postgres_ready(database_url: &str, max_attempts: u32) -> Result<()> {
    let mut attempts = 0;
    while attempts < max_attempts {
        if let Ok(pool) = PgPool::connect(database_url).await {
            if sqlx::query("SELECT 1").fetch_one(&pool).await.is_ok() {
                pool.close().await;
                return Ok(());
            }
            pool.close().await;
        }
        attempts += 1;
        if attempts < max_attempts {
            tokio::time::sleep(Duration::from_millis(500)).await;
        }
    }
    Err(DbSurveyorError::connection_failed(/*...*/))
}

The pattern uses:

Similar wait functions exist for MySQL and other database types.

Test Data Setup#

After database readiness, tests create schemas and tables:

let pool = PgPool::connect(&database_url).await.unwrap();

sqlx::query("CREATE SCHEMA IF NOT EXISTS test_schema_1")
    .execute(&pool)
    .await
    .unwrap();

sqlx::query(
    r#"
    CREATE TABLE IF NOT EXISTS public.comprehensive_table (
        id SERIAL PRIMARY KEY,
        uuid_col UUID,
        text_col TEXT NOT NULL,
        json_col JSONB,
        array_col INTEGER[],
        timestamp_col TIMESTAMP WITH TIME ZONE DEFAULT NOW()
    )
    "#,
)
.execute(&pool)
.await
.unwrap();

pool.close().await;

Automatic Cleanup#

Containers are automatically cleaned up when they go out of scope via Rust's Drop trait. Connection pools should be explicitly closed before test completion to avoid resource leaks.

SQLite Alternative Pattern#

SQLite tests use temporary files instead of containers:

use tempfile::NamedTempFile;

#[tokio::test]
async fn test_sqlite_with_schema() {
    let temp_file = NamedTempFile::new().expect("Failed to create temp file");
    let db_path = temp_file.path().to_str().unwrap();
    let connection_string = format!("sqlite://{db_path}");

    // ... test operations ...

    // Temporary file is automatically cleaned up when temp_file is dropped
}

Security Testing#

Credential Non-Leakage Testing#

Security tests verify credentials never appear in error messages:

#[tokio::test]
async fn test_postgres_no_credentials_in_error() {
    let connection_string = format!(
        "postgresql://{}:{}@invalid-host.example.com:5432/testdb",
        SENSITIVE_USERNAME, SENSITIVE_PASSWORD
    );

    let result = PostgresAdapter::new(&connection_string, config).await;
    assert!(result.is_err());

    if let Err(error) = result {
        let error_msg = format!("{:?}", error);
        assert!(!error_msg.contains(SENSITIVE_PASSWORD));
        assert!(!error_msg.contains(SENSITIVE_USERNAME));
    }
}

Tests deliberately connect to invalid hosts and validate that resulting error messages do not contain sensitive credentials.

Safe Description Validation#

Tests validate that adapter descriptions never expose connection details:

#[tokio::test]
async fn test_postgres_safe_description_no_credentials() {
    let adapter = PostgresAdapter::new(&connection_string, config).await?;
    let description = adapter.safe_description();

    assert!(!description.contains("postgres:postgres"));
    assert!(!description.contains(&port.to_string()));
    assert!(!description.contains("localhost"));
    assert!(description.contains("PostgreSQL"));
}

The implementation returns only pool configuration details, never connection strings.

Error Enum Sanitization#

Tests verify that all error variants use sanitized messages:

#[test]
fn test_adapter_error_messages_sanitized() {
    let errors = vec![
        AdapterError::ConnectionFailed,
        AdapterError::QueryFailed,
        // ... more variants
    ];

    for error in errors {
        let error_msg = format!("{}", error);
        assert!(!error_msg.contains("password"));
        assert!(!error_msg.contains("secret"));
    }
}

The error enum uses generic messages that never expose connection details.

Encryption Roundtrip Testing#

Encryption tests validate all cryptographic requirements:

#[test]
fn test_task_1_requirements_compliance() {
    let schema_data = br#"{ /* database schema JSON */ }\"#;
    let password = "secure_encryption_key_2024";

    let encrypted1 = encrypt_data(schema_data, password).unwrap();
    let encrypted2 = encrypt_data(schema_data, password).unwrap();

    // Validate algorithm and parameters
    assert_eq!(encrypted1.algorithm, "AES-GCM-256");
    assert_eq!(encrypted1.nonce.len(), 12); // 96 bits
    assert_eq!(encrypted1.auth_tag.len(), 16); // 128 bits

    // Validate nonce uniqueness
    assert_ne!(encrypted1.nonce, encrypted2.nonce);
    assert_ne!(encrypted1.ciphertext, encrypted2.ciphertext);

    // Validate Argon2id parameters
    assert_eq!(kdf_params.memory_cost, 65536); // 64 MiB
    assert_eq!(kdf_params.time_cost, 3);
    assert_eq!(kdf_params.parallelism, 4);
    assert_eq!(kdf_params.version, "1.3");

    // Validate roundtrip
    let decrypted = decrypt_data(&encrypted1, password).unwrap();
    assert_eq!(schema_data, &decrypted[..]);
}

Nonce Uniqueness Testing#

Tests verify random nonces are never reused:

#[test]
fn test_nonce_uniqueness_across_multiple_encryptions() {
    let mut nonces = std::collections::HashSet::new();
    let iterations = std::env::var("ENCRYPTION_TEST_ITERATIONS")
        .ok()
        .and_then(|v| v.parse::<usize>().ok())
        .unwrap_or(10);

    for _ in 0..iterations {
        let encrypted = encrypt_data(data, password).unwrap();
        assert!(nonces.insert(encrypted.nonce.clone()));

        let decrypted = decrypt_data(&encrypted, password).unwrap();
        assert_eq!(data, &decrypted[..]);
    }
}

The test is configurable via environment variable (default 10 iterations).

Authentication Failure Testing#

Tests comprehensively validate that tampering and incorrect passwords cause failure:

#[test]
fn test_comprehensive_error_handling() {
    let encrypted = encrypt_data(data, password).unwrap();

    // Wrong password should fail
    assert!(decrypt_data(&encrypted, "wrong_password").is_err());

    // Tampered ciphertext should fail
    let mut tampered = encrypted.clone();
    tampered.ciphertext[0] ^= 1;
    assert!(decrypt_data(&tampered, password).is_err());

    // Tampered auth tag should fail
    let mut tampered = encrypted.clone();
    tampered.auth_tag[0] ^= 1;
    assert!(decrypt_data(&tampered, password).is_err());

    // Invalid sizes should fail
    let mut tampered = encrypted.clone();
    tampered.nonce = vec![0u8; 11]; // Should be 12
    assert!(decrypt_data(&tampered, password).is_err());
}

Security Implementation Details#

The adapter implementation disables statement logging to prevent credential exposure:

pub async fn new(connection_string: &str, config: ConnectionConfig) -> AdapterResult<Self> {
    let mut connect_options = PgConnectOptions::from_str(connection_string)
        .map_err(|_| AdapterError::InvalidParameters)?;

    // Disable statement logging to prevent credential leakage
    connect_options = connect_options.disable_statement_logging();

    let pool = PgPoolOptions::new()
        .connect_with(connect_options)
        .await
        .map_err(|_| AdapterError::ConnectionFailed)?;

    Ok(Self { pool, config })
}

The encryption implementation uses zeroize::Zeroizing to automatically clear key material from memory.

Testing Philosophy#

DBSurveyor follows a security-first testing philosophy with several key principles:

  1. Security-First Testing: All tests verify security guarantees, especially credential protection
  2. Real Database Integration: Testcontainers provide authentic database testing
  3. Coverage Enforcement: 55% minimum line coverage with cargo llvm-cov
  4. Zero Warnings Policy: All test code must pass cargo clippy with -D warnings
  5. Deterministic Tests: Tests produce consistent results with no external service dependencies (except testcontainers)
  6. Fast Feedback: Dev profile optimized for rapid development cycles

Development Workflow#

Quick Test Commands#

# Run all tests
just test

# Run specific categories
just test-unit
just test-integration
just test-security

# Run database-specific tests
just test-postgres
just test-mysql
just test-sqlite

# Fast development testing
just test-verbose

Coverage Workflow#

# Generate coverage report
just coverage

# HTML coverage report
just coverage-html

# CI coverage validation
just coverage-ci

Security Validation#

# Full security suite
just security-full

# Individual security tests
just test-encryption
just test-credential-security
just test-offline

Composite Workflows#

Relevant Code Files#

File PathDescriptionLines
.config/nextest.tomlNextest configuration with profiles and test groups89
justfileTest recipes and development commands398
.github/workflows/ci.ymlCI test workflow with feature matrix135
.github/workflows/security.ymlSecurity validation workflow41
dbsurveyor-core/tests/postgres_comprehensive.rsComprehensive PostgreSQL integration tests763
dbsurveyor-core/tests/mysql_schema_collection.rsMySQL schema collection tests465
dbsurveyor-collect/tests/security_tests.rsCredential protection tests~250
dbsurveyor-collect/tests/integration_tests.rsHigh-level integration tests301
dbsurveyor-core/tests/encryption_integration.rsEncryption validation tests~200
dbsurveyor-core/src/adapters/postgres/tests.rsPostgreSQL adapter unit tests422
dbsurveyor-core/src/adapters/mysql/tests.rsMySQL adapter unit tests491
dbsurveyor-core/src/validation/tests.rsJSON Schema validation tests906
  • Cargo Nextest: Next-generation test runner for Rust with improved parallelism and reporting
  • Testcontainers: Library for providing throwaway database instances for integration testing
  • Cargo llvm-cov: Code coverage tool using LLVM instrumentation
  • Just: Command runner for project-specific tasks and workflows
  • GitHub Actions: CI/CD platform for automated testing and validation
  • AES-GCM Encryption: Authenticated encryption mode used for schema protection
  • Argon2id: Memory-hard key derivation function for password-based encryption