1use onestore::{
2 ExGuid, OutlineEdit as Edit, RevisionIndex, Store,
3 document::{Document, Kind},
4 op::PageOp,
5};
6use std::collections::BTreeSet;
7
8#[path = "support/current.rs"]
9mod current;
10#[path = "support/disk.rs"]
11mod disk;
12#[path = "support/metadata.rs"]
13mod metadata;
14#[path = "support/ops.rs"]
15mod ops;
16#[path = "support/sweep.rs"]
17mod sweep;
18
19const SOURCE: &[u8] = include_bytes!("../../../corpus/outline-edit/before/notebook/synthetic.one");
20
21fn cases(document: &Document<'_>) -> Vec<(String, ExGuid, ExGuid, Edit)> {
22 let mut cases = Vec::new();
23 for (sid, page) in document.pages().unwrap() {
24 let space = &document.spaces[&sid];
25 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
26 let Kind::Metadata {
27 title: Some(name), ..
28 } = &view.nodes[&view.roots[&2]].kind
29 else {
30 panic!()
31 };
32 let edit = match name.as_str() {
33 "Move outline" => Edit::Position { x: 180.0, y: 216.0 },
34 "Resize outline" => Edit::Width {
35 points: 144.0,
36 user_set: true,
37 },
38 "Automatic outline size" => Edit::Width {
39 points: 360.0,
40 user_set: false,
41 },
42 "Collapse subtree" => Edit::Collapsed(true),
43 "Expand subtree" => Edit::Collapsed(false),
44 _ => continue,
45 };
46 let mut object = *view.nodes[&page]
47 .children
48 .iter()
49 .find(|id| matches!(view.nodes[id].kind, Kind::Outline { .. }))
50 .unwrap();
51 if matches!(edit, Edit::Collapsed(_)) {
52 object = *view.nodes[&object].children.iter().find(|id| {
53 view.nodes[id].content.first().is_some_and(|text| matches!(
54 &view.nodes[text].kind, Kind::RichText { text, .. } if text.starts_with("Target ")
55 ))
56 }).unwrap();
57 }
58 cases.push((name.clone(), sid, object, edit));
59 }
60 assert_eq!(cases.len(), 5);
61 cases
62}
63
64#[test]
65fn geometry_and_expansion_preserve_unrelated_properties_objects_and_history() {
66 let store = Store::parse(SOURCE).unwrap();
67 let index = RevisionIndex::parse(&store).unwrap();
68 let document = Document::parse(&index).unwrap();
69 for (name, sid, object, edit) in cases(&document) {
70 let restored = serde_json::from_value(serde_json::to_value(edit).unwrap()).unwrap();
71 assert_eq!(edit, restored);
72 let started = ops::now();
73 let prepared = ops::page_op(
74 SOURCE,
75 sid,
76 PageOp::Outline {
77 object,
78 edit: restored,
79 },
80 )
81 .unwrap();
82 let finished = ops::now();
83 let after_store = Store::parse(prepared.as_bytes()).unwrap();
84 assert!(after_store.checksum_mismatches.is_empty());
85 let after_index = RevisionIndex::parse(&after_store).unwrap();
86 after_index.validate_current().unwrap();
87 let after_document = Document::parse(&after_index).unwrap();
88 let space = &document.spaces[&sid];
89 let before = &space.revisions[&space.contexts[&ExGuid::default()]];
90 let space = &after_document.spaces[&sid];
91 let after = &space.revisions[&space.contexts[&ExGuid::default()]];
92 let mut changed = BTreeSet::from([before.roots[&2]]);
93 let mut pending = vec![object];
94 while let Some(id) = pending.pop() {
95 if !changed.insert(id) {
96 continue;
97 }
98 pending.extend(before.nodes.iter().filter_map(|(parent, node)| {
99 node.children
100 .iter()
101 .chain(&node.content)
102 .chain(&node.structure)
103 .any(|child| *child == id)
104 .then_some(*parent)
105 }));
106 }
107 let old_raw = index.resolve_active(sid).unwrap();
108 let new_raw = after_index.resolve_active(sid).unwrap();
109 let at = metadata::check(&old_raw, &new_raw, "Author");
110 assert!((started..=finished).contains(&at));
111 let author = metadata::author(&new_raw.objects[&new_raw.roots[&4]]);
112 assert!(
113 after
114 .nodes
115 .keys()
116 .all(|id| before.nodes.contains_key(id) || *id == after.roots[&4] || *id == author)
117 );
118 for (id, node) in &before.nodes {
119 if Some(id) == before.roots.get(&4) {
120 continue;
121 }
122 let mut expected = serde_json::to_value(node).unwrap();
123 let actual = serde_json::to_value(&after.nodes[id]).unwrap();
124 if changed.contains(id) {
125 expected["modified"] = actual["modified"].clone();
126 if matches!(node.kind, Kind::Paragraph { .. }) {
127 assert_eq!(after.nodes[id].latest_author, Some(author));
128 assert_eq!(
129 after.nodes[id].modified.map(u64::from),
130 Some(at / 10_000_000 - 11_960_006_400)
131 );
132 expected["latest_author"] = actual["latest_author"].clone();
133 }
134 }
135 if *id == object {
136 match edit {
137 Edit::Position { x, y } => {
138 expected["layout"]["x"] = x.into();
139 expected["layout"]["y"] = y.into();
140 }
141 Edit::Width { points, user_set } => {
142 expected["layout"]["max_width"] = points.into();
143 expected["layout"]["width_set_by_user"] = user_set.into();
144 let fields = expected["extra"][0].as_array_mut().unwrap();
145 fields.retain(|field| field["id"].as_u64() != Some(0x14001cdb));
146 }
147 Edit::Collapsed(value) => {
148 expected["kind"]["collapse_state"] = u8::from(value).into()
149 }
150 }
151 }
152 let mut actual = actual;
153 // Property order is not semantic; every property and nested value still compares exactly.
154 expected["extra"][0]
155 .as_array_mut()
156 .unwrap()
157 .sort_by_key(|field| field["id"].as_u64().unwrap());
158 actual["extra"][0]
159 .as_array_mut()
160 .unwrap()
161 .sort_by_key(|field| field["id"].as_u64().unwrap());
162 assert_eq!(actual, expected, "{name}: {id}");
163 }
164 for (space_id, old_space) in &index.spaces {
165 for revision in old_space.revisions.keys() {
166 let old = index.resolve(*space_id, *revision).unwrap();
167 let retained = after_index.resolve(*space_id, *revision).unwrap();
168 assert_eq!(old.roots, retained.roots);
169 for (id, value) in old.objects {
170 assert_eq!(value.data, retained.objects[&id].data);
171 }
172 }
173 if *space_id != sid {
174 assert_eq!(old_space.labels, after_index.spaces[space_id].labels);
175 }
176 }
177 let again =
178 ops::page_op(prepared.as_bytes(), sid, PageOp::Outline { object, edit }).unwrap();
179 assert_eq!(again.as_bytes(), prepared.as_bytes());
180 }
181}
182
183#[test]
184fn invalid_geometry_and_non_outline_targets_are_rejected() {
185 let store = Store::parse(SOURCE).unwrap();
186 let index = RevisionIndex::parse(&store).unwrap();
187 let document = Document::parse(&index).unwrap();
188 for (_, sid, object, _) in cases(&document) {
189 for value in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
190 for edit in [
191 Edit::Position { x: value, y: 0.0 },
192 Edit::Position { x: 0.0, y: value },
193 Edit::Width {
194 points: value,
195 user_set: true,
196 },
197 ] {
198 assert!(ops::page_op(SOURCE, sid, PageOp::Outline { object, edit }).is_err());
199 }
200 }
201 for points in [-1.0, 0.0, 35.999] {
202 assert!(
203 ops::page_op(
204 SOURCE,
205 sid,
206 PageOp::Outline {
207 object,
208 edit: Edit::Width {
209 points,
210 user_set: false
211 }
212 }
213 )
214 .is_err()
215 );
216 }
217 let space = &document.spaces[&sid];
218 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
219 for (id, node) in &view.nodes {
220 if !matches!(node.kind, Kind::Outline { .. }) {
221 assert!(
222 ops::page_op(
223 SOURCE,
224 sid,
225 PageOp::Outline {
226 object: *id,
227 edit: Edit::Position { x: 72.0, y: 72.0 }
228 }
229 )
230 .is_err()
231 );
232 }
233 if matches!(node.kind, Kind::Title) {
234 let mut descendants = node.children.clone();
235 while let Some(child) = descendants.pop() {
236 descendants.extend(&view.nodes[&child].children);
237 for edit in [
238 Edit::Position { x: 72.0, y: 72.0 },
239 Edit::Width {
240 points: 144.0,
241 user_set: true,
242 },
243 Edit::Collapsed(true),
244 ] {
245 assert!(
246 ops::page_op(
247 SOURCE,
248 sid,
249 PageOp::Outline {
250 object: child,
251 edit
252 }
253 )
254 .is_err()
255 );
256 }
257 }
258 }
259 if !matches!(node.kind, Kind::Paragraph { .. }) {
260 assert!(
261 ops::page_op(
262 SOURCE,
263 sid,
264 PageOp::Outline {
265 object: *id,
266 edit: Edit::Collapsed(true)
267 }
268 )
269 .is_err()
270 );
271 }
272 }
273 assert!(
274 ops::page_op(
275 SOURCE,
276 ExGuid::default(),
277 PageOp::Outline {
278 object,
279 edit: Edit::Collapsed(true)
280 }
281 )
282 .is_err()
283 );
284 assert!(
285 ops::page_op(
286 SOURCE,
287 sid,
288 PageOp::Outline {
289 object: ExGuid::default(),
290 edit: Edit::Collapsed(true)
291 }
292 )
293 .is_err()
294 );
295 }
296}
297
298#[test]
299fn resizing_a_native_reserved_width_preserves_content() {
300 let source = include_bytes!("../../../corpus/outline-edit/after/notebook/synthetic.one");
301 let store = Store::parse(source).unwrap();
302 let index = RevisionIndex::parse(&store).unwrap();
303 let document = Document::parse(&index).unwrap();
304 let (_, sid, object, _) = cases(&document)
305 .into_iter()
306 .find(|(name, ..)| name == "Move outline")
307 .unwrap();
308 let space = &document.spaces[&sid];
309 let before = &space.revisions[&space.contexts[&ExGuid::default()]];
310 assert!(before.nodes[&object].layout.reserved_width.is_some());
311 let edit = ops::page_op(
312 source,
313 sid,
314 PageOp::Outline {
315 object,
316 edit: Edit::Width {
317 points: 144.0,
318 user_set: true,
319 },
320 },
321 )
322 .unwrap();
323 let after_store = Store::parse(edit.as_bytes()).unwrap();
324 let after_index = RevisionIndex::parse(&after_store).unwrap();
325 let after_document = Document::parse(&after_index).unwrap();
326 let space = &after_document.spaces[&sid];
327 let after = &space.revisions[&space.contexts[&ExGuid::default()]];
328 assert_eq!(after.nodes[&object].layout.reserved_width, None);
329 assert_eq!(after.nodes[&object].layout.max_width, Some(144.0));
330 for (id, node) in &before.nodes {
331 assert_eq!(node.children, after.nodes[id].children);
332 assert_eq!(node.content, after.nodes[id].content);
333 if matches!(node.kind, Kind::RichText { .. }) {
334 assert_eq!(
335 serde_json::to_value(node).unwrap(),
336 serde_json::to_value(&after.nodes[id]).unwrap()
337 );
338 }
339 }
340 if let Some(output) = std::env::var_os("ONESTORE_RESERVED_WIDTH_OUTPUT") {
341 let output = std::path::PathBuf::from(output);
342 assert!(output.is_absolute());
343 std::fs::create_dir(&output).unwrap();
344 std::fs::write(output.join("synthetic.one"), edit.as_bytes()).unwrap();
345 }
346}
347
348#[test]
349fn outline_movement_updates_both_automatic_title_fields_without_changing_content() {
350 let source = onestore::create_section("titles.one", "First", "Author").unwrap();
351 let store = Store::parse(&source).unwrap();
352 let index = RevisionIndex::parse(&store).unwrap();
353 let document = Document::parse(&index).unwrap();
354 let (sid, page) = document.pages().unwrap()[0];
355 let (add, insertion, _) = ops::new_outline(144.0, 144.0, "Second šŸ¦‹ é");
356 let source = ops::page_op(&source, sid, add).unwrap().image;
357 let store = Store::parse(&source).unwrap();
358 let index = RevisionIndex::parse(&store).unwrap();
359 let document = Document::parse(&index).unwrap();
360 let space = &document.spaces[&sid];
361 let rid = space.contexts[&ExGuid::default()];
362 let before = &space.revisions[&rid];
363 for (x, y, expected) in [
364 (144.0, 0.0, "Second šŸ¦‹ é"),
365 (0.0, 36.0, "Second šŸ¦‹ é"),
366 (144.0, 36.0, "First"),
367 (0.0, 144.0, "First"),
368 ] {
369 let started = ops::now();
370 let edit = ops::page_op(
371 &source,
372 sid,
373 PageOp::Outline {
374 object: insertion,
375 edit: Edit::Position { x, y },
376 },
377 )
378 .unwrap();
379 let finished = ops::now();
380 let saved = Store::parse(edit.as_bytes()).unwrap();
381 let saved_index = RevisionIndex::parse(&saved).unwrap();
382 saved_index.validate_current().unwrap();
383 let saved_document = Document::parse(&saved_index).unwrap();
384 let saved_space = &saved_document.spaces[&sid];
385 let after = &saved_space.revisions[&saved_space.contexts[&ExGuid::default()]];
386 let old_raw = index.resolve_active(sid).unwrap();
387 let new_raw = saved_index.resolve_active(sid).unwrap();
388 let at = metadata::check(&old_raw, &new_raw, "Author");
389 assert!((started..=finished).contains(&at));
390 let metadata = before.roots[&2];
391 assert!(
392 matches!(&after.nodes[&metadata].kind, Kind::Metadata { title: Some(title), .. } if title == expected)
393 );
394 assert!(
395 matches!(&after.nodes[&page].kind, Kind::Page { alternate_title: Some(title), .. } if title == expected)
396 );
397 assert_eq!(
398 before.nodes.keys().collect::<Vec<_>>(),
399 after.nodes.keys().collect::<Vec<_>>()
400 );
401 for (oid, node) in &before.nodes {
402 if ![page, metadata, insertion, before.roots[&4]].contains(oid) {
403 assert_eq!(
404 serde_json::to_value(node).unwrap(),
405 serde_json::to_value(&after.nodes[oid]).unwrap()
406 );
407 }
408 }
409 assert_eq!(
410 format!("{:?}", index.resolve(sid, rid).unwrap()),
411 format!("{:?}", saved_index.resolve(sid, rid).unwrap())
412 );
413 assert_eq!(
414 ops::page_op(
415 edit.as_bytes(),
416 sid,
417 PageOp::Outline {
418 object: insertion,
419 edit: Edit::Position { x, y }
420 }
421 )
422 .unwrap()
423 .as_bytes(),
424 edit.as_bytes()
425 );
426 }
427}
428
429#[test]
430fn native_automatic_and_explicit_titles_follow_outline_movement() {
431 let source = include_bytes!(
432 "../../../corpus/outline-edit/automatic-title/before/notebook/TitleControl.one"
433 );
434 let store = Store::parse(source).unwrap();
435 let index = RevisionIndex::parse(&store).unwrap();
436 let document = Document::parse(&index).unwrap();
437 let mut candidate = source.to_vec();
438 let pages = document.pages().unwrap();
439 assert_eq!(pages.len(), 3);
440 for (sid, page) in pages {
441 let space = &document.spaces[&sid];
442 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
443 let Kind::Metadata {
444 title: Some(title), ..
445 } = &view.nodes[&view.roots[&2]].kind
446 else {
447 panic!()
448 };
449 let outline = *view.nodes[&page].children.iter().find(|oid| {
450 view.nodes[oid].children.iter().any(|paragraph| view.nodes[paragraph].content.iter().any(|text| {
451 matches!(&view.nodes[text].kind, Kind::RichText { text, .. } if text.starts_with("Second"))
452 }))
453 }).unwrap();
454 let change = Edit::Position {
455 x: 36.0,
456 y: if title == "Horizontal first" {
457 108.0
458 } else {
459 72.0
460 },
461 };
462 candidate = ops::page_op(
463 &candidate,
464 sid,
465 PageOp::Outline {
466 object: outline,
467 edit: change,
468 },
469 )
470 .unwrap()
471 .as_bytes()
472 .to_vec();
473 let store = Store::parse(&candidate).unwrap();
474 let saved = RevisionIndex::parse(&store).unwrap();
475 saved.validate_current().unwrap();
476 let document = Document::parse(&saved).unwrap();
477 let space = &document.spaces[&sid];
478 let after = &space.revisions[&space.contexts[&ExGuid::default()]];
479 let expected = if title == "Explicit title" {
480 "Explicit title"
481 } else {
482 "Second šŸ¦‹ é"
483 };
484 assert!(
485 matches!(&after.nodes[&after.roots[&2]].kind, Kind::Metadata { title: Some(title), .. } if title == expected)
486 );
487 for (oid, node) in &view.nodes {
488 assert_eq!(node.children, after.nodes[oid].children);
489 assert_eq!(node.content, after.nodes[oid].content);
490 if matches!(node.kind, Kind::RichText { .. }) {
491 assert_eq!(
492 serde_json::to_value(node).unwrap(),
493 serde_json::to_value(&after.nodes[oid]).unwrap()
494 );
495 }
496 }
497 }
498 if let Some(output) = std::env::var_os("ONESTORE_OUTLINE_TITLE_OUTPUT") {
499 let output = std::path::PathBuf::from(output);
500 assert!(output.is_absolute());
501 std::fs::create_dir(&output).unwrap();
502 std::fs::write(output.join("TitleControl.one"), candidate).unwrap();
503 }
504}
505
506#[test]
507fn outline_publication_interruptions_reopen_as_complete_old_or_new_layout() {
508 let source = onestore::create_section("layout.one", "Before šŸ¦€ after", "Author").unwrap();
509 let store = Store::parse(&source).unwrap();
510 let index = RevisionIndex::parse(&store).unwrap();
511 let document = Document::parse(&index).unwrap();
512 let (sid, page) = document.pages().unwrap()[0];
513 let space = &document.spaces[&sid];
514 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
515 let outline = *view.nodes[&page]
516 .children
517 .iter()
518 .find(|id| matches!(view.nodes[id].kind, Kind::Outline { .. }))
519 .unwrap();
520 let paragraph = view.nodes[&outline].children[0];
521 let (add, _, _) = ops::new_outline(144.0, 36.0, "Second");
522 let source = ops::page_op(&source, sid, add).unwrap().image;
523 for (object, operation) in [
524 (outline, Edit::Position { x: 216.0, y: 72.0 }),
525 (
526 outline,
527 Edit::Width {
528 points: 144.0,
529 user_set: true,
530 },
531 ),
532 (paragraph, Edit::Collapsed(true)),
533 ] {
534 let edit = ops::page_op(
535 &source,
536 sid,
537 PageOp::Outline {
538 object,
539 edit: operation,
540 },
541 )
542 .unwrap();
543 let before = current::current(&source);
544 let after = current::current(edit.as_bytes());
545 for write_limit in [17, 4096] {
546 let disk = |fail_at| disk::Disk {
547 visible: source.clone(),
548 durable: source.clone(),
549 operation: 0,
550 fail_at,
551 write_limit,
552 random: 1927,
553 };
554 let mut success = disk(None);
555 edit.commit(&mut success).unwrap();
556 assert_eq!(success.durable, edit.as_bytes());
557 for at in 1..=success.operation {
558 let mut interrupted = disk(Some(at));
559 let failure = edit.commit(&mut interrupted).unwrap_err();
560 let actual = current::current(&interrupted.durable);
561 assert!(
562 actual == before || actual == after,
563 "{operation:?} operation {at}"
564 );
565 if failure.state == onestore::CommitState::NotCommitted {
566 assert_eq!(actual, before);
567 }
568 }
569 }
570 }
571}
572
573#[test]
574fn repeated_geometry_changes_and_expansion_match_an_independent_model() {
575 let original = onestore::create_section("layout.one", "Before šŸ¦€ é\rafter", "Author").unwrap();
576 let store = Store::parse(&original).unwrap();
577 let index = RevisionIndex::parse(&store).unwrap();
578 let document = Document::parse(&index).unwrap();
579 let (sid, page) = document.pages().unwrap()[0];
580 let space = &document.spaces[&sid];
581 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
582 let outline = *view.nodes[&page]
583 .children
584 .iter()
585 .find(|id| matches!(view.nodes[id].kind, Kind::Outline { .. }))
586 .unwrap();
587 let paragraph = view.nodes[&outline].children[0];
588 let text = view.nodes[&paragraph].content[0];
589 let (add, _, _) = ops::new_outline(144.0, 36.0, "Second");
590 let original = ops::page_op(&original, sid, add).unwrap().image;
591 for seed in sweep::seeds(1..17, 16) {
592 let mut rng = seed;
593 let mut source = original.clone();
594 let mut layout = serde_json::to_value(&view.nodes[&outline].layout).unwrap();
595 let mut collapsed = None;
596 for step in 0..48 {
597 rng ^= rng << 13;
598 rng ^= rng >> 7;
599 rng ^= rng << 17;
600 let (object, edit) = match step % 3 {
601 0 => {
602 let x = ((rng % 400) as f32 - 100.0) * 18.0;
603 let y = ((rng % 217) as f32 - 60.0) * 18.0;
604 layout["x"] = x.into();
605 layout["y"] = y.into();
606 (outline, Edit::Position { x, y })
607 }
608 1 => {
609 let points = (2 + rng % 86) as f32 * 18.0;
610 layout["max_width"] = points.into();
611 layout["width_set_by_user"] = (rng & 1 != 0).into();
612 (
613 outline,
614 Edit::Width {
615 points,
616 user_set: rng & 1 != 0,
617 },
618 )
619 }
620 _ => {
621 collapsed = Some(u8::from(rng & 1 != 0));
622 (paragraph, Edit::Collapsed(rng & 1 != 0))
623 }
624 };
625 source = ops::page_op(&source, sid, PageOp::Outline { object, edit })
626 .unwrap()
627 .as_bytes()
628 .to_vec();
629 let store = Store::parse(&source).unwrap();
630 let index = RevisionIndex::parse(&store).unwrap();
631 index.validate_current().unwrap();
632 let document = Document::parse(&index).unwrap();
633 let space = &document.spaces[&sid];
634 let current = &space.revisions[&space.contexts[&ExGuid::default()]];
635 let x = layout["x"].as_f64().unwrap();
636 let y = layout["y"].as_f64().unwrap();
637 let title = if y < 36.0 || (y == 36.0 && x <= 144.0) {
638 "Before šŸ¦€ é"
639 } else {
640 "Second"
641 };
642 assert!(
643 matches!(&current.nodes[&current.roots[&2]].kind, Kind::Metadata { title: Some(actual), .. } if actual == title)
644 );
645 assert!(
646 matches!(&current.nodes[&page].kind, Kind::Page { alternate_title: Some(actual), .. } if actual == title)
647 );
648 assert_eq!(
649 serde_json::to_value(&current.nodes[&outline].layout).unwrap(),
650 layout
651 );
652 let Kind::Paragraph { collapse_state, .. } = &current.nodes[&paragraph].kind else {
653 panic!()
654 };
655 assert_eq!(*collapse_state, collapsed);
656 assert_eq!(
657 current.nodes[&outline].children,
658 view.nodes[&outline].children
659 );
660 assert_eq!(
661 serde_json::to_value(&current.nodes[&text]).unwrap(),
662 serde_json::to_value(&view.nodes[&text]).unwrap()
663 );
664 }
665 }
666}
667
668#[test]
669#[ignore = "exports public-API outline candidates for cold native validation"]
670fn export_native_outline_candidates() {
671 let output = std::path::PathBuf::from(std::env::var_os("ONESTORE_OUTLINE_OUTPUT").unwrap());
672 assert!(output.is_absolute());
673 std::fs::create_dir(&output).unwrap();
674 let store = Store::parse(SOURCE).unwrap();
675 let index = RevisionIndex::parse(&store).unwrap();
676 let document = Document::parse(&index).unwrap();
677 let mut bytes = SOURCE.to_vec();
678 for (_, sid, object, edit) in cases(&document) {
679 bytes = ops::page_op(&bytes, sid, PageOp::Outline { object, edit })
680 .unwrap()
681 .as_bytes()
682 .to_vec();
683 }
684 std::fs::write(output.join("synthetic.one"), bytes).unwrap();
685}