Chapter 1. Truth Or Consequences

And the truth is we don’t know anything

They Might Be Giants

In this chapter, I’ll show you how to organize, run, and test a Rust program. I’ll be using a Unix platform (macOS) to explain some basic ideas about systems programs. Only some of these ideas apply to the Windows operating system, but the Rust programs themselves will work the same no matter which platform you use.

You will learn:

  • How to compile Rust code into an executable

  • How to use Cargo to start a new project

  • About the $PATH environment variable

  • How to use an external Rust crate from crates.io

  • About the exit status of a program

  • How to use common systems commands and options

  • How write Rust versions of the true and false programs

  • How to organize, write, and run tests

Getting Started with “Hello, world!”

The place to start is at the beginning, and it seems universally agreed that this means printing “Hello, world!” to the screen. You might change to a temporary directory with cd /tmp to write this first program, then fire up a text editor and type this Rust program into a file called hello.rs:

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

Functions are defined using fn. The name of this function is main.

2

println! (print line) is used to print to STDOUT (pronounced standard out). The semicolon indicates the end of the statement.

3

The body of the function is enclosed in curly braces.

Rust will automatically start in the main function. Functions arguments appear inside the parentheses that follow. Because there are no arguments listed in main(), the function takes no arguments. The last thing I’ll point out here is println! looks like a function but is actually a macro, which is essentially code that writes code. You will see other macros in this book that also end with an exclamation point such as assert!.

To run this program, you must first use the Rust compiler rustc (or rustc.exe on Windows) to compile the code into a form that your computer can execute:

$ rustc hello.rs

If all goes well, there will be no output from the preceding command but you should now have a new file called hello.exe on Windows or hello on macOS and Linux. On macOS, I can use the file command to see what kind of file this is:

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

You should be able to execute this directly to see a charming and heartfelt message:

$ ./hello 1
Hello, world!
1

The dot (.) indicates the current directory.

Tip

Due to security issues, it’s necessary to indicate an executable in the current directory using an explicit path.

On Windows, you can execute it like so:

> .hello.exe
Hello, world!

That was cool.

Organizing a Rust Project Directory

In your Rust projects, you will likely write many files of source code and will also use code from sources like crates.io. It would be better to create a directory to contain all this. I’ll create a directory called hello, and inside that I’ll create a src directory for the Rust source code files:

$ rm hello 1
$ mkdir -p hello/src 2
1

Remove the hello binary so I can make a hello directory.

2

The mkdir command will make a directory. The -p option says to create parent directories before creating child directories.

Now I’ll move the hello.rs source file into hello/src:

$ mv hello.rs hello/src

Go into that directory and compile your program again:

$ cd hello
$ rustc src/hello.rs

You should again have a hello executable in the directory. I will use the tree command (which you might need to install) to show you the contents of my directory:

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

This is the basic structure for a simple Rust project.

Creating and Running a Project with Cargo

It’s actually easier to start a new Rust project if you use the Cargo tool that I introduced in the Preface. Move out of your hello directory and remove it:

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

The cd command will change directories. Two dots (..) indicate the parent directory.

2

The rm command will remove a file or empty directory. The -r recursive option will remove the contents of a directory, and the -f force option will skip any errors.

Start your project anew using Cargo like so:

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

This should create a new hello directory that you can change into. I’ll use tree again to show you the contents:

$ cd hello
$ tree
.
├── Cargo.toml
└── src
    └── main.rs
Note

It’s not a requirement to use Cargo to start a project, but it is highly encouraged and nearly everyone in the Rust community uses it. You can create the directory structure and files yourself if you prefer.

You can see that Cargo creates a src directory with the file main.rs. I’ll use the command cat for concatenate (which you will write in Chapter 3) to show you the contents of this file, which should look familiar:

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

Rather than using rustc to compile the program, I’d like you to use cargo run to compile the source code and run it in one command:

$ 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

The first three lines are information about what Cargo is doing.

2

This is the output from the program.

If you would like for Cargo to be quieter, you can use the -q or --quiet option:

$ cargo run --quiet
Hello, world!

After running the program using Cargo, I can use the ls list command (which you will write in Chapter 15) to see that there is now a new directory called target:

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

You can use the tree command from earlier or the find command (which you will write in Chapter 7) to look at all the files that Cargo and Rust created. The executable file that ran should exist as target/debug/hello. You can run this directly just like the earlier program:

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

So, without any guidance from us, Cargo managed to find the source code in src/main.rs and used the main function there to build a program called hello and then run it. Why was the binary file still called hello, though, and not main? To answer that, look at Cargo.toml 1, which is a configuration file for the project:

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

[dependencies]
1

This was the name of the project I created with Cargo, so it will also be the name of the executable.

2

This is the version of the program.

3

This is the edition of Rust.

There are a couple of things to discuss here. First, Rust crates are expected to use semantic version numbers like major.minor.patch so that 1.2.4 is major version 1, minor version 2, patch version 4. Second, Rust editions are a way the community introduces changes that are not backwards compatible. I will use the 2018 edition for all the programs in this book.

Writing and Running Integration Tests

