Kapitel 1. Wahrheit oder Konsequenz

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

Und die Wahrheit ist, dass wir nichts wissen

They Might Be Giants, "Ana Ng" (1988)

In diesem Kapitel zeige ich dir, wie du ein Rust-Programm organisierst, ausführst und testest. Ich verwende eine Unix-Plattform (macOS), um einige grundlegende Ideen über Kommandozeilenprogramme zu erklären.Nur einige dieser Ideen gelten auch für das Windows-Betriebssystem, aber die Rust-Programme selbst funktionieren unabhängig von der Plattform, die du verwendest, gleich.

Du lernst, wie du die folgenden Dinge tun kannst:

  • Rust-Code in eine ausführbare Datei kompilieren

  • Verwende Cargo, um ein neues Projekt zu starten

  • Verwende die Umgebungsvariable $PATH

  • Externe Rust Crates von crates.io einbinden

  • Den Exit-Status eines Programms interpretieren

  • Allgemeine Systembefehle und Optionen verwenden

  • Schreibe Rust-Versionen der Programme true und false

  • Organisiere, schreibe und führe Tests durch

Erste Schritte mit "Hallo, Welt!"

Es scheint der allgemein anerkannte Weg zu sein, eine Programmiersprache zu lernen, indem man "Hallo, Welt!" auf den Bildschirm druckt. Wechsle in ein temporäres Verzeichnis mit cd /tmp um dieses erste Programm zu schreiben. Da wir nur herumspielen, brauchen wirnochkein richtigesVerzeichnis.Starte dann einen Texteditor und gib den folgenden Code in eine Datei namens hello.rs ein:

fn main() { 1
    println!("Hello, world!"); 2
} 3
1

Funktionen werden mit fn definiert. Der Name für diese Funktion lautet main.

2

println! (Zeile drucken) ist ein Makro und druckt den Text auf STDOUT (sprich: standard out). Das Semikolon zeigt das Ende der Anweisung an.

3

Der Körper der Funktion ist in geschweifte Klammern eingeschlossen.

Rust startet automatisch in der Funktion main. Funktionsargumente erscheinen innerhalb der Klammern, die dem Namen der Funktion folgen. Da in main() keine Argumente aufgelistet sind, benötigt die Funktion keine Argumente. Als Letztes möchte ich noch darauf hinweisen, dass println! wie eine Funktion aussieht, aber eigentlich ein Makro ist, also ein Code, der Code schreibt. Alle anderen Makros, die ich in diesem Buch verwende - wie z. B. assert! und - enden ebenfalls mit einem Ausrufezeichen. vec!- enden ebenfalls mit einem Ausrufezeichen.

Um dieses Programm auszuführen, musst du zuerst den Rust-Compiler rustc verwenden, um den Code in eine Form zu kompilieren, die dein Computer ausführen kann:

$ rustc hello.rs

Unter Windows wirst du diesen Befehl verwenden:

> rustc.exe .\hello.rs

Wenn alles gut geht, gibt es keine Ausgabe des vorangegangenen Befehls, aber du solltest jetzt eine neue Datei namens hello unter macOS und Linux oder hello.exe unter Windows haben. Dies ist eine binär kodierte Datei, die direkt von deinem Betriebssystem ausgeführt werden kann. Unter macOS kannst du den Befehl file verwenden, um zu sehen, um welche Art von Datei es sich handelt:

$ file hello
hello: Mach-O 64-bit executable x86_64

Du solltest in der Lage sein, das Programm auszuführen, um eine charmante und zu Herzen gehende Nachricht zu sehen:

$ ./hello 1
Hello, world!
1

Der Punkt (.) zeigt das aktuelle Verzeichnis an.

Tipp

Ich werde kurz auf die Umgebungsvariable $PATH eingehen, die die Verzeichnisse auflistet, in denen nach auszuführenden Programmen gesucht wird. Das aktuelle Arbeitsverzeichnis wird nie in diese Variable aufgenommen, um zu verhindern, dass bösartiger Code heimlich ausgeführt wird. Ein Bösewicht könnte zum Beispiel ein Programm namens ls erstellen, das rm -rf / ausführt und versucht, dein gesamtes Dateisystem zu löschen. Wenn du dieses Programm zufällig als Root-Benutzer ausführst, wäre dein ganzer Tag ruiniert.

