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