Code Monkey home page Code Monkey logo

smoke's Introduction

GitHub Actions Build Status

Smoke

An integration test framework for practically anything.

Smoke output

Smoke is designed to test anything that can be wrapped with a command-line interface. In practice, this amounts to almost any application or large piece of code. Whatever you're working on, no matter how big or complicated, you can usually wrap a CLI around it with minimum effort.

Smoke works especially well for testing large applications, especially after the fact. It allows you to create regression tests, golden master tests, and other things that make refactoring a legacy application much easier.

It's not a replacement for other, smaller tests. We recommend writing unit tests (perhaps even first), especially for new code.

Smoke is distributed under the MIT license.

Installation

You can download the latest release from the Releases page.

  • The latest Windows release was built on Windows Server 2022.
  • The latest macOS release was built on macOS 11 (Big Sur), on x86_64 hardware.
    • If you need native arm64 support, you will need to build it yourself.
  • The latest Linux release was built on Ubuntu 20.04, on x86_64 hardware.
    • The binary depends on the dynamic libraries glibc and gmp.
    • If you're running on a non-glibc-based OS such as Alpine Linux, you will either need to build it yourself or install both gcompat and gmp.
    • If you need native arm64 support, you will need to build it yourself.

Building

You can also build it yourself, using either Nix or Stack.

With Nix:

  1. Install Nix.
  2. Run nix-build -o ./out/build.
  3. Find Smoke at ./out/build/bin/smoke.

With Stack:

  1. Install Stack.
  2. Run stack install --local-bin-path=./out/build.
  3. Find Smoke at ./out/build/smoke.

Writing Test Cases

A test case consists of input and expected output. It's made with a YAML file.

First off, you need to specify the command itself. The command is the program to be run (and any common arguments). It is executed from the current working directory. The command can be overriden for each individual test case too.

Input can come in two forms: standard input and command-line arguments.

  • Command-line arguments are appended to the command to run it.
  • Standard input is piped into the program on execution.

Outputs that can be observed by Smoke consist of standard output, standard error and the exit status of the program. These are captured by running the program, then compared to the expected values specified. Any difference results in a test failure.

  • Expected standard output is compared with the actual standard output. Alternatively, multiple possible expected outputs can be specified. If there are multiple outputs, a match with any of them will be considered a success.
  • Expected standard error works in exactly the same way as expected standard output.
  • The expected exit status is a single number between 0 and 255.

At least one of standard output and standard error must be specified, though it can be empty. If no exit status is specified, it will be assumed to be 0.

Simple test cases

For a simple example, let's try testing a command-line calculator program.

Our simplest calculator test case looks like this. It's a specification file named smoke.yaml (the file basename is a convention; you can name it anything you want ending in .yaml).

command:
  - ruby
  - calculator.rb

tests:
  - name: addition
    stdin: |
      2 + 2
    stdout: |
      4

That's it.

We use the YAML operator | to capture the following indented block as a string. This allows us to easily check for multiline output, and includes the trailing newline, which is useful when dealing with software that typically prints a newline at the end of execution. It also guarantees that we parse the value as a string, and not, for example, as a number, as in the case above.

We might want to assert that certain things fail. For example, postfix notation should fail because the second token is expected to be an operator. In this example, our calculator is expected to produce a semi-reasonable error message and exit with a status of 2 to signify a parsing error.

tests:
  # ...
  - name: postfix-notation-fails
    stdin: |
      5 3 *
    exit-status: 2
    stderr: |
      "3" is not a valid operator.

Sometimes the response might be one of a few different values, in which case, we can specify an array of possible outcomes:

tests:
  # ...
  - name: square root
    stdin: |
      sqrt(4)
    stdout:
      - |
        2
      - |
        -2

We don't always want to check the full output; sometimes checking that it contains a given substring is more useful. We can use the contains: operator to specify this:

tests:
  # ...
  - name: big multiplication
    stdin: |
      961748927 * 982451653
    stdout:
      contains: "1021"

Note that we don't use | here, as we don't want to capture the trailing newline, which would make this test fail. Instead we use quotes around the value to ensure that the YAML parser treats it as a string, not a number.

You can also use equals: to explicitly specify that we're checking equality, though this is the default.

We can use files to specify the STDIN, STDOUT or STDERR values:

tests:
  - name: subtraction
    stdin:
      file: tests/subtraction.in
    stdout:
      file: tests/subtraction.out

Using files gives us one big advantage over specifying the content inline: if the tests fail, but the actual output looks correct, we can "bless" the new STDOUT or STDERR with the --bless flag. This means you don't have to spend time copying and pasting, and can instead just approve the results automatically.

