Skip to main content

rsynko_ui/
laws.rs

1//! Law scenarios for the presentation, stated once over the capabilities.
2//!
3//! A scenario authors its own collection through the manager capabilities it is bound to and
4//! reads screens back through the renderer a fixture supplies, so it constrains every renderer
5//! rather than the one this workspace happens to ship.
6
7use crate::*;
8use alux_ext::ext;
9use anyhow::{Result, bail};
10use rsynko_manager::*;
11
12/// States one line with every run of spaces reduced to one, so what it says can be compared.
13fn squeezed(line: &str) -> String {
14    line.split_whitespace().collect::<Vec<_>>().join(" ")
15}
16
17/// Names the application a presentation scenario states its screens for.
18const LAW_APPLICATION: Application<'static> = Application {
19    name: "law",
20    version: "0.0.0",
21};
22
23/// Supplies the renderer a presentation scenario cannot author for itself.
24pub trait ScreenLawFixture {
25    /// Represents the renderer whose screens the scenario reads.
26    type Syntax;
27
28    /// Supplies the renderer the scenario states its screens through.
29    fn law_screen_syntax(&self) -> Self::Syntax;
30}
31
32/// Authors the key-binding laws.
33#[ext(name = KeyBindingLaws)]
34pub impl<This> This
35where
36    This: NavigationStateAlg
37        + QueueCatalogAlg
38        + QueueAppendAlg
39        + QueueRemoveAlg
40        + QueuePauseAlg
41        + QueueDuplicateAlg
42        + QueueFormatEditAlg
43        + FormatCatalogStateAlg
44        + QueueDryRunAlg
45        + RehearsalStateAlg
46        + TransferStateAlg
47        + DetailSelectionAlg
48        + DraftStateAlg
49        + OutputDraftAlg
50        + InputDraftAlg
51        + QueueOutputAlg
52        + QueueSourceAlg
53        + SourceMetadataAlg
54        + TextEditorStateAlg
55        + ManagerStatusAlg
56        + SafeExitAlg
57        + SourceRequestAlg
58        + SubmissionAlg
59        + MediaOptionsAlg
60        + OutputChoiceAlg,
61    This::Entry: QueueEntryAlg + RequestOptionsAlg,
62    This::Id: Copy + Eq,
63{
64    /// Checks that every page binds its keys unambiguously and admits nothing it did not bind.
65    ///
66    /// The laws checked are:
67    ///
68    /// 1. no two bindings on one page are reached by the same keystroke;
69    /// 2. every keystroke a page binds denotes that binding;
70    /// 3. exit is bound on every page, and no other modified keystroke denotes anything;
71    /// 4. typed scalars denote insertion exactly on the pages holding a draft;
72    /// 5. a gated binding whose action has no meaning leaves the manager as it was.
73    ///
74    /// # Errors
75    ///
76    /// Returns the first violated law.
77    fn key_binding_laws(&mut self) -> Result<()> {
78        let ids = self.author_law_queue(2);
79        let pages = [
80            ManagerPage::Collection,
81            ManagerPage::AddSources,
82            ManagerPage::Details(ids[0]),
83            ManagerPage::Formats(ids[0]),
84            ManagerPage::Output(ids[0]),
85            ManagerPage::Log(ids[0]),
86        ];
87        for page in pages {
88            self.set_page(page);
89            let mut bound = Vec::new();
90            for binding in self.page_bindings() {
91                for key in binding.keys {
92                    if bound.contains(&key) {
93                        bail!("{key:?} is bound twice on one page");
94                    }
95                    bound.push(key);
96                    if self.keystroke_meaning(key).is_none() {
97                        bail!("a bound keystroke {key:?} denotes nothing");
98                    }
99                }
100            }
101            let exit = self.keystroke_meaning(EXIT_KEYSTROKE);
102            if exit.and_then(|binding| binding.action) != Some(ManagerAction::Exit) {
103                bail!("exit is not bound on every page");
104            }
105            let modified = Keystroke::control(Key::Character('x'));
106            if self.keystroke_meaning(modified).is_some() {
107                bail!("a modified keystroke denotes what its unmodified key denotes");
108            }
109            let typed = self.keystroke_meaning(Keystroke::plain(Key::Character('«')));
110            if typed.is_some() != matches!(page, ManagerPage::AddSources | ManagerPage::Output(_)) {
111                bail!("a typed scalar denotes insertion off a page holding a draft");
112            }
113        }
114        self.set_page(ManagerPage::Collection);
115        self.set_selected_queue_id(None);
116        let page = self.page();
117        // Removal names a selected entry, so with no selection its key must refuse the intention.
118        if self.apply_keystroke(Keystroke::plain(Key::Delete)) {
119            bail!("a key applied an action the menu states as unavailable");
120        }
121        if self.page() != page {
122            bail!("a refused keystroke changed the current page");
123        }
124        Ok(())
125    }
126}
127
128/// Authors the menu-presentation laws.
129#[ext(name = MenuPresentationLaws)]
130pub impl<This> This
131where
132    This: NavigationStateAlg
133        + QueueCatalogAlg
134        + QueueAppendAlg
135        + DraftStateAlg
136        + OutputDraftAlg
137        + InputDraftAlg
138        + TextEditorStateAlg
139        + DetailSelectionAlg
140        + ManagerStatusAlg
141        + SourceRequestAlg
142        + SubmissionAlg
143        + MediaOptionsAlg
144        + OutputChoiceAlg,
145    This::Entry: QueueEntryAlg + RequestOptionsAlg,
146    This::Id: Copy + Eq,
147{
148    /// Checks that the menu states what the keys actually do.
149    ///
150    /// The laws checked are:
151    ///
152    /// 1. every menu entry states at least one key, and states it in brackets;
153    /// 2. every key a menu entry states denotes an action on that page;
154    /// 3. every page offers exit, and offers it last;
155    /// 4. a menu entry is stated as unavailable exactly when its action has no meaning.
156    ///
157    /// # Errors
158    ///
159    /// Returns the first violated law.
160    fn menu_presentation_laws(&mut self) -> Result<()> {
161        let ids = self.author_law_queue(1);
162        let pages = [
163            ManagerPage::Collection,
164            ManagerPage::AddSources,
165            ManagerPage::Details(ids[0]),
166            ManagerPage::Formats(ids[0]),
167            ManagerPage::Output(ids[0]),
168            ManagerPage::Log(ids[0]),
169        ];
170        for page in pages {
171            self.set_page(page);
172            let items = self.menu_items().collect::<Vec<_>>();
173            let Some(last) = items.last() else {
174                bail!("a page offers no menu at all");
175            };
176            if last.action != ManagerAction::Exit {
177                bail!("a page does not offer exit last");
178            }
179            for item in &items {
180                let label = item.label();
181                if item.keys.is_empty() {
182                    bail!("the menu states an entry no key reaches");
183                }
184                if !label.starts_with('[') || !label.contains("] ") {
185                    bail!("a menu entry does not state its keys in brackets: {label}");
186                }
187                if item.availability != self.action_availability(item.action) {
188                    bail!("a menu entry disagrees with what its action means: {label}");
189                }
190                for key in &item.keys {
191                    let bound = self
192                        .keystroke_meaning(*key)
193                        .and_then(|binding| binding.action);
194                    let cursor =
195                        matches!(bound, Some(ManagerAction::Previous | ManagerAction::Next))
196                            && item.action == ManagerAction::Next;
197                    if bound != Some(item.action) && !cursor {
198                        bail!("the menu states {key:?} for {label}, which denotes {bound:?}");
199                    }
200                }
201            }
202        }
203        Ok(())
204    }
205}
206
207/// Authors the progress-gauge laws.
208///
209/// A gauge is derived from a share and a width alone, so this bundle names no capability: the
210/// bounds of a law are the meanings it is derived from, and here there are none.
211#[ext(name = GaugeLaws)]
212pub impl<This> This {
213    /// Checks that a gauge states a share without claiming more resolution than it has.
214    ///
215    /// The laws checked are:
216    ///
217    /// 1. a gauge occupies its stated width whatever share it states;
218    /// 2. no share is stated as filled before it is reached, and none is stated twice;
219    /// 3. nothing stated is nothing filled, and everything stated fills the whole track;
220    /// 4. the stated text occupies exactly one cell per counted cell.
221    ///
222    /// # Errors
223    ///
224    /// Returns the first violated law.
225    fn gauge_laws(&self) -> Result<()> {
226        let mut previous = 0;
227        for percent in 0..=100_u16 {
228            let gauge = Gauge::of(percent, GAUGE_WIDTH);
229            if gauge.width() != GAUGE_WIDTH {
230                bail!("a gauge of {percent}% occupies {} cells", gauge.width());
231            }
232            let eighths = gauge.filled * Gauge::LEADING.len() + gauge.leading;
233            if eighths < previous {
234                bail!("a gauge of {percent}% states less than a smaller share");
235            }
236            previous = eighths;
237            if gauge.text().chars().count() != GAUGE_WIDTH {
238                bail!(
239                    "a gauge of {percent}% states {} cells of text",
240                    gauge.text()
241                );
242            }
243        }
244        let empty = Gauge::of(0, GAUGE_WIDTH);
245        if empty.filled != 0 || empty.leading != 0 || empty.track != GAUGE_WIDTH {
246            bail!("an unstarted transfer states a filled gauge");
247        }
248        let full = Gauge::of(100, GAUGE_WIDTH);
249        if full.filled != GAUGE_WIDTH || full.track != 0 {
250            bail!("a completed transfer states an unfilled gauge");
251        }
252        Ok(())
253    }
254}
255
256/// Authors the screen-composition laws.
257#[ext(name = ScreenLaws)]
258pub impl<This> This
259where
260    This: NavigationStateAlg
261        + QueueCatalogAlg
262        + QueueAppendAlg
263        + DraftStateAlg
264        + OutputDraftAlg
265        + InputDraftAlg
266        + TextEditorStateAlg
267        + DetailSelectionAlg
268        + ManagerStatusAlg
269        + SourceRequestAlg
270        + SubmissionAlg
271        + MediaOptionsAlg
272        + OutputChoiceAlg
273        + QueuePauseAlg
274        + TransferStateAlg
275        + ScreenLawFixture
276        + SubmissionLawFixture,
277    This::Entry: QueueEntryAlg
278        + RequestOptionsAlg
279        + TransferViewAlg
280        + FormatChoiceViewAlg<Format: FormatDescriptionAlg>
281        + RehearsalViewAlg<Change: PlannedChangeAlg>,
282    This::Syntax: ScreenSyntax,
283    This::Id: Copy + Eq,
284{
285    /// Checks that every page states what the manager holds and offers.
286    ///
287    /// The laws checked are:
288    ///
289    /// 1. every page names what it belongs to and the pages it rests under, the collection
290    ///    excepted, which names itself;
291    /// 2. every page states every menu entry it offers;
292    /// 3. an empty collection names the key that fills it;
293    /// 4. expanded details state every control the entry offers, each exactly once;
294    /// 5. a record states every note observed about the request;
295    /// 6. the choice a request fixed reads the same where it is chosen and where it is shown;
296    /// 7. a request being worked on and one held still do not read alike.
297    ///
298    /// # Errors
299    ///
300    /// Returns the first violated law.
301    fn screen_laws(&mut self) -> Result<()> {
302        self.set_page(ManagerPage::Collection);
303        let empty = self.law_screen_lines();
304        let add = self
305            .action_keys(ManagerAction::AddSources)
306            .next()
307            .map(Keystroke::label)
308            .unwrap_or_default();
309        if !empty.iter().any(|line| line.contains(&format!("[{add}]"))) {
310            bail!("an empty collection does not name the key that fills it");
311        }
312        let ids = self.author_law_queue(2);
313        for page in [
314            ManagerPage::Collection,
315            ManagerPage::Details(ids[0]),
316            ManagerPage::Formats(ids[0]),
317            ManagerPage::Log(ids[0]),
318        ] {
319            self.set_page(page);
320            let lines = self.law_screen_lines();
321            // The collection is what every page rests under, and names itself.
322            for breadcrumb in self.breadcrumbs().into_iter().skip(1) {
323                if !lines.iter().any(|line| line.contains(&breadcrumb.label)) {
324                    bail!("a page does not state the path that reached it");
325                }
326            }
327            for item in self.menu_items() {
328                if !lines.iter().any(|line| line.contains(&item.label())) {
329                    bail!(
330                        "a page does not state the menu entry it offers: {}",
331                        item.label()
332                    );
333                }
334            }
335        }
336        self.set_page(ManagerPage::Details(ids[0]));
337        let details = self.law_screen_lines();
338        let controls = self
339            .queue_entry(ids[0])
340            .map(QueueEntryAlg::detail_controls)
341            .unwrap_or_default();
342        for control in controls {
343            let label = control.control_label();
344            let stated = details.iter().filter(|line| line.contains(label)).count();
345            if stated != 1 {
346                bail!("expanded details state {label} {stated} times");
347            }
348        }
349        self.set_page(ManagerPage::Log(ids[0]));
350        let record = self.law_screen_lines();
351        let notes = self
352            .queue_entry(ids[0])
353            .map(|entry| entry.download_log().map(str::to_owned).collect::<Vec<_>>())
354            .unwrap_or_default();
355        for note in notes {
356            if !record.iter().any(|line| line.contains(&note)) {
357                bail!("a record does not state the note {note}");
358            }
359        }
360        self.check_choice_laws()?;
361        self.check_phase_laws(ids[0], ids[1])
362    }
363
364    /// Checks that one fixed choice reads the same wherever a reader meets it.
365    ///
366    /// A chooser compares alternatives by reading down a column, and details state one choice as
367    /// one phrase, so the two are spaced differently on purpose. What they say is the same.
368    ///
369    /// # Errors
370    ///
371    /// Returns the first violated law.
372    fn check_choice_laws(&mut self) -> Result<()> {
373        let (submission, _, _) = self.law_folder_submission();
374        self.set_draft(submission);
375        self.submit_draft();
376        let Some(id) = self.queue_ids().last() else {
377            bail!("a submitted transfer was not collected");
378        };
379        let phrase = {
380            let Some(entry) = self.queue_entry(id) else {
381                bail!("a submitted transfer was not collected");
382            };
383            let Some(chosen) = entry.chosen_choice() else {
384                bail!("a transfer states no way of transferring");
385            };
386            let summary = entry.choice_summary(chosen).unwrap_or_default();
387            squeezed(&format!("{chosen} {summary}"))
388        };
389        self.set_page(ManagerPage::Formats(id));
390        if !self
391            .law_screen_lines()
392            .iter()
393            .any(|line| squeezed(line).contains(&phrase))
394        {
395            bail!("a chooser does not state the choice the request fixed: {phrase}");
396        }
397        self.set_page(ManagerPage::Details(id));
398        if !self
399            .law_screen_lines()
400            .iter()
401            .any(|line| squeezed(line).ends_with(&phrase))
402        {
403            bail!("details state the fixed choice in other words than the chooser does: {phrase}");
404        }
405        Ok(())
406    }
407
408    /// Checks that the collection distinguishes work happening from work held still.
409    ///
410    /// # Errors
411    ///
412    /// Returns the first violated law.
413    fn check_phase_laws(&mut self, running: This::Id, held: This::Id) -> Result<()> {
414        self.apply_transfer_event(running, TransferObservationOp::Started {});
415        self.apply_transfer_event(held, TransferObservationOp::Started {});
416        self.apply_transfer_event(
417            held,
418            TransferObservationOp::PauseCapability { supported: true },
419        );
420        self.toggle_queue_pause(held);
421        self.set_page(ManagerPage::Collection);
422        let lines = self.law_screen_lines();
423        let marked = |phase: TransferPhase| {
424            lines
425                .iter()
426                .filter(|line| line.contains(phase.phase_marker()))
427                .count()
428        };
429        if self.queue_entry(held).map(QueueEntryAlg::phase) != Some(TransferPhase::Paused) {
430            bail!("a request told to wait was not held");
431        }
432        if marked(TransferPhase::Downloading) == 0 || marked(TransferPhase::Paused) == 0 {
433            bail!("the collection does not mark work happening apart from work held still");
434        }
435        if TransferPhase::Downloading.phase_marker() == TransferPhase::Paused.phase_marker() {
436            bail!("work happening and work held still are marked the same");
437        }
438        Ok(())
439    }
440
441    /// Reads the current page back as the lines it states.
442    fn law_screen_lines(&self) -> Vec<String> {
443        let syntax = self.law_screen_syntax();
444        let screen = self.screen(&syntax, LAW_APPLICATION);
445        syntax.screen_text(&screen).collect()
446    }
447}