1 - פרויקט מסכם - פתרון פרויקט
פתרון פרויקט קורא הגדרות¶
הפתרון הבא מחזיק את הקלט בבעלות של תוכנית הפקודה ומחזיר מן הספרייה רק מבטים אליו. שם כפול מזוהה לפני החלפת ערך, וכך שגיאה אינה מתקנת בשקט קלט עמום.
צרו חבילה בשם settings_check. הכניסו ל־src/lib.rs:
use std::collections::BTreeMap;
#[derive(Debug, PartialEq)]
pub enum Kind {
MissingSeparator,
EmptyName,
EmptyValue,
Duplicate,
}
#[derive(Debug, PartialEq)]
pub struct ParseError {
pub line: usize,
pub kind: Kind,
}
pub fn parse(input: &str) -> Result<BTreeMap<&str, &str>, ParseError> {
let mut settings = BTreeMap::new();
for (offset, raw) in input.lines().enumerate() {
let line = offset + 1;
let raw = raw.trim();
if raw.is_empty() || raw.starts_with('#') {
continue;
}
let (name, value) = raw.split_once('=')
.ok_or(ParseError { line, kind: Kind::MissingSeparator })?;
let name = name.trim();
let value = value.trim();
if name.is_empty() {
return Err(ParseError { line, kind: Kind::EmptyName });
}
if value.is_empty() {
return Err(ParseError { line, kind: Kind::EmptyValue });
}
if settings.contains_key(name) {
return Err(ParseError { line, kind: Kind::Duplicate });
}
settings.insert(name, value);
}
Ok(settings)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalizes_and_borrows() {
let input = "# comment\n name = worker\nmode=fast\n";
let result = parse(input).unwrap();
assert_eq!(result.get("name"), Some(&"worker"));
assert_eq!(result.get("mode"), Some(&"fast"));
}
#[test]
fn rejects_invalid_settings() {
assert_eq!(parse("broken"), Err(ParseError { line: 1, kind: Kind::MissingSeparator }));
assert_eq!(parse("=x"), Err(ParseError { line: 1, kind: Kind::EmptyName }));
assert_eq!(parse("x="), Err(ParseError { line: 1, kind: Kind::EmptyValue }));
assert_eq!(parse("x=1\nx=2"), Err(ParseError { line: 2, kind: Kind::Duplicate }));
}
#[test]
fn accepts_utf8() {
let result = parse("שם=עמית").unwrap();
assert_eq!(result.get("שם"), Some(&"עמית"));
}
}
הפונקציות trim ו־split_once מחזירות מבטים למקור. ההכנסה למפה מעתיקה את ההפניות, ולא את הבייטים של השם והערך. לעומת זאת, המפה עצמה עדיין עשויה להקצות צמתים; הביטוי ללא העתקת טקסט אינו אומר ללא שום הקצאה.
הכניסו ל־src/main.rs:
use std::io::{self, Write};
use std::process::ExitCode;
fn run() -> Result<(), String> {
let mut args = std::env::args().skip(1);
let path = args.next().ok_or("usage: settings_check PATH")?;
if args.next().is_some() {
return Err("expected one path".into());
}
let input = std::fs::read_to_string(&path)
.map_err(|error| format!("cannot read {path}: {error}"))?;
let settings = settings_check::parse(&input)
.map_err(|error| format!("line {}: {:?}", error.line, error.kind))?;
let stdout = io::stdout();
let mut output = stdout.lock();
for (name, value) in settings {
writeln!(output, "{name}={value}").map_err(|error| error.to_string())?;
}
Ok(())
}
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
let _ = writeln!(io::stderr().lock(), "{error}");
ExitCode::FAILURE
}
}
}
בפתרון השתמשנו ב־Write כדי להחזיר גם שגיאת פלט רגילה במקום להניח שההדפסה תמיד מצליחה. כשל בכתיבת הודעת השגיאה עצמה אינו משנה את החלטת היציאה, ולכן הוא נזרק במפורש. בפרקי התהליכים נבחן גם את השפעת SIGPIPE והגדרת סביבת התהליך.
הדוגמה השלילית היא פונקציה שמנסה להחזיר את תוצאת parse עבור String מקומית שנוצרה בתוכה. המהדר ידחה החזרת הפניות לאחסון שנעלם. כדי לקבל תוצאה עצמאית אפשר להחזיר BTreeMap<String, String> ולהעתיק את השדות בזמן ההכנסה, או לתכנן בעלות על המסמך כולו וייצוג של טווחים. שתי הדרכים משנות את חוזה האחסון, ולכן צריך לבחור לפי משך החיים הנדרש וגודל הנתונים.
הבדיקות בודקות חוזי פענוח ולא פלט מעוצב. בנוסף יש להריץ את הבינארי עם קובץ אמיתי, נתיב לא קיים ושני ארגומנטים כדי לבדוק את שכבת הפקודה. אלה גבולות שונים של אותה מערכת, וכל אחד מהם ראוי לבדיקה משלו.