Unter Windows kannst du sie wie folgt ausführen:

> .\hello.exe
Hello, world!

Glückwunsch, wenn das dein erstes Rust-Programm war. Als nächstes zeige ich dir, wie du deinen Code besser organisieren kannst.

Organisieren eines Rust-Projektverzeichnisses

In deinen Rust-Projekten wirst du wahrscheinlich viele Quellcodedateien schreiben und auch den Code anderer Leute von Orten wie crates.io verwenden. Am besten legst du für jedes Projekt ein Verzeichnis mit einem src-Unterverzeichnis für die Rust-Quellcodedateien an.Auf einem Unix-System musst du zuerst das hello-Binary mit dem Befehl rm hello entfernen, da dies der Name des Verzeichnisses ist, das du erstellen wirst.Dann kannst du mit dem folgenden Befehl die Verzeichnisstruktur erstellen:

$ mkdir -p hello/src 1
1

Mit dem Befehl mkdir wird ein Verzeichnis erstellt. Die Option -p sagt, dass übergeordnete Verzeichnisse erstellt werden sollen, bevor untergeordnete Verzeichnisse erstellt werden. Die PowerShell benötigt diese Option nicht.

Verschiebe die Quelldatei hello.rs mit dem Befehl mv nach hello/src:

$ mv hello.rs hello/src

Verwende den Befehl cd, um in das Verzeichnis zu wechseln und dein Programm neu zu kompilieren:

$ cd hello
$ rustc src/hello.rs

Du solltest jetzt eine ausführbare Datei hello in dem Verzeichnis haben. Ich werde den Befehl tree (den du eventuell installieren musst) verwenden, um dir den Inhalt meines Verzeichnisses zu zeigen:

$ tree
.
├── hello
└── src
    └── hello.rs

Dies ist die Grundstruktur für ein einfaches Rust-Projekt.

Erstellen und Ausführen eines Projekts mit Cargo

Eine einfachere Möglichkeit, ein neues Rust-Projekt zu starten, ist die Verwendung des Cargo-Tools. Du kannst dein temporäres hello-Verzeichnis löschen:

$ cd .. 1
$ rm -rf hello 2
1

Wechsle in das übergeordnete Verzeichnis, das mit zwei Punkten gekennzeichnet ist (..).

2

Die Option -r recursive entfernt den Inhalt eines Verzeichnisses und die Option -f force überspringt alle Fehler.

Wenn du das folgende Programm speichern möchtest, wechsle in das Lösungsverzeichnis für deine Projekte. Dann starte dein Projekt neu mit Cargo wie folgt:

$ cargo new hello
     Created binary (application) `hello` package

Dadurch sollte ein neues Hallo-Verzeichnis erstellt werden, in das du wechseln kannst. Ich verwende wieder tree, um dir den Inhalt zu zeigen:

$ cd hello
$ tree
.
├── Cargo.toml 1
└── src 2
    └── main.rs 3
1

Cargo.toml ist eine Konfigurationsdatei für das Projekt. Die Erweiterung .toml steht für Tom's Obvious, Minimal Language.

2

Das Verzeichnis src ist für die Rust-Quellcode-Dateien.

3

main.rs ist der Standardstartpunkt für Rust-Programme.

Du kannst den folgenden Befehl cat (für concatenate) verwenden, um den Inhalt der einen Quelldatei zu sehen, die Cargo erstellt hat (in Kapitel 3 wirst du eine Rust-Version von cat schreiben):

$ cat src/main.rs
fn main() {
    println!("Hello, world!");
}

Statt rustc zum Kompilieren des Programms zu verwenden, benutze diesmal cargo run um den Quellcode zu kompilieren und ihn in einem Befehl auszuführen:

$ cargo run
   Compiling hello v0.1.0 (/private/tmp/hello) 1
    Finished dev [unoptimized + debuginfo] target(s) in 1.26s
     Running `target/debug/hello`
Hello, world! 2
1

Die ersten drei Zeilen enthalten Informationen darüber, was die Ladung macht.

2

Dies ist die Ausgabe des Programms.

