Rust er kjent за sin strikta system for управление ошибок и гарантией безопасности. В частности, механизм работы с ошибками через Result и модуль тестирования предоставляют мощные инструменты для разработчиков, стремящихся к созданию надежных и хорошо протестированных программ. В этой главе рассматривается, как можно использовать функциональность Rust для тестирования командных приложений, включая работу с типами ошибок и аргументами командной строки.

Основным принципом при работе с Rust является использование типа Result, который имеет две возможные вариации: Ok и Err. Эти типы служат для явного указания на успешное выполнение функции или на наличие ошибки. Например, в тестах для проверки команд можно использовать Result для возвращения значений, которые указывают на успешное выполнение операции или на ошибку.

Работа с командой в тестах

Одним из ключевых элементов тестирования является взаимодействие с командной строкой через программу. Rust предоставляет несколько удобных инструментов для тестирования командных приложений. В частности, с помощью библиотеки assert_cmd можно запускать команды и проверять их вывод. Это позволяет эффективно проверять, как программа реагирует на различные входные данные.

Пример кода:

rust
use anyhow::Result;
use assert_cmd::Command; use predicates::prelude::*; use std::fs; #[test] fn dies_no_args() -> Result<()> {
let mut cmd = Command::cargo_bin("echor")?;
cmd.
assert() .failure() .stderr(predicate::str::contains("Usage")); Ok(()) }

В приведенном примере создается тест, который запускает программу echor без аргументов. Мы ожидаем, что программа завершится с ошибкой, а в её выводе будет содержаться строка "Usage". Использование assert() из библиотеки assert_cmd позволяет проверить поведение программы с учётом различных условий.

Работа с несколькими аргументами и файлами

Для более сложных тестов, когда программа принимает несколько аргументов или использует файлы для сравнения, можно создать вспомогательные функции. Например, функция run принимает аргументы и файл с ожидаемым выводом, запускает команду и проверяет, что результат работы программы совпадает с ожидаемым:

rust
fn run(args: &[&str], expected_file: &str) -> Result<()> {
let expected = fs::read_to_string(expected_file)?; let output = Command::cargo_bin("echor")? .args(args) .output() .expect("fail"); let stdout = String::from_utf8(output.stdout).expect("invalid UTF-8"); assert_eq!(stdout, expected); Ok(()) }

Этот подход позволяет удобно запускать несколько тестов, передавая различные наборы аргументов и соответствующие им ожидаемые результаты.

Важность использования anyhow::Result

Использование типа anyhow::Result вместо стандартного Result позволяет упростить обработку ошибок. anyhow::Result автоматически обрабатывает ошибочные типы, избавляя от необходимости вручную указывать конкретные ошибки для каждого возможного случая. Это полезно, особенно когда речь идет о большом количестве операций с возможными ошибками.

Структура аргументов с использованием clap

Кроме стандартных инструментов для тестирования команд, важно также правильно обрабатывать аргументы командной строки. Одним из популярных решений для этой задачи является библиотека clap, которая позволяет легко парсить аргументы командной строки.

rust
use clap::Parser; #[derive(Debug, Parser)] #[command(author, version, about)] struct Args { #[arg(required(true))] text: Vec<String>, #[arg(short('n'))] omit_newline: bool, } fn main() { let args = Args::parse(); dbg!(args); }

С помощью атрибута #[derive(Parser)] можно автоматически создать структуру для парсинга аргументов. Этот подход упрощает обработку флагов и параметров командной строки, позволяя сосредоточиться на бизнес-логике программы.

Как всё это связано с тестированием

Приведенные тесты и примеры с использованием clap и assert_cmd показывают, как важно правильно настроить обработку аргументов командной строки и проверку результатов работы программы. Это помогает избежать ошибок, связанных с неправильным пониманием команд, и позволяет с легкостью поддерживать тесты, когда приложение изменяется.

Важно помнить, что в процессе тестирования необходимо учитывать не только саму логику программы, но и то, как она взаимодействует с пользователем через интерфейс командной строки. Тестирование выводов, ошибок и состояния программы помогает гарантировать, что приложение будет работать как ожидается в реальных условиях.

