runn

package module
v0.20.0 Latest Latest
Warning

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

Go to latest
Published: Jun 3, 2022 License: MIT Imports: 40 Imported by: 6

README

runn

runn ( means "Run N" ) is a package/tool for running operations following a scenario.

Key features of runn are:

  • As a tool for scenario testing.
  • As a test helper package for the Go language.
  • As a tool for automation.
  • OpenAPI Document-like syntax for HTTP request testing.

Usage

runn can run a multi-step scenario following a runbook written in YAML format.

As a tool for scenario testing / As a tool for automation.

runn can run one or more runbooks as a CLI tool.

$ runn list path/to/**/*.yml
  Desc                               Path                               If
---------------------------------------------------------------------------------
  Login and get projects.            pato/to/book/projects.yml
  Login and logout.                  pato/to/book/logout.yml
  Only if included.                  pato/to/book/only_if_included.yml  included
$ runn run path/to/**/*.yml
Login and get projects. ... ok
Login and logout. ... ok
Only if included. ... skip

3 scenarios, 1 skipped, 0 failures

As a test helper package for the Go language.

runn can also behave as a test helper for the Go language.

Run N runbooks using httptest.Server
func TestRouter(t *testing.T) {
	ctx := context.Background()
	db, err := sql.Open("postgres", "user=root password=root host=localhost dbname=test sslmode=disable")
	if err != nil {
		log.Fatal(err)
	}
	ts := httptest.NewServer(NewRouter(db))
	t.Cleanup(func() {
		ts.Close()
		db.Close()
	})
	o, err := runn.Load("testdata/books/**/*.yml", runn.T(t), runn.Runner("req", ts.URL), runn.DBRunner("db", db))
	if err != nil {
		t.Fatal(err)
	}
	if err := o.RunN(ctx); err != nil {
		t.Fatal(err)
	}
}
Run single runbook using httptest.Server
func TestRouter(t *testing.T) {
	ctx := context.Background()
	db, err := sql.Open("postgres", "user=root password=root host=localhost dbname=test sslmode=disable")
	if err != nil {
		log.Fatal(err)
	}
	ts := httptest.NewServer(NewRouter(db))
	t.Cleanup(func() {
		ts.Close()
		db.Close()
	})
	o, err := runn.New(runn.T(t), runn.Book("testdata/books/login.yml"), runn.Runner("req", ts.URL), runn.DBRunner("db", db))
	if err != nil {
		t.Fatal(err)
	}
	if err := o.Run(ctx); err != nil {
		t.Fatal(err)
	}
}
Run N runbooks with http.Handler
func TestRouter(t *testing.T) {
	ctx := context.Background()
	db, err := sql.Open("postgres", "user=root password=root host=localhost dbname=test sslmode=disable")
	if err != nil {
		log.Fatal(err)
	}
	t.Cleanup(func() {
		db.Close()
	})
	o, err := runn.Load("testdata/books/**/*.yml", runn.T(t), runn.HTTPRunnerWithHandler("req", NewRouter(db)), runn.DBRunner("db", db))
	if err != nil {
		t.Fatal(err)
	}
	if err := o.RunN(ctx); err != nil {
		t.Fatal(err)
	}
}

Runbook ( runn scenario file )

The runbook file has the following format.

step: section accepts array or ordered map.

Array:

desc: Login and get projects.
runners:
  req: https://example.com/api/v1
  db: mysql://root:mypass@localhost:3306/testdb
vars:
  username: alice
steps:
  -
    db:
      query: SELECT * FROM users WHERE name = '{{ vars.username }}'
  -
    req:
      /login:
        post:
          body:
            application/json:
              email: "{{ steps[0].rows[0].email }}"
              password: "{{ steps[0].rows[0].password }}"
    test: steps[1].res.status == 200
  -
    req:
      /projects:
        get:
          headers:
            Authorization: "token {{ steps[1].res.body.session_token }}"
          body: null
    test: steps[2].res.status == 200
  -
    test: len(steps[2].res.body.projects) > 0

Map:

desc: Login and get projects.
runners:
  req: https://example.com/api/v1
  db: mysql://root:mypass@localhost:3306/testdb
vars:
  username: alice
