• Stars
    star
    154
  • Rank 242,095 (Top 5 %)
  • Language
    Go
  • License
    MIT License
  • Created over 2 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

runn is a package/tool for running operations following a scenario.

runn

build Coverage Code to Test Ratio Test Execution Time

runn ( means "Run N". is pronounced /rʌ́n én/. ) is a package/tool for running operations following a scenario.

Key features of runn are:

  • As a tool for scenario based testing.
  • As a test helper package for the Go language.
  • As a tool for workflow automation.
  • Support HTTP request, gRPC request, DB query, Chrome DevTools Protocol, and SSH/Local command execution
  • OpenAPI Document-like syntax for HTTP request testing.
  • Single binary = CI-Friendly.

Online book

Quickstart

You can use the runn new command to quickly start creating scenarios (runbooks).

🚀 Create and run scenario using curl or grpcurl commands:

docs/runn.svg

Command details
$ curl https://httpbin.org/json -H "accept: application/json"
{
  "slideshow": {
    "author": "Yours Truly",
    "date": "date of publication",
    "slides": [
      {
        "title": "Wake up to WonderWidgets!",
        "type": "all"
      },
      {
        "items": [
          "Why <em>WonderWidgets</em> are great",
          "Who <em>buys</em> WonderWidgets"
        ],
        "title": "Overview",
        "type": "all"
      }
    ],
    "title": "Sample Slide Show"
  }
}
$ runn new --and-run --desc 'httpbin.org GET' --out http.yml -- curl https://httpbin.org/json -H "accept: application/json"
$ grpcurl -d '{"greeting": "alice"}' grpcb.in:9001 hello.HelloService/SayHello
{
  "reply": "hello alice"
}
$ runn new --and-run --desc 'grpcb.in Call' --out grpc.yml -- grpcurl -d '{"greeting": "alice"}' grpcb.in:9001 hello.HelloService/SayHello
$ runn list *.yml
  Desc             Path      If
---------------------------------
  grpcb.in Call    grpc.yml
  httpbin.org GET  http.yml
$ runn run *.yml
..

2 scenarios, 0 skipped, 0 failures

🚀 Create scenario using access log:

docs/runn_axslog.svg

Command details
$ cat access_log
183.87.255.54 - - [18/May/2019:05:37:09 +0200] "GET /?post=%3script%3ealert(1); HTTP/1.0" 200 42433
62.109.16.162 - - [18/May/2019:05:37:12 +0200] "GET /core/files/js/editor.js/?form=\xeb\x2a\x5e\x89\x76\x08\xc6\x46\x07\x00\xc7\x46\x0c\x00\x00\x00\x80\xe8\xdc\xff\xff\xff/bin/sh HTTP/1.0" 200 81956
87.251.81.179 - - [18/May/2019:05:37:13 +0200] "GET /login.php/?user=admin&amount=100000 HTTP/1.0" 400 4797
103.36.79.144 - - [18/May/2019:05:37:14 +0200] "GET /authorize.php/.well-known/assetlinks.json HTTP/1.0" 200 9436
$ cat access_log| runn new --out axslog.yml
$ cat axslog.yml| yq
desc: Generated by `runn new`
runners:
  req: https://dummy.example.com
steps:
  - req:
      /?post=%3script%3ealert(1);:
        get:
          body: null
  - req:
      /core/files/js/editor.js/?form=xebx2ax5ex89x76x08xc6x46x07x00xc7x46x0cx00x00x00x80xe8xdcxffxffxff/bin/sh:
        get:
          body: null
  - req:
      /login.php/?user=admin&amount=100000:
        get:
          body: null
  - req:
      /authorize.php/.well-known/assetlinks.json:
        get:
          body: null
$

Usage

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

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

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

$ runn list path/to/**/*.yml
  id:      desc:             if:       steps:  path
-------------------------------------------------------------------------
  a1b7b02  Only if included  included       2  p/t/only_if_included.yml
  85ccd5f  List projects.                   4  p/t/p/list.yml
  47d7ef7  List users.                      3  p/t/u/list.yml
  97f9884  Login                            2  p/t/u/login.yml
  2249d1b  Logout                           3  p/t/u/logout.yml
$ runn run path/to/**/*.yml
S....

5 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 and sql.DB

func TestRouter(t *testing.T) {
	ctx := context.Background()
	dsn := "username:password@tcp(localhost:3306)/testdb"
	db, err := sql.Open("mysql", dsn)
	if err != nil {
		log.Fatal(err)
	}
	dbr, err := sql.Open("mysql", dsn)
	if err != nil {
		log.Fatal(err)
	}
	ts := httptest.NewServer(NewRouter(db))
	t.Cleanup(func() {
		ts.Close()
		db.Close()
		dbr.Close()
	})
	opts := []runn.Option{
		runn.T(t),
		runn.Runner("req", ts.URL),
		runn.DBRunner("db", dbr),
	}
	o, err := runn.Load("testdata/books/**/*.yml", opts...)
	if err != nil {
		t.Fatal(err)
	}
	if err := o.RunN(ctx); err != nil {
		t.Fatal(err)
	}
}

