1use std::cell::RefCell;
22use std::collections::{BTreeMap, VecDeque};
23
24use super::assets::{AssetFormat, AssetSink, AssetSource};
25use super::lock::{Claim, Holding};
26use super::manifest::{self, BreakReport, Row};
27use super::record::{Digest, WallTime};
28use super::revs::{self, REVS};
29use super::storage::{Entry, Name, Residency, Storage, ready_now};
30
31use blockworx_doc::{block_model::Asset, document::Document, hash::AssetHash, rev::Rev};
32
33pub const MANIFEST: Entry = Entry::fixed("manifest.jsonl");
34pub const LOCK: Entry = Entry::fixed("lock");
35pub const GITATTRIBUTES: Entry = Entry::fixed(".gitattributes");
36pub const ASSETS: &str = "assets";
38
39const GITATTRIBUTES_TEMPLATE: &str = "\
44# A blockworx container. revs/ holds the document at every rev and
45# manifest.jsonl is the trail that names them.
46#
47# The merge driver named here is not built in. Define it once, globally:
48# git config --global merge.conflict-always.name 'never auto-merge'
49# git config --global merge.conflict-always.driver false
50# Without it git falls back to the text merge, and a merged manifest will
51# be refused by the integrity chain at load — loudly, but later.
52manifest.jsonl merge=conflict-always
53revs/** binary
54assets/** binary
55lock export-ignore
56";
57
58#[derive(Debug)]
61pub enum ReadOnlyReason {
62 Locked(Holding),
64 Reading,
69 HistoryBroken(BreakReport),
72 WriteFailed(std::io::Error),
76}
77
78impl std::fmt::Display for ReadOnlyReason {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 match self {
81 ReadOnlyReason::Locked(held) => {
82 write!(f, "another blockworx ({held}) has this diagram open")
83 }
84 ReadOnlyReason::Reading => write!(f, "this container was opened to be read"),
85 ReadOnlyReason::HistoryBroken(report) => {
86 write!(f, "the manifest does not verify at {report}")
87 }
88 ReadOnlyReason::WriteFailed(error) => {
89 write!(f, "a row could not be appended: {error}")
90 }
91 }
92 }
93}
94
95pub enum Access {
99 Writable,
100 ReadOnly(ReadOnlyReason),
101}
102
103#[derive(Debug, thiserror::Error)]
106pub enum ContainerError {
107 #[error("{0} already holds a diagram")]
108 Exists(String),
109 #[error("{0} is not a blockworx diagram: it has no {MANIFEST}")]
110 NotAContainer(String),
111 #[error(transparent)]
112 Io(#[from] std::io::Error),
113}
114
115enum Kept {
125 Nothing,
126 Everything(RefCell<Held>),
127}
128
129#[derive(Default)]
130struct Held {
131 bytes: BTreeMap<Entry, Vec<u8>>,
132 owed: VecDeque<(Entry, Op)>,
133}
134
135#[derive(Clone, Debug)]
138enum Op {
139 Write(Vec<u8>),
140 Append(Vec<u8>),
141 Truncate(u64),
142}
143
144#[derive(Clone, Copy, PartialEq, Eq, Debug)]
146pub struct Drained(usize);
147
148impl Drained {
149 pub const NOTHING: Self = Self(0);
150
151 pub fn performed(self) -> usize {
152 self.0
153 }
154}
155
156pub struct Container<S: Storage> {
157 storage: S,
158 access: Access,
159 kept: Kept,
160}
161
162impl<S: Storage> Container<S> {
163 pub fn create(storage: S, now: WallTime) -> Result<Self, ContainerError> {
169 ready_now(Self::creating(storage, now))
170 }
171
172 pub async fn creating(storage: S, now: WallTime) -> Result<Self, ContainerError> {
182 Self::creating_holding(storage, now, b"").await
183 }
184
185 pub fn create_holding(storage: S, now: WallTime, rows: &[u8]) -> Result<Self, ContainerError> {
193 ready_now(Self::creating_holding(storage, now, rows))
194 }
195
196 pub async fn creating_holding(
199 storage: S,
200 now: WallTime,
201 rows: &[u8],
202 ) -> Result<Self, ContainerError> {
203 if storage.exists(&MANIFEST).await? {
204 return Err(ContainerError::Exists(storage.names(&Entry::ROOT)));
205 }
206 storage.create_dir(&Entry::fixed(ASSETS)).await?;
207 storage.create_dir(&Entry::fixed(REVS)).await?;
208 storage
209 .write(&GITATTRIBUTES, GITATTRIBUTES_TEMPLATE.as_bytes())
210 .await?;
211 storage.write(&MANIFEST, rows).await?;
215 Self::attach(storage, now).await
216 }
217
218 pub fn open(storage: S, now: WallTime) -> Result<Self, ContainerError> {
224 ready_now(Self::opening(storage, now))
225 }
226
227 pub async fn opening(storage: S, now: WallTime) -> Result<Self, ContainerError> {
234 Self::require_manifest(&storage).await?;
235 Self::attach(storage, now).await
236 }
237
238 pub fn reading(storage: S) -> Result<Self, ContainerError> {
249 ready_now(Self::opening_to_read(storage))
250 }
251
252 pub async fn opening_to_read(storage: S) -> Result<Self, ContainerError> {
255 Self::require_manifest(&storage).await?;
256 Ok(Self {
257 kept: Self::kept(&storage).await?,
258 storage,
259 access: Access::ReadOnly(ReadOnlyReason::Reading),
260 })
261 }
262
263 async fn require_manifest(storage: &S) -> Result<(), ContainerError> {
264 if storage.exists(&MANIFEST).await? {
265 Ok(())
266 } else {
267 Err(ContainerError::NotAContainer(storage.names(&Entry::ROOT)))
268 }
269 }
270
271 async fn attach(storage: S, now: WallTime) -> Result<Self, ContainerError> {
272 let access = match storage.claim(now).await? {
273 Claim::Held(held) => Access::ReadOnly(ReadOnlyReason::Locked(held)),
274 Claim::Taken => Access::Writable,
275 };
276 Ok(Self {
277 kept: Self::kept(&storage).await?,
278 storage,
279 access,
280 })
281 }
282
283 async fn kept(storage: &S) -> Result<Kept, ContainerError> {
287 if let Residency::Lazy = storage.residency() {
288 return Ok(Kept::Nothing);
289 }
290 let mut held = Held::default();
291 if let Some(bytes) = storage.read(&MANIFEST).await? {
292 held.bytes.insert(MANIFEST, bytes);
293 }
294 for dir in [REVS, ASSETS] {
295 let within = storage.list(&Entry::fixed(dir)).await.unwrap_or_default();
296 for name in within {
297 let at = Entry::under(dir, &name);
298 if let Some(bytes) = storage.read(&at).await? {
299 held.bytes.insert(at, bytes);
300 }
301 }
302 }
303 Ok(Kept::Everything(RefCell::new(held)))
304 }
305
306 pub fn storage(&self) -> &S {
307 &self.storage
308 }
309
310 pub fn name(&self) -> Name {
312 self.storage.name()
313 }
314
315 pub fn access(&self) -> &Access {
316 &self.access
317 }
318
319 pub fn read(&self, at: &Entry) -> std::io::Result<Option<Vec<u8>>> {
326 match &self.kept {
327 Kept::Nothing => ready_now(self.storage.read(at)),
328 Kept::Everything(held) => Ok(held.borrow().bytes.get(at).cloned()),
329 }
330 }
331
332 pub fn exists(&self, at: &Entry) -> std::io::Result<bool> {
335 match &self.kept {
336 Kept::Nothing => ready_now(self.storage.exists(at)),
337 Kept::Everything(held) => Ok(held.borrow().bytes.contains_key(at)),
338 }
339 }
340
341 pub fn write(&self, at: &Entry, bytes: &[u8]) -> std::io::Result<()> {
345 let Kept::Everything(held) = &self.kept else {
346 return ready_now(self.storage.write(at, bytes));
347 };
348 let mut held = held.borrow_mut();
349 held.bytes.insert(at.clone(), bytes.to_vec());
350 held.owed.push_back((at.clone(), Op::Write(bytes.to_vec())));
351 Ok(())
352 }
353
354 fn append(&self, at: &Entry, bytes: &[u8]) -> std::io::Result<()> {
355 let Kept::Everything(held) = &self.kept else {
356 return ready_now(self.storage.append(at, bytes));
357 };
358 let mut held = held.borrow_mut();
359 held.bytes
360 .entry(at.clone())
361 .or_default()
362 .extend_from_slice(bytes);
363 held.owed
364 .push_back((at.clone(), Op::Append(bytes.to_vec())));
365 Ok(())
366 }
367
368 fn truncate(&self, at: &Entry, len: u64) -> std::io::Result<()> {
369 let Kept::Everything(held) = &self.kept else {
370 return ready_now(self.storage.truncate(at, len));
371 };
372 let mut held = held.borrow_mut();
373 if let Some(bytes) = held.bytes.get_mut(at) {
374 bytes.truncate(len as usize);
375 }
376 held.owed.push_back((at.clone(), Op::Truncate(len)));
377 Ok(())
378 }
379
380 pub fn pending(&self) -> usize {
383 match &self.kept {
384 Kept::Nothing => 0,
385 Kept::Everything(held) => held.borrow().owed.len(),
386 }
387 }
388
389 pub async fn drain(&mut self) -> std::io::Result<Drained> {
402 let mut performed = 0;
403 while let Some((at, op)) = self.next_owed() {
404 let done = match &op {
405 Op::Write(bytes) => self.storage.write(&at, bytes).await,
406 Op::Append(bytes) => self.storage.append(&at, bytes).await,
407 Op::Truncate(len) => self.storage.truncate(&at, *len).await,
408 };
409 if let Err(error) = done {
410 self.forget_what_is_owed();
411 let echo = std::io::Error::other(error.to_string());
412 self.demote(ReadOnlyReason::WriteFailed(error));
413 return Err(echo);
414 }
415 performed += 1;
416 }
417 Ok(Drained(performed))
418 }
419
420 fn next_owed(&self) -> Option<(Entry, Op)> {
421 match &self.kept {
422 Kept::Nothing => None,
423 Kept::Everything(held) => held.borrow_mut().owed.pop_front(),
424 }
425 }
426
427 fn forget_what_is_owed(&self) {
428 if let Kept::Everything(held) = &self.kept {
429 held.borrow_mut().owed.clear();
430 }
431 }
432
433 pub fn rename(&mut self, to: &Name) -> std::io::Result<()> {
445 ready_now(self.renaming_to(to))
446 }
447
448 pub async fn renaming_to(&mut self, to: &Name) -> std::io::Result<()> {
454 self.storage.rename(to).await
455 }
456
457 pub fn demote(&mut self, reason: ReadOnlyReason) {
460 self.release();
461 self.access = Access::ReadOnly(reason);
462 }
463
464 fn release(&mut self) {
466 if let Access::Writable = self.access {
467 self.storage.release();
468 }
469 }
470
471 pub fn read_manifest(&self) -> std::io::Result<String> {
474 let bytes = self.read(&MANIFEST)?.unwrap_or_default();
475 String::from_utf8(bytes)
476 .map_err(|why| std::io::Error::new(std::io::ErrorKind::InvalidData, why.to_string()))
477 }
478
479 pub fn assets(&self) -> Artwork<'_, S> {
481 Artwork(self)
482 }
483
484 pub fn revs(&self) -> Revs<'_, S> {
486 Revs(self)
487 }
488
489 pub fn write_rev(&self, rev: Rev, document: &Document) -> Result<Digest, WriteRefusal> {
501 let Access::Writable = &self.access else {
502 return Err(WriteRefusal::ReadOnly);
503 };
504 Ok(revs::write(
505 &mut self.revs(),
506 rev,
507 document,
508 &mut self.assets(),
509 )?)
510 }
511
512 pub fn read_rev(&self, rev: Rev) -> Result<Document, revs::RevFault> {
518 let document = revs::read(&self.revs(), rev)?;
519 Ok(revs::attached(document, &self.assets())?)
520 }
521
522 pub fn append_row(&self, row: &Row) -> Result<(), WriteRefusal> {
531 let Access::Writable = &self.access else {
532 return Err(WriteRefusal::ReadOnly);
533 };
534 let mut line = row.canonical_bytes();
535 line.push(b'\n');
536 Ok(self.append(&MANIFEST, &line)?)
537 }
538
539 pub fn truncate_manifest(&self, bytes: u64) -> Result<(), WriteRefusal> {
545 let Access::Writable = &self.access else {
546 return Err(WriteRefusal::ReadOnly);
547 };
548 Ok(self.truncate(&MANIFEST, bytes)?)
549 }
550}
551
552#[derive(Clone, Copy, PartialEq, Eq, Debug)]
555pub struct Glance {
556 pub rev: Rev,
557 pub written: WallTime,
558}
559
560impl<S: Storage> Container<S> {
561 pub async fn glance(storage: S) -> Result<Option<Glance>, ContainerError> {
567 let container = Self::opening_to_read(storage).await?;
568 let text = container.read_manifest()?;
569 let history = manifest::history(&manifest::scan(&text).rows);
570 Ok(history.rows.last().map(|row| Glance {
571 rev: row.rev,
572 written: row.wall_time,
573 }))
574 }
575}
576
577impl<S: Storage> Drop for Container<S> {
578 fn drop(&mut self) {
581 self.release();
582 }
583}
584
585pub struct Revs<'a, S: Storage>(&'a Container<S>);
587
588impl<S: Storage> revs::Backing for Revs<'_, S> {
589 fn put(&mut self, at: Rev, bytes: &[u8]) -> std::io::Result<()> {
590 self.0.write(&revs::entry(at), bytes)
591 }
592
593 fn get(&self, at: Rev) -> std::io::Result<Option<Vec<u8>>> {
594 self.0.read(&revs::entry(at))
595 }
596
597 fn names(&self, at: Rev) -> String {
598 self.0.storage.names(&revs::entry(at))
599 }
600}
601
602pub struct Artwork<'a, S: Storage>(&'a Container<S>);
604
605pub fn asset_entry(hash: AssetHash, format: AssetFormat) -> Entry {
607 Entry::under(ASSETS, &format.file_name(hash))
608}
609
610impl<S: Storage> AssetSource for Artwork<'_, S> {
611 fn names(&self, hash: AssetHash) -> String {
612 self.0
613 .storage
614 .names(&Entry::under(ASSETS, &hash.to_string()))
615 }
616
617 fn find(&self, hash: AssetHash) -> std::io::Result<(AssetFormat, Vec<u8>)> {
618 for format in AssetFormat::ALL {
619 if let Some(bytes) = self.0.read(&asset_entry(hash, format))? {
620 return Ok((format, bytes));
621 }
622 }
623 Err(std::io::Error::from(std::io::ErrorKind::NotFound))
624 }
625}
626
627impl<S: Storage> AssetSink for Artwork<'_, S> {
628 fn put(&mut self, hash: AssetHash, asset: &Asset) -> std::io::Result<()> {
631 let at = asset_entry(hash, AssetFormat::of(asset));
632 if self.0.exists(&at)? {
633 return Ok(());
634 }
635 self.0.write(&at, asset.bytes())
636 }
637}
638
639#[derive(Debug, thiserror::Error)]
640pub enum WriteRefusal {
641 #[error("this diagram is open read-only")]
642 ReadOnly,
643 #[error(transparent)]
644 Io(#[from] std::io::Error),
645}
646
647#[derive(Clone, Copy, PartialEq, Eq, Debug)]
649pub enum Discarded {
650 Removed,
651 Kept,
654}
655
656const LAID_DOWN: [Entry; 3] = [MANIFEST, LOCK, GITATTRIBUTES];
659
660const LAID_DOWN_DIRS: [&str; 2] = [ASSETS, REVS];
663
664pub async fn discard_pristine<S: Storage>(storage: &S) -> std::io::Result<Discarded> {
677 let manifest = storage
678 .read(&MANIFEST)
679 .await?
680 .ok_or_else(|| std::io::Error::from(std::io::ErrorKind::NotFound))?;
681 if !manifest.is_empty() {
682 return Ok(Discarded::Kept);
683 }
684 let mut ours = Vec::new();
685 for name in storage.list(&Entry::ROOT).await? {
686 if let Some(dir) = LAID_DOWN_DIRS.into_iter().find(|dir| *dir == name) {
687 if !matches!(storage.list(&Entry::fixed(dir)).await.as_deref(), Ok([])) {
691 return Ok(Discarded::Kept);
692 }
693 ours.push(Entry::fixed(dir));
694 } else if let Some(at) = LAID_DOWN.into_iter().find(|at| at.as_str() == name) {
695 ours.push(at);
696 } else {
697 return Ok(Discarded::Kept);
698 }
699 }
700 for at in ours {
701 storage.remove(&at).await?;
702 }
703 storage.discard().await?;
704 Ok(Discarded::Removed)
705}
706
707pub async fn discard_unclaimed<S: Storage>(
718 storage: &S,
719 now: WallTime,
720) -> std::io::Result<Discarded> {
721 if !matches!(storage.claim(now).await?, Claim::Taken) {
722 return Ok(Discarded::Kept);
723 }
724 let swept = discard_pristine(storage).await;
725 if !matches!(swept, Ok(Discarded::Removed)) {
728 storage.release();
729 }
730 swept
731}
732
733#[cfg(test)]
734mod tests {
735 use super::*;
736 use crate::fixture::{self, block_on};
737 use crate::storage::{Memory, Native};
738
739 fn native(dir: &crate::temp::TempDir, name: &str) -> Native {
740 Native::at(dir.join(name))
741 }
742
743 #[test]
744 fn a_created_container_holds_the_whole_template() {
745 let dir = fixture::dir("container-layout");
746 let root = dir.join("doc.bwx");
747 let container = Container::create(Native::at(&root), WallTime::EPOCH)
748 .expect("the container is laid out");
749
750 assert!(matches!(container.access(), Access::Writable));
751 assert_eq!(container.read_manifest().expect("an empty manifest"), "");
752 assert!(root.join(ASSETS).is_dir());
753 assert!(root.join(REVS).is_dir(), "a new container has no revs/");
754 assert!(
755 root.join(LOCK.as_str()).is_file(),
756 "a writable handle holds the lock"
757 );
758 let attributes =
759 std::fs::read_to_string(root.join(GITATTRIBUTES.as_str())).expect("the git template");
760 assert!(attributes.contains("manifest.jsonl merge=conflict-always"));
761 assert!(attributes.contains("revs/** binary"));
762 assert!(
763 !attributes.contains("document.json"),
764 "the template still names a file this layout does not write",
765 );
766 }
767
768 #[test]
769 fn creating_over_an_existing_container_is_refused() {
770 let dir = fixture::dir("container-exists");
771 let root = dir.join("doc.bwx");
772 let first =
773 Container::create(Native::at(&root), WallTime::EPOCH).expect("the first is laid out");
774 drop(first);
775
776 assert!(matches!(
777 Container::create(Native::at(&root), WallTime::EPOCH),
778 Err(ContainerError::Exists(_)),
779 ));
780 assert!(matches!(
781 Container::open(native(&dir, "nothing-here"), WallTime::EPOCH),
782 Err(ContainerError::NotAContainer(_)),
783 ));
784 }
785
786 #[test]
789 fn dropping_a_container_releases_its_lock() {
790 let dir = fixture::dir("container-lock-lifetime");
791 let root = dir.join("doc.bwx");
792 let container =
793 Container::create(Native::at(&root), WallTime::EPOCH).expect("the container");
794 assert!(matches!(
795 Container::open(Native::at(&root), WallTime::EPOCH)
796 .expect("a second handle opens")
797 .access(),
798 Access::ReadOnly(ReadOnlyReason::Locked(_)),
799 ));
800
801 drop(container);
802 assert!(matches!(
803 Container::open(Native::at(&root), WallTime::EPOCH)
804 .expect("a third handle opens")
805 .access(),
806 Access::Writable,
807 ));
808 }
809
810 #[test]
815 fn a_renamed_container_keeps_its_lock_and_its_open_manifest() {
816 let dir = fixture::dir("container-rename");
817 let root = dir.join("doc.bwx");
818 let renamed = dir.join("engine.bwx");
819 let mut container =
820 Container::create(Native::at(&root), WallTime::EPOCH).expect("the container");
821 let row = fixture::rows(1).pop().expect("one row");
822 container.append_row(&row).expect("the first row lands");
823
824 container
825 .rename(&Name::of_document("engine").expect("a name"))
826 .expect("the container moves");
827
828 assert_eq!(container.name().to_string(), "engine.bwx");
829 assert!(!root.exists(), "the old name still stands");
830 assert!(
831 renamed.join(LOCK.as_str()).is_file(),
832 "the lock did not travel with the container",
833 );
834 container
835 .append_row(&row)
836 .expect("an append after the rename");
837 assert_eq!(
838 std::fs::read_to_string(renamed.join(MANIFEST.as_str()))
839 .expect("the renamed manifest")
840 .lines()
841 .count(),
842 2,
843 "the session kept appending to the file it had open",
844 );
845 assert!(
846 matches!(
847 Container::open(Native::at(&renamed), WallTime::EPOCH)
848 .expect("a second handle")
849 .access(),
850 Access::ReadOnly(ReadOnlyReason::Locked(_)),
851 ),
852 "the lock was dropped somewhere in the move",
853 );
854
855 drop(container);
856 assert!(
857 !renamed.join(LOCK.as_str()).exists(),
858 "releasing the lock removed the wrong path",
859 );
860 }
861
862 #[test]
863 fn a_rename_onto_something_that_exists_is_refused() {
864 let dir = fixture::dir("container-rename-onto");
865 let root = dir.join("doc.bwx");
866 let taken = dir.join("taken.bwx");
867 let mut container =
868 Container::create(Native::at(&root), WallTime::EPOCH).expect("the container");
869 Container::create(Native::at(&taken), WallTime::EPOCH).expect("the container in the way");
870
871 let refusal = container
872 .rename(&Name::of_document("taken").expect("a name"))
873 .expect_err("renaming over a container must be refused");
874 assert_eq!(refusal.kind(), std::io::ErrorKind::AlreadyExists);
875 assert_eq!(
876 container.name().to_string(),
877 "doc.bwx",
878 "the store moved anyway"
879 );
880 assert!(
881 taken.join(MANIFEST.as_str()).is_file(),
882 "the container in the way is gone"
883 );
884 }
885
886 #[test]
889 fn only_a_container_with_nothing_in_it_is_discarded() {
890 let dir = fixture::dir("container-discard");
891
892 let empty = dir.join("empty.bwx");
893 drop(Container::create(Native::at(&empty), WallTime::EPOCH).expect("the empty container"));
894 assert_eq!(
895 block_on(discard_pristine(&Native::at(&empty))).expect("the sweep answers"),
896 Discarded::Removed,
897 );
898 assert!(!empty.exists(), "a pristine container was left behind");
899
900 let written = dir.join("written.bwx");
901 let container =
902 Container::create(Native::at(&written), WallTime::EPOCH).expect("the container");
903 let row = fixture::rows(1).pop().expect("one row");
904 container.append_row(&row).expect("the row lands");
905 drop(container);
906 assert_eq!(
907 block_on(discard_pristine(&Native::at(&written))).expect("the sweep answers"),
908 Discarded::Kept,
909 "a container with a row in it was removed",
910 );
911 assert!(written.join(MANIFEST.as_str()).is_file());
912
913 let theirs = dir.join("theirs.bwx");
914 drop(Container::create(Native::at(&theirs), WallTime::EPOCH).expect("the container"));
915 std::fs::write(theirs.join("notes.txt"), b"mine").expect("a file of the user's own");
916 assert_eq!(
917 block_on(discard_pristine(&Native::at(&theirs))).expect("the sweep answers"),
918 Discarded::Kept,
919 "something we did not write was removed",
920 );
921 assert!(
922 theirs.join(MANIFEST.as_str()).is_file(),
923 "and it was half-removed"
924 );
925 }
926
927 #[test]
931 fn a_held_container_survives_the_unclaimed_sweep() {
932 let held = Memory::deferred("held.bwx");
933 drop(block_on(Container::creating(held.clone(), WallTime::EPOCH)).expect("the container"));
934 let claimed = block_on(Storage::claim(&held, WallTime::EPOCH)).expect("the claim");
935 assert!(
936 matches!(claimed, Claim::Taken),
937 "precondition: the lock is ours to hold"
938 );
939
940 assert_eq!(
941 block_on(discard_unclaimed(&held, WallTime::EPOCH)).expect("the sweep answers"),
942 Discarded::Kept,
943 "a container someone holds was swept",
944 );
945 assert!(
946 block_on(held.exists(&MANIFEST)).expect("the read"),
947 "and it was half-removed",
948 );
949
950 held.release();
951 assert_eq!(
952 block_on(discard_unclaimed(&held, WallTime::EPOCH)).expect("the sweep answers"),
953 Discarded::Removed,
954 "a container nobody holds and nobody wrote to was left behind",
955 );
956 assert!(
957 !block_on(held.exists(&MANIFEST)).expect("the read"),
958 "and it was half-removed",
959 );
960 }
961
962 #[test]
963 fn a_read_only_handle_refuses_to_write() {
964 let dir = fixture::dir("container-read-only");
965 let root = dir.join("doc.bwx");
966 let _held = Container::create(Native::at(&root), WallTime::EPOCH).expect("the container");
967 let second = Container::open(Native::at(&root), WallTime::EPOCH).expect("a second handle");
968
969 let row = fixture::rows(1).pop().expect("one row");
970 assert!(matches!(
971 second.append_row(&row),
972 Err(WriteRefusal::ReadOnly)
973 ));
974 assert!(matches!(
975 second.truncate_manifest(0),
976 Err(WriteRefusal::ReadOnly)
977 ));
978 }
979
980 #[test]
984 fn a_resident_container_reads_back_what_it_was_written() {
985 let document = fixture::documents(1).pop().expect("one document");
986 let held = Container::create(Memory::resident("doc.bwx"), WallTime::EPOCH)
987 .expect("the container is laid out");
988 let at = blockworx_doc::fixtures::rev(1);
989 held.write_rev(at, &document).expect("the rev lands");
990
991 assert!(
992 matches!(held.kept, Kept::Everything(_)),
993 "precondition: this container is held in memory",
994 );
995 assert_eq!(held.read_rev(at).expect("it reads back"), document);
996 let row = fixture::rows(1).pop().expect("one row");
997 held.append_row(&row).expect("the row lands");
998 assert_eq!(
999 held.read_manifest().expect("the manifest").lines().count(),
1000 1,
1001 );
1002 }
1003}