跳到主内容

测试规范

贡献指南

测试规范

EnerOS 拥有 7300+ 测试用例,覆盖所有核心功能。所有 PR 必须附带相应测试且通过 CI,否则不予合并。本页详述测试分层、编写规范、覆盖率要求、CI 流程与性能基准测试。

测试分层

EnerOS 采用”测试金字塔”分层策略,从底层单元测试到顶层端到端测试,逐层收敛:

        ┌──────────┐
        │  E2E 测试  │  ← 真实部署,覆盖关键用户场景
        └────┬─────┘
       ┌─────┴─────┐
       │  集成测试  │  ← 跨 crate 协作
       └─────┬─────┘
      ┌──────┴──────┐
      │  属性测试    │  ← 不变量校验
      └──────┬──────┘
     ┌───────┴───────┐
     │   单元测试     │  ← 单个函数/模块
     └───────────────┘
层级范围工具运行命令占比
单元测试单个函数/模块#[test]cargo test -p <crate>70%
集成测试跨 crate 协作tests/ 目录cargo test --test <name>20%
属性测试不变量校验proptestcargo test --features proptest5%
性能基准关键路径耗时criterioncargo bench3%
端到端真实部署Docker Compose./scripts/e2e.sh2%

单元测试

单元测试位于 src/ 内的 #[cfg(test)] mod tests,命名以 test_ 开头:

// crates/eneros-powerflow/src/solver.rs
pub fn solve_newton_raphson(
    topology: &Topology,
    max_iterations: usize,
    tolerance: f64,
) -> Result<PowerflowResult> {
    // 实现
    Ok(PowerflowResult { converged: true, /* ... */ })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_solve_newton_raphson_converges_for_simple_case() {
        let topo = Topology::single_bus();
        let result = solve_newton_raphson(&topo, 20, 1e-8).unwrap();
        assert!(result.converged);
    }

    #[test]
    fn test_solve_newton_raphson_returns_error_on_non_convergence() {
        let topo = Topology::invalid_case();
        let result = solve_newton_raphson(&topo, 1, 1e-8);
        assert!(result.is_err());
        match result {
            Err(PowerflowError::NonConvergence { iterations, max_iterations }) => {
                assert_eq!(iterations, 1);
                assert_eq!(max_iterations, 1);
            }
            _ => panic!("应当返回 NonConvergence 错误"),
        }
    }
}

集成测试

集成测试位于 tests/ 目录,文件名以 _test.rs 结尾:

// crates/eneros-powerflow/tests/integration_test.rs
use eneros_powerflow::{NewtonRaphsonSolver, PowerflowResult};
use eneros_topology::Topology;

#[test]
fn test_powerflow_with_ieee_14bus_topology() {
    let topo = Topology::from_ieee_14bus();
    let solver = NewtonRaphsonSolver::new(50, 1e-8);
    let result: PowerflowResult = solver.solve(&topo).unwrap();

    assert!(result.converged, "IEEE 14-bus 应当收敛");
    assert_eq!(result.buses.len(), 14);

    // 与 IEEE 公开参考值对比,容差 1e-6
    let bus_1_voltage = result.buses[0].voltage_magnitude;
    assert!((bus_1_voltage - 1.06).abs() < 1e-6, "Bus 1 电压应为 1.06 p.u.,实际 {}", bus_1_voltage);
}

#[test]
fn test_powerflow_handles_isolated_bus() {
    let topo = Topology::with_isolated_bus();
    let solver = NewtonRaphsonSolver::new(50, 1e-8);
    let result = solver.solve(&topo);
    assert!(result.is_ok());
}

属性测试

属性测试通过 proptest 自动生成大量随机输入,验证不变量:

// crates/eneros-topology/src/graph.rs
#[cfg(test)]
mod proptests {
    use proptest::prelude::*;
    use super::*;

    proptest! {
        #[test]
        fn test_adding_bus_does_not_break_connectivity(
            n in 1usize..100,
            edges in proptest::collection::vec((1usize..100, 1usize..100), 0..200)
        ) {
            let mut graph = Graph::new();
            for i in 0..n {
                graph.add_bus(i as u64);
            }
            for (a, b) in edges {
                if a < n && b < n && a != b {
                    graph.add_branch(a as u64, b as u64);
                }
            }
            // 不变量:节点数等于添加的数量
            prop_assert_eq!(graph.bus_count(), n);
            // 不变量:每条边都是双向的
            for (a, b) in &edges {
                if *a < n && *b < n && *a != *b {
                    prop_assert!(graph.has_branch(*a as u64, *b as u64));
                    prop_assert!(graph.has_branch(*b as u64, *a as u64));
                }
            }
        }
    }
}