Run single runbook using httptest.Server and sql.DB

func TestRouter(t *testing.T) {
	ctx := context.Background()
	dsn := "username:password@tcp(localhost:3306)/testdb"
	db, err := sql.Open("mysql", dsn)
	if err != nil {
		log.Fatal(err)
	}
	dbr, err := sql.Open("mysql", dsn)
	if err != nil {
		log.Fatal(err)
	}
	ts := httptest.NewServer(NewRouter(db))
	t.Cleanup(func() {
		ts.Close()
		db.Close()
		dbr.Close()
	})
	opts := []runn.Option{
		runn.T(t),
		runn.Book("testdata/books/login.yml"),
		runn.Runner("req", ts.URL),
		runn.DBRunner("db", dbr),
	}
	o, err := runn.New(opts...)
	if err != nil {
		t.Fatal(err)
	}
	if err := o.Run(ctx); err != nil {
		t.Fatal(err)
	}
}

Run N runbooks using grpc.Server

func TestServer(t *testing.T) {
	addr := "127.0.0.1:8080"
	l, err := net.Listen("tcp", addr)
	if err != nil {
		t.Fatal(err)
	}
	ts := grpc.NewServer()
	myapppb.RegisterMyappServiceServer(s, NewMyappServer())
	reflection.Register(s)
	go func() {
		s.Serve(l)
	}()
	t.Cleanup(func() {
		ts.GracefulStop()
	})
	opts := []runn.Option{
		runn.T(t),
		runn.Runner("greq", fmt.Sprintf("grpc://%s", addr),
	}
	o, err := runn.Load("testdata/books/**/*.yml", opts...)
	if err != nil {
		t.Fatal(err)
	}
	if err := o.RunN(ctx); err != nil {
		t.Fatal(err)
	}
}

Run N runbooks with http.Handler and sql.DB

func TestRouter(t *testing.T) {
	ctx := context.Background()
	dsn := "username:password@tcp(localhost:3306)/testdb"
	db, err := sql.Open("mysql", dsn)
	if err != nil {
		log.Fatal(err)
	}
	dbr, err := sql.Open("mysql", dsn)
	if err != nil {
		log.Fatal(err)
	}
	t.Cleanup(func() {
		db.Close()
		dbr.Close()
	})
	opts := []runn.Option{
		runn.T(t),
		runn.HTTPRunnerWithHandler("req", NewRouter(db)),
		runn.DBRunner("db", dbr),
	}
	o, err := runn.Load("testdata/books/**/*.yml", opts...)
	if err != nil {
		t.Fatal(err)
	}
	if err := o.RunN(ctx); err != nil {
		t.Fatal(err)
	}
}

Examples

See the details

Runbook ( runn scenario file )

The runbook file has the following format.

step: section accepts list or ordered map.

List:

desc: Login and get projects.
runners:
  req: https://example.com/api/v1
  db: mysql://root:mypass@localhost:3306/testdb
vars:
  username: alice
  password: ${TEST_PASS}
steps:
  -
    db:
      query: SELECT * FROM users WHERE name = '{{ vars.username }}'
  -
    req:
      /login:
        post:
          body:
            application/json:
              email: "{{ steps[0].rows[0].email }}"
              password: "{{ vars.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
  password: ${TEST_PASS}
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: "{{ vars.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

Grouping of related parts by color

List:

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: [email protected]
  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

force:

Force all steps to run.

force: true

loop:

Loop setting for runbook.

Simple loop runbook

loop: 10
steps:
  [...]

or

loop:
  count: 10
steps:
  [...]

Retry runbook

It can be used as a retry mechanism by setting a condition in the until: section.

If the condition of until: is met, the loop is broken without waiting for the number of count: to be run.

Also, if the run of the number of count: completes but does not satisfy the condition of until:, then the step is considered to be failed.

loop:
  count: 10
  until: 'outcome == "success"' # until the runbook outcome is successful.
  minInterval: 0.5 # sec
  maxInterval: 10  # sec
  # jitter: 0.0
  # interval: 5
  # multiplier: 1.5
steps:
  waitingroom:
    req:
      /cart/in:
        post:
          body:
[...]
  • outcome ... the result of a completed (success, failure, skipped).

concurrency:

Runbooks with the same key are assured of a single run at the same time.

concurrency: use-shared-db

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[*].loop: steps.<key>.loop:

Loop settings for steps.

Simple loop step

steps:
  multicartin:
    loop: 10
    req:
      /cart/in:
        post:
          body:
            application/json:
              product_id: "{{ i }}" # The loop count (0..9) is assigned to `i`.
[...]

or

steps:
  multicartin:
    loop:
      count: 10
    req:
      /cart/in:
        post:
          body:
            application/json:
              product_id: "{{ i }}" # The loop count (0..9) is assigned to `i`.
[...]

Retry step

It can be used as a retry mechanism by setting a condition in the until: section.

If the condition of until: is met, the loop is broken without waiting for the number of count: to be run.

Also, if the run of the number of count: completes but does not satisfy the condition of until:, then the step is considered to be failed.

steps:
  waitingroom:
    loop:
      count: 10
      until: 'steps.waitingroom.res.status == "201"' # Store values of latest loop
      minInterval: 500ms
      maxInterval: 10 # sec
      # jitter: 0.0
      # interval: 5
      # multiplier: 1.5
    req:
      /cart/in:
        post:
          body:
[...]

( steps[*].retry: steps.<key>.retry: are deprecated )

Variables to be stored

runn can use variables and functions when running step.

Also, after step runs, HTTP responses, DB query results, etc. are automatically stored in variables.

The values are stored in predefined variables.

Variable name Description
vars Values set in the vars: section
steps Return values for each step
i Loop index (only in loop: section)
env Environment variables
current Return values of current step
previous Return values of previous step
parent Variables of parent runbook (only included)

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:
  req: https://example.com
steps:
  -
    desc: Post /users                     # description of step
    req:                                  # key to identify the runner. In this case, it is HTTP Runner.
      /users:                             # path of http request
        post:                             # method of http request
          headers:                        # headers of http request
            Authorization: 'Bearer xxxxx'
          body:                           # body of http request
            application/json:             # Content-Type specification. In this case, it is "Content-Type: application/json"
              username: alice
              password: passw0rd
    test: |                               # test for current step
      current.res.status == 201

See testdata/book/http.yml and testdata/book/http_multipart.yml.

Structure of recorded responses

The following response

HTTP/1.1 200 OK
Content-Length: 29
Content-Type: application/json
Date: Wed, 07 Sep 2022 06:28:20 GMT
Set-Cookie: cookie-name=cookie-value

{"data":{"username":"alice"}}

is recorded with the following structure.

[`step key` or `current` or `previous`]:
  res:
    status: 200                              # current.res.status
    headers:
      Content-Length:
        - '29'                               # current.res.headers["Content-Length"][0]
      Content-Type:
        - 'application/json'                 # current.res.headers["Content-Type"][0]
      Date:
        - 'Wed, 07 Sep 2022 06:28:20 GMT'    # current.res.headers["Date"][0]
      Set-Cookie:
        - 'cookie-name=cookie-value'         # current.res.headers["Set-Cookie"][0]
    cookies:
      cookie-name: *http.Cookie              # current.res.cookies["cookie-name"].Value
    body:
      data:
        username: 'alice'                    # current.res.body.data.username
    rawBody: '{"data":{"username":"alice"}}' # current.res.rawBody

Do not follow redirect

The HTTP Runner interprets HTTP responses and automatically redirects. To disable this, set notFollowRedirect to true.

runners:
  req:
    endpoint: https://example.com
    notFollowRedirect: true

Enable Cookie Sending

The HTTP Runner automatically saves cookies by interpreting HTTP responses. To enable cookie sending during requests, set useCookie to true.

runners:
  req:
    endpoint: https://example.com
    useCookie: true

See testdata/book/cookie.yml and testdata/book/cookie_in_requests_automatically.yml.

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

Custom CA and Certificates

runners:
  myapi:
    endpoint: https://api.github.com
    cacert: path/to/cacert.pem
    cert: path/to/cert.pem
    key: path/to/key.pem
    # skipVerify: false

gRPC Runner: Do gRPC request

Use grpc:// scheme to specify gRPC Runner.

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

runners:
  greq: grpc://grpc.example.com:80
steps:
  -
    desc: Request using Unary RPC                     # description of step
    greq:                                             # key to identify the runner. In this case, it is gRPC Runner.
      grpctest.GrpcTestService/Hello:                 # package.Service/Method of rpc
        headers:                                      # headers of rpc
          authentication: tokenhello
        message:                                      # message of rpc
          name: alice
          num: 3
          request_time: 2022-06-25T05:24:43.861872Z
  -
    desc: Request using Server streaming RPC
    greq:
      grpctest.GrpcTestService/ListHello:
        headers:
          authentication: tokenlisthello
        message:
          name: bob
          num: 4
          request_time: 2022-06-25T05:24:43.861872Z
        timeout: 3sec                                 # timeout for rpc
    test: |
      steps.server_streaming.res.status == 0 && len(steps.server_streaming.res.messages) > 0
  -
    desc: Request using Client streaming RPC
    greq:
      grpctest.GrpcTestService/MultiHello:
        headers:
          authentication: tokenmultihello
        messages:                                     # messages of rpc
          -
            name: alice
            num: 5
            request_time: 2022-06-25T05:24:43.861872Z
          -
            name: bob
            num: 6
            request_time: 2022-06-25T05:24:43.861872Z
runners:
  greq:
    addr: grpc.example.com:8080
    tls: true
    cacert: path/to/cacert.pem
    cert: path/to/cert.pem
    key: path/to/key.pem
    # skipVerify: false
    # protos:
    #   - general/health.proto
    #   - myapp/**/*.proto
    # importPaths:
    #   - protobuf/proto

See testdata/book/grpc.yml.

Structure of recorded responses

The following response

message HelloResponse {
  string message = 1;

  int32 num = 2;

  google.protobuf.Timestamp create_time = 3;
}
{"create_time":"2022-06-25T05:24:43.861872Z","message":"hello","num":32}

and headers

content-type: ["application/grpc"]
hello: ["this is header"]

and trailers

hello: ["this is trailer"]

are recorded with the following structure.

[`step key` or `current` or `previous`]:
  res:
    status: 0                                      # current.res.status
    headers:
      content-type:
        - 'application/grpc'                       # current.res.headers[0].content-type
      hello:
        - 'this is header'                         # current.res.headers[0].hello
    trailers:
      hello:
        - 'this is trailer'                        # current.res.trailers[0].hello
    message:
      create_time: '2022-06-25T05:24:43.861872Z'   # current.res.message.create_time
      message: 'hello'                             # current.res.message.message
      num: 32                                      # current.res.message.num
    messages:
      -
        create_time: '2022-06-25T05:24:43.861872Z' # current.res.messages[0].create_time
        message: 'hello'                           # current.res.messages[0].message
        num: 32                                    # current.res.messages[0].num

DB Runner: Query a database

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

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

runners:
  db: postgres://dbuser:dbpass@hostname:5432/dbname
steps:
  -
    desc: Select users            # description of step
    db:                           # key to identify the runner. In this case, it is DB Runner.
      query: SELECT * FROM users; # query to execute

See testdata/book/db.yml.

Structure of recorded responses

If the query is a SELECT clause, it records the selected rows,

[`step key` or `current` or `previous`]:
  rows:
    -
      id: 1                           # current.rows[0].id
      username: 'alice'               # current.rows[0].username
      password: 'passw0rd'            # current.rows[0].password
      email: '[email protected]'      # current.rows[0].email
      created: '2017-12-05T00:00:00Z' # current.rows[0].created
    -
      id: 2                           # current.rows[1].id
      username: 'bob'                 # current.rows[1].username
      password: 'passw0rd'            # current.rows[1].password
      email: '[email protected]'        # current.rows[1].email
      created: '2022-02-22T00:00:00Z' # current.rows[1].created

otherwise it records last_insert_id and rows_affected .

[`step key` or `current` or `previous`]:
  last_insert_id: 3 # current.last_insert_id
  rows_affected: 1  # current.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

Cloud Spanner:

runners:
  testdb: spanner://test-project/test-instance/test-database
runners:
  db: sp://test-project/test-instance/test-database

CDP Runner: Control browser using Chrome DevTools Protocol (CDP)

Use cdp:// or chrome:// scheme to specify CDP Runner.

When the step is invoked, it controls browser via Chrome DevTools Protocol.

runners:
  cc: chrome://new
steps:
  -
    desc: Navigate, click and get h1 using CDP  # description of step
    cc:                                         # key to identify the runner. In this case, it is CDP Runner.
      actions:                                  # actions to control browser
        - navigate: https://pkg.go.dev/time
        - click: 'body > header > div.go-Header-inner > nav > div > ul > li:nth-child(2) > a'
        - waitVisible: 'body > footer'
        - text: 'h1'
  -
    test: |
      previous.text == 'Install the latest version of Go'

See testdata/book/cdp.yml.

Functions for action to control browser

attributes (aliases: getAttributes, attrs, getAttrs)

Get the element attributes for the first element node matching the selector (sel).

actions:
  - attributes:
      sel: 'h1'
# record to current.attrs:

or

actions:
  - attributes: 'h1'

click

Send a mouse click event to the first element node matching the selector (sel).

actions:
  - click:
      sel: 'nav > div > a'

or

actions:
  - click: 'nav > div > a'

doubleClick

Send a mouse double click event to the first element node matching the selector (sel).

actions:
  - doubleClick:
      sel: 'nav > div > li'

or

actions:
  - doubleClick: 'nav > div > li'

evaluate (aliases: eval)

Evaluate the Javascript expression (expr).

actions:
  - evaluate:
      expr: 'document.querySelector("h1").textContent = "hello"'

or

actions:
  - evaluate: 'document.querySelector("h1").textContent = "hello"'

fullHTML (aliases: getFullHTML, getHTML, html)

Get the full html of page.

actions:
  - fullHTML
# record to current.html:

innerHTML (aliases: getInnerHTML)

Get the inner html of the first element node matching the selector (sel).

actions:
  - innerHTML:
      sel: 'h1'
# record to current.html:

or

actions:
  - innerHTML: 'h1'

latestTab (aliases: latestTarget)

Change current frame to latest tab.

actions:
  - latestTab

localStorage (aliases: getLocalStorage)

Get localStorage items.

actions:
  - localStorage:
      origin: 'https://github.com'
# record to current.items:

or

actions:
  - localStorage: 'https://github.com'

location (aliases: getLocation)

Get the document location.

actions:
  - location
# record to current.url:

navigate

Navigate the current frame to url page.

actions:
  - navigate:
      url: 'https://pkg.go.dev/time'

or

actions:
  - navigate: 'https://pkg.go.dev/time'

outerHTML (aliases: getOuterHTML)

Get the outer html of the first element node matching the selector (sel).

actions:
  - outerHTML:
      sel: 'h1'
# record to current.html:

or

actions:
  - outerHTML: 'h1'

screenshot (aliases: getScreenshot)

Take a full screenshot of the entire browser viewport.

actions:
  - screenshot
# record to current.png:

scroll (aliases: scrollIntoView)

Scroll the window to the first element node matching the selector (sel).

actions:
  - scroll:
      sel: 'body > footer'

or

actions:
  - scroll: 'body > footer'

sendKeys

Send keys (value) to the first element node matching the selector (sel).

actions:
  - sendKeys:
      sel: 'input[name=username]'
      value: '[email protected]'

sessionStorage (aliases: getSessionStorage)

Get sessionStorage items.

actions:
  - sessionStorage:
      origin: 'https://github.com'
# record to current.items:

or

actions:
  - sessionStorage: 'https://github.com'

setUploadFile (aliases: setUpload)

Set upload file (path) to the first element node matching the selector (sel).

actions:
  - setUploadFile:
      sel: 'input[name=avator]'
      path: '/path/to/image.png'

setUserAgent (aliases: setUA, ua, userAgent)

Set the default User-Agent

actions:
  - setUserAgent:
      userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36'

or

actions:
  - setUserAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36'

submit

Submit the parent form of the first element node matching the selector (sel).

actions:
  - submit:
      sel: 'form.login'

or

actions:
  - submit: 'form.login'

text (aliases: getText)

Get the visible text of the first element node matching the selector (sel).

actions:
  - text:
      sel: 'h1'
# record to current.text:

or

actions:
  - text: 'h1'

textContent (aliases: getTextContent)

Get the text content of the first element node matching the selector (sel).

actions:
  - textContent:
      sel: 'h1'
# record to current.text:

or

actions:
  - textContent: 'h1'

title (aliases: getTitle)

Get the document title.

actions:
  - title
# record to current.title:

value (aliases: getValue)

Get the Javascript value field of the first element node matching the selector (sel).

actions:
  - value:
      sel: 'input[name=address]'
# record to current.value:

or

actions:
  - value: 'input[name=address]'

wait (aliases: sleep)

Wait for the specified time.

actions:
  - wait:
      time: '10sec'

or

actions:
  - wait: '10sec'

waitReady

Wait until the element matching the selector (sel) is ready.

actions:
  - waitReady:
      sel: 'body > footer'

or

actions:
  - waitReady: 'body > footer'

waitVisible

Wait until the element matching the selector (sel) is visible.

actions:
  - waitVisible:
      sel: 'body > footer'

or

actions:
  - waitVisible: 'body > footer'

SSH Runner: execute commands on a remote server connected via SSH

Use ssh:// scheme to specify SSH Runner.

When step is invoked, it executes commands on a remote server connected via SSH.

runners:
  sc: ssh://username@hostname:port
steps:
  -
    desc: 'execute `hostname`' # description of step
    sc:
      command: hostname
runners:
  sc:
    hostname: hostname
    user: username
    port: 22
    # host: myserver
    # sshConfig: path/to/ssh_config
    # keepSession: false
    # localForward: '33306:127.0.0.1:3306'
    # keyboardInteractive:
    #   - match: Username
    #     answer: k1low
    #   - match: OTP
    #     answer: ${MY_OTP}

See testdata/book/sshd.yml.

Structure of recorded responses

The response to the run command is always stdout and stderr.

[`step key` or `current` or `previous`]:
  stdout: 'hello world' # current.stdout
  stderr: ''            # current.stderr

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 hello
    stdin: '{{ steps[3].res.rawBody }}'

See testdata/book/exec.yml.

Structure of recorded responses

The response to the run command is always stdout, stderr and exit_code.

[`step key` or `current` or `previous`]:
  stdout: 'hello world' # current.stdout
  stderr: ''            # current.stderr
  exit_code: 0          # current.exit_code

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

or

-
  dump:
    expr: steps[4].rows
    out: path/to/dump.out

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

It is also possible to force all steps in the included runbook to run.

-
  include:
    path: path/to/signup.yml
    force: 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.

Expression evaluation engine

runn has embedded antonmedv/expr as the evaluation engine for the expression.

See Language Definition.

Additional built-in functions

  • urlencode ... url.QueryEscape
  • bool ... cast.ToBool
  • compare ... Compare two values ( func(x, y any, ignoreKeys ...string) bool ).
  • diff ... Difference between two values ( func(x, y any, ignoreKeys ...string) string ).
  • input ... prompter.Prompt
  • intersect ... Find the intersection of two iterable values ( func(x, y any) any ).
  • secret ... prompter.Password
  • select ... prompter.Choose
  • basename ... filepath.Base
  • faker.* ... Generate fake data using Faker ).