We can ignore tests (temporarily, we hope) by adding ignored: true.

And, of course, you can combine all these techniques together.

Testing files

Smoke can also test that your application wrote a file.

For example, if we wanted to test that our implementation of the cp (copy) command worked, we could write the following:

working-directory: .

tests:
  - name: copy a file
    command:
      - cp
    args:
      - input.file
      - output.file
    files:
      - path: output.file
        contents:
          file: input.file
    revert:
      - .

You also need to create a file called input.file, containing whatever you want.

This test will run cp input.file output.file. In doing so, it will set the working directory to the same directory as the smoke.yaml file (so you can run the tests from anywhere, and they'll behave exactly the same). It will then revert the entire contents of the specified directory, ., to its state before the test was run.

As with STDOUT and STDERR, you can also use --bless to automatically accept the new output if it changes.

Take a look at the files fixture for more examples.

Running with a shell

Sometimes writing a small program for testing purposes is a bit distracting or over the top. We can specify a string as the contents of a command, which will be passed to the default shell (sh on Unix, cmd on Windows).

You can override the shell, too, both per test, or at the top-level, by providing a shell: section. For example, we could use this to pass the -e flag to bash, making sure it fails fast:

tests:
  - name: use custom shell flags
    command:
      shell:
        - bash
        - -e
      script: |
        echo 'Something.' >&2
        false
        echo 'Something else.' >&2
    exit-status: 1
    stderr: |
      Something.

You could even set your shell to python or ruby (or your favorite scripting language) to allow you to embed scripts of any kind in your Smoke specification.

You can find more examples in the local shell fixture and global shell fixture.

Setting environment variables

It is possible to set environment variables, both as defaults for a suite, and for a specific test. Environment variables inherit from the suite, and the suite inherits from the environment smoke is started in.

environment:
  CI: "0"

tests:
  - name:
    environment:
      CI: "1"
    command: echo ${CI}
    stdout: |
      1

You can find more examples in the environment fixture.

Filtering output

Sometimes, things aren't quite so deterministic. When some of the output (or input) is meaningless, or if there's just too much, you can specify filters to transform the data.

httpbin.org provides a simple set of HTTP endpoints that repeat what you tell them to. When I GET https://httpbin.org/get?foo=bar, the response body looks like this:

{
  "args": {
    "foo": "bar"
  },
  "headers": {
    "Accept": "*/*",
    "Accept-Encoding": "gzip, deflate",
    "Connection": "close",
    "Host": "httpbin.org",
    "User-Agent": "HTTPie/1.0.0"
  },
  "origin": "1.2.3.4",
  "url": "https://httpbin.org/get?foo=bar"
}

Unfortunately, because of the "origin", this isn't very testable, as that might as well be random data. Given that I only really care about the "args" property, I can use jq to just extract that part:

command:
  - http

tests:
  - name: get
    args:
      - GET
      - https://httpbin.org/get?foo=bar
    stdout:
      contents: |
        {
          "foo": "bar"
        }
      filter:
        - jq
        - .args

Now my test passes every time.

You can also specify the filter as an inline script by using a string rather than an array. It will be run with sh -c. We prefer the array structure, as it's more portable.

There are more examples in the processing-filters fixture.

Further examples

If you're looking for more examples, take a look in the fixtures directory.

Running Tests

In order to run tests against an application, you simply invoke Smoke with the directory containing the tests. Given the tests in the test directory, we would run the tests as follows:

smoke test

You can provide individual file names instead:

smoke test/one.yaml test/two.yaml

If you want to run all tests matching a pattern, you can use find to find the files, and then pass them to Smoke (type man find for help):

smoke $(find test -name '*.smoke.yaml')

Tests can also be passed on an individual basis:

smoke test/smoke.yaml@addition test/smoke.yaml@subtraction

To override the command, or to specify it on the command line instead of the command property, you can use the --command option:

smoke --command='ruby calculator.rb' test

Bear in mind that Smoke simply splits the argument to the --command option by whitespace, so quoting, escaping, etc. will not work. For anything complicated, use a file instead.

Smoke will exit with a code of 0 if all tests succeed, or non-zero if any test fails, or if the invocation of Smoke itself was not understood (for example, if no test locations are provided).

Output will be in color if outputting to a terminal. You can force color output on or off with the --color and --no-color switches.

Enjoy. Any feedback is welcome.

Origins

We had a problem at work. It was a pretty nice problem to have. We were getting too many job applicants and we needed to screen them quickly. So we put some tests online and pointed the guinea pigs candidates at 'em.

We quickly found we had another problem: it was taking a lot of developer time to decide whether we should bring in the furballs for real-life interviews. So one night, while more than a little tipsy, I wrote Smoke.

We let our interview candidates write code in whatever they like: Java, C#, Python, Ruby… I needed a test framework that could handle any language under the sun. At first, I thought about ways to crowbar RSpec into running tests for applications in any and all languages. This was a stupid idea. Eventually I decided the only thing every language has in common is the command line: every language can pretty easily support standard input and output (with the obvious exception of Java, which makes everything difficult).

I have to stress that this is not a replacement for looking over people's code. I've invited people for further interview even when failed every one of my test cases, because they understood the problem and mostly solved it. Similarly, someone that passes every case but writes Python like people wrote C in the 80s makes me very sad, despite all the green output from Smoke.

Contributing

Issues and pull requests are very welcome. Please don't hesitate.

Developers of Smoke pledge to follow the Contributor Covenant.

You will need to set up Nix as above, and enter a Nix shell with nix-shell, or use lorri with direnv.

We dog-food. Smoke is tested using itself.

Before committing, these commands should be run, and any failures should be fixed:

cabal v2-update # Update the Cabal packages.
make reformat   # Reformats the code using ormolu and nixpkgs-fmt.
make build      # Builds the application using Cabal.
make test       # Run the unit tests.
make spec       # Tests the application using itself, with the tests in the "spec" directory.
make lint       # Lints the code using HLint.

(You can typically just run make reformat check to trigger them all.)

Some development tools, such as ormolu, don't work on Windows, so we encourage you to develop on a Nix-compatible environment. However, if you need to write or test some code on Windows, you can always read the Makefile to figure out what commands to run.

Smoke should work on Linux and macOS without any issue. Almost all features should also work on Windows, with the exception of allowing scripts as commands. This is due to a (quite reasonable) limitation of Windows; you can't make text files executable.

smoke's People

Contributors

andir avatar codecop avatar gilligan avatar rradczewski avatar samirtalwar avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

smoke's Issues

Support for a way to run the same kind of test on many files

One use case for Smoke is to simply provide many different potential input and output files, and compare them. However, it gets old doing the following:

tests:
  - name: thing 1
    stdin:
      file: thing1.in
    stdout:
      file: thing1.out

  - name: thing 2
    stdin:
      file: thing2.in
    stdout:
      file: thing2.out

  - name: thing 3
    stdin:
      file: thing3.in
    stdout:
      file: thing3.out

# ...

Not only is it monotonous, but error-prone.

@jaspervdj suggested finding a better way to avoid this repetition, just like goldplate does.

about regex test case

example
I add daytime into os-release , like OS-VERSION=GITHUB-20230320-AAAA

my test cast

command:
  - sh

tests:
  - name: bsp version check
    command: |
      day=20230320
      cat /etc/os-release
    stdout:
      matches: 
        OS-VERSION=.*${day}.*

it is failed:smoke: ParseError {errError = ICUError U_REGEX_BAD_INTERVAL, errLine = Just 1, errOffset = Just 20}

if change ${day} to 20230320, it is success

command:
  - sh

tests:
  - name: bsp version check
    command: |
      day=20230320
      cat /etc/os-release
    stdout:
      matches: 
        OS-VERSION=.*20230320.*

so , If i want use parm for day , How should I write stdout?

How to assert part of a string

I would love to do something like this

tests:
  - name: ...
    stdout:
      contains: |
        12345

Currently, I had not found a way to assert for part of the string. In the docs I only saw how to match all of stdout.

PS: what is the | at the end of contains: |, is this yaml standard? You use it in many places but I didnt find an explanation. Thanks

Why strip the input?

approx line 81 .collect { |_, type, file| [type, IO.read(file).strip] } strips the input. This makes it impossible to send content with a leading empty line to the system under test. Also it makes it impossible to send a single CR/LF to it. Maybe input should be verbatim, so used exactly as is?

--bless adds Windows newlines in binary files

Hi Samir, hope you are doing well. I am using latest Smoke to test some command line tools. Some tools create binary output, e.g. BMP images. In these situations,

  - name: "XCOLOR"
    command: "..\\bin\\XCOLOR 80x25x256.bmp test.out 0 1"
    files:
      - path: test.out
        contents:
          file: xcolor.bmp

the xcolor.bmp had 0d added before all 0a bytes. I am using Windows. I guess it is something of the fwrite/C function or mode of file you use. Or maybe we need a switch to indicate binary files?

qemu aarch64 build fail

text-icu> configure
text-icu> Configuring text-icu-0.8.0.2...
text-icu> Cabal-simple_SvXsv1f__3.6.3.0_ghc-9.2.5: The program 'pkg-config' version
text-icu> >=0.9.0 is required but it could not be found.
text-icu>
Progress 1/2

Error: [S-7282]
Stack failed to execute the build plan.

   While executing the build plan, Stack encountered the following errors:

   [S-7011]
   While building package text-icu-0.8.0.2 (scroll up to its section to see the error) using:

qemu : ubuntu 20.04.5 lts aarch64
kernel linux version 5.10.146
already install libicu-dev

Concurrent test runs where possible

In many cases, Smoke could potentially run multiple tests concurrently. Lots of the examples (e.g. the calculator) in the fixtures directory would work.

Because Smoke spawns programs that might do anything, I think this should be opt-in, behind a switch that sets the maximum number of concurrent tests (e.g. --concurrency=4).

Often, most of your tests can be run concurrently, but a few may not be able to. For example, tests that mutate files used by other tests are probably way out. This means that there needs to be a way to mark certain tests as serial, potentially with a YAML switch such as serial: true or exclusive: true.

I expect there's certain tests that should never be run concurrently. For example, if two tests refer to the same directory in a revert: declaration (or one refers to a descendant of another), those tests should never be run at the same time. However, they might be able to run in parallel with other tests. A simple implementation may not want to bother with this, though, and just force all tests with a revert: declaration to run serially.

Regex support

Hey

I didn't see it any support for testing the output against a regex expression in the documentation.
Sorry if I miss it, but I consider it essential. One of the reasons I'm still using ShellTestRunner.

Show only test failures

Hey, thanks for the hard work here. I have recently started adopting Smoke for the Juvix project at the Smoke branch. So, let me open a few issues related to our needs. I'll start by saying that, for larger projects, warning about only failures
comes in handy instead of the current output. The flag could be as follows.

-h --hide-successes   Show only test failures

100 files with >100 lines output don't find error in 2nd line of files

I generated test cases for the uglytrivia shellscript game (you know it: https://github.com/jbrains/trivia) with the following command/script:

!/bin/bash

for I in {0..100};
do echo $I;
echo SEED=$I ./game.sh > test/game$I.in
SEED=$I ./game.sh > test/game$I.out
done

Anytime I open one of the .out files, add "asdf" as the second line (creative, I know) and run "smoke '/bin/bash' test/", it tells me that 100 tests succeeded even though they shouldn't have.

If I add any other line, the test usually failed as expected in my trying out.

Compatibility with alpine linux

First of all, thanks for this tool! It works very well, and severely underrated in terms of popularity IMO.

I've been using this to test my own solutions for the worksheets I make in my tutor job at my college, since apparently I cant be trusted with writing them right;-).

However while running this locally on windows or my regular linux works well, I stumbled when trying to use it in my gitlab pipeline via your prebuild binary, where I am using alpine images for the different tasks.

Concretely, after downloading the provided executable for linux to the right directory and making it executable I'm getting this:

Exec format error (os error 8)

I'm not entirely certain why its failing, not that experienced of a system programmer, my first guess was that smoke relies upon shared libraries that dont exist, prominently glibc instead of musl which alpine has, but at least installing gcompat (https://wiki.alpinelinux.org/wiki/Running_glibc_programs) didnt fix that.

I'm still looking for the issue or another fix so this is unknown as of now, and in general I will find an easy fix, but I just wanted to let you know that perhaps something could be done there, even if just documenting alternatives.

Gotta leave the train now, thanks for your time

Update to newer nixpkgs revision to support aarch64-darwin

It would be great if you could upgrade to a newer nixpkgs version. In particular one from the last couple of months. The reason for it being aarch64-darwin support which is becoming more and more common.

Within npmlock2nix we are using some for our integration tests and some users/contributors are on aarch64-darwin. Unfortunately passing a newer nixpkgs instance via the pkgs attribute of your default.nix doesn't entirely cut it. This leads to some weird Haskell errors that I can't make any sense of (as a non-haskell developer).

Remove outdated stack configuration

haskell/tar#26 has been merged some time ago.

smoke/stack.yaml

Lines 8 to 12 in 2ee7356

extra-deps:
# There was a bug in the `tar` package that is fixed in https://github.com/haskell/tar/pull/26.
# However, this code has not been released, so we need to grab the latest commit instead.
- git: https://github.com/haskell/tar.git
commit: dbf8c995153c8a80450724d9f94cf33403740c80

Run tests from within the test file's directory

Let Smoke discover and run all the tests available within a folder. The flag can be --execdir as in shelltestrunner. One optional flag could be -r --recursive if one wants to traverse the whole input directory.

The user may want to provide the extension for the test files to perform the search. I suggest having the suffix .smoke.yaml for this. The option flag can be --extension.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.