Wenn du möchtest, dass Cargo keine Statusmeldungen über die Kompilierung und Ausführung des Codes ausgibt, kannst du die Option -q oder --quiet verwenden:

$ cargo run --quiet
Hello, world!

Nachdem du das Programm mit Cargo ausgeführt hast, verwende den Befehl ls, um den Inhalt des aktuellen Arbeitsverzeichnisses aufzulisten.(Du wirst in Kapitel 14 eine Rust-Version von ls schreiben.) Es sollte ein neues Verzeichnis namens target geben. Standardmäßig erstellt Cargo ein Debug-Target, daher siehst du das Verzeichnis target/debug, das die Build-Artefakte enthält:

$ ls
Cargo.lock  Cargo.toml  src/        target/

Du kannst den tree -Befehl von vorhin oder den find -Befehl verwenden (du wirst in Kapitel 7 eine Rust-Version von find schreiben), um dir alle Dateien anzusehen, die Cargo und Rust erstellt haben.Die ausführbare Datei, die ausgeführt wurde, sollte als target/debug/hello existieren. Du kannst sie direkt ausführen:

$ ./target/debug/hello
Hello, world!

Zusammenfassend lässt sich sagen, dass Cargo den Quellcode in src/main.rs gefunden hat, die Funktion main verwendet hat, um die Binärdatei target/debug/hello zu erstellen, und sie dann ausgeführt hat. Warum heißt die Binärdatei aber hello und nicht main? Um das zu beantworten, schau dir Cargo.toml an:

$ cat Cargo.toml
[package]
name = "hello" 1
version = "0.1.0" 2
edition = "2021" 3

# See more keys and their definitions at 4
# https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies] 5
1

Dies war der Name des Projekts, das ich mit Cargo erstellt habe, also wird dies auch der Name der ausführbaren Datei sein.

2

Dies ist die Version des Programms.

3

Dies ist die Edition von Rust, die zum Kompilieren des Programms verwendet werden sollte. Mit den Editionen führt die Rust-Gemeinschaft Änderungen ein, die nicht abwärtskompatibel sind. Ich werde für alle Programme in diesem Buch die Edition 2021 verwenden.

4

Dies ist eine Kommentarzeile, die ich nur dieses eine Mal einfügen werde. Du kannst diese Zeile aus deiner Datei entfernen, wenn du möchtest.

5

Hier kannst du alle externen Kisten auflisten, die dein Projekt verwendet. In diesem Projekt gibt es noch keine, daher ist dieser Abschnitt leer.

Hinweis

Rust-Bibliotheken werden Crates genannt, und es wird erwartet, dass sie semantische Versionsnummern in der Form major.minor.patchverwenden, so dass 1.2.4 die Hauptversion 1, die Nebenversion 2 und die Patch-Version 4 ist. Eine Änderung der Hauptversion bedeutet, dass die öffentliche Programmierschnittstelle (API) der Crate geändert wurde.

Schreiben und Ausführen von Integrationstests

"Mehr noch als das Testen ist das Entwerfen von Tests eine der besten bekannten Fehlervermeidungsmethoden. Die Überlegungen, die angestellt werden müssen, um einen nützlichen Test zu erstellen, könnenFehler aufdecken undbeseitigen, bevor sie programmiert werden. Tatsächlich kann das Denken beim Testentwurf Fehler in jeder Phase der Softwareentwicklung aufdeckenund beseitigen, von der Konzeption über die Spezifikation, das Design und die Programmierung bis hin zum Rest.

Boris Beizer, Software Testing Techniques (Van Nostrand Reinhold)

Auch wenn "Hallo, Welt!" recht einfach ist, gibt es dennoch Dinge, die getestet werden können. Es gibt zwei große Kategorien von Tests, die ich in diesem Buch vorstellen werde.Inside-out oder Unit-Tests sind Tests, die du für die Funktionen innerhalb deines Programms schreibst. Ich werde Unit-Tests in Kapitel 5 vorstellen.Outside-in oder Integrationstests sind Tests, die deine Programme so ausführen, wie es der Benutzer tun könnte, und das werden wir für dieses Programm tun. Die Konvention in Rust-Projekten ist es, ein Tests-Verzeichnis parallel zum src-Verzeichnis für den Testcode zu erstellen, und du kannst den Befehl mkdir tests dafür verwenden.

