use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use amethystate::store::builder::{Backend, StoreBuilder};
use amethystate::{ReactiveMap, amethystate};
mod common;
use amethystate_core::test_utils::TempPath;
use amethystate_test_macros::backends;
#[amethystate(prefix = "app")]
pub struct TableConfig {
#[amestate(default = 110u64)]
pub default_width: u64,
#[amestate(default = {
"name": 280u64,
"cpu": 80u64,
})]
pub widths: ReactiveMap<String, u64>,
}
#[backends(all)]
fn entry_cell_reads_existing_value(backend: Backend) {
let path = TempPath::new("entry_existing");
let store = StoreBuilder::new(&path).backend(backend).build().unwrap();
let config = TableConfig::new_with(&store);
let entry = config.widths().entry_cell("name".to_string());
assert_eq!(entry.get(), Some(280));
}
#[backends(all)]
fn entry_cell_on_a_missing_key_is_empty_and_refuses_writes(backend: Backend) {
let path = TempPath::new("entry_default");
let store = StoreBuilder::new(&path).backend(backend).build().unwrap();
let config = TableConfig::new_with(&store);
let entry = config.widths().entry_cell("disk".to_string());
assert_eq!(entry.get(), None);
let err = entry.set(110).unwrap_err();
insta::assert_snapshot!("write_to_an_absent_key", err.to_string());
assert_eq!(config.widths().get("disk"), None);
}
#[backends(all)]
fn write_lands_in_store(backend: Backend) {
let path = TempPath::new("entry_write");
let store = StoreBuilder::new(&path).backend(backend).build().unwrap();
let config = TableConfig::new_with(&store);
let entry = config.widths().entry_cell("cpu".to_string());
entry.set(144).unwrap();
assert_eq!(config.widths().get("cpu"), Some(144));
assert_eq!(entry.get(), Some(144));
}
#[backends(all)]
fn external_map_write_lands_in_cell(backend: Backend) {
let path = TempPath::new("entry_external");
let store = StoreBuilder::new(&path).backend(backend).build().unwrap();
let config = TableConfig::new_with(&store);
let entry = config.widths().entry_cell("cpu".to_string());
config.widths().update("cpu", &99).unwrap();
assert_eq!(entry.get(), Some(99));
}
#[backends(all)]
fn removed_key_empties_the_cell(backend: Backend) {
let path = TempPath::new("entry_remove");
let store = StoreBuilder::new(&path).backend(backend).build().unwrap();
let config = TableConfig::new_with(&store);
let entry = config.widths().entry_cell("cpu".to_string());
assert_eq!(entry.get(), Some(80));
config.widths().remove("cpu").unwrap();
assert_eq!(entry.get(), None);
let err = entry.set(110).unwrap_err();
insta::assert_snapshot!("write_to_a_removed_key", err.to_string());
assert_eq!(config.widths().get("cpu"), None);
}
#[backends(all)]
fn two_cells_on_one_key_fire_once_per_write(backend: Backend) {
let path = TempPath::new("entry_no_echo");
let store = StoreBuilder::new(&path).backend(backend).build().unwrap();
let config = TableConfig::new_with(&store);
let a = config.widths().entry_cell("cpu".to_string());
let b = config.widths().entry_cell("cpu".to_string());
let fires = Arc::new(AtomicUsize::new(0));
let fires_clone = fires.clone();
let _watch = a.subscribe(move |_: &Option<u64>| {
fires_clone.fetch_add(1, Ordering::SeqCst);
});
b.set(200).unwrap();
assert_eq!(a.get(), Some(200), "b's write must reach a");
assert_eq!(config.widths().get("cpu"), Some(200));
assert_eq!(
fires.load(Ordering::SeqCst),
1,
"a remote write must fire a exactly once"
);
a.set(300).unwrap();
assert_eq!(b.get(), Some(300), "a's write must reach b");
assert_eq!(
fires.load(Ordering::SeqCst),
2,
"a local write must fire once too, not twice"
);
}
#[backends(all)]
fn a_cell_survives_the_handle_it_was_made_from(backend: Backend) {
let path = TempPath::new("entry_keepalive");
let store = StoreBuilder::new(&path).backend(backend).build().unwrap();
let config = TableConfig::new_with(&store);
let entry = {
let widths = config.widths();
widths.entry_cell("cpu".to_string())
};
config.widths().update("cpu", &55).unwrap();
assert_eq!(entry.get(), Some(55), "cache must still be fed");
}
#[backends(all)]
fn a_cell_dies_with_the_map_it_views(backend: Backend) {
let path = TempPath::new("entry_dead_map");
let store = StoreBuilder::new(&path).backend(backend).build().unwrap();
let entry = {
let map = store.kv().map::<String, u64>("standalone").unwrap();
map.insert("cpu".to_string(), &80).unwrap();
map.entry_cell("cpu".to_string())
};
assert_eq!(entry.get(), None);
let err = entry.set(1).unwrap_err();
insta::assert_snapshot!("write_through_a_cell_whose_map_is_gone", err.to_string());
}
#[backends(all)]
fn entry_cell_write_persists_across_store_rebuild(backend: Backend) {
let path = TempPath::new("entry_persist");
{
let store = StoreBuilder::new(&path).backend(backend).build().unwrap();
let config = TableConfig::new_with(&store);
config.widths().insert("memory".to_string(), &0).unwrap();
let entry = config.widths().entry_cell("memory".to_string());
entry.set(256).unwrap();
}
{
let store = StoreBuilder::new(&path).backend(backend).build().unwrap();
let config = TableConfig::new_with(&store);
assert_eq!(config.widths().get("memory"), Some(256));
let entry = config.widths().entry_cell("memory".to_string());
assert_eq!(entry.get(), Some(256));
}
}
#[backends(all)]
fn a_forgotten_cell_does_not_hold_the_store_open(backend: Backend) {
let path = TempPath::new("entry_no_pin");
let entry = {
let store = StoreBuilder::new(path.path())
.backend(backend)
.build()
.unwrap();
let map = store.kv().map::<String, u64>("widths").unwrap();
map.insert("cpu".to_string(), &80).unwrap();
map.entry_cell("cpu".to_string())
};
let reopened = StoreBuilder::new(path.path()).backend(backend).build();
assert!(
reopened.is_ok(),
"the cell outlived the store and kept the file open: {:?}",
reopened.err()
);
assert_eq!(entry.get(), None);
}
#[backends(all)]
fn an_owning_cell_survives_the_struct_it_came_from(backend: Backend) {
let path = TempPath::new("entry_owned");
let store = StoreBuilder::new(&path).backend(backend).build().unwrap();
let width = {
let config = TableConfig::new_with(&store);
config.default_width().into_cell()
};
assert_eq!(width.get(), Some(110));
width.set(140).unwrap();
assert_eq!(
store.get::<u64>(["app", "default_width"]).unwrap(),
Some(140)
);
}
#[backends(all)]
fn an_owning_entry_cell_survives_the_map_it_came_from(backend: Backend) {
let path = TempPath::new("entry_owned_map");
let store = StoreBuilder::new(&path).backend(backend).build().unwrap();
let cpu = {
let map = store.kv().map::<String, u64>("widths").unwrap();
map.insert("cpu".to_string(), &80).unwrap();
map.into_entry_cell("cpu".to_string())
};
assert_eq!(cpu.get(), Some(80));
cpu.set(96).unwrap();
assert_eq!(cpu.get(), Some(96));
}
#[backends(all)]
fn an_update_through_a_cell_on_a_missing_key_says_the_key_is_absent(backend: Backend) {
let path = TempPath::new("entry_absent_update");
let store = StoreBuilder::new(&path).backend(backend).build().unwrap();
let config = TableConfig::new_with(&store);
let gpu = config.widths().entry_cell("gpu".to_string());
let updated = gpu.update(|width| width + 1).unwrap_err();
assert!(
matches!(
&updated,
amethystate::errors::WriteValue::Absent { at } if at.to_string() == "app.widths.gpu"
),
"{updated:?}"
);
let modified = gpu.modify(|width| *width += 1).unwrap_err();
assert!(
matches!(&modified, amethystate::errors::WriteValue::Absent { .. }),
"{modified:?}"
);
}
#[backends(all)]
fn an_update_through_a_cell_whose_map_is_gone_says_the_source_is_gone(backend: Backend) {
let path = TempPath::new("entry_gone_update");
let store = StoreBuilder::new(&path).backend(backend).build().unwrap();
let cpu = {
let config = TableConfig::new_with(&store);
config.widths().entry_cell("cpu".to_string())
};
let updated = cpu.update(|width| width + 1).unwrap_err();
assert!(
matches!(&updated, amethystate::errors::WriteValue::SourceGone),
"{updated:?}"
);
}