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| {
46 self.source(
47 law_source(index),
48 self.suggested_output(),
49 self.progressive(),
50 )
51 })
52 .collect();
53 let ids = self.append_sources(requests);
54 self.set_selected_queue_id(ids.first().copied());
57 ids
58 }
59}
60
61fn law_source(index: usize) -> String {
63 format!("law://source-{index}")
64}
65
66#[ext(name = NavigationLaws)]
68pub impl<This> This
69where
70 This: NavigationStateAlg
71 + QueueCatalogAlg
72 + QueueAppendAlg
73 + QueueRemoveAlg
74 + DraftStateAlg
75 + ManagerStatusAlg
76 + SourceRequestAlg
77 + SubmissionAlg
78 + MediaOptionsAlg
79 + OutputChoiceAlg,
80 This::Entry: QueueEntryAlg + RequestOptionsAlg,
81 This::Id: Copy + Eq + Debug,
82{
83 fn navigation_laws(&mut self) -> Result<()> {
98 let ids = self.author_law_queue(3);
99 if ids.len() != 3 {
100 bail!("authoring three sources produced {} entries", ids.len());
101 }
102 self.select_relative(true);
103 if self.selected_queue_id() != ids.get(1).copied() {
104 bail!("selecting next did not advance in collection order");
105 }
106 self.select_relative(false);
107 self.select_relative(false);
108 if self.selected_queue_id() != ids.last().copied() {
109 bail!("selecting previous did not wrap to the last identity");
110 }
111 self.select_relative(true);
112 if self.selected_queue_id() != ids.first().copied() {
113 bail!("selecting next did not wrap to the first identity");
114 }
115
116 self.set_page(ManagerPage::Details(ids[0]));
117 self.select_relative(true);
118 if self.page() != ManagerPage::Details(ids[1]) {
119 bail!("expanded details did not move with the selection");
120 }
121 if self.selected_queue_id() != Some(ids[1]) {
122 bail!("expanding details displaced the collection selection");
123 }
124
125 self.remove_selected();
126 let remaining: Vec<This::Id> = self.queue_ids().collect();
127 if remaining != [ids[0], ids[2]] {
128 bail!("removal did not preserve the surviving collection order: {remaining:?}");
129 }
130 if self.selected_queue_id() != Some(ids[2]) {
131 bail!("removing the selected entry did not choose a remaining neighbor");
132 }
133 if self.page() != ManagerPage::Collection {
134 bail!("removing the expanded entry did not denote the collection again");
135 }
136
137 let rooted = self.breadcrumbs();
138 if rooted.len() != 1 {
139 bail!("the collection rests under {} pages", rooted.len() - 1);
140 }
141 self.set_page(ManagerPage::Details(ids[2]));
142 let expanded = self.breadcrumbs();
143 if expanded.first() != rooted.first() {
144 bail!("expanded details are not rooted at the collection");
145 }
146 let named = self.queue_entry(ids[2]).map(QueueEntryAlg::label);
147 if expanded.last().map(|crumb| crumb.label.as_str()) != named {
148 bail!("the breadcrumb of an expanded entry does not name what it denotes");
149 }
150 Ok(())
151 }
152}
153
154#[ext(name = DraftLaws)]
156pub impl<This> This
157where
158 This: NavigationStateAlg
159 + QueueCatalogAlg
160 + QueueAppendAlg
161 + DraftStateAlg
162 + OutputDraftAlg
163 + InputDraftAlg
164 + ManagerStatusAlg
165 + SourceRequestAlg
166 + SubmissionAlg
167 + MediaOptionsAlg
168 + OutputChoiceAlg,
169 This::Entry: QueueEntryAlg,
170 This::Id: Copy + Eq + Debug,
171{
172 fn draft_laws(&mut self) -> Result<()> {
186 self.set_draft("first\nsecond".to_owned());
187 if self.draft() != "first\nsecond" {
188 bail!("a replaced draft is not the one observed back");
189 }
190 self.set_output_draft("name.mp4".to_owned());
191 if self.draft() != "first\nsecond" {
192 bail!("replacing the output-name draft disturbed the source draft");
193 }
194 if self.output_draft() != "name.mp4" {
195 bail!("a replaced output-name draft is not the one observed back");
196 }
197
198 self.set_draft(String::new());
199 self.submit_draft();
200 if self.queue_ids().next().is_some() {
201 bail!("submitting a draft naming no source still added an entry");
202 }
203
204 self.set_page(ManagerPage::AddSources);
205 self.set_draft(format!(" {} \n\n{}\n", law_source(0), law_source(1)));
206 self.submit_draft();
207 let submitted: Vec<String> = self
208 .queue_ids()
209 .filter_map(|id| self.queue_entry(id))
210 .map(|entry| entry.label().to_owned())
211 .collect();
212 if submitted != [law_source(0), law_source(1)] {
213 bail!("submitting did not add exactly the sources the draft names: {submitted:?}");
214 }
215 if !self.draft().is_empty() {
216 bail!("submitting did not clear the draft");
217 }
218 if self.page() != ManagerPage::Collection {
219 bail!("submitting did not denote the collection again");
220 }
221
222 let focused = self.selected_queue_id();
223 self.set_draft(" ".to_owned());
224 self.submit_draft();
225 if self.selected_queue_id() != focused {
226 bail!("submitting a draft naming no source moved the focus");
227 }
228 Ok(())
229 }
230}
231
232#[ext(name = TextLaws)]
234pub impl<This> This
235where
236 This: NavigationStateAlg + TextEditorStateAlg,
237{
238 fn text_editor_laws(&mut self) -> Result<()> {
251 self.set_page(ManagerPage::AddSources);
253 self.set_active_text_editor("a😀b".to_owned(), "a😀".len());
254 self.move_cursor_left();
255 self.check_law_editor("a😀b", 1)?;
256 self.move_cursor_right();
257 self.check_law_editor("a😀b", "a😀".len())?;
258 self.delete_before_cursor();
259 self.check_law_editor("ab", 1)?;
260
261 self.insert_text("😀");
262 self.check_law_editor("a😀b", "a😀".len())?;
263 self.move_cursor_home();
264 self.check_law_editor("a😀b", 0)?;
265 self.delete_at_cursor();
266 self.check_law_editor("😀b", 0)?;
267 self.move_cursor_end();
268 self.check_law_editor("😀b", "😀b".len())
269 }
270
271 fn check_law_editor(&self, text: &str, cursor: usize) -> Result<()> {
277 let Some((observed, position)) = self.active_text_editor() else {
278 bail!("the editor an edit was applied to observes no text");
279 };
280 if observed != text || position != cursor {
281 bail!("editing denoted {observed:?} at {position} rather than {text:?} at {cursor}");
282 }
283 if !observed.is_char_boundary(position) {
284 bail!("the observed cursor {position} splits a Unicode scalar in {observed:?}");
285 }
286 Ok(())
287 }
288}
289
290#[ext(name = QueueLaws)]
292pub impl<This> This
293where
294 This: NavigationStateAlg
295 + QueueCatalogAlg
296 + QueueAppendAlg
297 + TransferStateAlg
298 + SourceMetadataAlg
299 + DraftStateAlg
300 + ManagerStatusAlg
301 + SourceRequestAlg
302 + SubmissionAlg
303 + MediaOptionsAlg
304 + OutputChoiceAlg,
305 This::Entry: QueueEntryAlg + TransferViewAlg,
306 This::Id: Copy + Eq + Debug,
307{
308 fn queue_laws(&mut self) -> Result<()> {
321 let ids = self.author_law_queue(2);
322 let destination = PathBuf::from("law.bin");
323
324 self.apply_transfer_event(
325 ids[0],
326 TransferObservationOp::Progress {
327 destination: destination.clone(),
328 downloaded: 4_000_000,
329 total: Some(10_000_000),
330 },
331 );
332 self.apply_transfer_event(
333 ids[0],
334 TransferObservationOp::Elapsed {
335 elapsed: Duration::from_secs(2),
336 },
337 );
338 if self.law_transferred(ids[1]) != Some(0) {
339 bail!("a keyed observation reached an identity it does not address");
340 }
341
342 let observed = self.law_entry(ids[0])?;
343 if observed.bytes_per_second() != Some(2_000_000) {
344 bail!("byte rate is not derived from observed bytes and elapsed time");
345 }
346 if observed.estimated_remaining() != Some(Duration::from_secs(3)) {
347 bail!("remaining time is not derived from observed bytes and elapsed time");
348 }
349 if observed.percent() != Some(40) {
350 bail!("the completed share is not derived from observed bytes and expected size");
351 }
352
353 self.apply_transfer_event(
354 ids[0],
355 TransferObservationOp::Progress {
356 destination: destination.clone(),
357 downloaded: 10_000_000,
358 total: Some(10_000_000),
359 },
360 );
361 let observed = self.law_entry(ids[0])?;
362 if observed.percent() != Some(100) {
363 bail!("full byte progress is not observed as a complete share");
364 }
365 if observed.transfer_complete() {
366 bail!("full byte progress denoted completion before terminal success");
367 }
368 self.apply_transfer_event(
369 ids[0],
370 TransferObservationOp::Completed {
371 destination,
372 bytes: 10_000_000,
373 },
374 );
375 if !self.law_entry(ids[0])?.transfer_complete() {
376 bail!("terminal success did not denote completion");
377 }
378
379 if self.law_entry(ids[1])?.label() != law_source(1) {
380 bail!("the submitted source is not the label before extraction supplies a title");
381 }
382 self.apply_source_metadata(ids[1], "law".to_owned(), Some("Law Title".to_owned()));
383 if self.law_entry(ids[1])?.label() != "Law Title" {
384 bail!("an extracted title is not the observed label");
385 }
386 Ok(())
387 }
388
389 fn law_entry(&self, id: This::Id) -> Result<&This::Entry> {
395 match self.queue_entry(id) {
396 Some(entry) => Ok(entry),
397 None => bail!("the collection holds no entry for {id:?}"),
398 }
399 }
400
401 fn law_transferred(&self, id: This::Id) -> Option<u64> {
403 self.queue_entry(id).map(TransferViewAlg::transferred)
404 }
405}
406
407#[ext(name = TransitionLaws)]
409pub impl<This> This
410where
411 This: NavigationStateAlg
412 + QueueCatalogAlg
413 + QueueAppendAlg
414 + QueuePauseAlg
415 + QueueDuplicateAlg
416 + QueueOutputAlg
417 + QueueSourceAlg
418 + TransferStateAlg
419 + SourceMetadataAlg
420 + DetailSelectionAlg
421 + DraftStateAlg
422 + OutputDraftAlg
423 + InputDraftAlg
424 + ManagerStatusAlg
425 + SourceRequestAlg
426 + SubmissionAlg
427 + MediaOptionsAlg
428 + OutputChoiceAlg,
429 This::Entry: QueueEntryAlg,
430 This::Id: Copy + Eq + Debug,
431{
432 fn transition_laws(&mut self) -> Result<()> {
447 let ids = self.author_law_queue(2);
448 for (index, id) in ids.iter().copied().enumerate() {
449 self.apply_source_metadata(id, format!("law-{index}"), Some(format!("Law {index}")));
450 }
451 if self.law_space(ids[0])? != Some(SpaceAction::Start) {
452 bail!("a ready entry naming an output does not offer Start");
453 }
454 let derived = self.law_entry(ids[0])?.output().map(Path::to_owned);
455 let stem = derived
456 .as_deref()
457 .and_then(Path::file_stem)
458 .and_then(OsStr::to_str);
459 if stem != Some("Law 0") {
460 bail!("the extracted title is not the default output stem: {stem:?}");
461 }
462
463 self.set_selected_queue_id(Some(ids[0]));
464 self.set_page(ManagerPage::Output(ids[0]));
465 self.set_output_draft("My: copy".to_owned());
466 self.submit_output();
467 let named = self.law_entry(ids[0])?;
468 let Some(output) = named.output() else {
469 bail!("applying an output-name draft named no output");
470 };
471 if output.components().count() != 1 || output.to_string_lossy().contains(':') {
472 bail!(
473 "an applied output name is not one portable file component: {}",
474 output.display()
475 );
476 }
477
478 self.apply_selected_space();
479 if self.law_entry(ids[0])?.is_editable() {
480 bail!("first start did not fix the request options");
481 }
482 if self.law_space(ids[1])? != Some(SpaceAction::Start) {
483 bail!("scheduling one entry disturbed another entry's Space meaning");
484 }
485 if self.law_space(ids[0])?.is_some() {
486 bail!("a waiting entry offers a Space action");
487 }
488
489 self.apply_transfer_event(ids[0], TransferObservationOp::Started {});
490 if self.law_space(ids[0])?.is_some() {
491 bail!("an active transfer offers pause although no interpreter advertises it");
492 }
493 self.apply_transfer_event(
494 ids[0],
495 TransferObservationOp::PauseCapability { supported: true },
496 );
497 if self.law_space(ids[0])? != Some(SpaceAction::Pause) {
498 bail!("an advertised pause capability does not offer pause");
499 }
500 self.apply_selected_space();
501 if self.law_space(ids[0])? != Some(SpaceAction::Resume) {
502 bail!("a paused transfer does not offer resumption");
503 }
504 self.apply_selected_space();
505 if self.law_space(ids[0])? != Some(SpaceAction::Pause) {
506 bail!("a resumed transfer does not offer pause again");
507 }
508
509 self.apply_transfer_event(
510 ids[0],
511 TransferObservationOp::ProgramFailed {
512 summary: "no matching format".to_owned(),
513 detail: "no directly retrievable format matches the request".to_owned(),
514 },
515 );
516 if !self
517 .law_entry(ids[0])?
518 .detail_controls()
519 .contains(&DetailControl::Restart)
520 {
521 bail!("a failed request offers no restart");
522 }
523 self.restart_selected();
524 if self.law_entry(ids[0])?.is_editable() {
525 bail!("restarting a failed request made it editable again");
526 }
527
528 self.duplicate_selected();
529 let Some(duplicate) = self.selected_queue_id() else {
530 bail!("duplication focused no identity");
531 };
532 if duplicate == ids[0] {
533 bail!("duplication reused the identity it duplicates");
534 }
535 let fresh = self.law_entry(duplicate)?;
536 if !fresh.is_editable() {
537 bail!("a duplicate is not editable");
538 }
539 if fresh.space_action() != Some(SpaceAction::Start) {
540 bail!("a duplicate does not start from a ready transfer state");
541 }
542 if fresh.label() != self.law_entry(ids[0])?.label() {
543 bail!("duplication did not preserve what the source denotes");
544 }
545 Ok(())
546 }
547
548 fn law_entry(&self, id: This::Id) -> Result<&This::Entry> {
554 match self.queue_entry(id) {
555 Some(entry) => Ok(entry),
556 None => bail!("the collection holds no entry for {id:?}"),
557 }
558 }
559
560 fn law_space(&self, id: This::Id) -> Result<Option<SpaceAction>> {
566 Ok(self.law_entry(id)?.space_action())
567 }
568}
569
570#[ext(name = MenuLaws)]
572pub impl<This> This
573where
574 This: NavigationStateAlg
575 + QueueCatalogAlg
576 + QueueAppendAlg
577 + QueuePauseAlg
578 + QueueDuplicateAlg
579 + TransferStateAlg
580 + SourceMetadataAlg
581 + DetailSelectionAlg
582 + DraftStateAlg
583 + OutputDraftAlg
584 + InputDraftAlg
585 + TextEditorStateAlg
586 + ManagerStatusAlg
587 + SourceRequestAlg
588 + SubmissionAlg
589 + MediaOptionsAlg
590 + OutputChoiceAlg,
591 This::Entry: QueueEntryAlg + RequestOptionsAlg,
592 This::Id: Copy + Eq + Debug,
593{
594 fn menu_laws(&mut self) -> Result<()> {
608 for (action, expected) in [
609 (ManagerAction::Space, ActionAvailability::Disabled),
610 (ManagerAction::Remove, ActionAvailability::Disabled),
611 (ManagerAction::Activate, ActionAvailability::Disabled),
612 (ManagerAction::Next, ActionAvailability::Disabled),
613 (ManagerAction::AddSources, ActionAvailability::Enabled),
614 (ManagerAction::Exit, ActionAvailability::Enabled),
615 ] {
616 if self.action_availability(action) != expected {
617 bail!("an empty collection denotes {action:?} as {expected:?} nowhere");
618 }
619 }
620
621 let ids = self.author_law_queue(1);
622 self.apply_source_metadata(ids[0], "law".to_owned(), Some("Law".to_owned()));
623 for action in [
624 ManagerAction::Space,
625 ManagerAction::Remove,
626 ManagerAction::Activate,
627 ] {
628 if self.action_availability(action) != ActionAvailability::Enabled {
629 bail!("a selected ready entry does not enable {action:?}");
630 }
631 }
632 if self.action_availability(ManagerAction::Next) != ActionAvailability::Disabled {
633 bail!("a collection of one entry offers cursor movement");
634 }
635 let before = self.law_space(ids[0])?;
636 self.author_law_queue(1);
637 if self.action_availability(ManagerAction::Next) != ActionAvailability::Enabled {
638 bail!("a collection of two entries does not offer cursor movement");
639 }
640
641 self.set_page(ManagerPage::AddSources);
642 self.set_draft(" ".to_owned());
643 if self.action_availability(ManagerAction::Activate) != ActionAvailability::Disabled {
644 bail!("a draft naming no source enables activation");
645 }
646 if self.action_availability(ManagerAction::Back) != ActionAvailability::Enabled {
647 bail!("a page below the collection does not enable returning to it");
648 }
649 self.set_draft(law_source(2));
650 if self.action_availability(ManagerAction::Activate) != ActionAvailability::Enabled {
651 bail!("a draft naming a source does not enable activation");
652 }
653
654 if self.action_availability(ManagerAction::Space) != ActionAvailability::Disabled {
656 bail!("the add-sources page enables Space");
657 }
658 self.set_selected_queue_id(Some(ids[0]));
659 self.apply_selected_space();
660 if self.law_space(ids[0])? != before {
661 bail!("a disabled action denoted an operation when it was applied");
662 }
663 Ok(())
664 }
665
666 fn law_space(&self, id: This::Id) -> Result<Option<SpaceAction>> {
672 match self.queue_entry(id) {
673 Some(entry) => Ok(entry.space_action()),
674 None => bail!("the collection holds no entry for {id:?}"),
675 }
676 }
677}
678
679#[ext(name = DownloadsLaws)]
681pub impl<This, Source> This
682where
683 This: ManagerSorts<Source = Source, Downloads: DownloadCollectionAlg<Source = Source>>
684 + DownloadsAlg
685 + SourceRequestAlg
686 + SubmissionAlg
687 + MediaOptionsAlg
688 + OutputChoiceAlg
689 + DownloadsLawFixture,
690{
691 fn downloads_laws(&self) -> Result<()> {
703 let empty = self.empty_downloads();
704 if !self.law_collected_sources(&empty).is_empty() {
705 bail!("the empty collection holds a source");
706 }
707 let stated = [law_source(0), law_source(1)];
708 let collected = self.progressive_downloads(stated.clone());
709 if self.law_collected_sources(&collected) != stated {
710 bail!("a collection does not hold its sources in declaration order");
711 }
712 let appended = collected.add_sources([self.source(
713 law_source(2),
714 self.suggested_output(),
715 self.progressive(),
716 )]);
717 if self.law_collected_sources(&appended)
718 != [stated[0].clone(), stated[1].clone(), law_source(2)]
719 {
720 bail!("adding did not append after what the collection already holds");
721 }
722 Ok(())
723 }
724}
725
726#[ext(name = LogLaws)]
728pub impl<This> This
729where
730 This: NavigationStateAlg
731 + QueueCatalogAlg
732 + QueueAppendAlg
733 + FormatCatalogStateAlg
734 + QueueDryRunAlg
735 + RehearsalStateAlg
736 + DownloadLogAlg
737 + DetailSelectionAlg
738 + DraftStateAlg
739 + ManagerStatusAlg
740 + SourceRequestAlg
741 + SubmissionAlg
742 + MediaOptionsAlg
743 + OutputChoiceAlg,
744 This::Entry: QueueEntryAlg,
745 This::Id: Copy + Eq + Debug,
746{
747 fn log_laws(&mut self) -> Result<()> {
760 let ids = self.author_law_queue(2);
761 let unaddressed: Vec<String> = self.law_notes(ids[1])?;
762 self.note_download(ids[0], "first note".to_owned());
763 self.note_download(ids[0], "second note".to_owned());
764
765 if self.law_notes(ids[1])? != unaddressed {
766 bail!("a note reached an identity it does not address");
767 }
768 let stated = self.law_notes(ids[0])?;
769 let tail: Vec<&String> = stated.iter().rev().take(2).collect();
770 if tail != [&"second note".to_owned(), &"first note".to_owned()] {
771 bail!("notes are not observed in the order they were stated: {stated:?}");
772 }
773
774 let controls = self
775 .queue_entry(ids[0])
776 .map(QueueEntryAlg::detail_controls)
777 .unwrap_or_default();
778 if !controls.contains(&DetailControl::Log) {
779 bail!("an entry offers no record among its controls: {controls:?}");
780 }
781 self.set_selected_queue_id(Some(ids[0]));
782 self.set_page(ManagerPage::Details(ids[0]));
783 self.set_selected_detail_control(Some(DetailControl::Log));
784 self.open_log();
785 if self.page() != ManagerPage::Log(ids[0]) {
786 bail!("activating the record control did not denote that record");
787 }
788 Ok(())
789 }
790
791 fn law_notes(&self, id: This::Id) -> Result<Vec<String>> {
797 match self.queue_entry(id) {
798 Some(entry) => Ok(entry.download_log().map(str::to_owned).collect()),
799 None => bail!("the collection holds no entry for {id:?}"),
800 }
801 }
802}
803
804pub trait SubmissionLawFixture {
809 fn law_folder_submission(&self) -> (String, String, String);
811
812 fn law_lone_submission(&self) -> (String, String);
814
815 fn law_media_source(&self) -> String;
817
818 fn law_unrecognized_source(&self) -> String;
820}
821
822#[ext(name = SubmissionLaws)]
824pub impl<This> This
825where
826 This: NavigationStateAlg
827 + QueueCatalogAlg
828 + QueueAppendAlg
829 + DraftStateAlg
830 + TextEditorStateAlg
831 + ManagerStatusAlg
832 + SourceRequestAlg
833 + SubmissionAlg
834 + MediaOptionsAlg
835 + OutputChoiceAlg
836 + SourceRecognitionAlg
837 + SubmissionLawFixture,
838 This::Entry: QueueEntryAlg + RequestOptionsAlg,
839 This::Id: Copy + Eq + Debug,
840{
841 fn submission_laws(&mut self) -> Result<()> {
861 let (folder, input, output) = self.law_folder_submission();
862 let media = self.law_media_source();
863 self.set_draft(format!("{folder}\n{media}"));
864 self.submit_draft();
865 let ids = self.queue_ids().collect::<Vec<_>>();
866 let [folder_id, media_id] = ids[..] else {
867 bail!("submitting two sources did not append two requests");
868 };
869
870 if self.selected_queue_id() != Some(folder_id) {
871 bail!("submitting sources did not focus the first of them");
872 }
873
874 let submitted = self.submission_law_entry(folder_id)?;
875 if submitted.source() != input {
876 bail!("a transfer does not come from the end its line named");
877 }
878 if submitted.output().map(Path::to_path_buf) != Some(PathBuf::from(&output)) {
879 bail!("a transfer does not come to rest at the end its line named");
880 }
881 if submitted.performer() != Performer::Program {
882 bail!("a transfer is not performed by naming a program");
883 }
884 if submitted.media_streams().is_some() {
885 bail!("a folder chose a media role");
886 }
887 if submitted.selectable_choices().next().is_none() {
889 bail!("a folder offers no way of transferring itself");
890 }
891 if submitted.chosen_choice().is_none() {
892 bail!("a folder transfers no stated way");
893 }
894 if submitted
895 .selectable_choices()
896 .any(|choice| submitted.choice_summary(choice).is_none())
897 {
898 bail!("a way of transferring does not say what it does");
899 }
900 if submitted.dry_run() != Some(true) {
901 bail!("a folder does not begin by stating what it would do");
902 }
903 if submitted.stated_command().is_none() {
904 bail!("a folder does not state the command that would perform it");
905 }
906 if submitted.space_action() != Some(SpaceAction::Rehearse) {
907 bail!("Space does not rehearse a folder nobody has rehearsed");
908 }
909
910 self.check_unrecognized_laws()?;
911 for id in [folder_id, media_id] {
912 let submitted = self.submission_law_entry(id)?;
913 let names_a_program = submitted.performer() == Performer::Program;
916 if submitted.stated_command().is_some() != names_a_program {
917 bail!("what performs a request and whether it states a command disagree");
918 }
919 }
920
921 let submitted = self.submission_law_entry(media_id)?;
922 if submitted.performer() != Performer::Retrieval {
923 bail!("a line a source claimed is not retrieved");
924 }
925 if submitted.media_streams().is_none() {
926 bail!("a media item chose no media role");
927 }
928 if submitted.dry_run().is_some() {
929 bail!("a media item states a rehearsal mode it cannot have");
930 }
931 if submitted.chosen_choice().is_some() {
932 bail!("a media item fixed a representation nobody chose");
933 }
934 if submitted.stated_command().is_some() {
935 bail!("a media item states a command, and it is fetched rather than run");
936 }
937 Ok(())
938 }
939
940 fn check_unrecognized_laws(&mut self) -> Result<()> {
946 let line = self.law_unrecognized_source();
947 if self.recognizes_source(&line) {
948 bail!("the line authored as unrecognized is recognized by a source");
949 }
950 self.set_draft(line.clone());
951 self.submit_draft();
952 let Some(id) = self.selected_queue_id() else {
953 bail!("submitting {line} appended nothing");
954 };
955 let submitted = self.submission_law_entry(id)?;
956 if submitted.source() != line {
957 bail!("a transfer does not come from the path its line named");
958 }
959 if submitted.performer() != Performer::Program {
960 bail!("a line no source recognizes is not performed by naming a program");
961 }
962 if submitted.stated_command().is_none() {
963 bail!("a line no source recognizes states no command to perform it");
964 }
965 Ok(())
966 }
967
968 fn submission_law_entry(&self, id: This::Id) -> Result<&This::Entry> {
970 let Some(entry) = self.queue_entry(id) else {
971 bail!("the collection no longer holds {id:?}");
972 };
973 Ok(entry)
974 }
975}
976pub trait RehearsalLawFixture: ManagerSorts {
978 fn law_change(&self, path: &str, kind: ChangeKind, size: Option<u64>) -> Self::Change;
980
981 fn law_offer_rehearsal(&mut self, id: Self::Id);
983}
984
985#[ext(name = RehearsalLaws)]
987pub impl<This> This
988where
989 This: NavigationStateAlg
990 + QueueCatalogAlg
991 + QueueAppendAlg
992 + QueueDryRunAlg
993 + QueueDuplicateAlg
994 + QueueOutputAlg
995 + QueueSourceAlg
996 + RehearsalStateAlg
997 + DetailSelectionAlg
998 + DraftStateAlg
999 + ManagerStatusAlg
1000 + SourceRequestAlg
1001 + SubmissionAlg
1002 + MediaOptionsAlg
1003 + OutputChoiceAlg
1004 + RehearsalLawFixture,
1005 This::Entry: QueueEntryAlg + RehearsalViewAlg<Change: PlannedChangeAlg>,
1006 This::Id: Copy + Eq + Debug,
1007{
1008 fn rehearsal_laws(&mut self) -> Result<()> {
1026 let ids = self.author_law_queue(1);
1027 let id = ids[0];
1028 if self.rehearsal_law_entry(id)?.dry_run().is_some() {
1029 bail!("a request states a rehearsal mode it was never given");
1030 }
1031 if self
1032 .rehearsal_law_controls(id)?
1033 .contains(&DetailControl::DryRun)
1034 {
1035 bail!("a request with no rehearsal mode offers the control turning it");
1036 }
1037 self.law_offer_rehearsal(id);
1038 self.set_queue_output(id, PathBuf::from("law-destination"));
1040 if self.rehearsal_law_entry(id)?.dry_run() != Some(true) {
1041 bail!("a folder request does not begin by stating what it would do");
1042 }
1043 if !self
1044 .rehearsal_law_controls(id)?
1045 .contains(&DetailControl::DryRun)
1046 {
1047 bail!("a request with a rehearsal mode does not offer the control turning it");
1048 }
1049 if self
1050 .rehearsal_law_controls(id)?
1051 .contains(&DetailControl::Report)
1052 {
1053 bail!("an unrehearsed request offers a report of what it would do");
1054 }
1055 if self.rehearsal_law_entry(id)?.space_action() != Some(SpaceAction::Rehearse) {
1056 bail!("Space does not rehearse while the rehearsal mode is on");
1057 }
1058
1059 let editable = self.rehearsal_law_entry(id)?.is_editable();
1060 self.apply_rehearsal_event(id, RehearsalObservationOp::Started {});
1061 if self.rehearsal_law_entry(id)?.phase() != TransferPhase::Rehearsing {
1062 bail!("a started rehearsal is not observable as one");
1063 }
1064 let changes = vec![
1065 self.law_change("kept.txt", ChangeKind::Unchanged, Some(2)),
1066 self.law_change("new.txt", ChangeKind::Create, Some(4)),
1067 ];
1068 self.apply_rehearsal_event(id, RehearsalObservationOp::Reported { changes });
1069 if self.rehearsal_law_entry(id)?.is_editable() != editable {
1070 bail!("a rehearsal changed what the request permits");
1071 }
1072 if self.rehearsal_law_entry(id)?.phase() != TransferPhase::Ready {
1073 bail!("a finished rehearsal left the request in the phase that ran it");
1074 }
1075 let reported = self
1076 .rehearsal_law_entry(id)?
1077 .planned_changes()
1078 .map(|change| (change.change_path().to_owned(), change.change_kind()))
1079 .collect::<Vec<_>>();
1080 if reported
1081 != vec![
1082 ("kept.txt".to_owned(), ChangeKind::Unchanged),
1083 ("new.txt".to_owned(), ChangeKind::Create),
1084 ]
1085 {
1086 bail!("a report does not state the changes the rehearsal stated: {reported:?}");
1087 }
1088 if !self
1089 .rehearsal_law_controls(id)?
1090 .contains(&DetailControl::Report)
1091 {
1092 bail!("a rehearsed request does not offer its report");
1093 }
1094
1095 self.check_open_input_laws(id)?;
1096 let rehearsed = self.rehearsal_law_entry(id)?.stated_command();
1097 self.set_queue_dry_run(id, false);
1098 let armed = self.rehearsal_law_entry(id)?.stated_command();
1099 if rehearsed.is_some() && rehearsed == armed {
1100 bail!("turning the rehearsal mode off did not change what would be run");
1101 }
1102 if self.rehearsal_law_entry(id)?.space_action() != Some(SpaceAction::Start) {
1103 bail!("Space does not transfer once the rehearsal mode is off");
1104 }
1105 if !self
1106 .rehearsal_law_controls(id)?
1107 .contains(&DetailControl::DryRun)
1108 {
1109 bail!("turning the rehearsal mode off took away the control turning it back on");
1110 }
1111 self.apply_rehearsal_event(
1112 id,
1113 RehearsalObservationOp::Failed {
1114 message: "the far end refused".to_owned(),
1115 },
1116 );
1117 if self.rehearsal_law_entry(id)?.space_action().is_none() {
1118 bail!("a failed rehearsal left the request with nothing Space can do");
1119 }
1120 self.check_duplicate_laws(id)
1121 }
1122
1123 fn check_duplicate_laws(&mut self, id: This::Id) -> Result<()> {
1133 let armed = self.rehearsal_law_entry(id)?;
1134 let (source, output) = (
1135 armed.source().to_owned(),
1136 armed.output().map(Path::to_path_buf),
1137 );
1138 let Some(duplicate) = self.duplicate_queue_entry(id) else {
1139 bail!("a request that may be duplicated was not");
1140 };
1141 let fresh = self.rehearsal_law_entry(duplicate)?;
1142 if fresh.dry_run() != Some(true) {
1143 bail!("a duplicate of an armed request is armed itself");
1144 }
1145 if fresh.source() != source {
1146 bail!("a duplicated transfer does not come from where the first one did");
1147 }
1148 if fresh.output().map(Path::to_path_buf) != output {
1149 bail!("a duplicated transfer does not come to rest where the first one did");
1150 }
1151 Ok(())
1152 }
1153
1154 fn check_open_input_laws(&mut self, id: This::Id) -> Result<()> {
1162 let replacement = "law://replaced-input";
1163 self.set_queue_source(id, replacement.to_owned());
1164 if self.rehearsal_law_entry(id)?.source() != replacement {
1165 bail!("a rehearsed request refused to change its input");
1166 }
1167 if !self
1168 .rehearsal_law_controls(id)?
1169 .contains(&DetailControl::Input)
1170 {
1171 bail!("a request that permits its input to change does not offer to change it");
1172 }
1173 Ok(())
1174 }
1175
1176 fn rehearsal_law_entry(&self, id: This::Id) -> Result<&This::Entry> {
1178 let Some(entry) = self.queue_entry(id) else {
1179 bail!("the collection no longer holds {id:?}");
1180 };
1181 Ok(entry)
1182 }
1183
1184 fn rehearsal_law_controls(&self, id: This::Id) -> Result<Vec<DetailControl>> {
1186 Ok(self.rehearsal_law_entry(id)?.detail_controls())
1187 }
1188}
1189
1190#[ext(name = AttentionLaws)]
1192pub impl<This> This
1193where
1194 This: NavigationStateAlg
1195 + QueueCatalogAlg
1196 + QueueAppendAlg
1197 + QueueRemoveAlg
1198 + QueuePauseAlg
1199 + QueueOutputAlg
1200 + DraftStateAlg
1201 + ManagerStatusAlg
1202 + SafeExitAlg
1203 + SourceRequestAlg
1204 + SubmissionAlg
1205 + MediaOptionsAlg
1206 + OutputChoiceAlg
1207 + TransferStateAlg
1208 + RehearsalStateAlg,
1209 This::Entry: QueueEntryAlg,
1210 This::Change: PlannedChangeAlg,
1211 This::Id: Copy + Eq + Debug,
1212{
1213 fn attention_laws(&mut self) -> Result<()> {
1228 let ids = self.author_law_queue(2);
1229 let destination = PathBuf::from("law-destination");
1231 for id in &ids {
1232 self.set_queue_output(*id, destination.clone());
1233 }
1234 if !self.wanting_work().is_empty() {
1235 bail!("a request nobody asked for wants work done");
1236 }
1237 self.set_waiting(ids[0]);
1238 if self.wanting_work() != vec![ids[0]] {
1239 bail!("the requests wanting work are not the requests waiting for it");
1240 }
1241 if self.destination_is_taken(ids[1]) {
1242 bail!("a destination nobody is writing to is taken");
1243 }
1244
1245 self.begun(ids[0], true);
1246 if !self.wanting_work().is_empty() {
1247 bail!("a request whose work began is still waiting for it to begin");
1248 }
1249 if self.wanted(ids[0]) != Wanted::Running {
1250 bail!("a request being worked on does not want its run");
1251 }
1252 if !self.destination_is_taken(ids[1]) {
1253 bail!("a destination another request is writing to is free");
1254 }
1255 if self.destination_is_taken(ids[0]) {
1256 bail!("a request takes its own destination from itself");
1257 }
1258
1259 self.toggle_queue_pause(ids[0]);
1260 if self.wanted(ids[0]) != Wanted::Held {
1261 bail!("a paused request does not want its run held still");
1262 }
1263 if !self.destination_is_taken(ids[1]) {
1264 bail!("a destination a held request still holds open is free");
1265 }
1266 self.toggle_queue_pause(ids[0]);
1267 if self.wanted(ids[0]) != Wanted::Running {
1268 bail!("a resumed request does not want its run to carry on");
1269 }
1270
1271 self.remove_queue_entry(ids[0]);
1272 if self.wanted(ids[0]) != Wanted::Unwanted {
1273 bail!("a removed request still wants its run");
1274 }
1275 if self.destination_is_taken(ids[1]) {
1276 bail!("a removed request still holds its destination");
1277 }
1278
1279 self.set_waiting(ids[1]);
1280 self.request_safe_exit();
1281 if self.wanted(ids[1]) != Wanted::Unwanted {
1282 bail!("a request still wants its run once the reader is leaving");
1283 }
1284 Ok(())
1285 }
1286}
1287
1288#[ext(name = OptionsLaws)]
1290pub impl<This, Format> This
1291where
1292 This: ManagerSorts<Format = Format>
1293 + MediaSorts<Format = Format>
1294 + MetadataAlg
1295 + FormatAlg
1296 + FormatPredicateAlg
1297 + FormatPredicateMatchAlg
1298 + NavigationStateAlg
1299 + QueueCatalogAlg
1300 + QueueAppendAlg
1301 + QueueFormatEditAlg
1302 + FormatCatalogStateAlg
1303 + QueueDryRunAlg
1304 + RehearsalStateAlg
1305 + SourceMetadataAlg
1306 + TransferStateAlg
1307 + DraftStateAlg
1308 + ManagerStatusAlg
1309 + SourceRequestAlg
1310 + SubmissionAlg
1311 + MediaOptionsAlg
1312 + OutputChoiceAlg,
1313 This::Entry: QueueEntryAlg + RequestOptionsAlg,
1314 This::Id: Copy + Eq + Debug,
1315{
1316 fn options_laws(&mut self) -> Result<()> {
1331 let described = [
1332 ("progressive", true, true),
1333 ("audio", true, false),
1334 ("video", false, true),
1335 ]
1336 .map(|(id, audio, video)| self.law_format(id, audio, video));
1337 for (role, expected) in [
1338 (MediaStreams::AudioVideo, "progressive"),
1339 (MediaStreams::Audio, "audio"),
1340 (MediaStreams::Video, "video"),
1341 ] {
1342 let predicate = self.stream_role_format(role);
1343 let accepted: Vec<&str> = ["progressive", "audio", "video"]
1344 .into_iter()
1345 .zip(&described)
1346 .filter(|(_, format)| self.format_matches(&predicate, format))
1347 .map(|(id, _)| id)
1348 .collect();
1349 if accepted != [expected] {
1350 bail!("the predicate {role:?} denotes accepts {accepted:?} rather than {expected}");
1351 }
1352 }
1353
1354 let ids = self.author_law_queue(2);
1355 self.apply_format_catalog_event(
1356 ids[0],
1357 FormatDiscoveryOp::Available {
1358 formats: described.into_iter().collect(),
1359 },
1360 );
1361 if self.law_selectable(ids[1])?.next().is_some() {
1362 bail!("a discovery observation reached an identity it does not address");
1363 }
1364 let discovered = ["progressive", "audio", "video"];
1365 let selectable: Vec<String> = self.law_selectable(ids[0])?.map(str::to_owned).collect();
1366 if selectable != discovered {
1367 bail!("the selectable formats are not the discovered ones: {selectable:?}");
1368 }
1369
1370 let mut walked = Vec::new();
1372 for _ in 0..MediaStreams::OFFERED.len() + discovered.len() {
1373 let entry = self.law_entry(ids[0])?;
1374 walked.push(entry.chosen_choice().map(str::to_owned));
1375 self.cycle_queue_format(ids[0], true);
1376 }
1377 let roles = walked
1378 .iter()
1379 .take(MediaStreams::OFFERED.len())
1380 .filter(|chosen| chosen.is_none())
1381 .count();
1382 if roles != MediaStreams::OFFERED.len() {
1383 bail!("selection does not begin with the offered roles: {walked:?}");
1384 }
1385 let identities: Vec<String> = walked.into_iter().flatten().collect();
1386 if identities != discovered {
1387 bail!("selection does not continue through the discovered identities: {identities:?}");
1388 }
1389
1390 self.cycle_queue_format(ids[0], true);
1392 if self.law_entry(ids[0])?.chosen_choice().is_some() {
1393 bail!("preferring a role kept the identity it was meant to release");
1394 }
1395
1396 self.apply_source_metadata(ids[0], "law".to_owned(), Some("Law".to_owned()));
1397 self.set_waiting(ids[0]);
1398 let fixed = self.law_entry(ids[0])?.media_streams();
1399 self.cycle_queue_format(ids[0], true);
1400 if self.law_entry(ids[0])?.media_streams() != fixed {
1401 bail!("a started request changed the choice it was started with");
1402 }
1403 Ok(())
1404 }
1405
1406 fn law_format(&self, id: &str, audio: bool, video: bool) -> Format {
1408 self.format(
1409 id,
1410 self.metadata([
1411 (FORMAT_HAS_AUDIO.to_owned(), self.boolean_metadata(audio)),
1412 (FORMAT_HAS_VIDEO.to_owned(), self.boolean_metadata(video)),
1413 (FORMAT_EXTENSION.to_owned(), self.string_metadata("mp4")),
1414 ]),
1415 )
1416 }
1417
1418 fn law_entry(&self, id: This::Id) -> Result<&This::Entry> {
1424 match self.queue_entry(id) {
1425 Some(entry) => Ok(entry),
1426 None => bail!("the collection holds no entry for {id:?}"),
1427 }
1428 }
1429
1430 fn law_selectable(&self, id: This::Id) -> Result<impl Iterator<Item = &str>> {
1436 Ok(self.law_entry(id)?.selectable_choices())
1437 }
1438}
1439
1440#[ext(name = IntentLaws)]
1442pub impl<This> This
1443where
1444 This: NavigationStateAlg
1445 + QueueCatalogAlg
1446 + QueueAppendAlg
1447 + QueueRemoveAlg
1448 + QueuePauseAlg
1449 + QueueDuplicateAlg
1450 + QueueFormatEditAlg
1451 + FormatCatalogStateAlg
1452 + QueueDryRunAlg
1453 + RehearsalStateAlg
1454 + TransferStateAlg
1455 + DetailSelectionAlg
1456 + DraftStateAlg
1457 + OutputDraftAlg
1458 + InputDraftAlg
1459 + QueueOutputAlg
1460 + QueueSourceAlg
1461 + SourceMetadataAlg
1462 + TextEditorStateAlg
1463 + ManagerStatusAlg
1464 + SafeExitAlg
1465 + SourceRequestAlg
1466 + SubmissionAlg
1467 + MediaOptionsAlg
1468 + OutputChoiceAlg,
1469 This::Entry: QueueEntryAlg + RequestOptionsAlg,
1470 This::Id: Copy + Eq + Debug,
1471{
1472 fn intent_laws(&mut self) -> Result<()> {
1485 let ids = self.author_law_queue(3);
1486 self.apply_manager_event(ManagerIntentOp::SelectNext {});
1487 let intended = self.selected_queue_id();
1488 self.select_relative(false);
1489 self.select_relative(true);
1490 if self.selected_queue_id() != intended {
1491 bail!("the select-next intention does not denote derived collection navigation");
1492 }
1493
1494 let selected = self.selected_queue_id();
1495 self.apply_manager_event(ManagerIntentOp::OpenSelected {});
1496 if self.page() != ManagerPage::Details(ids[1]) {
1497 bail!("opening details did not expand the selected identity");
1498 }
1499 self.apply_manager_event(ManagerIntentOp::Back {});
1500 if self.page() != ManagerPage::Collection || self.selected_queue_id() != selected {
1501 bail!("expanding and returning is not the identity on page and selection");
1502 }
1503
1504 self.apply_manager_event(ManagerIntentOp::OpenAddSources {});
1505 self.apply_manager_event(ManagerIntentOp::InsertText {
1506 text: "héllo".to_owned(),
1507 });
1508 self.apply_manager_event(ManagerIntentOp::MoveCursorLeft {});
1509 self.apply_manager_event(ManagerIntentOp::DeleteBeforeCursor {});
1510 if self.draft() != "hélo" {
1511 bail!(
1512 "the editing intentions do not denote the derived cursor operations: {}",
1513 self.draft()
1514 );
1515 }
1516
1517 self.apply_manager_event(ManagerIntentOp::Back {});
1518 let unaddressed = self.law_entry_shape(ids[2]);
1519 self.apply_manager_event(ManagerIntentOp::Transfer {
1520 id: ids[0],
1521 event: TransferObservationOp::Progress {
1522 destination: PathBuf::from("law.bin"),
1523 downloaded: 5,
1524 total: Some(10),
1525 },
1526 });
1527 if self.law_entry_shape(ids[2]) != unaddressed {
1528 bail!("an intention addressed to one identity reached another");
1529 }
1530 Ok(())
1531 }
1532
1533 fn law_entry_shape(&self, id: This::Id) -> Option<(Vec<DetailControl>, Option<SpaceAction>)> {
1535 self.queue_entry(id)
1536 .map(|entry| (entry.detail_controls(), entry.space_action()))
1537 }
1538}