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 {
19 name: "law",
20 version: "0.0.0",
21};
22
23pub trait ScreenLawFixture {
25 type Syntax;
27
28 fn law_screen_syntax(&self) -> Self::Syntax;
30}
31
32#[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 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 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#[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 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#[ext(name = GaugeLaws)]
212pub impl<This> This {
213 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#[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 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 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(¬e)) {
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 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 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 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}