steps:
  find_user:
    db:
      query: SELECT * FROM users WHERE name = '{{ vars.username }}'
  login:
    req:
      /login:
        post:
          body:
            application/json:
              email: "{{ steps.find_user.rows[0].email }}"
              password: "{{ steps.find_user.rows[0].password }}"
    test: steps.login.res.status == 200
  list_projects:
    req:
      /projects:
        get:
          headers:
            Authorization: "token {{ steps.login.res.body.session_token }}"
          body: null
    test: steps.list_projects.res.status == 200
  count_projects:
    test: len(steps.list_projects.res.body.projects) > 0

Array:

color

Map:

color

desc:

Description of runbook.

runners:

Mapping of runners that run steps: of runbook.

In the steps: section, call the runner with the key specified in the runners: section.

Built-in runners such as test runner do not need to be specified in this section.

runners:
  ghapi: ${GITHUB_API_ENDPOINT}
  idp: https://auth.example.com
  db: my:dbuser:${DB_PASS}@hostname:3306/dbname

In the example, each runner can be called by ghapi:, idp: or db: in steps:.

vars:

Mapping of variables available in the steps: of runbook.

vars:
  username: alice@example.com
  token: ${SECRET_TOKEN}

In the example, each variable can be used in {{ vars.username }} or {{ vars.token }} in steps:.

debug:

Enable debug output for runn.

debug: true

if:

Conditions for skip all steps.

if: included # Run steps only if included

skipTest:

Skip all test: sections

skipTest: true

steps:

Steps to run in runbook.

The steps are invoked in order from top to bottom.

Any return values are recorded for each step.

When steps: is array, recorded values can be retrieved with {{ steps[*].* }}.

steps:
  -
    db:
      query: SELECT * FROM users WHERE name = '{{ vars.username }}'
  -
    req:
      /users/{{ steps[0].rows[0].id }}:
        get:
          body: null

When steps: is map, recorded values can be retrieved with {{ steps.<key>.* }}.

steps:
  find_user:
    db:
      query: SELECT * FROM users WHERE name = '{{ vars.username }}'
  user_info:
    req:
      /users/{{ steps.find_user.rows[0].id }}:
        get:
          body: null

steps[*].desc: steps.<key>.desc:

Description of step.

steps[*].if: steps.<key>.if:

Conditions for skip step.

steps:
  login:
    if: 'len(vars.token) == 0' # Run step only if var.token is not set
    req:
      /login:
        post:
          body:
[...]

steps[*].retry: steps.<key>.retry:

Retry settings for steps.

steps:
  waitingroom:
    retry:
      count: 10
      until: 'steps.waitingroom.res.status == "201"'
      minInterval: 0.5 # sec
      maxInterval: 10  # sec
      # jitter: 0.0
      # interval: 5
      # multiplier: 1.5
    req:
      /cart/in:
        post:
          body:
[...]

Runner

HTTP Runner: Do HTTP request

Use https:// or http:// scheme to specify HTTP Runner.

When the step is invoked, it sends the specified HTTP Request and records the response.

runners:
  ghapi: https://api.github.com
Validation of HTTP request and HTTP response

HTTP requests sent by runn and their HTTP responses can be validated.

OpenAPI v3:

runners:
  myapi:
    endpoint: https://api.github.com
    openapi3: path/to/openapi.yaml
    # skipValidateRequest: false
    # skipValidateResponse: false

DB Runner: Query a database

Use dsn (Data Source Name) to specify DB Runner.

When step is executed, it executes the specified query the database.

If the query is a SELECT clause, it records the selected rows, otherwise it records last_insert_id and rows_affected .

Support Databases

PostgreSQL:

runners:
  mydb: postgres://dbuser:dbpass@hostname:5432/dbname
runners:
  db: pg://dbuser:dbpass@hostname:5432/dbname

MySQL:

runners:
  testdb: mysql://dbuser:dbpass@hostname:3306/dbname
runners:
  db: my://dbuser:dbpass@hostname:3306/dbname

SQLite3:

runners:
  db: sqlite:///path/to/dbname.db
runners:
  local: sq://dbname.db

Exec Runner: execute command

The exec runner is a built-in runner, so there is no need to specify it in the runners: section.

It execute command using command: and stdin:

-
  exec:
    command: grep error
    stdin: '{{ steps[3].res.rawBody }}'

