runn

package module
v0.0.0-...-38f41cd Latest Latest
Warning

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

Go to latest
Published: Jan 23, 2024 License: MIT Imports: 112 Imported by: 0

README

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

List:

color

Map:

color

desc:

Description of runbook.

desc: Login and get projects.
runners:
  req: https://example.com/api/v1
vars:
  username: alice
steps:
[...]

labels:

Labels of runbook.

desc: Login
runners:
  req: https://example.com/api/v1
labels:
  - users
  - auth
steps:
[...]

Runbooks to be run can be filtered by labels.

$ runn run path/to/**/*.yml --label users --label projects
$ runn run path/to/**/*.yml --label 'users and auth'

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

hostRules:

Allows remapping any request hostname to another hostname, IP address in HTTP/gRPC/DB/CDP/SSH runners.

hostRules:
  example.com: 127.0.0.1:8080
  '*.example.test': 192.168.0.16

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

interval:

Interval between steps.

interval: 1

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

trace:

Add tokens for tracing to headers and queries by default.

Currently, HTTP runner, gRPC runner and DB runner are supported.

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

or

concurrency:
  - use-shared-db
  - use-shared-api

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:
  -
    desc: Login
    req:
      /login:
        post:
          body:
[...]

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:
[...]

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
          trace: false                    # add `X-Runn-Trace` header to HTTP request for tracing
    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

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.example.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
Add X-Runn-Trace header to HTTP request for tracing
runners:
  myapi:
    endpoint: https://api.github.com
    trace: true

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
        trace: false                                  # add `x-runn-trace` header to gRPC request for tracing
  -
    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
Add x-runn-trace header to gRPC request for tracing
runners:
  greq:
    addr: grpc.example.com:8080
    trace: true

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
      trace: false                # add comment with trace token to query for tracing

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: 'alice@example.com'      # 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: 'bob@example.com'        # 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
Add comment with trace token to query for tracing
runners:
  db:
    dsn: mysql://dbuser:dbpass@hostname:3306/dbname
    trace: true
``

#### Support Databases

**PostgreSQL:**

