testbasher

package module
v1.0.9 Latest Latest
Warning

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

Go to latest
Published: Jan 5, 2025 License: Apache-2.0 Imports: 15 Imported by: 1

README

Test BASHer

PkgGoDev GitHub build and test Go Report Card Coverage

"Test BASHer" is a painfully simple bash script management and execution for simple unit test script harnesses. It is intended for such cases where only one or few short and simple test scripts are required per specific test and it's best to keep them near the test case itself to keep scripts and test in sync.

A test starts a (BASHer) script and then interacts with it: the script may report information about things it set up dynamically, such as the IDs of Linux kernel namespaces, et cetera, which the test needs to read in order to test dynamic assumptions. And the script in turn may wait for the test to step it through multiple phases in order to complete a specific test. An example is lxkns where transient namespaces get created, but the processes keeping them alive must not terminate before the test has reached a certain phase in its course.

Please refer to the PkgGoDev for details.

Usage

The basic usage pattern is as follows:

  • create a b := Basher{}, and don't forget to defer b.Done().
  • if required, add common BASH code using b.Common("...script code...") to be reused in your scripts.
  • add one or more BASH scripts using b.Script("name", "...script code...").
  • start your entry point script with c := b.Start("name"), and defer c.Close().
  • read data output from your script: c.Decode(&data).
    • Golang 1.14 and later: in case the expected data cannot be decoded, c.Decode panics with details including the exact (JSON) data read from the script which could not be decoded. No more stupid JSON "syntax errors at offset 666", but instead you'll see the JSON data read up to the point where things went south.
  • in case of multiple phases, step forward by calling c.Proceed().

And now for some code to further illustrate the above usage pattern list:

func example() {
    scripts := Basher{}
    defer scripts.Done()
    // Define a first script named "newuserns", which we will later start as
    // out entry point. This script creates a new Linux kernel user namespace,
    // where the unshare(2) command then executes a second script (which we'll
    // define next).
    //
    // Since the defined script are stored in temporary files, there is no way
    // to know beforehand where these files will be stored and named, and thus
    // we don't know how to directly call them. Instead, we use the $userns
    // environment variable which will point to the correct temporary
    // filepath.
    scripts.Script("newuserns", `
unshare -Ufr $userns # call the script named "userns" via $userns.
`)
    // This second script named "userns" returns information about the newly
    // created and then waits to proceed into termination, thereby destroying
    // the user namespace.
    scripts.Script("userns", `
echo "\"$(readlink /proc/self/ns/user)\"" # ...turns it into a JSON string.
read # wait for test to proceed()
`)
    // Start the first defined script named "newuserns"...
    cmd := scripts.Start("newuserns")
    defer cmd.Close()
    // Read in the (JSON) information sent by the running script.
    var userns string
    cmd.Decode(&userns)
    fmt.Println("temporary user namespace:", userns)
    // Tell the script to finish; this can be omitted if the last step,
    // because calling Close() on a Basher will automatically issue a final
    // Proceed().
    cmd.Proceed()
}

DevContainer

[!CAUTION]

Do not use VSCode's "Dev Containers: Clone Repository in Container Volume" command, as it is utterly broken by design, ignoring .devcontainer/devcontainer.json.

  1. git clone https://github.com/thediveo/enumflag
  2. in VSCode: Ctrl+Shift+P, "Dev Containers: Open Workspace in Container..."
  3. select enumflag.code-workspace and off you go...

Supported Go Versions

native supports versions of Go that are noted by the Go release policy, that is, major versions N and N-1 (where N is the current major version).

testbasher is Copyright 2020-25 Harald Albrecht, and licensed under the Apache License, Version 2.0.

Documentation

Overview

Package testbasher provides painfully simple BASH script management and execution for small unit test script harnesses. It is intended for such cases where only one or few short and simple auxiliary scripts are required per specific test and it's best to keep them near the test case itself to keep scripts and test in sync.

This package defines only these two elements: Basher and TestCommand.

Basher

Basher handles auxiliary test harness scripts as integral parts of your unit tests. Running a Basher script (or often set of scripts) is done via TestCommand.

TestCommand

TestCommand simplifies handling and interaction with (test) commands and scripts. It has a simplified reporting and interaction interface tailored towards test harness scripts. As its name already suggests, TestCommand is good for use in some types of tests, but it is not a general purpose tool.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Basher

type Basher struct {
	// contains filtered or unexported fields
}

Basher manages (temporary) auxiliary scripts for setting up special environments for individual test cases. Make sure to always call Done at the end of a test case using Basher, preferably by immediately defer'ing the Done call.

Scripts are added to a Basher by calling the Script method, giving the script a name and specifying the script itself. The script then gets written to a temporary file in a temporary directory, created especially for the test function which calls the Script method. When working with multiple test scripts, these scripts must be referenced via automatically injected environment variables named as the script (but without an ".sh" suffix).

For example, script “a.sh” wants to call script “b.sh”, so it needs to substitute “b.sh” by “$b” (or “${b}”):

# script "a"
$b arg1 arg2 etc # call script "b"

Invalid characters in shell script names, such as “-”, will be replaced by “_” in the name of the corresponding environment variable.

Example

Defines two scripts, where the first script will be the entry point, and the second script is called (indirectly) from the first script and run within a new Linux kernel user namespace. The second script will send information about the newly created user namespace to our example, which might be a test case then checking the results. Finally, it tells the auxiliary script to enter the next phase, which is simply terminating, thereby automatically destroying the newly created user namespace.

Please note that some distributions mistakenly see the Linux kernel ability to create new user namespaces without special capabilities to be insecure (but cannot demonstrate how), so they patch upstream kernels to block this functionality. On such distributions this example will fail unless run as root ... "thank you for observing the security precautions".