性能基准

性能基准使用 criterion,位于 benches/ 目录:

// benches/benches/powerflow_bench.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use eneros_powerflow::NewtonRaphsonSolver;
use eneros_topology::Topology;

fn bench_ieee_14bus(c: &mut Criterion) {
    let topo = Topology::from_ieee_14bus();
    let solver = NewtonRaphsonSolver::new(50, 1e-8);

    c.bench_function("powerflow/ieee_14bus", |b| {
        b.iter(|| {
            let result = solver.solve(black_box(&topo)).unwrap();
            black_box(result);
        })
    });
}

fn bench_ieee_118bus(c: &mut Criterion) {
    let topo = Topology::from_ieee_118bus();
    let solver = NewtonRaphsonSolver::new(50, 1e-8);

    c.bench_function("powerflow/ieee_118bus", |b| {
        b.iter(|| {
            let result = solver.solve(black_box(&topo)).unwrap();
            black_box(result);
        })
    });
}

criterion_group!(benches, bench_ieee_14bus, bench_ieee_118bus);
criterion_main!(benches);

端到端测试

E2E 测试位于 tests/e2e/,使用 Docker Compose 启动完整环境:

// tests/e2e/tests/e2e_tests.rs
use std::process::Command;

#[test]
fn test_e2e_full_startup_and_dispatch() {
    // 启动完整环境(假设已通过 docker-compose up 启动)
    let output = Command::new("enerosctl")
        .args(&["--server", "localhost:8080", "dispatch", "solve"])
        .output()
        .expect("enerosctl 执行失败");

    assert!(output.status.success(), "enerosctl dispatch solve 应当成功");
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("converged"), "输出应包含 converged");
}

运行测试

基础命令

# 全部测试(默认)
cargo test

# 推荐使用 nextest(更快、更友好的输出)
cargo nextest run

# 单个 crate
cargo test -p eneros-powerflow

# 单个 crate(nextest)
cargo nextest run -p eneros-powerflow

# 显示 println! 输出
cargo test -- --nocapture

# 仅运行特定测试
cargo test powerflow::newton_raphson

# 仅运行特定测试(nextest)
cargo nextest run -E 'test(powerflow::newton_raphson)'

# 性能基准
cargo bench -p eneros-powerflow

# 性能基准(特定用例)
cargo bench -p eneros-powerflow -- ieee_14bus

进阶用法

# 仅运行单元测试
cargo nextest run --lib

# 仅运行集成测试
cargo nextest run --test '*'

# 跳过需要网络的测试
cargo nextest run -E 'not test(network)'

# 并行度控制
cargo nextest run --test-threads=4

# 失败后立即停止
cargo nextest run --fail-fast

# 重复运行(验证 flaky 测试)
cargo nextest run --retries 3

# 仅运行被修改代码影响的测试
cargo nextest run -p eneros-powerflow --no-fail-fast

# 显示执行时间
cargo test -- --report-time

# 输出 JUnit XML(CI 集成)
cargo nextest run --junit-path target/test-results.xml

测试过滤

cargo-nextest 支持强大的过滤表达式:

# 按名称匹配
cargo nextest run -E 'test(ieee_14bus)'

# 按包过滤
cargo nextest run -E 'package(eneros-powerflow)'

# 组合条件
cargo nextest run -E 'package(eneros-powerflow) & test(solve)'

# 排除某些测试
cargo nextest run -E 'not test(network) & not test(slow)'

# 按编译特性过滤
cargo nextest run --features worm -E 'test(audit)'

配置文件

在仓库根目录创建 .config/nextest.toml

[default-profile]
# 默认重试次数
retries = 0
# 测试超时(秒)
slow-timeout = { period = "60s", terminate-after = 1 }
# 失败后立即停止
fail-fast = false