Test Runner: test using recorded values

The test runner is a built-in runner, so there is no need to specify it in the runners: section.

It evaluates the conditional expression using the recorded values.

-
  test: steps[3].res.status == 200

The test runner can run in the same steps as the other runners.

Dump Runner: dump recorded values

The dump runner is a built-in runner, so there is no need to specify it in the runners: section.

It dumps the specified recorded values.

-
  dump: steps[4].rows

The dump runner can run in the same steps as the other runners.

Include Runner: include other runbook

The include runner is a built-in runner, so there is no need to specify it in the runners: section.

Include runner reads and runs the runbook in the specified path.

Recorded values are nested.

-
  include: path/to/get_token.yml

It is also possible to override vars: of included runbook.

-
  include:
    path: path/to/login.yml
    vars:
      username: alice
      password: alicepass
-
  include:
    path: path/to/login.yml
    vars:
      username: bob
      password: bobpass

It is also possible to skip all test: sections in the included runbook.

-
  include:
    path: path/to/signup.yml
    skipTest: true

Bind Runner: bind variables

The bind runner is a built-in runner, so there is no need to specify it in the runners: section.

It bind runner binds any values with another key.

  -
    req:
      /users/k1low:
        get:
          body: null
  -
    bind:
      user_id: steps[0].res.body.data.id
  -
    dump: user_id

The bind runner can run in the same steps as the other runners.

Option

See https://pkg.go.dev/github.com/k1LoW/runn#Option

Example: Run as a test helper

https://pkg.go.dev/github.com/k1LoW/runn#T

o, err := runn.Load("testdata/**/*.yml", runn.T(t))
if err != nil {
	t.Fatal(err)
}
if err := o.RunN(ctx); err != nil {
	t.Fatal(err)
}

Example: Add custom function

https://pkg.go.dev/github.com/k1LoW/runn#Func

desc: Test using GitHub
runners:
  req:
    endpoint: https://github.com
steps:
  -
    req:
      /search?l={{ urlencode('C++') }}&q=runn&type=Repositories:
        get:
          body:
            application/json:
              null
    test: 'steps[0].res.status == 200'
o, err := runn.Load("testdata/**/*.yml", runn.Func("urlencode", url.QueryEscape))
if err != nil {
	t.Fatal(err)
}
if err := o.RunN(ctx); err != nil {
	t.Fatal(err)
}

Install

As a CLI tool

deb:

Use dpkg-i-from-url

$ export RUNN_VERSION=X.X.X
$ curl -o runn.deb -L https://github.com/k1LoW/runn/releases/download/v$RUNN_VERSION/runn_$RUNN_VERSION-1_amd64.deb
$ dpkg -i runn.deb

RPM:

$ export RUNN_VERSION=X.X.X
$ yum install https://github.com/k1LoW/runn/releases/download/v$RUNN_VERSION/runn_$RUNN_VERSION-1_amd64.rpm

apk:

Use apk-add-from-url

$ export RUNN_VERSION=X.X.X
$ curl -o runn.apk -L https://github.com/k1LoW/runn/releases/download/v$RUNN_VERSION/runn_$RUNN_VERSION-1_amd64.apk
$ apk add runn.apk

homebrew tap:

$ brew install k1LoW/tap/runn

manually:

Download binary from releases page

docker:

$ docker pull ghcr.io/k1low/runn:latest

go install:

$ go install github.com/k1LoW/runn/cmd/runn@latest

As a test helper

$ go get github.com/k1LoW/runn

Alternatives

References

Documentation

Index

Constants

View Source
const (
	MediaTypeApplicationJSON           = "application/json"
	MediaTypeTextPlain                 = "text/plain"
	MediaTypeApplicationFormUrlencoded = "application/x-www-form-urlencoded"
)

Variables

View Source
var (
	AsTestHelper = T
	Runbook      = Book
)

Functions

func GetDesc added in v0.4.0

func GetDesc(opt Option) string

func Load added in v0.2.0

func Load(pathp string, opts ...Option) (*operators, error)

func LoadBook

func LoadBook(path string) (*book, error)

func New

func New(opts ...Option) (*operator, error)

func NewHttpValidator added in v0.13.0

func NewHttpValidator(c *RunnerConfig) (httpValidator, error)