Das Ziel ist es, das Programm hello zu testen, indem du es auf der Kommandozeile so ausführst, wie es der Benutzer tun wird. Erstelle die Datei tests/cli.rs für die Kommandozeilenschnittstelle (CLI) mit dem folgenden Code.Beachte, dass diese Funktion den einfachsten möglichen Test in Rust darstellen soll, aber noch nichts Nützliches tut:

#[test] 1
fn works() {
    assert!(true); 2
}
1

Das #[test] Attribut sagt Rust, dass diese Funktion beim Testen ausgeführt werden soll.

2

Das Makroassert! behauptet, dass ein boolescher Ausdruck true ist.

Dein Projekt sollte jetzt wie folgt aussehen:

$ tree -L 2
.
├── Cargo.lock 1
├── Cargo.toml
├── src 2
│   └── main.rs
├── target 3
│   ├── CACHEDIR.TAG
│   ├── debug
│   └── tmp
└── tests 4
    └── cli.rs
1

In der DateiCargo.lock werden die genauen Versionen der Abhängigkeiten festgehalten, die für die Erstellung deines Programms verwendet werden. Du solltest diese Datei nicht bearbeiten.

2

Das src-Verzeichnis ist für die Rust-Quellcodedateien, mit denen das Programm erstellt wird.

3

Das Zielverzeichnis enthält die Build-Artefakte.

4

Das Verzeichnis tests enthält den Rust-Quellcode zum Testen des Programms.

Alle Tests in diesem Buch verwenden assert!, um zu überprüfen, ob eine Erwartung true ist, oder assert_eq! um zu überprüfen, ob etwas ein erwarteter Wert ist. Da dieser Test den literalen Wert true auswertet, wird er immer erfolgreich sein. Um diesen Test in Aktion zu sehen, führe cargo testIn der Ausgabe solltest du diese Zeilen sehen:

running 1 test
test works ... ok

Um zu sehen, ob der Test fehlschlägt, ändere true in false in der Datei tests/cli.rs:

#[test]
fn works() {
    assert!(false);
}

In der Ausgabe solltest du den folgenden fehlgeschlagenen Test sehen:

running 1 test
test works ... FAILED
Tipp

Du kannst so viele assert! und assert_eq! Aufrufe in einer Testfunktion haben, wie du willst. Wenn einer von ihnen fehlschlägt, schlägt der gesamte Test fehl.

Jetzt wollen wir einen nützlicheren Test erstellen, der einen Befehl ausführt und das Ergebnis überprüft.Der Befehl ls funktioniert sowohl unter Unix als auch unter Windows PowerShell, also fangen wir damit an. Ersetze den Inhalt von tests/cli.rs durch den folgenden Code:

use std::process::Command; 1

#[test]
fn runs() {
    let mut cmd = Command::new("ls"); 2
    let res = cmd.output(); 3
    assert!(res.is_ok()); 4
}
1

Importieren std::process::Command. Die std sagt uns, dass dies in der Standardbibliothek ist und dass es sich um Rust-Code handelt, der so universell nützlich ist, dass er in der Sprache enthalten ist.

2

Erstelle eine neue Command, um ls auszuführen. Mit dem Schlüsselwortlet wird ein Wert an eine Variable gebunden. Das Schlüsselwortmut macht diese Variable veränderbar, so dass sie sich ändern kann.

3

Führen Sie den Befehl aus und erfassen Sie die Ausgabe, die ein Result.

4

Überprüfe, ob das Ergebnis eine Ok Variante ist, die anzeigt, dass die Aktion erfolgreich war.

Tipp

Standardmäßig sind Rust-Variablen unveränderlich, das heißt, ihre Werte können nicht geändert werden.

Führen Sie aus cargo test und überprüfe, ob du in der Ausgabe einen bestandenen Test siehst:

running 1 test
test runs ... ok

Aktualisiere tests/cli.rs mit dem folgenden Code, damit die Funktion runs hello statt ls ausführt:

use std::process::Command;

#[test]
fn runs() {
    let mut cmd = Command::new("hello");
    let res = cmd.output();
    assert!(res.is_ok());
}

