1use crate::*;
9use alux_ext::ext;
10use anyhow::{Result, bail};
11use rsynko_media::*;
12use rsynko_session::Wanted;
13use std::ffi::OsStr;
14use std::fmt::Debug;
15use std::path::{Path, PathBuf};
16use std::time::Duration;
17
18pub trait DownloadsLawFixture: ManagerSorts {
20 fn law_collected_sources(&self, downloads: &Self::Downloads) -> Vec<String>;
22}
23
24#[ext(name = ManagerLawAuthoring)]
26pub impl<This> This
27where
28 This: NavigationStateAlg
29 + QueueCatalogAlg
30 + QueueAppendAlg
31 + DraftStateAlg
32 + ManagerStatusAlg
33 + SourceRequestAlg
34 + SubmissionAlg
35 + MediaOptionsAlg
36 + OutputChoiceAlg,
37 This::Id: Copy,
38{
39 fn author_law_queue(&mut self, count: usize) -> Vec<This::Id> {
44 let requests = (0..count)
45 .map(|index| self.source(law_source(index), self.suggested_output(), self.progressive()))
46 .collect();
47 let ids = self.append_sources(requests);
48 self.set_selected_queue_id(ids.first().copied());
51 ids
52 }
53}
54
55fn law_source(index: usize) -> String {
57 format!("law://source-{index}")
58}
59
60#[ext(name = NavigationLaws)]
62pub impl<This> This
63where
64 This: NavigationStateAlg
65 + QueueCatalogAlg
66 + QueueAppendAlg
67 + QueueRemoveAlg
68 + DraftStateAlg
69 + ManagerStatusAlg
70 + SourceRequestAlg
71 + SubmissionAlg
72 + MediaOptionsAlg
73 + OutputChoiceAlg,
74 This::Entry: QueueEntryAlg + RequestOptionsAlg,
75 This::Id: Copy + Eq + Debug,
76{
77 fn navigation_laws(&mut self) -> Result<()> {
92 let ids = self.author_law_queue(3);
93 if ids.len() != 3 {
94 bail!("authoring three sources produced {} entries", ids.len());
95 }
96 self.select_relative(true);
97 if self.selected_queue_id() != ids.get(1).copied() {
98 bail!("selecting next did not advance in collection order");
99 }
100 self.select_relative(false);
101 self.select_relative(false);
102 if self.selected_queue_id() != ids.last().copied() {
103 bail!("selecting previous did not wrap to the last identity");
104 }
105 self.select_relative(true);
106 if self.selected_queue_id() != ids.first().copied() {
107 bail!("selecting next did not wrap to the first identity");
108 }
109
110 self.set_page(ManagerPage::Details(ids[0]));
111 self.select_relative(true);
112 if self.page() != ManagerPage::Details(ids[1]) {
113 bail!("expanded details did not move with the selection");
114 }
115 if self.selected_queue_id() != Some(ids[1]) {
116 bail!("expanding details displaced the collection selection");
117 }
118
119 self.remove_selected();
120 let remaining: Vec<This::Id> = self.queue_ids().collect();
121 if remaining != [ids[0], ids[2]] {
122 bail!("removal did not preserve the surviving collection order: {remaining:?}");
123 }
124 if self.selected_queue_id() != Some(ids[2]) {
125 bail!("removing the selected entry did not choose a remaining neighbor");
126 }
127 if self.page() != ManagerPage::Collection {
128 bail!("removing the expanded entry did not denote the collection again");
129 }
130
131 let rooted = self.breadcrumbs();
132 if rooted.len() != 1 {
133 bail!("the collection rests under {} pages", rooted.len() - 1);
134 }
135 self.set_page(ManagerPage::Details(ids[2]));
136 let expanded = self.breadcrumbs();
137 if expanded.first() != rooted.first() {
138 bail!("expanded details are not rooted at the collection");
139 }
140 let named = self.queue_entry(ids[2]).map(QueueEntryAlg::label);
141 if expanded.last().map(|crumb| crumb.label.as_str()) != named {
142 bail!("the breadcrumb of an expanded entry does not name what it denotes");
143 }
144 Ok(())
145 }
146}
147
148#[ext(name = DraftLaws)]
150pub impl<This> This
151where
152 This: NavigationStateAlg
153 + QueueCatalogAlg
154 + QueueAppendAlg
155 + DraftStateAlg
156 + OutputDraftAlg
157 + InputDraftAlg
158 + ManagerStatusAlg
159 + SourceRequestAlg
160 + SubmissionAlg
161 + MediaOptionsAlg
162 + OutputChoiceAlg,
163 This::Entry: QueueEntryAlg,
164 This::Id: Copy + Eq + Debug,
165{
166 fn draft_laws(&mut self) -> Result<()> {
180 self.set_draft("first\nsecond".to_owned());
181 if self.draft() != "first\nsecond" {
182 bail!("a replaced draft is not the one observed back");
183 }
184 self.set_output_draft("name.mp4".to_owned());
185 if self.draft() != "first\nsecond" {
186 bail!("replacing the output-name draft disturbed the source draft");
187 }
188 if self.output_draft() != "name.mp4" {
189 bail!("a replaced output-name draft is not the one observed back");
190 }
191
192 self.set_draft(String::new());
193 self.submit_draft();
194 if self.queue_ids().next().is_some() {
195 bail!("submitting a draft naming no source still added an entry");
196 }
197
198 self.set_page(ManagerPage::AddSources);
199 self.set_draft(format!(" {} \n\n{}\n", law_source(0), law_source(1)));
200 self.submit_draft();
201 let submitted: Vec<String> =
202 self.queue_ids().filter_map(|id| self.queue_entry(id)).map(|entry| entry.label().to_owned()).collect();
203 if submitted != [law_source(0), law_source(1)] {
204 bail!("submitting did not add exactly the sources the draft names: {submitted:?}");
205 }
206 if !self.draft().is_empty() {
207 bail!("submitting did not clear the draft");
208 }
209 if self.page() != ManagerPage::Collection {
210 bail!("submitting did not denote the collection again");
211 }
212
213 let focused = self.selected_queue_id();
214 self.set_draft(" ".to_owned());
215 self.submit_draft();
216 if self.selected_queue_id() != focused {
217 bail!("submitting a draft naming no source moved the focus");
218 }
219 Ok(())
220 }
221}
222
223#[ext(name = TextLaws)]
225pub impl<This> This
226where
227 This: NavigationStateAlg + TextEditorStateAlg,
228{
229 fn text_editor_laws(&mut self) -> Result<()> {
242 self.set_page(ManagerPage::AddSources);
244 self.set_active_text_editor("a😀b".to_owned(), "a😀".len());
245 self.move_cursor_left();
246 self.check_law_editor("a😀b", 1)?;
247 self.move_cursor_right();
248 self.check_law_editor("a😀b", "a😀".len())?;
249 self.delete_before_cursor();
250 self.check_law_editor("ab", 1)?;
251
252 self.insert_text("😀");
253 self.check_law_editor("a😀b", "a😀".len())?;
254 self.move_cursor_home();
255 self.check_law_editor("a😀b", 0)?;
256 self.delete_at_cursor();
257 self.check_law_editor("😀b", 0)?;
258 self.move_cursor_end();
259 self.check_law_editor("😀b", "😀b".len())
260 }
261
262 fn check_law_editor(&self, text: &str, cursor: usize) -> Result<()> {
268 let Some((observed, position)) = self.active_text_editor() else {
269 bail!("the editor an edit was applied to observes no text");
270 };
271 if observed != text || position != cursor {
272 bail!("editing denoted {observed:?} at {position} rather than {text:?} at {cursor}");
273 }
274 if !observed.is_char_boundary(position) {
275 bail!("the observed cursor {position} splits a Unicode scalar in {observed:?}");
276 }
277 Ok(())
278 }
279}
280
281#[ext(name = QueueLaws)]
283pub impl<This> This
284where
285 This: NavigationStateAlg
286 + QueueCatalogAlg
287 + QueueAppendAlg
288 + TransferStateAlg
289 + SourceMetadataAlg
290 + DraftStateAlg
291 + ManagerStatusAlg
292 + SourceRequestAlg
293 + SubmissionAlg
294 + MediaOptionsAlg
295 + OutputChoiceAlg,
296 This::Entry: QueueEntryAlg + TransferViewAlg,
297 This::Id: Copy + Eq + Debug,
298{
299 fn queue_laws(&mut self) -> Result<()> {
312 let ids = self.author_law_queue(2);
313 let destination = PathBuf::from("law.bin");
314
315 self.apply_transfer_event(
316 ids[0],
317 TransferObservationOp::Progress {
318 destination: destination.clone(),
319 downloaded: 4_000_000,
320 total: Some(10_000_000),
321 },
322 );
323 self.apply_transfer_event(ids[0], TransferObservationOp::Elapsed { elapsed: Duration::from_secs(2) });
324 if self.law_transferred(ids[1]) != Some(0) {
325 bail!("a keyed observation reached an identity it does not address");
326 }
327
328 let observed = self.law_entry(ids[0])?;
329 if observed.bytes_per_second() != Some(2_000_000) {
330 bail!("byte rate is not derived from observed bytes and elapsed time");
331 }
332 if observed.estimated_remaining() != Some(Duration::from_secs(3)) {
333 bail!("remaining time is not derived from observed bytes and elapsed time");
334 }
335 if observed.percent() != Some(40) {
336 bail!("the completed share is not derived from observed bytes and expected size");
337 }
338
339 self.apply_transfer_event(
340 ids[0],
341 TransferObservationOp::Progress {
342 destination: destination.clone(),
343 downloaded: 10_000_000,
344 total: Some(10_000_000),
345 },
346 );
347 let observed = self.law_entry(ids[0])?;
348 if observed.percent() != Some(100) {
349 bail!("full byte progress is not observed as a complete share");
350 }
351 if observed.transfer_complete() {
352 bail!("full byte progress denoted completion before terminal success");
353 }
354 self.apply_transfer_event(ids[0], TransferObservationOp::Completed { destination, bytes: 10_000_000 });
355 if !self.law_entry(ids[0])?.transfer_complete() {
356 bail!("terminal success did not denote completion");
357 }
358
359 if self.law_entry(ids[1])?.label() != law_source(1) {
360 bail!("the submitted source is not the label before extraction supplies a title");
361 }
362 self.apply_source_metadata(ids[1], "law".to_owned(), Some("Law Title".to_owned()));
363 if self.law_entry(ids[1])?.label() != "Law Title" {
364 bail!("an extracted title is not the observed label");
365 }
366 Ok(())
367 }
368
369 fn law_entry(&self, id: This::Id) -> Result<&This::Entry> {
375 match self.queue_entry(id) {
376 Some(entry) => Ok(entry),
377 None => bail!("the collection holds no entry for {id:?}"),
378 }
379 }
380
381 fn law_transferred(&self, id: This::Id) -> Option<u64> {
383 self.queue_entry(id).map(TransferViewAlg::transferred)
384 }
385}
386
387#[ext(name = TransitionLaws)]
389pub impl<This> This
390where
391 This: NavigationStateAlg
392 + QueueCatalogAlg
393 + QueueAppendAlg
394 + QueuePauseAlg
395 + QueueDuplicateAlg
396 + QueueOutputAlg
397 + QueueSourceAlg
398 + TransferStateAlg
399 + SourceMetadataAlg
400 + DetailSelectionAlg
401 + DraftStateAlg
402 + OutputDraftAlg
403 + InputDraftAlg
404 + ManagerStatusAlg
405 + SourceRequestAlg
406 + SubmissionAlg
407 + MediaOptionsAlg
408 + OutputChoiceAlg,
409 This::Entry: QueueEntryAlg,
410 This::Id: Copy + Eq + Debug,
411{
412 fn transition_laws(&mut self) -> Result<()> {
427 let ids = self.author_law_queue(2);
428 for (index, id) in ids.iter().copied().enumerate() {
429 self.apply_source_metadata(id, format!("law-{index}"), Some(format!("Law {index}")));
430 }
431 if self.law_space(ids[0])? != Some(SpaceAction::Start) {
432 bail!("a ready entry naming an output does not offer Start");
433 }
434 let derived = self.law_entry(ids[0])?.output().map(Path::to_owned);
435 let stem = derived.as_deref().and_then(Path::file_stem).and_then(OsStr::to_str);
436 if stem != Some("Law 0") {
437 bail!("the extracted title is not the default output stem: {stem:?}");
438 }
439
440 self.set_selected_queue_id(Some(ids[0]));
441 self.set_page(ManagerPage::Output(ids[0]));
442 self.set_output_draft("My: copy".to_owned());
443 self.submit_output();
444 let named = self.law_entry(ids[0])?;
445 let Some(output) = named.output() else {
446 bail!("applying an output-name draft named no output");
447 };
448 if output.components().count() != 1 || output.to_string_lossy().contains(':') {
449 bail!("an applied output name is not one portable file component: {}", output.display());
450 }
451
452 self.apply_selected_space();
453 if self.law_entry(ids[0])?.is_editable() {
454 bail!("first start did not fix the request options");
455 }
456 if self.law_space(ids[1])? != Some(SpaceAction::Start) {
457 bail!("scheduling one entry disturbed another entry's Space meaning");
458 }
459 if self.law_space(ids[0])?.is_some() {
460 bail!("a waiting entry offers a Space action");
461 }
462
463 self.apply_transfer_event(ids[0], TransferObservationOp::Started {});
464 if self.law_space(ids[0])?.is_some() {
465 bail!("an active transfer offers pause although no interpreter advertises it");
466 }
467 self.apply_transfer_event(ids[0], TransferObservationOp::PauseCapability { supported: true });
468 if self.law_space(ids[0])? != Some(SpaceAction::Pause) {
469 bail!("an advertised pause capability does not offer pause");
470 }
471 self.apply_selected_space();
472 if self.law_space(ids[0])? != Some(SpaceAction::Resume) {
473 bail!("a paused transfer does not offer resumption");
474 }
475 self.apply_selected_space();
476 if self.law_space(ids[0])? != Some(SpaceAction::Pause) {
477 bail!("a resumed transfer does not offer pause again");
478 }
479
480 self.apply_transfer_event(
481 ids[0],
482 TransferObservationOp::ProgramFailed {
483 summary: "no matching format".to_owned(),
484 detail: "no directly retrievable format matches the request".to_owned(),
485 },
486 );
487 if !self.law_entry(ids[0])?.detail_controls().contains(&DetailControl::Restart) {
488 bail!("a failed request offers no restart");
489 }
490 self.restart_selected();
491 if self.law_entry(ids[0])?.is_editable() {
492 bail!("restarting a failed request made it editable again");
493 }
494
495 self.duplicate_selected();
496 let Some(duplicate) = self.selected_queue_id() else {
497 bail!("duplication focused no identity");
498 };
499 if duplicate == ids[0] {
500 bail!("duplication reused the identity it duplicates");
501 }
502 let fresh = self.law_entry(duplicate)?;
503 if !fresh.is_editable() {
504 bail!("a duplicate is not editable");
505 }
506 if fresh.space_action() != Some(SpaceAction::Start) {
507 bail!("a duplicate does not start from a ready transfer state");
508 }
509 if fresh.label() != self.law_entry(ids[0])?.label() {
510 bail!("duplication did not preserve what the source denotes");
511 }
512 Ok(())
513 }
514
515 fn law_entry(&self, id: This::Id) -> Result<&This::Entry> {
521 match self.queue_entry(id) {
522 Some(entry) => Ok(entry),
523 None => bail!("the collection holds no entry for {id:?}"),
524 }
525 }
526
527 fn law_space(&self, id: This::Id) -> Result<Option<SpaceAction>> {
533 Ok(self.law_entry(id)?.space_action())
534 }
535}
536
537#[ext(name = MenuLaws)]
539pub impl<This> This
540where
541 This: NavigationStateAlg
542 + QueueCatalogAlg
543 + QueueAppendAlg
544 + QueuePauseAlg
545 + QueueDuplicateAlg
546 + TransferStateAlg
547 + SourceMetadataAlg
548 + DetailSelectionAlg
549 + DraftStateAlg
550 + OutputDraftAlg
551 + InputDraftAlg
552 + TextEditorStateAlg
553 + ManagerStatusAlg
554 + SourceRequestAlg
555 + SubmissionAlg
556 + MediaOptionsAlg
557 + OutputChoiceAlg,
558 This::Entry: QueueEntryAlg + RequestOptionsAlg,
559 This::Id: Copy + Eq + Debug,
560{
561 fn menu_laws(&mut self) -> Result<()> {
575 for (action, expected) in [
576 (ManagerAction::Space, ActionAvailability::Disabled),
577 (ManagerAction::Remove, ActionAvailability::Disabled),
578 (ManagerAction::Activate, ActionAvailability::Disabled),
579 (ManagerAction::Next, ActionAvailability::Disabled),
580 (ManagerAction::AddSources, ActionAvailability::Enabled),
581 (ManagerAction::Exit, ActionAvailability::Enabled),
582 ] {
583 if self.action_availability(action) != expected {
584 bail!("an empty collection denotes {action:?} as {expected:?} nowhere");
585 }
586 }
587
588 let ids = self.author_law_queue(1);
589 self.apply_source_metadata(ids[0], "law".to_owned(), Some("Law".to_owned()));
590 for action in [ManagerAction::Space, ManagerAction::Remove, ManagerAction::Activate] {
591 if self.action_availability(action) != ActionAvailability::Enabled {
592 bail!("a selected ready entry does not enable {action:?}");
593 }
594 }
595 if self.action_availability(ManagerAction::Next) != ActionAvailability::Disabled {
596 bail!("a collection of one entry offers cursor movement");
597 }
598 let before = self.law_space(ids[0])?;
599 self.author_law_queue(1);
600 if self.action_availability(ManagerAction::Next) != ActionAvailability::Enabled {
601 bail!("a collection of two entries does not offer cursor movement");
602 }
603
604 self.set_page(ManagerPage::AddSources);
605 self.set_draft(" ".to_owned());
606 if self.action_availability(ManagerAction::Activate) != ActionAvailability::Disabled {
607 bail!("a draft naming no source enables activation");
608 }
609 if self.action_availability(ManagerAction::Back) != ActionAvailability::Enabled {
610 bail!("a page below the collection does not enable returning to it");
611 }
612 self.set_draft(law_source(2));
613 if self.action_availability(ManagerAction::Activate) != ActionAvailability::Enabled {
614 bail!("a draft naming a source does not enable activation");
615 }
616
617 if self.action_availability(ManagerAction::Space) != ActionAvailability::Disabled {
619 bail!("the add-sources page enables Space");
620 }
621 self.set_selected_queue_id(Some(ids[0]));
622 self.apply_selected_space();
623 if self.law_space(ids[0])? != before {
624 bail!("a disabled action denoted an operation when it was applied");
625 }
626 Ok(())
627 }
628
629 fn law_space(&self, id: This::Id) -> Result<Option<SpaceAction>> {
635 match self.queue_entry(id) {
636 Some(entry) => Ok(entry.space_action()),
637 None => bail!("the collection holds no entry for {id:?}"),
638 }
639 }
640}
641
642#[ext(name = DownloadsLaws)]
644pub impl<This, Source> This
645where
646 This: ManagerSorts<Source = Source, Downloads: DownloadCollectionAlg<Source = Source>>
647 + DownloadsAlg
648 + SourceRequestAlg
649 + SubmissionAlg
650 + MediaOptionsAlg
651 + OutputChoiceAlg
652 + DownloadsLawFixture,
653{
654 fn downloads_laws(&self) -> Result<()> {
666 let empty = self.empty_downloads();
667 if !self.law_collected_sources(&empty).is_empty() {
668 bail!("the empty collection holds a source");
669 }
670 let stated = [law_source(0), law_source(1)];
671 let collected = self.progressive_downloads(stated.clone());
672 if self.law_collected_sources(&collected) != stated {
673 bail!("a collection does not hold its sources in declaration order");
674 }
675 let appended = collected.add_sources([self.source(law_source(2), self.suggested_output(), self.progressive())]);
676 if self.law_collected_sources(&appended) != [stated[0].clone(), stated[1].clone(), law_source(2)] {
677 bail!("adding did not append after what the collection already holds");
678 }
679 Ok(())
680 }
681}
682
683#[ext(name = LogLaws)]
685pub impl<This> This
686where
687 This: NavigationStateAlg
688 + QueueCatalogAlg
689 + QueueAppendAlg
690 + FormatCatalogStateAlg
691 + QueueDryRunAlg
692 + RehearsalStateAlg
693 + DownloadLogAlg
694 + DetailSelectionAlg
695 + DraftStateAlg
696 + ManagerStatusAlg
697 + SourceRequestAlg
698 + SubmissionAlg
699 + MediaOptionsAlg
700 + OutputChoiceAlg,
701 This::Entry: QueueEntryAlg,
702 This::Id: Copy + Eq + Debug,
703{
704 fn log_laws(&mut self) -> Result<()> {
717 let ids = self.author_law_queue(2);
718 let unaddressed: Vec<String> = self.law_notes(ids[1])?;
719 self.note_download(ids[0], "first note".to_owned());
720 self.note_download(ids[0], "second note".to_owned());
721
722 if self.law_notes(ids[1])? != unaddressed {
723 bail!("a note reached an identity it does not address");
724 }
725 let stated = self.law_notes(ids[0])?;
726 let tail: Vec<&String> = stated.iter().rev().take(2).collect();
727 if tail != [&"second note".to_owned(), &"first note".to_owned()] {
728 bail!("notes are not observed in the order they were stated: {stated:?}");
729 }
730
731 let controls = self.queue_entry(ids[0]).map(QueueEntryAlg::detail_controls).unwrap_or_default();
732 if !controls.contains(&DetailControl::Log) {
733 bail!("an entry offers no record among its controls: {controls:?}");
734 }
735 self.set_selected_queue_id(Some(ids[0]));
736 self.set_page(ManagerPage::Details(ids[0]));
737 self.set_selected_detail_control(Some(DetailControl::Log));
738 self.open_log();
739 if self.page() != ManagerPage::Log(ids[0]) {
740 bail!("activating the record control did not denote that record");
741 }
742 Ok(())
743 }
744
745 fn law_notes(&self, id: This::Id) -> Result<Vec<String>> {
751 match self.queue_entry(id) {
752 Some(entry) => Ok(entry.download_log().map(str::to_owned).collect()),
753 None => bail!("the collection holds no entry for {id:?}"),
754 }
755 }
756}
757
758pub trait SubmissionLawFixture {
763 fn law_folder_submission(&self) -> (String, String, String);
765
766 fn law_lone_submission(&self) -> (String, String);
768
769 fn law_media_source(&self) -> String;
771
772 fn law_unrecognized_source(&self) -> String;
774}
775
776#[ext(name = SubmissionLaws)]
778pub impl<This> This
779where
780 This: NavigationStateAlg
781 + QueueCatalogAlg
782 + QueueAppendAlg
783 + DraftStateAlg
784 + TextEditorStateAlg
785 + ManagerStatusAlg
786 + SourceRequestAlg
787 + SubmissionAlg
788 + MediaOptionsAlg
789 + OutputChoiceAlg
790 + SourceRecognitionAlg
791 + SubmissionLawFixture,
792 This::Entry: QueueEntryAlg + RequestOptionsAlg,
793 This::Id: Copy + Eq + Debug,
794{
795 fn submission_laws(&mut self) -> Result<()> {
815 let (folder, input, output) = self.law_folder_submission();
816 let media = self.law_media_source();
817 self.set_draft(format!("{folder}\n{media}"));
818 self.submit_draft();
819 let ids = self.queue_ids().collect::<Vec<_>>();
820 let [folder_id, media_id] = ids[..] else {
821 bail!("submitting two sources did not append two requests");
822 };
823
824 if self.selected_queue_id() != Some(folder_id) {
825 bail!("submitting sources did not focus the first of them");
826 }
827
828 let submitted = self.submission_law_entry(folder_id)?;
829 if submitted.source() != input {
830 bail!("a transfer does not come from the end its line named");
831 }
832 if submitted.output().map(Path::to_path_buf) != Some(PathBuf::from(&output)) {
833 bail!("a transfer does not come to rest at the end its line named");
834 }
835 if submitted.performer() != Performer::Program {
836 bail!("a transfer is not performed by naming a program");
837 }
838 if submitted.media_streams().is_some() {
839 bail!("a folder chose a media role");
840 }
841 if submitted.selectable_choices().next().is_none() {
843 bail!("a folder offers no way of transferring itself");
844 }
845 if submitted.chosen_choice().is_none() {
846 bail!("a folder transfers no stated way");
847 }
848 if submitted.selectable_choices().any(|choice| submitted.choice_summary(choice).is_none()) {
849 bail!("a way of transferring does not say what it does");
850 }
851 if submitted.dry_run() != Some(true) {
852 bail!("a folder does not begin by stating what it would do");
853 }
854 if submitted.stated_command().is_none() {
855 bail!("a folder does not state the command that would perform it");
856 }
857 if submitted.space_action() != Some(SpaceAction::Rehearse) {
858 bail!("Space does not rehearse a folder nobody has rehearsed");
859 }
860
861 self.check_unrecognized_laws()?;
862 for id in [folder_id, media_id] {
863 let submitted = self.submission_law_entry(id)?;
864 let names_a_program = submitted.performer() == Performer::Program;
867 if submitted.stated_command().is_some() != names_a_program {
868 bail!("what performs a request and whether it states a command disagree");
869 }
870 }
871
872 let submitted = self.submission_law_entry(media_id)?;
873 if submitted.performer() != Performer::Retrieval {
874 bail!("a line a source claimed is not retrieved");
875 }
876 if submitted.media_streams().is_none() {
877 bail!("a media item chose no media role");
878 }
879 if submitted.dry_run().is_some() {
880 bail!("a media item states a rehearsal mode it cannot have");
881 }
882 if submitted.chosen_choice().is_some() {
883 bail!("a media item fixed a representation nobody chose");
884 }
885 if submitted.stated_command().is_some() {
886 bail!("a media item states a command, and it is fetched rather than run");
887 }
888 Ok(())
889 }
890
891 fn check_unrecognized_laws(&mut self) -> Result<()> {
897 let line = self.law_unrecognized_source();
898 if self.recognizes_source(&line) {
899 bail!("the line authored as unrecognized is recognized by a source");
900 }
901 self.set_draft(line.clone());
902 self.submit_draft();
903 let Some(id) = self.selected_queue_id() else {
904 bail!("submitting {line} appended nothing");
905 };
906 let submitted = self.submission_law_entry(id)?;
907 if submitted.source() != line {
908 bail!("a transfer does not come from the path its line named");
909 }
910 if submitted.performer() != Performer::Program {
911 bail!("a line no source recognizes is not performed by naming a program");
912 }
913 if submitted.stated_command().is_none() {
914 bail!("a line no source recognizes states no command to perform it");
915 }
916 Ok(())
917 }
918
919 fn submission_law_entry(&self, id: This::Id) -> Result<&This::Entry> {
921 let Some(entry) = self.queue_entry(id) else {
922 bail!("the collection no longer holds {id:?}");
923 };
924 Ok(entry)
925 }
926}
927pub trait RehearsalLawFixture: ManagerSorts {
929 fn law_change(&self, path: &str, kind: ChangeKind, size: Option<u64>) -> Self::Change;
931
932 fn law_offer_rehearsal(&mut self, id: Self::Id);
934}
935
936#[ext(name = RehearsalLaws)]
938pub impl<This> This
939where
940 This: NavigationStateAlg
941 + QueueCatalogAlg
942 + QueueAppendAlg
943 + QueueDryRunAlg
944 + QueueDuplicateAlg
945 + QueueOutputAlg
946 + QueueSourceAlg
947 + RehearsalStateAlg
948 + DetailSelectionAlg
949 + DraftStateAlg
950 + ManagerStatusAlg
951 + SourceRequestAlg
952 + SubmissionAlg
953 + MediaOptionsAlg
954 + OutputChoiceAlg
955 + RehearsalLawFixture,
956 This::Entry: QueueEntryAlg + RehearsalViewAlg<Change: PlannedChangeAlg>,
957 This::Id: Copy + Eq + Debug,
958{
959 fn rehearsal_laws(&mut self) -> Result<()> {
977 let ids = self.author_law_queue(1);
978 let id = ids[0];
979 if self.rehearsal_law_entry(id)?.dry_run().is_some() {
980 bail!("a request states a rehearsal mode it was never given");
981 }
982 if self.rehearsal_law_controls(id)?.contains(&DetailControl::DryRun) {
983 bail!("a request with no rehearsal mode offers the control turning it");
984 }
985 self.law_offer_rehearsal(id);
986 self.set_queue_output(id, PathBuf::from("law-destination"));
988 if self.rehearsal_law_entry(id)?.dry_run() != Some(true) {
989 bail!("a folder request does not begin by stating what it would do");
990 }
991 if !self.rehearsal_law_controls(id)?.contains(&DetailControl::DryRun) {
992 bail!("a request with a rehearsal mode does not offer the control turning it");
993 }
994 if self.rehearsal_law_controls(id)?.contains(&DetailControl::Report) {
995 bail!("an unrehearsed request offers a report of what it would do");
996 }
997 if self.rehearsal_law_entry(id)?.space_action() != Some(SpaceAction::Rehearse) {
998 bail!("Space does not rehearse while the rehearsal mode is on");
999 }
1000
1001 let editable = self.rehearsal_law_entry(id)?.is_editable();
1002 self.apply_rehearsal_event(id, RehearsalObservationOp::Started {});
1003 if self.rehearsal_law_entry(id)?.phase() != TransferPhase::Rehearsing {
1004 bail!("a started rehearsal is not observable as one");
1005 }
1006 let changes = vec![
1007 self.law_change("kept.txt", ChangeKind::Unchanged, Some(2)),
1008 self.law_change("new.txt", ChangeKind::Create, Some(4)),
1009 ];
1010 self.apply_rehearsal_event(id, RehearsalObservationOp::Reported { changes });
1011 if self.rehearsal_law_entry(id)?.is_editable() != editable {
1012 bail!("a rehearsal changed what the request permits");
1013 }
1014 if self.rehearsal_law_entry(id)?.phase() != TransferPhase::Ready {
1015 bail!("a finished rehearsal left the request in the phase that ran it");
1016 }
1017 let reported = self
1018 .rehearsal_law_entry(id)?
1019 .planned_changes()
1020 .map(|change| (change.change_path().to_owned(), change.change_kind()))
1021 .collect::<Vec<_>>();
1022 if reported != vec![("kept.txt".to_owned(), ChangeKind::Unchanged), ("new.txt".to_owned(), ChangeKind::Create)]
1023 {
1024 bail!("a report does not state the changes the rehearsal stated: {reported:?}");
1025 }
1026 if !self.rehearsal_law_controls(id)?.contains(&DetailControl::Report) {
1027 bail!("a rehearsed request does not offer its report");
1028 }
1029
1030 self.check_open_input_laws(id)?;
1031 let rehearsed = self.rehearsal_law_entry(id)?.stated_command();
1032 self.set_queue_dry_run(id, false);
1033 let armed = self.rehearsal_law_entry(id)?.stated_command();
1034 if rehearsed.is_some() && rehearsed == armed {
1035 bail!("turning the rehearsal mode off did not change what would be run");
1036 }
1037 if self.rehearsal_law_entry(id)?.space_action() != Some(SpaceAction::Start) {
1038 bail!("Space does not transfer once the rehearsal mode is off");
1039 }
1040 if !self.rehearsal_law_controls(id)?.contains(&DetailControl::DryRun) {
1041 bail!("turning the rehearsal mode off took away the control turning it back on");
1042 }
1043 self.apply_rehearsal_event(id, RehearsalObservationOp::Failed { message: "the far end refused".to_owned() });
1044 if self.rehearsal_law_entry(id)?.space_action().is_none() {
1045 bail!("a failed rehearsal left the request with nothing Space can do");
1046 }
1047 self.check_duplicate_laws(id)
1048 }
1049
1050 fn check_duplicate_laws(&mut self, id: This::Id) -> Result<()> {
1060 let armed = self.rehearsal_law_entry(id)?;
1061 let (source, output) = (armed.source().to_owned(), armed.output().map(Path::to_path_buf));
1062 let Some(duplicate) = self.duplicate_queue_entry(id) else {
1063 bail!("a request that may be duplicated was not");
1064 };
1065 let fresh = self.rehearsal_law_entry(duplicate)?;
1066 if fresh.dry_run() != Some(true) {
1067 bail!("a duplicate of an armed request is armed itself");
1068 }
1069 if fresh.source() != source {
1070 bail!("a duplicated transfer does not come from where the first one did");
1071 }
1072 if fresh.output().map(Path::to_path_buf) != output {
1073 bail!("a duplicated transfer does not come to rest where the first one did");
1074 }
1075 Ok(())
1076 }
1077
1078 fn check_open_input_laws(&mut self, id: This::Id) -> Result<()> {
1086 let replacement = "law://replaced-input";
1087 self.set_queue_source(id, replacement.to_owned());
1088 if self.rehearsal_law_entry(id)?.source() != replacement {
1089 bail!("a rehearsed request refused to change its input");
1090 }
1091 if !self.rehearsal_law_controls(id)?.contains(&DetailControl::Input) {
1092 bail!("a request that permits its input to change does not offer to change it");
1093 }
1094 Ok(())
1095 }
1096
1097 fn rehearsal_law_entry(&self, id: This::Id) -> Result<&This::Entry> {
1099 let Some(entry) = self.queue_entry(id) else {
1100 bail!("the collection no longer holds {id:?}");
1101 };
1102 Ok(entry)
1103 }
1104
1105 fn rehearsal_law_controls(&self, id: This::Id) -> Result<Vec<DetailControl>> {
1107 Ok(self.rehearsal_law_entry(id)?.detail_controls())
1108 }
1109}
1110
1111#[ext(name = AttentionLaws)]
1113pub impl<This> This
1114where
1115 This: NavigationStateAlg
1116 + QueueCatalogAlg
1117 + QueueAppendAlg
1118 + QueueRemoveAlg
1119 + QueuePauseAlg
1120 + QueueOutputAlg
1121 + DraftStateAlg
1122 + ManagerStatusAlg
1123 + SafeExitAlg
1124 + SourceRequestAlg
1125 + SubmissionAlg
1126 + MediaOptionsAlg
1127 + OutputChoiceAlg
1128 + TransferStateAlg
1129 + RehearsalStateAlg,
1130 This::Entry: QueueEntryAlg,
1131 This::Change: PlannedChangeAlg,
1132 This::Id: Copy + Eq + Debug,
1133{
1134 fn attention_laws(&mut self) -> Result<()> {
1149 let ids = self.author_law_queue(2);
1150 let destination = PathBuf::from("law-destination");
1152 for id in &ids {
1153 self.set_queue_output(*id, destination.clone());
1154 }
1155 if !self.wanting_work().is_empty() {
1156 bail!("a request nobody asked for wants work done");
1157 }
1158 self.set_waiting(ids[0]);
1159 if self.wanting_work() != vec![ids[0]] {
1160 bail!("the requests wanting work are not the requests waiting for it");
1161 }
1162 if self.destination_is_taken(ids[1]) {
1163 bail!("a destination nobody is writing to is taken");
1164 }
1165
1166 self.begun(ids[0], true);
1167 if !self.wanting_work().is_empty() {
1168 bail!("a request whose work began is still waiting for it to begin");
1169 }
1170 if self.wanted(ids[0]) != Wanted::Running {
1171 bail!("a request being worked on does not want its run");
1172 }
1173 if !self.destination_is_taken(ids[1]) {
1174 bail!("a destination another request is writing to is free");
1175 }
1176 if self.destination_is_taken(ids[0]) {
1177 bail!("a request takes its own destination from itself");
1178 }
1179
1180 self.toggle_queue_pause(ids[0]);
1181 if self.wanted(ids[0]) != Wanted::Held {
1182 bail!("a paused request does not want its run held still");
1183 }
1184 if !self.destination_is_taken(ids[1]) {
1185 bail!("a destination a held request still holds open is free");
1186 }
1187 self.toggle_queue_pause(ids[0]);
1188 if self.wanted(ids[0]) != Wanted::Running {
1189 bail!("a resumed request does not want its run to carry on");
1190 }
1191
1192 self.remove_queue_entry(ids[0]);
1193 if self.wanted(ids[0]) != Wanted::Unwanted {
1194 bail!("a removed request still wants its run");
1195 }
1196 if self.destination_is_taken(ids[1]) {
1197 bail!("a removed request still holds its destination");
1198 }
1199
1200 self.set_waiting(ids[1]);
1201 self.request_safe_exit();
1202 if self.wanted(ids[1]) != Wanted::Unwanted {
1203 bail!("a request still wants its run once the reader is leaving");
1204 }
1205 Ok(())
1206 }
1207}
1208
1209#[ext(name = OptionsLaws)]
1211pub impl<This, Format> This
1212where
1213 This: ManagerSorts<Format = Format>
1214 + MediaSorts<Format = Format>
1215 + MetadataAlg
1216 + FormatAlg
1217 + FormatPredicateAlg
1218 + FormatPredicateMatchAlg
1219 + NavigationStateAlg
1220 + QueueCatalogAlg
1221 + QueueAppendAlg
1222 + QueueFormatEditAlg
1223 + FormatCatalogStateAlg
1224 + QueueDryRunAlg
1225 + RehearsalStateAlg
1226 + SourceMetadataAlg
1227 + TransferStateAlg
1228 + DraftStateAlg
1229 + ManagerStatusAlg
1230 + SourceRequestAlg
1231 + SubmissionAlg
1232 + MediaOptionsAlg
1233 + OutputChoiceAlg,
1234 This::Entry: QueueEntryAlg + RequestOptionsAlg,
1235 This::Id: Copy + Eq + Debug,
1236{
1237 fn options_laws(&mut self) -> Result<()> {
1252 let described = [("progressive", true, true), ("audio", true, false), ("video", false, true)]
1253 .map(|(id, audio, video)| self.law_format(id, audio, video));
1254 for (role, expected) in
1255 [(MediaStreams::AudioVideo, "progressive"), (MediaStreams::Audio, "audio"), (MediaStreams::Video, "video")]
1256 {
1257 let predicate = self.stream_role_format(role);
1258 let accepted: Vec<&str> = ["progressive", "audio", "video"]
1259 .into_iter()
1260 .zip(&described)
1261 .filter(|(_, format)| self.format_matches(&predicate, format))
1262 .map(|(id, _)| id)
1263 .collect();
1264 if accepted != [expected] {
1265 bail!("the predicate {role:?} denotes accepts {accepted:?} rather than {expected}");
1266 }
1267 }
1268
1269 let ids = self.author_law_queue(2);
1270 self.apply_format_catalog_event(
1271 ids[0],
1272 FormatDiscoveryOp::Available { formats: described.into_iter().collect() },
1273 );
1274 if self.law_selectable(ids[1])?.next().is_some() {
1275 bail!("a discovery observation reached an identity it does not address");
1276 }
1277 let discovered = ["progressive", "audio", "video"];
1278 let selectable: Vec<String> = self.law_selectable(ids[0])?.map(str::to_owned).collect();
1279 if selectable != discovered {
1280 bail!("the selectable formats are not the discovered ones: {selectable:?}");
1281 }
1282
1283 let mut walked = Vec::new();
1285 for _ in 0..MediaStreams::OFFERED.len() + discovered.len() {
1286 let entry = self.law_entry(ids[0])?;
1287 walked.push(entry.chosen_choice().map(str::to_owned));
1288 self.cycle_queue_format(ids[0], true);
1289 }
1290 let roles = walked.iter().take(MediaStreams::OFFERED.len()).filter(|chosen| chosen.is_none()).count();
1291 if roles != MediaStreams::OFFERED.len() {
1292 bail!("selection does not begin with the offered roles: {walked:?}");
1293 }
1294 let identities: Vec<String> = walked.into_iter().flatten().collect();
1295 if identities != discovered {
1296 bail!("selection does not continue through the discovered identities: {identities:?}");
1297 }
1298
1299 self.cycle_queue_format(ids[0], true);
1301 if self.law_entry(ids[0])?.chosen_choice().is_some() {
1302 bail!("preferring a role kept the identity it was meant to release");
1303 }
1304
1305 self.apply_source_metadata(ids[0], "law".to_owned(), Some("Law".to_owned()));
1306 self.set_waiting(ids[0]);
1307 let fixed = self.law_entry(ids[0])?.media_streams();
1308 self.cycle_queue_format(ids[0], true);
1309 if self.law_entry(ids[0])?.media_streams() != fixed {
1310 bail!("a started request changed the choice it was started with");
1311 }
1312 Ok(())
1313 }
1314
1315 fn law_format(&self, id: &str, audio: bool, video: bool) -> Format {
1317 self.format(
1318 id,
1319 self.metadata([
1320 (FORMAT_HAS_AUDIO.to_owned(), self.boolean_metadata(audio)),
1321 (FORMAT_HAS_VIDEO.to_owned(), self.boolean_metadata(video)),
1322 (FORMAT_EXTENSION.to_owned(), self.string_metadata("mp4")),
1323 ]),
1324 )
1325 }
1326
1327 fn law_entry(&self, id: This::Id) -> Result<&This::Entry> {
1333 match self.queue_entry(id) {
1334 Some(entry) => Ok(entry),
1335 None => bail!("the collection holds no entry for {id:?}"),
1336 }
1337 }
1338
1339 fn law_selectable(&self, id: This::Id) -> Result<impl Iterator<Item = &str>> {
1345 Ok(self.law_entry(id)?.selectable_choices())
1346 }
1347}
1348
1349#[ext(name = IntentLaws)]
1351pub impl<This> This
1352where
1353 This: NavigationStateAlg
1354 + QueueCatalogAlg
1355 + QueueAppendAlg
1356 + QueueRemoveAlg
1357 + QueuePauseAlg
1358 + QueueDuplicateAlg
1359 + QueueFormatEditAlg
1360 + FormatCatalogStateAlg
1361 + QueueDryRunAlg
1362 + RehearsalStateAlg
1363 + TransferStateAlg
1364 + DetailSelectionAlg
1365 + DraftStateAlg
1366 + OutputDraftAlg
1367 + InputDraftAlg
1368 + QueueOutputAlg
1369 + QueueSourceAlg
1370 + SourceMetadataAlg
1371 + TextEditorStateAlg
1372 + ManagerStatusAlg
1373 + SafeExitAlg
1374 + SourceRequestAlg
1375 + SubmissionAlg
1376 + MediaOptionsAlg
1377 + OutputChoiceAlg,
1378 This::Entry: QueueEntryAlg + RequestOptionsAlg,
1379 This::Id: Copy + Eq + Debug,
1380{
1381 fn intent_laws(&mut self) -> Result<()> {
1394 let ids = self.author_law_queue(3);
1395 self.apply_manager_event(ManagerIntentOp::SelectNext {});
1396 let intended = self.selected_queue_id();
1397 self.select_relative(false);
1398 self.select_relative(true);
1399 if self.selected_queue_id() != intended {
1400 bail!("the select-next intention does not denote derived collection navigation");
1401 }
1402
1403 let selected = self.selected_queue_id();
1404 self.apply_manager_event(ManagerIntentOp::OpenSelected {});
1405 if self.page() != ManagerPage::Details(ids[1]) {
1406 bail!("opening details did not expand the selected identity");
1407 }
1408 self.apply_manager_event(ManagerIntentOp::Back {});
1409 if self.page() != ManagerPage::Collection || self.selected_queue_id() != selected {
1410 bail!("expanding and returning is not the identity on page and selection");
1411 }
1412
1413 self.apply_manager_event(ManagerIntentOp::OpenAddSources {});
1414 self.apply_manager_event(ManagerIntentOp::InsertText { text: "héllo".to_owned() });
1415 self.apply_manager_event(ManagerIntentOp::MoveCursorLeft {});
1416 self.apply_manager_event(ManagerIntentOp::DeleteBeforeCursor {});
1417 if self.draft() != "hélo" {
1418 bail!("the editing intentions do not denote the derived cursor operations: {}", self.draft());
1419 }
1420
1421 self.apply_manager_event(ManagerIntentOp::Back {});
1422 let unaddressed = self.law_entry_shape(ids[2]);
1423 self.apply_manager_event(ManagerIntentOp::Transfer {
1424 id: ids[0],
1425 event: TransferObservationOp::Progress {
1426 destination: PathBuf::from("law.bin"),
1427 downloaded: 5,
1428 total: Some(10),
1429 },
1430 });
1431 if self.law_entry_shape(ids[2]) != unaddressed {
1432 bail!("an intention addressed to one identity reached another");
1433 }
1434 Ok(())
1435 }
1436
1437 fn law_entry_shape(&self, id: This::Id) -> Option<(Vec<DetailControl>, Option<SpaceAction>)> {
1439 self.queue_entry(id).map(|entry| (entry.detail_controls(), entry.space_action()))
1440 }
1441}