1use crate::*;
8use alux_ext::ext;
9use anyhow::{Result, bail};
10use rsynko_manager::*;
11
12fn squeezed(line: &str) -> String {
14 line.split_whitespace().collect::<Vec<_>>().join(" ")
15}
16
17const LAW_APPLICATION: Application<'static> = Application { name: "law", version: "0.0.0" };
19
20pub trait ScreenLawFixture {
22 type Syntax;
24
25 fn law_screen_syntax(&self) -> Self::Syntax;
27}
28
29#[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 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 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#[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 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#[ext(name = GaugeLaws)]
206pub impl<This> This {
207 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#[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 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 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(¬e)) {
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 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 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 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}