diff --git a/exercises/practice/grep/.meta/test_template.tera b/exercises/practice/grep/.meta/test_template.tera new file mode 100644 index 000000000..de27f4dc3 --- /dev/null +++ b/exercises/practice/grep/.meta/test_template.tera @@ -0,0 +1,148 @@ +use grep::*; + +#[test] +#[ignore] +fn nonexistent_file_returns_error() { + let pattern = "Agamemnon"; + let flags = Flags::new(&[]); + let files = ["0-1-nonexistent-file-returns-error-iliad.txt"]; + assert!(grep(pattern, &flags, &files).is_err()); +} + +#[test] +#[ignore] +fn grep_returns_result() { + let pattern = "Agamemnon"; + let flags = Flags::new(&[]); + let files = Files::new(&["0-2-grep-returns-result-iliad.txt"]); + assert!(grep(pattern, &flags, files.as_ref()).is_ok()); +} + +{% for test_group in cases %} +{% set group_idx = loop.index %} +{% for test in test_group.cases %} +{% set test_idx = loop.index %} + +{# + This prefix is added to the file names to ensure each test case has its own + set of files. This is necessary because every test case creates and deletes + its own files, so the names must be unique to prevent conflicts. +#} +{% set prefix = group_idx ~ "-" ~ test_idx %} + +#[test] +#[ignore] +fn {{ test.description | snake_case }}() { + let pattern = {{ test.input.pattern | json_encode() }}; + let flags = Flags::new(&{{ test.input.flags | json_encode() }}); + let files = Files::new(&[ + {% for file in test.input.files -%} + "{{ prefix }}-{{ file }}", + {%- endfor %} + ]); + let actual = grep(pattern, &flags, files.as_ref()).unwrap(); + let expected: &[&str] = &[ + {% if test.input.files | length > 1 or test.input.flags is containing("-l") %} + {% for line in test.expected %} + "{{ prefix }}-{{ line }}", + {% endfor %} + {% else %} + {% for line in test.expected %} + "{{ line }}", + {% endfor %} + {% endif %} + ]; + assert_eq!(actual, expected); +} +{% endfor %} +{% endfor %} + +static ILIAD_CONTENT: &str = "\ +Achilles sing, O Goddess! Peleus' son; +His wrath pernicious, who ten thousand woes +Caused to Achaia's host, sent many a soul +Illustrious into Ades premature, +And Heroes gave (so stood the will of Jove) +To dogs and to all ravening fowls a prey, +When fierce dispute had separated once +The noble Chief Achilles from the son +Of Atreus, Agamemnon, King of men. +"; + +static MIDSUMMER_NIGHT_CONTENT: &str = "\ +I do entreat your grace to pardon me. +I know not by what power I am made bold, +Nor how it may concern my modesty, +In such a presence here to plead my thoughts; +But I beseech your grace that I may know +The worst that may befall me in this case, +If I refuse to wed Demetrius. +"; + +static PARADISE_LOST_CONTENT: &str = "\ +Of Mans First Disobedience, and the Fruit +Of that Forbidden Tree, whose mortal tast +Brought Death into the World, and all our woe, +With loss of Eden, till one greater Man +Restore us, and regain the blissful Seat, +Sing Heav'nly Muse, that on the secret top +Of Oreb, or of Sinai, didst inspire +That Shepherd, who first taught the chosen Seed +"; + +/// In The White Night +/// A poem by Alexander Blok(https://en.wikipedia.org/wiki/Alexander_Blok) +/// a Russian poet who is regarded as one of the most important figures of the Silver Age of Russian Poetry +/// You can read the translation here: https://lyricstranslate.com/ru/белой-ночью-месяц-красный-white-night-crimson-crescent.html +static IN_THE_WHITE_NIGHT_CONTENT: &str = " +Белой ночью месяц красный +Выплывает в синеве. +Бродит призрачно-прекрасный, +Отражается в Неве. +Мне провидится и снится +Исполненье тайных дум. +В вас ли доброе таится, +Красный месяц, тихий шум?.. +"; + +struct Files<'a> { + file_names: &'a [&'a str], +} + +impl<'a> Files<'a> { + fn new(file_names: &'a [&'a str]) -> Self { + for file_name in file_names { + let content = if file_name.ends_with("iliad.txt") { + ILIAD_CONTENT + } else if file_name.ends_with("midsummer-night.txt") { + MIDSUMMER_NIGHT_CONTENT + } else if file_name.ends_with("paradise-lost.txt") { + PARADISE_LOST_CONTENT + } else { + IN_THE_WHITE_NIGHT_CONTENT + }; + std::fs::write(file_name, content).unwrap_or_else(|_| { + panic!( + "Error setting up file '{file_name}' with the following content:\n{content}" + ) + }); + } + + Self { file_names } + } +} + +impl<'a> Drop for Files<'a> { + fn drop(&mut self) { + for file_name in self.file_names { + std::fs::remove_file(file_name) + .unwrap_or_else(|e| panic!("Could not delete file '{file_name}': {e}")); + } + } +} + +impl<'a> AsRef<[&'a str]> for Files<'a> { + fn as_ref(&self) -> &[&'a str] { + self.file_names + } +} diff --git a/exercises/practice/grep/.meta/tests.toml b/exercises/practice/grep/.meta/tests.toml index be690e975..04c51e71b 100644 --- a/exercises/practice/grep/.meta/tests.toml +++ b/exercises/practice/grep/.meta/tests.toml @@ -1,3 +1,85 @@ -# This is an auto-generated file. Regular comments will be removed when this -# file is regenerated. Regenerating will not touch any manually added keys, -# so comments can be added in a "comment" key. +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[9049fdfd-53a7-4480-a390-375203837d09] +description = "Test grepping a single file -> One file, one match, no flags" + +[76519cce-98e3-46cd-b287-aac31b1d77d6] +description = "Test grepping a single file -> One file, one match, print line numbers flag" + +[af0b6d3c-e0e8-475e-a112-c0fc10a1eb30] +description = "Test grepping a single file -> One file, one match, case-insensitive flag" + +[ff7af839-d1b8-4856-a53e-99283579b672] +description = "Test grepping a single file -> One file, one match, print file names flag" + +[8625238a-720c-4a16-81f2-924ec8e222cb] +description = "Test grepping a single file -> One file, one match, match entire lines flag" + +[2a6266b3-a60f-475c-a5f5-f5008a717d3e] +description = "Test grepping a single file -> One file, one match, multiple flags" + +[842222da-32e8-4646-89df-0d38220f77a1] +description = "Test grepping a single file -> One file, several matches, no flags" + +[4d84f45f-a1d8-4c2e-a00e-0b292233828c] +description = "Test grepping a single file -> One file, several matches, print line numbers flag" + +[0a483b66-315b-45f5-bc85-3ce353a22539] +description = "Test grepping a single file -> One file, several matches, match entire lines flag" + +[3d2ca86a-edd7-494c-8938-8eeed1c61cfa] +description = "Test grepping a single file -> One file, several matches, case-insensitive flag" + +[1f52001f-f224-4521-9456-11120cad4432] +description = "Test grepping a single file -> One file, several matches, inverted flag" + +[7a6ede7f-7dd5-4364-8bf8-0697c53a09fe] +description = "Test grepping a single file -> One file, no matches, various flags" + +[3d3dfc23-8f2a-4e34-abd6-7b7d140291dc] +description = "Test grepping a single file -> One file, one match, file flag takes precedence over line flag" + +[87b21b24-b788-4d6e-a68b-7afe9ca141fe] +description = "Test grepping a single file -> One file, several matches, inverted and match entire lines flags" + +[ba496a23-6149-41c6-a027-28064ed533e5] +description = "Test grepping multiples files at once -> Multiple files, one match, no flags" + +[4539bd36-6daa-4bc3-8e45-051f69f5aa95] +description = "Test grepping multiples files at once -> Multiple files, several matches, no flags" + +[9fb4cc67-78e2-4761-8e6b-a4b57aba1938] +description = "Test grepping multiples files at once -> Multiple files, several matches, print line numbers flag" + +[aeee1ef3-93c7-4cd5-af10-876f8c9ccc73] +description = "Test grepping multiples files at once -> Multiple files, one match, print file names flag" + +[d69f3606-7d15-4ddf-89ae-01df198e6b6c] +description = "Test grepping multiples files at once -> Multiple files, several matches, case-insensitive flag" + +[82ef739d-6701-4086-b911-007d1a3deb21] +description = "Test grepping multiples files at once -> Multiple files, several matches, inverted flag" + +[77b2eb07-2921-4ea0-8971-7636b44f5d29] +description = "Test grepping multiples files at once -> Multiple files, one match, match entire lines flag" + +[e53a2842-55bb-4078-9bb5-04ac38929989] +description = "Test grepping multiples files at once -> Multiple files, one match, multiple flags" + +[9c4f7f9a-a555-4e32-bb06-4b8f8869b2cb] +description = "Test grepping multiples files at once -> Multiple files, no matches, various flags" + +[ba5a540d-bffd-481b-bd0c-d9a30f225e01] +description = "Test grepping multiples files at once -> Multiple files, several matches, file flag takes precedence over line number flag" + +[ff406330-2f0b-4b17-9ee4-4b71c31dd6d2] +description = "Test grepping multiples files at once -> Multiple files, several matches, inverted and match entire lines flags" diff --git a/exercises/practice/grep/tests/grep.rs b/exercises/practice/grep/tests/grep.rs index 49a6060b7..e6358f42c 100644 --- a/exercises/practice/grep/tests/grep.rs +++ b/exercises/practice/grep/tests/grep.rs @@ -1,8 +1,417 @@ -use grep::{grep, Flags}; +use grep::*; -use std::fs; +#[test] +fn nonexistent_file_returns_error() { + let pattern = "Agamemnon"; + let flags = Flags::new(&[]); + let files = ["0-1-nonexistent-file-returns-error-iliad.txt"]; + assert!(grep(pattern, &flags, &files).is_err()); +} + +#[test] +#[ignore] +fn grep_returns_result() { + let pattern = "Agamemnon"; + let flags = Flags::new(&[]); + let files = Files::new(&["0-2-grep-returns-result-iliad.txt"]); + assert!(grep(pattern, &flags, files.as_ref()).is_ok()); +} + +#[test] +#[ignore] +fn one_file_one_match_no_flags() { + let pattern = "Agamemnon"; + let flags = Flags::new(&[]); + let files = Files::new(&["1-1-iliad.txt"]); + let actual = grep(pattern, &flags, files.as_ref()).unwrap(); + let expected: &[&str] = &["Of Atreus, Agamemnon, King of men."]; + assert_eq!(actual, expected); +} + +#[test] +#[ignore] +fn one_file_one_match_print_line_numbers_flag() { + let pattern = "Forbidden"; + let flags = Flags::new(&["-n"]); + let files = Files::new(&["1-2-paradise-lost.txt"]); + let actual = grep(pattern, &flags, files.as_ref()).unwrap(); + let expected: &[&str] = &["2:Of that Forbidden Tree, whose mortal tast"]; + assert_eq!(actual, expected); +} + +#[test] +#[ignore] +fn one_file_one_match_case_insensitive_flag() { + let pattern = "FORBIDDEN"; + let flags = Flags::new(&["-i"]); + let files = Files::new(&["1-3-paradise-lost.txt"]); + let actual = grep(pattern, &flags, files.as_ref()).unwrap(); + let expected: &[&str] = &["Of that Forbidden Tree, whose mortal tast"]; + assert_eq!(actual, expected); +} + +#[test] +#[ignore] +fn one_file_one_match_print_file_names_flag() { + let pattern = "Forbidden"; + let flags = Flags::new(&["-l"]); + let files = Files::new(&["1-4-paradise-lost.txt"]); + let actual = grep(pattern, &flags, files.as_ref()).unwrap(); + let expected: &[&str] = &["1-4-paradise-lost.txt"]; + assert_eq!(actual, expected); +} + +#[test] +#[ignore] +fn one_file_one_match_match_entire_lines_flag() { + let pattern = "With loss of Eden, till one greater Man"; + let flags = Flags::new(&["-x"]); + let files = Files::new(&["1-5-paradise-lost.txt"]); + let actual = grep(pattern, &flags, files.as_ref()).unwrap(); + let expected: &[&str] = &["With loss of Eden, till one greater Man"]; + assert_eq!(actual, expected); +} + +#[test] +#[ignore] +fn one_file_one_match_multiple_flags() { + let pattern = "OF ATREUS, Agamemnon, KIng of MEN."; + let flags = Flags::new(&["-n", "-i", "-x"]); + let files = Files::new(&["1-6-iliad.txt"]); + let actual = grep(pattern, &flags, files.as_ref()).unwrap(); + let expected: &[&str] = &["9:Of Atreus, Agamemnon, King of men."]; + assert_eq!(actual, expected); +} + +#[test] +#[ignore] +fn one_file_several_matches_no_flags() { + let pattern = "may"; + let flags = Flags::new(&[]); + let files = Files::new(&["1-7-midsummer-night.txt"]); + let actual = grep(pattern, &flags, files.as_ref()).unwrap(); + let expected: &[&str] = &[ + "Nor how it may concern my modesty,", + "But I beseech your grace that I may know", + "The worst that may befall me in this case,", + ]; + assert_eq!(actual, expected); +} + +#[test] +#[ignore] +fn one_file_several_matches_print_line_numbers_flag() { + let pattern = "may"; + let flags = Flags::new(&["-n"]); + let files = Files::new(&["1-8-midsummer-night.txt"]); + let actual = grep(pattern, &flags, files.as_ref()).unwrap(); + let expected: &[&str] = &[ + "3:Nor how it may concern my modesty,", + "5:But I beseech your grace that I may know", + "6:The worst that may befall me in this case,", + ]; + assert_eq!(actual, expected); +} + +#[test] +#[ignore] +fn one_file_several_matches_match_entire_lines_flag() { + let pattern = "may"; + let flags = Flags::new(&["-x"]); + let files = Files::new(&["1-9-midsummer-night.txt"]); + let actual = grep(pattern, &flags, files.as_ref()).unwrap(); + let expected: &[&str] = &[]; + assert_eq!(actual, expected); +} + +#[test] +#[ignore] +fn one_file_several_matches_case_insensitive_flag() { + let pattern = "ACHILLES"; + let flags = Flags::new(&["-i"]); + let files = Files::new(&["1-10-iliad.txt"]); + let actual = grep(pattern, &flags, files.as_ref()).unwrap(); + let expected: &[&str] = &[ + "Achilles sing, O Goddess! Peleus' son;", + "The noble Chief Achilles from the son", + ]; + assert_eq!(actual, expected); +} + +#[test] +#[ignore] +fn one_file_several_matches_inverted_flag() { + let pattern = "Of"; + let flags = Flags::new(&["-v"]); + let files = Files::new(&["1-11-paradise-lost.txt"]); + let actual = grep(pattern, &flags, files.as_ref()).unwrap(); + let expected: &[&str] = &[ + "Brought Death into the World, and all our woe,", + "With loss of Eden, till one greater Man", + "Restore us, and regain the blissful Seat,", + "Sing Heav'nly Muse, that on the secret top", + "That Shepherd, who first taught the chosen Seed", + ]; + assert_eq!(actual, expected); +} + +#[test] +#[ignore] +fn one_file_no_matches_various_flags() { + let pattern = "Gandalf"; + let flags = Flags::new(&["-n", "-l", "-x", "-i"]); + let files = Files::new(&["1-12-iliad.txt"]); + let actual = grep(pattern, &flags, files.as_ref()).unwrap(); + let expected: &[&str] = &[]; + assert_eq!(actual, expected); +} + +#[test] +#[ignore] +fn one_file_one_match_file_flag_takes_precedence_over_line_flag() { + let pattern = "ten"; + let flags = Flags::new(&["-n", "-l"]); + let files = Files::new(&["1-13-iliad.txt"]); + let actual = grep(pattern, &flags, files.as_ref()).unwrap(); + let expected: &[&str] = &["1-13-iliad.txt"]; + assert_eq!(actual, expected); +} + +#[test] +#[ignore] +fn one_file_several_matches_inverted_and_match_entire_lines_flags() { + let pattern = "Illustrious into Ades premature,"; + let flags = Flags::new(&["-x", "-v"]); + let files = Files::new(&["1-14-iliad.txt"]); + let actual = grep(pattern, &flags, files.as_ref()).unwrap(); + let expected: &[&str] = &[ + "Achilles sing, O Goddess! Peleus' son;", + "His wrath pernicious, who ten thousand woes", + "Caused to Achaia's host, sent many a soul", + "And Heroes gave (so stood the will of Jove)", + "To dogs and to all ravening fowls a prey,", + "When fierce dispute had separated once", + "The noble Chief Achilles from the son", + "Of Atreus, Agamemnon, King of men.", + ]; + assert_eq!(actual, expected); +} + +#[test] +#[ignore] +fn multiple_files_one_match_no_flags() { + let pattern = "Agamemnon"; + let flags = Flags::new(&[]); + let files = Files::new(&[ + "2-1-iliad.txt", + "2-1-midsummer-night.txt", + "2-1-paradise-lost.txt", + ]); + let actual = grep(pattern, &flags, files.as_ref()).unwrap(); + let expected: &[&str] = &["2-1-iliad.txt:Of Atreus, Agamemnon, King of men."]; + assert_eq!(actual, expected); +} + +#[test] +#[ignore] +fn multiple_files_several_matches_no_flags() { + let pattern = "may"; + let flags = Flags::new(&[]); + let files = Files::new(&[ + "2-2-iliad.txt", + "2-2-midsummer-night.txt", + "2-2-paradise-lost.txt", + ]); + let actual = grep(pattern, &flags, files.as_ref()).unwrap(); + let expected: &[&str] = &[ + "2-2-midsummer-night.txt:Nor how it may concern my modesty,", + "2-2-midsummer-night.txt:But I beseech your grace that I may know", + "2-2-midsummer-night.txt:The worst that may befall me in this case,", + ]; + assert_eq!(actual, expected); +} -static ILIAD_CONTENT: &str = "Achilles sing, O Goddess! Peleus' son; +#[test] +#[ignore] +fn multiple_files_several_matches_print_line_numbers_flag() { + let pattern = "that"; + let flags = Flags::new(&["-n"]); + let files = Files::new(&[ + "2-3-iliad.txt", + "2-3-midsummer-night.txt", + "2-3-paradise-lost.txt", + ]); + let actual = grep(pattern, &flags, files.as_ref()).unwrap(); + let expected: &[&str] = &[ + "2-3-midsummer-night.txt:5:But I beseech your grace that I may know", + "2-3-midsummer-night.txt:6:The worst that may befall me in this case,", + "2-3-paradise-lost.txt:2:Of that Forbidden Tree, whose mortal tast", + "2-3-paradise-lost.txt:6:Sing Heav'nly Muse, that on the secret top", + ]; + assert_eq!(actual, expected); +} + +#[test] +#[ignore] +fn multiple_files_one_match_print_file_names_flag() { + let pattern = "who"; + let flags = Flags::new(&["-l"]); + let files = Files::new(&[ + "2-4-iliad.txt", + "2-4-midsummer-night.txt", + "2-4-paradise-lost.txt", + ]); + let actual = grep(pattern, &flags, files.as_ref()).unwrap(); + let expected: &[&str] = &["2-4-iliad.txt", "2-4-paradise-lost.txt"]; + assert_eq!(actual, expected); +} + +#[test] +#[ignore] +fn multiple_files_several_matches_case_insensitive_flag() { + let pattern = "TO"; + let flags = Flags::new(&["-i"]); + let files = Files::new(&[ + "2-5-iliad.txt", + "2-5-midsummer-night.txt", + "2-5-paradise-lost.txt", + ]); + let actual = grep(pattern, &flags, files.as_ref()).unwrap(); + let expected: &[&str] = &[ + "2-5-iliad.txt:Caused to Achaia's host, sent many a soul", + "2-5-iliad.txt:Illustrious into Ades premature,", + "2-5-iliad.txt:And Heroes gave (so stood the will of Jove)", + "2-5-iliad.txt:To dogs and to all ravening fowls a prey,", + "2-5-midsummer-night.txt:I do entreat your grace to pardon me.", + "2-5-midsummer-night.txt:In such a presence here to plead my thoughts;", + "2-5-midsummer-night.txt:If I refuse to wed Demetrius.", + "2-5-paradise-lost.txt:Brought Death into the World, and all our woe,", + "2-5-paradise-lost.txt:Restore us, and regain the blissful Seat,", + "2-5-paradise-lost.txt:Sing Heav'nly Muse, that on the secret top", + ]; + assert_eq!(actual, expected); +} + +#[test] +#[ignore] +fn multiple_files_several_matches_inverted_flag() { + let pattern = "a"; + let flags = Flags::new(&["-v"]); + let files = Files::new(&[ + "2-6-iliad.txt", + "2-6-midsummer-night.txt", + "2-6-paradise-lost.txt", + ]); + let actual = grep(pattern, &flags, files.as_ref()).unwrap(); + let expected: &[&str] = &[ + "2-6-iliad.txt:Achilles sing, O Goddess! Peleus' son;", + "2-6-iliad.txt:The noble Chief Achilles from the son", + "2-6-midsummer-night.txt:If I refuse to wed Demetrius.", + ]; + assert_eq!(actual, expected); +} + +#[test] +#[ignore] +fn multiple_files_one_match_match_entire_lines_flag() { + let pattern = "But I beseech your grace that I may know"; + let flags = Flags::new(&["-x"]); + let files = Files::new(&[ + "2-7-iliad.txt", + "2-7-midsummer-night.txt", + "2-7-paradise-lost.txt", + ]); + let actual = grep(pattern, &flags, files.as_ref()).unwrap(); + let expected: &[&str] = &["2-7-midsummer-night.txt:But I beseech your grace that I may know"]; + assert_eq!(actual, expected); +} + +#[test] +#[ignore] +fn multiple_files_one_match_multiple_flags() { + let pattern = "WITH LOSS OF EDEN, TILL ONE GREATER MAN"; + let flags = Flags::new(&["-n", "-i", "-x"]); + let files = Files::new(&[ + "2-8-iliad.txt", + "2-8-midsummer-night.txt", + "2-8-paradise-lost.txt", + ]); + let actual = grep(pattern, &flags, files.as_ref()).unwrap(); + let expected: &[&str] = &["2-8-paradise-lost.txt:4:With loss of Eden, till one greater Man"]; + assert_eq!(actual, expected); +} + +#[test] +#[ignore] +fn multiple_files_no_matches_various_flags() { + let pattern = "Frodo"; + let flags = Flags::new(&["-n", "-l", "-x", "-i"]); + let files = Files::new(&[ + "2-9-iliad.txt", + "2-9-midsummer-night.txt", + "2-9-paradise-lost.txt", + ]); + let actual = grep(pattern, &flags, files.as_ref()).unwrap(); + let expected: &[&str] = &[]; + assert_eq!(actual, expected); +} + +#[test] +#[ignore] +fn multiple_files_several_matches_file_flag_takes_precedence_over_line_number_flag() { + let pattern = "who"; + let flags = Flags::new(&["-n", "-l"]); + let files = Files::new(&[ + "2-10-iliad.txt", + "2-10-midsummer-night.txt", + "2-10-paradise-lost.txt", + ]); + let actual = grep(pattern, &flags, files.as_ref()).unwrap(); + let expected: &[&str] = &["2-10-iliad.txt", "2-10-paradise-lost.txt"]; + assert_eq!(actual, expected); +} + +#[test] +#[ignore] +fn multiple_files_several_matches_inverted_and_match_entire_lines_flags() { + let pattern = "Illustrious into Ades premature,"; + let flags = Flags::new(&["-x", "-v"]); + let files = Files::new(&[ + "2-11-iliad.txt", + "2-11-midsummer-night.txt", + "2-11-paradise-lost.txt", + ]); + let actual = grep(pattern, &flags, files.as_ref()).unwrap(); + let expected: &[&str] = &[ + "2-11-iliad.txt:Achilles sing, O Goddess! Peleus' son;", + "2-11-iliad.txt:His wrath pernicious, who ten thousand woes", + "2-11-iliad.txt:Caused to Achaia's host, sent many a soul", + "2-11-iliad.txt:And Heroes gave (so stood the will of Jove)", + "2-11-iliad.txt:To dogs and to all ravening fowls a prey,", + "2-11-iliad.txt:When fierce dispute had separated once", + "2-11-iliad.txt:The noble Chief Achilles from the son", + "2-11-iliad.txt:Of Atreus, Agamemnon, King of men.", + "2-11-midsummer-night.txt:I do entreat your grace to pardon me.", + "2-11-midsummer-night.txt:I know not by what power I am made bold,", + "2-11-midsummer-night.txt:Nor how it may concern my modesty,", + "2-11-midsummer-night.txt:In such a presence here to plead my thoughts;", + "2-11-midsummer-night.txt:But I beseech your grace that I may know", + "2-11-midsummer-night.txt:The worst that may befall me in this case,", + "2-11-midsummer-night.txt:If I refuse to wed Demetrius.", + "2-11-paradise-lost.txt:Of Mans First Disobedience, and the Fruit", + "2-11-paradise-lost.txt:Of that Forbidden Tree, whose mortal tast", + "2-11-paradise-lost.txt:Brought Death into the World, and all our woe,", + "2-11-paradise-lost.txt:With loss of Eden, till one greater Man", + "2-11-paradise-lost.txt:Restore us, and regain the blissful Seat,", + "2-11-paradise-lost.txt:Sing Heav'nly Muse, that on the secret top", + "2-11-paradise-lost.txt:Of Oreb, or of Sinai, didst inspire", + "2-11-paradise-lost.txt:That Shepherd, who first taught the chosen Seed", + ]; + assert_eq!(actual, expected); +} + +static ILIAD_CONTENT: &str = "\ +Achilles sing, O Goddess! Peleus' son; His wrath pernicious, who ten thousand woes Caused to Achaia's host, sent many a soul Illustrious into Ades premature, @@ -13,7 +422,8 @@ The noble Chief Achilles from the son Of Atreus, Agamemnon, King of men. "; -static MIDSUMMER_NIGHT_CONTENT: &str = "I do entreat your grace to pardon me. +static MIDSUMMER_NIGHT_CONTENT: &str = "\ +I do entreat your grace to pardon me. I know not by what power I am made bold, Nor how it may concern my modesty, In such a presence here to plead my thoughts; @@ -22,7 +432,8 @@ The worst that may befall me in this case, If I refuse to wed Demetrius. "; -static PARADISE_LOST_CONTENT: &str = "Of Mans First Disobedience, and the Fruit +static PARADISE_LOST_CONTENT: &str = "\ +Of Mans First Disobedience, and the Fruit Of that Forbidden Tree, whose mortal tast Brought Death into the World, and all our woe, With loss of Eden, till one greater Man @@ -36,7 +447,8 @@ That Shepherd, who first taught the chosen Seed /// A poem by Alexander Blok(https://en.wikipedia.org/wiki/Alexander_Blok) /// a Russian poet who is regarded as one of the most important figures of the Silver Age of Russian Poetry /// You can read the translation here: https://lyricstranslate.com/ru/белой-ночью-месяц-красный-white-night-crimson-crescent.html -static IN_THE_WHITE_NIGHT_CONTENT: &str = "Белой ночью месяц красный +static IN_THE_WHITE_NIGHT_CONTENT: &str = " +Белой ночью месяц красный Выплывает в синеве. Бродит призрачно-прекрасный, Отражается в Неве. @@ -46,512 +458,42 @@ static IN_THE_WHITE_NIGHT_CONTENT: &str = "Белой ночью месяц кр Красный месяц, тихий шум?.. "; -struct Fixture<'a> { +struct Files<'a> { file_names: &'a [&'a str], } -impl<'a> Fixture<'a> { +impl<'a> Files<'a> { fn new(file_names: &'a [&'a str]) -> Self { - Fixture { file_names } - } + for file_name in file_names { + let content = if file_name.ends_with("iliad.txt") { + ILIAD_CONTENT + } else if file_name.ends_with("midsummer-night.txt") { + MIDSUMMER_NIGHT_CONTENT + } else if file_name.ends_with("paradise-lost.txt") { + PARADISE_LOST_CONTENT + } else { + IN_THE_WHITE_NIGHT_CONTENT + }; + std::fs::write(file_name, content).unwrap_or_else(|_| { + panic!("Error setting up file '{file_name}' with the following content:\n{content}") + }); + } - fn set_up(&self) { - let file_name_content_pairs = self - .file_names - .iter() - .cloned() - .map(|file_name| { - if file_name.ends_with("iliad.txt") { - (file_name, ILIAD_CONTENT) - } else if file_name.ends_with("midsummer_night.txt") { - (file_name, MIDSUMMER_NIGHT_CONTENT) - } else if file_name.ends_with("paradise_lost.txt") { - (file_name, PARADISE_LOST_CONTENT) - } else { - (file_name, IN_THE_WHITE_NIGHT_CONTENT) - } - }) - .collect::>(); - - set_up_files(&file_name_content_pairs); + Self { file_names } } } -impl<'a> Drop for Fixture<'a> { +impl<'a> Drop for Files<'a> { fn drop(&mut self) { - tear_down_files(self.file_names); - } -} - -fn set_up_files(files: &[(&str, &str)]) { - for (file_name, file_content) in files { - fs::write(file_name, file_content).unwrap_or_else(|_| { - panic!( - "Error setting up file '{file_name}' with the following content:\n{file_content}" - ) - }); - } -} - -fn tear_down_files(files: &[&str]) { - for file_name in files { - fs::remove_file(file_name) - .unwrap_or_else(|_| panic!("Could not delete file '{file_name}'")); - } -} - -/// This macro is here so that every test case had its own set of files to be used in test. -/// The approach is to create required files for every test case and to append test name to the -/// file names (so for test with a name 'one_file_one_match_no_flags' and a required file -/// 'iliad.txt' there would be created a file with a name -/// 'one_file_one_match_no_flags_iliad.txt'). -/// This allows us to create files for every test case with no intersection between them. -/// -/// A better way would be to create required set of files at the start of tests run and to -/// delete them after every test is finished, but there is no trivial way to create such -/// a test fixture in standard Rust, and Exercism restricts the usage of external dependencies -/// in test files. Therefore the above approach is chosen. -/// -/// If you have an idea about a better way to implement test fixture for this exercise, -/// please submit PR to the Rust Exercism track: https://github.com/exercism/rust -macro_rules! set_up_test_case { - ($(#[$flag:meta])+ $test_case_name:ident(pattern=$pattern:expr, flags=[$($grep_flag:expr),*], files=[$($file:expr),+], expected=[$($expected:expr),*])) => { - $(#[$flag])+ - fn $test_case_name() { - let pattern = $pattern; - - let flags = vec![$($grep_flag),*]; - - let files = vec![$(concat!(stringify!($test_case_name), "_" , $file)),+]; - - let expected = vec![$($expected),*]; - - process_grep_case(pattern, &flags, &files, &expected); - } - }; - ($(#[$flag:meta])+ $test_case_name:ident(pattern=$pattern:expr, flags=[$($grep_flag:expr),*], files=[$($file:expr),+], prefix_expected=[$($expected:expr),*])) => { - $(#[$flag])+ - fn $test_case_name() { - let pattern = $pattern; - - let flags = vec![$($grep_flag),*]; - - let files = vec![$(concat!(stringify!($test_case_name), "_" , $file)),+]; - - let expected = vec![$(concat!(stringify!($test_case_name), "_", $expected)),*]; - - process_grep_case(pattern, &flags, &files, &expected); + for file_name in self.file_names { + std::fs::remove_file(file_name) + .unwrap_or_else(|e| panic!("Could not delete file '{file_name}': {e}")); } - } } -fn process_grep_case(pattern: &str, flags: &[&str], files: &[&str], expected: &[&str]) { - let test_fixture = Fixture::new(files); - - test_fixture.set_up(); - - let flags = Flags::new(flags); - - let grep_result = grep(pattern, &flags, files).unwrap(); - - assert_eq!(grep_result, expected); -} - -// Test returning a Result - -#[test] -fn nonexistent_file_returns_error() { - let pattern = "Agamemnon"; - - let flags = Flags::new(&[]); - - let files = vec!["nonexistent_file_returns_error_iliad.txt"]; - - assert!(grep(pattern, &flags, &files).is_err()); -} - -#[test] -#[ignore] -fn grep_returns_result() { - let pattern = "Agamemnon"; - - let flags = Flags::new(&[]); - - let files = vec!["grep_returns_result_iliad.txt"]; - - let test_fixture = Fixture::new(&files); - - test_fixture.set_up(); - - assert!(grep(pattern, &flags, &files).is_ok()); +impl<'a> AsRef<[&'a str]> for Files<'a> { + fn as_ref(&self) -> &[&'a str] { + self.file_names + } } - -// Test grepping a single file - -set_up_test_case!( - #[test] - #[ignore] - one_file_one_match_no_flags( - pattern = "Agamemnon", - flags = [], - files = ["iliad.txt"], - expected = ["Of Atreus, Agamemnon, King of men."] - ) -); - -set_up_test_case!( - #[test] - #[ignore] - one_file_one_match_print_line_numbers_flag( - pattern = "Forbidden", - flags = ["-n"], - files = ["paradise_lost.txt"], - expected = ["2:Of that Forbidden Tree, whose mortal tast"] - ) -); - -set_up_test_case!( - #[test] - #[ignore] - one_file_one_match_caseinsensitive_flag( - pattern = "FORBIDDEN", - flags = ["-i"], - files = ["paradise_lost.txt"], - expected = ["Of that Forbidden Tree, whose mortal tast"] - ) -); - -set_up_test_case!( - #[test] - #[ignore] - one_file_one_match_print_file_names_flag( - pattern = "Forbidden", - flags = ["-l"], - files = ["paradise_lost.txt"], - prefix_expected = ["paradise_lost.txt"] - ) -); - -set_up_test_case!( - #[test] - #[ignore] - one_file_one_match_match_entire_lines_flag( - pattern = "With loss of Eden, till one greater Man", - flags = ["-x"], - files = ["paradise_lost.txt"], - expected = ["With loss of Eden, till one greater Man"] - ) -); - -set_up_test_case!( - #[test] - #[ignore] - one_file_one_match_multiple_flags( - pattern = "OF ATREUS, Agamemnon, KIng of MEN.", - flags = ["-x", "-i", "-n"], - files = ["iliad.txt"], - expected = ["9:Of Atreus, Agamemnon, King of men."] - ) -); - -set_up_test_case!( - #[test] - #[ignore] - one_file_several_matches_no_flags( - pattern = "may", - flags = [], - files = ["midsummer_night.txt"], - expected = [ - "Nor how it may concern my modesty,", - "But I beseech your grace that I may know", - "The worst that may befall me in this case," - ] - ) -); - -set_up_test_case!( - #[test] - #[ignore] - one_file_several_matches_print_line_numbers_flag( - pattern = "may", - flags = ["-n"], - files = ["midsummer_night.txt"], - expected = [ - "3:Nor how it may concern my modesty,", - "5:But I beseech your grace that I may know", - "6:The worst that may befall me in this case," - ] - ) -); - -set_up_test_case!( - #[test] - #[ignore] - one_file_several_matches_match_entire_lines_flag( - pattern = "may", - flags = ["-x"], - files = ["midsummer_night.txt"], - expected = [] - ) -); - -set_up_test_case!( - #[test] - #[ignore] - one_file_several_matches_caseinsensitive_flag( - pattern = "ACHILLES", - flags = ["-i"], - files = ["iliad.txt"], - expected = [ - "Achilles sing, O Goddess! Peleus' son;", - "The noble Chief Achilles from the son" - ] - ) -); - -set_up_test_case!( - #[test] - #[ignore] - one_file_several_matches_inverted_flag( - pattern = "Of", - flags = ["-v"], - files = ["paradise_lost.txt"], - expected = [ - "Brought Death into the World, and all our woe,", - "With loss of Eden, till one greater Man", - "Restore us, and regain the blissful Seat,", - "Sing Heav'nly Muse, that on the secret top", - "That Shepherd, who first taught the chosen Seed" - ] - ) -); - -set_up_test_case!( - #[test] - #[ignore] - one_file_no_matches_various_flags( - pattern = "Gandalf", - flags = ["-n", "-l", "-x", "-i"], - files = ["iliad.txt"], - expected = [] - ) -); - -set_up_test_case!( - #[test] - #[ignore] - one_file_several_matches_inverted_and_match_entire_lines_flags( - pattern = "Illustrious into Ades premature,", - flags = ["-x", "-v"], - files = ["iliad.txt"], - expected = [ - "Achilles sing, O Goddess! Peleus' son;", - "His wrath pernicious, who ten thousand woes", - "Caused to Achaia's host, sent many a soul", - "And Heroes gave (so stood the will of Jove)", - "To dogs and to all ravening fowls a prey,", - "When fierce dispute had separated once", - "The noble Chief Achilles from the son", - "Of Atreus, Agamemnon, King of men." - ] - ) -); - -set_up_test_case!( - #[test] - #[ignore] - one_file_one_match_file_flag_takes_precedence_over_line_flag( - pattern = "ten", - flags = ["-n", "-l"], - files = ["iliad.txt"], - prefix_expected = ["iliad.txt"] - ) -); - -// Test grepping multiples files at once - -set_up_test_case!( - #[test] - #[ignore] - multiple_files_one_match_no_flags( - pattern = "Agamemnon", - flags = [], - files = ["iliad.txt", "midsummer_night.txt", "paradise_lost.txt"], - prefix_expected = ["iliad.txt:Of Atreus, Agamemnon, King of men."] - ) -); - -set_up_test_case!( - #[test] - #[ignore] - multiple_files_several_matches_no_flags( - pattern = "may", - flags = [], - files = ["iliad.txt", "midsummer_night.txt", "paradise_lost.txt"], - prefix_expected = [ - "midsummer_night.txt:Nor how it may concern my modesty,", - "midsummer_night.txt:But I beseech your grace that I may know", - "midsummer_night.txt:The worst that may befall me in this case," - ] - ) -); - -set_up_test_case!( - #[test] - #[ignore] - multiple_files_several_matches_print_line_numbers_flag( - pattern = "that", - flags = ["-n"], - files = ["iliad.txt", "midsummer_night.txt", "paradise_lost.txt"], - prefix_expected = [ - "midsummer_night.txt:5:But I beseech your grace that I may know", - "midsummer_night.txt:6:The worst that may befall me in this case,", - "paradise_lost.txt:2:Of that Forbidden Tree, whose mortal tast", - "paradise_lost.txt:6:Sing Heav'nly Muse, that on the secret top" - ] - ) -); - -set_up_test_case!( - #[test] - #[ignore] - multiple_files_one_match_print_file_names_flag( - pattern = "who", - flags = ["-l"], - files = ["iliad.txt", "midsummer_night.txt", "paradise_lost.txt"], - prefix_expected = ["iliad.txt", "paradise_lost.txt"] - ) -); - -set_up_test_case!( - #[test] - #[ignore] - multiple_files_several_matches_caseinsensitive_flag( - pattern = "TO", - flags = ["-i"], - files = ["iliad.txt", "midsummer_night.txt", "paradise_lost.txt"], - prefix_expected = [ - "iliad.txt:Caused to Achaia's host, sent many a soul", - "iliad.txt:Illustrious into Ades premature,", - "iliad.txt:And Heroes gave (so stood the will of Jove)", - "iliad.txt:To dogs and to all ravening fowls a prey,", - "midsummer_night.txt:I do entreat your grace to pardon me.", - "midsummer_night.txt:In such a presence here to plead my thoughts;", - "midsummer_night.txt:If I refuse to wed Demetrius.", - "paradise_lost.txt:Brought Death into the World, and all our woe,", - "paradise_lost.txt:Restore us, and regain the blissful Seat,", - "paradise_lost.txt:Sing Heav'nly Muse, that on the secret top" - ] - ) -); - -set_up_test_case!( - #[test] - #[ignore] - multiple_files_several_matches_caseinsensitive_flag_utf8( - pattern = "В", // This letter stands for cyrillic 'Ve' and not latin 'B'. Therefore there should be no matches from paradise_lost.txt - flags = ["-i"], - files = ["paradise_lost.txt", "in_the_white_night.txt"], - prefix_expected = [ - "in_the_white_night.txt:Выплывает в синеве.", - "in_the_white_night.txt:Отражается в Неве.", - "in_the_white_night.txt:Мне провидится и снится", - "in_the_white_night.txt:В вас ли доброе таится," - ] - ) -); - -set_up_test_case!( - #[test] - #[ignore] - multiple_files_several_matches_inverted_flag( - pattern = "a", - flags = ["-v"], - files = ["iliad.txt", "midsummer_night.txt", "paradise_lost.txt"], - prefix_expected = [ - "iliad.txt:Achilles sing, O Goddess! Peleus' son;", - "iliad.txt:The noble Chief Achilles from the son", - "midsummer_night.txt:If I refuse to wed Demetrius." - ] - ) -); - -set_up_test_case!( - #[test] - #[ignore] - multiple_files_one_match_match_entire_lines_flag( - pattern = "But I beseech your grace that I may know", - flags = ["-x"], - files = ["iliad.txt", "midsummer_night.txt", "paradise_lost.txt"], - prefix_expected = ["midsummer_night.txt:But I beseech your grace that I may know"] - ) -); - -set_up_test_case!( - #[test] - #[ignore] - multiple_files_one_match_multiple_flags( - pattern = "WITH LOSS OF EDEN, TILL ONE GREATER MAN", - flags = ["-n", "-i", "-x"], - files = ["iliad.txt", "midsummer_night.txt", "paradise_lost.txt"], - prefix_expected = ["paradise_lost.txt:4:With loss of Eden, till one greater Man"] - ) -); - -set_up_test_case!( - #[test] - #[ignore] - multiple_files_no_matches_various_flags( - pattern = "Frodo", - flags = ["-n", "-i", "-x", "-l"], - files = ["iliad.txt", "midsummer_night.txt", "paradise_lost.txt"], - expected = [] - ) -); - -set_up_test_case!( - #[test] - #[ignore] - multiple_files_several_matches_file_flag_takes_precedence_over_line_number_flag( - pattern = "who", - flags = ["-n", "-l"], - files = ["iliad.txt", "midsummer_night.txt", "paradise_lost.txt"], - prefix_expected = ["iliad.txt", "paradise_lost.txt"] - ) -); - -set_up_test_case!( - #[test] - #[ignore] - multiple_files_several_matches_inverted_and_match_entire_lines_flags( - pattern = "Illustrious into Ades premature,", - flags = ["-x", "-v"], - files = ["iliad.txt", "midsummer_night.txt", "paradise_lost.txt"], - prefix_expected = [ - "iliad.txt:Achilles sing, O Goddess! Peleus' son;", - "iliad.txt:His wrath pernicious, who ten thousand woes", - "iliad.txt:Caused to Achaia's host, sent many a soul", - "iliad.txt:And Heroes gave (so stood the will of Jove)", - "iliad.txt:To dogs and to all ravening fowls a prey,", - "iliad.txt:When fierce dispute had separated once", - "iliad.txt:The noble Chief Achilles from the son", - "iliad.txt:Of Atreus, Agamemnon, King of men.", - "midsummer_night.txt:I do entreat your grace to pardon me.", - "midsummer_night.txt:I know not by what power I am made bold,", - "midsummer_night.txt:Nor how it may concern my modesty,", - "midsummer_night.txt:In such a presence here to plead my thoughts;", - "midsummer_night.txt:But I beseech your grace that I may know", - "midsummer_night.txt:The worst that may befall me in this case,", - "midsummer_night.txt:If I refuse to wed Demetrius.", - "paradise_lost.txt:Of Mans First Disobedience, and the Fruit", - "paradise_lost.txt:Of that Forbidden Tree, whose mortal tast", - "paradise_lost.txt:Brought Death into the World, and all our woe,", - "paradise_lost.txt:With loss of Eden, till one greater Man", - "paradise_lost.txt:Restore us, and regain the blissful Seat,", - "paradise_lost.txt:Sing Heav'nly Muse, that on the secret top", - "paradise_lost.txt:Of Oreb, or of Sinai, didst inspire", - "paradise_lost.txt:That Shepherd, who first taught the chosen Seed" - ] - ) -);