mod common;
use std::collections::HashSet;
use lazily::{ComputedMap, Context, EntryKind, SourceMap};
use serde_json::Value;
const SPEC_DIR: &str = "../lazily-spec/conformance/materialization";
type V = i64;
fn spec_fixtures_present() -> bool {
std::path::Path::new(SPEC_DIR).exists()
}
fn load_fixture(name: &str) -> Value {
let path = format!("{SPEC_DIR}/{name}");
let raw = crate::common::spec_read_to_string(&path)
.unwrap_or_else(|e| panic!("failed to read fixture {path}: {e}"));
serde_json::from_str(&raw).unwrap_or_else(|e| panic!("failed to parse fixture {path}: {e}"))
}
fn str_array(v: &Value, path: &str) -> Vec<String> {
v.get(path)
.and_then(|v| v.as_array())
.unwrap_or_else(|| panic!("missing array {path}"))
.iter()
.map(|k| k.as_str().expect("array of strings").to_string())
.collect()
}
fn as_set(keys: &[String]) -> HashSet<String> {
keys.iter().cloned().collect()
}
fn parse_val_spec(fixture: &Value) -> Vec<(String, V)> {
let obj = fixture
.get("spec")
.and_then(|s| s.get("val"))
.and_then(|v| v.as_object())
.expect("spec.val object");
obj.iter()
.map(|(k, v)| (k.clone(), v.as_i64().expect("integer val")))
.collect()
}
fn lookup_fn(
entries: Vec<(String, V)>,
) -> impl Fn(&lazily::Compute<'_>, &String) -> V + Clone + 'static {
move |_ctx: &lazily::Compute<'_>, k: &String| -> V {
entries
.iter()
.find(|(key, _)| key == k)
.map(|(_, v)| *v)
.unwrap_or_else(|| panic!("no spec val for key {k}"))
}
}
fn eager_computed_map(
ctx: &Context,
keys: Vec<String>,
entries: Vec<(String, V)>,
) -> ComputedMap<String, V> {
let map: ComputedMap<String, V> = ComputedMap::new(ctx);
map.materialize_all(ctx, keys, lookup_fn(entries));
map
}
fn lazy_computed_map(ctx: &Context) -> ComputedMap<String, V> {
ComputedMap::new(ctx)
}
fn check_val_fixture(name: &str) -> Value {
let fixture = load_fixture(name);
let entries = parse_val_spec(&fixture);
let keys: Vec<String> = entries.iter().map(|(k, _)| k.clone()).collect();
let expected = fixture.get("expected").expect("expected");
assert_eq!(
expected.get("default_mode").and_then(|v| v.as_str()),
Some("eager")
);
let ctx = Context::new();
let eager = eager_computed_map(&ctx, keys.clone(), entries.clone());
let lazy = lazy_computed_map(&ctx);
assert_eq!(eager.present_count(), keys.len());
assert_eq!(
as_set(&eager.present_keys()),
as_set(&str_array(expected, "eager_present"))
);
assert_eq!(lazy.present_count(), 0);
let observe = expected
.get("observe")
.and_then(|v| v.as_object())
.expect("expected.observe");
let lookup = lookup_fn(entries);
for (k, want) in observe {
let want = want.as_i64().expect("observe int");
assert_eq!(eager.get(&ctx, k).unwrap(), want, "eager observe {k}");
assert_eq!(
lazy.get_or_insert_with(&ctx, k.clone(), lookup.clone()),
want,
"lazy observe {k}"
);
}
fixture
}
#[test]
fn observational_transparency() {
if !spec_fixtures_present() {
eprintln!("skipping: {SPEC_DIR} not present");
return;
}
let fixture = check_val_fixture("observational_transparency.json");
let expected = fixture.get("expected").unwrap();
let entries = parse_val_spec(&fixture);
let ctx = Context::new();
let lazy = lazy_computed_map(&ctx);
let lookup = lookup_fn(entries);
for k in str_array(&fixture, "reads") {
lazy.get_or_insert_with(&ctx, k, lookup.clone());
}
assert_eq!(
as_set(&lazy.present_keys()),
as_set(&str_array(expected, "lazy_present_after_reads"))
);
}
#[test]
fn deferral_not_deallocation() {
if !spec_fixtures_present() {
eprintln!("skipping: {SPEC_DIR} not present");
return;
}
let fixture = check_val_fixture("deferral_not_deallocation.json");
let expected = fixture.get("expected").unwrap();
let entries = parse_val_spec(&fixture);
let ctx = Context::new();
let lazy = lazy_computed_map(&ctx);
let lookup = lookup_fn(entries);
let want_sizes: Vec<usize> = expected
.get("present_after_each_read")
.and_then(|v| v.as_array())
.expect("present_after_each_read")
.iter()
.map(|n| n.as_u64().expect("size") as usize)
.collect();
let mut got_sizes = Vec::new();
for k in str_array(&fixture, "reads") {
lazy.get_or_insert_with(&ctx, k, lookup.clone());
got_sizes.push(lazy.present_count());
}
assert_eq!(got_sizes, want_sizes, "cumulative present-set sizes");
let lazy_present = as_set(&lazy.present_keys());
assert_eq!(
lazy_present,
as_set(&str_array(expected, "lazy_present_after_reads"))
);
let eager_present = as_set(&str_array(expected, "eager_present"));
assert!(
lazy_present.is_subset(&eager_present),
"lazy present set must be a subset of eager present set"
);
}
fn entry_kind_from_fixture_token(kind: &str) -> EntryKind {
match kind {
"cell" | "source" => EntryKind::Source,
"slot" | "computed" => EntryKind::Computed,
other => panic!("unknown entry kind {other}"),
}
}
#[test]
fn entry_kind_fixture_tokens_accept_both_spellings() {
assert_eq!(entry_kind_from_fixture_token("cell"), EntryKind::Source);
assert_eq!(entry_kind_from_fixture_token("slot"), EntryKind::Computed);
assert_eq!(entry_kind_from_fixture_token("source"), EntryKind::Source);
assert_eq!(
entry_kind_from_fixture_token("computed"),
EntryKind::Computed
);
}
#[test]
#[should_panic(expected = "unknown entry kind effect")]
fn entry_kind_fixture_token_rejects_unknown() {
let _ = entry_kind_from_fixture_token("effect");
}
#[test]
fn entry_kind_orthogonal_to_mode() {
if !spec_fixtures_present() {
eprintln!("skipping: {SPEC_DIR} not present");
return;
}
let fixture = load_fixture("entry_kind_orthogonal_to_mode.json");
let expected = fixture.get("expected").unwrap();
assert_eq!(
expected.get("default_mode").and_then(|v| v.as_str()),
Some("eager")
);
let spec_entries = fixture
.get("spec")
.and_then(|s| s.get("entries"))
.and_then(|v| v.as_object())
.expect("spec.entries");
let mut cell_keys: Vec<String> = Vec::new();
let mut slot_keys: Vec<String> = Vec::new();
let mut vals: Vec<(String, V)> = Vec::new();
for (key, entry) in spec_entries {
let kind = entry.get("kind").and_then(|v| v.as_str()).expect("kind");
let val = entry.get("val").and_then(|v| v.as_i64()).expect("val");
vals.push((key.clone(), val));
match entry_kind_from_fixture_token(kind) {
EntryKind::Source => cell_keys.push(key.clone()),
EntryKind::Computed => slot_keys.push(key.clone()),
}
}
let lookup = lookup_fn(vals.clone());
let value_of = |k: &String| -> V {
vals.iter()
.find(|(key, _)| key == k)
.map(|(_, v)| *v)
.unwrap_or_else(|| panic!("no val for {k}"))
};
let ctx = Context::new();
let eager_cells: SourceMap<String, V> = SourceMap::new(&ctx);
for k in &cell_keys {
eager_cells.entry(&ctx, k.clone(), value_of(k));
}
let eager_slots: ComputedMap<String, V> = ComputedMap::new(&ctx);
eager_slots.materialize_all(&ctx, slot_keys.clone(), lookup.clone());
assert_eq!(eager_cells.entry_kind(), EntryKind::Source);
assert_eq!(eager_slots.entry_kind(), EntryKind::Computed);
let mut eager_present = as_set(&eager_cells.present_keys());
eager_present.extend(eager_slots.present_keys());
assert_eq!(eager_present, as_set(&str_array(expected, "eager_present")));
let lazy_cells: SourceMap<String, V> = SourceMap::new(&ctx);
for k in &cell_keys {
lazy_cells.entry(&ctx, k.clone(), value_of(k));
}
let lazy_slots: ComputedMap<String, V> = ComputedMap::new(&ctx);
let present_at_build = as_set(&lazy_cells.present_keys());
assert!(
lazy_slots.present_keys().is_empty(),
"slots deferred at build"
);
assert_eq!(
present_at_build,
as_set(&str_array(expected, "lazy_present_at_build"))
);
for k in str_array(&fixture, "reads") {
if slot_keys.contains(&k) {
lazy_slots.get_or_insert_with(&ctx, k, lookup.clone());
} else {
lazy_cells.get_or_insert_with(&ctx, k, lookup.clone());
}
}
let mut lazy_after = as_set(&lazy_cells.present_keys());
lazy_after.extend(lazy_slots.present_keys());
assert_eq!(
lazy_after,
as_set(&str_array(expected, "lazy_present_after_reads"))
);
let observe = expected.get("observe").and_then(|v| v.as_object()).unwrap();
for (k, want) in observe {
let want = want.as_i64().unwrap();
if cell_keys.contains(k) {
assert_eq!(eager_cells.get(&ctx, k), Some(want));
assert_eq!(lazy_cells.get(&ctx, k), Some(want));
} else {
assert_eq!(eager_slots.get(&ctx, k), Some(want));
assert_eq!(
lazy_slots.get_or_insert_with(&ctx, k.clone(), lookup.clone()),
want
);
}
}
}