use super::*;
use crate::ids::Remembering;
use crate::scene::{Bare, Uses};
use crate::wire;
use std::collections::HashMap;
#[derive(Clone)]
struct Model {
rows: Vec<(u32, bool, String, bool)>,
next: u32,
layout: &'static str,
fill: &'static str,
card_fill: &'static str,
card_radius: u32,
accent: &'static str,
blank: bool,
}
impl Model {
fn new(n: u32) -> Model {
Model {
rows: (0..n)
.map(|i| (i, i % 3 == 0, format!("Row {i}"), i % 4 == 1))
.collect(),
next: n,
layout: "column",
fill: "#ffffff",
card_fill: "#eeeeee",
card_radius: 4,
accent: "#2563eb",
blank: false,
}
}
fn screen(&self) -> String {
let rows: String = self
.rows
.iter()
.map(|(n, keyed, text, card)| {
let key = if *keyed {
format!(" key=\"r{n}\"")
} else {
String::new()
};
if *card {
format!(" <Card{key} label=\"{text}\" />\n")
} else {
format!(
" <Frame{key} width={{200}} height={{20}} fill={{theme.accent}}><Text>{text}</Text></Frame>\n"
)
}
})
.collect();
format!(
"{}import Card from \"./card.design\"\n<Frame layout=\"{}\" fill=\"{}\">\n{rows}</Frame>\n",
if self.blank { "\n" } else { "" },
self.layout,
self.fill
)
}
fn card(&self) -> String {
format!(
"<Frame props={{{{ label: \"Card\", tint: theme.accent }}}} width={{120}} height={{30}} fill=\"{}\" radius={{{}}} shadow={{{{ y: 2, blur: 4, color: \"#00000033\" }}}}>\n <Text>{{label}}</Text>\n <Rect x={{100}} y={{0}} width={{10}} height={{4}} fill={{tint}} />\n</Frame>\n",
self.card_fill, self.card_radius
)
}
fn themes(&self) -> String {
format!(
"kind: themes\nlight:\n accent: \"{}\"\ndark:\n accent: \"#000000\"\n",
self.accent
)
}
}
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
self.0
}
fn below(&mut self, n: usize) -> usize {
(self.next() % n.max(1) as u64) as usize
}
}
fn edit(m: &mut Model, rng: &mut Rng) -> &'static str {
let n = m.rows.len();
match rng.below(13) {
0 => {
let at = [0, n / 2, n][rng.below(3)];
m.rows.insert(
at,
(
m.next,
rng.below(2) == 0,
format!("New {}", m.next),
rng.below(3) == 0,
),
);
m.next += 1;
}
1 if n > 1 => {
m.rows.remove(rng.below(n));
}
2 if n > 1 => {
let row = m.rows.remove(rng.below(n));
let at = rng.below(m.rows.len() + 1);
m.rows.insert(at, row);
}
3 if n > 0 => {
let i = rng.below(n);
m.rows[i].2 = format!("Edited {}", rng.below(1000));
}
4 if n > 0 => {
let i = rng.below(n);
m.rows[i].3 = !m.rows[i].3;
}
5 => m.layout = ["column", "row", "none"][rng.below(3)],
6 => m.fill = ["#ffffff", "#f0f0f0"][rng.below(2)],
7 => {
m.card_fill = ["#eeeeee", "#dddddd"][rng.below(2)];
return "card.design";
}
8 => {
m.card_radius = rng.below(8) as u32;
return "card.design";
}
9 => {
m.accent = ["#2563eb", "#16a34a"][rng.below(2)];
return "themes.yml";
}
10 => m.blank = !m.blank,
11 if n > 0 => {
let i = rng.below(n);
m.rows[i].1 = !m.rows[i].1;
}
_ => {
}
}
"screen.design"
}
fn save(saves: &mut Remembering<HashMap<String, String>>, m: &Model) {
saves.files.insert("screen.design".into(), m.screen());
saves.files.insert("card.design".into(), m.card());
saves.files.insert("themes.yml".into(), m.themes());
}
fn same(a: &Scene, b: &Scene) {
assert_eq!(a.root, b.root);
assert_eq!(a.images, b.images);
assert_eq!(a.fonts, b.fonts);
assert_eq!(a.themed, b.themed);
assert_eq!(a.ids.len(), b.ids.len());
}
#[test]
fn every_save_resolved_again_is_a_fresh_resolve() {
for seed in 1..=12u64 {
let mut rng = Rng(seed.wrapping_mul(0x9e37_79b9_7f4a_7c15));
let mut m = Model::new(6 + seed as u32);
let mut saves = Remembering::new(HashMap::<String, String>::new());
save(&mut saves, &m);
let (mut res, scene) =
Resolution::new(&saves, "screen.design").unwrap_or_else(|p| panic!("{p:?}"));
let mut held = Bare::new(scene);
let mut browser = wire::decode_scene(
&wire::scene(&held.scene),
(&Default::default(), &Default::default()),
)
.unwrap();
let mut uses = Uses::of(&browser.root);
for step in 0..80 {
let file = edit(&mut m, &mut rng);
save(&mut saves, &m);
let applies = held.applies;
let outcome = res.update(&mut held, &saves, &[file.to_string()]);
let fresh = crate::resolve(&saves, "screen.design");
let once = if matches!(outcome, Outcome::Changed(..)) {
1
} else {
0
};
assert_eq!(held.applies - applies, once, "seed {seed} step {step}");
match (fresh, outcome) {
(Ok(fresh), Outcome::Same) => same(&held.scene, &fresh),
(Ok(fresh), Outcome::Changed(edits, _)) => {
same(&held.scene, &fresh);
wire::apply(&mut browser, &mut uses, &wire::patch(&edits))
.unwrap_or_else(|e| panic!("seed {seed} step {step}: {e}"));
assert_eq!(
wire::encode(&browser.root),
wire::encode(&fresh.root),
"seed {seed} step {step}"
);
}
(Err(want), Outcome::Problems(got)) => assert_eq!(want, got),
(fresh, outcome) => panic!(
"seed {seed} step {step}: fresh {:?}, but {outcome:?}",
fresh.map(|_| "a scene")
),
}
assert_eq!(
res.wholes, 0,
"seed {seed} step {step}: brought across whole"
);
}
}
}
fn list(n: usize, edited: Option<usize>, front: bool) -> String {
let mut body = String::new();
if front {
body.push_str(" <Frame width={400} height={20}><Text>Front</Text></Frame>\n");
}
for i in 0..n {
let t = if Some(i) == edited {
"Edited".to_string()
} else {
format!("Row {i}")
};
body.push_str(&format!(
" <Frame width={{400}} height={{20}}><Text>{t}</Text></Frame>\n"
));
}
format!("<Frame layout=\"column\">\n{body}</Frame>\n")
}
#[test]
fn an_edit_resolves_again_only_what_it_reaches() {
let mut counts = Vec::new();
for n in [1_000usize, 100_000] {
let mut saves = Remembering::new(HashMap::<String, String>::new());
saves.files.insert("a.design".into(), list(n, None, false));
let (mut res, scene) = Resolution::new(&saves, "a.design").unwrap();
let mut held = Bare::new(scene);
saves
.files
.insert("a.design".into(), list(n, Some(n / 2), false));
let outcome = res.update(&mut held, &saves, &["a.design".to_string()]);
let Outcome::Changed(edits, applied) = outcome else {
panic!("{outcome:?}")
};
assert_eq!(edits.list.len(), 1, "{n}");
let edited = (res.elements, applied.changed.len());
saves
.files
.insert("a.design".into(), list(n, Some(n / 2), true));
let Outcome::Changed(edits, applied) =
res.update(&mut held, &saves, &["a.design".to_string()])
else {
panic!()
};
assert_eq!(edits.list.len(), 1, "{n}");
assert_eq!(applied.placed.len(), 1);
let inserted = (res.elements, applied.placed.len(), applied.changed.len());
same(&held.scene, &crate::resolve(&saves, "a.design").unwrap());
assert_eq!(res.wholes, 0, "{n}: brought across whole");
counts.push((n, edited, inserted));
}
eprintln!("{counts:?}");
let (_, edited, inserted) = counts[0];
assert_eq!((edited.0, inserted.0), (1, 2), "{counts:?}");
for &(n, e, i) in &counts {
assert_eq!((e, i), (edited, inserted), "{n} rows: {counts:?}");
}
}
#[test]
fn a_component_or_a_theme_reaches_only_what_uses_it() {
let mut m = Model::new(10);
let mut saves = Remembering::new(HashMap::<String, String>::new());
save(&mut saves, &m);
let (mut res, scene) = Resolution::new(&saves, "screen.design").unwrap();
let mut held = Bare::new(scene);
m.card_fill = "#dddddd";
save(&mut saves, &m);
let Outcome::Changed(_, applied) = res.update(&mut held, &saves, &["card.design".to_string()])
else {
panic!()
};
let cards = m.rows.iter().filter(|r| r.3).count();
assert_eq!(applied.changed.len(), cards);
for (id, _) in &applied.changed {
assert_eq!(id.0.len(), 2, "an instance's root: {id}");
}
m.accent = "#16a34a";
save(&mut saves, &m);
let Outcome::Changed(_, applied) = res.update(&mut held, &saves, &["themes.yml".to_string()])
else {
panic!()
};
assert_eq!(applied.changed.len(), m.rows.len());
same(
&held.scene,
&crate::resolve(&saves, "screen.design").unwrap(),
);
}
#[test]
fn saves_while_a_design_has_problems_arrive_with_its_fix() {
let mut m = Model::new(5);
let mut saves = Remembering::new(HashMap::<String, String>::new());
save(&mut saves, &m);
let (mut res, scene) = Resolution::new(&saves, "screen.design").unwrap();
let mut held = Bare::new(scene);
let before = held.scene.clone();
saves.files.insert(
"card.design".into(),
"<Frame fill={theme.nothing} />".into(),
);
assert!(matches!(
res.update(&mut held, &saves, &["card.design".to_string()]),
Outcome::Problems(_)
));
m.rows[0].2 = "While broken".into();
m.rows.push((99, false, "Added while broken".into(), false));
saves.files.insert("screen.design".into(), m.screen());
assert!(matches!(
res.update(&mut held, &saves, &["screen.design".to_string()]),
Outcome::Problems(_)
));
same(&held.scene, &before);
let mut browser = wire::decode_scene(
&wire::scene(&before),
(&Default::default(), &Default::default()),
)
.unwrap();
let mut uses = Uses::of(&browser.root);
saves.files.insert("card.design".into(), m.card());
let Outcome::Changed(edits, _) = res.update(&mut held, &saves, &["card.design".to_string()])
else {
panic!()
};
let fresh = crate::resolve(&saves, "screen.design").unwrap();
same(&held.scene, &fresh);
wire::apply(&mut browser, &mut uses, &wire::patch(&edits)).unwrap();
assert_eq!(wire::encode(&browser.root), wire::encode(&fresh.root));
}