[ci-profile]
# CI 环境下重试 2 次以规避 flaky
retries = 2
# 并行度
test-threads = 8
# JUnit 输出
junit.path = "target/test-results.xml"
# 使用 CI 配置文件
cargo nextest run --profile ci

编写测试规范

命名规范

测试类型命名规范示例
单元测试test_<function>_<scenario>test_solve_converges_on_ieee_14bus
失败用例test_<function>_returns_error_on_<condition>test_solve_returns_error_on_invalid_topology
边界用例test_<function>_handles_<edge_case>test_solve_handles_single_bus
集成测试test_<feature>_<scenario>test_powerflow_with_ieee_14bus
属性测试test_<invariant>test_adding_bus_does_not_break_connectivity
性能基准bench_<module>_<case>bench_powerflow_ieee_14bus

测试结构

遵循 Arrange-Act-Assert(AAA)模式:

#[test]
fn test_solve_converges_for_simple_two_bus_system() {
    // Arrange:准备测试数据
    let mut topo = Topology::new();
    topo.add_bus(1, BusType::Slack, 1.06, 0.0);
    topo.add_bus(2, BusType::PQ, 1.0, 0.0);
    topo.add_branch(1, 2, 0.01, 0.05);
    topo.add_load(2, 0.5, 0.2);
    let solver = NewtonRaphsonSolver::new(20, 1e-8);

    // Act:执行被测函数
    let result = solver.solve(&topo).expect("潮流计算应当成功");

    // Assert:验证结果
    assert!(result.converged, "两节点系统应当收敛");
    assert_eq!(result.buses.len(), 2);
    assert!((result.buses[1].voltage_magnitude - 0.95).abs() < 0.1,
        "Bus 2 电压应当在合理范围内");
}

测试编写要点

  • 新功能必须附带至少一个失败用例与一个成功用例
  • 物理相关计算应与 IEEE 标准参考值对比,容差 1e-6
  • 异步测试使用 #[tokio::test]
  • 避免依赖外部网络与时间,使用注入式时钟与内存通道
  • 测试不应相互依赖,每个测试可独立运行
  • 测试执行时间应控制在 100ms 以内,超过 1s 的测试须标注 #[ignore]
#[tokio::test]
async fn test_ed_respects_unit_limits() {
    // Arrange
    let agent = EconomicDispatchAgent::new(Duration::from_secs(60));
    agent.add_unit(Unit::new(1, 0.0, 50.0, 10.0));  // Pmin=0, Pmax=50
    agent.add_unit(Unit::new(2, 0.0, 50.0, 12.0));
    agent.set_total_demand(80.0);

    // Act
    let result = agent.solve_ed(100.0).await.unwrap();

    // Assert
    assert_eq!(result.len(), 2);
    for sp in &result {
        assert!(sp.power >= 0.0 && sp.power <= 50.0,
            "机组出力 {} 超出 [0, 50] 范围", sp.power);
    }
    let total: f64 = result.iter().map(|sp| sp.power).sum();
    assert!((total - 80.0).abs() < 1e-6, "总出力应等于负荷需求");
}

#[tokio::test]
async fn test_ed_returns_error_when_demand_exceeds_capacity() {
    let agent = EconomicDispatchAgent::new(Duration::from_secs(60));
    agent.add_unit(Unit::new(1, 0.0, 50.0, 10.0));
    agent.set_total_demand(100.0);  // 超过总容量 50

    let result = agent.solve_ed(100.0).await;
    assert!(matches!(result, Err(EconomicDispatchError::InsufficientCapacity)));
}

#[test]
#[ignore = "需要外部数据库,运行时使用 --ignored"]
fn test_timeseries_persistence_with_real_database() {
    // 仅在 CI 或本地手动运行
}

使用测试工具

eneros-test-utils crate 提供常用测试工具:

use eneros_test_utils::{assert_close, TestTopology, MockClock};

#[test]
fn test_powerflow_with_mock_clock() {
    let clock = MockClock::fixed("2024-01-01T00:00:00Z");
    let topo = TestTopology::ieee_14bus();
    let solver = NewtonRaphsonSolver::new(50, 1e-8);

    let result = solver.solve_with_clock(&topo, &clock).unwrap();

    assert_close(result.buses[0].voltage_magnitude, 1.06, 1e-6);
}

测试组织