Option

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

Example: Run as a test helper ( func T )

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 ( func Func )

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)
}

Filter runbooks to be executed by the environment variable RUNN_RUN

Run only runbooks matching the filename "login".

$ env RUNN_RUN=login go test ./... -run TestRouter

Measure elapsed time as profile

opts := []runn.Option{
	runn.T(t),
	runn.Book("testdata/books/login.yml"),
	runn.Profile(true)
}
o, err := runn.New(opts...)
if err != nil {
	t.Fatal(err)
}
if err := o.Run(ctx); err != nil {
	t.Fatal(err)
}
f, err := os.Open("profile.json")
if err != nil {
	t.Fatal(err)
}
if err := o.DumpProfile(f); err != nil {
	t.Fatal(err)
}

or

$ runn run testdata/books/login.yml --profile

The runbook run profile can be read with runn rprof command.

$ runn rprof runn.prof
  runbook[login site](t/b/login.yml)           2995.72ms
    steps[0].req                                747.67ms
    steps[1].req                                185.69ms
    steps[2].req                                192.65ms
    steps[3].req                                188.23ms
    steps[4].req                                569.53ms
    steps[5].req                                299.88ms
    steps[6].test                                 0.14ms
    steps[7].include                            620.88ms
      runbook[include](t/b/login_include.yml)   605.56ms
        steps[0].req                            605.54ms
    steps[8].req                                190.92ms
  [total]                                      2995.84ms

