cmdtest

package module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 8, 2022 License: Apache-2.0 Imports: 16 Imported by: 1

README

Build Status godoc Go Report Card

Testing your CLI

The cmdtest package simplifies testing of command-line interfaces. It provides a simple, cross-platform, shell-like language to express command execution. It can compare actual output with the expected output, and can also update a file with new "golden" output that is deemed correct.

Test files

Start using cmdtest by writing a test file with the extension .ct. The test file will consist of commands prefixed by $ and expected output following each command. Lines starting with # are comments. Example:

# Testing for my-cli.

# The "help" command.
$ my-cli help
my-cli is a CLI, and this is its help.

# Verify that an invalid command fails and prints a useful error.
$ my-cli invalidcmd --> FAIL
Error: unknown command "invalidcmd".

You can leave the expected output out and let cmdtest fill it in for you using update mode (see below).

More details on test file format:

  • Before the first line starting with a $, empty lines and lines beginning with "#" are ignored.
  • A sequence of consecutive lines starting with $ begin a test case. These lines are commands to execute. See below for the valid commands.
  • Lines following the $ lines are command output (merged stdout and stderr). Output is always treated literally.
  • After the command output there should be a blank line. Between that blank line and the next $ line, empty lines and lines beginning with # are ignored. (Because of these rules, cmdtest cannot distinguish trailing blank lines in the output.)
  • Syntax of a line beginning with $:
    • A sequence of space-separated words (no quoting is supported). The first word is the command, the rest are its args. If the next-to-last word is <, the last word is interpreted as a file and becomes the standard input to the command. None of the built-in commands (see below) support input redirection, but commands defined with Program do.
  • By default, commands are expected to succeed, and the test will fail otherwise. However, commands that are expected to fail can be marked with a --> FAIL suffix.

All test files in the same directory make up a test suite. See the TestSuite documentation for the syntax of test files, and the testdata/ directory for examples.

Commands

cmdtest comes with the following built-in commands:

  • cd DIR
  • cat FILE
  • mkdir DIR
  • setenv VAR VALUE
  • echo ARG1 ARG2 ...
  • fecho FILE ARG1 ARG2 ...

These all have their usual Unix shell meaning, except for fecho, which writes its arguments to a file (output redirection is not supported). All file and directory arguments must refer to the current directory; that is, they cannot contain slashes.

You can add your own custom commands by adding them to the TestSuite.Commands map; keep reading for an example.

Variable substitution

cmdtest does its own environment variable substitution, using the syntax ${VAR}. Test execution inherits the full environment of the test binary caller (typically, your shell). The environment variable ROOTDIR is set to the temporary directory created to run the test file.

Running the tests

To test, first read the suite:

ts, err := cmdtest.Read("testdata")

Next, configure the resulting TestSuite by adding a Setup function and/or adding commands to the Commands map. In particular, you will want to add a command for your CLI. There are two ways to do this: you can run your CLI binary directly from from inside the test binary process, or you can build the CLI binary and have the test binary run it as a sub-process.

Invoking your CLI in-process

To run your CLI from inside the test binary, you will have to prevent it from calling os.Exit. You may be able to refactor your main function like this:

func main() {
        os.Exit(run())
}

func run() int {
    // Your previous main here, returning 0 for success.
}

Then, add the command for your CLI to the TestSuite:

ts.Commands["my-cli"] = cmdtest.InProcessProgram("my-cli", run)
Invoking your CLI out-of-process

You can also run your CLI as an ordinary program, if you build it first. You can do this outside of your test, or inside with code like

if err := exec.Command("go", "build", ".").Run(); err != nil {
        t.Fatal(err)
}
defer os.Remove("my-cli")

Then add the command for your CLI to the TestSuite:

ts.Commands["my-cli"] = cmdtest.Program("my-cli")

Running the test

Finally, call TestSuite.Run with false to compare the expected output to the actual output, or true to update the expected output. Typically, this boolean will be the value of a flag. So, your final test code will look something like:

var update = flag.Bool("update", false, "update test files with results")

func TestCLI(t *testing.T) {
    ts, err := cmdtest.Read("testdata")
    if err != nil {
        t.Fatal(err)
    }
    ts.Commands["my-cli"] = cmdtest.InProcessProgram("my-cli", run)
    ts.Run(t, *update)
}

Documentation

Overview

The cmdtest package simplifies testing of command-line interfaces. It provides a simple, cross-platform, shell-like language to express command execution. It can compare actual output with the expected output, and can also update a file with new "golden" output that is deemed correct.

Start using cmdtest by writing a test file with commands and expected output, giving it the extension ".ct". All test files in the same directory make up a test suite. See the TestSuite documentation for the syntax of test files.

To test, first read the suite:

ts, err := cmdtest.Read("testdata")

Then configure the resulting TestSuite by adding commands or enabling debugging features. Lastly, call TestSuite.Run with false to compare or true to update. Typically, this boolean will be the value of a flag:

var update = flag.Bool("update", false, "update test files with results")
...
ts.Run(t, *update)

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type CommandFunc

type CommandFunc func(args []string, inputFile string) ([]byte, error)

CommandFunc is the signature of a command function. The function takes the subsequent words on the command line (so that arg[0] is the first argument), as well as the name of a file to use for input redirection. It returns the command's output.

func InProcessProgram

func InProcessProgram(name string, f func() int) CommandFunc

InProcessProgram defines a command function that will invoke f, which must behave like an actual main function except that it returns an error code instead of calling os.Exit. Before calling f:

  • os.Args is set to the concatenation of name and args.
  • If inputFile is non-empty, it is redirected to standard input.
  • Standard output and standard error are redirected to a buffer, which is returned.

func Program

func Program(path string) CommandFunc

Program defines a command function that will run the executable at path using the exec.Command package and return its combined output. If path is relative, it is converted to an absolute path using the current directory at the time Program is called.

In the unlikely event that Program cannot obtain the current directory, it panics.

type ExitCodeErr added in v0.4.0

type ExitCodeErr struct {
	Msg  string
	Code int
}

ExitCodeErr is an error that a CommandFunc can return to provide an exit code. Tests can check the code by writing the desired value after "--> FAIL".

ExitCodeErr is only necessary when writing commands that don't return errors that come from the OS. Commands that return the error from os/exec.Cmd.Run or functions in the os package like Chdir and Mkdir don't need to use this, because those errors already contain error codes.

func (*ExitCodeErr) Error added in v0.4.0

func (e *ExitCodeErr) Error() string

type TestSuite

type TestSuite struct {
	// If non-nil, this function is called for each test. It is passed the root
	// directory after it has been made the current directory.
	Setup func(string) error

	// The commands that can be executed (that is, whose names can occur as the
	// first word of a command line).
	Commands map[string]CommandFunc

	// If true, don't delete the temporary root directories for each test file,
	// and print out their names for debugging.
	KeepRootDirs bool

	// If true, don't log while comparing.
	DisableLogging bool
	// contains filtered or unexported fields
}

A TestSuite contains a set of test files, each of which may contain multiple test cases. Use Read to build a TestSuite from all the test files in a directory. Then configure it and call Run.

Format of a test file:

Before the first line starting with a '$', empty lines and lines beginning with "#" are ignored.

A sequence of consecutive lines starting with '$' begins a test case. These lines are commands to execute. See below for the valid commands.

Lines following the '$' lines are command output (merged stdout and stderr). Output is always treated literally. After the command output there should be a blank line. Between that blank line and the next '$' line, empty lines and lines beginning with '#' are ignored. (Because of these rules, cmdtest cannot distinguish trailing blank lines in the output.)

Syntax of a line beginning with '$': A sequence of space-separated words (no quoting is supported). The first word is the command, the rest are its args. If the next-to-last word is '<', the last word is interpreted as a file and becomes the standard input to the command. None of the built-in commands (see below) support input redirection, but commands defined with Program do.

By default, commands are expected to succeed, and the test will fail otherwise. However, commands that are expected to fail can be marked with a " --> FAIL" suffix. The word FAIL may optionally be followed by a non-zero integer specifying the expected exit code.

The cases of a test file are executed in order, starting in a freshly created temporary directory. Execution of a file stops with the first case that doesn't behave as expected, but other files in the suite will still run.

The built-in commands (initial contents of the Commands map) are:

cd DIR
cat FILE
mkdir DIR
setenv VAR VALUE
echo ARG1 ARG2 ...
fecho FILE ARG1 ARG2 ...

These all have their usual Unix shell meaning, except for fecho, which writes its arguments to a file (output redirection is not supported). All file and directory arguments must refer to the current directory; that is, they cannot contain slashes.

cmdtest does its own environment variable substitution, using the syntax "${VAR}". Test execution inherits the full environment of the test binary caller (typically, your shell). The environment variable ROOTDIR is set to the temporary directory created to run the test file.

func Read

func Read(dir string) (*TestSuite, error)

Read reads all the files in dir with extension ".ct" and returns a TestSuite containing them. See the TestSuite documentation for syntax.

func (*TestSuite) Run

func (ts *TestSuite) Run(t *testing.T, update bool)

Run runs the commands in each file in the test suite. Each file runs in a separate subtest.

If update is false, it compares their output with the output in the file, line by line.

If update is true, it writes the output back to the file, overwriting the previous output.

Before comparing/updating, occurrences of the root directory in the output are replaced by ${ROOTDIR}.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL