测试规范
EnerOS 拥有 7300+ 测试用例,覆盖所有核心功能。所有 PR 必须附带相应测试且通过 CI,否则不予合并。本页详述测试分层、编写规范、覆盖率要求、CI 流程与性能基准测试。
测试分层
EnerOS 采用”测试金字塔”分层策略,从底层单元测试到顶层端到端测试,逐层收敛:
┌──────────┐
│ E2E 测试 │ ← 真实部署,覆盖关键用户场景
└────┬─────┘
┌─────┴─────┐
│ 集成测试 │ ← 跨 crate 协作
└─────┬─────┘
┌──────┴──────┐
│ 属性测试 │ ← 不变量校验
└──────┬──────┘
┌───────┴───────┐
│ 单元测试 │ ← 单个函数/模块
└───────────────┘
| 层级 | 范围 | 工具 | 运行命令 | 占比 |
|---|---|---|---|---|
| 单元测试 | 单个函数/模块 | #[test] | cargo test -p <crate> | 70% |
| 集成测试 | 跨 crate 协作 | tests/ 目录 | cargo test --test <name> | 20% |
| 属性测试 | 不变量校验 | proptest | cargo test --features proptest | 5% |
| 性能基准 | 关键路径耗时 | criterion | cargo bench | 3% |
| 端到端 | 真实部署 | Docker Compose | ./scripts/e2e.sh | 2% |
单元测试
单元测试位于 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 tests | test_* | cargo test -p <crate> |
| 集成测试 | tests/*.rs | *_test.rs | cargo test --test <name> |
| 端到端测试 | tests/e2e/ | e2e_*.rs | cargo test --package e2e |
| 协议一致性 | tests/protocol_conformance/ | — | cargo test --package protocol_conformance |
| 安全测试 | tests/security/ | — | cargo test --package security |
| OS 启动测试 | os/tests/ | *_test.rs | cargo test --package os-tests |
| 性能基准 | benches/benches/ | *_bench.rs | cargo 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.rs | Agent 运行时 | Agent 调度延迟 |
ai_bench.rs | AI 推理 | 推理延迟、吞吐 |
api_bench.rs | API 性能 | REST/GraphQL QPS |
ha_bench.rs | 高可用 | 故障切换时间 |
iot_bench.rs | IoT | 接入吞吐 |
perf_bench.rs | 通用性能 | 内存分配、并发 |
scada_bench.rs | SCADA | 数据采集吞吐 |
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.yml | push/PR | 构建 + 单元测试 + clippy + fmt |
| 端到端 | e2e.yml | push to main | E2E 测试 |
| 安全扫描 | security.yml | push/PR | SAST + 依赖审计 |
| 性能基准 | benchmark.yml | push to main | 性能回归检测 |
| 覆盖率 | coverage.yml | push to main | 代码覆盖率上报 |
| 协议一致性 | conformance.yml | push/PR | 电力协议一致性 |
| 发布 | release.yml | tag | 发布到 crates.io + GitHub Release |
| 文档站 | deploy-web.yml | push 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%
- 性能敏感的代码已补充基准测试