Язык программирования Rust
Скачать 7.02 Mb.
|
Развитие функциональности библиотеки разработкой на основе тестов Теперь, когда мы извлекли логику в src/lib.rs и оставили разбор аргументов командной строки и обработку ошибок в src/main.rs, стало гораздо проще писать тесты для основной функциональности нашего кода. Мы можем вызывать функции напрямую с различными аргументами и проверить возвращаемые значения без необходимости вызова нашего двоичного файла из командной строки. В этом разделе в программу minigrep мы добавим логику поиска с использованием процесса разработки через тестирование (TDD). Это техника разработки программного обеспечения следует этим шагам: 1. Напишите тест, который не прошёл и запустите его, чтобы убедиться, что он не прошёл по той причине, которую вы ожидаете. 2. Пишите или изменяйте ровно столько кода, чтобы успешно выполнился новый тест. 3. Модифицируйте код, который вы только что добавили или изменили и убедитесь, что тесты продолжают проходить. 4. Повторите с шага 1! Этот процесс является лишь одним из многих способов написания программного обеспечения, но TDD также может помочь дизайну кода. Написание теста перед написанием кода, который делает тестовый прогон помогает поддерживать высокое покрытие тестированием в течение всего процесса. Мы протестируем реализацию функциональности, которая делает поиск строки запроса в содержимом файла и создание списка строк, соответствующих запросу. Мы добавим эту функциональность в функцию под названием search Написание теста с ошибкой Поскольку они нам больше не нужны, давайте удалим строки с println! , которые мы использовали для проверки поведения программы в src/lib.rs и src/main.rs. Затем в src/lib.rs мы добавим модуль tests с тестовой функцией, как делали это в главе 11 Тестовая функция определяет поведение, которое мы хотим проверить в функции search . Она должна принимать запрос и текст для поиска, а возвращать только те строки из текста, которые содержат запрос. В листинге 12-15 показан этот тест, он пока не компилируется. Файл: src/lib.rs Листинг 12-15. Создание теста с ошибкой для функции search , которую мы хотели бы получить Этот тест ищет строку "duct" . Текст, который мы ищем состоит из трёх строк, только одна из которых содержит "duct" (обратите внимание, что обратная косая черта после открывающей двойной кавычки говорит Rust не помещать символ новой строки в начало содержимого этого строкового литерала). Мы проверяем, что значение, возвращаемое функцией search , содержит только ожидаемую нами строку. Мы не можем запустить этот тест и увидеть сбой, потому что тест даже не компилируется: функции search ещё не существует! Так что мы добавим код, чтобы тест компилировался и запускался, написав определение функции search , которая всегда возвращает пустой вектор, как показано в листинге 12-16. Потом тест должен скомпилироваться и потерпеть неудачу при запуске, потому что пустой вектор не равен вектору, содержащему строку "safe, fast, productive." Файл: src/lib.rs Листинг 12-16. Определение функции search , достаточное, чтобы тест скомпилировался Заметьте, что в сигнатуре search нужно явно указать время жизни 'a для аргумента contents и возвращаемого значения. Напомним из Главы 10 , что параметры времени жизни указывают с временем жизни какого аргумента связано время жизни возвращаемого значения. В данном случае мы говорим, что возвращаемый вектор должен содержать срезы строк, ссылающиеся на содержимое аргумента contents (а не аргумента query ). Другими словами, мы говорим Rust, что данные, возвращаемые функцией search , будут жить до тех пор, пока живут данные, переданные в функцию search через аргумент contents . Это важно! Чтобы ссылки были действительными, данные, на которые #[cfg(test)] mod tests { use super::*; #[test] fn one_result () { let query = "duct" ; let contents = "\ Rust: safe, fast, productive. Pick three." ; assert_eq! ( vec! [ "safe, fast, productive." ], search(query, contents)); } } pub fn search < 'a >(query: & str , contents: & 'a str ) -> Vec <& 'a str > { vec! [] } ссылаются с помощью срезов тоже должны быть действительными; если компилятор предполагает, что мы делаем строковые срезы переменной query , а не переменной contents , он неправильно выполнит проверку безопасности. Если мы забудем аннотации времени жизни и попробуем скомпилировать эту функцию, то получим следующую ошибку: Rust не может понять, какой из двух аргументов нам нужен, поэтому нужно сказать ему об этом. Так как contents является тем аргументом, который содержит весь наш текст, и мы хотим вернуть части этого текста, которые совпали при поиске, мы понимаем, что contents является аргументом, который должен быть связан с возвращаемым значением временем жизни. Другие языки программирования не требуют от вас связывания в сигнатуре аргументов с возвращаемыми значениями. Хотя сейчас это может показаться странным, со временем станет понятнее. Можете сравнить этот пример с разделом «Валидация ссылок при помощи времён жизни» главы 10. Запустим тест: $ cargo build Compiling minigrep v0.1.0 (file:///projects/minigrep) error[E0106]: missing lifetime specifier --> src/lib.rs:28:51 | 28 | pub fn search(query: &str, contents: &str) -> Vec<&str> { | ---- ---- ^ expected named lifetime parameter | = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `query` or `contents` help: consider introducing a named lifetime parameter | 28 | pub fn search<'a>(query: &'a str, contents: &'a str) -> Vec<&'a str> { | ++++ ++ ++ ++ For more information about this error, try `rustc --explain E0106`. error: could not compile `minigrep` due to previous error Отлично. Наш тест не сработал, как мы и ожидали. Давайте сделаем так, чтобы он срабатывал! Написание кода для прохождения теста Сейчас наш тест не проходит, потому что мы всегда возвращаем пустой вектор. Чтобы исправить это и реализовать search , наша программа должна выполнить следующие шаги: Итерироваться по каждой строке содержимого. Проверить, содержит ли данная строка искомую. Если это так, добавить её в список значений, которые мы возвращаем. Если это не так, ничего не делать. Вернуть список результатов. Давайте проработаем каждый шаг, начиная с перебора строк. Перебор строк с помощью метода lines В Rust есть полезный метод для построчной итерации строк, удобно названый lines , как показано в листинге 12-17. Обратите внимание, код пока не компилируется. Файл: src/lib.rs $ cargo test Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished test [unoptimized + debuginfo] target(s) in 0.97s Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) running 1 test test tests::one_result ... FAILED failures: ---- tests::one_result stdout ---- thread 'main' panicked at 'assertion failed: `(left == right)` left: `["safe, fast, productive."]`, right: `[]`', src/lib.rs:44:9 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::one_result test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass '--lib' Листинг 12-17: Итерация по каждой строке из contents Метод lines возвращает итератор. Мы подробно поговорим об итераторах в Главе 13 , но вспомните, что вы видели этот способ использования итератора в Листинге 3-5 , где мы использовали цикл for с итератором, чтобы выполнить некоторый код для каждого элемента в коллекции. Поиск в каждой строке текста запроса Далее мы проверяем, содержит ли текущая строка нашу искомую строку. К счастью, у строк есть полезный метод contains , который именно это и делает! Добавьте вызов метода contains в функции search , как показано в листинге 12-18. Обратите внимание, что это все ещё не компилируется. Файл: src/lib.rs Листинг 12-18. Добавление проверки, содержится ли query в строке Сохранение совпавшей строки Нам также нужен способ хранить строки, содержащие искомую строку. Для этого мы можем создать изменяемый вектор перед циклом for и вызывать метод push для сохранения line в векторе. После цикла for мы возвращаем вектор, как показано в листинге 12-19. Файл: src/lib.rs pub fn search < 'a >(query: & str , contents: & 'a str ) -> Vec <& 'a str > { for line in contents.lines() { // do something with line } } pub fn search < 'a >(query: & str , contents: & 'a str ) -> Vec <& 'a str > { for line in contents.lines() { if line.contains(query) { // do something with line } } } Листинг 12-19. Сохранение совпадающих строк, чтобы вернуть их Теперь функция search должна возвратить только строки, содержащие query , и тест должен пройти. Запустим его: Наш тест пройден, значит он работает! На этом этапе мы могли бы рассмотреть возможности изменения реализации функции поиска, сохраняя прохождение тестов и поддерживая имеющуюся функциональность. Код в функции поиска не так уж плох, но он не использует некоторые полезные функции итераторов. Вернёмся к этому примеру в главе 13 , где будем исследовать итераторы подробно, и посмотрим как его улучшить. Использование функции search в функции run pub fn search < 'a >(query: & str , contents: & 'a str ) -> Vec <& 'a str > { let mut results = Vec ::new(); for line in contents.lines() { if line.contains(query) { results.push(line); } } results } $ cargo test Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished test [unoptimized + debuginfo] target(s) in 1.22s Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) running 1 test test tests::one_result ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests minigrep running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Теперь, когда функция search работает и протестирована, нужно вызвать search из нашей функции run . Нам нужно передать значение config.query и contents , которые run читает из файла, в функцию search . Тогда run напечатает каждую строку, возвращаемую из search : Файл: src/lib.rs Мы по-прежнему используем цикл for для возврата каждой строки из функции search и её печати. Теперь вся программа должна работать! Давайте попробуем сначала запустить её со словом «frog», которое должно вернуть только одну строчку из стихотворения Эмили Дикинсон: Здорово! Теперь давайте попробуем слово, которое будет соответствовать нескольким строкам, например «body»: И наконец, давайте удостоверимся, что мы не получаем никаких строк, когда ищем слово, отсутствующее в стихотворении, например «monomorphization»: Отлично! Мы создали собственную мини-версию классического инструмента и научились тому, как структурировать приложения. Мы также немного узнали о файловом вводе и pub fn run (config: Config) -> Result <(), Box < dyn Error>> { let contents = fs::read_to_string(config.file_path)?; for line in search(&config.query, &contents) { println! ( "{line}" ); } Ok (()) } $ cargo run -- frog poem.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.38s Running `target/debug/minigrep frog poem.txt` How public, like a frog $ cargo run -- body poem.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep body poem.txt` I'm nobody! Who are you? Are you nobody, too? How dreary to be somebody! $ cargo run -- monomorphization poem.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep monomorphization poem.txt` выводе, временах жизни, тестировании и разборе аргументов командной строки. Чтобы завершить этот проект, мы кратко продемонстрируем пару вещей: как работать с переменными окружения и как печатать в стандартный поток ошибок, обе из которых полезны при написании консольных программ. Работа с переменными окружения Мы улучшим minigrep , добавив дополнительную функцию: опцию для поиск без учёта регистра, которую пользователь может включить с помощью переменной среды окружения. Мы могли бы сделать эту функцию параметром командной строки и потребовать, чтобы пользователи вводили бы её каждый раз при её применении, но вместо этого мы будем использовать переменную среды окружения. Это позволит нашим пользователям устанавливать переменную среды один раз и все поиски будут не чувствительны к регистру в этом терминальном сеансе. Написание ошибочного теста для функции search с учётом регистра Мы хотим добавить новую функцию search_case_insensitive , которую мы будем вызывать, когда переменная окружения включена. Мы продолжим следовать процессу TDD, поэтому первый шаг - это снова написать не проходящий тест. Мы добавим новый тест для новой функции search_case_insensitive и переименуем наш старый тест из one_result в case_sensitive , чтобы прояснить различия между двумя тестами, как показано в листинге 12-20. Файл: src/lib.rs Листинг 12-20. Добавление нового не проходящего теста для функции поиска нечувствительной к регистру, которую мы собираемся добавить Обратите внимание, что мы также отредактировали содержимое переменной contents из старого теста. Мы добавили новую строку с текстом "Duct tape." , используя заглавную D, которая не должна соответствовать запросу "duct" при поиске с учётом регистра. Такое изменение старого теста помогает избежать случайного нарушения функциональности поиска чувствительного к регистру, который мы уже реализовали. Этот тест должен пройти сейчас и должен продолжать выполняться успешно, пока мы работаем над поиском без учёта регистра. Новый тест для поиска нечувствительного к регистру использует "rUsT" качестве строки запроса. В функции search_case_insensitive , которую мы собираемся реализовать, запрос "rUsT" должен соответствовать строке содержащей "Rust:" с большой буквы R и соответствовать строке "Trust me." , хотя обе имеют разные регистры из запроса. Это наш не проходящий тест, он не компилируется, потому что мы ещё не определили функцию search_case_insensitive . Не стесняйтесь добавлять скелет реализация, которая всегда возвращает пустой вектор, аналогично тому, как мы это делали для функции search в листинге 12-16, чтобы увидеть компиляцию теста и его сбой. #[cfg(test)] mod tests { use super::*; #[test] fn case_sensitive () { let query = "duct" ; let contents = "\ Rust: safe, fast, productive. Pick three. Duct tape." ; assert_eq! ( vec! [ "safe, fast, productive." ], search(query, contents)); } #[test] fn case_insensitive () { let query = "rUsT" ; let contents = "\ Rust: safe, fast, productive. Pick three. Trust me." ; assert_eq! ( vec! [ "Rust:" , "Trust me." ], search_case_insensitive(query, contents) ); } } Реализация функции search_case_insensitive Функция search_case_insensitive , показанная в листинге 12-21, будет почти такая же, как функция search . Разница лишь в том, что текст будет в нижнем регистре для query и для каждой line , так что для любого регистра входных аргументов это будет тот же случай, когда мы проверяем, содержит ли строка запрос. Файл: src/lib.rs Листинг 12-21. Определение функции search_case_insensitive с уменьшением регистра строки запроса и строки содержимого перед их сравнением Сначала преобразуем в нижний регистр строку query и сохраняем её в затенённой переменной с тем же именем. Вызов to_lowercase для строки запроса необходим, так что независимо от того, будет ли пользовательский запрос "rust" , "RUST" , "Rust" или "rUsT" , мы будем преобразовывать запрос к "rust" и делать значение нечувствительным к регистру. Хотя to_lowercase будет обрабатывать Unicode, он не будет точным на 100%. Если бы мы писали реальное приложение, мы бы хотели проделать здесь немного больше работы, но этот раздел посвящён переменным среды, а не Unicode, поэтому мы оставим это здесь. Обратите внимание, что query теперь имеет тип String , а не срез строки, потому что вызов to_lowercase создаёт новые данные, а не ссылается на существующие. К примеру, запрос: "rUsT" это срез строки не содержащий строчных букв u или t , которые мы можем использовать, поэтому мы должны выделить новую String , содержащую «rust» Когда мы передаём запрос query в качестве аргумента метода contains , нам нужно добавить амперсанд, поскольку сигнатура contains , определена для приёмы среза строки. Затем мы добавляем вызов to_lowercase у каждой строки line , прежде чем проверять, содержит ли она query из всех строчных символов. Теперь, когда мы преобразовали line и query в нижний регистр, мы найдём совпадения независимо от того, в каком регистре находится переменная с запросом. pub fn search_case_insensitive < 'a >( query: & str , contents: & 'a str , ) -> Vec <& 'a str > { let query = query.to_lowercase(); let mut results = Vec ::new(); for line in contents.lines() { if line.to_lowercase().contains(&query) { results.push(line); } } results } Давайте посмотрим, проходит ли эта реализация тесты: Отлично! Тесты прошли. Теперь давайте вызовем новую функцию search_case_insensitive из функции run . Во-первых, мы добавим параметр конфигурации в структуру Config для переключения между поиском с учётом регистра и без учёта регистра. Добавление этого поля приведёт к ошибкам компилятора, потому что мы ещё нигде не инициализируем это поле: Файл: src/lib.rs Обратите внимание, что мы добавили поле case_sensitive , которое содержит логическое значение. Далее нам нужна функция run , чтобы проверить значение поля case_sensitive и использовать его, чтобы решить, вызывать ли функцию search или функцию search_case_insensitive , как показано в листинге 12-22. Обратите внимание, что код все ещё не компилируется. Файл: src/lib.rs $ cargo test Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished test [unoptimized + debuginfo] target(s) in 1.33s Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) running 2 tests test tests::case_insensitive ... ok test tests::case_sensitive ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests minigrep running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s pub struct Config { pub query: String , pub file_path: String , pub ignore_case: bool , } |