From f74f5c71255e25916e3fff635507eea168ad9f98 Mon Sep 17 00:00:00 2001 From: Josh Klar Date: Wed, 16 Feb 2022 11:21:48 -0800 Subject: [PATCH] docs: manual pages and helper scripts to read them --- .gitignore | 9 ++ Makefile | 19 +++ README.md | 22 ++- manual/seatrial.1.scd | 4 +- manual/seatrial.5.scd | 302 ++++++++++++++++++++++++++++++++++++++ manual/seatrial.lua.3.scd | 103 +++++++++++++ read_manual_page.sh | 28 ++++ 7 files changed, 478 insertions(+), 9 deletions(-) create mode 100644 Makefile create mode 100644 manual/seatrial.5.scd create mode 100644 manual/seatrial.lua.3.scd create mode 100755 read_manual_page.sh diff --git a/.gitignore b/.gitignore index ea8c4bf..60ddd9e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,10 @@ +# keep in sync with .dockerignore where appropriate + /target +*.1 +*.2 +*.3 +*.4 +*.5 +*.6 +*.7 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..07b03a4 --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +.POSIX: + +.SUFFIXES: .1.scd .1 .3.scd .3 .5.scd .5 + +# teach make how to build man pages (thanks section 1.11 of +# http://khmere.com/freebsd_book/html/ch01.html) +.1.scd.1: + scdoc < $< > $@ || (rm -f $@; exit 1) +.3.scd.3: + scdoc < $< > $@ || (rm -f $@; exit 1) +.5.scd.5: + scdoc < $< > $@ || (rm -f $@; exit 1) + +.PHONY: clean +clean: + rm -rf manual/*.1 manual/*.3 manual/*.5 + +doc: manual +manual: manual/seatrial.1 manual/seatrial.5 manual/seatrial.lua.3 diff --git a/README.md b/README.md index e220818..b6c92c3 100644 --- a/README.md +++ b/README.md @@ -18,13 +18,6 @@ indeed, for helping developers make such scale events, Non-Events). ## Usage -> For further detail and commentary, see `man 1 seatrial`, or -> `manual/seatrial.1.scd` in the source tree. While you're at it, there's also the -> following other manual pages, accessible via the same pattern: -> -> - `seatrial(5)` -> - `seatrial.lua(3)` - ``` Usage: seatrial [] [-m ] @@ -41,6 +34,21 @@ Options: --help display usage information ``` +Further detail, commentary, API documentation, etc. are provided in scdoc +format in the source repo, and in Unix manual page format in installed copies +of `seatrial`. + +- `seatrial(1)` documents the CLI application itself +- `seatrial(5)` documents the configuration format +- `seatrial.lua(3)` documents the Lua API to implement dynamic values and + validators + +In an installed copy of `seatrial`, including the Docker container, these are +accessible with `man
`, for example `man 3 seatrial.lua` or +`docker run --rm -it man 3 seatrial.lua`. From this source +tree, provided [scdoc](https://git.sr.ht/~sircmpwn/scdoc) is installed, you can +instead run, for example, `./read_manual_page 'seatrial.lua(3)'`. + ## Development and Packaging Minimum Supported Rust Version is 1.58, as specified in `Cargo.toml`. diff --git a/manual/seatrial.1.scd b/manual/seatrial.1.scd index 50c8c2a..d809317 100644 --- a/manual/seatrial.1.scd +++ b/manual/seatrial.1.scd @@ -70,8 +70,8 @@ writing, such functionality is not yet concretely planned. # SEE ALSO -*seatrial(5)*++ -*seatrial.lua(3)* +- *seatrial(5)* +- *seatrial.lua(3)* # AUTHORS diff --git a/manual/seatrial.5.scd b/manual/seatrial.5.scd new file mode 100644 index 0000000..26ddc68 --- /dev/null +++ b/manual/seatrial.5.scd @@ -0,0 +1,302 @@ +seatrial(5) "https://engineering.dockwa.com" "situational-mock-based load testing" + +# SYNOPSIS + +*seatrial(1)* is configured with one declarative, plaintext, RON-formatted file +per Situation to be tested. This manual page provides a quick summary of RON +(Rusty Object Notation - further reading in _SEE ALSO_ below) syntax and a +walkthrough of the fields that make up a *seatrial* Situation config. + +# RON SYNTAX + +- _structs_ are denoted by an optional name followed by parentheses, and contain + positional or named arguments +- _tuples_ look almost identical to _structs_, but have no preceeding name, and + cannot contain named arguments +- _maps_ are denoted by curly braces, and contain key-value pairs, where the + keys are _strings_ (see below) and the values are of whatever type is + appropriate in the context +- _arrays_ are denoted by brackets; the values within are of whatever type is + appropriate in the context +- _strings_ are double-quoted +- _booleans_ are one of the following literal values: + - true + - false +- _numbers_ follow standard integer and float formatting rules as one would + expect, plus support for 0x and 0b prefixes, for hex and binary numbers, + respectively + +# SEATRIAL CONFIGURATION FORMAT + +*seatrial* configurations are an unnamed struct, and thus, the entire config +will be wrapped in parentheses (this does mean all useful data is indented a +level if formatted in the typical RON style seen in the wild, but the same is +largely true of JSON anyway). + +## lua_file + +While optional in the data model, _lua\_file_ is currently a +functionally-required key in the configuration, containing a string path, relative to +the situation file, where the Lua user script (perhaps using *seatrial.lua(3)* +APIs) can be found. This file, when executed in a Lua interpreter, must return a +table with string keys and function values to be used elsewhere in the pipeline. +More on Lua interactions later. For an example of a _lua\_file_, see the +_examples/_ directory in the source tree. + +## grunts + +_grunts_ is an array of Grunts, *seatrial*'s tongue-in-cheek name for simulated +users. Grunts are simple creatures: they follow the rules provided in their +_persona_ (a _Persona_ struct as described below), and additionally have a +_base\_name_ (a string) and a _count_ (an integer), which together with the +global _multiplier_ (see *seatrial(1)*), determines how many of this Grunt +should be created. + +## Persona + +_personas_ describe the actions a Grunt will take, and a few other +characteristics regarding how HTTP requests will be made. They are defined in an +anonymous struct as follows: + +- _timeout_ is one of the following enum members, and describes the *overall* + timeout that will be applied to HTTP requests within the Persona: + - _Seconds()_ + - _Milliseconds() +- _headers_ is a map of strings to _References_, described below +- _sequence_ is an array of _Actions_, described below + +## Persona: References + +_References_ are found as the type of numerous _Action_ arguments, as well as +_Persona.headers_ described above. They are enums, and take one of the following +forms: + +- _Value()_ hard-codes a string value, and is often useful for query + parameters, headers, etc. that can be known *statically* (meaning no Lua is + needed to calculate their value). Hard-coded string _Values_ will *always* be + more performant than anything that needs to cross the *seatrial*-Lua boundary, + and thus should be used whenever possible. + +- _LuaValue_ plucks the Lua value returned by the last step in the pipeline, + stringifies it, and passes that stringified value to the caller. This is a + fairly niche state, useful when only one parameter/header/etc. in an HTTP + request needs to be dynamic. If the data in the pipe is not a Lua value, doesn't + exist to begin with, or is not stringifiable (notably, functions, tables, and + nil will never be stringified), a fatal error will be thrown and the Grunt will + stop execution. + +- _LuaTableIndex()_ and _LuaTableValue()_ pluck a value from + the Lua table returned by the last step in the pipeline by either its numeric + index or its string key, as appropriate. This is almost always the most useful + way to plumb dynamic data to an HTTP request. If the data in the pipe is not a + Lua table, doesn't exist to begin with, if the specified key doesn't exist in + the table, or if the key resolves to a value that cannot be stringified + (notably, functions, tables, and nil will never be stringified), a fatal error + will be thrown and the Grunt will stop execution. + +## Persona: Actions + +_Actions_ are the proverbial meat and potatoes of *seatrial*, although +properly-seasoned tofu and potatoes also makes for an excellent meal and is +recommended. Each Action in a Sequence *may, optionally*, populate data in the +pipeline for the next step to read. The steps that do so are documented as such +below. Actions are named enum members in RON syntax terms, though some are +double-enumed (think of this as namespacing, perhaps), which is an +implementation detail that may change in a future version (deprecation notices +will be provided). + +- _ControlFlow()_ is a namespace containing only one action: + - _ControlFlow(GoTo(index: , max_times: ))_ jumps + to the specified index in the pipeline, presuming it exists, allowing for + looping and/or skipping of steps. If _max\_times_ is specified, it serves as + an end to the loop after that number of arrivals at _GoTo_ + +- _Http()_ is a namespace containing the following actions: + - _Http(Delete())_ + - _Http(Get())_ + - _Http(Head())_ + - _Http(Post())_ + - _Http(Put())_ + + Each of these take the same _args_, of which _url_ is required, and the rest + are all optional: + + - _url_ is a string containing the relative (to the _base\_url_ provided + at the CLI; see *seatrial(1)*) path to send the request to + + - _body_ is a _Reference_ containing the request body. Currently + non-string bodies are relatively untested, and thus not fully defined, + behavior (this is a known issue in *seatrial*) + + - _headers_ is a map of strings to _References_ containing HTTP headers. + These are merged with (and in the event of a collision, will override) + the _headers_ of the Persona. + + - _params_ is a map of strings to _References_ containing HTTP query + parameters (which are ultimately passed as part of the URL). + + - _timeout_ follows the same rules as _Persona.timeout_ and will + override the Persona-provided timeout for this request. + + A successful request with *any* status code (not just a 2xx) will be placed + in the pipe for the next step to read (for details on how to access this + from a Lua function, see _LuaFunction_ below). Failures (perhaps due to + timeout, or due to some other system-level failure, like a socket issue) + will immediately end Sequence and Grunt execution, and terminate the thread. + +- _LuaFunction()_ runs the specified Lua function, and places the return + value on the stack. While its implementation in *seatrial* is consistent, it's + also context-specific, so both usecases are documented below. If the Lua + function raises an exception or cannot be found, Grunt execution stops + immediately. + + If the data in the pipe is an HTTP request (either by way of this + _LuaFunction_ call being the sole step following an Http(\*) step, or as + part of a _Combinator_, described below), the function will be called with a + table as the sole argument, containing the following properties. This + usecase is almost exclusively useful for _Validators_; the requirements of + _LuaFunction_ in a _Validator_ context is described in _Validator_ below. + + - _body_, a table of 8-bit integers representing the raw bytes of the + response body. This will always exist, albeit potentially with a table + length of 0. + + - _body\_string_, which will be _nil_ if the body was not parseable as a + UTF-8 string (no other encodings are supported; it's 2022 at time of + writing, use UTF-8 or provide a Lua library to handle other + encodings), or will be the UTF-8 string the body provided, with no + further processing. If the body is, say, JSON, this is the field that + is likely most useful to libraries like _json.lua_ (see _SEE ALSO_ + below). + + - _content\_type_, a string which contains the Content-Type as dictated + by the returned headers + + - _headers_, a table of strings to strings that is unmodified from + whatever the server returned in the response headers + + - _status\_code_, a 16-bit integer containing the status code as + provided by the server + + If there is no data, or any other type of data, in the pipe, consider the + argument, if any (depending on the version of *seatrial* you have) passed to + the function to be undefined behavior, unstable, and unusable. In this case, + the _LuaFunction_ is likely being used to generate values for future steps + in the Sequence, and should be (within Lua, at least) self-sufficient. + +- _Combinator(Combinator([Validator, ...]))_ is a namespace containing three + actions, which allow running multiple _Validators_ (see below) over the same + pipe data: + + - _AllOf([Validator, ...])_ runs each listed validator in sequence and + requires that all of them return with an _Ok_ or _OkWithWarnings_ + status. If any return with an _Error_ status, execution stops immediately + for the entire persona's pipeline. + + - _AnyOf([Validator, ...])_ runs each listed validator in sequence and + requires that one of them return with an _Ok_ or _OkWithWarnings_ status. + This combinator ends once it finds such a status, or fails the entire + pipeline and persona if all listed validators return _Error_. + + - _NoneOf([Validator, ...])_ runs each listed validator in sequence and + expects that all of them return an _Error_ status. Immediately upon finding + an _Ok_ or _OkWithWarnings_ status, the pipeline is failed. + + +- _Validator()_ (namespace prefix not allowed in a combinator array; + this is an implementation detail that bleeds through to the UX, sorry) allows + validation of data in the pipe; currently all _Validators_ work on HTTP + responses only. In general, they have one of two prefixes: _WarnUnless_, which + return _Ok_ or _OkWithWarnings_, and _Assert_, which return _Ok_ or immediately + fail the pipeline on _Error_ (barring interactions with _Combinators_, see + above). Further, _LuaFunction_ is a valid _Validator_; functions receive the + data in the pipe as their first argument (see _LuaFunction_ above) and must + return a _ValidationResult_ object as documented in *seatrial.lua(3)*. An + example _LuaFunction_ validator is provided in + _examples/simpleish/seatrial.lua_ in the *seatrial* source tree. + + All of the following have _Assert..._ and _WarnUnless..._ forms, so only the + shared parts of their names are listed here for brevity. For example, + _HeaderEquals_ should be expanded to _AssertHeaderEquals_ when writing config + files. + + - _HeaderEquals(, )_ takes a case-insensitive header + name and case-sensitive expected value + + - _HeaderExists(_ takes a case-insensitive header name, and simply + checks whether it is present in the response at all + + - _StatusCode()_ takes a 16-bit unsigned integer and checks whether the + HTTP status code exactly matches + + - _StatusCodeInRange(, )_ takes two 16-bit unsigned integers and + checks whether the HTTP status code is greater than or equal two the + first, *and* is less than or equal to the second + +# EXAMPLE + +Further examples are provided in the _examples/_ directory in the source tree. + +``` +( + lua_file: "seatrial.lua", + + grunts: [ + ( + base_name: "Postmaster General", + count: 1, + persona: ( + timeout: Seconds(30), + sequence: [ + LuaFunction("generate_profile"), + Http(Post( + url: "/profile", + body: LuaTableValue("profile"), + headers: { "Content-Type": Value("application/json") }, + )), + Combinator(AllOf([ + WarnUnlessStatusCodeInRange(200, 299), + WarnUnlessHeaderExists("X-Never-Gonna-Give-You-Up"), + ])) + ] + ), + ), + ( + base_name: "Reloader Grunt", + count: 10, + persona: ( + timeout: Seconds(30), + + sequence: [ + LuaFunction("generate_30_day_range"), + Http(Get( + url: "/calendar", + params: { + "start_date": LuaTableValue("start_date"), + "end_date": LuaTableValue("end_date"), + }, + )), + Combinator(AllOf([ + WarnUnlessStatusCodeInRange(200, 299), + WarnUnlessHeaderExists("X-Never-Gonna-Give-You-Up"), + LuaFunction("is_valid_esoteric_format"), + ])), + ControlFlow(GoTo(index: 0, max_times: 2)), + ], + ), + ), + ], +) +``` + +# SEE ALSO + +- *seatrial(1)* +- *seatrial.lua(3)* +- https://github.com/ron-rs/ron +- https://github.com/rxi/json.lua + +# AUTHORS + +Built by Dockwa Engineering. Sources can be found at +https://github.com/dockwa/seatrial. diff --git a/manual/seatrial.lua.3.scd b/manual/seatrial.lua.3.scd new file mode 100644 index 0000000..58bc639 --- /dev/null +++ b/manual/seatrial.lua.3.scd @@ -0,0 +1,103 @@ +seatrial.lua(3) "https://engineering.dockwa.com" "situational-mock-based load testing" + +# SYNOPSIS + +*seatrial* user-facing Lua API for dynamic pipeline values + +# DESCRIPTION + +*seatrial(1)* is generally configured in a declarative style in RON format, as +described in *seatrial(5)*. However, sometimes it's useful to generate values +dynamically at runtime (perhaps, today's date), validate responses in some way +*seatrial* doesn't provide first-class instructions for (perhaps most notably, +anything involving request bodies currently requires Lua instrumentation), or +perhaps even a combination thereof (generating a session token in sequence step +1 which is then referred to in the remainder of the grunt's lifetime, preserved +in a Lua global variable across sequence loops). The semantics of how to refer +to Lua files and values generated therein are described in *seatrial(5)*; this +manual page describes the "standard library", or perhaps more accurately, +utility belt, such Lua code has access to, above and beyond the Lua standard +library itself. + +Each top-level value is accessible by their bare name in Lua code as global +variables - in other words, the _ValidationResult_ section describes a Lua table +accessible as _ValidationResult_ directly, and its members, for example, +_ValidationResult.Ok()_. + +# ValidationResult + +_ValidationResult_ is a fairly-direct Lua mapping of a Rust enum by the same +name, having three function members: _Ok_, _OkWithWarnings_, and _Error_. A +table returned by one of these three functions is the *only* allowed response +from a _LuaFunction_ validator, notably, *nil (as is the default return value +from a Lua function) is never allowed as a validator return value*. + +All _ValidationResult_ members currently empty the pipe, leaving nothing for the +next step to read. This may change in a future version of *seatrial*. + +## ValidationResult.Ok() + +Used to denote that a validation succeeded, and will allow the pipeline to +proceed to the next step. + +``` +function noop_validator() + return ValidationResult.Ok() +end +``` + +## ValidationResult.OkWithWarnings(warnings) + +Accepts a list (table) of warning strings. Used to denote that a validation +succeeded, but non-fatal warnings were emitted. This still allows the pipeline +to proceed to the next step, but the warnings will be logged. + +``` +function warn_validator(response) + if response.body_string:match("stars") == nil then + return ValidationResult.OkWithWarnings({ "expected ratings API to discuss stars" }) + end + + return ValidationResult.Ok() +end +``` + +## ValidationResult.Error(err) + +Accepts an error message as a string. Used to denote that a validation fatally +failed, and that the pipeline should stop here (unless otherwise overridden by a +_Combinator_, see *seatrial(5)*). + +``` +function err_validator(response) + if response.body_string:match("stars") == nil then + return ValidationResult.Error("expected ratings API to discuss stars") + end + + return ValidationResult.Ok() +end +``` + +# IMPLEMENTATION NOTES + +## LIFECYCLES AND LIFETIMES + +The Lua VM for a Grunt is initialized exactly once, when the Grunt's thread is +started. Thus, global variables (be them seatrial-borne, such as with the API +described in this manual, or user-generated data from the pipeline stored in a +global) live until the end of the pipeline and thus thread, or until garbage +collected by the usual Lua means. This makes Lua globals an +untested-but-theoretically-sane place to stash dynamic data that may be +necessary in later pipeline runs (perhaps a session token returned by an +authentication API). First class support for non-pipe-aligned data flows +(perhaps a simple key-value store) is being considered, but has no ETA. + +# SEE ALSO + +- *seatrial(1)* +- *seatrial(5)* + +# AUTHORS + +Built by Dockwa Engineering. Sources can be found at +https://github.com/dockwa/seatrial. diff --git a/read_manual_page.sh b/read_manual_page.sh new file mode 100755 index 0000000..74006f5 --- /dev/null +++ b/read_manual_page.sh @@ -0,0 +1,28 @@ +#!/bin/sh + +set -eu + +# read_manual_page.sh colloquial_name(section) +# +# ex: read_manual_page.sh seatrial.lua(3) + +die() { + message="${1:-died without an error message, this is a bug in the script}" + code="${2:-1}" + + echo "${message}" >&2 + exit "${code}" +} + +hash scdoc 2>/dev/null || die "rendering manual pages requires scdoc to be installed (https://git.sr.ht/~sircmpwn/scdoc)" +hash man 2>/dev/null || die "rendering manual pages requires man(1) to be in your PATH, perhaps install man, mandoc, or similar" + +# given "seatrial.lua(3)", return "manual/seatrial.lua.3.scd" +source_path_from_user_input() { + echo "${1}" | awk -F"[()]" '{print "manual/" $1 "." $2 ".scd"}' +} + +MANUAL_PAGE_SOURCE=$(source_path_from_user_input "${1}") +MANUAL_PAGE_RENDERED=$(echo "${MANUAL_PAGE_SOURCE}" | sed "s#\.scd\$##") +scdoc < "${MANUAL_PAGE_SOURCE}" > "${MANUAL_PAGE_RENDERED}"|| die "failed to render manual page ${MANUAL_PAGE_SOURCE}" +exec man "${MANUAL_PAGE_RENDERED}"