{"id":13511252,"url":"https://github.com/zoncoen/scenarigo","last_synced_at":"2025-05-16T06:07:07.753Z","repository":{"id":35485154,"uuid":"173053663","full_name":"zoncoen/scenarigo","owner":"zoncoen","description":"An end-to-end scenario testing tool for HTTP/gRPC server.","archived":false,"fork":false,"pushed_at":"2025-02-09T15:06:26.000Z","size":2278,"stargazers_count":322,"open_issues_count":10,"forks_count":22,"subscribers_count":7,"default_branch":"main","last_synced_at":"2025-02-09T15:19:45.323Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zoncoen.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"zoncoen"}},"created_at":"2019-02-28T06:21:31.000Z","updated_at":"2025-02-09T14:00:59.000Z","dependencies_parsed_at":"2023-02-12T23:31:15.925Z","dependency_job_id":"760251b1-da38-48fb-a585-1eb2ee0b3676","html_url":"https://github.com/zoncoen/scenarigo","commit_stats":{"total_commits":629,"total_committers":9,"mean_commits":69.88888888888889,"dds":"0.33386327503974567","last_synced_commit":"70b58c9aab1f98474c36c4e395973d47cb028438"},"previous_names":[],"tags_count":59,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zoncoen%2Fscenarigo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zoncoen%2Fscenarigo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zoncoen%2Fscenarigo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zoncoen%2Fscenarigo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zoncoen","download_url":"https://codeload.github.com/zoncoen/scenarigo/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":237986869,"owners_count":19397809,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-01T03:00:41.858Z","updated_at":"2025-02-16T18:08:00.136Z","avatar_url":"https://github.com/zoncoen.png","language":"Go","funding_links":["https://github.com/sponsors/zoncoen"],"categories":["Go","Automated API Testing \u0026 Load Testing Tools","Automated Testing"],"sub_categories":["47. [scenarigo](https://github.com/zoncoen/scenarigo)"],"readme":"\u003e [!IMPORTANT]\n\u003e This repository has been transferred to the [scenarigo organization](https://github.com/scenarigo/scenarigo).\n\n\u003ca href=\"https://github.com/zoncoen/scenarigo\"\u003e\n  \u003cp align=\"center\"\u003e\n    \u003cimg alt=\"Scenarigo\" src=\"https://user-images.githubusercontent.com/2238852/205980597-37eaaf03-fd35-4a04-93c4-884c95f48df3.png\" width=\"485px\"\u003e\n  \u003c/p\u003e\n\u003c/a\u003e\n\nA scenario-based API testing tool for HTTP/gRPC server.\n\n[![godoc](https://godoc.org/github.com/zoncoen/scenarigo?status.svg)](https://pkg.go.dev/github.com/zoncoen/scenarigo)\n![test](https://github.com/zoncoen/scenarigo/workflows/test/badge.svg?branch=main)\n[![codecov](https://codecov.io/gh/zoncoen/scenarigo/branch/main/graph/badge.svg)](https://codecov.io/gh/zoncoen/scenarigo)\n[![go report](https://goreportcard.com/badge/zoncoen/scenarigo)](https://goreportcard.com/report/github.com/zoncoen/scenarigo)\n[![codebeat](https://codebeat.co/badges/93ee2453-1a25-4db6-b98e-c430c994b4b8)](https://codebeat.co/projects/github-com-zoncoen-scenarigo-main)\n[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)\n\n## Overview\n\nScenarigo is a scenario-based API testing tool for HTTP/gRPC server.\nIt is written in Go and provides [a plugin feature](#plugin) that enables you to extend by writing Go code.\nYou can write test scenarios as YAML files and executes them.\n\n```yaml github.yaml\ntitle: get scenarigo repository\nvars:\n  user: zoncoen\n  repo: scenarigo\nsteps:\n- title: get repository\n  protocol: http\n  request:\n    method: GET\n    url: 'https://api.github.com/repos/{{vars.user}}/{{vars.repo}}'\n  expect:\n    code: OK\n    body:\n      id: '{{int($) \u003e 0}}'\n      name: '{{vars.repo}}'\n```\n\n## Installation\n\n### go install command (recommend)\n\n```shell\n$ go install github.com/zoncoen/scenarigo/cmd/scenarigo@latest\n```\n\n### from release page\n\nGo to the [releases page](https://github.com/zoncoen/scenarigo/releases) and download the zip file. Unpack the zip file, and put the binary to a directory in your `$PATH`.\n\nYou can download the latest command into the `./scenarigo` directory with the following one-liner code. Place the binary `./scenarigo/scenarigo` into your `$PATH`.\n\n```shell\n$ version=$(curl -s https://api.github.com/repos/zoncoen/scenarigo/releases/latest | jq -r '.tag_name') \u0026\u0026 \\\n    go_version=$(echo -n $(curl -s 'https://go.dev/VERSION?m=text' | head -n 1)) \u0026\u0026 \\\n    curl -sLJ https://github.com/zoncoen/scenarigo/releases/download/${version}/scenarigo_${version}_${go_version}_$(uname)_$(uname -m).tar.gz -o scenarigo.tar.gz \u0026\u0026 \\\n    mkdir ./scenarigo \u0026\u0026 tar -zxvf ./scenarigo.tar.gz -C ./scenarigo \u0026\u0026 rm scenarigo.tar.gz\n```\n\n**Notes**: If you use the plugin mechanism, the `scenarigo` command and plugins must be built using the same version of Go.\n\n### Setup\n\nYou can generate a configuration file `scenarigo.yaml` via the following command.\n\n```shell\n$ scenarigo config init\n```\n\n```yaml scenarigo.yaml\nschemaVersion: config/v1\n\n# global variables\nvars:\n  endpoint: http://api.example.com\n\nscenarios: [] # Specify test scenario files and directories.\n\npluginDirectory: ./gen    # Specify the root directory of plugins.\nplugins:                  # Specify configurations to build plugins.\n  plugin.so:              # Map keys specify plugin output file path from the root directory of plugins.\n    src: ./path/to/plugin # Specify the source file, directory, or \"go gettable\" module path of the plugin.\n\noutput:\n  verbose: false # Enable verbose output.\n  colored: false # Enable colored output with ANSI color escape codes. It is enabled by default but disabled when a NO_COLOR environment variable is set (regardless of its value).\n  summary: false # Enable summary output.\n  report:\n    json:\n      filename: ./report.json # Specify a filename for test report output in JSON.\n    junit:\n      filename: ./junit.xml   # Specify a filename for test report output in JUnit XML format.\n```\n\n## Usage\n\n`scenarigo run` executes test scenarios based on the configuration file.\n\n```yaml scenarigo.yaml\nschemaVersion: config/v1\n\nscenarios:\n- github.yaml\n```\n\n```yaml github.yaml\ntitle: get scenarigo repository\nsteps:\n- title: GET https://api.github.com/repos/zoncoen/scenarigo\n  vars:\n    user: zoncoen\n    repo: scenarigo\n  protocol: http\n  request:\n    method: GET\n    url: \"https://api.github.com/repos/{{vars.user}}/{{vars.repo}}\"\n  expect:\n    code: OK\n    body:\n      name: \"{{vars.repo}}\"\n```\n\n```shell\n$ scenarigo run\nok      github.yaml     0.068s\n```\n\nYou can see all commands and options by `scenarigo help`.\n\n```\nscenarigo is a scenario-based API testing tool.\n\nUsage:\n  scenarigo [command]\n\nAvailable Commands:\n  completion  Generate the autocompletion script for the specified shell\n  config      manage the scenarigo configuration file\n  dump        dump test scenario files\n  help        Help about any command\n  list        list the test scenario files\n  plugin      provide operations for plugins\n  run         run test scenarios\n  version     print scenarigo version\n\nFlags:\n  -c, --config string   specify configuration file path (read configuration from stdin if specified \"-\")\n  -h, --help            help for scenarigo\n      --root string     specify root directory (default value is the directory of configuration file)\n\nUse \"scenarigo [command] --help\" for more information about a command.\n```\n\n## How to write test scenarios\n\nYou can write test scenarios easily in YAML.\n\n### Send HTTP requests\n\nA test scenario consists of some steps. A step represents an API request. The scenario steps will be run from top to bottom sequentially.\nThis simple example has a step that sends a `GET` request to `http://example.com/message`.\n\n```yaml\ntitle: check /message\nsteps:\n- title: GET /message\n  protocol: http\n  request:\n    method: GET\n    url: http://example.com/message\n```\n\nTo send a query parameter, add it directly to the URL or use the `query` field.\n\n```yaml\ntitle: check /message\nsteps:\n- title: GET /message\n  protocol: http\n  request:\n    method: GET\n    url: http://example.com/message\n    query:\n      id: 1\n```\n\nYou can use other methods to send data to your APIs.\n\n```yaml\ntitle: check /message\nsteps:\n- title: POST /message\n  protocol: http\n  request:\n    method: POST\n    url: http://example.com/message\n    body:\n      message: hello\n```\n\nBy default, Scenarigo will send body data as JSON. If you want to use other formats, set the `Content-Type` header.\n\n```yaml\ntitle: check /message\nsteps:\n- title: POST /message\n  protocol: http\n  request:\n    method: POST\n    url: http://example.com/message\n    header:\n      Content-Type: application/x-www-form-urlencoded\n    body:\n      message: hello\n```\n\nAvailable `Content-Type` header to encode request body is the following.\n\n- `application/json` (default)\n- `text/plain`\n- `application/x-www-form-urlencoded`\n\n### Check HTTP responses\n\nYou can test your APIs by checking responses. If the result differs expected values, Scenarigo aborts the execution of the test scenario and notify the error.\n\n```yaml\ntitle: check /message\nsteps:\n- title: GET /message\n  protocol: http\n  request:\n    method: GET\n    url: http://example.com/message\n    query:\n      id: 1\n  expect:\n    code: OK\n    header:\n      Content-Type: application/json; charset=utf-8\n    body:\n      id: 1\n      message: hello\n```\n\nScenarigo allows [template string](#template-string) as expected values.\nBesides, you can write assertions by conditional expressions with the actual value `$`.\n\n```yaml\ntitle: check /message\nsteps:\n- title: GET /message\n  protocol: http\n  request:\n    method: GET\n    url: http://example.com/message\n    query:\n      id: 1\n  expect:\n    code: OK\n    header:\n      Content-Type: application/json; charset=utf-8\n    body:\n      id: {{int($) \u003e 0}}\n      message: '{{\"hello\" + \" world\"}}'\n```\n\n### Variables\n\nThe `vars` field defines variables that can be referred by [template string](#template-string) like `'{{vars.id}}'`.\n\n```yaml\ntitle: get message 1\nvars:\n  id: 1\nsteps:\n- title: GET /messages\n  protocol: http\n  request:\n    method: GET\n    url: 'http://example.com/messages/{{vars.id}}'\n```\n\nYou can define `step` scope variables that can't be accessed from other steps.\n\n```yaml\ntitle: get message 1\nsteps:\n- title: GET /messages\n  vars:\n    id: 1\n  protocol: http\n  request:\n    method: GET\n    url: 'http://example.com/messages/{{vars.id}}'\n```\n\nIf you want to pass the response data to the subsequent steps, use the `bind` field.\n\n```yaml\ntitle: re-post message 1\nvars:\n  id: 1\nsteps:\n- title: GET /messages\n  protocol: http\n  request:\n    method: GET\n    url: 'http://example.com/messages/{{vars.id}}'\n  bind:\n    vars:\n      msg: '{{response.body.text}}'\n- title: POST /messages\n  protocol: http\n  request:\n    method: POST\n    url: http://example.com/messages\n    header:\n      Content-Type: application/json\n    body:\n      text: '{{vars.msg}}'\n  expect:\n    code: OK\n    body:\n      id: '{{assert.notZero}}'\n      text: '{{request.body.text}}'\n```\n\nYou can also define global variables in the `scenarigo.yaml`. The defined variables can be used from all test scenarios.\n\n```yaml\nschemaVersion: config/v1\nvars:\n  name: zoncoen\n```\n\n### Secrets\n\nThe `secrets` field allows defining variables like the `vars` field. Besides, the values defined by the `secrets` field are masked in the outputs.\n\n```yaml\nschemaVersion: scenario/v1\nplugins:\n  plugin: plugin.so\nvars:\n  clientId: abcdef\nsecrets:\n  clientSecret: XXXXX\ntitle: get user profile\nsteps:\n- title: get access token\n  protocol: http\n  request:\n    method: POST\n    url: 'http://example.com/oauth/token'\n    header:\n      Content-Type: application/x-www-form-urlencoded\n    body:\n      grant_type: client_credentials\n      client_id: '{{vars.clientId}}'\n      client_secret: '{{secrets.clientSecret}}'\n  expect:\n    code: OK\n    body:\n      access_token: '{{$ != \"\"}}'\n      token_type: Bearer\n  bind:\n    secrets:\n      accessToken: '{{response.body.access_token}}'\n- title: get user profile\n  protocol: http\n  request:\n    method: GET\n    url: 'http://example.com/users/zoncoen'\n    header:\n      Authorization: 'Bearer {{secrets.accessToken}}'\n  expect:\n    code: OK\n    body:\n      name: zoncoen\n```\n\n```shell\n...\n        --- PASS: scenarios/get-profile.yaml/get_user_profile/get_access_token (0.00s)\n                request:\n                  method: POST\n                  url: http://example.com/oauth/token\n                  header:\n                    Content-Type:\n                    - application/x-www-form-urlencoded\n                  body:\n                    client_id: abcdef\n                    client_secret: {{secrets.clientSecret}}\n                    grant_type: client_credentials\n                response:\n...\n                  body:\n                    access_token: {{secrets.accessToken}}\n                    token_type: Bearer\n                elapsed time: 0.001743 sec\n        --- PASS: scenarios/get-profile.yaml/get_user_profile/get_user_profile (0.00s)\n                request:\n                  method: GET\n                  url: http://example.com/users/zoncoen\n                  header:\n                    Authorization:\n                    - Bearer {{secrets.accessToken}}\n...\n```\n\n### Timeout/Retry\n\nYou can set timeout and retry policy for each step.\nDuration strings are parsed by [`time.ParseDuration`](https://pkg.go.dev/time#ParseDuration).\nValid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\".\n\n```yaml\nsteps:\n- protocol: http\n  request:\n    method: GET\n    url: http://example.com\n  expect:\n    code: OK\n  timeout: 30s           # default values is 0, 0 means no timeout\n  retry:                 # default policy is never retry\n    constant:\n      interval: 5s       # default value is 1s\n      maxRetries: 1      # default value is 5, 0 means forever\n      maxElapsedTime: 1m # default value is 0, 0 means forever\n```\n\nScenarigo also provides the retry feature with an exponential backoff algorithm.\n\n```yaml\nsteps:\n- protocol: http\n  request:\n    method: GET\n    url: http://example.com\n  expect:\n    code: OK\n  timeout: 30s             # default values is 0, 0 means no timeout\n  retry:                   # default policy is never retry\n    exponential:\n      initialInterval: 1s  # default value is 500ms\n      factor: 2            # default value is 1.5\n      jitterFactor: 0.5    # default value is 0.5\n      maxInterval: 180s    # default value is 60s\n      maxRetries: 10       # default value is 5, 0 means forever\n      maxElapsedTime: 10m  # default value is 0, 0 means forever\n```\n\nThe actual interval is calculated using the following formula.\n\n```\ninitialInterval * factor ^ (retry count - 1) * (random value in range [1 - jitterFactor, 1 + jitterFactor])\n```\n\nFor example, the retry intervals will be like the following table with the above retry policy.\n\nNote: `maxInterval` caps the retry interval, not the randomized interval.\n\n|Retry #|Retry interval|Randomized interval range|\n|---|---|---|\n|1|1s|[0.5s, 1.5s]|\n|2|2s|[1s, 3s]|\n|3|4s|[2s, 6s]|\n|4|8s|[4s, 12s]|\n|5|16s|[8s, 24s]|\n|6|32s|[16s, 48s]|\n|7|64s|[32s, 96s]|\n|8|128s|[64s, 192s]|\n|9|180s|[90s, 270s]|\n|10|180s|[90s, 270s]|\n\n### Using conditions to control step execution\n\nYou can use `if` field to prevent a step from execution unless a condition is met. The template expression must return a boolean value. For example, you can access the results of other steps like `{{steps.step_id.result}}`. There are three result kinds of steps: `passed`, `failed`, and `skipped`.\n\nScenarigo doesn't execute subsequent steps if a step fails in default. If you want to continue running the test scenario even if a step fails, set true to the `continueOnError` field.\n\nFor example, the second step will be executed in the following test scenario when the first step fails only.\n\n```yaml\nschemaVersion: scenario/v1\ntitle: create item if not found\nvars:\n  itemName: foo\n  itemPrice: 100\nsteps:\n- id: find # need to set id to access the result of this step\n  title: find by name\n  continueOnError: true # the errors of this step don't fail the test scenario\n  protocol: http\n  request:\n    method: GET\n    url: 'http://example.com/items?name={{vars.itemName}}'\n  expect:\n    code: OK\n    body:\n      name: '{{vars.itemName}}'\n  bind:\n    vars:\n      itemId: '{{response.body.id}}'\n- title: create\n  if: '{{steps.find.result == \"failed\"}}' # this step will be executed when the find step fails only\n  protocol: http\n  request:\n    method: POST\n    url: 'http://example.com/items'\n    header:\n      Content-Type: application/json\n    body:\n      name: '{{vars.itemName}}'\n      price: '{{vars.itemPrice}}'\n  expect:\n    code: OK\n    body:\n      name: '{{vars.itemName}}'\n  bind:\n    vars:\n      itemId: '{{response.body.id}}'\n```\n\n## Template String\n\nScenarigo provides the original template string feature which is evaluated at runtime. You can use expressions with a pair of double braces `{{}}` in YAML strings. All expression return an arbitrary value.\n\nFor instance, `'{{1}}'` is evaluated as an integer `1` at runtime.\n\n```yaml\nvars:\n  id: '{{1}}' # id: 1\n```\n\nYou can mix the templates into a raw string if all expressions' results are a string.\n\n```yaml\nvars:\n  text: 'foo-{{\"bar\"}}-baz' # text: 'foo-bar-baz'\n```\n\n### Syntax\n\nThe grammar of the template is defined below, using `|` for alternatives, `[]` for optional, `{}` for repeated, `()` for grouping, and `...` for character range.\n\n```\nParameterExpr   = \"{{\" Expr \"}}\"\nExpr            = UnaryExpr | BinaryExpr | ConditionalExpr\nUnaryExpr       = [UnaryOp] (\n                    ParenExpr | SelectorExpr | IndexExpr | CallExpr |\n                    INT | FLOAT | BOOL | STRING | IDENT\n                  )\nUnaryOp         = \"!\" | \"-\"\nParenExpr       = \"(\" Expr \")\"\nSelectorExpr    = Expr \".\" IDENT\nIndexExpr       = Expr \"[\" INT \"]\"\nCallExpr        = Expr \"(\" [Expr {\",\" Expr}] \")\"\nBinaryExpr      = Expr BinaryOp Expr\nBinaryOp        = \"+\" | \"-\" | \"*\" | \"/\" | \"%\" |\n                  \"\u0026\u0026\" | \"||\" | \"??\" |\n                  \"==\" | \"!=\" | \"\u003c\" | \"\u003c=\" | \"\u003e\" | \"\u003e=\" \nConditionalExpr = Expr ? Expr : Expr\n```\n\nThe lexis is defined below.\n\n```\nINT           = \"0\" | (\"1\"...\"9\" {DECIMAL_DIGIT})\nFLOAT         = INT \".\" DECIMAL_DIGIT {DECIMAL_DIGIT}\nBOOL          = \"true\" | \"false\"\nSTRING        = `\"` {UNICODE_VALUE} `\"`\nIDENT         = (LETTER {LETTER | DECIMAL_DIGIT | \"-\" | \"_\"} | \"$\") - RESERVED\n\nDECIMAL_DIGIT = \"0\"...\"9\"\nUNICODE_VALUE = UNICODE_CHAR | ESCAPED_CHAR\nUNICODE_CHAR  = /* an arbitrary UTF-8 encoded char */\nESCAPED_CHAR  = \"\\\" `\"`\nLETTER        = \"a\"...\"Z\"\nTYPES         = \"int\" | \"uint\" | \"float\" | \"bool\" | \"string\" |\n                \"bytes\" | \"time\" | \"duration\" | \"any\"\nRESERVED      = BOOL | TYPES | \"type\" | \"defined\" | \"size\"\n```\n\n### Types\n\nThe template feature has abstract types for operations.\n\n|Template Type|Description|Go Type|\n|---|---|---|\n|int|64-bit signed integers|int, int8, int16, int32, int64|\n|uint|64-bit unsigned integers|uint, uint8, uint16, uint32, uint64|\n|float|IEEE-754 64-bit floating-point numbers|float32, float64|\n|bool|booleans|bool|\n|string|UTF-8 strings|string|\n|bytes|byte sequence|[]byte|\n|time|time with nanosecond precision|[time.Time](https://pkg.go.dev/time#Time)|\n|duration|amount of time|[time.Duration](https://pkg.go.dev/time#Duration)|\n|any|other all Go types|any|\n\n#### Type Conversions\n\nThe template feature provides functions to convert types.\n\n\u003ctable\u003e\n  \u003cthead\u003e\n    \u003ctr\u003e\n      \u003cth\u003eFunction\u003c/th\u003e\n      \u003cth\u003eType\u003c/th\u003e\n      \u003cth\u003eDescription\u003c/th\u003e\n    \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n    \u003ctr\u003e\n      \u003ctd align=\"center\" rowspan=5\u003eint\u003c/td\u003e\n      \u003ctd\u003e(*int) -\u003e int\u003c/td\u003e\n      \u003ctd\u003etype conversion (returns an error if arg is nil)\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(uint) -\u003e int\u003c/td\u003e\n      \u003ctd\u003etype conversion (returns an error if result is out of range)\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(float) -\u003e int\u003c/td\u003e\n      \u003ctd\u003etype conversion (rounds toward zero, returns an error if result is out of range)\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(string) -\u003e int\u003c/td\u003e\n      \u003ctd\u003etype conversion (returns an error if arg in invalid int string)\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(duration) -\u003e int\u003c/td\u003e\n      \u003ctd\u003etype conversion\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd align=\"center\" rowspan=4\u003euint\u003c/td\u003e\n      \u003ctd\u003e(int) -\u003e uint\u003c/td\u003e\n      \u003ctd\u003etype conversion (returns an error if result is out of range)\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(*uint) -\u003e uint\u003c/td\u003e\n      \u003ctd\u003etype conversion (returns an error if arg is nil)\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(float) -\u003e uint\u003c/td\u003e\n      \u003ctd\u003etype conversion (rounds toward zero, returns an error if result is out of range)\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(string) -\u003e uint\u003c/td\u003e\n      \u003ctd\u003etype conversion (returns an error if arg in invalid uint string)\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd align=\"center\" rowspan=4\u003efloat\u003c/td\u003e\n      \u003ctd\u003e(int) -\u003e float\u003c/td\u003e\n      \u003ctd\u003etype conversion\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(uint) -\u003e float\u003c/td\u003e\n      \u003ctd\u003etype conversion\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(*float) -\u003e float\u003c/td\u003e\n      \u003ctd\u003etype conversion (returns an error if arg is nil)\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(string) -\u003e float\u003c/td\u003e\n      \u003ctd\u003etype conversion (returns an error if arg in invalid float string)\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd align=\"center\"\u003ebool\u003c/td\u003e\n      \u003ctd\u003e(*bool) -\u003e bool\u003c/td\u003e\n      \u003ctd\u003etype conversion (returns an error if arg is nil)\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd align=\"center\" rowspan=7\u003estring\u003c/td\u003e\n      \u003ctd\u003e(int) -\u003e string\u003c/td\u003e\n      \u003ctd\u003etype conversion\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(uint) -\u003e string\u003c/td\u003e\n      \u003ctd\u003etype conversion\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(float) -\u003e string\u003c/td\u003e\n      \u003ctd\u003etype conversion\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(*string) -\u003e string\u003c/td\u003e\n      \u003ctd\u003etype conversion (returns an error if arg is nil)\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(bytes) -\u003e string\u003c/td\u003e\n      \u003ctd\u003etype conversion (returns an error if arg contains invalid UTF-8 encoded characters)\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(time) -\u003e string\u003c/td\u003e\n      \u003ctd\u003econvert to string according to RFC3339 format\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(duration) -\u003e string\u003c/td\u003e\n      \u003ctd\u003econvert to string according to \u003ca href=\"https://pkg.go.dev/time#Duration.String\"\u003etime.Duration.String\u003c/a\u003e format\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd align=\"center\" rowspan=2\u003ebytes\u003c/td\u003e\n      \u003ctd\u003e(string) -\u003e bytes\u003c/td\u003e\n      \u003ctd\u003etype conversion\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(*bytes) -\u003e bytes\u003c/td\u003e\n      \u003ctd\u003etype conversion (returns an error if arg is nil)\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd align=\"center\" rowspan=2\u003etime\u003c/td\u003e\n      \u003ctd\u003e(string) -\u003e time\u003c/td\u003e\n      \u003ctd\u003eparse RFC3339 format string as time\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(*time) -\u003e time\u003c/td\u003e\n      \u003ctd\u003etype conversion (returns an error if arg is nil)\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd align=\"center\" rowspan=3\u003eduration\u003c/td\u003e\n      \u003ctd\u003e(int) -\u003e duration\u003c/td\u003e\n      \u003ctd\u003etype conversion\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(string) -\u003e duration\u003c/td\u003e\n      \u003ctd\u003eparse string as duration by \u003ca href=\"https://pkg.go.dev/time#ParseDuration\"\u003etime.ParseDuration\u003c/a\u003e\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(*duration) -\u003e duration\u003c/td\u003e\n      \u003ctd\u003etype conversion (returns an error if arg is nil)\u003c/td\u003e\n    \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\n### Operators\n\n\u003ctable\u003e\n  \u003cthead\u003e\n    \u003ctr\u003e\n      \u003cth\u003eOperator\u003c/th\u003e\n      \u003cth\u003eType\u003c/th\u003e\n      \u003cth\u003eDescription\u003c/th\u003e\n    \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n    \u003ctr\u003e\n      \u003ctd align=\"center\"\u003e! _\u003c/td\u003e\n      \u003ctd\u003e(bool) -\u003e bool\u003c/td\u003e\n      \u003ctd\u003elogical not\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd align=\"center\" rowspan=3\u003e- _\u003c/td\u003e\n      \u003ctd\u003e(int) -\u003e int\u003c/td\u003e\n      \u003ctd\u003enegation\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(float) -\u003e float\u003c/td\u003e\n      \u003ctd\u003enegation\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(duration) -\u003e duration\u003c/td\u003e\n      \u003ctd\u003enegation\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd align=\"center\" rowspan=8\u003e_ + _\u003c/td\u003e\n      \u003ctd\u003e(int, int) -\u003e int\u003c/td\u003e\n      \u003ctd\u003earithmetic\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(uint, uint) -\u003e uint\u003c/td\u003e\n      \u003ctd\u003earithmetic\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(float, float) -\u003e float\u003c/td\u003e\n      \u003ctd\u003earithmetic\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(string, string) -\u003e string\u003c/td\u003e\n      \u003ctd\u003econcatenation\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(bytes, bytes) -\u003e bytes\u003c/td\u003e\n      \u003ctd\u003econcatenation\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(time, duration) -\u003e time\u003c/td\u003e\n      \u003ctd\u003earithmetic\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(duration, time) -\u003e time\u003c/td\u003e\n      \u003ctd\u003earithmetic\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(duration, duration) -\u003e duration\u003c/td\u003e\n      \u003ctd\u003earithmetic\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd align=\"center\" rowspan=6\u003e_ - _\u003c/td\u003e\n      \u003ctd\u003e(int, int) -\u003e int\u003c/td\u003e\n      \u003ctd\u003earithmetic\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(uint, uint) -\u003e uint\u003c/td\u003e\n      \u003ctd\u003earithmetic\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(float, float) -\u003e float\u003c/td\u003e\n      \u003ctd\u003earithmetic\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(time, time) -\u003e duration\u003c/td\u003e\n      \u003ctd\u003earithmetic\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(time, duration) -\u003e time\u003c/td\u003e\n      \u003ctd\u003earithmetic\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(duration, duration) -\u003e duration\u003c/td\u003e\n      \u003ctd\u003earithmetic\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd align=\"center\" rowspan=3\u003e_ * _\u003c/td\u003e\n      \u003ctd\u003e(int, int) -\u003e int\u003c/td\u003e\n      \u003ctd\u003earithmetic\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(uint, uint) -\u003e uint\u003c/td\u003e\n      \u003ctd\u003earithmetic\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(float, float) -\u003e float\u003c/td\u003e\n      \u003ctd\u003earithmetic\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd align=\"center\" rowspan=3\u003e_ / _\u003c/td\u003e\n      \u003ctd\u003e(int, int) -\u003e int\u003c/td\u003e\n      \u003ctd\u003earithmetic\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(uint, uint) -\u003e uint\u003c/td\u003e\n      \u003ctd\u003earithmetic\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(float, float) -\u003e float\u003c/td\u003e\n      \u003ctd\u003earithmetic\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd align=\"center\" rowspan=2\u003e_ % _\u003c/td\u003e\n      \u003ctd\u003e(int, int) -\u003e int\u003c/td\u003e\n      \u003ctd\u003earithmetic\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(uint, uint) -\u003e uint\u003c/td\u003e\n      \u003ctd\u003earithmetic\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd align=\"center\"\u003e_ == _\u003c/td\u003e\n      \u003ctd\u003e(A, A) -\u003e bool\u003c/td\u003e\n      \u003ctd\u003eequality\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd align=\"center\"\u003e_ != _\u003c/td\u003e\n      \u003ctd\u003e(A, A) -\u003e bool\u003c/td\u003e\n      \u003ctd\u003einequality\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd align=\"center\" rowspan=7\u003e_ \u003c _\u003c/td\u003e\n      \u003ctd\u003e(int, int) -\u003e bool\u003c/td\u003e\n      \u003ctd\u003eordering\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(uint, uint) -\u003e bool\u003c/td\u003e\n      \u003ctd\u003eordering\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(float, float) -\u003e bool\u003c/td\u003e\n      \u003ctd\u003eordering\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(string, string) -\u003e bool\u003c/td\u003e\n      \u003ctd\u003eordering\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(bytes, bytes) -\u003e bool\u003c/td\u003e\n      \u003ctd\u003eordering\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(time, time) -\u003e bool\u003c/td\u003e\n      \u003ctd\u003eordering\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(duration, duration) -\u003e bool\u003c/td\u003e\n      \u003ctd\u003eordering\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd align=\"center\" rowspan=7\u003e_ \u003c= _\u003c/td\u003e\n      \u003ctd\u003e(int, int) -\u003e bool\u003c/td\u003e\n      \u003ctd\u003eordering\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(uint, uint) -\u003e bool\u003c/td\u003e\n      \u003ctd\u003eordering\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(float, float) -\u003e bool\u003c/td\u003e\n      \u003ctd\u003eordering\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(string, string) -\u003e bool\u003c/td\u003e\n      \u003ctd\u003eordering\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(bytes, bytes) -\u003e bool\u003c/td\u003e\n      \u003ctd\u003eordering\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(time, time) -\u003e bool\u003c/td\u003e\n      \u003ctd\u003eordering\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(duration, duration) -\u003e bool\u003c/td\u003e\n      \u003ctd\u003eordering\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd align=\"center\" rowspan=7\u003e_ \u003e _\u003c/td\u003e\n      \u003ctd\u003e(int, int) -\u003e bool\u003c/td\u003e\n      \u003ctd\u003eordering\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(uint, uint) -\u003e bool\u003c/td\u003e\n      \u003ctd\u003eordering\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(float, float) -\u003e bool\u003c/td\u003e\n      \u003ctd\u003eordering\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(string, string) -\u003e bool\u003c/td\u003e\n      \u003ctd\u003eordering\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(bytes, bytes) -\u003e bool\u003c/td\u003e\n      \u003ctd\u003eordering\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(time, time) -\u003e bool\u003c/td\u003e\n      \u003ctd\u003eordering\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(duration, duration) -\u003e bool\u003c/td\u003e\n      \u003ctd\u003eordering\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd align=\"center\" rowspan=7\u003e_ \u003e= _\u003c/td\u003e\n      \u003ctd\u003e(int, int) -\u003e bool\u003c/td\u003e\n      \u003ctd\u003eordering\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(uint, uint) -\u003e bool\u003c/td\u003e\n      \u003ctd\u003eordering\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(float, float) -\u003e bool\u003c/td\u003e\n      \u003ctd\u003eordering\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(string, string) -\u003e bool\u003c/td\u003e\n      \u003ctd\u003eordering\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(bytes, bytes) -\u003e bool\u003c/td\u003e\n      \u003ctd\u003eordering\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(time, time) -\u003e bool\u003c/td\u003e\n      \u003ctd\u003eordering\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e(duration, duration) -\u003e bool\u003c/td\u003e\n      \u003ctd\u003eordering\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd align=\"center\"\u003e_ \u0026\u0026 _\u003c/td\u003e\n      \u003ctd\u003e(bool, bool) -\u003e bool\u003c/td\u003e\n      \u003ctd\u003elogical and\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd align=\"center\"\u003e_ || _\u003c/td\u003e\n      \u003ctd\u003e(bool, bool) -\u003e bool\u003c/td\u003e\n      \u003ctd\u003elogical or\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd align=\"center\"\u003e_ ?? _\u003c/td\u003e\n      \u003ctd\u003e(A, B) -\u003e A | B\u003c/td\u003e\n      \u003ctd\u003e\n        nullish coalescing operator.\n        The operator returns the left-hand side if it is defined and not null, otherwise the operator returns the right-hand side.\n      \u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd align=\"center\"\u003e_ ? _ : _\u003c/td\u003e\n      \u003ctd\u003e(bool, A, A) -\u003e A\u003c/td\u003e\n      \u003ctd\u003eternary conditional operator\u003c/td\u003e\n    \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\n### Predefined Variables\n\n|Variables|Description|\n|---|---|\n|vars|user-defined variables|\n|plugins|loaded plugins|\n|env|environment variables|\n|request|request data|\n|response|response data|\n|assert|assert functions|\n|steps|results of steps|\n\n### Predefined Functions\n\n\u003ctable\u003e\n  \u003cthead\u003e\n    \u003ctr\u003e\n      \u003cth\u003eFunction\u003c/th\u003e\n      \u003cth\u003eDescription\u003c/th\u003e\n      \u003cth\u003eExample\u003c/th\u003e\n    \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n    \u003ctr\u003e\n      \u003ctd\u003etype\u003c/td\u003e\n      \u003ctd\u003ereturns the abstract type of expression in string\u003c/td\u003e\n      \u003ctd\u003e\u003ccode\u003etype(0) == \"int\"\u003c/code\u003e\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003edefined\u003c/td\u003e\n      \u003ctd\u003etells whether a variable is defined or not\u003c/td\u003e\n      \u003ctd\u003e\u003ccode\u003edefined(a) ? a : b\u003c/code\u003e\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd rowspan=4\u003esize\u003c/td\u003e\n      \u003ctd\u003ereturns the string length\u003c/td\u003e\n      \u003ctd\u003e\u003ccode\u003esize(\"foo\")\u003c/code\u003e\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003ereturns the bytes length\u003c/td\u003e\n      \u003ctd\u003e\u003ccode\u003esize(bytes(\"foo\"))\u003c/code\u003e\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003ereturns the number of list elements\u003c/td\u003e\n      \u003ctd\u003e\u003ccode\u003esize(items)\u003c/code\u003e\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003ereturns the number of map elements\u003c/td\u003e\n      \u003ctd\u003e\u003ccode\u003esize(index)\u003c/code\u003e\u003c/td\u003e\n    \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\n## Plugin\n\nScenarigo has a plugin mechanism that enables you to add new functionalities you need by writing Go code.\nThis feature is based on [Go's standard library `plugin`](https://pkg.go.dev/plugin), which has the following limitations.\n\n- Supported on Linux, FreeBSD, and macOS only.\n- All plugins (and installed `scenarigo` command) must be built with the same version of the Go compiler and dependent packages.\n\nScenarigo loads built plugins at runtime and accesses any exported variable or function via [template string](#template-string).\n\nSee [the official document](https://pkg.go.dev/plugin) for details of the `plugin` package.\n\n### How to write plugins\n\nA Go plugin is a `main` package with **exported** variables and functions.\n\n```go main.go\npackage main\n\nimport \"time\"\n\nvar Layout = \"2006-01-02\"\n\nfunc Today() string {\n\treturn time.Now().Format(Layout)\n}\n\n```\n\nYou can use the variables and functions via template strings like below in your test scenarios.\n\n- `{{plugins.date.Layout}}` =\u003e `\"2006-01-02\"`\n- `{{plugins.date.Today()}}` =\u003e `\"2022-02-22\"`\n\nScenarigo allows functions to return a value or a value and an error. The template string execution will fail if the function returns a non-nil error.\n\n\n```go main.go\npackage main\n\nimport \"time\"\n\nvar Layout = \"2006-01-02\"\n\nfunc TodayIn(s string) (string, error) {\n\tloc, err := time.LoadLocation(s)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn time.Now().In(loc).Format(Layout), nil\n}\n```\n\n- `{{plugins.date.TodayIn(\"UTC\")}}` =\u003e `\"2022-02-22\"`\n- `{{plugins.date.TodayIn(\"INVALID\")}}` =\u003e `failed to execute: {{plugins.date.TodayIn(\"INVALID\")}}: unknown time zone INVALID`\n\n### How to build plugins\n\nGo plugin can be built with `go build -buildmode=plugin`, but we recommend you use `scenarigo plugin build` instead. The wrapper command requires `go` command installed in your machine. Scenarigo always builds plugins with the same go version that is used to build its own. Because of that, scenarigo add `toolchain` directive to the `go.mod` files of plugins.\n\nScenarigo builds plugins according to the configuration.\n\n```yaml scenarigo.yaml\nschemaVersion: config/v1\n\nscenarios:\n- scenarios\n\npluginDirectory: ./gen  # Specify the root directory of plugins.\nplugins:                # Specify configurations to build plugins.\n  date.so:              # Map keys specify plugin output file path from the root directory of plugins.\n    src: ./plugins/date # Specify the source file, directory, or \"go gettable\" module path of the plugin.\n```\n\n```shell\n.\n├── plugins\n│   └── date\n│       └── main.go\n├── scenarigo.yaml\n└── scenarios\n    └── echo.yaml\n```\n\nIn this case, the plugin will be built and written to `date.so`.\n\n```shell\n$ scenarigo plugin build\n```\n\n```shell\n.\n├── gen\n│   └── date.so     # built plugin\n├── plugins\n│   └── date\n│       ├── go.mod  # generated automatically if not exists\n│       └── main.go\n├── scenarigo.yaml\n└── scenarios\n    └── echo.yaml\n```\n\nScenarigo checks the dependent packages of each plugin before building. If the plugins depend on a different version of the same package, Scenarigo overrides `go.mod` files by the maximum version to avoid the build error.\n\nNow you can use the plugin in test scenarios.\n\n```yaml echo.yaml\ntitle: echo\nplugins:\n  date: date.so # relative path from \"pluginDirectory\"\nsteps:\n- title: POST /echo\n  protocol: http\n  request:\n    method: POST\n    url: 'http://{{env.ECHO_ADDR}}/echo'\n    body:\n      message: '{{plugins.date.Today()}}'\n  expect:\n    code: 200\n```\n\nScenarigo can download source codes from remote repositories and build it with [`go get`-able](https://go.dev/ref/mod#go-get) module query.\n\n```yaml scenarigo.yaml\nplugins:\n  uuid.so:\n    src: github.com/zoncoen-sample/scenarigo-plugins/uuid@latest\n```\n\n### Advanced features\n\n#### Setup Funciton\n\n[`plugin.RegisterSetup`](https://pkg.go.dev/github.com/zoncoen/scenarigo/plugin#RegisterSetup) registers a setup function that will be called before running scenario tests once only. If the registered function returns a non-nil function as a second returned value, it will be executed after finished all tests.\n\n```go main.go\npackage main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/zoncoen/scenarigo/plugin\"\n\n\tsecretmanager \"cloud.google.com/go/secretmanager/apiv1\"\n\tsecretmanagerpb \"google.golang.org/genproto/googleapis/cloud/secretmanager/v1\"\n)\n\nconst (\n\tprojectName = \"foo\"\n)\n\nfunc init() {\n\tplugin.RegisterSetup(setupClient)\n}\n\nvar client *secretmanager.Client\n\nfunc setupClient(ctx *plugin.Context) (*plugin.Context, func(*plugin.Context)) {\n\tvar err error\n\tclient, err = secretmanager.NewClient(context.Background())\n\tif err != nil {\n\t\tctx.Reporter().Fatalf(\"failed to create secretmanager client: %v\", err)\n\t}\n\treturn ctx, func(ctx *plugin.Context) {\n\t\tclient.Close()\n\t}\n}\n\nfunc GetSecretString(name string) (string, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)\n\tdefer cancel()\n\tresp, err := client.AccessSecretVersion(ctx, \u0026secretmanagerpb.AccessSecretVersionRequest{\n\t\tName: fmt.Sprintf(\"projects/%s/secrets/%s/versions/latest\", projectName, name),\n\t})\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get secret: %v\", err)\n\t}\n\treturn string(resp.Payload.Data), nil\n}\n```\n\n```yaml scenarigo.yaml\nplugins:\n  setup.so:\n    src: ./plugins/date # call \"setupClient\" before running test scenarios\n```\n\nSimilarly, [`plugin.RegisterSetupEachScenario`](https://pkg.go.dev/github.com/zoncoen/scenarigo/plugin#RegisterSetupEachScenario) can register a setup function. The registered function will be called before each test scenario that uses the plugin.\n\n```go main.go\npackage main\n\nimport (\n\t\"github.com/zoncoen/scenarigo/plugin\"\n\n\t\"github.com/google/uuid\"\n)\n\nfunc init() {\n\tplugin.RegisterSetupEachScenario(setRunID)\n}\n\nfunc setRunID(ctx *plugin.Context) (*plugin.Context, func(*plugin.Context)) {\n\treturn ctx.WithVars(map[string]string{\n\t\t\"runId\": uuid.NewString(),\n\t}), nil\n}\n```\n\n```yaml echo.yaml\ntitle: echo\nplugins:\n  setup: setup.so # call \"setRunID\" before running this test scenario\nsteps:\n- title: POST /echo\n  protocol: http\n  request:\n    method: POST\n    url: 'http://{{env.ECHO_ADDR}}/echo'\n    header:\n      Run-Id: '{{vars.runId}}'\n    body:\n      message: hello\n  expect:\n    code: 200\n```\n\n#### Custom Step Function\n\nGenerally, a `step` represents sending a request in Scenarigo. However, you can use a Go's function as a step with the plugin.\n\n```go main.go\npackage main\n\nimport (\n\t\"github.com/zoncoen/scenarigo/plugin\"\n\t\"github.com/zoncoen/scenarigo/schema\"\n)\n\nvar Nop = plugin.StepFunc(func(ctx *plugin.Context, step *schema.Step) *plugin.Context {\n\tctx.Reporter().Log(\"nop step\")\n\treturn ctx\n})\n```\n\n```yaml nop.yaml\ntitle: nop\nplugins:\n  step: step.so\nsteps:\n- title: nop step\n  ref: '{{plugins.step.Nop}}'\n```\n\n#### Left Arrow Function (a function takes arguments in YAML)\n\nScenarigo enables you to define a function that takes arguments in YAML for readability. It is called the \"Left Arrow Function\" since its syntax `{{funcName \u003c-}}`.\n\n```go main.go\npackage main\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/zoncoen/scenarigo/plugin\"\n)\n\nvar CoolFunc plugin.LeftArrowFunc = \u0026fn{}\n\ntype fn struct{}\n\ntype arg struct {\n\tFoo string `yaml:\"foo\"`\n\tBar string `yaml:\"bar\"`\n\tBaz string `yaml:\"baz\"`\n}\n\nfunc (_ *fn) UnmarshalArg(unmarshal func(interface{}) error) (interface{}, error) {\n\tvar a arg\n\tif err := unmarshal(\u0026a); err != nil {\n\t\treturn nil, err\n\t}\n\treturn \u0026a, nil\n}\n\nfunc (_ *fn) Exec(in interface{}) (interface{}, error) {\n\ta, ok := in.(*arg)\n\tif !ok {\n\t\treturn nil, errors.New(\"arg must be a arg\")\n\t}\n\treturn fmt.Sprintf(\"foo: %s, bar: %s, baz: %s\", a.Foo, a.Bar, a.Baz), nil\n}\n```\n\n```yaml echo.yaml\ntitle: echo\nplugins:\n  cool: cool.so\nsteps:\n- title: POST /echo\n  protocol: http\n  request:\n    method: POST\n    url: 'http://{{env.ECHO_ADDR}}/echo'\n    body:\n      message:\n        '{{plugins.cool.CoolFunc \u003c-}}':\n          foo: 1\n          bar: 2\n          baz: 3\n  expect:\n    code: 200\n```\n\n## ytt Integration (templating and overlays)\n\nScenarigo integrates [ytt](https://carvel.dev/ytt/) to provide flexible templating and overlay features for test scenarios. You can use this experimental feature by enabling it in `scenarigo.yaml`.\n\n```yaml scenarigo.yaml\ninput:\n  yaml:\n    ytt:\n      enabled: true\n```\n\n### Single File\n\nAll test scenarios are processed as ytt templates when the feature is enabled. For example, the following simple test scenario will set \"hello\" to the `message` field.\n\n```yaml simple.yaml\n#@ msg = \"hello\"\nschemaVersion: scenario/v1\ntitle: echo\nsteps:\n- title: POST /echo\n  protocol: http\n  request:\n    method: POST\n    url: http://example.com/echo\n    header:\n      Content-Type: application/json\n    body:\n      message: #@ msg\n  expect:\n    body:\n      message: \"{{request.body.message}}\"\n```\n\nYou can check the test scenarios generated by ytt integration with `scenarigo dump` sub-command.\n\n```shell\n$ scenarigo dump ./scenarios/simple.yaml\nschemaVersion: scenario/v1\ntitle: echo\nsteps:\n- title: POST /echo\n  protocol: http\n  request:\n    method: POST\n    url: http://example.com/echo\n    header:\n      Content-Type: application/json\n    body:\n      message: hello\n  expect:\n    body:\n      message: \"{{request.body.message}}\"\n```\n\n### Multiple File\n\n`ytt/v1` schema type file allows giving multiple ytt files.\n\n```yaml scenarios.yaml\n# This configuration equals the following command.\n# ytt -f template.ytt.yaml -f values.ytt.yaml\nschemaVersion: ytt/v1\nfiles:\n- template.ytt.yaml\n- values.ytt.yaml\n```\n\n```yaml template.ytt.yaml\n#@ load(\"@ytt:data\", \"data\")\n#@ for params in data.values:\n---\nschemaVersion: scenario/v1\nplugins:\n  plugin: plugin.so\ntitle: #@ params.title\nvars: #@ params.vars\nsteps:\n- title: #@ \"{} /{}\".format(params.request.method, params.request.path)\n  protocol: http\n  request:\n    method: #@ params.request.method\n    url: #@ \"http://example.com/{}\".format(params.request.path)\n    header:\n      Content-Type: application/json\n    body:\n      message: \"{{vars.message}}\"\n  expect: #@ params.expect\n#@ end\n```\n\n```yaml values.ytt.yaml\n#@data/values\n---\n- title: success\n  vars:\n    message: hello\n  request:\n    method: POST\n    path: echo\n  expect:\n    code: OK\n    body:\n      message: \"{{request.body.message}}\"\n\n- title: invalid method\n  vars:\n    message: hello\n  request:\n    method: GET\n    path: echo\n  expect:\n    code: Method Not Allowed\n\n- title: invalid path\n  vars:\n    message: hello\n  request:\n    method: POST\n    path: invalid\n  expect:\n    code: Not Found\n```\n\nThis example will run three test scenarios.\n\n```shell\n$ scenarigo dump ./scenarios/scenarios.yaml\nschemaVersion: scenario/v1\ntitle: success\nplugins:\n  plugin: plugin.so\nvars:\n  message: hello\nsteps:\n- title: POST /echo\n  protocol: http\n  request:\n    method: POST\n    url: http://example.com/echo\n    header:\n      Content-Type: application/json\n    body:\n      message: \"{{vars.message}}\"\n  expect:\n    code: OK\n    body:\n      message: \"{{request.body.message}}\"\n---\nschemaVersion: scenario/v1\ntitle: invalid method\nplugins:\n  plugin: plugin.so\nvars:\n  message: hello\nsteps:\n- title: GET /echo\n  protocol: http\n  request:\n    method: GET\n    url: http://example.com/echo\n    header:\n      Content-Type: application/json\n    body:\n      message: \"{{vars.message}}\"\n  expect:\n    code: Method Not Allowed\n---\nschemaVersion: scenario/v1\ntitle: invalid path\nplugins:\n  plugin: plugin.so\nvars:\n  message: hello\nsteps:\n- title: POST /invalid\n  protocol: http\n  request:\n    method: POST\n    url: http://example.com/invalid\n    header:\n      Content-Type: application/json\n    body:\n      message: \"{{vars.message}}\"\n  expect:\n    code: Not Found\n```\n\n:warning: You should exclude ytt files specified from `ytt/v1` type test scenarios by setting [regular expressions](https://pkg.go.dev/regexp/syntax#hdr-Syntax) to `excludes` field.\n\n```yaml scenarigo.yaml\ninput:\n  excludes:\n  - \\.ytt\\.yaml$\n  yaml:\n    ytt:\n      enabled: true\n```\n\n### Default ytt Files\n\nThe files set to `defaultFiles` field will be used to generate all test scenarios.\n\n```yaml scenarigo.yaml\ninput:\n  yaml:\n    ytt:\n      enabled: true\n      defaultFiles:\n      - default.yaml\n```\n\n```yaml default.yaml\n#@ load(\"@ytt:overlay\", \"overlay\")\n#@overlay/match by=overlay.map_key(\"schemaVersion\"), expects=\"0+\"\n---\nschemaVersion: scenario/v1\nsteps:\n#@overlay/match by=overlay.all, expects=\"0+\"\n-\n  #@overlay/match when=0\n  timeout: 30s\n```\n\nThis example set 30 sec. as the default timeout for all test scenarios.\n\n```shell\n$ scenarigo dump ./scenarios/simple.yaml\nschemaVersion: scenario/v1\ntitle: echo\nplugins:\n  plugin: plugin.so\nsteps:\n- title: POST /echo\n  protocol: http\n  request:\n    method: POST\n    url: http://{{plugins.plugin.ServerAddr}}/echo\n    header:\n      Content-Type: application/json\n    body:\n      message: hello\n  expect:\n    body:\n      message: \"{{request.body.message}}\"\n  timeout: 30s\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzoncoen%2Fscenarigo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzoncoen%2Fscenarigo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzoncoen%2Fscenarigo/lists"}