Hvordan bygge et eget program for tekststatistikk i Rust

I arbeidet med utviklingen av programmet som teller linjer, ord og byte i filer, er det viktig å forstå hvordan funksjoner og tester fungerer for å formatere og vise dataene på en effektiv måte. Et sentralt element i denne prosessen er funksjonen format_field, som er designet for å returnere et formatert strengenavn eller en tom streng basert på en betingelse. Denne funksjonen kan være nyttig for å strukturere utdataene på en tydelig og konsistent måte.

Funksjonen format_field tar to argumenter: en usize verdi og en boolsk verdi. Hvis den boolske verdien er sann, returnerer funksjonen et formatert tall med 8 tegn, ellers returnerer den en tom streng. Dette kan være veldig nyttig når man arbeider med programutdata som skal vises i et tabellformat, der det er nødvendig å justere bredden på kolonnene.

rust
fn format_field(value: usize, show: bool) -> String { if show { format!("{value:>8}") } else { "".to_string() } }

Funksjonen returnerer en String i stedet for en str, selv om begge representerer tekst. Grunnen til dette valget er at String er en dynamisk allokert, vokst streng, mens str er en fast størrelse og immutabel. I dette tilfellet er utdataene som skal genereres av funksjonen dynamiske og kan variere i lengde, derfor er det nødvendig å bruke String.

For å teste denne funksjonen, kan vi utvide testmodulen med en enhetstest som kontrollerer om funksjonen fungerer som forventet. Testene bør sjekke at den tomme strengen returneres når show er usann, og at tallene blir korrekt formatert når show er sann.

rust
#[cfg(test)]
mod tests { use super::{count, format_field, FileInfo}; use std::io::Cursor; #[test] fn test_format_field() { assert_eq!(format_field(1, false), "");
assert_eq!(format_field(3, true), " 3");
assert_eq!(format_field(10, true), " 10"); } }

Med testene på plass kan vi være sikre på at funksjonen gjør det den skal: den formaterer utdataene på en konsistent måte avhengig av de angitte betingelsene.

I konteksten av programmet som teller linjer, ord, byte og tegn i filer, er det viktig å merke seg at programmet kan håndtere flere filer på en gang. Det innebærer at hvis flere filer behandles, blir det nødvendig å holde styr på totalene for hver statistikktype. For dette kan vi bruke mutable variabler til å akkumulere totalene, som deretter kan skrives ut etter at alle filer er behandlet.

Her er en oppdatert versjon av run-funksjonen som håndterer flere filer og skriver ut totalsummene når flere filer behandles:

rust
fn run(mut args: Args) -> Result<()> { if [args.words, args.bytes, args.chars, args.lines] .iter() .all(|v| v == &false) { args.lines = true; args.words = true; args.bytes = true; } let mut total_lines = 0; let mut total_words = 0; let mut total_bytes = 0; let mut total_chars = 0; for filename in &args.files { match open(filename) { Err(err) => eprintln!("{filename}: {err}"), Ok(file) => { let info = count(file)?; println!( "{}{}{}{}{}", format_field(info.num_lines, args.lines), format_field(info.num_words, args.words), format_field(info.num_bytes, args.bytes), format_field(info.num_chars, args.chars), if filename.as_str() == "-" { "".to_string() } else { format!(" {}", filename) } ); total_lines += info.num_lines; total_words += info.num_words; total_bytes += info.num_bytes; total_chars += info.num_chars; } } } if args.files.len() > 1 { println!( "{}{}{}{} total", format_field(total_lines, args.lines), format_field(total_words, args.words), format_field(total_bytes, args.bytes), format_field(total_chars, args.chars) ); } Ok(()) }

Denne funksjonen oppdaterer de totale verdiene for linjer, ord, byte og tegn ved å iterere gjennom filene, og skriver ut de akkumulerte resultatene etter at alle filene er behandlet.

Det er også viktig å merke seg at programmet kan tilpasses for å bruke forskjellige alternativer, for eksempel for å vise eller skjule kolonner, eller for å bruke forskjellige metoder for telling (for eksempel å telle tegn i stedet for byte). Dette kan gjøre programmet mer fleksibelt og i stand til å håndtere flere ulike scenarier.