Führe den Test erneut durch und stelle fest, dass er fehlschlägt, weil das Programm hello nicht gefunden werden kann:

running 1 test
test runs ... FAILED

Wenn du versuchst, hello auf der Kommandozeile auszuführen, wirst du feststellen, dass das Programm nicht gefunden werden kann:

$ hello
-bash: hello: command not found

Wenn du einen Befehl ausführst, sucht dein Betriebssystem in einer vordefinierten Reihe von Verzeichnissen nach etwas, das diesen Namen trägt.1 Auf Unix-Systemen kannst du die Umgebungsvariable PATH deiner Shell einsehen, um diese Liste von Verzeichnissen zu sehen, die durch Doppelpunkte getrennt sind. (Unter Windows ist dies $env:Path.) Ich kann tr (Zeichen übersetzen) verwenden, um die Doppelpunkte (:) durch Zeilenumbrüche (\n) zu ersetzen, um dir mein PATH zu zeigen:

$ echo $PATH | tr : '\n' 1
/opt/homebrew/bin
/Users/kyclark/.cargo/bin
/Users/kyclark/.local/bin
/usr/local/bin
/usr/bin
/bin
/usr/sbin
/sbin
1

$PATH weist bash an, die Variable zu interpolieren. Verwende eine Pipe (|), um dies an tr weiterzuleiten.

Selbst wenn ich in das Ziel-/Debug-Verzeichnis wechsle, kann hello aufgrund der bereits erwähnten Sicherheitseinschränkungen, die das aktuelle Arbeitsverzeichnis von meinem PATH ausschließen, nicht gefunden werden:

$ cd target/debug/
$ hello
-bash: hello: command not found

Ich muss explizit auf das aktuelle Arbeitsverzeichnis verweisen, damit das Programm läuft:

$ ./hello
Hello, world!

Als Nächstes muss ich einen Weg finden, um Binärdateien auszuführen, die nur in der aktuellen Kiste existieren.

Hinzufügen von Projektabhängigkeiten

Derzeit existiert das Programm hello nur im Verzeichnis target/debug.Wenn ich es in eines der Verzeichnisse in meinem PATH kopiere (beachte, dass ich das Verzeichnis $HOME/.local/bin für private Programme mit einbeziehe), kann ich es ausführen und den Test erfolgreich durchführen. Ich möchte aber nicht mein Programm kopieren, um es zu testen, sondern das Programm testen, das sich in der aktuellen crate befindet.Ich kann die crate assert_cmd verwenden, um das Programm in meinem crate-Verzeichnis zu finden.Ich werde auch die crate pretty_assertions verwenden, um eine Version des assert_eq! Makros zu benutzen, die Unterschiede zwischen zwei Strings besser anzeigt als die Standardversion.

Zuerst muss ich diese als Entwicklungsabhängigkeiten zur Cargo.toml hinzufügen.Das sagt Cargo, dass ich diese Kisten nur zum Testen und Benchmarking brauche:

[package]
name = "hello"
version = "0.1.0"
edition = "2021"

[dependencies]

[dev-dependencies]
assert_cmd = "2.0.13"
pretty_assertions = "1.4.0"

Ich kann dann assert_cmd verwenden, um ein Command zu erstellen, das in den Cargo-Binärverzeichnissen nachschaut.Der folgende Test überprüft nicht, ob das Programm die richtige Ausgabe produziert, sondern nur, ob es erfolgreich zu sein scheint.Aktualisiere deine tests/cli.rs mit dem folgenden Code, damit die Funktion runs assert_cmd::Command statt std::process::Command verwendet:

use assert_cmd::Command; 1

#[test]
fn runs() {
    let mut cmd = Command::cargo_bin("hello").unwrap(); 2
    cmd.assert().success(); 3
}
1

Importiere assert_cmd::Command.

2

Erstelle ein Command, um hello in der aktuellen Kiste auszuführen. Dies gibt ein Result zurück, und der Code ruft Result::unwrap auf, weil die Binärdatei gefunden werden sollte. Wenn das nicht der Fall ist, löst unwrap eine Panik aus und der Test schlägt fehl, was eine gute Sache ist.

3

Verwende Assert::success um sicherzustellen, dass der Befehl erfolgreich war.