// A zero Basher is already usable, so need for NewBasher().
scripts := Basher{}
defer scripts.Done()
// Define a first script named "newuserns", which we will later start as
// out entry point. This script creates a new Linux kernel user namespace,
// where the unshare(2) command then executes a second script (which we'll
// define next).
//
// Since the defined script are stored in temporary files, there is no way
// to know beforehand where these files will be stored and named, and thus
// we don't know how to directly call them. Instead, we use the $userns
// environment variable which will point to the correct temporary
// filepath.
scripts.Script("newuserns", `
unshare -Ufr $userns # call the script named "userns" via $userns.
`)
// This second script named "userns" returns information about the newly
// created and then waits to proceed into termination, thereby destroying
// the user namespace.
scripts.Script("userns", `
echo "\"$(readlink /proc/self/ns/user)\"" # ...turns it into a JSON string.
read # wait for test to proceed()
`)
// Start the first defined script named "newuserns"...
cmd := scripts.Start("newuserns")
defer cmd.Close()
// Read in the (JSON) information sent by the running script.
var userns string
cmd.Decode(&userns)
fmt.Println("temporary user namespace:", userns)
// Tell the script to finish; this can be omitted if the last step,
// because calling Close() on a Basher will automatically issue a final
// Proceed().
cmd.Proceed()
Output:

func (*Basher) Common

func (b *Basher) Common(script string)

Common adds an unnamed script with common definitions, which are then automatically made available to all (non-common) scripts.

func (*Basher) Done

func (b *Basher) Done()

Done cleans up all temporary scripts and preferably is to be defer'ed by a test case immediately after creating a Basher.

func (*Basher) Script

func (b *Basher) Script(name, script string)

Script adds a (BASH) script with the given name. The script will automatically get the usual shebang prepended, so scripts should not include it themselves. An additional next line after the shebang will also be injected for sourcing a temporary file with script environment variables referencing the correct temporary script file path and filenames.

For example: script “foo” (or “foo.sh”) will have an associated environment variable “$foo” pointing to its temporary location. A script “foo-bar” has the associated environment variable “$foo_bar”.

func (*Basher) Start

func (b *Basher) Start(name string, args ...string) *TestCommand

Start starts the named script as a new TestCommand, with the given arguments.

type Decoder

type Decoder struct {
	// wrapped JSON decoder
	*json.Decoder
	// contains filtered or unexported fields
}

Decoder wraps a json.Decoder using a memento stream reader in order to provide actually helpful error messages in case of JSON syntax errors, detailing the concrete input stream data where the problem occurred.

func NewDecoder

func NewDecoder(r io.Reader) *Decoder

NewDecoder returns a new memento-enabled JSON decoder, reading from the specified reader.

func (*Decoder) Decode

func (d *Decoder) Decode(v interface{}) error

Decode reads the next JSON-encoded value from its input and stores it in the value pointed to by v. In case of a JSON syntax error, the error returned contains the decoded input data so far in this Decode() call to give better insight of where things went wrong. The error returned wraps the original json.SyntaxError object. Other errors get also wrapped with additional details about the JSON input read up to now to provide a more meaningful context.

type MementoReader

type MementoReader struct {
	// contains filtered or unexported fields
}

MementoReader is an io.Reader wrapping another io.Reader and remembering what has been read so far, until it is allowed to forget by starting a new memory cycle using Mark().

func NewMementoReader

func NewMementoReader(r io.Reader) *MementoReader

NewMementoReader returns a new MemontoReader wrapping the specified io.Reader.

func (*MementoReader) Mark

func (m *MementoReader) Mark(offset int64)

Mark sets the beginning of the memento, forgetting the previous memento. All data read from now on will be remembered until the next Mark().

func (*MementoReader) Memento

func (m *MementoReader) Memento(offset int64) []byte

Memento returns the data read since the last Mark(). This does not reset the memento.

func (*MementoReader) Read

func (m *MementoReader) Read(p []byte) (n int, err error)

Read reads up to len(p) bytes into p. It returns the number of bytes read (0 <= n <= len(p)) and any error encountered. Even if Read returns n < len(p), it may use all of p as scratch space during the call. If some data is available but not len(p) bytes, Read conventionally returns what is available instead of waiting for more.

type TestCommand

type TestCommand struct {
	// contains filtered or unexported fields
}

TestCommand is a command run as part of testing which, for instance, sets up some namespaces. The output of the TestCommand is streamed back in order to return data back to the test which is relevant to the test itself (such as namespace IDs). This return data is Decode'd as JSON, and it can be transferred in multiple and separate JSON data elements, to allow for a multi-stage test command (see also the Proceed method).

func NewTestCommand

func NewTestCommand(command string, args ...string) *TestCommand

NewTestCommand starts a command with arguments and then allows to read JSON data from the command and interact with the command in order to optionally step it through multiple stages under full control of a test. See the Decode and Proceed methods for details. When done, please Close a TestCommand.

func (*TestCommand) Close

func (cmd *TestCommand) Close()

Close completes the command by sending it an ENTER input and then closing the input pipe to the command. Then close waits at most 2s for the command to finish its business. If the command passes the timeout, then it will be killed hard.

This method does nothing if the test command has already been closed or is in the process of being closed.

func (*TestCommand) Decode

func (cmd *TestCommand) Decode(v interface{})

Decode reads JSON from the test command's output and tries to decode it into the data element specified.

func (*TestCommand) Proceed

func (cmd *TestCommand) Proceed()

Proceed sends the test command an ENTER input. This should be interpreted by the test command to advance into the next test phase for this command, or to finally terminate gracefully.

func (*TestCommand) Tell added in v1.0.4

func (cmd *TestCommand) Tell(what string)

Tell send the test command some text input, followed by ENTER.

Jump to

Keyboard shortcuts

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