More than the act of testing, the act of designing tests is one of the best bug preventers known. The thinking that must be done to create a useful test can discover and eliminate bugs before they are coded—indeed, test-design thinking can discover and eliminate bugs at every stage in the creation of software, from conception to specification, to design, coding, and the rest.

Boris Beizer, Software Testing Techniques

A big part of what I hope to show you is the value of testing your code. As simple as this program is, there are still some things to verify. There are a couple of broad categories of tests I use which I might describe as inside-out, where I write tests for functions inside my program, and outside-in, where I write tests that run my programs as the user might. The first kind of tests are often called unit tests because functions are a basic unit of programming. I’ll show you how to write those in Chapter 2. For this program, I want to start with the latter kind of testing which is often called integration testing because it checks that the program works as a whole.

It’s common to create a tests directory for the integration test code. This keeps your source code nicely organized and also makes it easy for the compiler to ignore this code when you are not testing:

$ mkdir tests

I want to test the hello program by running it on the command line as the user will do, so I will create the file tests/cli.rs for command line interface (CLI). Your project should now look like this:

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

Start off by adding this function that shows the basic structure of a test in Rust:

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

The #[test] attribute tell Rust to run this function when testing.

2

The assert! macro asserts that a Boolean expression is true.

All the tests in this book will use assert! to verify that some expectation is true or assert_eq! to verify that something is an expected value. Since this test is evaluating the literal value true, it will always succeed. To see this test in action, execute cargo test. You should see these lines among the output:

     Running tests/cli.rs (target/debug/deps/cli-27c6c9a94ed7c7df)

running 1 test
test works ... ok

To observe a failing test, change true to false:

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

Among the output, you should see the following failed test:

running 1 test
test works ... FAILED
Tip

You can have as many assert! and assert_eq! calls in a test function as you like. If any of them fail, then the whole test fails.

Just asserting true and false is not useful, so remove that function. Instead I will see if the hello program can be executed. I will use std::process::Command to execute a command and check the result. To start, I’ll demonstrate using the command ls, which I know works on both Unix and Windows:

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

Import the std::process::Command struct for creating a new command.

2

Create a new command to run ls. Use mut to make this variable mutable as it will change.

3

Run the command and capture the output which will be a Result.

4

Verify that the result is an Ok value.

Note

By default, Rust variables are immutable, meaning their values cannot be changed.

Run cargo test and verify that you see a passing test among all the output:

running 1 test
test runs ... ok

Try changing the function to execute hello instead of ls:

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

Run the tests again and note that it will fail because the hello program can’t be found:

running 1 test
test runs ... FAILED

Recall that the binary exists in target/debug/hello. If you try to execute hello on the command line, you will see that the program can’t be found:

$ hello
-bash: hello: command not found

When you execute any command, your operating system will look in a predefined set of directories for something by that name2. On Unix-type systems, you can inspect the PATH environment variable of your shell to see this list of directories that are delimited by colons. (On Windows, this is $env:Path.) I can use tr (translate characters) to replace the colons (:) with newlines ( ) to show you my PATH:

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

$PATH tells bash to interpolate the variable. Use a pipe | to feed this to tr.

Even if I change into the target/debug directory, hello can’t be found due to the aforementioned security restrictions that exclude the current working directory from your PATH:

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

I still have to explicitly reference the path:

$ ./hello
Hello, world!

Adding a Project Dependency

Currently, the hello program only exists in the target/debug directory. If I copy it to any of the directories in my PATH (note that I include $HOME/.local/bin directory for private programs), I can execute it and run the test successfully. I don’t want to copy my program to test it; rather, I want to test the program that lives in the current crate. I can use the crate assert_cmd to find the program in my crate directory. I first need to add this as a development dependency to Cargo.toml. This tells Cargo that I only need this crate for testing and benchmarking:

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

[dependencies]

[dev-dependencies]
assert_cmd = "1"

I can then use this crate to create a Command that looks in the Cargo binary directories. The following test does not verify that the program produces the correct output, only that it appears to succeed. Update your runs function with this definition:

use assert_cmd::Command; 1

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

Import assert_cmd::Command.

2

Create a Command to run hello in the current crate. This returns a Result which I assume is safe to Result::unwrap because the binary is found.

3

Execute the program and use Assert::success to “ensure the command succeeded.”

Note

I’ll have more to say about the Result type in following chapters. For now, just know that this is a way to model something that could succeed or fail for which there are two possible variants, Ok and Err, respectively.

Understanding Program Exit Values

What does it mean for a program to exit successfully? Systems programs should report a final exit status to the operating system. The Portable Operating System Interface (POSIX) standards dictate that the standard exit code is 0 to indicate success (think zero errors) and any number from 1 to 255 otherwise. I can show you this using the bash shell and the true command. Here is the manual page from man true for the version that exists on macOS:

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

As the documentation shows, this program does nothing successfully. If I run true, I see nothing, but I can inspect the bash variable $? to see the exit status of the most recent program:

$ true
$ echo $?
0

The false command is a corollary in that it “always exits with a nonzero exit code”:

$ false
$ echo $?
1