Hinweis

Ich werde in späteren Kapiteln mehr über den Typ Result sagen. Für den Moment solltet ihr wissen, dass dies eine Möglichkeit ist, etwas zu modellieren, das gelingen oder fehlschlagen kann und für das es zwei mögliche Varianten gibt: Ok bzw. Err.

Führen Sie cargo test erneut aus und überprüfe, ob du jetzt einen bestandenen Test siehst:

running 1 test
test runs ... ok

Verstehen der Programmausstiegswerte

Was bedeutet es, wenn ein Programm erfolgreich ausgeführt wurde?Kommandozeilenprogramme sollten dem Betriebssystem einen endgültigen Exit-Status melden, um Erfolg oder Misserfolg anzuzeigen. Die POSIX-Standards (Portable Operating System Interface) schreiben vor, dass der Standard-Exit-Code 0 ist, um Erfolg zu signalisieren (d.h. keine Fehler), und andernfalls eine beliebige Zahl von 1 bis 255.Ich kann dir das anhand der Shell bash und dem Befehl true zeigen. Hier ist die Handbuchseite von man true für die Version, die es unter macOS gibt:

TRUE(1)                   BSD General Commands Manual                  TRUE(1)

NAME
     true -- Return true value.

SYNOPSIS
     true

DESCRIPTION
     The true utility always returns with exit code zero.

SEE ALSO
     csh(1), sh(1), false(1)

STANDARDS
     The true utility conforms to IEEE Std 1003.2-1992 (''POSIX.2'').

BSD                              June 27, 1991                             BSD

Wie in der Dokumentation beschrieben, tut dieses Programm nichts, außer den Exit-Code Null zurückzugeben.Wenn ich das Programm trueausführe, gibt es keine Ausgabe, aber ich kann die Variable bash $? einsehen, um den Exit-Status des letzten Befehls zu sehen:

$ true
$ echo $?
0

Der Befehl false ist eine logische Konsequenz, da er immer mit einem Exit-Code ungleich Null beendet:

$ false
$ echo $?
1

Von allen Programmen, die du in diesem Buch schreiben wirst, wird erwartet, dass sie bei normaler Beendigung Null zurückgeben und bei einem Fehler einen Wert ungleich Null. Um dies zu sehen, kannst du Versionen von true und false schreiben. Beginne damit, ein src/bin-Verzeichnis mit mkdir src/binund erstelle dann src/bin/true.rs mit dem folgenden Inhalt:

fn main() {
    std::process::exit(0); 1
}
1

Verwende die Funktionstd::process::exit , um das Programm mit dem Wert Null zu beenden.

Dein src-Verzeichnis sollte jetzt die folgende Struktur haben:

$ tree src/
src/
├── bin
│   └── true.rs
└── main.rs

Führe das Programm aus und überprüfe den Exit-Wert manuell:

$ cargo run --quiet --bin true 1
$ echo $?
0
1

Die Option --bin ist der Name des Binärziels, das ausgeführt werden soll.

Füge den folgenden Test zu tests/cli.rs hinzu, um sicherzustellen, dass er richtig funktioniert. Es ist egal, ob du ihn vor oder nach der bestehenden Funktion runs hinzufügst:

#[test]
fn true_ok() {
    let mut cmd = Command::cargo_bin("true").unwrap();
    cmd.assert().success();
}

Wenn du cargo testaufrufst, solltest du die Ergebnisse der beiden Tests sehen:

running 2 tests
test true_ok ... ok
test runs ... ok
Hinweis

Die Tests werden nicht unbedingt in der gleichen Reihenfolge ausgeführt, in der sie im Code deklariert sind. Das liegt daran, dass Rust eine sichere Sprache für das Schreiben von nebenläufigem Code ist, was bedeutet, dass der Code in mehreren Threads ausgeführt werden kann. Die Tests nutzen diese Parallelität, um viele Tests parallel auszuführen, sodass die Testergebnisse bei jeder Ausführung in einer anderen Reihenfolge erscheinen können. Das ist eine Funktion und kein Fehler. Wenn du die Tests in der richtigen Reihenfolge ausführen möchtest, kannst du sie in einem einzelnen Thread über cargo test -- --test-threads=1.

