1use std::fmt::Write as _;
19
20use shuyi_assets::font::CellWidths;
21use shuyi_assets::lang::{Lang, LangCode, LineText, MadeBy, Orphan, Overflow};
22
23pub const TEST_PACK: &str = "test";
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum Part {
29 Screen,
31 Data,
33 Dialogue,
35}
36
37impl Part {
38 const ALL: [Self; 3] = [Self::Screen, Self::Data, Self::Dialogue];
39
40 fn heading(self) -> &'static str {
41 match self {
42 Self::Screen => "screen text",
43 Self::Data => "data",
44 Self::Dialogue => "dialogue",
45 }
46 }
47}
48
49#[derive(Debug, Clone, Default, PartialEq, Eq)]
51pub struct Options {
52 pub only: Option<Part>,
54 pub file: Option<String>,
57}
58
59pub fn parse_args(args: &[String]) -> Result<(String, Options), String> {
61 let mut code = None;
62 let mut options = Options::default();
63 let mut args = args.iter();
64 while let Some(arg) = args.next() {
65 match arg.as_str() {
66 "--only" => {
67 options.only = Some(match args.next().map(String::as_str) {
68 Some("ui") => Part::Screen,
69 Some("data") => Part::Data,
70 Some("dialogue") => Part::Dialogue,
71 _ => return Err("--only takes ui, data or dialogue".to_owned()),
72 });
73 }
74 "--file" => match args.next() {
75 Some(stem) => options.file = Some(stem.trim_end_matches(".dlg").to_owned()),
76 None => return Err("--file takes a .dlg file name, like ch01".to_owned()),
77 },
78 flag if flag.starts_with("--") => return Err(format!("unknown option {flag}")),
79 _ if code.is_none() => code = Some(arg.clone()),
80 _ => return Err("only one language code".to_owned()),
81 }
82 }
83 if options.file.is_some() && options.only.is_some_and(|part| part != Part::Dialogue) {
84 return Err("--file is dialogue; it can't go with --only ui or data".to_owned());
85 }
86 let code = code.ok_or("no language code")?;
87 Ok((code, options))
88}
89
90struct Scope<'a> {
92 lang: &'a Lang,
93 options: &'a Options,
94 tests: bool,
96 left_out: usize,
98}
99
100impl Scope<'_> {
101 fn part_of(&self, key: &str) -> Part {
102 if self.lang.has(key) {
103 Part::Screen
104 } else if self.lang.source().lines.contains_key(key) {
105 Part::Dialogue
106 } else {
107 Part::Data
108 }
109 }
110
111 fn shows(&self, part: Part) -> bool {
112 let file = self.options.file.as_ref().map(|_| Part::Dialogue);
113 self.options
114 .only
115 .or(file)
116 .is_none_or(|wanted| wanted == part)
117 }
118
119 fn keeps(&mut self, key: &str) -> bool {
122 let part = self.part_of(key);
123 if !self.shows(part) {
124 return false;
125 }
126 let line = self.lang.source().lines.get(key);
127 if let (Some(stem), Some(line)) = (&self.options.file, line)
128 && line.file != *stem
129 {
130 return false;
131 }
132 let test = match (part, line) {
135 (Part::Dialogue, Some(line)) => line.file.starts_with("test"),
136 (Part::Data, _) => key
137 .split('.')
138 .nth(1)
139 .is_some_and(|id| id.starts_with("test")),
140 _ => false,
141 };
142 if test && !self.tests {
143 self.left_out += 1;
144 return false;
145 }
146 true
147 }
148
149 fn keeps_orphan(&self, orphan: &Orphan) -> bool {
150 if !self.shows(Part::Dialogue) {
151 return false;
152 }
153 match &self.options.file {
154 None => true,
155 Some(stem) => orphan
156 .nearest
157 .as_ref()
158 .and_then(|key| self.lang.source().lines.get(key))
159 .is_some_and(|line| line.file == *stem),
160 }
161 }
162}
163
164pub fn report<S: AsRef<str>>(
170 lang: &Lang,
171 variants: &[S],
172 widths: &CellWidths,
173 code: &str,
174 options: &Options,
175) -> Result<String, String> {
176 let known = || {
177 let codes: Vec<&str> = lang.codes().map(LangCode::as_str).collect();
178 format!("the packs are: {}", codes.join(", "))
179 };
180 let code = LangCode::parse(code).map_err(|_| format!("no language \"{code}\"; {}", known()))?;
181 let (Some(pack), Some(status)) = (lang.pack(&code), lang.status(&code)) else {
182 return Err(format!("no language pack \"{code}\"; {}", known()));
183 };
184 let made_by = match &pack.info.made_by {
185 MadeBy::Machine => "machine-made".to_owned(),
186 MadeBy::Human(credit) => format!("by {credit}"),
187 };
188 let mut scope = Scope {
189 lang,
190 options,
191 tests: code.as_str() == TEST_PACK,
192 left_out: 0,
193 };
194 let missing: Vec<&String> = status.missing.iter().filter(|k| scope.keeps(k)).collect();
195 let stale: Vec<&String> = status.stale.iter().filter(|k| scope.keeps(k)).collect();
196 let overflow = if scope.shows(Part::Screen) {
197 lang.overflow(&code, widths).unwrap_or_default()
198 } else {
199 Vec::new()
200 };
201 let orphans: Vec<&Orphan> = status
202 .orphans
203 .iter()
204 .filter(|o| scope.keeps_orphan(o))
205 .collect();
206 let mut out = format!(
207 "lang-status {code} ({}, {made_by}): {} missing, {} stale, {} orphaned",
208 pack.info.name,
209 missing.len(),
210 stale.len(),
211 orphans.len()
212 );
213 if scope.left_out > 0 {
214 let _ = write!(out, "; {} test texts left out", scope.left_out);
216 }
217 out.push('\n');
218 for part in Part::ALL.into_iter().filter(|&part| scope.shows(part)) {
219 let of_part = |keys: &[&String]| -> Vec<String> {
220 keys.iter()
221 .filter(|k| scope.part_of(k) == part)
222 .map(|k| (*k).clone())
223 .collect()
224 };
225 let (missing, stale) = (of_part(&missing), of_part(&stale));
226 let orphans: &[&Orphan] = if part == Part::Dialogue {
227 &orphans
228 } else {
229 &[]
230 };
231 let _ = write!(
232 out,
233 "{}: {} missing, {} stale",
234 part.heading(),
235 missing.len(),
236 stale.len()
237 );
238 match part {
239 Part::Screen => {
240 let _ = write!(out, ", {} too long", overflow.len());
241 }
242 Part::Dialogue => {
243 let _ = write!(out, ", {} orphaned", orphans.len());
244 }
245 Part::Data => {}
246 }
247 out.push('\n');
248 if !missing.is_empty() {
249 out.push_str("missing:\n");
250 }
251 for key in &missing {
252 let source = lang.source_text(key).unwrap_or_default();
253 let _ = writeln!(out, " {key}\n source: {source:?}");
254 }
255 if !stale.is_empty() {
256 out.push_str("stale:\n");
257 }
258 for key in &stale {
259 let entry = pack.entry(key).map(|e| &e.source);
260 let was = entry.or_else(|| pack.line(key).map(|e| &e.source));
261 let was = was.map_or("", String::as_str);
262 let now = lang.source_text(key).unwrap_or_default();
263 let _ = writeln!(out, " {key}\n was: {was:?}\n now: {now:?}");
264 }
265 if part == Part::Screen {
266 write_overflow(&mut out, &overflow);
267 }
268 if !orphans.is_empty() {
269 out.push_str("orphaned:\n");
270 }
271 for orphan in orphans {
272 write_orphan(&mut out, lang, variants, orphan);
273 }
274 }
275 Ok(out)
276}
277
278fn write_overflow(out: &mut String, overflow: &[Overflow]) {
279 if !overflow.is_empty() {
280 out.push_str("too long:\n");
281 }
282 for entry in overflow {
283 let _ = writeln!(
284 out,
285 " {}\n {} cells; the place has {}\n text: {:?}",
286 entry.key, entry.width, entry.max_cells, entry.text
287 );
288 }
289}
290
291fn write_orphan<S: AsRef<str>>(out: &mut String, lang: &Lang, variants: &[S], orphan: &Orphan) {
292 let _ = writeln!(out, " {}\n was: {:?}", orphan.key, orphan.source);
293 match &orphan.text {
294 LineText::One(text) => {
295 let _ = writeln!(out, " text: {text:?}");
296 }
297 LineText::Variants(texts) => {
298 for variant in variants {
300 let variant = variant.as_ref();
301 let text = texts.get(variant).map_or("", String::as_str);
302 let _ = writeln!(out, " text_{variant}: {text:?}");
303 }
304 }
305 }
306 match &orphan.nearest {
307 Some(key) => {
308 let now = lang.source_text(key).unwrap_or_default();
309 let _ = writeln!(out, " nearest: {key}\n now: {now:?}");
310 }
311 None => out.push_str(" nearest: no untranslated line in its scene\n"),
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use std::collections::BTreeMap;
318
319 use shuyi_assets::dialogue::{DialogueLimits, DialogueTable, NoTokens, parse_dlg};
320 use shuyi_assets::lang::{
321 LangFile, PackRules, SourceText, english_from_source, from_files, pack_from_sources,
322 };
323
324 use super::*;
325
326 fn no_problem(_: &str, _: &str) -> Option<String> {
328 None
329 }
330
331 const RULES: PackRules<'static> = PackRules {
333 data_problem: no_problem,
334 tokens: &NoTokens,
335 limits: DialogueLimits {
336 box_width: 40,
337 box_lines: 3,
338 max_text: 200,
339 max_option: 30,
340 min_options: 2,
341 max_options: 4,
342 max_reaction_texts: 3,
343 },
344 widths: &shuyi_assets::font::CellWidths::NARROW,
345 variants: &[],
346 };
347
348 const ONE_TEXT: [&str; 0] = [];
350
351 fn report(lang: &Lang, code: &str, options: &Options) -> Result<String, String> {
352 super::report(lang, &ONE_TEXT, &CellWidths::NARROW, code, options)
353 }
354
355 fn lang(info: &str, ui: &str) -> Lang {
356 let english = r#"{"a.one": "One", "a.two": "Two", "a.three": "Three"}"#;
357 let english = english_from_source("en/ui.ron", english).unwrap();
358 let pack = pack_from_sources(("l", info), ("u", ui), &english, &RULES).unwrap();
359 let packs = BTreeMap::from([(LangCode::new("xx").unwrap(), pack)]);
360 Lang::new(english, packs)
361 }
362
363 #[test]
364 fn lists_missing_and_stale_keys() {
365 let ui = r#"[
366 (key: "a.one", source: "One", text: "Un"),
367 (key: "a.two", source: "Too", text: "Deux"),
368 ]"#;
369 let lang = lang(r#"(name: "Xx", made_by: Machine)"#, ui);
370 assert_eq!(
371 report(&lang, "xx", &Options::default()).unwrap(),
372 "lang-status xx (Xx, machine-made): 1 missing, 1 stale, 0 orphaned\n\
373 screen text: 1 missing, 1 stale, 0 too long\n\
374 missing:\n a.three\n source: \"Three\"\n\
375 stale:\n a.two\n was: \"Too\"\n now: \"Two\"\n\
376 data: 0 missing, 0 stale\n\
377 dialogue: 0 missing, 0 stale, 0 orphaned\n"
378 );
379 }
380
381 #[test]
382 fn a_complete_pack_is_one_line() {
383 let ui = r#"[
384 (key: "a.one", source: "One", text: "Un"),
385 (key: "a.two", source: "Two", text: "Deux"),
386 (key: "a.three", source: "Three", text: "Trois"),
387 ]"#;
388 let lang = lang(r#"(name: "Xx", made_by: Human("A. Translator"))"#, ui);
389 assert_eq!(
390 report(&lang, "xx", &Options::default()).unwrap(),
391 "lang-status xx (Xx, by A. Translator): 0 missing, 0 stale, 0 orphaned\n\
392 screen text: 0 missing, 0 stale, 0 too long\n\
393 data: 0 missing, 0 stale\n\
394 dialogue: 0 missing, 0 stale, 0 orphaned\n"
395 );
396 }
397
398 #[test]
401 fn lists_the_screen_text_too_long_for_its_place() {
402 let english = r#"{
403 "a.one": (text: "One", max_cells: 4, note: "A button."),
404 "a.two": (text: "Two", max_cells: 5),
405 "a.three": "Three",
406 }"#;
407 let ui = r#"[
408 (key: "a.one", source: "One", text: "Einhundert"),
409 (key: "a.two", source: "Two", text: "にばん"),
410 (key: "a.three", source: "Three", text: "Dreihundertdreiunddreißig"),
411 ]"#;
412 let files = [
413 ("en/ui.ron", english),
414 ("xx/lang.ron", r#"(name: "Xx", made_by: Machine)"#),
415 ("xx/ui.ron", ui),
416 ]
417 .map(|(path, source)| LangFile {
418 path,
419 source: Some(source),
420 });
421 let lang = from_files(&files, None, str::to_owned, &RULES).unwrap();
422 let wide = CellWidths::wide("にばん".chars());
423 let said = super::report(&lang, &ONE_TEXT, &wide, "xx", &Options::default()).unwrap();
424 assert_eq!(
425 said,
426 "lang-status xx (Xx, machine-made): 0 missing, 0 stale, 0 orphaned\n\
427 screen text: 0 missing, 0 stale, 2 too long\n\
428 too long:\n\
429 \x20 a.one\n 10 cells; the place has 4\n text: \"Einhundert\"\n\
430 \x20 a.two\n 6 cells; the place has 5\n text: \"にばん\"\n\
431 data: 0 missing, 0 stale\n\
432 dialogue: 0 missing, 0 stale, 0 orphaned\n"
433 );
434 let only_data = Options {
435 only: Some(Part::Data),
436 ..Options::default()
437 };
438 let said = super::report(&lang, &ONE_TEXT, &wide, "xx", &only_data).unwrap();
439 assert!(!said.contains("too long"), "{said}");
440 }
441
442 #[test]
443 fn an_unknown_language_names_the_packs() {
444 let lang = lang(r#"(name: "Xx", made_by: Machine)"#, "[]");
445 assert_eq!(
446 report(&lang, "zz", &Options::default()).unwrap_err(),
447 "no language pack \"zz\"; the packs are: xx"
448 );
449 assert!(report(&lang, "en", &Options::default()).is_err());
451 assert_eq!(
452 report(&lang, "Not A Code", &Options::default()).unwrap_err(),
453 "no language \"Not A Code\"; the packs are: xx"
454 );
455 }
456
457 #[test]
459 fn a_tag_in_another_spelling_names_its_pack() {
460 let english = english_from_source("en/ui.ron", r#"{"a.one": "One"}"#).unwrap();
461 let info = r#"(name: "Português do Brasil", made_by: Machine)"#;
462 let pack = pack_from_sources(("l", info), ("u", "[]"), &english, &RULES).unwrap();
463 let packs = BTreeMap::from([(LangCode::new("pt-BR").unwrap(), pack)]);
464 let lang = Lang::new(english, packs);
465 for spelling in ["pt-br", "pt_BR.UTF-8", "PT-BR"] {
466 let said = report(&lang, spelling, &Options::default()).unwrap();
467 assert!(
468 said.starts_with("lang-status pt-BR (Português do Brasil, machine-made)"),
469 "{said}"
470 );
471 }
472 }
473
474 fn table(files: &[(&str, &str)]) -> DialogueTable {
476 let mut table = DialogueTable::default();
477 for &(path, text) in files {
478 let (parsed, errors) = parse_dlg(path, text);
479 assert!(errors.is_empty(), "{errors:?}");
480 for scene in parsed.into_iter().map(|p| p.scene) {
481 table.files.insert(scene.id.clone(), path.to_owned());
482 table.scenes.insert(scene.id.clone(), scene);
483 }
484 }
485 table
486 }
487
488 fn lines_of(dlg: &str) -> SourceText {
490 let mut data = SourceText::default();
491 data.add_dialogue(&table(&[("t.dlg", dlg)]));
492 data
493 }
494
495 fn id_of(data: &SourceText, text: &str) -> String {
497 let line = data.lines.iter().find(|(_, line)| line.text == text);
498 line.map(|(id, _)| id.clone()).unwrap()
499 }
500
501 #[test]
505 fn a_reworded_line_is_an_orphan_paired_with_the_new_line() {
506 let before = lines_of(
507 "@scene gate\n> The gate is shut.\n> Nobody answers.\n@end\n\
508 @scene road\n> Dust.\n@end\n",
509 );
510 let entries: Vec<String> = before
512 .lines
513 .iter()
514 .map(|(id, line)| {
515 let (source, text) = (&line.text, line.text.to_uppercase());
516 format!("(key: {id:?}, source: {source:?}, text: {text:?})")
517 })
518 .collect();
519 let pack = format!("[{}]", entries.join(","));
520 let lang = |data: SourceText| {
521 let files = [
522 ("en/ui.ron", "{}"),
523 ("xx/lang.ron", r#"(name: "Xx", made_by: Machine)"#),
524 ("xx/ui.ron", "[]"),
525 ("xx/dialogue/t.ron", pack.as_str()),
526 ]
527 .map(|(path, source)| LangFile {
528 path,
529 source: Some(source),
530 });
531 from_files(&files, Some(data), str::to_owned, &RULES).unwrap()
532 };
533 assert_eq!(
534 report(&lang(before.clone()), "xx", &Options::default()).unwrap(),
535 "lang-status xx (Xx, machine-made): 0 missing, 0 stale, 0 orphaned\n\
536 screen text: 0 missing, 0 stale, 0 too long\n\
537 data: 0 missing, 0 stale\n\
538 dialogue: 0 missing, 0 stale, 0 orphaned\n"
539 );
540 let after = lines_of("@scene gate\n> The gate is barred.\n> Nobody answers.\n@end\n");
542 let new_id = id_of(&after, "The gate is barred.");
543 assert_eq!(
544 report(&lang(after.clone()), "xx", &Options::default()).unwrap(),
545 format!(
546 "lang-status xx (Xx, machine-made): 1 missing, 0 stale, 2 orphaned\n\
547 screen text: 0 missing, 0 stale, 0 too long\n\
548 data: 0 missing, 0 stale\n\
549 dialogue: 1 missing, 0 stale, 2 orphaned\n\
550 missing:\n {new_id}\n source: \"The gate is barred.\"\n\
551 orphaned:\n {gate}\n was: \"The gate is shut.\"\n \
552 text: \"THE GATE IS SHUT.\"\n \
553 nearest: {new_id}\n now: \"The gate is barred.\"\n \
554 {road}\n was: \"Dust.\"\n text: \"DUST.\"\n \
555 nearest: no untranslated line in its scene\n",
556 gate = id_of(&before, "The gate is shut."),
557 road = id_of(&before, "Dust."),
558 )
559 );
560 let in_file = |stem: &str| Options {
562 file: Some(stem.to_owned()),
563 ..Options::default()
564 };
565 let report = |stem| report(&lang(after.clone()), "xx", &in_file(stem)).unwrap();
566 assert!(report("t").starts_with(
567 "lang-status xx (Xx, machine-made): 1 missing, 0 stale, 1 orphaned
568"
569 ));
570 assert!(report("t").contains(&format!(
571 " {}
572 was: \"The gate",
573 id_of(&before, "The gate is shut.")
574 )));
575 assert!(report("other").contains(
576 "0 missing, 0 stale, 0 orphaned
577"
578 ));
579 }
580
581 fn two_files(pack_ui: &str) -> Lang {
584 let table = table(&[
585 (
586 "story/dialogue/ch01.dlg",
587 "@scene gate\n> The gate is shut.\n@end\n",
588 ),
589 ("story/dialogue/test.dlg", "@scene t\n> Test line.\n@end\n"),
590 ]);
591 let mut data = SourceText::default();
592 data.add_dialogue(&table);
593 for (key, text) in [
594 ("names.king", "the King"),
595 ("names.test_guide", "Test Guide"),
596 ] {
597 data.data.insert(key.to_owned(), text.to_owned());
598 }
599 let gate = id_of(&data, "The gate is shut.");
600 let dialogue =
601 format!(r#"[(key: {gate:?}, source: "The gate is shut.", text: "LA PORTE")]"#);
602 let files = [
603 ("en/ui.ron", r#"{"a.one": "One"}"#),
604 ("xx/lang.ron", r#"(name: "Xx", made_by: Machine)"#),
605 ("xx/ui.ron", pack_ui),
606 (
607 "xx/data.ron",
608 r#"[(key: "names.king", source: "the King", text: "le roi")]"#,
609 ),
610 ("xx/dialogue/ch01.ron", dialogue.as_str()),
611 ]
612 .map(|(path, source)| LangFile {
613 path,
614 source: Some(source),
615 });
616 from_files(&files, Some(data), str::to_owned, &RULES).unwrap()
617 }
618
619 const ONE: &str = r#"[(key: "a.one", source: "One", text: "Un")]"#;
620
621 #[test]
622 fn a_pack_that_translates_all_but_the_test_text_lacks_nothing() {
623 let lang = two_files(ONE);
624 assert_eq!(
625 report(&lang, "xx", &Options::default()).unwrap(),
626 "lang-status xx (Xx, machine-made): 0 missing, 0 stale, 0 orphaned; \
627 2 test texts left out\n\
628 screen text: 0 missing, 0 stale, 0 too long\n\
629 data: 0 missing, 0 stale\n\
630 dialogue: 0 missing, 0 stale, 0 orphaned\n"
631 );
632 }
633
634 #[test]
635 fn only_narrows_the_report_to_a_part() {
636 let lang = two_files("[]");
637 let only = |part| Options {
638 only: Some(part),
639 file: None,
640 };
641 let ui = report(&lang, "xx", &only(Part::Screen)).unwrap();
642 assert_eq!(
643 ui,
644 "lang-status xx (Xx, machine-made): 1 missing, 0 stale, 0 orphaned\n\
645 screen text: 1 missing, 0 stale, 0 too long\nmissing:\n a.one\n source: \"One\"\n"
646 );
647 let data = report(&lang, "xx", &only(Part::Data)).unwrap();
648 assert!(data.contains("\ndata: 0 missing, 0 stale\n"), "{data}");
649 assert!(!data.contains("screen text"), "{data}");
650 assert!(!data.contains("a.one"), "{data}");
651 let dialogue = report(&lang, "xx", &only(Part::Dialogue)).unwrap();
652 assert!(
653 dialogue.ends_with("dialogue: 0 missing, 0 stale, 0 orphaned\n"),
654 "{dialogue}"
655 );
656 assert!(!dialogue.contains("a.one"), "{dialogue}");
657 }
658
659 #[test]
660 fn file_lists_only_that_dlg_files_lines() {
661 let lang = two_files("[]");
662 let file = |stem: &str| Options {
663 file: Some(stem.to_owned()),
664 ..Options::default()
665 };
666 assert_eq!(
668 report(&lang, "xx", &file("ch01")).unwrap(),
669 "lang-status xx (Xx, machine-made): 0 missing, 0 stale, 0 orphaned\n\
670 dialogue: 0 missing, 0 stale, 0 orphaned\n"
671 );
672 assert_eq!(
673 report(&lang, "xx", &file("test")).unwrap(),
674 "lang-status xx (Xx, machine-made): 0 missing, 0 stale, 0 orphaned; \
675 1 test texts left out\n\
676 dialogue: 0 missing, 0 stale, 0 orphaned\n"
677 );
678 }
679
680 #[test]
683 fn the_test_pack_keeps_the_test_text_and_variants_are_listed_in_order() {
684 let before = lines_of("@scene test_s\n> Old words.\n> Kept.\n@end\n");
685 let after = lines_of("@scene test_s\n> New words.\n> Kept.\n@end\n");
686 let old = id_of(&before, "Old words.");
687 let entry = format!(r#"[(key: {old:?}, source: "Old words.", text_f: "F", text_m: "M")]"#);
688 let rules = PackRules {
689 variants: &["m", "f"],
690 ..RULES
691 };
692 let files = [
693 ("en/ui.ron", r#"{"names.test_guide": "Test"}"#),
694 ("test/lang.ron", r#"(name: "TEST", made_by: Machine)"#),
695 ("test/ui.ron", "[]"),
696 ("test/dialogue/t.ron", entry.as_str()),
697 ]
698 .map(|(path, source)| LangFile {
699 path,
700 source: Some(source),
701 });
702 let lang = from_files(&files, Some(after.clone()), str::to_owned, &rules).unwrap();
703 let said = super::report(
704 &lang,
705 &["m", "f"],
706 &CellWidths::NARROW,
707 TEST_PACK,
708 &Options::default(),
709 )
710 .unwrap();
711 assert!(!said.contains("left out"), "{said}");
712 assert!(said.contains(" names.test_guide\n"), "{said}");
713 assert!(
714 said.contains(&format!(
715 "orphaned:\n {old}\n was: \"Old words.\"\n text_m: \"M\"\n \
716 text_f: \"F\"\n nearest: "
717 )),
718 "{said}"
719 );
720 }
721
722 #[test]
723 fn arguments_are_parsed() {
724 let args = |a: &[&str]| a.iter().map(|s| (*s).to_owned()).collect::<Vec<_>>();
725 let (code, options) = parse_args(&args(&["xx", "--only", "data"])).unwrap();
726 assert_eq!((code.as_str(), options.only), ("xx", Some(Part::Data)));
727 let (_, options) = parse_args(&args(&["--file", "ch01.dlg", "xx"])).unwrap();
728 assert_eq!(options.file.as_deref(), Some("ch01"));
729 assert!(parse_args(&args(&[])).is_err());
730 assert!(parse_args(&args(&["xx", "yy"])).is_err());
731 assert!(parse_args(&args(&["xx", "--only", "nope"])).is_err());
732 assert!(parse_args(&args(&["xx", "--only"])).is_err());
733 assert!(parse_args(&args(&["xx", "--file"])).is_err());
734 assert!(parse_args(&args(&["xx", "--bogus"])).is_err());
735 assert_eq!(
736 parse_args(&args(&["--bogus"])).unwrap_err(),
737 "unknown option --bogus"
738 );
739 let (_, options) = parse_args(&args(&["xx", "--only", "ui"])).unwrap();
740 assert_eq!(options.only, Some(Part::Screen));
741 assert!(parse_args(&args(&["xx", "--only", "ui", "--file", "a"])).is_err());
742 assert!(parse_args(&args(&["xx", "--only", "dialogue", "--file", "a"])).is_ok());
743 }
744}