``` yaml
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: 'k1lowxb@gmail.com'

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

Note Exec runner requires run:exec scope to run.

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: and shell:.

-
  exec:
    command: grep hello
    stdin: '{{ steps[3].res.rawBody }}'
-
  exec:
    command: echo $0
    shell: bash

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 expr-lang/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 ).
  • pick ... Returns same map type filtered by given keys left lo.PickByKeys.
  • omit ... Returns same map type filtered by given keys excluded lo.OmitByKeys.
  • merge ... Merges multiple maps from left to right lo.Assign.
  • 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)
}

Scope

runn requires explicit specification of scope for some features.

runn has the following scopes.

Scope Description Default
read:parent Required for reading files above the working directory. false
read:remote Required for reading remote files. false
run:exec Required for running Exec runner. false

To specify scopes, using the --scopes option or the environment variable RUNN_SCOPES.

$ runn run path/to/**/*.yml --scopes read:parent,read:remote
$ env RUNN_SCOPES=read:parent,read:remote runn run path/to/**/*.yml

Also, runn.Scopes can be used in the code

o, err := runn.Load("path/to/**/*.yml", runn.Scopes(runn.ScopeAllowReadParent, runn.ScopeAllowReadRemote))
if err != nil {
	t.Fatal(err)
}
if err := o.RunN(ctx); err != nil {
	t.Fatal(err)
}

To disable scope, can use !read:* instead of read:*

$ runn run path/to/**/*.yml --scopes '!read:parent'

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

aqua:

$ aqua g -i k1LoW/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

Documentation

Index

Constants

View Source
const (
	MediaTypeApplicationJSON           = "application/json"
	MediaTypeTextPlain                 = "text/plain"
	MediaTypeApplicationFormUrlencoded = "application/x-www-form-urlencoded"
	MediaTypeMultipartFormData         = "multipart/form-data"
	MediaTypeApplicationOctetStream    = "application/octet-stream"
)
View Source
const (
	ScopeAllowReadParent = "read:parent"
	ScopeAllowReadRemote = "read:remote"
	ScopeAllowRunExec    = "run:exec" //nostyle:repetition
	ScopeDenyReadParent  = "!read:parent"
	ScopeDenyReadRemote  = "!read:remote"
	ScopeDenyRunExec     = "!run:exec" //nostyle:repetition
)

Variables

View Source
var (
	AsTestHelper = T
	Runbook      = Book
	RunPart      = RunShard //nostyle:repetition
)
View Source
var CDPFnMap = map[string]CDPFn{
	"navigate": {
		Desc: "Navigate the current frame to `url` page.",
		Fn:   chromedp.Navigate,
		Args: CDPFnArgs{
			{CDPArgTypeArg, "url", "https://pkg.go.dev/time"},
		},
	},
	"latestTab": {
		Desc: "Change current frame to latest tab.",
		Fn: func() chromedp.Action {

			return nil
		},
		Args:    CDPFnArgs{},
		Aliases: []string{"latestTarget"},
	},
	"click": {
		Desc: "Send a mouse click event to the first element node matching the selector (`sel`).",
		Fn:   chromedp.Click,
		Args: CDPFnArgs{
			{CDPArgTypeArg, "sel", "nav > div > a"},
		},
	},
	"doubleClick": {
		Desc: "Send a mouse double click event to the first element node matching the selector (`sel`).",
		Fn:   chromedp.DoubleClick,
		Args: CDPFnArgs{
			{CDPArgTypeArg, "sel", "nav > div > li"},
		},
	},
	"sendKeys": {
		Desc: "Send keys (`value`) to the first element node matching the selector (`sel`).",
		Fn:   chromedp.SendKeys,
		Args: CDPFnArgs{
			{CDPArgTypeArg, "sel", "input[name=username]"},
			{CDPArgTypeArg, "value", "k1lowxb@gmail.com"},
		},
	},
	"submit": {
		Desc: "Submit the parent form of the first element node matching the selector (`sel`).",
		Fn:   chromedp.Submit,
		Args: CDPFnArgs{
			{CDPArgTypeArg, "sel", "form.login"},
		},
	},
	"scroll": {
		Desc: "Scroll the window to the first element node matching the selector (`sel`).",
		Fn:   chromedp.ScrollIntoView,
		Args: CDPFnArgs{
			{CDPArgTypeArg, "sel", "body > footer"},
		},
		Aliases: []string{"scrollIntoView"},
	},
	"wait": {
		Desc: "Wait for the specified `time`.",
		Fn: func(d string) chromedp.Action {
			return &waitAction{d: d}
		},
		Args: CDPFnArgs{
			{CDPArgTypeArg, "time", "10sec"},
		},
		Aliases: []string{"sleep"},
	},
	"waitReady": {
		Desc: "Wait until the element matching the selector (`sel`) is ready.",
		Fn:   chromedp.WaitReady,
		Args: CDPFnArgs{
			{CDPArgTypeArg, "sel", "body > footer"},
		},
	},
	"waitVisible": {
		Desc: "Wait until the element matching the selector (`sel`) is visible.",
		Fn:   chromedp.WaitVisible,
		Args: CDPFnArgs{
			{CDPArgTypeArg, "sel", "body > footer"},
		},
	},
	"setUserAgent": {
		Desc: "Set the default User-Agent",
		Fn: func(ua string) []chromedp.Action {
			headers := map[string]any{"User-Agent": ua}
			return []chromedp.Action{
				network.Enable(),
				network.SetExtraHTTPHeaders(network.Headers(headers)),
			}
		},
		Args: CDPFnArgs{
			{CDPArgTypeArg, "userAgent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36"},
		},
		Aliases: []string{"setUA", "ua", "userAgent"},
	},
	"text": {
		Desc: "Get the visible text of the first element node matching the selector (`sel`).",
		Fn:   chromedp.Text,
		Args: CDPFnArgs{
			{CDPArgTypeArg, "sel", "h1"},
			{CDPArgTypeRes, "text", "Install the latest version of Go"},
		},
		Aliases: []string{"getText"},
	},
	"textContent": {
		Desc: "Get the text content of the first element node matching the selector (`sel`).",
		Fn:   chromedp.TextContent,
		Args: CDPFnArgs{
			{CDPArgTypeArg, "sel", "h1"},
			{CDPArgTypeRes, "text", "Install the latest version of Go"},
		},
		Aliases: []string{"getTextContent"},
	},
	"innerHTML": {
		Desc: "Get the inner html of the first element node matching the selector (`sel`).",
		Fn:   chromedp.InnerHTML,
		Args: CDPFnArgs{
			{CDPArgTypeArg, "sel", "h1"},
			{CDPArgTypeRes, "html", "Install the latest version of Go"},
		},
		Aliases: []string{"getInnerHTML"},
	},
	"outerHTML": {
		Desc: "Get the outer html of the first element node matching the selector (`sel`).",
		Fn:   chromedp.OuterHTML,
		Args: CDPFnArgs{
			{CDPArgTypeArg, "sel", "h1"},
			{CDPArgTypeRes, "html", "<h1>Install the latest version of Go</h1>"},
		},
		Aliases: []string{"getOuterHTML"},
	},
	"fullHTML": {
		Desc: "Get the full html of page.",
		Fn: func(html *string) chromedp.Action {
			expr := "new XMLSerializer().serializeToString(document);"
			return chromedp.Evaluate(expr, html)
		},
		Args: CDPFnArgs{
			{CDPArgTypeRes, "html", "<!DOCTYPE html><html><body><h1>hello</h1></body></html>"},
		},
		Aliases: []string{"getFullHTML", "getHTML", "html"},
	},
	"value": {
		Desc: "Get the Javascript value field of the first element node matching the selector (`sel`).",
		Fn:   chromedp.Value,
		Args: CDPFnArgs{
			{CDPArgTypeArg, "sel", "input[name=address]"},
			{CDPArgTypeRes, "value", "Fukuoka"},
		},
		Aliases: []string{"getValue"},
	},
	"setUploadFile": {
		Desc: "Set upload file (`path`) to the first element node matching the selector (`sel`).",
		Fn: func(sel, path string) chromedp.Action {
			abs, err := filepath.Abs(path)
			if err != nil {
				return &errAction{err: err}
			}
			if _, err := os.Stat(abs); err != nil {
				return &errAction{err: err}
			}
			return chromedp.SetUploadFiles(sel, []string{abs})
		},
		Args: CDPFnArgs{
			{CDPArgTypeArg, "sel", "input[name=avator]"},
			{CDPArgTypeArg, "path", "/path/to/image.png"},
		},
		Aliases: []string{"setUpload"},
	},
	"title": {
		Desc: "Get the document `title`.",
		Fn:   chromedp.Title,
		Args: CDPFnArgs{
			{CDPArgTypeRes, "title", "GitHub"},
		},
		Aliases: []string{"getTitle"},
	},
	"location": {
		Desc: "Get the document location.",
		Fn:   chromedp.Location,
		Args: CDPFnArgs{
			{CDPArgTypeRes, "url", "https://github.com"},
		},
		Aliases: []string{"getLocation"},
	},
	"attributes": {
		Desc: "Get the element attributes for the first element node matching the selector (`sel`).",
		Fn:   chromedp.Attributes,
		Args: CDPFnArgs{
			{CDPArgTypeArg, "sel", "h1"},
			{CDPArgTypeRes, "attrs", `{"class": "sr-only"}`},
		},
		Aliases: []string{"getAttributes", "attrs", "getAttrs"},
	},
	"screenshot": {
		Desc: "Take a full screenshot of the entire browser viewport.",
		Fn: func(b *[]byte) chromedp.Action {
			return chromedp.FullScreenshot(b, 100)
		},
		Args: CDPFnArgs{
			{CDPArgTypeRes, "png", "[]byte"},
		},
		Aliases: []string{"getScreenshot"},
	},
	"evaluate": {
		Desc: "Evaluate the Javascript expression (`expr`).",
		Fn: func(expr string) chromedp.Action {

			return chromedp.Evaluate(expr, nil)
		},
		Args: CDPFnArgs{
			{CDPArgTypeArg, "expr", `document.querySelector("h1").textContent = "hello"`},
		},
		Aliases: []string{"eval"},
	},
	"localStorage": {
		Desc: "Get localStorage items.",
		Fn: func(origin string, items *map[string]string) chromedp.Action {
			return chromedp.ActionFunc(func(ctx context.Context) error {
				frameTree, err := page.GetFrameTree().Do(ctx)
				if err != nil {
					return err
				}
				strageKey := domstorage.SerializedStorageKey(frameTree.Frame.SecurityOrigin + "/")
				storageID := &domstorage.StorageID{
					StorageKey:     strageKey,
					IsLocalStorage: true,
				}
				resp, err := domstorage.GetDOMStorageItems(storageID).Do(ctx)
				if err != nil {
					return err
				}
				m := make(map[string]string)
				for _, v := range resp {
					if len(v) != 2 {
						continue
					}
					m[v[0]] = v[1]
				}
				*items = m
				return nil
			})
		},
		Args: CDPFnArgs{
			{CDPArgTypeArg, "origin", "https://github.com"},
			{CDPArgTypeRes, "items", `{"key": "value"}`},
		},
		Aliases: []string{"getLocalStorage"},
	},
	"sessionStorage": {
		Desc: "Get sessionStorage items.",
		Fn: func(origin string, items *map[string]string) chromedp.Action {
			return chromedp.ActionFunc(func(ctx context.Context) error {
				frameTree, err := page.GetFrameTree().Do(ctx)
				if err != nil {
					return err
				}
				strageKey := domstorage.SerializedStorageKey(frameTree.Frame.SecurityOrigin + "/")
				storageID := &domstorage.StorageID{
					StorageKey:     strageKey,
					IsLocalStorage: false,
				}
				resp, err := domstorage.GetDOMStorageItems(storageID).Do(ctx)
				if err != nil {
					return err
				}
				m := make(map[string]string)
				for _, v := range resp {
					if len(v) != 2 {
						continue
					}
					m[v[0]] = v[1]
				}
				*items = m
				return nil
			})
		},
		Args: CDPFnArgs{
			{CDPArgTypeArg, "origin", "https://github.com"},
			{CDPArgTypeRes, "items", `{"key": "value"}`},
		},
		Aliases: []string{"getSessionStorage"},
	},
}
View Source
var ErrInvalidScope = errors.New("invalid scope")
View Source
var ErrNilBook = errors.New("runbook is nil")

Functions

func CACert

func CACert(path string) grpcRunnerOption

func CACertFromData

func CACertFromData(b []byte) grpcRunnerOption

func Cert

func Cert(path string) grpcRunnerOption

func CertFromData

func CertFromData(b []byte) grpcRunnerOption

func CreateHTTPStepMapSlice

func CreateHTTPStepMapSlice(key string, req *http.Request) (yaml.MapSlice, error)

CreateHTTPStepMapSlice creates yaml.MapSlice from *http.Request.

func DBTrace

func DBTrace(trace bool) dbRunnerOption

func Eval

func Eval(e string, store any) (any, error)

func EvalAny

func EvalAny(e any, store any) (any, error)

EvalAny evaluate any type. but, EvalAny do not evaluate map key.

func EvalCond

func EvalCond(cond string, store any) (bool, error)

func EvalCount

func EvalCount(count string, store any) (int, error)

func EvalExpand

func EvalExpand(in, store any) (any, error)

EvalExpand evaluates `in` and expand `{{ }}` in `in` using `store`.

func GRPCTrace

func GRPCTrace(trace bool) grpcRunnerOption

func HTTPCACert

func HTTPCACert(path string) httpRunnerOption

func HTTPCert

func HTTPCert(path string) httpRunnerOption

func HTTPKey

func HTTPKey(path string) httpRunnerOption

func HTTPSkipVerify

func HTTPSkipVerify(skip bool) httpRunnerOption

func HTTPTimeout

func HTTPTimeout(timeout string) httpRunnerOption

func HTTPTrace

func HTTPTrace(trace bool) httpRunnerOption

func Host

func Host(h string) sshRunnerOption

func Hostname

func Hostname(h string) sshRunnerOption

func IdentityFile

func IdentityFile(p string) sshRunnerOption

func IdentityKey

func IdentityKey(k []byte) sshRunnerOption

func ImportPaths

func ImportPaths(paths []string) grpcRunnerOption

ImportPaths set import paths.

func KeepSession

func KeepSession(enable bool) sshRunnerOption

func Key

func Key(path string) grpcRunnerOption

func KeyFromData

func KeyFromData(b []byte) grpcRunnerOption

func Load

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

func LoadBook

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

func LocalForward

func LocalForward(l string) sshRunnerOption

func MultipartBoundary

func MultipartBoundary(b string) httpRunnerOption

func New

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

New returns *operator.

func NewCmdOut

func NewCmdOut(out io.Writer, verbose bool) *cmdOut

func NewDebugger

func NewDebugger(out io.Writer) *debugger

func NewLoadtResult

func NewLoadtResult(rc int, w, d time.Duration, c, m int, r *or.Result) (*loadtResult, error)

func NewRunbook

func NewRunbook(desc string) *runbook

func NotFollowRedirect

func NotFollowRedirect(nf bool) httpRunnerOption

func OpenApi3

func OpenApi3(l string) httpRunnerOption

OpenApi3 sets OpenAPI Document using file path.

func OpenApi3FromData

func OpenApi3FromData(d []byte) httpRunnerOption

OpenApi3FromData sets OpenAPI Document from data.

func ParseRunbook

func ParseRunbook(in io.Reader) (*runbook, error)

func Port

func Port(p int) sshRunnerOption

func Protos

func Protos(protos []string) grpcRunnerOption

Protos append protos.

func RemoveCacheDir

func RemoveCacheDir() error

RemoveCacheDir remove cache directory for remote runbooks.

func SSHConfig

func SSHConfig(p string) sshRunnerOption

func SetCacheDir

func SetCacheDir(dir string) error

SetCacheDir set cache directory for remote runbooks.

func ShortenPath

func ShortenPath(p string) string

ShortenPath shorten path.

func SkipValidateRequest

func SkipValidateRequest(skip bool) httpRunnerOption

SkipValidateRequest sets whether to skip validation of HTTP request with OpenAPI Document.

func SkipValidateResponse

func SkipValidateResponse(skip bool) httpRunnerOption

SkipValidateResponse sets whether to skip validation of HTTP response with OpenAPI Document.

func SprintMultilinef

func SprintMultilinef(lineformat, format string, a ...any) string

func TLS

func TLS(useTLS bool) grpcRunnerOption

func UseCookie

func UseCookie(use bool) httpRunnerOption

func User

func User(u string) sshRunnerOption

Types

type AfterFuncError

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

func (*AfterFuncError) Error

func (e *AfterFuncError) Error() string

func (*AfterFuncError) Unwrap

func (e *AfterFuncError) Unwrap() error

type BeforeFuncError

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

func (*BeforeFuncError) Error

func (e *BeforeFuncError) Error() string

func (*BeforeFuncError) Unwrap

func (e *BeforeFuncError) Unwrap() error

type CDPAction

type CDPAction struct {
	Fn   string
	Args map[string]any
}

type CDPActions

type CDPActions []CDPAction

type CDPArgType

type CDPArgType string
const (
	CDPArgTypeArg CDPArgType = "arg"
	CDPArgTypeRes CDPArgType = "res"
)

type CDPFn

type CDPFn struct {
	Desc    string
	Fn      any
	Args    CDPFnArgs
	Aliases []string
}

type CDPFnArg

type CDPFnArg struct {
	Typ     CDPArgType
	Key     string
	Example string
}

type CDPFnArgs

type CDPFnArgs []CDPFnArg

func (CDPFnArgs) ArgArgs

func (a CDPFnArgs) ArgArgs() CDPFnArgs

func (CDPFnArgs) ResArgs

func (a CDPFnArgs) ResArgs() CDPFnArgs

type Capturer

type Capturer interface {
	CaptureStart(trs Trails, bookPath, desc string)
	CaptureResult(trs Trails, result *RunResult)
	CaptureEnd(trs Trails, bookPath, desc string)

	CaptureResultByStep(trs Trails, result *RunResult)

	CaptureHTTPRequest(name string, req *http.Request)
	CaptureHTTPResponse(name string, res *http.Response)

	CaptureGRPCStart(name string, typ GRPCType, service, method string)
	CaptureGRPCRequestHeaders(h map[string][]string)
	CaptureGRPCRequestMessage(m map[string]any)
	CaptureGRPCResponseStatus(*status.Status)
	CaptureGRPCResponseHeaders(h map[string][]string)
	CaptureGRPCResponseMessage(m map[string]any)
	CaptureGRPCResponseTrailers(t map[string][]string)
	CaptureGRPCClientClose()
	CaptureGRPCEnd(name string, typ GRPCType, service, method string)

	CaptureCDPStart(name string)
	CaptureCDPAction(a CDPAction)
	CaptureCDPResponse(a CDPAction, res map[string]any)
	CaptureCDPEnd(name string)

	CaptureSSHCommand(command string)
	CaptureSSHStdout(stdout string)
	CaptureSSHStderr(stderr string)

	CaptureDBStatement(name string, stmt string)
	CaptureDBResponse(name string, res *DBResponse)

	CaptureExecCommand(command, shell string)
	CaptureExecStdin(stdin string)
	CaptureExecStdout(stdout string)
	CaptureExecStderr(stderr string)

	SetCurrentTrails(trs Trails)
	Errs() error
}

type Coverage

type Coverage struct {
	Specs []*SpecCoverage `json:"specs"`
}

Coverage is a coverage of runbooks.

type DBResponse

type DBResponse struct {
	LastInsertID int64
	RowsAffected int64
	Columns      []string
	Rows         []map[string]any
}

type GRPCOp

type GRPCOp string
const (
	GRPCOpMessage GRPCOp = "message"
	GRPCOpReceive GRPCOp = "receive"
	GRPCOpClose   GRPCOp = "close"
)

type GRPCType

type GRPCType string
const (
	GRPCUnary           GRPCType = "unary"
	GRPCServerStreaming GRPCType = "server"
	GRPCClientStreaming GRPCType = "client"
	GRPCBidiStreaming   GRPCType = "bidi"
)

type Loop

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

func (*Loop) Loop

func (l *Loop) Loop(ctx context.Context) bool

type Option

type Option func(*book) error

func AfterFunc

func AfterFunc(fn func(*RunResult) error) Option

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

func AfterFuncIf

func AfterFuncIf(fn func(*RunResult) error, ifCond string) Option

AfterFuncIf - Register the function to be run after the runbook is run if condition is true.

func BeforeFunc

func BeforeFunc(fn func(*RunResult) 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

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

Books - Load multiple runbooks.

func Capture

func Capture(c Capturer) Option

Capture - Register the capturer to capture steps.

func DBRunner

func DBRunner(name string, client Querier) Option

DBRunner - Set DB runner to runbook.

func DBRunnerWithOptions

func DBRunnerWithOptions(name, dsn string, opts ...dbRunnerOption) Option

DBRunnerWithOptions - Set DB runner to runbook using options.

func Debug

func Debug(debug bool) Option

Debug - Enable debug output.

func Desc

func Desc(desc string) Option

Desc - Set description to runbook.

func FailFast

func FailFast(enable bool) Option

FailFast - Enable fail-fast.

func Force

func Force(enable bool) Option

Force - Force all steps to run.

func Func

func Func(k string, v any) Option

Func - Set function to runner.

func GRPCImportPaths

func GRPCImportPaths(paths []string) Option

GRPCImportPaths - Set the path to the directory where proto sources can be imported for gRPC runners.

func GRPCNoTLS

func GRPCNoTLS(noTLS bool) Option

GRPCNoTLS - Disable TLS use in all gRPC runners.

func GRPCProtos

func GRPCProtos(protos []string) Option

GRPCProtos - Set the name of proto source for gRPC runners.

func GrpcRunner

func GrpcRunner(name string, cc *grpc.ClientConn) Option

GrpcRunner - Set gRPC runner to runbook.

func GrpcRunnerWithOptions

func GrpcRunnerWithOptions(name, target string, opts ...grpcRunnerOption) Option

GrpcRunnerWithOptions - Set gRPC runner to runbook using options.

func HTTPOpenApi3

func HTTPOpenApi3(l string) Option

HTTPOpenApi3 - Set the path of OpenAPI Document for HTTP runners. Deprecated: Use HTTPOpenApi3s instead.

func HTTPOpenApi3s

func HTTPOpenApi3s(locations []string) Option

HTTPOpenApi3s - Set the path of OpenAPI Document for HTTP runners.

func HTTPRunner

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

HTTPRunner - Set HTTP runner to runbook.

func HTTPRunnerWithHandler

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

HTTPRunnerWithHandler - Set HTTP runner to runbook with http.Handler.

func HostRules

func HostRules(rules ...string) Option

HostRules - Set host rules for runn.

func Interval

func Interval(d time.Duration) Option

Interval - Set interval between steps.

func LoadOnly

func LoadOnly() Option

LoadOnly - Load only.

func Overlay

func Overlay(path string) Option

Overlay - Overlay values on a runbook.

func Profile

func Profile(enable bool) Option

Profile - Enable profile.

func RunConcurrent

func RunConcurrent(enable bool, max int) Option

RunConcurrent - Run runbooks concurrently.

func RunID

func RunID(ids ...string) Option

RunID - Run the matching runbooks in order if there is only one runbook with a forward matching ID.

func RunLabel

func RunLabel(labels ...string) Option

RunLabel - Run all runbooks matching the label specification.

func RunMatch

func RunMatch(m string) Option

RunMatch - Run only runbooks with matching paths.

func RunRandom

func RunRandom(n int) Option

RunRandom - Run the specified number of runbooks at random. Sometimes the same runbook is run multiple times.

func RunSample

func RunSample(n int) Option

RunSample - Sample the specified number of runbooks.

func RunShard

func RunShard(n, i int) Option

RunShard - Distribute runbooks into a specified number of shards and run the specified shard of them.

func RunShuffle

func RunShuffle(enable bool, seed int64) Option

RunShuffle - Randomize the order of running runbooks.

func Runner

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

Runner - Set runner to runbook.

func SSHRunner

func SSHRunner(name string, client *ssh.Client) Option

SSHRunner - Set SSH runner to runbook.

func SSHRunnerWithOptions

func SSHRunnerWithOptions(name string, opts ...sshRunnerOption) Option

SSHRunnerWithOptions - Set SSH runner to runbook using options.

func Scopes

func Scopes(scopes ...string) Option

Scopes - Set scopes for runn.

func SkipIncluded

func SkipIncluded(enable bool) Option

SkipIncluded - Skip running the included step by itself.

func SkipTest

func SkipTest(enable bool) Option

SkipTest - Skip test section.

func Stderr

func Stderr(w io.Writer) Option

Stderr - Set STDERR.

func Stdout

func Stdout(w io.Writer) Option

Stdout - Set STDOUT.

func T

func T(t *testing.T) Option

T - Acts as test helper.

func Trace

func Trace(enable bool) Option

Trace - Add tokens for tracing to headers and queries by default.

func Underlay

func Underlay(path string) Option

Underlay - Lay values under the runbook.

func Var

func Var(k any, v any) Option

Var - Set variable to runner.

type Querier

type Querier interface {
	sqlexp.Querier
}

type RunResult

type RunResult struct {
	// runbook ID
	ID          string
	Desc        string
	Labels      []string
	Path        string
	Skipped     bool
	Err         error
	StepResults []*StepResult
	Store       map[string]any
	Elapsed     time.Duration
}

RunResult is the result of a runbook run.

func (*RunResult) OutFailure

func (rr *RunResult) OutFailure(out io.Writer) error

type RunnerType

type RunnerType string
const (
	RunnerTypeHTTP    RunnerType = "http"
	RunnerTypeDB      RunnerType = "db"
	RunnerTypeGRPC    RunnerType = "grpc"
	RunnerTypeCDP     RunnerType = "cdp"
	RunnerTypeSSH     RunnerType = "ssh"
	RunnerTypeExec    RunnerType = "exec"
	RunnerTypeTest    RunnerType = "test"
	RunnerTypeDump    RunnerType = "dump"
	RunnerTypeInclude RunnerType = "include"
	RunnerTypeBind    RunnerType = "bind"
)

type SpecCoverage

type SpecCoverage struct {
	Key       string         `json:"key"`
	Coverages map[string]int `json:"coverages"`
}

SpecCoverage is a coverage of spec (e.g. OpenAPI Document, servive of protocol buffers).

type StepResult

type StepResult struct {
	// runbook ID
	ID      string
	Key     string
	Desc    string
	Skipped bool
	Err     error
	// Run result of runbook loaded by include runner
	IncludedRunResult *RunResult
	Elapsed           time.Duration
}

StepResult is the result of a step run.

type Trail

type Trail struct {
	Type           TrailType  `json:"type"`
	Desc           string     `json:"desc,omitempty"`
	RunbookID      string     `json:"id,omitempty"`
	RunbookPath    string     `json:"path,omitempty"`
	StepIndex      *int       `json:"step_index,omitempty"`
	StepKey        string     `json:"step_key,omitempty"`
	StepRunnerType RunnerType `json:"step_runner_type,omitempty"`
	StepRunnerKey  string     `json:"step_runner_key,omitempty"`
	FuncIndex      *int       `json:"func_index,omitempty"`
	LoopIndex      *int       `json:"loop_index,omitempty"`
}

Trail - The trail of elements in the runbook at runtime. Trail does not use slices to copy values.

func (Trail) String

func (tr Trail) String() string

type TrailType

type TrailType string
const (
	TrailTypeRunbook    TrailType = "runbook"
	TrailTypeStep       TrailType = "step"
	TrailTypeBeforeFunc TrailType = "beforeFunc"
	TrailTypeAfterFunc  TrailType = "afterFunc"
	TrailTypeLoop       TrailType = "loop"
)

type Trails

type Trails []Trail

type TxQuerier

type TxQuerier interface {
	nest.Querier
	BeginTx(ctx context.Context, opts *nest.TxOptions) (*nest.Tx, error)
}

type UnsupportedError

type UnsupportedError struct {
	Cause error
}

func (*UnsupportedError) Error

func (e *UnsupportedError) Error() string

func (*UnsupportedError) Unwrap

func (e *UnsupportedError) Unwrap() error

Directories

Path Synopsis
cmd
examples

Jump to

Keyboard shortcuts

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