Rust-Programme beenden sich standardmäßig mit dem Wert Null. Erinnere dich daran, dass src/main.rs nicht explizit std::process::exit aufruft. Das bedeutet, dass das Programm true überhaupt nichts tun kann. Wenn du sichergehen willst, ändere src/bin/true.rs wie folgt:

fn main() {}

Führe die Testsuite aus und überprüfe, ob sie immer noch besteht. Als Nächstes schreiben wir eine Version des Programms false mit dem folgenden Quellcode in src/bin/false.rs:

fn main() {
    std::process::exit(1); 1
}
1

Beende mit einem beliebigen Wert zwischen 1 und 255, um einen Fehler anzuzeigen.

Überprüfe manuell, dass der Exit-Wert des Programms nicht Null ist:

$ cargo run --quiet --bin false
$ echo $?
1

Füge dann diesen Test zu tests/cli.rs hinzu, um zu überprüfen, ob das Programm bei der Ausführung einen Fehler meldet:

#[test]
fn false_not_ok() {
    let mut cmd = Command::cargo_bin("false").unwrap();
    cmd.assert().failure(); 1
}
1

Verwende die FunktionAssert::failure , um sicherzustellen, dass der Befehl fehlgeschlagen ist.

Führe aus cargo test um zu überprüfen, ob alle Programme wie erwartet funktionieren:

running 3 tests
test runs ... ok
test true_ok ... ok
test false_not_ok ... ok

Eine andere Möglichkeit, das Programm false zu schreiben, verwendet std::process::abort. Ändere src/bin/false.rs wie folgt:

fn main() {
    std::process::abort();
}

Führe die Testsuite erneut aus, um sicherzustellen, dass das Programm immer noch wie erwartet funktioniert.

Testen der Programmausgabe

Es ist zwar schön zu wissen, dass mein Programm hello korrekt beendet wird, aber ich möchte auch sicherstellen, dass es die korrekte Ausgabe auf STDOUT ausgibt.Ändere deine Funktion runs in tests/cli.rs wiefolgt:

use assert_cmd::Command;
use pretty_assertions::assert_eq; 1

#[test]
fn runs() {
    let mut cmd = Command::cargo_bin("hello").unwrap();
    let output = cmd.output().expect("fail"); 2
    assert!(output.status.success()); 3
    let stdout = String::from_utf8(output.stdout).expect("invalid UTF-8"); 4
    assert_eq!(stdout, "Hello, world!\n"); 5
}
1

Importiere das pretty_assertions::assert_eq Makro zum Vergleichen von Werten anstelle der Standardversion von Rust.

2

Rufe auf Command::output um den Befehl hello auszuführen. Verwende Result::expect um die Ausgabe des Befehls zu erhalten oder mit der Meldung "fehlgeschlagen" zu sterben.

3

Überprüfe, ob der Befehl erfolgreich war.

4

Konvertiere die Ausgabe des Programms in UTF-8, worauf ich in Kapitel 4 näher eingehen werde.

5

Vergleiche die Ausgabe des Programms mit einem erwarteten Wert. Beachte, dass hier die pretty_assertions Version des Makros assert_eq verwendet wird.

Führe die Tests durch und überprüfe, ob hello tatsächlich richtig funktioniert. Als Nächstes änderst du src/main.rs, um weitere Ausrufezeichen hinzuzufügen:

fn main() {
    println!("Hello, world!!!");
}

Führe die Tests erneut durch, um zu sehen, ob der Test fehlschlägt:

running 3 tests
test true_ok ... ok
test false_not_ok ... ok
test runs ... FAILED

failures:

---- runs stdout ----
thread runs panicked at tests/cli.rs:10:5:
assertion failed: `(left == right)`

Diff < left / right > :
<Hello, world!!!
>Hello, world!

Das vorstehende Testergebnis gibt sich große Mühe, dir zu zeigen, wie die erwartete Ausgabe (rechts) von der tatsächlichen Ausgabe (links) abweicht.Die Terminalausgabe enthält sogar roten und grünen Text und hervorgehobenen Text, der hier nicht wiedergegeben werden kann. Auch wenn es sich hier um ein triviales Programm handelt, hoffe ich, dass du den Wert einer automatischen Überprüfung aller Aspekte der Programme, die wir schreiben, erkennen kannst.