Capture runbook runs

opts := []runn.Option{
	runn.T(t),
	runn.Capture(capture.Runbook("path/to/dir")),
}
o, err := runn.Load("testdata/books/**/*.yml", opts...)
if err != nil {
	t.Fatal(err)
}
if err := o.RunN(ctx); err != nil {
	t.Fatal(err)
}

or

$ runn run path/to/**/*.yml --capture path/to/dir

Load test using runbooks

You can use the runn loadt command for load testing using runbooks.

$ runn loadt --load-concurrent 2 --max-rps 0 path/to/*.yml

Number of runbooks per RunN....: 15
Warm up time (--warm-up).......: 5s
Duration (--duration)..........: 10s
Concurrent (--load-concurrent).: 2
Max RunN per second (--max-rps): 0

Total..........................: 12
Succeeded......................: 12
Failed.........................: 0
Error rate.....................: 0%
RunN per seconds...............: 1.2
Latency .......................: max=1,835.1ms min=1,451.3ms avg=1,627.8ms med=1,619.8ms p(90)=1,741.5ms p(99)=1,788.4ms

It also checks the results of the load test with the --threshold option. If the condition is not met, it returns exit status 1.

$ runn loadt --load-concurrent 2 --max-rps 0 --threshold 'error_rate < 10' path/to/*.yml

Number of runbooks per RunN...: 15
Warm up time (--warm-up)......: 5s
Duration (--duration).........: 10s
Concurrent (--load-concurrent): 2

Total.........................: 13
Succeeded.....................: 12
Failed........................: 1
Error rate....................: 7.6%
RunN per seconds..............: 1.3
Latency ......................: max=1,790.2ms min=95.0ms avg=1,541.4ms med=1,640.4ms p(90)=1,749.7ms p(99)=1,786.5ms

Error: (error_rate < 10) is not true
error_rate < 10
├── error_rate => 14.285714285714285
└── 10 => 10

Variables for threshold

Variable name Type Description
total int Total
succeeded int Succeeded
failed int Failed
error_rate float Error rate
rps float RunN per seconds
max float Latency max (ms)
mid float Latency mid (ms)
min float Latency min (ms)
p90 float Latency p(90) (ms)
p99 float Latency p(99) (ms)
avg float Latency avg (ms)

Install

As a CLI tool

deb:

$ 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:

$ 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 container run -it --rm --name runn -v $PWD:/books ghcr.io/k1low/runn:latest list /books/*.yml

go install:

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

As a test helper

$ go get github.com/k1LoW/runn

Alternatives

References

More Repositories

1

tbls

tbls is a CI-Friendly tool for document a database, written in Go.
Go
2,344
star
2

awspec

RSpec tests for your AWS resources.
Ruby
1,173
star
3

octocov

octocov is a toolkit for collecting code metrics (code coverage, code to test ratio and test execution time).
Go
198
star
4

ndiag

ndiag is a high-level architecture diagramming/documentation tool.
Go
176
star
5

serverless-s3-sync

A plugin to sync local directories and S3 prefixes for Serverless Framework ⚡
JavaScript
170
star
6

gh-grep

:octocat: Print lines matching a pattern in repositories using GitHub API
Go
155
star
7

tcpdp

tcpdp is TCP dump tool with custom dumper and structured logger written in Go.
Go
125
star
8

filt

filt is a interactive/realtime stream filter ( also known as "trial-and-error pipe" ).
Go
74
star
9

evry

Split STDIN stream and execute specified command every N lines/seconds.
Go
64
star
10

colr

🎨 colr colors strings, colorfully.
Go
59
star
11

ghput

:octocat: ghput is a CI-friendly tool that puts * on GitHub.
Go
34
star
12

harvest

🪲 Portable log aggregation tool for middle-scale system operation/troubleshooting.
Go
31
star
13

utsusemi

A tool to generate a static website by crawling the original site.
JavaScript
30
star
14

emacs-cake

Minor Mode for editing CakePHP code in Emacs
Emacs Lisp
27
star
15

sakuravps

Shell
27
star
16

emacs-drill-instructor

Enforce key-bind of Emacs. a.k.a 鬼軍曹.el
Emacs Lisp
27
star
17

frgm

frgm is a meta snippet (fragment) manager.
Go
25
star
18

holiday_jp

[DEPRECATED PROJECT] holiday_jp
Ruby
23
star
19

github-script-ruby

Write workflows scripting the GitHub API in Ruby
Ruby
23
star
20

ghdag

:octocat: ghdag is a tiny workflow engine for GitHub issue and pull request.
Go
23
star
21

fatty

Simple Git repogitory browser plugin for CakePHP
JavaScript
22
star
22

serverless-static-hosting-with-basic-auth

Serverless boilerplate for Static website hosting with Basic authentication
JavaScript
21
star
23

trivy-db-to

trivy-db-to is a tool for migrating/converting vulnerability information from Trivy DB to other datasource.
Go
19
star
24

yalog

Yet Another Logger for CakePHP
PHP
19
star
25

awsdo

awsdo is a tool to do anything using AWS temporary credentials.
Go
19
star
26

execop

ExeCop is a checker that check commands and environment variables before execute command.
Shell
18
star
27

emacs-titanium

Minor Mode for editing Titanium code in Emacs
Emacs Lisp
17
star
28

koma

Koma is an inventory monitoring tool that doesn’t require agent installation on the sever side.
Ruby
17
star
29

awsecrets

AWS credentials loader
Ruby
17
star
30

sheer-heart-attack

💣 A debugging tool that can execute any command on process/host metrics trigger 💥 .
Go
17
star
31

octocov-action

:octocat: GitHub Action for octocov
Shell
17
star
32

certman

CLI tool for AWS Certificate Manager.
Ruby
17
star
33

aws-graph

Draw AWS network graph with Graphviz.
Ruby
16
star
34

sconb

Ssh CONfig Buckup tool.
Ruby
16
star
35

docker-alpine-pandoc-ja

Pandoc for Japanese based on Alpine Linux
Dockerfile
15
star
36

model_info

CakePHP DB Schema/Model Info Plugin
PHP
13
star
37

fake

Fixture generator plugin for cAKEphp.
PHP
13
star
38

viewpath

Viewpath: View file path display plugin for CakePHP
PHP
12
star
39

CacooViewer

Simple `Cacoo' diagrams viewer.
JavaScript
12
star
40

Yacsv

Yet another CSV utility plugin for CakePHP
PHP
12
star
41

grouped_process_exporter

Exporter for grouped process
Go
11
star
42

emacs-serverspec

Serverspec minor mode
Emacs Lisp
11
star
43

dirmap

📁 dirmap is a tool for generating a directory map.
Go
11
star
44

metr

metr provides an easy way to use host/process metrics for shell script/monitoring tool.
Go
11
star
45

gh-setup

:octocat: Setup asset of Github releases.
Go
10
star
46

connected

🔌 Watch your MacBook connection ⚡
Go
10
star
47

glyph

Icon as Code
Go
10
star
48

controller_prefix

`Controller name prefix' custom route plugin for CakePHP
PHP
10
star
49

emacs-cake2

Minor Mode for editing CakePHP2 code in Emacs
Emacs Lisp
10
star
50

sshc

sshc.NewClient() returns *ssh.Client using ssh_config(5)
Go
10
star
51

tbls-ask

tbls-ask is an external subcommand of tbls for asking OpenAI using the datasource.
Go
10
star
52

tokyotyrant_php

Yet Another "Tokyo Tyrant" PHP Interface
PHP
9
star
53

pr-bullet

pr-bullet is a tool for copying pull request to multiple repositories.
Go
9
star
54

recipe

recipe - CakePHP CLI Package Installer -
PHP
9
star
55

emacs-historyf

file history library like browser
Emacs Lisp
9
star
56

escape

Auto escaping plugin for CakePHP
PHP
8
star
57

yak

Yet Another Ktai plugin for CakePHP
PHP
8
star
58

ebk

ebk is a tiny tool for ebook
Go
8
star
59

emacs-ac-cake2

Emacs Lisp
8
star
60

pear_local

PEAR Local install plugin for CakePHP
PHP
8
star
61

stopw

A stopwatch library in Go for nested time measurement.
Go
8
star
62

emacs-ac-cake

Emacs Lisp
8
star
63

duration

duration.Parse() parses a formatted string and returns the time.Duration value it represents.
Go
8
star
64

auto-complete-exuberant-ctags

Exuberant ctags auto-complete.el source
Emacs Lisp
8
star
65

has_no

Simple binding model practice plugin for CakePHP.
PHP
7
star
66

mackerel-plugin-prometheus-exporter

🐟 Mackerel plugin for scraping Prometheus exporter metrics. 🔥
Go
7
star
67

awsrm

Simple AWS Resource "READONLY" Mapper for awspec.
Ruby
7
star
68

jquery-cakephp-debugkit

Add JavaScript valiables panel to 'CakePHP DebugKit'
JavaScript
7
star
69

gh-star-history

:octocat: Show star history of repositories. ⭐
Go
7
star
70

tbls-meta

tbls-meta is an external subcommand of tbls for applying metadata managed by tbls to the datasource.
Go
6
star
71

tmpfk

Temporary foreign key add/drop tool for using ERD generator for "Keyless entry (SQL Antipatterns)" schema.
Ruby
6
star
72

DebugMemo

DebugMemo: Memo tool for development / CakePHP plugin
PHP
6
star
73

anything-exuberant-ctags

Exuberant ctags anything.el interface
Emacs Lisp
6
star
74

brewfile-desc

brewfile-desc add descriptions of formulae to Brewfile.
Go
6
star
75

po

CakePHP .po File Edit Plugin
PHP
6
star
76

ghfs

:octocat: Go io/fs implementation for GitHub remote repository
Go
5
star
77

Exception

Exception plugin for CakePHP
PHP
5
star
78

backlog-favicon-changer

Chrome Extension to change favicon for multiple Backlog projects.
JavaScript
5
star
79

emacs-yarm

Yet Another Ruby on Rails Minor Mode for Emacs
Emacs Lisp
5
star
80

calendar

Calendar plugin for CakePHP
PHP
5
star
81

anything-replace-string

replace-string() and query-string() `anything.el' interface
Emacs Lisp
5
star
82

calver

calver is a package/tool provides the ability to work with Calendar Versioning in Go.
Go
5
star
83

ssh_config_to_vuls_config

sc2vc: ssh_config to vuls config TOML format
Ruby
5
star
84

phpenv-nginx-ansible-vagrant

Ruby
5
star
85

typd

お前は今まで入力したフォームの値を覚えているのか?
JavaScript
5
star
86

Setting

Database driven setting plugin for CakePHP.
PHP
5
star
87

backslack

Bridge between Backlog and Slack
JavaScript
5
star
88

anything-hatena-bookmark

Hatena::Bookmark anything.el interface
Emacs Lisp
5
star
89

Yasd

Yet Another SoftDeletable Behavior for CakePHP
PHP
5
star
90

tbls-build

tbls-build is an external subcommand of tbls for customizing config file of tbls using other tbls.yml or schema.json.
Go
5
star
91

curlreq

curlreq creates *http.Request from curl command.
Go
4
star
92

Sample-Calendar-Application

Sample Application for the CakePHP Calendar Plugin
PHP
4
star
93

repin

repin is a tool to replace strings between keyword pair.
Go
4
star
94

lrep

lrep = l/re/p = line regular expression parser
Go
4
star
95

webroot_view

CakePHP library for use elements and helpers under app/webroot/
PHP
4
star
96

Back

Sessoin base `history back' plugin for CakePHP
PHP
4
star
97

keyp

keyp is a tool to keep public keys up to date.
Go
4
star
98

property-enum

Property based enum plugin for CakePHP 3
PHP
4
star
99

slkm

slkm is github.com/slack-go/slack wrapper package for posting message.
Go
4
star
100

emacs-key-cast

Key Storke Casting Minor Mode for Emacs
4
star