Files
swagger-cli/tests/crash_consistency_test.rs
teernisse 4ac8659ebd Wave 7: Phase 2 features - sync --all, external refs, cross-alias discovery, CI/CD, reliability tests (bd-1ky, bd-1bp, bd-1rk, bd-1lj, bd-gvr, bd-1x5)
- Sync --all with async concurrency, per-host throttling, failure budgets, resumable execution
- External ref bundling at fetch time with origin tracking
- Cross-alias discovery (--all-aliases) for list and search commands
- CI/CD pipeline (.gitlab-ci.yml), cargo-deny config, Dockerfile, install script
- Reliability test suite: crash consistency (8 tests), lock contention (3 tests), property-based (4 tests)
- Criterion performance benchmarks (5 benchmarks)
- Bug fix: doctor --fix now repairs missing index.json when raw.json exists
- Bug fix: shared $ref references no longer incorrectly flagged as circular (refs.rs)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 15:56:42 -05:00

169 lines
5.8 KiB
Rust

//! Crash consistency tests for the cache write protocol.
//!
//! Simulates partial writes at each stage of the crash-consistent write protocol
//! and verifies that the read protocol detects corruption and doctor --fix repairs it.
mod helpers;
use std::fs;
/// Helper: fetch a spec into the test environment so we have valid cache state.
fn setup_with_cached_spec(env: &helpers::TestEnv) {
helpers::fetch_fixture(env, "petstore.json", "petstore");
}
/// When raw.json exists but meta.json is missing (crash before commit marker),
/// load_index should fail with AliasNotFound (meta is the commit marker).
#[test]
fn test_crash_before_meta_write() {
let env = helpers::TestEnv::new();
setup_with_cached_spec(&env);
let alias_dir = env.cache_dir.join("petstore");
let meta_path = alias_dir.join("meta.json");
// Simulate crash: remove meta.json (the commit marker)
assert!(meta_path.exists(), "meta.json should exist after fetch");
fs::remove_file(&meta_path).expect("failed to remove meta.json");
// Read should fail — no commit marker
let result = helpers::run_cmd(&env, &["list", "petstore", "--robot"]);
result.failure();
}
/// When meta.json exists but index.json is corrupted (partial write),
/// load_index should fail with CacheIntegrity (hash mismatch).
#[test]
fn test_crash_corrupted_index() {
let env = helpers::TestEnv::new();
setup_with_cached_spec(&env);
let alias_dir = env.cache_dir.join("petstore");
let index_path = alias_dir.join("index.json");
// Corrupt the index file
fs::write(&index_path, b"{ corrupted }").expect("failed to corrupt index");
// Read should fail — hash mismatch
let result = helpers::run_cmd(&env, &["list", "petstore", "--robot"]);
result.failure();
}
/// When meta.json exists but raw.json is corrupted,
/// load_raw should fail with CacheIntegrity (hash mismatch).
/// Note: list/search only need index.json, so we test via show which needs raw.json.
#[test]
fn test_crash_corrupted_raw_json() {
let env = helpers::TestEnv::new();
setup_with_cached_spec(&env);
let alias_dir = env.cache_dir.join("petstore");
let raw_path = alias_dir.join("raw.json");
// Corrupt raw.json
fs::write(&raw_path, b"{ corrupted }").expect("failed to corrupt raw.json");
// show needs raw.json for $ref expansion → should fail
let result = helpers::run_cmd(
&env,
&["show", "petstore", "/pets", "--method", "GET", "--robot"],
);
result.failure();
}
/// Doctor --fix should repair a missing index by rebuilding from raw.json.
#[test]
fn test_doctor_fix_repairs_missing_index() {
let env = helpers::TestEnv::new();
setup_with_cached_spec(&env);
let alias_dir = env.cache_dir.join("petstore");
let index_path = alias_dir.join("index.json");
// Remove index.json
fs::remove_file(&index_path).expect("failed to remove index.json");
// Doctor --fix should repair
let result = helpers::run_cmd(&env, &["doctor", "--fix", "--robot"]);
result.success();
// After fix, list should work again
let result = helpers::run_cmd(&env, &["list", "petstore", "--robot"]);
result.success();
}
/// Doctor --fix should repair a corrupted index (hash mismatch) by rebuilding.
#[test]
fn test_doctor_fix_repairs_corrupted_index() {
let env = helpers::TestEnv::new();
setup_with_cached_spec(&env);
let alias_dir = env.cache_dir.join("petstore");
let index_path = alias_dir.join("index.json");
// Corrupt the index
fs::write(&index_path, b"[]").expect("failed to corrupt index");
// Doctor --fix should detect and repair
let result = helpers::run_cmd(&env, &["doctor", "--fix", "--robot"]);
result.success();
// After fix, list should work
let result = helpers::run_cmd(&env, &["list", "petstore", "--robot"]);
result.success();
}
/// When meta.json has a generation mismatch with index.json, read should fail.
#[test]
fn test_generation_mismatch_detected() {
let env = helpers::TestEnv::new();
setup_with_cached_spec(&env);
let alias_dir = env.cache_dir.join("petstore");
let meta_path = alias_dir.join("meta.json");
// Tamper with generation in meta.json
let meta_bytes = fs::read(&meta_path).expect("failed to read meta");
let mut meta: serde_json::Value =
serde_json::from_slice(&meta_bytes).expect("failed to parse meta");
meta["generation"] = serde_json::json!(9999);
let tampered = serde_json::to_vec_pretty(&meta).expect("failed to serialize");
fs::write(&meta_path, &tampered).expect("failed to write tampered meta");
// Read should fail — generation mismatch
let result = helpers::run_cmd(&env, &["list", "petstore", "--robot"]);
result.failure();
}
/// Leftover .tmp files from a crash should not interfere with normal operation.
#[test]
fn test_leftover_tmp_files_harmless() {
let env = helpers::TestEnv::new();
setup_with_cached_spec(&env);
let alias_dir = env.cache_dir.join("petstore");
// Create leftover .tmp files (simulating interrupted write)
fs::write(alias_dir.join("raw.json.tmp"), b"partial data").expect("failed to create tmp file");
fs::write(alias_dir.join("index.json.tmp"), b"partial index")
.expect("failed to create tmp file");
// Normal operations should still work
let result = helpers::run_cmd(&env, &["list", "petstore", "--robot"]);
result.success();
}
/// A completely empty alias directory (no files at all) should be treated as not found.
#[test]
fn test_empty_alias_directory() {
let env = helpers::TestEnv::new();
// Create an empty directory that looks like an alias
let empty_alias_dir = env.cache_dir.join("ghost-alias");
fs::create_dir_all(&empty_alias_dir).expect("failed to create dir");
// Should fail — no meta.json
let result = helpers::run_cmd(&env, &["list", "ghost-alias", "--robot"]);
result.failure();
}