Exit-Werte machen Programme kompatibler

Die korrekte Meldung des Exit-Status ist ein Merkmal gut funktionierender Befehlszeilenprogramme.Der Exit-Wert ist wichtig, weil ein fehlgeschlagener Prozess in Verbindung mit einem anderen Prozess dazu führen sollte, dass die Kombination fehlschlägt. Zum Beispiel kann ich den logischen und Operator && in bash verwenden, um die beiden Befehle true und ls zu verketten. Nur wenn der erste Prozess Erfolg meldet, wird der zweite Prozess ausgeführt:

$ true && ls
Cargo.lock  Cargo.toml  src/        target/     tests/

Wenn ich stattdessen den Befehl false && lsausführe, schlägt der erste Prozess fehl und ls wird nie ausgeführt.Außerdem ist der Exit-Status des gesamten Befehls ungleich Null:

$ false && ls
$ echo $?
1

Wenn Kommandozeilenprogramme Fehler korrekt melden, sind sie mit anderen Programmen kompatibel. In Unix-Umgebungen ist es sehr üblich, viele kleine Befehle zu Ad-hoc-Programmen auf der Kommandozeile zu kombinieren. Wenn ein Programm auf einen Fehler stößt, diesen aber nicht an das Betriebssystem meldet, könnten die Ergebnisse falsch sein. Es ist viel besser, wenn ein Programm abbricht, damit die zugrunde liegenden Probleme behoben werden können.

Zusammenfassung

In diesem Kapitel hast du einige wichtige Ideen zum Organisieren eines Rust-Projekts und einige grundlegende Ideen zu Kommandozeilenprogrammen kennengelernt. Zur Wiederholung:

  • Der Rust-Compiler rustc kompiliert Rust-Quellcode in eine maschinenausführbare Datei unter Windows, macOS und Linux.

  • Das Cargo-Tool erstellt ein neues Rust-Projekt und kompiliert, startet und testet den Code.

  • Standardmäßig, cargo new erstellt ein neues Rust-Programm, das "Hallo, Welt!" ausgibt.

  • Kommandozeilen-Tools wie ls, cd, mkdir und rm akzeptieren oft Kommandozeilen-Argumente wie Datei- oder Verzeichnisnamen sowie Optionen wie -f oder -p.

  • POSIX-kompatible Programme sollten mit einem Wert von 0 beendet werden, um einen Erfolg anzuzeigen, und mit einem Wert zwischen 1 und 255, um einen Fehler anzuzeigen.

  • Du hast gelernt, Crate-Abhängigkeiten zu Cargo.toml hinzuzufügen und die Crates in deinem Code zu verwenden.

  • Du hast ein Verzeichnis tests erstellt, um den Testcode zu organisieren, und du hast #[test] verwendet, um Funktionen zu markieren, die als Tests ausgeführt werden sollen.

  • Du hast gelernt, wie man den Exit-Status eines Programms prüft und wie man den auf STDOUT ausgegebenen Text überprüft.

  • Du hast gelernt, wie du alternative Binärdateien in einem Cargo-Projekt schreibst, ausführst und testest, indem du Quellcodedateien im Verzeichnis src/bin erstellst.

  • Du hast deine Implementierungen der Programme true und false zusammen mit Tests geschrieben, um zu überprüfen, ob sie wie erwartet erfolgreich sind oder fehlschlagen. Du hast gesehen, dass ein Rust-Programm standardmäßig mit dem Wert Null beendet wird und dass die Funktion std::process::exit verwendet werden kann, um es explizit mit einem bestimmten Code zu beenden. Außerdem kann die Funktion std::process::abort verwendet werden, um ein Programm mit einem Fehlercode ungleich Null zu beenden.

Im nächsten Kapitel zeige ich dir, wie du ein Programm schreibst, das Kommandozeilenargumente verwendet, um die Ausgabe zu verändern.

1 Shell-Aliase und Funktionen können auch wie Befehle ausgeführt werden, aber ich spreche an dieser Stelle nur davon, Programme zu finden, die ausgeführt werden sollen.

Get Befehlszeilen-Rost now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.