func NewNopValidator added in v0.13.0

func NewNopValidator() *nopValidator

func NewOpenApi3Validator added in v0.13.0

func NewOpenApi3Validator(c *RunnerConfig) (*openApi3Validator, error)

func Paths added in v0.4.0

func Paths(pathp string) ([]string, error)

Types

type Option

type Option func(*book) error

func AfterFunc added in v0.20.0

func AfterFunc(fn func() error) Option

AfterFunc - Register the function to be run after the runbook is run.

func BeforeFunc added in v0.20.0

func BeforeFunc(fn func() error) Option

BeforeFunc - Register the function to be run before the runbook is run.

func Book

func Book(path string) Option

Book - Load runbook

func Books added in v0.4.0

func Books(pathp string) ([]Option, error)

func DBRunner

func DBRunner(name string, client *sql.DB) Option

DBRunner - Set db runner to runbook

func Debug added in v0.3.0

func Debug(debug bool) Option

Debug - Enable debug output

func Desc

func Desc(desc string) Option

Desc - Set description to runbook

func FailFast added in v0.10.1

func FailFast(enable bool) Option

FailFast - Enable fail-fast

func Func added in v0.18.0

func Func(k string, v interface{}) Option

Func - Set function to runner

func HTTPRunner

func HTTPRunner(name, endpoint string, client *http.Client, opts ...RunnerOption) Option

HTTPRunner - Set http runner to runbook

func HTTPRunnerWithHandler added in v0.6.0

func HTTPRunnerWithHandler(name string, h http.Handler, opts ...RunnerOption) Option

HTTPRunner - Set http runner to runbook with http.Handler

func Interval added in v0.9.0

func Interval(d time.Duration) Option

Interval - Set interval between steps

func Runner

func Runner(name, dsn string, opts ...RunnerOption) Option

Runner - Set runner to runbook

func SkipIncluded added in v0.19.0

func SkipIncluded(enable bool) Option

SkipIncluded - Skip running the included step by itself.

func SkipTest added in v0.20.0

func SkipTest(enable bool) Option

SkipTest - Skip test section

func T

func T(t *testing.T) Option

T - Acts as test helper

func Var added in v0.4.0

func Var(k string, v interface{}) Option

Var - Set variable to runner

type Retry added in v0.17.0

type Retry struct {
	Count       *int     `yaml:"count,omitempty"`
	Interval    *float64 `yaml:"interval,omitempty"`
	MinInterval *float64 `yaml:"minInterval,omitempty"`
	MaxInterval *float64 `yaml:"maxInterval,omitempty"`
	Jitter      *float64 `yaml:"jitter,omitempty"`
	Multiplier  *float64 `yaml:"multiplier,omitempty"`
	Until       string   `yaml:"until"`
	// contains filtered or unexported fields
}

func (*Retry) Retry added in v0.17.0

func (r *Retry) Retry(ctx context.Context) bool

type RunnerConfig added in v0.13.0

type RunnerConfig struct {
	// for httpRunner
	Endpoint             string `yaml:"endpoint,omitempty"`
	OpenApi3DocLocation  string `yaml:"openapi3,omitempty"`
	SkipValidateRequest  bool   `yaml:"skipValidateRequest,omitempty"`
	SkipValidateResponse bool   `yaml:"skipValidateResponse,omitempty"`
	// contains filtered or unexported fields
}

RunnerConfig is polymorphic config for runner

type RunnerOption added in v0.13.0

type RunnerOption func(*RunnerConfig) error

func OpenApi3 added in v0.13.0

func OpenApi3(l string) RunnerOption

func OpenApi3FromData added in v0.13.0

func OpenApi3FromData(d []byte) RunnerOption

func SkipValidateRequest added in v0.13.0

func SkipValidateRequest(skip bool) RunnerOption

func SkipValidateResponse added in v0.13.0

func SkipValidateResponse(skip bool) RunnerOption

type UnsupportedError added in v0.17.1

type UnsupportedError struct {
	Cause error
}

func (*UnsupportedError) Error added in v0.17.1

func (e *UnsupportedError) Error() string

func (UnsupportedError) Unwrap added in v0.17.1

func (e UnsupportedError) Unwrap() error

Directories

Path Synopsis
cmd

Jump to

Keyboard shortcuts

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