All the programs you will write in this book will be expected to return the value 0 when they terminate normally and a nonzero value when there is an error. I can write my own versions of true and false to show you how to do this. Start by creating a src/bin directory, then create src/bin/true.rs with the following contents:

$ cat src/bin/true.rs
fn main() {
    std::process::exit(0); 1
}
1

Use the std::process::exit function to exit the program with the value 0.

You’re src directory should now have the following structure:

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

You can run the program and manually check the exit value:

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

The --bin option is the “Name of the bin target to run.”

Add the following test to tests/cli.rs to ensure it works correctly:

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

If you run cargo test, you should see the following output:

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

The tests are not necessarily run in the same order they are declared in the code. This is because Rust is a safe language for writing concurrent code, which means code can be run across multiple threads. The testing takes advantage of this concurrency to run many tests in parallel, so the test results may appear in a different order each time you run them. This is a feature, not a bug. If you would like to run the tests in order, you can run them on a single thread via cargo test -- --test-threads=1.

Rust programs will exit with the code 0 by default. Recall that src/main.rs doesn’t explicitly call std::process::exit. This means that the true program can do nothing at all. Want to be sure? Change src/bin/true.rs to the following:

$ cat src/bin/true.rs
fn main() {}

Run the test suite and verify it still passes. Next, let’s write a version of the false program with the following source code:

$ cat src/bin/false.rs
fn main() {
    std::process::exit(1); 1
}
1

Exit with any value from 1-255 to indicate an error.

Verify this looks OK to you:

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

Add this test to tests/cli.rs to verify that the program fails:

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

Use the Assert::failure function to “ensure the command failed.”

Run cargo test to verify that the programs all work as expected:

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

Another way to write this program uses std::process::abort:

$ cat src/bin/false.rs
fn main() {
    std::process::abort();
}

Again, run the test suite to ensure that the program still works as expected.

Testing the Program Output

While it’s nice to know that my hello program exits correctly, I’d like to ensure it actually prints the correct output to STDOUT (pronounced standard out), which is the standard place for output to appear and is usually the console. Update your runs function in tests/cli.rs to the following:

#[test]
fn runs() {
    let mut cmd = Command::cargo_bin("hello").unwrap();
    cmd.assert().success().stdout("Hello, world!
"); 1
}
1

Verify that the command exits successfully and prints the given text to STDOUT.

Run the tests and verify that hello does, indeed, work correctly. Next, change src/main.rs to add some more exclamation points:

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

Run the tests again to observe a failing test:

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

failures:

---- runs stdout ----
thread runs panicked at 'Unexpected stdout, failed diff var original
├── original: Hello, world!

├── diff:
--- value	expected
+ value	actual
@@ -1 +1 @@
-Hello, world!
+Hello, world!!!

└── var as str: Hello, world!!!

Learning to read test output is a skill in itself, but the output is trying very hard to show you what was expected output and what was the actual output. While this is a trivial program, I hope you can see the value in automatically checking all aspects of the program we write.

Exit Values Make Programs Composable

Correctly reporting the exit status is a characteristic of well-behaved systems programs. The exit value is important because a failed process used in conjunction with another process should cause the combination to fail. For instance, I can use the logical and operator && in bash to chain the two commands true and ls. Only if the first process reports success will the second process run:

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

If instead I execute false && ls, the result is that the first process fails and ls is never executed. Additionally, the exit status of the whole command is nonzero.

$ false && ls
$ echo $?
1

Systems programs that correctly report errors make them composable with other programs. This is important because it’s extremely common in Unix environments to combine many small commands to make ad hoc programs on the command line. If a program encounters an error but fails to report it to the operating system, then the results could be incorrect. It’s far better for a program to abort so that the underlying problems can be fixed.

Summary

This chapter was meant to introduce you to some key ideas about organizing a Rust project and some basic ideas about systems programs. Here are some of the things you should understand:

  • The Rust compiler rustc can compile Rust source code into a machine-executable file on Windows, macOS, and Linux.

  • The Cargo tool can help create a Rust project. You can also use it to compile, run, and test the code.

  • You saw several examples of systems tools like ls, cd, mkdir, and rm that accept arguments like file or directory name as well as options like -f or -p.

  • POSIX-compatible programs should exit with a value of 0 to indicate success and any value 1-255 to indicate an error.

  • You wrote the hello program to say “Hello, world!” and saw how to test it for the exit status and the text it printed to STDOUT.

  • You learned to add crate dependencies to Cargo.toml and use the crates in your code.

  • You created a tests directory to organize testing code, and you used #[test] to make functions that should be as tests.

  • You learned how to write, run, and test alternate binaries in a Cargo project by creating souce code files in the src/bin directory.

  • You wrote your own implementations of the true and false programs along with tests to verify that they succeed and fail as expected. You saw that by default a Rust program will exit with the value 0 and that the std::process::exit function can be used to explicitly exit with a given code. Additionally, the std::process::abort function can be used to exit with an error code.

1 TOML stands for Tom’s Obvious, Minimal Language.

2 Shell aliases and functions can also be executed like commands, but I’m only talking about finding programs to run at this point.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset