//! Lock contention tests for cache write safety. //! //! Verifies that concurrent access to the same alias doesn't cause corruption //! or panics. Uses multiple threads (not processes) for simplicity. #![allow(deprecated)] // assert_cmd::Command::cargo_bin deprecation mod helpers; use std::sync::{Arc, Barrier}; use std::thread; /// Multiple threads fetching the same alias concurrently should all succeed /// or receive CACHE_LOCKED errors — never corruption. /// /// After all threads complete, the final cache state should be valid. #[test] fn test_concurrent_fetch_no_corruption() { let env = helpers::TestEnv::new(); let fixture_path = helpers::fixture_path("petstore.json"); let fixture_str = fixture_path.to_str().expect("fixture path not UTF-8"); // First, do an initial fetch so the alias exists helpers::run_cmd(&env, &["fetch", fixture_str, "--alias", "petstore"]).success(); let thread_count = 8; let barrier = Arc::new(Barrier::new(thread_count)); let home_dir = env.home_dir.clone(); let fixture = fixture_str.to_string(); let handles: Vec<_> = (0..thread_count) .map(|_| { let barrier = Arc::clone(&barrier); let home = home_dir.clone(); let fix = fixture.clone(); thread::spawn(move || { // Wait for all threads to be ready barrier.wait(); // All threads try to re-fetch the same alias with --force let output = assert_cmd::Command::cargo_bin("swagger-cli") .expect("binary not found") .env("SWAGGER_CLI_HOME", &home) .args(["fetch", &fix, "--alias", "petstore", "--force"]) .output() .expect("failed to execute"); output.status.code().unwrap_or(-1) }) }) .collect(); let exit_codes: Vec = handles.into_iter().map(|h| h.join().unwrap()).collect(); // All should either succeed (0) or get CACHE_LOCKED (exit code 9) for (i, code) in exit_codes.iter().enumerate() { assert!( *code == 0 || *code == 9, "thread {i} exited with unexpected code {code} (expected 0 or 9)" ); } // At least one should succeed assert!( exit_codes.contains(&0), "at least one thread should succeed" ); // Final state should be valid — list should work let result = helpers::run_cmd(&env, &["list", "petstore", "--robot"]); result.success(); // Doctor should report healthy let result = helpers::run_cmd(&env, &["doctor", "--robot"]); let a = result.success(); let json = helpers::parse_robot_json(&a.get_output().stdout); // Doctor output should indicate health assert!( json["data"]["health"].as_str().is_some(), "doctor should report health status" ); } /// Multiple threads listing the same alias concurrently should all succeed. /// Reads are not locked (only writes acquire the lock). #[test] fn test_concurrent_reads_no_contention() { let env = helpers::TestEnv::new(); helpers::fetch_fixture(&env, "petstore.json", "petstore"); let thread_count = 16; let barrier = Arc::new(Barrier::new(thread_count)); let home_dir = env.home_dir.clone(); let handles: Vec<_> = (0..thread_count) .map(|_| { let barrier = Arc::clone(&barrier); let home = home_dir.clone(); thread::spawn(move || { barrier.wait(); let output = assert_cmd::Command::cargo_bin("swagger-cli") .expect("binary not found") .env("SWAGGER_CLI_HOME", &home) .args(["list", "petstore", "--robot"]) .output() .expect("failed to execute"); output.status.code().unwrap_or(-1) }) }) .collect(); let exit_codes: Vec = handles.into_iter().map(|h| h.join().unwrap()).collect(); // ALL reads should succeed (no locking on reads) for (i, code) in exit_codes.iter().enumerate() { assert_eq!(*code, 0, "thread {i} read failed with exit code {code}"); } } /// Concurrent writes to DIFFERENT aliases should never interfere with each other. #[test] fn test_concurrent_different_aliases() { let env = helpers::TestEnv::new(); let fixture_path = helpers::fixture_path("petstore.json"); let fixture_str = fixture_path.to_str().expect("fixture path not UTF-8"); let aliases = ["alpha", "bravo", "charlie", "delta"]; let barrier = Arc::new(Barrier::new(aliases.len())); let home_dir = env.home_dir.clone(); let fixture = fixture_str.to_string(); let handles: Vec<_> = aliases .iter() .map(|alias| { let barrier = Arc::clone(&barrier); let home = home_dir.clone(); let fix = fixture.clone(); let a = alias.to_string(); thread::spawn(move || { barrier.wait(); let output = assert_cmd::Command::cargo_bin("swagger-cli") .expect("binary not found") .env("SWAGGER_CLI_HOME", &home) .args(["fetch", &fix, "--alias", &a]) .output() .expect("failed to execute"); (a, output.status.code().unwrap_or(-1)) }) }) .collect(); let results: Vec<(String, i32)> = handles.into_iter().map(|h| h.join().unwrap()).collect(); // All should succeed — different aliases means different lock files for (alias, code) in &results { assert_eq!(*code, 0, "fetch to alias '{alias}' failed with code {code}"); } // All aliases should be listable for alias in &aliases { helpers::run_cmd(&env, &["list", alias, "--robot"]).success(); } }