Når du bygger et program som etterligner verktøy som wc (word count), er det viktig å vurdere forskjellene mellom de forskjellige implementasjonene, som den som finnes i GNU-versjonen og BSD-versjonen. Forskjellene kan for eksempel ligge i hvordan spesifikke tegn (som mellomrom) håndteres, eller hvordan filene tolkes når de inneholder spesielle tegn eller ikke-standard tegnsett.

Et annet interessant aspekt ved utviklingen av et slikt program er forståelsen av hvordan tekst behandles i forskjellige kodesett og hva som skjer når programmet møter tekst med ulike tegnsett (som japanske tegn). Dette kan føre til uventede resultater, som hvordan antall ord kan variere avhengig av kodesettet som brukes.

For å gjøre programmet enda mer robust, kan det være nyttig å legge til flere funksjoner og alternativer, for eksempel støtte for å lese filnavn fra en fil eller å vise lengden på den lengste linjen i stedet for byte-telling. Dette vil gjøre programmet mer komplett og nyttig for forskjellige brukstilfeller.

Endelig er det viktig å huske på at dette programmet er et relativt enkelt eksempel på hvordan Rust kan brukes til å implementere et praktisk verktøy, men det kan utvides og tilpasses til å møte mer spesifikke behov, avhengig av hva slags data man ønsker å analysere og hvordan man ønsker å vise resultatene.

Hvordan håndtere delimiter og posisjonsvalg i Rust-programmer

Når vi skriver programmer som håndterer tekst og data, møter vi ofte på utfordringer knyttet til innlesing av data, spesielt når vi bruker CSV-filer eller andre format som krever spesifikasjon av delimitere og indekser for felt. Dette kan være et komplekst problem når dataene ikke er formatert som forventet. Her skal vi utforske hvordan man kan validere og håndtere delimiter i et Rust-program, samt hvordan vi kan implementere funksjoner for å håndtere og validere posisjonsvalg.

En av de første utfordringene vi møter på, er hvordan man definerer og håndterer delimiter i et Rust-program, spesielt når input kan inneholde uventede eller feilaktige tegn. Et eksempel på dette er programmet som krever at delimiteren skal være en enkelt byte (u8). Dette kan forårsake problemer dersom delimiteren består av flere tegn, som i testtilfellet hvor delimiteren er satt til ",,".

For å håndtere slike feil på en effektiv måte, kan vi bruke anyhow::Result og bail!-makroen. Ved å bruke as_bytes() på strengen som representerer delimiteren, kan vi konvertere strengen til en byte-representasjon og sjekke om lengden på denne byte-representasjonen er nøyaktig 1. Hvis lengden ikke stemmer, kan vi returnere en feilmelding som forteller brukeren hvordan de kan rette opp inputen. Dette kan gjøres med følgende kode:

