Skip to main content

rsynko_manager/
laws.rs

1//! Law scenarios for the manager, stated once over the capabilities.
2//!
3//! A scenario authors its own collection through the capabilities it is bound to, so it constrains
4//! any interpreter of the manager sorts and a runner names it without supplying anything. One
5//! bundle states one module's meaning, and its bounds name exactly the capabilities that meaning
6//! is derived from.
7
8use 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
18/// Supplies the collection observation a downloads scenario cannot author for itself.
19pub trait DownloadsLawFixture: ManagerSorts {
20    /// Observes the inputs one downloads collection holds, in collection order.
21    fn law_collected_sources(&self, downloads: &Self::Downloads) -> Vec<String>;
22}
23
24/// Authors the collection every stateful manager scenario starts from.
25#[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    /// Authors the stated number of media requests and observes the identities they were given.
40    ///
41    /// The requests are stated rather than submitted: what a line reads as is what one
42    /// interpreter recognizes, and a scenario about the collection is not about that.
43    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        // Where the cursor rests after adding is what submitting states; a scenario about
55        // anything else starts from the collection already focused.
56        self.set_selected_queue_id(ids.first().copied());
57        ids
58    }
59}
60
61/// Names the source one authored entry submits.
62fn law_source(index: usize) -> String {
63    format!("law://source-{index}")
64}
65
66/// Authors the collection-navigation laws.
67#[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    /// Checks that selection, expansion, and removal preserve stable queue identity.
84    ///
85    /// The laws checked are:
86    ///
87    /// 1. adding sources focuses the first new identity;
88    /// 2. selection advances in collection order and wraps at both ends;
89    /// 3. expanded details name the selected identity and move with it;
90    /// 4. removing the selected entry chooses a remaining neighbor;
91    /// 5. removing an expanded entry denotes the collection again;
92    /// 6. breadcrumbs are rooted at the collection and name the expanded identity.
93    ///
94    /// # Errors
95    ///
96    /// Returns the first violated law.
97    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/// Authors the editor-draft laws.
155#[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    /// Checks that a draft is observed back and submits exactly the sources it names.
173    ///
174    /// The laws checked are:
175    ///
176    /// 1. a draft is observed back exactly as it was replaced;
177    /// 2. the source draft and the output-name draft are independent;
178    /// 3. submitting adds the non-blank trimmed lines in draft order and nothing else;
179    /// 4. submitting clears the draft and denotes the collection;
180    /// 5. submitting a draft naming no source adds nothing and moves no focus.
181    ///
182    /// # Errors
183    ///
184    /// Returns the first violated law.
185    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/// Authors the text-editing laws.
233#[ext(name = TextLaws)]
234pub impl<This> This
235where
236    This: NavigationStateAlg + TextEditorStateAlg,
237{
238    /// Checks that editing moves by Unicode scalar and never splits one.
239    ///
240    /// The laws checked are:
241    ///
242    /// 1. the cursor moves by whole scalars in both directions;
243    /// 2. insertion advances past exactly the inserted bytes;
244    /// 3. deleting before and at the cursor removes one whole scalar;
245    /// 4. every observed cursor rests on a UTF-8 boundary.
246    ///
247    /// # Errors
248    ///
249    /// Returns the first violated law.
250    fn text_editor_laws(&mut self) -> Result<()> {
251        // An editor exists on the page that offers one, so the scenario opens one first.
252        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    /// Checks the observed text and cursor against the meaning one edit denotes.
272    ///
273    /// # Errors
274    ///
275    /// Returns the violated law.
276    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/// Authors the keyed-transfer laws.
291#[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    /// Checks that observations are keyed and that derived progress follows only from them.
309    ///
310    /// The laws checked are:
311    ///
312    /// 1. an observation updates exactly the identity it addresses;
313    /// 2. byte rate, remaining time, and completed share are derived from bytes and elapsed time;
314    /// 3. full byte progress denotes publication, while only terminal success denotes completion;
315    /// 4. the submitted source is the label until extraction supplies a title.
316    ///
317    /// # Errors
318    ///
319    /// Returns the first violated law.
320    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    /// Observes one entry by stable identity.
390    ///
391    /// # Errors
392    ///
393    /// Returns a violation when the collection holds no such identity.
394    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    /// Observes the bytes one identity has retrieved, when it names an entry.
402    fn law_transferred(&self, id: This::Id) -> Option<u64> {
403        self.queue_entry(id).map(TransferViewAlg::transferred)
404    }
405}
406
407/// Authors the derived-transition laws.
408#[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    /// Checks that Space, restart, duplication, and output editing denote their transitions.
433    ///
434    /// The laws checked are:
435    ///
436    /// 1. Space schedules exactly the selected ready entry and fixes its request;
437    /// 2. pause is offered only while the active interpreter advertises that capability;
438    /// 3. a failed request offers restart and restarts without becoming editable again;
439    /// 4. duplication creates a fresh editable identity with its own transfer state;
440    /// 5. an extracted title is the default output stem;
441    /// 6. an output name is editable before first start and normalized when applied.
442    ///
443    /// # Errors
444    ///
445    /// Returns the first violated law.
446    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    /// Observes one entry by stable identity.
549    ///
550    /// # Errors
551    ///
552    /// Returns a violation when the collection holds no such identity.
553    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    /// Observes the Space meaning one identity currently offers.
561    ///
562    /// # Errors
563    ///
564    /// Returns a violation when the collection holds no such identity.
565    fn law_space(&self, id: This::Id) -> Result<Option<SpaceAction>> {
566        Ok(self.law_entry(id)?.space_action())
567    }
568}
569
570/// Authors the menu-availability laws.
571#[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    /// Checks that availability is derived from page, selection, and queue observations.
595    ///
596    /// The laws checked are:
597    ///
598    /// 1. an empty collection enables only the actions that still have meaning;
599    /// 2. a selected ready entry enables Space, Remove, and activation;
600    /// 3. cursor movement is enabled exactly when the page holds another value to move to;
601    /// 4. activation on the add-sources page is enabled exactly when the draft names a source;
602    /// 5. a disabled action denotes no operation, so applying it changes nothing observable.
603    ///
604    /// # Errors
605    ///
606    /// Returns the first violated law.
607    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        // Space has no meaning away from the collection, so applying it must deny that meaning.
655        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    /// Observes one entry's Space meaning by stable identity.
667    ///
668    /// # Errors
669    ///
670    /// Returns a violation when the collection holds no such identity.
671    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/// Authors the downloads-collection laws.
680#[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    /// Checks that a collection holds exactly the sources it was given, in the order given.
692    ///
693    /// The laws checked are:
694    ///
695    /// 1. the empty collection holds nothing;
696    /// 2. a collection holds its sources in declaration order;
697    /// 3. adding appends after what the collection already holds.
698    ///
699    /// # Errors
700    ///
701    /// Returns the first violated law.
702    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/// Authors the download-record laws.
727#[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    /// Checks that the record is keyed, ordered, and reachable from the entry it describes.
748    ///
749    /// The laws checked are:
750    ///
751    /// 1. a note reaches exactly the identity it addresses;
752    /// 2. notes are observed in the order they were stated;
753    /// 3. every entry offers its record among its details controls;
754    /// 4. activating that control denotes the record of that identity.
755    ///
756    /// # Errors
757    ///
758    /// Returns the first violated law.
759    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    /// Observes the notes stated about one identity.
792    ///
793    /// # Errors
794    ///
795    /// Returns a violation when the collection holds no such identity.
796    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
804/// Supplies the sources a submission scenario cannot author for itself.
805///
806/// Which texts name a folder and which name a media item is what an interpreter recognizes; that
807/// there are two kinds, and what each becomes, is what this specification states.
808pub trait SubmissionLawFixture {
809    /// Names one line this interpreter reads as a transfer, and the two ends it names.
810    fn law_folder_submission(&self) -> (String, String, String);
811
812    /// Names one line naming where a transfer comes from, and where it should come to rest.
813    fn law_lone_submission(&self) -> (String, String);
814
815    /// Names one line this interpreter reads as a media item.
816    fn law_media_source(&self) -> String;
817
818    /// Names one line no source recognizes, which is therefore one path to transfer.
819    fn law_unrecognized_source(&self) -> String;
820}
821
822/// Authors the submission laws.
823#[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    /// Checks that what a source names decides what transferring it means.
842    ///
843    /// The laws checked are:
844    ///
845    /// 1. a line naming two ends states one request from the first end into the second;
846    /// 2. a folder chooses no media role, and a media item does;
847    /// 3. a folder offers the ways it may be transferred, and always transfers one of them;
848    /// 4. a folder states a rehearsal mode, begins with it on, and Space rehearses it;
849    /// 5. a media item states no rehearsal mode, and Space starts it once it names an output;
850    /// 6. a request performed by naming a program states that command, and one fetched does not;
851    /// 7. a transfer names where it would come to rest without anything being extracted first,
852    ///    and names it after what is being transferred rather than after where anybody stands;
853    /// 8. submitting sources focuses the first of them;
854    /// 9. a line no source recognizes is transferred, whatever else it looks like;
855    /// 10. exactly the requests performed by naming a program state the command they run.
856    ///
857    /// # Errors
858    ///
859    /// Returns the first violated law.
860    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        // A folder chooses no representation, and chooses one way of transferring instead.
888        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            // What performs a request decides whether there is a command to read: a program is
914            // named and read back, and a retrieval is not named at all.
915            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    /// Checks that a line nobody claimed is a path rather than something to fetch.
941    ///
942    /// # Errors
943    ///
944    /// Returns the first violated law.
945    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    /// Observes the entry one identity denotes, or that the collection no longer holds it.
969    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}
976/// Supplies the changes a rehearsal scenario cannot author for itself.
977pub trait RehearsalLawFixture: ManagerSorts {
978    /// Defines one change a rehearsal would make to a path.
979    fn law_change(&self, path: &str, kind: ChangeKind, size: Option<u64>) -> Self::Change;
980
981    /// Turns the rehearsal mode on for one request, the way a folder source would.
982    fn law_offer_rehearsal(&mut self, id: Self::Id);
983}
984
985/// Authors the rehearsal laws.
986#[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    /// Checks that stating what a transfer would do is not doing it.
1009    ///
1010    /// The laws checked are:
1011    ///
1012    /// 1. a request states a rehearsal mode exactly when it offers the control turning it;
1013    /// 2. the mode decides what Space means, and nothing else about the request;
1014    /// 3. a rehearsal leaves the request exactly as editable as it found it;
1015    /// 4. a reported rehearsal states every change it was given, and offers its report;
1016    /// 5. a rehearsed request still permits its input to change, and states that it does;
1017    /// 6. changing what would be run forgets the report describing what would have been;
1018    /// 7. the mode changes what the request states would be run;
1019    /// 8. a failed rehearsal is not a failed request: Space still means something afterward;
1020    /// 9. a duplicate keeps both ends and is not armed, however armed the first one was.
1021    ///
1022    /// # Errors
1023    ///
1024    /// Returns the first violated law.
1025    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        // A folder transfer names the folder it would write into, the same as any other request.
1039        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    /// Checks that a request duplicated from an armed one is not itself armed.
1124    ///
1125    /// Duplicating states a fresh request, and a fresh request states what it would do before it
1126    /// does it, however it came to exist. A transfer is the pair of ends it names, so both are
1127    /// kept: a duplicate nobody can run without naming them again is not a duplicate.
1128    ///
1129    /// # Errors
1130    ///
1131    /// Returns the first violated law.
1132    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    /// Checks that a rehearsed request still permits its input to change.
1155    ///
1156    /// A rehearsal is not a start, so everything the request states is still open afterward.
1157    ///
1158    /// # Errors
1159    ///
1160    /// Returns the first violated law.
1161    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    /// Observes the entry one identity denotes, or that the collection no longer holds it.
1177    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    /// Observes the controls one identity offers.
1185    fn rehearsal_law_controls(&self, id: This::Id) -> Result<Vec<DetailControl>> {
1186        Ok(self.rehearsal_law_entry(id)?.detail_controls())
1187    }
1188}
1189
1190/// Authors the attendance laws.
1191#[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    /// Checks what the collection states to whoever is doing the work it asks for.
1214    ///
1215    /// The laws checked are:
1216    ///
1217    /// 1. exactly the requests waiting for an interpreter want work done;
1218    /// 2. a request whose work has begun is no longer waiting for it;
1219    /// 3. a request being worked on wants its run, a paused one wants it held, and a removed one
1220    ///    wants none;
1221    /// 4. once the reader is leaving, no request wants its run;
1222    /// 5. a destination is taken exactly while another request is writing to it.
1223    ///
1224    /// # Errors
1225    ///
1226    /// Returns the first violated law.
1227    fn attention_laws(&mut self) -> Result<()> {
1228        let ids = self.author_law_queue(2);
1229        // Both name one destination, which is the situation the collection has to state.
1230        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/// Authors the request-options laws.
1289#[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    /// Checks that one selector states the preferred roles and every discovered identity.
1317    ///
1318    /// The laws checked are:
1319    ///
1320    /// 1. the predicate one role denotes accepts exactly the formats carrying those streams;
1321    /// 2. a discovery observation reaches exactly the identity it addresses;
1322    /// 3. every discovered format is selectable, in discovery order;
1323    /// 4. selection walks the offered roles first and the discovered identities after;
1324    /// 5. preferring a role releases any identity, and choosing an identity fixes exactly it;
1325    /// 6. a request that is no longer editable keeps the choice it was started with.
1326    ///
1327    /// # Errors
1328    ///
1329    /// Returns the first violated law.
1330    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        // Selection walks one list: every offered role, then every discovered identity.
1371        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        // Stepping back onto a role releases whatever identity was fixed.
1391        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    /// Defines one described format carrying the stated streams.
1407    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    /// Observes one entry by stable identity.
1419    ///
1420    /// # Errors
1421    ///
1422    /// Returns a violation when the collection holds no such identity.
1423    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    /// Observes the identities one entry currently offers.
1431    ///
1432    /// # Errors
1433    ///
1434    /// Returns a violation when the collection holds no such identity.
1435    fn law_selectable(&self, id: This::Id) -> Result<impl Iterator<Item = &str>> {
1436        Ok(self.law_entry(id)?.selectable_choices())
1437    }
1438}
1439
1440/// Authors the intent-stream laws.
1441#[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    /// Checks that each intention denotes exactly the meaning derived for it.
1473    ///
1474    /// The laws checked are:
1475    ///
1476    /// 1. the selection intentions denote derived collection navigation;
1477    /// 2. expanding and returning is the identity on page and selection;
1478    /// 3. the editing intentions denote the derived cursor operations;
1479    /// 4. an intention addressed to one identity leaves the others unobserved.
1480    ///
1481    /// # Errors
1482    ///
1483    /// Returns the first violated law.
1484    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    /// Observes one entry through exactly what the specification exposes about it.
1534    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}