- 1
//! Concurrent edits to `.vak/feeds.toml` all land, and every reader sees a - 2
//! whole document: each edit is a read-modify-write held under one lock and - 3
//! published through a temporary file no other write shares. - 4
- 5
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - 6
- 7
use std::path::Path; - 8
- 9
use vak_core::Core; - 10
- 11
fn source_ids(path: &Path) -> Vec<String> { - 12
let text = std::fs::read_to_string(path).unwrap(); - 13
let document: toml::Table = toml::from_str(&text) - 14
.unwrap_or_else(|error| panic!("feeds.toml does not parse: {error}\n{text}")); - 15
document - 16
.get("sources") - 17
.and_then(toml::Value::as_array) - 18
.map(|sources| { - 19
sources - 20
.iter() - 21
.filter_map(|source| source.get("id").and_then(toml::Value::as_str)) - 22
.map(ToOwned::to_owned) - 23
.collect() - 24
}) - 25
.unwrap_or_default() - 26
} - 27
- 28
#[tokio::test(flavor = "multi_thread", worker_threads = 8)] - 29
async fn concurrent_feed_source_adds_all_land() { - 30
let dir = tempfile::tempdir().unwrap(); - 31
let cwd = dir.path().join("ws"); - 32
std::fs::create_dir_all(cwd.join(".vak")).unwrap(); - 33
let config = cwd.join(".vak").join("feeds.toml"); - 34
// A key no feed handler writes; every edit must carry it through. - 35
std::fs::write(&config, "# kept\n[general]\nowner_note = \"mine\"\n").unwrap(); - 36
- 37
vak_config::paths::isolate_home_for_tests(); - 38
let core = Core::new_with_trust(cwd.clone(), true).expect("core"); - 39
core.set_sessions_home(dir.path().join("home")); - 40
- 41
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); - 42
let addr = listener.local_addr().unwrap(); - 43
let app = vak_server::router(core); - 44
tokio::spawn(async move { - 45
axum::serve(listener, app).await.unwrap(); - 46
}); - 47
let base = format!("http://{addr}"); - 48
let client = reqwest::Client::new(); - 49
- 50
const ROUNDS: usize = 20; - 51
const WRITERS: usize = 8; - 52
let mut expected = Vec::new(); - 53
for round in 0..ROUNDS { - 54
let mut requests = Vec::new(); - 55
for writer in 0..WRITERS { - 56
let id = format!("src-{round}-{writer}"); - 57
expected.push(id.clone()); - 58
let client = client.clone(); - 59
let url = format!("{base}/feeds/sources"); - 60
requests.push(tokio::spawn(async move { - 61
client - 62
.post(url) - 63
.json(&serde_json::json!({ - 64
"id": id, - 65
"name": format!("Feed {id}"), - 66
"type": "rss", - 67
"url": "https://example.com/feed.xml", - 68
})) - 69
.send() - 70
.await - 71
.unwrap() - 72
.status() - 73
})); - 74
} - 75
for request in requests { - 76
assert_eq!(request.await.unwrap(), 200); - 77
} - 78
let ids = source_ids(&config); - 79
for id in &expected { - 80
assert!(ids.contains(id), "round {round}: {id} was lost"); - 81
} - 82
assert_eq!(ids.len(), expected.len(), "round {round}: {ids:?}"); - 83
} - 84
- 85
let text = std::fs::read_to_string(&config).unwrap(); - 86
assert!(text.contains("owner_note = \"mine\"")); - 87
let strays: Vec<_> = std::fs::read_dir(cwd.join(".vak")) - 88
.unwrap() - 89
.filter_map(Result::ok) - 90
.map(|entry| entry.file_name().to_string_lossy().into_owned()) - 91
.filter(|name| name.ends_with(".tmp")) - 92
.collect(); - 93
assert!(strays.is_empty(), "temporary files left behind: {strays:?}"); - 94
} - 95
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.