rust
fn run(args: Args) -> Result<()> {
let delim_bytes = args.delimiter.as_bytes(); if delim_bytes.len() != 1 { bail!(r#"--delim "{}" must be a single byte"#, args.delimiter); } let delimiter: u8 = *delim_bytes.first().unwrap(); println!("{delimiter}"); Ok(()) }

I denne koden bruker vi first() for å hente den første byte i vektoren som representerer delimiteren, og unwrap() er trygg fordi vi allerede har verifisert at vektoren har akkurat én byte.

I tillegg til delimiter-validering, er det også viktig å håndtere posisjonsvalg for feltene, byte, eller tegn som skal ekstraheres fra inngangsdataene. Dette kan være en utfordring når brukerens input kan være i form av individuelle tall eller intervaller, som for eksempel "2-4" eller "1,3,5". For å håndtere dette på en robust måte, kan vi bruke en enum som beskriver hvilke typer data som skal ekstraheres – enten felt, byte eller tegn. En slik enum kan se ut som dette:

rust
type PositionList = Vec<std::ops::Range<usize>>;
#[derive(Debug)] pub enum Extract { Fields(PositionList), Bytes(PositionList), Chars(PositionList), }

Denne enum-en gjør det mulig å definere en type for de ulike datavariantene som vi ønsker å ekstrahere, og gjør det lettere å validere inputen.

For å validere brukerens input når det gjelder intervaller og enkeltnumre, kan vi bruke en funksjon som parse_pos(). Denne funksjonen skal håndtere både enkeltnumre og intervaller, og den skal validere at inputen er riktig formatert. Her er en start på hvordan denne funksjonen kan implementeres:

rust
fn parse_pos(range: String) -> Result<PositionList> {
unimplemented!(); }

Funksjonen må validere en rekke ting: den kan ikke tillate ledende nuller, den kan ikke tillate ikke-numeriske tegn, og intervaller må være formatert som "start-slutt". Vi kan implementere en rekke tester for å sørge for at inputen er riktig formatert, som for eksempel å avvise "0" eller "+1", eller å håndtere tilfeller hvor intervallene er ugyldige, som "1-1" eller "2-1". Dette kan gjøres med et sett av tester som sikrer at programmet reagerer korrekt på feil input:

rust
#[cfg(test)] mod unit_tests { use super::parse_pos; #[test] fn test_parse_pos() {
assert!(parse_pos("".to_string()).is_err());
let res = parse_pos("0".to_string()); assert!(res.is_err());
assert_eq!(res.unwrap_err().to_string(), r#"illegal list value: "0""#);
let res = parse_pos("0-1".to_string()); assert!(res.is_err());
assert_eq!(res.unwrap_err().to_string(), r#"illegal list value: "0""#);
let res = parse_pos("+1".to_string()); assert!(res.is_err());
assert_eq!(res.unwrap_err().to_string(), r#"illegal list value: "+1""#);
let res = parse_pos("1-1".to_string()); assert!(res.is_err());
assert_eq!(res.unwrap_err().to_string(), "First number in range (1) must be lower than second number (1)");
let res = parse_pos("1-2".to_string()); assert!(res.is_ok()); assert_eq!(res.unwrap(), vec![0..1, 1..2]); } }

Testene sikrer at programmet reagerer korrekt på ulike feil og bare tillater gyldige intervaller og tall.

Det er viktig å forstå at når vi bruker indekser i Rust, starter de fra 0. Derfor må vi sørge for at alle posisjonsvalgene som gis av brukeren blir justert til nullindeksert form. For eksempel, dersom en bruker spesifiserer posisjonen "2", bør dette konverteres til indeksen 1 i programmet vårt. Dette kan kreve at vi trekker 1 fra hver posisjon i intervallet.

En annen viktig del av programmet er hvordan vi håndterer ulike formaterte data, og hvordan vi sørger for at inputene våre er nøyaktige før vi prøver å bearbeide dem videre. Dette er spesielt viktig i programmer som jobber med CSV-filer eller annen tekstbasert input, hvor feil input kan føre til alvorlige feil i behandlingen av dataene.

Hvordan bruke clap til å analysere kommandolinjeargumenter i Rust

Å arbeide med kommandolinjeargumenter kan virke som en enkel oppgave ved første øyekast, men når programmene blir mer komplekse, vil behovet for et robust verktøy for å analysere argumentene vokse. I denne sammenhengen har vi valgt å bruke clap, et populært crate for kommandolinjeanalyse i Rust, for å håndtere argumentene på en strukturert og effektiv måte.

For å komme i gang, må vi fortelle Cargo at vi ønsker å laste ned clap-craten og bruke den i prosjektet vårt. Dette gjøres ved å legge det til som en avhengighet i filen Cargo.toml:

toml
[package] name = "echor" version = "0.1.0" edition = "2021" [dependencies] clap = "4.5.0"

Her angir vi versjonen "4.5.0" for å bruke nøyaktig denne versjonen av clap. Ved å kjøre en cargo build, vil Cargo automatisk laste ned koden for clap og dens avhengigheter. En viktig ting å merke seg er at Cargo plasserer disse nedlastede kildene i .cargo-mappen i hjemmemappen, mens de byggede artefaktene finner veien til target/debug/deps.

En interessant egenskap ved Rusts prosjektstruktur er at hvert program kan bruke forskjellige versjoner av crate-er, og hvert prosjekt bygges i sin egen mappe. Dette eliminerer problemene med delte moduler som vi ser i andre språk som Perl og Python, og gir mer kontroll over prosjektavhengigheter. Dette systemet gir en høy grad av sikkerhet mot versjonskonflikter, og gjør det lettere å jobbe med flere prosjekter samtidig.

Nå som avhengighetene er på plass, kan vi begynne å analysere kommandolinjeargumentene ved hjelp av clap. Det finnes flere måter å gjøre dette på, men i vårt tilfelle velger vi den såkalte "builder"-metoden. Her lager vi et nytt Command-objekt som håndterer argumentene:

rust
use clap::Command;
fn main() { let _matches = Command::new("echor") .version("0.1.0") .author("Ken Youens-Clark ") .about("Rust version of `echo`") .get_matches(); }

Denne koden setter opp et program som heter echor, definerer en versjon og legger til informasjon om forfatteren. Når vi kjører programmet med -h eller --help-flaggene, får vi automatisk en bruksanvisning generert av clap, som viser tilgjengelige alternativer for programmet:

bash
$ cargo run -- -h Rust version of `echo` Usage: echor [OPTIONS] ... Options: -h, --help Print help -V, --version Print version

Dette er bare en del av funksjonaliteten. Nå kan vi begynne å definere de spesifikke argumentene som vårt program skal akseptere. Vi bruker clap::Arg for å definere både obligatoriske og valgfrie argumenter, og clap::ArgAction for å definere hva som skal skje når hvert argument er til stede. For eksempel, i vårt tilfelle definerer vi et obligatorisk tekstargument og en valgfri flagg for å unngå linjeskift:

rust
use clap::{Arg, ArgAction, Command};
fn main() { let matches = Command::new("echor") .version("0.1.0") .author("Ken Youens-Clark ") .about("Rust version of `echo`") .arg( Arg::new("text") .value_name("TEXT") .help("Input text") .required(true) .num_args(1..), ) .arg( Arg::new("omit_newline") .short('n') .action(ArgAction::SetTrue) .help("Do not print newline"), ) .get_matches(); println!("{:#?}", matches); }

I denne koden definerer vi et obligatorisk text-argument som må oppgis ved kjøring, samt et valgfritt omit_newline-flagg som styrer om det skal være linjeskift etter teksten. Når programmet kjøres med forskjellige argumenter, kan vi inspisere strukturen av argumentene, som vist nedenfor:

arduino
$ cargo run -- -n Hello world ArgMatches { valid_args: [ "text", "omit_newline", "help", "version", ], ... }

Hvis du prøver å kjøre programmet uten å spesifisere det obligatoriske text-argumentet, vil du få en feil som påminner deg om at argumentet er påkrevd:

javascript
$ cargo run
error: the following required arguments were not provided: <TEXT> Usage: echor [OPTIONS] <TEXT> For more information, try '--help'.

clap håndterer automatisk feilene for deg, og returnerer en ikke-null exit-kode. Dette gjør at programmet kan stoppe og rapportere feil på en kontrollert måte, noe som er essensielt for produksjonsklar programvare.

En annen viktig funksjon ved clap er håndteringen av udefinerte argumenter. Hvis du gir programmet et argument som ikke er definert, vil det resultere i en feil og et passende feiluttrykk:

python
$ cargo run -- -x error: unexpected argument '-x' found tip: to pass '-x' as a value, use '-- -x' Usage: echor [OPTIONS] ... For more information, try '--help'.

Denne feilhåndteringen skjer via Command::get_matches, som avslutter programmet ved feil og skriver ut relevante feilmeldinger. En annen ting som er interessant å merke seg, er at når clap skriver ut feilmeldinger eller bruksanvisning, skjer dette via STDERR, mens all vanlig utdata skjer via STDOUT. Dette gir bedre kontroll over hvordan programmet kommuniserer med brukeren, og gjør det enklere å håndtere utdata i forskjellige sammenhenger.

I tillegg bør man være oppmerksom på hvordan clap håndterer standardinnstillinger og feil. For eksempel, når du definerer argumenter med spesifikke handlinger som SetTrue (for flagg som -n), gjør clap alt arbeidet for deg, noe som gir deg mer fokus på programlogikken.