测试类型位置命名运行命令
单元测试src/*.rs#[cfg(test)] mod teststest_*cargo test -p <crate>
集成测试tests/*.rs*_test.rscargo test --test <name>
端到端测试tests/e2e/e2e_*.rscargo test --package e2e
协议一致性tests/protocol_conformance/cargo test --package protocol_conformance
安全测试tests/security/cargo test --package security
OS 启动测试os/tests/*_test.rscargo test --package os-tests
性能基准benches/benches/*_bench.rscargo bench

覆盖率

覆盖率要求

CI 使用 cargo-llvm-cov 生成覆盖率报告:

代码类型覆盖率要求
新增代码≥ 80%
核心内核(topology / powerflow / constraint)≥ 90%
安全网关(gateway / trust / audit)≥ 95%
Agent 运行时≥ 75%
协议适配≥ 70%
可视化 / 报表≥ 60%

生成覆盖率

# 安装
cargo install cargo-llvm-cov

# 生成摘要
cargo llvm-cov --summary-only

# 生成 HTML 报告
cargo llvm-cov --html

# 生成 LCOV 格式(CI 集成)
cargo llvm-cov --lcov --output-path target/lcov.info

# 仅特定 crate
cargo llvm-cov -p eneros-powerflow --html

# 仅显示未覆盖的行
cargo llvm-cov --show-instantiations --summary-only | grep "MISS"

覆盖率配置

在仓库根目录的 .cargo/config.toml 中添加:

[llvm-cov]
# 接口输出
output-dir = "target/llvm-cov"
# 排除标准库与外部依赖
ignore-filename-regex = "registry/|/.cargo/|target/debug/"

CI 中的覆盖率

CI 流水线 .github/workflows/coverage.yml 会在每次 push 至 main 时上报覆盖率:

name: Coverage
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  coverage:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: llvm-tools-preview
      - run: cargo install cargo-llvm-cov
      - run: cargo llvm-cov --lcov --output-path lcov.info
      - uses: codecov/codecov-action@v4
        with:
          files: ./lcov.info
          fail_ci_if_error: false

性能基准测试

基准测试套件

EnerOS 维护完整的性能基准测试套件,位于 benches/benches/

基准文件测试内容关键指标
powerflow_bench.rs潮流计算IEEE 14/30/118/300 bus 延迟
grid_analysis_bench.rs电网分析拓扑分析、孤岛检测耗时
agent_bench.rsAgent 运行时Agent 调度延迟
ai_bench.rsAI 推理推理延迟、吞吐
api_bench.rsAPI 性能REST/GraphQL QPS
ha_bench.rs高可用故障切换时间
iot_bench.rsIoT接入吞吐
perf_bench.rs通用性能内存分配、并发
scada_bench.rsSCADA数据采集吞吐
twin_bench.rs数字孪生同步延迟
visualization_bench.rs可视化渲染帧率

运行基准测试

# 运行全部基准
cargo bench

# 运行特定 crate 的基准
cargo bench -p eneros-powerflow

# 运行特定用例
cargo bench -p eneros-powerflow -- ieee_14bus

# 保存基线(用于对比)
cargo bench -p eneros-powerflow -- --save-baseline before-change

# 修改代码后对比
cargo bench -p eneros-powerflow -- --baseline before-change

# 生成 HTML 报告(需安装 cargo-criterion)
cargo install cargo-criterion
cargo criterion --message-format=json > target/criterion.json

性能回归检测

CI 流水线 .github/workflows/benchmark.yml 在每次 push 至 main 时执行基准测试,并与基线对比:

name: Benchmark
on:
  push:
    branches: [main]

jobs:
  benchmark:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - run: cargo bench --workspace | tee target/bench-output.txt
      - uses: benchmark-action/github-action-benchmark@v1
        with:
          tool: 'cargo'
          output-file-path: target/bench-output.txt
          github-token: ${{ secrets.GITHUB_TOKEN }}
          auto-push: true
          alert-threshold: '120%'
          comment-on-alert: true
          fail-on-alert: true

性能回归阈值:

指标警告阈值失败阈值
单次操作延迟+10%+20%
吞吐量-5%-10%
内存占用+5%+10%

完整基准示例

// benches/benches/powerflow_bench.rs
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use eneros_powerflow::{NewtonRaphsonSolver, FastDecoupledSolver};
use eneros_topology::Topology;

fn bench_powerflow_solvers(c: &mut Criterion) {
    let cases = [
        ("ieee_14bus", Topology::from_ieee_14bus()),
        ("ieee_30bus", Topology::from_ieee_30bus()),
        ("ieee_118bus", Topology::from_ieee_118bus()),
    ];

    let mut group = c.benchmark_group("powerflow");
    group.sample_size(50);
    group.warm_up_time(std::time::Duration::from_secs(2));
    group.measurement_time(std::time::Duration::from_secs(10));

    for (name, topo) in &cases {
        group.throughput(Throughput::Elements(topo.bus_count() as u64));

        let nr = NewtonRaphsonSolver::new(50, 1e-8);
        group.bench_with_input(BenchmarkId::new("newton_raphson", name), topo, |b, t| {
            b.iter(|| nr.solve(black_box(t)).unwrap())
        });

        let fd = FastDecoupledSolver::new(50, 1e-8);
        group.bench_with_input(BenchmarkId::new("fast_decoupled", name), topo, |b, t| {
            b.iter(|| fd.solve(black_box(t)).unwrap())
        });
    }
    group.finish();
}

criterion_group!(benches, bench_powerflow_solvers);
criterion_main!(benches);

CI 流程

流水线概览

CI 配置位于 .github/workflows/,包含以下流水线:

流水线文件触发条件内容
持续集成ci.ymlpush/PR构建 + 单元测试 + clippy + fmt
端到端e2e.ymlpush to mainE2E 测试
安全扫描security.ymlpush/PRSAST + 依赖审计
性能基准benchmark.ymlpush to main性能回归检测
覆盖率coverage.ymlpush to main代码覆盖率上报
协议一致性conformance.ymlpush/PR电力协议一致性
发布release.ymltag发布到 crates.io + GitHub Release
文档站deploy-web.ymlpush to main部署文档站到 GitHub Pages

CI 主流程详解

.github/workflows/ci.yml

name: CI
on:
  push:
    branches: [main, 'release/*']
  pull_request:
    branches: [main]

env:
  CARGO_TERM_COLOR: always
  RUST_BACKTRACE: 1

jobs:
  fmt:
    name: Rustfmt
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: rustfmt
      - run: cargo fmt --all -- --check

  clippy:
    name: Clippy
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: clippy
      - uses: Swatinem/rust-cache@v2
      - run: cargo clippy --all-targets -- -D warnings

  test:
    name: Test
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        rust: [stable, beta]
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@master
        with:
          toolchain: ${{ matrix.rust }}
      - uses: Swatinem/rust-cache@v2
      - run: cargo install cargo-nextest
      - run: cargo nextest run --workspace
      - run: cargo test --doc --workspace

  deny:
    name: Cargo Deny
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: EmbarkStudios/cargo-deny-action@v1
        with:
          arguments: --workspace

本地模拟 CI

# 模拟完整 CI 流程
cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings
cargo nextest run --workspace
cargo test --doc --workspace
cargo deny check --workspace
cargo doc --no-deps --all-features

提供脚本 scripts/ci-local.sh(Linux/macOS):

#!/bin/bash
set -e

echo "==> Rustfmt 检查..."
cargo fmt --all -- --check

echo "==> Clippy 检查..."
cargo clippy --all-targets -- -D warnings

echo "==> 运行测试..."
cargo nextest run --workspace

echo "==> 运行文档测试..."
cargo test --doc --workspace

echo "==> Cargo Deny 检查..."
cargo deny check --workspace

echo "==> 构建文档..."
cargo doc --no-deps --all-features

echo "✅ 全部检查通过"

测试检查清单

提交 PR 前请逐项检查:

  • 新功能已附至少一个成功用例与一个失败用例
  • 测试命名符合 test_<function>_<scenario> 规范
  • 测试遵循 Arrange-Act-Assert 结构
  • 物理计算与 IEEE 参考值对比,容差 1e-6
  • 异步测试使用 #[tokio::test]
  • 测试不依赖外部网络与时间
  • 单个测试执行时间 < 100ms,超时测试已标注 #[ignore]
  • cargo nextest run --workspace 全部通过
  • cargo test --doc --workspace 全部通过
  • 新增代码覆盖率 ≥ 80%
  • 性能敏感的代码已补充基准测试

相关文档