From 11a66ea5230a9d365f00766d150e2c82ae162411 Mon Sep 17 00:00:00 2001 From: Josh Klar Date: Fri, 7 Jan 2022 23:13:38 -0800 Subject: [PATCH 01/14] WIP (rebase this away): checkpoint for weekend `cargo run -- 'http://localhost:9020' examples/simpleish/seatrial.ron` spins up dummy threads, but otherwise fully parses the config file into the format we actually want for each grunt's worker thread. pipeline *definitions* are implemented, their backing implementations are not, not network requests happen yet --- .gitignore | 1 + .rustfmt.toml | 1 + Cargo.lock | 513 ++++++++++++++++++++++++++++++++ Cargo.toml | 13 + README.md | 10 + examples/simpleish/seatrial.ron | 32 ++ src/main.rs | 421 ++++++++++++++++++++++++++ 7 files changed, 991 insertions(+) create mode 100644 .gitignore create mode 100644 .rustfmt.toml create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 examples/simpleish/seatrial.ron create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..3a26366 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1 @@ +edition = "2021" diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..380edf4 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,513 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "argh" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb41d85d92dfab96cb95ab023c265c5e4261bb956c0fb49ca06d90c570f1958" +dependencies = [ + "argh_derive", + "argh_shared", +] + +[[package]] +name = "argh_derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be69f70ef5497dd6ab331a50bd95c6ac6b8f7f17a7967838332743fbd58dc3b5" +dependencies = [ + "argh_shared", + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "argh_shared" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6f8c380fa28aa1b36107cd97f0196474bb7241bb95a453c5c01a15ac74b2eac" + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "memchr", +] + +[[package]] +name = "bumpalo" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" + +[[package]] +name = "cc" +version = "1.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chunked_transfer" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" + +[[package]] +name = "crc32fast" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "738c290dfaea84fc1ca15ad9c168d083b05a714e1efddd8edaab678dc28d2836" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "flate2" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f" +dependencies = [ + "cfg-if", + "crc32fast", + "libc", + "miniz_oxide", +] + +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "js-sys" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + +[[package]] +name = "nanoserde" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2786e3e6331deef0ed595643fb9266686369917bdf99b4169701399c9d262868" +dependencies = [ + "nanoserde-derive", +] + +[[package]] +name = "nanoserde-derive" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290eec6719d68aef1f5ca0e695f8ad6421adcf8791fc17641c4ccc6c5388fb39" + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "proc-macro2" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47aa80447ce4daf1717500037052af176af5d38cc3e571d9ec1c7353fc10c87d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "rlua" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d76c08e66e69d182a41ff735a0931ecf1879de3552c6989c65a8268ee95b19" +dependencies = [ + "bitflags", + "bstr", + "cc", + "libc", + "num-traits", +] + +[[package]] +name = "rustls" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d37e5e2290f3e040b594b1a9e04377c2c671f1a1cfd9bfdef82106ac1c113f84" +dependencies = [ + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "seatrial" +version = "0.1.0" +dependencies = [ + "argh", + "nanoserde", + "rlua", + "strfmt", + "ureq", + "url", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "strfmt" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b278b244ef7aa5852b277f52dd0c6cac3a109919e1f6d699adde63251227a30f" + +[[package]] +name = "syn" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a684ac3dcd8913827e18cd09a68384ee66c1de24157e3c556c9ab16d85695fb7" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "tinyvec" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "unicode-bidi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "ureq" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9399fa2f927a3d327187cbd201480cee55bee6ac5d3c77dd27f0c6814cff16d5" +dependencies = [ + "base64", + "chunked_transfer", + "flate2", + "log", + "once_cell", + "rustls", + "url", + "webpki", + "webpki-roots", +] + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" + +[[package]] +name = "web-sys" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552ceb903e957524388c4d3475725ff2c8b7960922063af6ce53c9a43da07449" +dependencies = [ + "webpki", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..b380475 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "seatrial" +version = "0.1.0" +edition = "2021" +rust-version = "1.57" + +[dependencies] +argh = "0.1" +nanoserde = "0.1" +rlua = "0.18" +strfmt = "0.1" +ureq = "2.4" +url = "2.2" diff --git a/README.md b/README.md index eec4054..4d725dd 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,16 @@ optimized for taking historical learnings plus data gleamed from the rest of your observability stack, and preventing repeats of the same outages (and indeed, for helping developers make such scale events, Non-Events). +## Usage + +- fill +- this +- in + +## Development and Packaging + +> MSRV: 1.57, as specified in `Cargo.toml` + ## Legal (c) 2022 The Wanderlust Group, All Rights Reserved diff --git a/examples/simpleish/seatrial.ron b/examples/simpleish/seatrial.ron new file mode 100644 index 0000000..5d9386f --- /dev/null +++ b/examples/simpleish/seatrial.ron @@ -0,0 +1,32 @@ +( + grunts: [ + ( + base_name: "Reloader Grunt", + persona: "spam_reloader", + count: 10, + ), + ], + + personas: { + "spam_reloader": ( + timeout: Seconds(30), + + pipeline: [ + LuaFunction("generate_30_day_range"), + Get( + url: "/calendar", + params: { + "start_date": LuaTableValue("start_date"), + "end_date": LuaTableValue("end_date"), + }, + ), + AllOf([ + WarnUnlessStatusCodeInRange(200, 299), + WarnUnlessHeaderExists("X-Never-Gonna-Give-You-Up"), + LuaFunction("was_valid_esoteric_format"), + ]), + GoTo(index: 0), + ], + ), + }, +) diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..b285952 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,421 @@ +use argh::FromArgs; +use nanoserde::{DeRon, DeRonErr}; +use rlua::Lua; +use ureq::AgentBuilder; +use url::Url; + +use std::collections::HashMap; +use std::fs::{canonicalize, read_to_string}; +use std::path::PathBuf; +use std::str::FromStr; +use std::sync::{mpsc, Arc, Barrier}; +use std::thread; +use std::thread::JoinHandle; +use std::time::Duration; + +/// situational-mock-based load testing +#[derive(FromArgs)] +struct CmdArgs { + /// integral multiplier for grunt counts (minimum 1) + #[argh(option, short = 'm', default = "1")] + multiplier: usize, + + /// base URL for all situations in this run + #[argh(positional)] + base_url: String, + + // work around https://github.com/google/argh/issues/13 wherein repeatable positional arguments + // (situations, in this struct) allow any vec length 0+, where we require a vec length 1+. this + // could be hacked around with some From magic and a custom Vec, but this is more + // straightforward + /// path to a RON file in seatrial(5) situation config format + #[argh(positional)] + req_situation: SituationSpec, + + /// optional paths to additional RON files in seatrial(5) situation config format + #[argh(positional)] + situations: Vec, +} + +// built out of a SituationSpec after post-parse contextual validations have been run +#[derive(Clone, Debug)] +struct Situation { + base_url: Url, + lua_file: Option, + grunts: Vec, + personas: Vec, +} + +impl Situation { + fn from_spec( + spec: &SituationSpec, + base_url: &Url, + grunt_multiplier: usize, + ) -> Result { + let mut relocated_personas: HashMap<&str, usize> = + HashMap::with_capacity(spec.personas.len()); + let personas = spec + .personas + .iter() + .enumerate() + .map(|(idx, (name, spec))| { + relocated_personas.insert(name, idx); + + Persona { + name: name.clone(), + spec: spec.clone(), + + // TODO: populate with Value/LuaFunction returns, error on other PipelineAction + // variants + headers: HashMap::new(), + } + }) + .collect(); + let grunts = { + let mut slot: usize = 0; + let mut grunts: Vec = Vec::with_capacity( + spec.grunts + .iter() + .map(|grunt| grunt.real_count() * grunt_multiplier) + .sum(), + ); + + for (idx, grunt_spec) in spec.grunts.iter().enumerate() { + let num_grunts = grunt_spec.real_count(); + if num_grunts < 1 { + return Err(SituationParseErr { + kind: SituationParseErrKind::Semantics { + message: "if provided, grunt count must be >=1".into(), + location: format!("grunts[{}]", idx), + }, + }); + } + let num_grunts = num_grunts * grunt_multiplier; + + match relocated_personas.get(&*grunt_spec.persona) { + Some(persona_idx) => { + for _ in 0..num_grunts { + grunts.push(Grunt { + name: grunt_spec.formatted_name(slot), + persona_idx: *persona_idx, + }); + slot += 1; + } + } + None => { + return Err(SituationParseErr { + kind: SituationParseErrKind::Semantics { + message: format!( + "grunt refers to non-existent persona \"{}\"", + grunt_spec.persona + ), + location: format!("grunts[{}]", idx), + }, + }); + } + } + } + + grunts + }; + + Ok(Self { + base_url: base_url.clone(), + lua_file: spec + .lua_file + .as_deref() + .and_then(|file| canonicalize(file).map(Some).unwrap_or(None)), + grunts, + personas, + }) + } +} + +#[derive(Clone, Debug, DeRon)] +struct SituationSpec { + lua_file: Option, + grunts: Vec, + personas: HashMap, +} + +// build out of a GruntSpec during Situation construction +#[derive(Clone, Debug)] +struct Grunt { + name: String, + persona_idx: usize, +} + +#[derive(Clone, Debug, DeRon)] +struct GruntSpec { + base_name: Option, + persona: String, + count: Option, +} + +impl GruntSpec { + pub fn formatted_name(&self, uniqueness: impl std::fmt::Display) -> String { + format!( + "{} {}", + self.base_name + .clone() + .unwrap_or_else(|| format!("Grunt<{}>", self.persona)), + uniqueness, + ) + } + + pub fn real_count(&self) -> usize { + self.count.unwrap_or(1) + } +} + +// built out of a PersonaSpec during Situation construction +#[derive(Clone, Debug)] +struct Persona { + name: String, + spec: PersonaSpec, + headers: HashMap, +} + +#[derive(Clone, Debug, DeRon)] +struct PersonaSpec { + timeout: ConfigDuration, + headers: Option>, + pipeline: Vec, +} + +#[derive(Clone, Debug, DeRon)] +enum ConfigDuration { + Milliseconds(u64), + Seconds(u64), +} + +impl From<&ConfigDuration> for Duration { + fn from(src: &ConfigDuration) -> Self { + match src { + ConfigDuration::Milliseconds(ms) => Duration::from_millis(*ms), + ConfigDuration::Seconds(ms) => Duration::from_secs(*ms), + } + } +} + +#[derive(Clone, Debug, DeRon)] +enum PipelineAction { + GoTo { + index: usize, + max_times: Option, + }, + // this is mostly used for URL params, since those _can_ come from Lua, and thus have to be a + // PipelineAction member + Value(String), + + // http verbs. this section could be fewer LOC with macros eg + // https://stackoverflow.com/a/37007315/17630058, but (1) this is still manageable (there's + // only a few HTTP verbs), and (2) rust macros are cryptic enough to a passer-by that if we're + // going to introduce them and their mental overhead to this codebase (other than depending on + // a few from crates), we should have a strong reason (and perhaps multiple usecases). + + // TODO: figure out what, if anything, are appropriate guardrails for a PATCH verb + Delete { + url: String, + headers: Option>, + params: Option>, + timeout_ms: Option, + }, + Get { + url: String, + headers: Option>, + params: Option>, + timeout_ms: Option, + }, + Head { + url: String, + headers: Option>, + params: Option>, + timeout_ms: Option, + }, + Post { + url: String, + headers: Option>, + params: Option>, + timeout_ms: Option, + }, + Put { + url: String, + headers: Option>, + params: Option>, + timeout_ms: Option, + }, + + // validations of whatever the current thing in the pipe is. Asserts are generally fatal when + // falsey, except in the context of an AnyOf or NoneOf combinator, which can "catch" the errors + // as appropriate. WarnUnless validations are never fatal and likewise can never fail a + // combinator + AssertHeaderExists(String), + AssertStatusCode(u16), + AssertStatusCodeInRange(u16, u16), + WarnUnlessHeaderExists(String), + WarnUnlessStatusCode(u16), + WarnUnlessStatusCodeInRange(u16, u16), + + // basic logic. rust doesn't allow something like + // All(AssertStatusCode|AssertStatusCodeInRange), so instead, **any** PipelineAction is a valid + // member of a combinator for now, which is less than ideal ergonomically to say the least + AllOf(Vec), + AnyOf(Vec), + NoneOf(Vec), + + // the "Here Be Dragons" section, for when dynamism is absolutely needed: an escape hatch to + // Lua. TODO: document the Lua APIs and semantics... + LuaFunction(String), + LuaValue, + LuaTableIndex(usize), + LuaTableValue(String), +} + +#[derive(Debug)] +struct SituationParseErr { + kind: SituationParseErrKind, +} + +#[derive(Debug)] +enum SituationParseErrKind { + IO(std::io::Error), + Parsing(DeRonErr), + Semantics { + message: String, + location: String, // should we try to refer back to line numbers in the config somehow? + }, +} + +impl SituationParseErr { + pub fn message(&self) -> String { + match &self.kind { + SituationParseErrKind::IO(err) => err.to_string(), + SituationParseErrKind::Parsing(err) => err.to_string(), + SituationParseErrKind::Semantics { message, .. } => message.clone(), + } + } +} + +impl std::fmt::Display for SituationParseErr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "SituationParseErr: {}", self.message()) + } +} + +impl From for SituationParseErr { + fn from(src: std::io::Error) -> Self { + Self { + kind: SituationParseErrKind::IO(src), + } + } +} + +impl From for SituationParseErr { + fn from(src: DeRonErr) -> Self { + Self { + kind: SituationParseErrKind::Parsing(src), + } + } +} + +impl FromStr for SituationSpec { + type Err = SituationParseErr; + + fn from_str(it: &str) -> Result { + Ok(DeRon::deserialize_ron(&read_to_string(canonicalize(it)?)?)?) + } +} + +fn main() -> std::io::Result<()> { + let args = { + let mut args: CmdArgs = argh::from_env(); + args.situations.insert(0, args.req_situation.clone()); + args + }; + + // TODO: no unwrap, which will also kill the nasty parens + let base_url = (if args.base_url.ends_with('/') { + Url::from_str(&args.base_url) + } else { + Url::from_str(&format!("{}/", args.base_url)) + }) + .unwrap(); + + // TODO: get rid of unwrap! + let situations: Vec> = args + .situations + .iter() + .map(|situation| { + Arc::new(Situation::from_spec(situation, &base_url, args.multiplier).unwrap()) + }) + .collect(); + + // TODO: find a less hacky way of dealing with situation lifecycles. this is a brute-force + // "just throw it on the heap until the kernel kills the process when we exit" hackaround + // to the borrow checker complaining about needing 'static lifespans down in thread-spawn + // land, which _works_, but feels messy + let situations = Box::new(situations).leak(); + + // no need for any of the ephemeral *Spec objects at this point + drop(args); + + let mut situation_threads: Vec> = Vec::with_capacity(situations.len()); + let barrier = Arc::new(Barrier::new(situation_threads.len())); + + for situation in situations { + let barrier = barrier.clone(); + + situation_threads.push(thread::spawn(move || { + let (tx, rx) = mpsc::channel(); + + for grunt in &situation.grunts { + let barrier = barrier.clone(); + let situation = situation.clone(); + let tx = tx.clone(); + + thread::spawn(move || grunt_worker(barrier, situation, grunt, tx)); + } + + // have to drop the original tx to get refcounts correct, else controller thread will + // hang indefinitely while rx thinks it has potential inbound data + drop(tx); + + for received in rx { + println!("Got: {}", received); + } + })); + } + + for thread in situation_threads { + thread.join().unwrap(); + } + + Ok(()) +} + +fn grunt_worker( + barrier: Arc, + situation: Arc, + grunt: &Grunt, + tx: mpsc::Sender, +) { + let lua = Lua::new(); + let persona = &situation.personas[grunt.persona_idx]; + let agent = AgentBuilder::new() + .timeout((&persona.spec.timeout).into()) + .build(); + let vals = vec![ + String::from("hi"), + String::from("from"), + String::from("the"), + String::from("thread"), + ]; + + barrier.wait(); + + for val in vals { + tx.send(val).unwrap(); + thread::sleep(Duration::from_secs(1)); + } +} From c6e0c96bd8e64a888bd02f60c525d5de56e47f81 Mon Sep 17 00:00:00 2001 From: Josh Klar Date: Sat, 8 Jan 2022 00:10:00 -0800 Subject: [PATCH 02/14] WIP (rebase away): trivial --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index b285952..dab330b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -361,7 +361,7 @@ fn main() -> std::io::Result<()> { drop(args); let mut situation_threads: Vec> = Vec::with_capacity(situations.len()); - let barrier = Arc::new(Barrier::new(situation_threads.len())); + let barrier = Arc::new(Barrier::new(situations.len())); for situation in situations { let barrier = barrier.clone(); From f3c7b6452b9daa875a736a21c9ad9ec22d96846c Mon Sep 17 00:00:00 2001 From: Josh Klar Date: Thu, 13 Jan 2022 17:55:10 -0800 Subject: [PATCH 03/14] checkpoint 13 jan 2022 --- examples/simpleish/date.lua | 742 ++++++++++++++++++++++++++++++++ examples/simpleish/seatrial.lua | 15 + examples/simpleish/seatrial.ron | 2 + src/config_duration.rs | 18 + src/grunt.rs | 33 ++ src/main.rs | 539 ++++++++++------------- src/persona.rs | 21 + src/pipeline.rs | 35 ++ src/pipeline_action.rs | 81 ++++ src/situation.rs | 214 +++++++++ src/step_goto.rs | 21 + src/step_http.rs | 170 ++++++++ src/step_lua.rs | 25 ++ 13 files changed, 1615 insertions(+), 301 deletions(-) create mode 100644 examples/simpleish/date.lua create mode 100644 examples/simpleish/seatrial.lua create mode 100644 src/config_duration.rs create mode 100644 src/grunt.rs create mode 100644 src/persona.rs create mode 100644 src/pipeline.rs create mode 100644 src/pipeline_action.rs create mode 100644 src/situation.rs create mode 100644 src/step_goto.rs create mode 100644 src/step_http.rs create mode 100644 src/step_lua.rs diff --git a/examples/simpleish/date.lua b/examples/simpleish/date.lua new file mode 100644 index 0000000..35fdab6 --- /dev/null +++ b/examples/simpleish/date.lua @@ -0,0 +1,742 @@ +--------------------------------------------------------------------------------------- +-- Module for date and time calculations +-- +-- Version 2.2 +-- Copyright (C) 2005-2006, by Jas Latrix (jastejada@yahoo.com) +-- Copyright (C) 2013-2021, by Thijs Schreijer +-- Licensed under MIT, http://opensource.org/licenses/MIT + +--[[ CONSTANTS ]]-- + local HOURPERDAY = 24 + local MINPERHOUR = 60 + local MINPERDAY = 1440 -- 24*60 + local SECPERMIN = 60 + local SECPERHOUR = 3600 -- 60*60 + local SECPERDAY = 86400 -- 24*60*60 + local TICKSPERSEC = 1000000 + local TICKSPERDAY = 86400000000 + local TICKSPERHOUR = 3600000000 + local TICKSPERMIN = 60000000 + local DAYNUM_MAX = 365242500 -- Sat Jan 01 1000000 00:00:00 + local DAYNUM_MIN = -365242500 -- Mon Jan 01 1000000 BCE 00:00:00 + local DAYNUM_DEF = 0 -- Mon Jan 01 0001 00:00:00 + local _; +--[[ GLOBAL SETTINGS ]]-- + local centuryflip = 0 -- year >= centuryflip == 1900, < centuryflip == 2000 +--[[ LOCAL ARE FASTER ]]-- + local type = type + local pairs = pairs + local error = error + local assert = assert + local tonumber = tonumber + local tostring = tostring + local string = string + local math = math + local os = os + local unpack = unpack or table.unpack + local setmetatable = setmetatable + local getmetatable = getmetatable +--[[ EXTRA FUNCTIONS ]]-- + local fmt = string.format + local lwr = string.lower + local rep = string.rep + local len = string.len -- luacheck: ignore + local sub = string.sub + local gsub = string.gsub + local gmatch = string.gmatch or string.gfind + local find = string.find + local ostime = os.time + local osdate = os.date + local floor = math.floor + local ceil = math.ceil + local abs = math.abs + -- removes the decimal part of a number + local function fix(n) n = tonumber(n) return n and ((n > 0 and floor or ceil)(n)) end + -- returns the modulo n % d; + local function mod(n,d) return n - d*floor(n/d) end + -- is `str` in string list `tbl`, `ml` is the minimun len + local function inlist(str, tbl, ml, tn) + local sl = len(str) + if sl < (ml or 0) then return nil end + str = lwr(str) + for k, v in pairs(tbl) do + if str == lwr(sub(v, 1, sl)) then + if tn then tn[0] = k end + return k + end + end + end + local function fnil() end +--[[ DATE FUNCTIONS ]]-- + local DATE_EPOCH -- to be set later + local sl_weekdays = { + [0]="Sunday",[1]="Monday",[2]="Tuesday",[3]="Wednesday",[4]="Thursday",[5]="Friday",[6]="Saturday", + [7]="Sun",[8]="Mon",[9]="Tue",[10]="Wed",[11]="Thu",[12]="Fri",[13]="Sat", + } + local sl_meridian = {[-1]="AM", [1]="PM"} + local sl_months = { + [00]="January", [01]="February", [02]="March", + [03]="April", [04]="May", [05]="June", + [06]="July", [07]="August", [08]="September", + [09]="October", [10]="November", [11]="December", + [12]="Jan", [13]="Feb", [14]="Mar", + [15]="Apr", [16]="May", [17]="Jun", + [18]="Jul", [19]="Aug", [20]="Sep", + [21]="Oct", [22]="Nov", [23]="Dec", + } + -- added the '.2' to avoid collision, use `fix` to remove + local sl_timezone = { + [000]="utc", [0.2]="gmt", + [300]="est", [240]="edt", + [360]="cst", [300.2]="cdt", + [420]="mst", [360.2]="mdt", + [480]="pst", [420.2]="pdt", + } + -- set the day fraction resolution + local function setticks(t) + TICKSPERSEC = t; + TICKSPERDAY = SECPERDAY*TICKSPERSEC + TICKSPERHOUR= SECPERHOUR*TICKSPERSEC + TICKSPERMIN = SECPERMIN*TICKSPERSEC + end + -- is year y leap year? + local function isleapyear(y) -- y must be int! + return (mod(y, 4) == 0 and (mod(y, 100) ~= 0 or mod(y, 400) == 0)) + end + -- day since year 0 + local function dayfromyear(y) -- y must be int! + return 365*y + floor(y/4) - floor(y/100) + floor(y/400) + end + -- day number from date, month is zero base + local function makedaynum(y, m, d) + local mm = mod(mod(m,12) + 10, 12) + return dayfromyear(y + floor(m/12) - floor(mm/10)) + floor((mm*306 + 5)/10) + d - 307 + --local yy = y + floor(m/12) - floor(mm/10) + --return dayfromyear(yy) + floor((mm*306 + 5)/10) + (d - 1) + end + -- date from day number, month is zero base + local function breakdaynum(g) + local g = g + 306 + local y = floor((10000*g + 14780)/3652425) + local d = g - dayfromyear(y) + if d < 0 then y = y - 1; d = g - dayfromyear(y) end + local mi = floor((100*d + 52)/3060) + return (floor((mi + 2)/12) + y), mod(mi + 2,12), (d - floor((mi*306 + 5)/10) + 1) + end + --[[ for floats or int32 Lua Number data type + local function breakdaynum2(g) + local g, n = g + 306; + local n400 = floor(g/DI400Y);n = mod(g,DI400Y); + local n100 = floor(n/DI100Y);n = mod(n,DI100Y); + local n004 = floor(n/DI4Y); n = mod(n,DI4Y); + local n001 = floor(n/365); n = mod(n,365); + local y = (n400*400) + (n100*100) + (n004*4) + n001 - ((n001 == 4 or n100 == 4) and 1 or 0) + local d = g - dayfromyear(y) + local mi = floor((100*d + 52)/3060) + return (floor((mi + 2)/12) + y), mod(mi + 2,12), (d - floor((mi*306 + 5)/10) + 1) + end + ]] + -- day fraction from time + local function makedayfrc(h,r,s,t) + return ((h*60 + r)*60 + s)*TICKSPERSEC + t + end + -- time from day fraction + local function breakdayfrc(df) + return + mod(floor(df/TICKSPERHOUR),HOURPERDAY), + mod(floor(df/TICKSPERMIN ),MINPERHOUR), + mod(floor(df/TICKSPERSEC ),SECPERMIN), + mod(df,TICKSPERSEC) + end + -- weekday sunday = 0, monday = 1 ... + local function weekday(dn) return mod(dn + 1, 7) end + -- yearday 0 based ... + local function yearday(dn) + return dn - dayfromyear((breakdaynum(dn))-1) + end + -- parse v as a month + local function getmontharg(v) + local m = tonumber(v); + return (m and fix(m - 1)) or inlist(tostring(v) or "", sl_months, 2) + end + -- get daynum of isoweek one of year y + local function isow1(y) + local f = makedaynum(y, 0, 4) -- get the date for the 4-Jan of year `y` + local d = weekday(f) + d = d == 0 and 7 or d -- get the ISO day number, 1 == Monday, 7 == Sunday + return f + (1 - d) + end + local function isowy(dn) + local w1; + local y = (breakdaynum(dn)) + if dn >= makedaynum(y, 11, 29) then + w1 = isow1(y + 1); + if dn < w1 then + w1 = isow1(y); + else + y = y + 1; + end + else + w1 = isow1(y); + if dn < w1 then + w1 = isow1(y-1) + y = y - 1 + end + end + return floor((dn-w1)/7)+1, y + end + local function isoy(dn) + local y = (breakdaynum(dn)) + return y + (((dn >= makedaynum(y, 11, 29)) and (dn >= isow1(y + 1))) and 1 or (dn < isow1(y) and -1 or 0)) + end + local function makedaynum_isoywd(y,w,d) + return isow1(y) + 7*w + d - 8 -- simplified: isow1(y) + ((w-1)*7) + (d-1) + end +--[[ THE DATE MODULE ]]-- + local fmtstr = "%x %X"; +--#if not DATE_OBJECT_AFX then + local date = {} + setmetatable(date, date) +-- Version: VMMMRRRR; V-Major, M-Minor, R-Revision; e.g. 5.45.321 == 50450321 + do + local major = 2 + local minor = 2 + local revision = 0 + date.version = major * 10000000 + minor * 10000 + revision + end +--#end -- not DATE_OBJECT_AFX +--[[ THE DATE OBJECT ]]-- + local dobj = {} + dobj.__index = dobj + dobj.__metatable = dobj + -- shout invalid arg + local function date_error_arg() return error("invalid argument(s)",0) end + -- create new date object + local function date_new(dn, df) + return setmetatable({daynum=dn, dayfrc=df}, dobj) + end + +--#if not NO_LOCAL_TIME_SUPPORT then + -- magic year table + local date_epoch, yt; + local function getequivyear(y) + assert(not yt) + yt = {} + local de = date_epoch:copy() + local dw, dy + for _ = 0, 3000 do + de:setyear(de:getyear() + 1, 1, 1) + dy = de:getyear() + dw = de:getweekday() * (isleapyear(dy) and -1 or 1) + if not yt[dw] then yt[dw] = dy end --print(de) + if yt[1] and yt[2] and yt[3] and yt[4] and yt[5] and yt[6] and yt[7] and yt[-1] and yt[-2] and yt[-3] and yt[-4] and yt[-5] and yt[-6] and yt[-7] then + getequivyear = function(y) return yt[ (weekday(makedaynum(y, 0, 1)) + 1) * (isleapyear(y) and -1 or 1) ] end + return getequivyear(y) + end + end + end + -- TimeValue from date and time + local function totv(y,m,d,h,r,s) + return (makedaynum(y, m, d) - DATE_EPOCH) * SECPERDAY + ((h*60 + r)*60 + s) + end + -- TimeValue from TimeTable + local function tmtotv(tm) + return tm and totv(tm.year, tm.month - 1, tm.day, tm.hour, tm.min, tm.sec) + end + -- Returns the bias in seconds of utc time daynum and dayfrc + local function getbiasutc2(self) + local y,m,d = breakdaynum(self.daynum) + local h,r,s = breakdayfrc(self.dayfrc) + local tvu = totv(y,m,d,h,r,s) -- get the utc TimeValue of date and time + local tml = osdate("*t", tvu) -- get the local TimeTable of tvu + if (not tml) or (tml.year > (y+1) or tml.year < (y-1)) then -- failed try the magic + y = getequivyear(y) + tvu = totv(y,m,d,h,r,s) + tml = osdate("*t", tvu) + end + local tvl = tmtotv(tml) + if tvu and tvl then + return tvu - tvl, tvu, tvl + else + return error("failed to get bias from utc time") + end + end + -- Returns the bias in seconds of local time daynum and dayfrc + local function getbiasloc2(daynum, dayfrc) + local tvu + -- extract date and time + local y,m,d = breakdaynum(daynum) + local h,r,s = breakdayfrc(dayfrc) + -- get equivalent TimeTable + local tml = {year=y, month=m+1, day=d, hour=h, min=r, sec=s} + -- get equivalent TimeValue + local tvl = tmtotv(tml) + + local function chkutc() + tml.isdst = nil; local tvug = ostime(tml) if tvug and (tvl == tmtotv(osdate("*t", tvug))) then tvu = tvug return end + tml.isdst = true; local tvud = ostime(tml) if tvud and (tvl == tmtotv(osdate("*t", tvud))) then tvu = tvud return end + tvu = tvud or tvug + end + chkutc() + if not tvu then + tml.year = getequivyear(y) + tvl = tmtotv(tml) + chkutc() + end + return ((tvu and tvl) and (tvu - tvl)) or error("failed to get bias from local time"), tvu, tvl + end +--#end -- not NO_LOCAL_TIME_SUPPORT + +--#if not DATE_OBJECT_AFX then + -- the date parser + local strwalker = {} -- ^Lua regular expression is not as powerful as Perl$ + strwalker.__index = strwalker + local function newstrwalker(s)return setmetatable({s=s, i=1, e=1, c=len(s)}, strwalker) end + function strwalker:aimchr() return "\n" .. self.s .. "\n" .. rep(".",self.e-1) .. "^" end + function strwalker:finish() return self.i > self.c end + function strwalker:back() self.i = self.e return self end + function strwalker:restart() self.i, self.e = 1, 1 return self end + function strwalker:match(s) return (find(self.s, s, self.i)) end + function strwalker:__call(s, f)-- print("strwalker:__call "..s..self:aimchr()) + local is, ie; is, ie, self[1], self[2], self[3], self[4], self[5] = find(self.s, s, self.i) + if is then self.e, self.i = self.i, 1+ie; if f then f(unpack(self)) end return self end + end + local function date_parse(str) + local y,m,d, h,r,s, z, w,u, j, e, x,c, dn,df + local sw = newstrwalker(gsub(gsub(str, "(%b())", ""),"^(%s*)","")) -- remove comment, trim leading space + --local function error_out() print(y,m,d,h,r,s) end + local function error_dup(q) --[[error_out()]] error("duplicate value: " .. (q or "") .. sw:aimchr()) end + local function error_syn(q) --[[error_out()]] error("syntax error: " .. (q or "") .. sw:aimchr()) end + local function error_inv(q) --[[error_out()]] error("invalid date: " .. (q or "") .. sw:aimchr()) end + local function sety(q) y = y and error_dup() or tonumber(q); end + local function setm(q) m = (m or w or j) and error_dup(m or w or j) or tonumber(q) end + local function setd(q) d = d and error_dup() or tonumber(q) end + local function seth(q) h = h and error_dup() or tonumber(q) end + local function setr(q) r = r and error_dup() or tonumber(q) end + local function sets(q) s = s and error_dup() or tonumber(q) end + local function adds(q) s = s + tonumber(q) end + local function setj(q) j = (m or w or j) and error_dup() or tonumber(q); end + local function setz(q) z = (z ~= 0 and z) and error_dup() or q end + local function setzn(zs,zn) zn = tonumber(zn); setz( ((zn<24) and (zn*60) or (mod(zn,100) + floor(zn/100) * 60))*( zs=='+' and -1 or 1) ) end + local function setzc(zs,zh,zm) setz( ((tonumber(zh)*60) + tonumber(zm))*( zs=='+' and -1 or 1) ) end + + if not (sw("^(%d%d%d%d)",sety) and (sw("^(%-?)(%d%d)%1(%d%d)",function(_,a,b) setm(tonumber(a)); setd(tonumber(b)) end) or sw("^(%-?)[Ww](%d%d)%1(%d?)",function(_,a,b) w, u = tonumber(a), tonumber(b or 1) end) or sw("^%-?(%d%d%d)",setj) or sw("^%-?(%d%d)",function(a) setm(a);setd(1) end)) + and ((sw("^%s*[Tt]?(%d%d):?",seth) and sw("^(%d%d):?",setr) and sw("^(%d%d)",sets) and sw("^(%.%d+)",adds)) + or sw:finish() or (sw"^%s*$" or sw"^%s*[Zz]%s*$" or sw("^%s-([%+%-])(%d%d):?(%d%d)%s*$",setzc) or sw("^%s*([%+%-])(%d%d)%s*$",setzn)) + ) ) + then --print(y,m,d,h,r,s,z,w,u,j) + sw:restart(); y,m,d,h,r,s,z,w,u,j = nil,nil,nil,nil,nil,nil,nil,nil,nil,nil + repeat -- print(sw:aimchr()) + if sw("^[tT:]?%s*(%d%d?):",seth) then --print("$Time") + _ = sw("^%s*(%d%d?)",setr) and sw("^%s*:%s*(%d%d?)",sets) and sw("^(%.%d+)",adds) + elseif sw("^(%d+)[/\\%s,-]?%s*") then --print("$Digits") + x, c = tonumber(sw[1]), len(sw[1]) + if (x >= 70) or (m and d and (not y)) or (c > 3) then + sety( x + ((x >= 100 or c>3) and 0 or x 12) or (h < 0) then return error_inv() end + if x == 'a' and h == 12 then h = 0 end -- am + if x == 'p' and h ~= 12 then h = h + 12 end -- pm + else error_syn() end + end + elseif not(sw("^([+-])(%d%d?):(%d%d)",setzc) or sw("^([+-])(%d+)",setzn) or sw("^[Zz]%s*$")) then -- sw{"([+-])",{"(%d%d?):(%d%d)","(%d+)"}} + error_syn("?") + end + sw("^%s*") until sw:finish() + --else print("$Iso(Date|Time|Zone)") + end + -- if date is given, it must be complete year, month & day + if (not y and not h) or ((m and not d) or (d and not m)) or ((m and w) or (m and j) or (j and w)) then return error_inv("!") end + -- fix month + if m then m = m - 1 end + -- fix year if we are on BCE + if e and e < 0 and y > 0 then y = 1 - y end + -- create date object + dn = (y and ((w and makedaynum_isoywd(y,w,u)) or (j and makedaynum(y, 0, j)) or makedaynum(y, m, d))) or DAYNUM_DEF + df = makedayfrc(h or 0, r or 0, s or 0, 0) + ((z or 0)*TICKSPERMIN) + --print("Zone",h,r,s,z,m,d,y,df) + return date_new(dn, df) -- no need to :normalize(); + end + local function date_fromtable(v) + local y, m, d = fix(v.year), getmontharg(v.month), fix(v.day) + local h, r, s, t = tonumber(v.hour), tonumber(v.min), tonumber(v.sec), tonumber(v.ticks) + -- atleast there is time or complete date + if (y or m or d) and (not(y and m and d)) then return error("incomplete table") end + return (y or h or r or s or t) and date_new(y and makedaynum(y, m, d) or DAYNUM_DEF, makedayfrc(h or 0, r or 0, s or 0, t or 0)) + end + local tmap = { + ['number'] = function(v) return date_epoch:copy():addseconds(v) end, + ['string'] = function(v) return date_parse(v) end, + ['boolean']= function(v) return date_fromtable(osdate(v and "!*t" or "*t")) end, + ['table'] = function(v) local ref = getmetatable(v) == dobj; return ref and v or date_fromtable(v), ref end + } + local function date_getdobj(v) + local o, r = (tmap[type(v)] or fnil)(v); + return (o and o:normalize() or error"invalid date time value"), r -- if r is true then o is a reference to a date obj + end +--#end -- not DATE_OBJECT_AFX + local function date_from(arg1, arg2, arg3, arg4, arg5, arg6, arg7) + local y, m, d = fix(arg1), getmontharg(arg2), fix(arg3) + local h, r, s, t = tonumber(arg4 or 0), tonumber(arg5 or 0), tonumber(arg6 or 0), tonumber(arg7 or 0) + if y and m and d and h and r and s and t then + return date_new(makedaynum(y, m, d), makedayfrc(h, r, s, t)):normalize() + else + return date_error_arg() + end + end + + --[[ THE DATE OBJECT METHODS ]]-- + function dobj:normalize() + local dn, df = fix(self.daynum), self.dayfrc + self.daynum, self.dayfrc = dn + floor(df/TICKSPERDAY), mod(df, TICKSPERDAY) + return (dn >= DAYNUM_MIN and dn <= DAYNUM_MAX) and self or error("date beyond imposed limits:"..self) + end + + function dobj:getdate() local y, m, d = breakdaynum(self.daynum) return y, m+1, d end + function dobj:gettime() return breakdayfrc(self.dayfrc) end + + function dobj:getclockhour() local h = self:gethours() return h>12 and mod(h,12) or (h==0 and 12 or h) end + + function dobj:getyearday() return yearday(self.daynum) + 1 end + function dobj:getweekday() return weekday(self.daynum) + 1 end -- in lua weekday is sunday = 1, monday = 2 ... + + function dobj:getyear() local r,_,_ = breakdaynum(self.daynum) return r end + function dobj:getmonth() local _,r,_ = breakdaynum(self.daynum) return r+1 end-- in lua month is 1 base + function dobj:getday() local _,_,r = breakdaynum(self.daynum) return r end + function dobj:gethours() return mod(floor(self.dayfrc/TICKSPERHOUR),HOURPERDAY) end + function dobj:getminutes() return mod(floor(self.dayfrc/TICKSPERMIN), MINPERHOUR) end + function dobj:getseconds() return mod(floor(self.dayfrc/TICKSPERSEC ),SECPERMIN) end + function dobj:getfracsec() return mod(floor(self.dayfrc/TICKSPERSEC ),SECPERMIN)+(mod(self.dayfrc,TICKSPERSEC)/TICKSPERSEC) end + function dobj:getticks(u) local x = mod(self.dayfrc,TICKSPERSEC) return u and ((x*u)/TICKSPERSEC) or x end + + function dobj:getweeknumber(wdb) + local wd, yd = weekday(self.daynum), yearday(self.daynum) + if wdb then + wdb = tonumber(wdb) + if wdb then + wd = mod(wd-(wdb-1),7)-- shift the week day base + else + return date_error_arg() + end + end + return (yd < wd and 0) or (floor(yd/7) + ((mod(yd, 7)>=wd) and 1 or 0)) + end + + function dobj:getisoweekday() return mod(weekday(self.daynum)-1,7)+1 end -- sunday = 7, monday = 1 ... + function dobj:getisoweeknumber() return (isowy(self.daynum)) end + function dobj:getisoyear() return isoy(self.daynum) end + function dobj:getisodate() + local w, y = isowy(self.daynum) + return y, w, self:getisoweekday() + end + function dobj:setisoyear(y, w, d) + local cy, cw, cd = self:getisodate() + if y then cy = fix(tonumber(y))end + if w then cw = fix(tonumber(w))end + if d then cd = fix(tonumber(d))end + if cy and cw and cd then + self.daynum = makedaynum_isoywd(cy, cw, cd) + return self:normalize() + else + return date_error_arg() + end + end + + function dobj:setisoweekday(d) return self:setisoyear(nil, nil, d) end + function dobj:setisoweeknumber(w,d) return self:setisoyear(nil, w, d) end + + function dobj:setyear(y, m, d) + local cy, cm, cd = breakdaynum(self.daynum) + if y then cy = fix(tonumber(y))end + if m then cm = getmontharg(m) end + if d then cd = fix(tonumber(d))end + if cy and cm and cd then + self.daynum = makedaynum(cy, cm, cd) + return self:normalize() + else + return date_error_arg() + end + end + + function dobj:setmonth(m, d)return self:setyear(nil, m, d) end + function dobj:setday(d) return self:setyear(nil, nil, d) end + + function dobj:sethours(h, m, s, t) + local ch,cm,cs,ck = breakdayfrc(self.dayfrc) + ch, cm, cs, ck = tonumber(h or ch), tonumber(m or cm), tonumber(s or cs), tonumber(t or ck) + if ch and cm and cs and ck then + self.dayfrc = makedayfrc(ch, cm, cs, ck) + return self:normalize() + else + return date_error_arg() + end + end + + function dobj:setminutes(m,s,t) return self:sethours(nil, m, s, t) end + function dobj:setseconds(s, t) return self:sethours(nil, nil, s, t) end + function dobj:setticks(t) return self:sethours(nil, nil, nil, t) end + + function dobj:spanticks() return (self.daynum*TICKSPERDAY + self.dayfrc) end + function dobj:spanseconds() return (self.daynum*TICKSPERDAY + self.dayfrc)/TICKSPERSEC end + function dobj:spanminutes() return (self.daynum*TICKSPERDAY + self.dayfrc)/TICKSPERMIN end + function dobj:spanhours() return (self.daynum*TICKSPERDAY + self.dayfrc)/TICKSPERHOUR end + function dobj:spandays() return (self.daynum*TICKSPERDAY + self.dayfrc)/TICKSPERDAY end + + function dobj:addyears(y, m, d) + local cy, cm, cd = breakdaynum(self.daynum) + if y then y = fix(tonumber(y))else y = 0 end + if m then m = fix(tonumber(m))else m = 0 end + if d then d = fix(tonumber(d))else d = 0 end + if y and m and d then + self.daynum = makedaynum(cy+y, cm+m, cd+d) + return self:normalize() + else + return date_error_arg() + end + end + + function dobj:addmonths(m, d) + return self:addyears(nil, m, d) + end + + local function dobj_adddayfrc(self,n,pt,pd) + n = tonumber(n) + if n then + local x = floor(n/pd); + self.daynum = self.daynum + x; + self.dayfrc = self.dayfrc + (n-x*pd)*pt; + return self:normalize() + else + return date_error_arg() + end + end + function dobj:adddays(n) return dobj_adddayfrc(self,n,TICKSPERDAY,1) end + function dobj:addhours(n) return dobj_adddayfrc(self,n,TICKSPERHOUR,HOURPERDAY) end + function dobj:addminutes(n) return dobj_adddayfrc(self,n,TICKSPERMIN,MINPERDAY) end + function dobj:addseconds(n) return dobj_adddayfrc(self,n,TICKSPERSEC,SECPERDAY) end + function dobj:addticks(n) return dobj_adddayfrc(self,n,1,TICKSPERDAY) end + local tvspec = { + -- Abbreviated weekday name (Sun) + ['%a']=function(self) return sl_weekdays[weekday(self.daynum) + 7] end, + -- Full weekday name (Sunday) + ['%A']=function(self) return sl_weekdays[weekday(self.daynum)] end, + -- Abbreviated month name (Dec) + ['%b']=function(self) return sl_months[self:getmonth() - 1 + 12] end, + -- Full month name (December) + ['%B']=function(self) return sl_months[self:getmonth() - 1] end, + -- Year/100 (19, 20, 30) + ['%C']=function(self) return fmt("%.2d", fix(self:getyear()/100)) end, + -- The day of the month as a number (range 1 - 31) + ['%d']=function(self) return fmt("%.2d", self:getday()) end, + -- year for ISO 8601 week, from 00 (79) + ['%g']=function(self) return fmt("%.2d", mod(self:getisoyear() ,100)) end, + -- year for ISO 8601 week, from 0000 (1979) + ['%G']=function(self) return fmt("%.4d", self:getisoyear()) end, + -- same as %b + ['%h']=function(self) return self:fmt0("%b") end, + -- hour of the 24-hour day, from 00 (06) + ['%H']=function(self) return fmt("%.2d", self:gethours()) end, + -- The hour as a number using a 12-hour clock (01 - 12) + ['%I']=function(self) return fmt("%.2d", self:getclockhour()) end, + -- The day of the year as a number (001 - 366) + ['%j']=function(self) return fmt("%.3d", self:getyearday()) end, + -- Month of the year, from 01 to 12 + ['%m']=function(self) return fmt("%.2d", self:getmonth()) end, + -- Minutes after the hour 55 + ['%M']=function(self) return fmt("%.2d", self:getminutes())end, + -- AM/PM indicator (AM) + ['%p']=function(self) return sl_meridian[self:gethours() > 11 and 1 or -1] end, --AM/PM indicator (AM) + -- The second as a number (59, 20 , 01) + ['%S']=function(self) return fmt("%.2d", self:getseconds()) end, + -- ISO 8601 day of the week, to 7 for Sunday (7, 1) + ['%u']=function(self) return self:getisoweekday() end, + -- Sunday week of the year, from 00 (48) + ['%U']=function(self) return fmt("%.2d", self:getweeknumber()) end, + -- ISO 8601 week of the year, from 01 (48) + ['%V']=function(self) return fmt("%.2d", self:getisoweeknumber()) end, + -- The day of the week as a decimal, Sunday being 0 + ['%w']=function(self) return self:getweekday() - 1 end, + -- Monday week of the year, from 00 (48) + ['%W']=function(self) return fmt("%.2d", self:getweeknumber(2)) end, + -- The year as a number without a century (range 00 to 99) + ['%y']=function(self) return fmt("%.2d", mod(self:getyear() ,100)) end, + -- Year with century (2000, 1914, 0325, 0001) + ['%Y']=function(self) return fmt("%.4d", self:getyear()) end, + -- Time zone offset, the date object is assumed local time (+1000, -0230) + ['%z']=function(self) local b = -self:getbias(); local x = abs(b); return fmt("%s%.4d", b < 0 and "-" or "+", fix(x/60)*100 + floor(mod(x,60))) end, + -- Time zone name, the date object is assumed local time + ['%Z']=function(self) return self:gettzname() end, + -- Misc -- + -- Year, if year is in BCE, prints the BCE Year representation, otherwise result is similar to "%Y" (1 BCE, 40 BCE) + ['%\b']=function(self) local x = self:getyear() return fmt("%.4d%s", x>0 and x or (-x+1), x>0 and "" or " BCE") end, + -- Seconds including fraction (59.998, 01.123) + ['%\f']=function(self) local x = self:getfracsec() return fmt("%s%.9f",x >= 10 and "" or "0", x) end, + -- percent character % + ['%%']=function(self) return "%" end, + -- Group Spec -- + -- 12-hour time, from 01:00:00 AM (06:55:15 AM); same as "%I:%M:%S %p" + ['%r']=function(self) return self:fmt0("%I:%M:%S %p") end, + -- hour:minute, from 01:00 (06:55); same as "%I:%M" + ['%R']=function(self) return self:fmt0("%I:%M") end, + -- 24-hour time, from 00:00:00 (06:55:15); same as "%H:%M:%S" + ['%T']=function(self) return self:fmt0("%H:%M:%S") end, + -- month/day/year from 01/01/00 (12/02/79); same as "%m/%d/%y" + ['%D']=function(self) return self:fmt0("%m/%d/%y") end, + -- year-month-day (1979-12-02); same as "%Y-%m-%d" + ['%F']=function(self) return self:fmt0("%Y-%m-%d") end, + -- The preferred date and time representation; same as "%x %X" + ['%c']=function(self) return self:fmt0("%x %X") end, + -- The preferred date representation, same as "%a %b %d %\b" + ['%x']=function(self) return self:fmt0("%a %b %d %\b") end, + -- The preferred time representation, same as "%H:%M:%\f" + ['%X']=function(self) return self:fmt0("%H:%M:%\f") end, + -- GroupSpec -- + -- Iso format, same as "%Y-%m-%dT%T" + ['${iso}'] = function(self) return self:fmt0("%Y-%m-%dT%T") end, + -- http format, same as "%a, %d %b %Y %T GMT" + ['${http}'] = function(self) return self:fmt0("%a, %d %b %Y %T GMT") end, + -- ctime format, same as "%a %b %d %T GMT %Y" + ['${ctime}'] = function(self) return self:fmt0("%a %b %d %T GMT %Y") end, + -- RFC850 format, same as "%A, %d-%b-%y %T GMT" + ['${rfc850}'] = function(self) return self:fmt0("%A, %d-%b-%y %T GMT") end, + -- RFC1123 format, same as "%a, %d %b %Y %T GMT" + ['${rfc1123}'] = function(self) return self:fmt0("%a, %d %b %Y %T GMT") end, + -- asctime format, same as "%a %b %d %T %Y" + ['${asctime}'] = function(self) return self:fmt0("%a %b %d %T %Y") end, + } + function dobj:fmt0(str) return (gsub(str, "%%[%a%%\b\f]", function(x) local f = tvspec[x];return (f and f(self)) or x end)) end + function dobj:fmt(str) + str = str or self.fmtstr or fmtstr + return self:fmt0((gmatch(str, "${%w+}")) and (gsub(str, "${%w+}", function(x)local f=tvspec[x];return (f and f(self)) or x end)) or str) + end + + function dobj.__lt(a, b) if (a.daynum == b.daynum) then return (a.dayfrc < b.dayfrc) else return (a.daynum < b.daynum) end end + function dobj.__le(a, b) if (a.daynum == b.daynum) then return (a.dayfrc <= b.dayfrc) else return (a.daynum <= b.daynum) end end + function dobj.__eq(a, b)return (a.daynum == b.daynum) and (a.dayfrc == b.dayfrc) end + function dobj.__sub(a,b) + local d1, d2 = date_getdobj(a), date_getdobj(b) + local d0 = d1 and d2 and date_new(d1.daynum - d2.daynum, d1.dayfrc - d2.dayfrc) + return d0 and d0:normalize() + end + function dobj.__add(a,b) + local d1, d2 = date_getdobj(a), date_getdobj(b) + local d0 = d1 and d2 and date_new(d1.daynum + d2.daynum, d1.dayfrc + d2.dayfrc) + return d0 and d0:normalize() + end + function dobj.__concat(a, b) return tostring(a) .. tostring(b) end + function dobj:__tostring() return self:fmt() end + + function dobj:copy() return date_new(self.daynum, self.dayfrc) end + +--[[ THE LOCAL DATE OBJECT METHODS ]]-- + function dobj:tolocal() + local dn,df = self.daynum, self.dayfrc + local bias = getbiasutc2(self) + if bias then + -- utc = local + bias; local = utc - bias + self.daynum = dn + self.dayfrc = df - bias*TICKSPERSEC + return self:normalize() + else + return nil + end + end + + function dobj:toutc() + local dn,df = self.daynum, self.dayfrc + local bias = getbiasloc2(dn, df) + if bias then + -- utc = local + bias; + self.daynum = dn + self.dayfrc = df + bias*TICKSPERSEC + return self:normalize() + else + return nil + end + end + + function dobj:getbias() return (getbiasloc2(self.daynum, self.dayfrc))/SECPERMIN end + + function dobj:gettzname() + local _, tvu, _ = getbiasloc2(self.daynum, self.dayfrc) + return tvu and osdate("%Z",tvu) or "" + end + +--#if not DATE_OBJECT_AFX then + function date.time(h, r, s, t) + h, r, s, t = tonumber(h or 0), tonumber(r or 0), tonumber(s or 0), tonumber(t or 0) + if h and r and s and t then + return date_new(DAYNUM_DEF, makedayfrc(h, r, s, t)) + else + return date_error_arg() + end + end + + function date:__call(arg1, ...) + local arg_count = select("#", ...) + (arg1 == nil and 0 or 1) + if arg_count > 1 then return (date_from(arg1, ...)) + elseif arg_count == 0 then return (date_getdobj(false)) + else local o, r = date_getdobj(arg1); return r and o:copy() or o end + end + + date.diff = dobj.__sub + + function date.isleapyear(v) + local y = fix(v); + if not y then + y = date_getdobj(v) + y = y and y:getyear() + end + return isleapyear(y+0) + end + + function date.epoch() return date_epoch:copy() end + + function date.isodate(y,w,d) return date_new(makedaynum_isoywd(y + 0, w and (w+0) or 1, d and (d+0) or 1), 0) end + function date.setcenturyflip(y) + if y ~= floor(y) or y < 0 or y > 100 then date_error_arg() end + centuryflip = y + end + function date.getcenturyflip() return centuryflip end + +-- Internal functions + function date.fmt(str) if str then fmtstr = str end; return fmtstr end + function date.daynummin(n) DAYNUM_MIN = (n and n < DAYNUM_MAX) and n or DAYNUM_MIN return n and DAYNUM_MIN or date_new(DAYNUM_MIN, 0):normalize()end + function date.daynummax(n) DAYNUM_MAX = (n and n > DAYNUM_MIN) and n or DAYNUM_MAX return n and DAYNUM_MAX or date_new(DAYNUM_MAX, 0):normalize()end + function date.ticks(t) if t then setticks(t) end return TICKSPERSEC end +--#end -- not DATE_OBJECT_AFX + + local tm = osdate("!*t", 0); + if tm then + date_epoch = date_new(makedaynum(tm.year, tm.month - 1, tm.day), makedayfrc(tm.hour, tm.min, tm.sec, 0)) + -- the distance from our epoch to os epoch in daynum + DATE_EPOCH = date_epoch and date_epoch:spandays() + else -- error will be raise only if called! + date_epoch = setmetatable({},{__index = function() error("failed to get the epoch date") end}) + end + +--#if not DATE_OBJECT_AFX then +return date +--#else +--$return date_from +--#end + diff --git a/examples/simpleish/seatrial.lua b/examples/simpleish/seatrial.lua new file mode 100644 index 0000000..9c889a8 --- /dev/null +++ b/examples/simpleish/seatrial.lua @@ -0,0 +1,15 @@ +-- uses https://tieske.github.io/date/, a pure-Lua date library +local date = require('date') + +function generate_30_day_range() + local today = date(true) + local plus30 = today:copy():adddays(30) + return { + start_date = today:fmt('%F'), + end_date = plus30:fmt('%F'), + } +end + +return { + generate_30_day_range = generate_30_day_range, +} diff --git a/examples/simpleish/seatrial.ron b/examples/simpleish/seatrial.ron index 5d9386f..27bd11a 100644 --- a/examples/simpleish/seatrial.ron +++ b/examples/simpleish/seatrial.ron @@ -1,4 +1,6 @@ ( + lua_file: "seatrial.lua", + grunts: [ ( base_name: "Reloader Grunt", diff --git a/src/config_duration.rs b/src/config_duration.rs new file mode 100644 index 0000000..764a7c7 --- /dev/null +++ b/src/config_duration.rs @@ -0,0 +1,18 @@ +use nanoserde::DeRon; + +use std::time::Duration; + +#[derive(Clone, Debug, DeRon)] +pub enum ConfigDuration { + Milliseconds(u64), + Seconds(u64), +} + +impl From<&ConfigDuration> for Duration { + fn from(src: &ConfigDuration) -> Self { + match src { + ConfigDuration::Milliseconds(ms) => Duration::from_millis(*ms), + ConfigDuration::Seconds(ms) => Duration::from_secs(*ms), + } + } +} diff --git a/src/grunt.rs b/src/grunt.rs new file mode 100644 index 0000000..cef5783 --- /dev/null +++ b/src/grunt.rs @@ -0,0 +1,33 @@ +use nanoserde::DeRon; + +use std::fmt::Display; + +// build out of a GruntSpec during Situation construction +#[derive(Clone, Debug)] +pub struct Grunt { + pub name: String, + pub persona_idx: usize, +} + +#[derive(Clone, Debug, DeRon)] +pub struct GruntSpec { + pub base_name: Option, + pub persona: String, + pub count: Option, +} + +impl GruntSpec { + pub fn formatted_name(&self, uniqueness: impl Display) -> String { + format!( + "{} {}", + self.base_name + .clone() + .unwrap_or_else(|| format!("Grunt<{}>", self.persona)), + uniqueness, + ) + } + + pub fn real_count(&self) -> usize { + self.count.unwrap_or(1) + } +} diff --git a/src/main.rs b/src/main.rs index dab330b..cd6b814 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,18 +1,37 @@ use argh::FromArgs; -use nanoserde::{DeRon, DeRonErr}; use rlua::Lua; -use ureq::AgentBuilder; +use ureq::{Agent, AgentBuilder}; use url::Url; use std::collections::HashMap; -use std::fs::{canonicalize, read_to_string}; -use std::path::PathBuf; use std::str::FromStr; use std::sync::{mpsc, Arc, Barrier}; use std::thread; use std::thread::JoinHandle; use std::time::Duration; +mod config_duration; +mod grunt; +mod persona; +mod pipeline; +mod pipeline_action; +mod situation; +mod step_goto; +mod step_http; +mod step_lua; + +use crate::grunt::Grunt; +use crate::persona::Persona; +use crate::pipeline::{PipeContents, StepCompletion, StepError}; +use crate::pipeline_action::PipelineAction; +use crate::situation::{Situation, SituationSpec}; +use crate::step_goto::step as do_step_goto; +use crate::step_http::{ + step_delete as do_step_http_delete, step_get as do_step_http_get, + step_head as do_step_http_head, step_post as do_step_http_post, step_put as do_step_http_put, +}; +use crate::step_lua::step_function as do_step_lua_function; + /// situational-mock-based load testing #[derive(FromArgs)] struct CmdArgs { @@ -37,296 +56,6 @@ struct CmdArgs { situations: Vec, } -// built out of a SituationSpec after post-parse contextual validations have been run -#[derive(Clone, Debug)] -struct Situation { - base_url: Url, - lua_file: Option, - grunts: Vec, - personas: Vec, -} - -impl Situation { - fn from_spec( - spec: &SituationSpec, - base_url: &Url, - grunt_multiplier: usize, - ) -> Result { - let mut relocated_personas: HashMap<&str, usize> = - HashMap::with_capacity(spec.personas.len()); - let personas = spec - .personas - .iter() - .enumerate() - .map(|(idx, (name, spec))| { - relocated_personas.insert(name, idx); - - Persona { - name: name.clone(), - spec: spec.clone(), - - // TODO: populate with Value/LuaFunction returns, error on other PipelineAction - // variants - headers: HashMap::new(), - } - }) - .collect(); - let grunts = { - let mut slot: usize = 0; - let mut grunts: Vec = Vec::with_capacity( - spec.grunts - .iter() - .map(|grunt| grunt.real_count() * grunt_multiplier) - .sum(), - ); - - for (idx, grunt_spec) in spec.grunts.iter().enumerate() { - let num_grunts = grunt_spec.real_count(); - if num_grunts < 1 { - return Err(SituationParseErr { - kind: SituationParseErrKind::Semantics { - message: "if provided, grunt count must be >=1".into(), - location: format!("grunts[{}]", idx), - }, - }); - } - let num_grunts = num_grunts * grunt_multiplier; - - match relocated_personas.get(&*grunt_spec.persona) { - Some(persona_idx) => { - for _ in 0..num_grunts { - grunts.push(Grunt { - name: grunt_spec.formatted_name(slot), - persona_idx: *persona_idx, - }); - slot += 1; - } - } - None => { - return Err(SituationParseErr { - kind: SituationParseErrKind::Semantics { - message: format!( - "grunt refers to non-existent persona \"{}\"", - grunt_spec.persona - ), - location: format!("grunts[{}]", idx), - }, - }); - } - } - } - - grunts - }; - - Ok(Self { - base_url: base_url.clone(), - lua_file: spec - .lua_file - .as_deref() - .and_then(|file| canonicalize(file).map(Some).unwrap_or(None)), - grunts, - personas, - }) - } -} - -#[derive(Clone, Debug, DeRon)] -struct SituationSpec { - lua_file: Option, - grunts: Vec, - personas: HashMap, -} - -// build out of a GruntSpec during Situation construction -#[derive(Clone, Debug)] -struct Grunt { - name: String, - persona_idx: usize, -} - -#[derive(Clone, Debug, DeRon)] -struct GruntSpec { - base_name: Option, - persona: String, - count: Option, -} - -impl GruntSpec { - pub fn formatted_name(&self, uniqueness: impl std::fmt::Display) -> String { - format!( - "{} {}", - self.base_name - .clone() - .unwrap_or_else(|| format!("Grunt<{}>", self.persona)), - uniqueness, - ) - } - - pub fn real_count(&self) -> usize { - self.count.unwrap_or(1) - } -} - -// built out of a PersonaSpec during Situation construction -#[derive(Clone, Debug)] -struct Persona { - name: String, - spec: PersonaSpec, - headers: HashMap, -} - -#[derive(Clone, Debug, DeRon)] -struct PersonaSpec { - timeout: ConfigDuration, - headers: Option>, - pipeline: Vec, -} - -#[derive(Clone, Debug, DeRon)] -enum ConfigDuration { - Milliseconds(u64), - Seconds(u64), -} - -impl From<&ConfigDuration> for Duration { - fn from(src: &ConfigDuration) -> Self { - match src { - ConfigDuration::Milliseconds(ms) => Duration::from_millis(*ms), - ConfigDuration::Seconds(ms) => Duration::from_secs(*ms), - } - } -} - -#[derive(Clone, Debug, DeRon)] -enum PipelineAction { - GoTo { - index: usize, - max_times: Option, - }, - // this is mostly used for URL params, since those _can_ come from Lua, and thus have to be a - // PipelineAction member - Value(String), - - // http verbs. this section could be fewer LOC with macros eg - // https://stackoverflow.com/a/37007315/17630058, but (1) this is still manageable (there's - // only a few HTTP verbs), and (2) rust macros are cryptic enough to a passer-by that if we're - // going to introduce them and their mental overhead to this codebase (other than depending on - // a few from crates), we should have a strong reason (and perhaps multiple usecases). - - // TODO: figure out what, if anything, are appropriate guardrails for a PATCH verb - Delete { - url: String, - headers: Option>, - params: Option>, - timeout_ms: Option, - }, - Get { - url: String, - headers: Option>, - params: Option>, - timeout_ms: Option, - }, - Head { - url: String, - headers: Option>, - params: Option>, - timeout_ms: Option, - }, - Post { - url: String, - headers: Option>, - params: Option>, - timeout_ms: Option, - }, - Put { - url: String, - headers: Option>, - params: Option>, - timeout_ms: Option, - }, - - // validations of whatever the current thing in the pipe is. Asserts are generally fatal when - // falsey, except in the context of an AnyOf or NoneOf combinator, which can "catch" the errors - // as appropriate. WarnUnless validations are never fatal and likewise can never fail a - // combinator - AssertHeaderExists(String), - AssertStatusCode(u16), - AssertStatusCodeInRange(u16, u16), - WarnUnlessHeaderExists(String), - WarnUnlessStatusCode(u16), - WarnUnlessStatusCodeInRange(u16, u16), - - // basic logic. rust doesn't allow something like - // All(AssertStatusCode|AssertStatusCodeInRange), so instead, **any** PipelineAction is a valid - // member of a combinator for now, which is less than ideal ergonomically to say the least - AllOf(Vec), - AnyOf(Vec), - NoneOf(Vec), - - // the "Here Be Dragons" section, for when dynamism is absolutely needed: an escape hatch to - // Lua. TODO: document the Lua APIs and semantics... - LuaFunction(String), - LuaValue, - LuaTableIndex(usize), - LuaTableValue(String), -} - -#[derive(Debug)] -struct SituationParseErr { - kind: SituationParseErrKind, -} - -#[derive(Debug)] -enum SituationParseErrKind { - IO(std::io::Error), - Parsing(DeRonErr), - Semantics { - message: String, - location: String, // should we try to refer back to line numbers in the config somehow? - }, -} - -impl SituationParseErr { - pub fn message(&self) -> String { - match &self.kind { - SituationParseErrKind::IO(err) => err.to_string(), - SituationParseErrKind::Parsing(err) => err.to_string(), - SituationParseErrKind::Semantics { message, .. } => message.clone(), - } - } -} - -impl std::fmt::Display for SituationParseErr { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "SituationParseErr: {}", self.message()) - } -} - -impl From for SituationParseErr { - fn from(src: std::io::Error) -> Self { - Self { - kind: SituationParseErrKind::IO(src), - } - } -} - -impl From for SituationParseErr { - fn from(src: DeRonErr) -> Self { - Self { - kind: SituationParseErrKind::Parsing(src), - } - } -} - -impl FromStr for SituationSpec { - type Err = SituationParseErr; - - fn from_str(it: &str) -> Result { - Ok(DeRon::deserialize_ron(&read_to_string(canonicalize(it)?)?)?) - } -} - fn main() -> std::io::Result<()> { let args = { let mut args: CmdArgs = argh::from_env(); @@ -400,22 +129,230 @@ fn grunt_worker( grunt: &Grunt, tx: mpsc::Sender, ) { - let lua = Lua::new(); + let mut lua = Lua::new(); + + if let Some(file) = situation.lua_file.as_ref() { + let fpath = if let Some(parent) = file.parent() { + let mut ret = parent.to_path_buf(); + ret.push("?.lua"); + ret.to_string_lossy().into_owned() + } else { + file.to_string_lossy().into_owned() + }; + let fname = file + .file_stem() + .unwrap_or_else(|| file.as_os_str()) + .to_string_lossy(); + + // TODO: something cleaner than unwrap() here + lua.context(|ctx| { + ctx.load(&format!( + "package.path = package.path .. \";{}\"; user_script = require('{}')", + fpath, fname + )) + .set_name(&format!("user_script<{}, {}>", grunt.name, fpath))? + .exec() + }) + .unwrap_or_else(|err| { + eprintln!("[{}] aborting due to lua error", grunt.name); + eprintln!("[{}] err was: {}", grunt.name, err); + panic!(); + }); + } + let persona = &situation.personas[grunt.persona_idx]; let agent = AgentBuilder::new() .timeout((&persona.spec.timeout).into()) .build(); - let vals = vec![ - String::from("hi"), - String::from("from"), - String::from("the"), - String::from("thread"), - ]; + let vals = vec![]; barrier.wait(); + let mut current_pipe_contents: Option = None; + let mut current_pipe_idx: usize = 0; + let mut goto_counters: HashMap = HashMap::with_capacity( + persona + .spec + .pipeline + .iter() + .filter(|step| matches!(step, PipelineAction::GoTo { .. })) + .count(), + ); + + loop { + if let Some(step) = &persona.spec.pipeline.get(current_pipe_idx) { + match do_step( + step, + current_pipe_idx, + &situation.base_url, + persona, + &mut lua, + &agent, + current_pipe_contents.as_ref(), + &mut goto_counters, + ) { + Ok(StepCompletion::Success { + next_index, + pipe_data, + }) => { + current_pipe_contents = pipe_data; + current_pipe_idx = next_index; + } + Ok(StepCompletion::SuccessWithWarnings { + next_index, + pipe_data, + }) => { + // TODO: log event for warnings + current_pipe_contents = pipe_data; + current_pipe_idx = next_index; + } + Err(StepError::Unclassified) => { + eprintln!( + "[{}] aborting due to unclassified error in pipeline", + grunt.name + ); + eprintln!( + "[{}] this is an error in seatrial - TODO fix this", + grunt.name + ); + eprintln!("[{}] step was: {:?}", grunt.name, step); + break; + } + Err(StepError::InvalidActionInContext) => { + eprintln!( + "[{}] aborting due to invalid action definition in the given context", + grunt.name + ); + eprintln!( + "[{}] that this was not caught in a linter run is an error in seatrial - TODO fix this", + grunt.name + ); + eprintln!("[{}] step was: {:?}", grunt.name, step); + break; + } + Err(StepError::LuaException(err)) => { + eprintln!("[{}] aborting due to lua error", grunt.name); + eprintln!("[{}] err was: {}", grunt.name, err); + eprintln!("[{}] step was: {:?}", grunt.name, step); + break; + } + Err(StepError::UrlParsing(err)) => { + eprintln!("[{}] aborting due to url parsing error", grunt.name); + eprintln!("[{}] err was: {}", grunt.name, err); + eprintln!("[{}] step was: {:?}", grunt.name, step); + break; + } + Err(StepError::Http(err)) => { + eprintln!("[{}] aborting due to http error", grunt.name); + eprintln!("[{}] err was: {}", grunt.name, err); + eprintln!("[{}] step was: {:?}", grunt.name, step); + break; + } + } + } else { + eprintln!("[{}] reached end of pipeline, goodbye!", grunt.name); + break; + } + } + for val in vals { tx.send(val).unwrap(); thread::sleep(Duration::from_secs(1)); } } + +fn do_step( + step: &PipelineAction, + idx: usize, + base_url: &Url, + persona: &Persona, + lua: &mut Lua, + agent: &Agent, + _last: Option<&PipeContents>, + goto_counters: &mut HashMap, +) -> Result { + match step { + PipelineAction::GoTo { index, max_times } => { + do_step_goto(*index, *max_times, persona, goto_counters) + } + PipelineAction::LuaTableIndex(..) + | PipelineAction::LuaTableValue(..) + | PipelineAction::LuaValue => Err(StepError::InvalidActionInContext), + PipelineAction::LuaFunction(fname) => do_step_lua_function(idx, fname, lua), + PipelineAction::Delete { + url, + headers, + params, + timeout, + } => do_step_http_delete( + idx, + base_url, + url, + headers.as_ref(), + params.as_ref(), + timeout.as_ref(), + agent, + ), + PipelineAction::Get { + url, + headers, + params, + timeout, + } => do_step_http_get( + idx, + base_url, + url, + headers.as_ref(), + params.as_ref(), + timeout.as_ref(), + agent, + ), + PipelineAction::Head { + url, + headers, + params, + timeout, + } => do_step_http_head( + idx, + base_url, + url, + headers.as_ref(), + params.as_ref(), + timeout.as_ref(), + agent, + ), + PipelineAction::Post { + url, + headers, + params, + timeout, + } => do_step_http_post( + idx, + base_url, + url, + headers.as_ref(), + params.as_ref(), + timeout.as_ref(), + agent, + ), + PipelineAction::Put { + url, + headers, + params, + timeout, + } => do_step_http_put( + idx, + base_url, + url, + headers.as_ref(), + params.as_ref(), + timeout.as_ref(), + agent, + ), + // TODO: remove + _ => Ok(StepCompletion::Success { + next_index: idx + 1, + pipe_data: None, + }), + } +} diff --git a/src/persona.rs b/src/persona.rs new file mode 100644 index 0000000..b842501 --- /dev/null +++ b/src/persona.rs @@ -0,0 +1,21 @@ +use nanoserde::DeRon; + +use std::collections::HashMap; + +use crate::config_duration::ConfigDuration; +use crate::pipeline_action::{ConfigActionMap, PipelineAction}; + +// built out of a PersonaSpec during Situation construction +#[derive(Clone, Debug)] +pub struct Persona { + pub name: String, + pub spec: PersonaSpec, + pub headers: HashMap, +} + +#[derive(Clone, Debug, DeRon)] +pub struct PersonaSpec { + pub timeout: ConfigDuration, + pub headers: Option, + pub pipeline: Vec, +} diff --git a/src/pipeline.rs b/src/pipeline.rs new file mode 100644 index 0000000..2898ebc --- /dev/null +++ b/src/pipeline.rs @@ -0,0 +1,35 @@ +use rlua::Error as LuaError; + +#[derive(Debug)] +pub enum PipeContents { + HttpResponse(ureq::Response), + LuaReference(String), +} + +#[derive(Debug)] +pub enum StepCompletion { + Success { + next_index: usize, + pipe_data: Option, + }, + SuccessWithWarnings { + next_index: usize, + pipe_data: Option, + }, +} + +#[derive(Debug)] +pub enum StepError { + // TODO: this is a placeholder to replace former empty struct init, remove + Unclassified, + InvalidActionInContext, + LuaException(LuaError), + UrlParsing(url::ParseError), + Http(ureq::Error), +} + +impl From for StepError { + fn from(src: LuaError) -> Self { + Self::LuaException(src) + } +} diff --git a/src/pipeline_action.rs b/src/pipeline_action.rs new file mode 100644 index 0000000..31cb2de --- /dev/null +++ b/src/pipeline_action.rs @@ -0,0 +1,81 @@ +use nanoserde::DeRon; + +use std::collections::HashMap; + +use crate::config_duration::ConfigDuration; + +pub type ConfigActionMap = HashMap; + +#[derive(Clone, Debug, DeRon)] +pub enum PipelineAction { + GoTo { + index: usize, + max_times: Option, + }, + // this is mostly used for URL params, since those _can_ come from Lua, and thus have to be a + // PipelineAction member + Value(String), + + // http verbs. this section could be fewer LOC with macros eg + // https://stackoverflow.com/a/37007315/17630058, but (1) this is still manageable (there's + // only a few HTTP verbs), and (2) rust macros are cryptic enough to a passer-by that if we're + // going to introduce them and their mental overhead to this codebase (other than depending on + // a few from crates), we should have a strong reason (and perhaps multiple usecases). + + // TODO: figure out what, if anything, are appropriate guardrails for a PATCH verb + Delete { + url: String, + headers: Option, + params: Option, + timeout: Option, + }, + Get { + url: String, + headers: Option, + params: Option, + timeout: Option, + }, + Head { + url: String, + headers: Option, + params: Option, + timeout: Option, + }, + Post { + url: String, + headers: Option, + params: Option, + timeout: Option, + }, + Put { + url: String, + headers: Option, + params: Option, + timeout: Option, + }, + + // validations of whatever the current thing in the pipe is. Asserts are generally fatal when + // falsey, except in the context of an AnyOf or NoneOf combinator, which can "catch" the errors + // as appropriate. WarnUnless validations are never fatal and likewise can never fail a + // combinator + AssertHeaderExists(String), + AssertStatusCode(u16), + AssertStatusCodeInRange(u16, u16), + WarnUnlessHeaderExists(String), + WarnUnlessStatusCode(u16), + WarnUnlessStatusCodeInRange(u16, u16), + + // basic logic. rust doesn't allow something like + // All(AssertStatusCode|AssertStatusCodeInRange), so instead, **any** PipelineAction is a valid + // member of a combinator for now, which is less than ideal ergonomically to say the least + AllOf(Vec), + AnyOf(Vec), + NoneOf(Vec), + + // the "Here Be Dragons" section, for when dynamism is absolutely needed: an escape hatch to + // Lua. TODO: document the Lua APIs and semantics... + LuaFunction(String), + LuaValue, + LuaTableIndex(usize), + LuaTableValue(String), +} diff --git a/src/situation.rs b/src/situation.rs new file mode 100644 index 0000000..ea04c87 --- /dev/null +++ b/src/situation.rs @@ -0,0 +1,214 @@ +use nanoserde::{DeRon, DeRonErr}; +use url::Url; + +use std::collections::HashMap; +use std::ffi::OsString; +use std::fs::{canonicalize, read_to_string}; +use std::path::PathBuf; +use std::str::FromStr; + +use crate::grunt::{Grunt, GruntSpec}; +use crate::persona::{Persona, PersonaSpec}; + +// built out of a SituationSpec after post-parse contextual validations have been run +#[derive(Clone, Debug)] +pub struct Situation { + pub base_url: Url, + pub lua_file: Option, + pub grunts: Vec, + pub personas: Vec, +} + +impl Situation { + pub fn from_spec( + spec: &SituationSpec, + base_url: &Url, + grunt_multiplier: usize, + ) -> Result { + let mut relocated_personas: HashMap<&str, usize> = + HashMap::with_capacity(spec.contents.personas.len()); + let personas = spec + .contents + .personas + .iter() + .enumerate() + .map(|(idx, (name, spec))| { + relocated_personas.insert(name, idx); + + Persona { + name: name.into(), + spec: spec.clone(), + + // TODO: populate with Value/LuaFunction returns, error on other PipelineAction + // variants + headers: HashMap::new(), + } + }) + .collect(); + let grunts = { + let mut slot: usize = 0; + let mut grunts: Vec = Vec::with_capacity( + spec.contents + .grunts + .iter() + .map(|grunt| grunt.real_count() * grunt_multiplier) + .sum(), + ); + + for (idx, grunt_spec) in spec.contents.grunts.iter().enumerate() { + let num_grunts = grunt_spec.real_count(); + if num_grunts < 1 { + return Err(SituationParseErr { + kind: SituationParseErrKind::Semantics { + message: "if provided, grunt count must be >=1".into(), + location: format!("grunts[{}]", idx), + }, + }); + } + let num_grunts = num_grunts * grunt_multiplier; + + match relocated_personas.get(&*grunt_spec.persona) { + Some(persona_idx) => { + for _ in 0..num_grunts { + grunts.push(Grunt { + name: grunt_spec.formatted_name(slot), + persona_idx: *persona_idx, + }); + slot += 1; + } + } + None => { + return Err(SituationParseErr { + kind: SituationParseErrKind::Semantics { + message: format!( + "grunt refers to non-existent persona \"{}\"", + grunt_spec.persona + ), + location: format!("grunts[{}]", idx), + }, + }); + } + } + } + + grunts + }; + + Ok(Self { + base_url: base_url.clone(), + lua_file: { + // this comical chain attempts to canonicalize a given string, presuming it's a + // path to a file. if that fails, it will just pass the given string through to lua + // unchanged (perhaps we're requiring a lua library from elsewhere in the search + // path, or a native sofile, or whatever). Nones get passed all the way through, + // skipping the entire song and dance + spec.contents.lua_file.as_ref().map(|file| { + let rel_path = { + let mut rel_base = PathBuf::from(&spec.source); + rel_base.pop(); + rel_base.push(file); + rel_base + }; + + eprintln!("rel_path: {:?}", rel_path); + + let canon = canonicalize(rel_path); + + if let Ok(path) = canon { + path + } else { + if let Some(provided) = spec.contents.lua_file.as_ref() { + eprintln!("[situation parser] error canonicalizing provided lua_file \"{}\" to a path, passing through to lua unmodified", provided); + } + + PathBuf::from(file) + } + }) + }, + grunts, + personas, + }) + } +} + +#[derive(Clone, Debug, DeRon)] +pub struct SituationSpec { + source: String, + contents: SituationSpecContents, +} + +impl FromStr for SituationSpec { + type Err = SituationParseErr; + + fn from_str(it: &str) -> Result { + let source = canonicalize(it)?; + Ok(Self { + contents: DeRon::deserialize_ron(&read_to_string(&source)?)?, + source: source.into_os_string().into_string()?, + }) + } +} + +#[derive(Clone, Debug, DeRon)] +pub struct SituationSpecContents { + lua_file: Option, + grunts: Vec, + personas: HashMap, +} + +#[derive(Debug)] +pub struct SituationParseErr { + kind: SituationParseErrKind, +} + +#[derive(Debug)] +pub enum SituationParseErrKind { + Inspecific(OsString), + IO(std::io::Error), + Parsing(DeRonErr), + Semantics { + message: String, + location: String, // should we try to refer back to line numbers in the config somehow? + }, +} + +impl SituationParseErr { + pub fn message(&self) -> String { + match &self.kind { + SituationParseErrKind::Inspecific(msg) => msg.to_string_lossy().into(), + SituationParseErrKind::IO(err) => err.to_string(), + SituationParseErrKind::Parsing(err) => err.to_string(), + SituationParseErrKind::Semantics { message, .. } => message.clone(), + } + } +} + +impl std::fmt::Display for SituationParseErr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "SituationParseErr: {}", self.message()) + } +} + +impl From for SituationParseErr { + fn from(src: std::io::Error) -> Self { + Self { + kind: SituationParseErrKind::IO(src), + } + } +} + +impl From for SituationParseErr { + fn from(src: DeRonErr) -> Self { + Self { + kind: SituationParseErrKind::Parsing(src), + } + } +} + +impl From for SituationParseErr { + fn from(src: OsString) -> Self { + Self { + kind: SituationParseErrKind::Inspecific(src), + } + } +} diff --git a/src/step_goto.rs b/src/step_goto.rs new file mode 100644 index 0000000..2f067b4 --- /dev/null +++ b/src/step_goto.rs @@ -0,0 +1,21 @@ +use std::collections::HashMap; + +use crate::persona::Persona; +use crate::pipeline::{StepCompletion, StepError}; + +pub fn step( + desired_index: usize, + max_times: Option, + persona: &Persona, + goto_counters: &mut HashMap, +) -> Result { + if desired_index > persona.spec.pipeline.len() { + // TODO: provide details (expand enum to allow) + return Err(StepError::Unclassified); + } + + Ok(StepCompletion::Success { + next_index: desired_index, + pipe_data: None, + }) +} diff --git a/src/step_http.rs b/src/step_http.rs new file mode 100644 index 0000000..6a743bc --- /dev/null +++ b/src/step_http.rs @@ -0,0 +1,170 @@ +use ureq::Agent; +use url::Url; + +use crate::config_duration::ConfigDuration; +use crate::pipeline::{PipeContents, StepCompletion, StepError}; +use crate::pipeline_action::ConfigActionMap; + +#[derive(Debug)] +enum Verb { + Delete, + Get, + Head, + Post, + Put, +} + +pub fn step_delete( + idx: usize, + base_url: &Url, + path: &str, + headers: Option<&ConfigActionMap>, + params: Option<&ConfigActionMap>, + timeout: Option<&ConfigDuration>, + agent: &Agent, +) -> Result { + step( + Verb::Delete, + idx, + base_url, + path, + headers, + params, + timeout, + agent, + ) +} + +pub fn step_get( + idx: usize, + base_url: &Url, + path: &str, + headers: Option<&ConfigActionMap>, + params: Option<&ConfigActionMap>, + timeout: Option<&ConfigDuration>, + agent: &Agent, +) -> Result { + step( + Verb::Get, + idx, + base_url, + path, + headers, + params, + timeout, + agent, + ) +} + +pub fn step_head( + idx: usize, + base_url: &Url, + path: &str, + headers: Option<&ConfigActionMap>, + params: Option<&ConfigActionMap>, + timeout: Option<&ConfigDuration>, + agent: &Agent, +) -> Result { + step( + Verb::Head, + idx, + base_url, + path, + headers, + params, + timeout, + agent, + ) +} + +pub fn step_post( + idx: usize, + base_url: &Url, + path: &str, + headers: Option<&ConfigActionMap>, + params: Option<&ConfigActionMap>, + timeout: Option<&ConfigDuration>, + agent: &Agent, +) -> Result { + step( + Verb::Post, + idx, + base_url, + path, + headers, + params, + timeout, + agent, + ) +} + +pub fn step_put( + idx: usize, + base_url: &Url, + path: &str, + headers: Option<&ConfigActionMap>, + params: Option<&ConfigActionMap>, + timeout: Option<&ConfigDuration>, + agent: &Agent, +) -> Result { + step( + Verb::Put, + idx, + base_url, + path, + headers, + params, + timeout, + agent, + ) +} + +fn step( + verb: Verb, + idx: usize, + base_url: &Url, + path: &str, + headers: Option<&ConfigActionMap>, + params: Option<&ConfigActionMap>, + timeout: Option<&ConfigDuration>, + agent: &Agent, +) -> Result { + let stringified_path = &path.to_string(); + + base_url + .join(path) + .map_err(StepError::UrlParsing) + .and_then(|url| { + request_common( + match verb { + Verb::Delete => agent.delete(stringified_path), + Verb::Get => agent.get(stringified_path), + Verb::Head => agent.head(stringified_path), + Verb::Post => agent.post(stringified_path), + Verb::Put => agent.put(stringified_path), + }, + timeout, + idx, + headers, + params, + ) + }) +} + +fn request_common( + mut req: ureq::Request, + timeout: Option<&ConfigDuration>, + idx: usize, + _headers: Option<&ConfigActionMap>, + _params: Option<&ConfigActionMap>, +) -> Result { + if let Some(timeout) = timeout { + req = req.timeout(timeout.into()) + } + req.call() + .map(|response| StepCompletion::Success { + next_index: idx + 1, + pipe_data: Some(PipeContents::HttpResponse(response)), + }) + .map_err(StepError::Http) +} diff --git a/src/step_lua.rs b/src/step_lua.rs new file mode 100644 index 0000000..8b24218 --- /dev/null +++ b/src/step_lua.rs @@ -0,0 +1,25 @@ +use rlua::{Error as LuaError, Lua}; + +use crate::pipeline::{PipeContents, StepCompletion, StepError}; + +pub fn step_function(idx: usize, fname: &str, lua: &mut Lua) -> Result { + lua.context(|ctx| { + let ref_name = format!("pipeline_call_{}", fname); + let globals = ctx.globals(); + + eprintln!("running lua function {}", fname); + + ctx.load(&format!( + "{} = user_script[\"{}\"]() -- TODO: add last", + ref_name, fname + )) + .set_name(&format!("pipeline action<{}>", fname))? + .exec()?; + + Ok(StepCompletion::Success { + next_index: idx + 1, + pipe_data: Some(PipeContents::LuaReference(ref_name)), + }) + }) + .map_err(|err: LuaError| err.into()) +} From 2be13c77b3cbf3d4ee24b368f8eb71068ad498dd Mon Sep 17 00:00:00 2001 From: Josh Klar Date: Thu, 13 Jan 2022 18:53:53 -0800 Subject: [PATCH 04/14] bump msrv to just-released 1.58, may as well --- Cargo.toml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b380475..ac9ddff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "seatrial" version = "0.1.0" edition = "2021" -rust-version = "1.57" +rust-version = "1.58" [dependencies] argh = "0.1" diff --git a/README.md b/README.md index 4d725dd..72af279 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ indeed, for helping developers make such scale events, Non-Events). ## Development and Packaging -> MSRV: 1.57, as specified in `Cargo.toml` +> MSRV: 1.58, as specified in `Cargo.toml` ## Legal From 2ea80a9ee0835ec284d300869c986ade4efdf7bc Mon Sep 17 00:00:00 2001 From: Josh Klar Date: Tue, 18 Jan 2022 17:26:46 -0800 Subject: [PATCH 05/14] mostly cleanup, start working on passing Last values around --- Cargo.lock | 174 ++++++++++++++++++++++++++++++++ Cargo.toml | 13 +++ examples/simpleish/seatrial.ron | 10 +- examples/simpleish/server.rs | 94 +++++++++++++++++ src/main.rs | 136 +++++++++++++------------ src/pipeline.rs | 14 ++- src/pipeline_action.rs | 56 +++++++--- src/shared_lua.rs | 1 + src/situation.rs | 18 ++-- src/step_goto.rs | 11 +- src/step_http.rs | 33 ++++-- src/step_lua.rs | 19 ++-- 12 files changed, 460 insertions(+), 119 deletions(-) create mode 100644 examples/simpleish/server.rs create mode 100644 src/shared_lua.rs diff --git a/Cargo.lock b/Cargo.lock index 380edf4..0997f07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,6 +82,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "chunked_transfer" version = "1.4.0" @@ -97,6 +107,38 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e54ea8bc3fb1ee042f5aace6e3c6e025d3874866da222930f70ce62aceba0bfa" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcae03edb34f947e64acdb1c33ec169824e20657e9ecb61cef6c8c74dcb8120" +dependencies = [ + "cfg-if", + "lazy_static", +] + +[[package]] +name = "enum_dispatch" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd53b3fde38a39a06b2e66dc282f3e86191e53bd04cc499929c15742beae3df8" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "flate2" version = "1.0.22" @@ -119,6 +161,17 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "getrandom" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "heck" version = "0.3.3" @@ -128,6 +181,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "idna" version = "0.2.3" @@ -160,6 +222,15 @@ version = "0.2.112" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" +[[package]] +name = "libc-strftime" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3badb12f3d8623017f2cff9de476ff32f426ef45419253366fa088b8df6364cf" +dependencies = [ + "libc", +] + [[package]] name = "log" version = "0.4.14" @@ -206,6 +277,16 @@ version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290eec6719d68aef1f5ca0e695f8ad6421adcf8791fc17641c4ccc6c5388fb39" +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.14" @@ -215,6 +296,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "once_cell" version = "1.9.0" @@ -227,6 +318,12 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + [[package]] name = "proc-macro2" version = "1.0.36" @@ -245,6 +342,46 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" +dependencies = [ + "rand_core", +] + [[package]] name = "ring" version = "0.16.20" @@ -300,11 +437,15 @@ name = "seatrial" version = "0.1.0" dependencies = [ "argh", + "chrono", + "enum_dispatch", "nanoserde", + "rand", "rlua", "strfmt", "ureq", "url", + "vial", ] [[package]] @@ -330,6 +471,18 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "threadfin" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e4b647a40fefe14511723ec89557e4469fb273f59dc13deccf48385091b45a1" +dependencies = [ + "crossbeam-channel", + "num_cpus", + "once_cell", + "waker-fn", +] + [[package]] name = "tinyvec" version = "1.5.1" @@ -407,6 +560,27 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "vial" +version = "0.1.11" +source = "git+https://github.com/sigaloid/vial#3dfa4959f19d149f8e7a848e672d2361b8b2b7af" +dependencies = [ + "libc-strftime", + "threadfin", +] + +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + +[[package]] +name = "wasi" +version = "0.10.3+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a2e384a3f170b0c7543787a91411175b71afd56ba4d3a0ae5678d4e2243c0e" + [[package]] name = "wasm-bindgen" version = "0.2.78" diff --git a/Cargo.toml b/Cargo.toml index ac9ddff..6472f3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,8 +6,21 @@ rust-version = "1.58" [dependencies] argh = "0.1" +enum_dispatch = "0.3" nanoserde = "0.1" rlua = "0.18" strfmt = "0.1" ureq = "2.4" url = "2.2" + +[dev-dependencies] +chrono = { version = "0.4", default-features = false, features = ["std"] } +rand = "0.8" +vial = "0.1" + +[patch.crates-io] +vial = { git = "https://github.com/sigaloid/vial" } + +[[example]] +name = "simpleish" +path = "examples/simpleish/server.rs" diff --git a/examples/simpleish/seatrial.ron b/examples/simpleish/seatrial.ron index 27bd11a..77d905c 100644 --- a/examples/simpleish/seatrial.ron +++ b/examples/simpleish/seatrial.ron @@ -15,19 +15,19 @@ pipeline: [ LuaFunction("generate_30_day_range"), - Get( + Http(Get( url: "/calendar", params: { "start_date": LuaTableValue("start_date"), "end_date": LuaTableValue("end_date"), }, - ), - AllOf([ + )), + Combinator(AllOf([ WarnUnlessStatusCodeInRange(200, 299), WarnUnlessHeaderExists("X-Never-Gonna-Give-You-Up"), LuaFunction("was_valid_esoteric_format"), - ]), - GoTo(index: 0), + ])), + ControlFlow(GoTo(index: 0)), ], ), }, diff --git a/examples/simpleish/server.rs b/examples/simpleish/server.rs new file mode 100644 index 0000000..1c8f62e --- /dev/null +++ b/examples/simpleish/server.rs @@ -0,0 +1,94 @@ +use chrono::prelude::*; +use vial::prelude::*; + +use std::error::Error; +use std::fmt::{Display, Formatter}; +use std::time::Duration; + +routes! { + GET "/calendar" => calendar; +} + +#[derive(Debug)] +enum CalendarError { + UnparseableDates { + source: CalendarErrorSource, + query_param: &'static str, + }, +} + +#[derive(Debug)] +enum CalendarErrorSource { + Chrono(chrono::ParseError), + Vial, +} + +impl Display for CalendarError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Calendar Error: {}", + match self { + CalendarError::UnparseableDates { + query_param, + source, + } => format!( + "query param unparseable as date: {} ({:?})", + query_param, source + ), + } + ) + } +} + +impl Error for CalendarError {} + +fn calendar(req: Request) -> Result { + let start_date: NaiveDate = req + .query("start_date") + .ok_or(()) + .map(|date| NaiveDate::parse_from_str(date, "%Y-%m-%d")) + .map_err(|_| CalendarError::UnparseableDates { + source: CalendarErrorSource::Vial, + query_param: "start_date", + })? + .map_err(|err| CalendarError::UnparseableDates { + source: CalendarErrorSource::Chrono(err), + query_param: "start_date", + })?; + let end_date: NaiveDate = req + .query("end_date") + .ok_or(()) + .map(|date| NaiveDate::parse_from_str(date, "%Y-%m-%d")) + .map_err(|_| CalendarError::UnparseableDates { + source: CalendarErrorSource::Vial, + query_param: "end_date", + })? + .map_err(|err| CalendarError::UnparseableDates { + source: CalendarErrorSource::Chrono(err), + query_param: "end_date", + })?; + + let diff = end_date - start_date; + + // sleep for up to two seconds to simulate doing work + let artificial_delay = Duration::from_millis((rand::random::() * 2000.00).round() as u64); + std::thread::sleep(artificial_delay); + + // some contrived esoteric format just designed to give the LuaFunction validator something + // worth doing + Ok(format!( + "DAYS {} SYEAR {} EYEAR {} SMON {} EMON {} SDAY {} EDAY {}", + diff.num_days(), + start_date.year(), + end_date.year(), + start_date.month(), + end_date.month(), + start_date.day(), + end_date.day(), + )) +} + +fn main() { + vial::run!().unwrap(); +} diff --git a/src/main.rs b/src/main.rs index cd6b814..14ba228 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,7 @@ mod grunt; mod persona; mod pipeline; mod pipeline_action; +mod shared_lua; mod situation; mod step_goto; mod step_http; @@ -23,7 +24,7 @@ mod step_lua; use crate::grunt::Grunt; use crate::persona::Persona; use crate::pipeline::{PipeContents, StepCompletion, StepError}; -use crate::pipeline_action::PipelineAction; +use crate::pipeline_action::{Combinator, ControlFlow, Http, PipelineAction as PA, Reference, Validator}; use crate::situation::{Situation, SituationSpec}; use crate::step_goto::step as do_step_goto; use crate::step_http::{ @@ -175,7 +176,7 @@ fn grunt_worker( .spec .pipeline .iter() - .filter(|step| matches!(step, PipelineAction::GoTo { .. })) + .filter(|step| matches!(step, PA::ControlFlow(ControlFlow::GoTo { .. }))) .count(), ); @@ -191,14 +192,18 @@ fn grunt_worker( current_pipe_contents.as_ref(), &mut goto_counters, ) { - Ok(StepCompletion::Success { + Ok(StepCompletion::WithExit) => { + grunt_exit(grunt); + break; + } + Ok(StepCompletion::Normal { next_index, pipe_data, }) => { current_pipe_contents = pipe_data; current_pipe_idx = next_index; } - Ok(StepCompletion::SuccessWithWarnings { + Ok(StepCompletion::WithWarnings { next_index, pipe_data, }) => { @@ -250,7 +255,7 @@ fn grunt_worker( } } } else { - eprintln!("[{}] reached end of pipeline, goodbye!", grunt.name); + grunt_exit(grunt); break; } } @@ -261,96 +266,99 @@ fn grunt_worker( } } +fn grunt_exit(grunt: &Grunt) { + eprintln!("[{}] reached end of pipeline, goodbye!", grunt.name); +} + fn do_step( - step: &PipelineAction, + step: &PA, idx: usize, base_url: &Url, persona: &Persona, lua: &mut Lua, agent: &Agent, - _last: Option<&PipeContents>, + last: Option<&PipeContents>, goto_counters: &mut HashMap, ) -> Result { match step { - PipelineAction::GoTo { index, max_times } => { - do_step_goto(*index, *max_times, persona, goto_counters) + PA::ControlFlow(ControlFlow::GoTo { index, max_times }) => { + if let Some(times) = max_times { + if *times == 0 { + // TODO: should probably warn here, or just outright disallow this (either by a + // bounded integral type rather than usize, or by failing at lint time) + return Ok(StepCompletion::WithExit); + } + + match goto_counters.get(index) { + Some(rem) => { + if *rem == 0 { + return Ok(StepCompletion::WithExit); + } + } + None => { + goto_counters.insert(*index, times - 1); + } + }; + } + + do_step_goto(*index, persona) } - PipelineAction::LuaTableIndex(..) - | PipelineAction::LuaTableValue(..) - | PipelineAction::LuaValue => Err(StepError::InvalidActionInContext), - PipelineAction::LuaFunction(fname) => do_step_lua_function(idx, fname, lua), - PipelineAction::Delete { + PA::Reference(Reference::LuaTableIndex(..)) + | PA::Reference(Reference::LuaTableValue(..)) + | PA::Reference(Reference::LuaValue) => Err(StepError::InvalidActionInContext), + PA::LuaFunction(fname) => do_step_lua_function(idx, fname, lua, last), + act @ (PA::Http(Http::Delete { url, headers, params, timeout, - } => do_step_http_delete( - idx, - base_url, - url, - headers.as_ref(), - params.as_ref(), - timeout.as_ref(), - agent, - ), - PipelineAction::Get { + }) + | PA::Http(Http::Get { url, headers, params, timeout, - } => do_step_http_get( - idx, - base_url, - url, - headers.as_ref(), - params.as_ref(), - timeout.as_ref(), - agent, - ), - PipelineAction::Head { + }) + | PA::Http(Http::Head { url, headers, params, timeout, - } => do_step_http_head( - idx, - base_url, - url, - headers.as_ref(), - params.as_ref(), - timeout.as_ref(), - agent, - ), - PipelineAction::Post { + }) + | PA::Http(Http::Post { url, headers, params, timeout, - } => do_step_http_post( - idx, - base_url, - url, - headers.as_ref(), - params.as_ref(), - timeout.as_ref(), - agent, - ), - PipelineAction::Put { + }) + | PA::Http(Http::Put { url, headers, params, timeout, - } => do_step_http_put( - idx, - base_url, - url, - headers.as_ref(), - params.as_ref(), - timeout.as_ref(), - agent, - ), + })) => { + let method = match act { + PA::Http(Http::Delete { .. }) => do_step_http_delete, + PA::Http(Http::Get { .. }) => do_step_http_get, + PA::Http(Http::Head { .. }) => do_step_http_head, + PA::Http(Http::Post { .. }) => do_step_http_post, + PA::Http(Http::Put { .. }) => do_step_http_put, + _ => unreachable!(), + }; + + method( + idx, + base_url, + url, + headers.as_ref(), + params.as_ref(), + timeout.as_ref(), + agent, + last, + ) + } // TODO: remove - _ => Ok(StepCompletion::Success { + _ => Ok(StepCompletion::Normal { next_index: idx + 1, pipe_data: None, }), diff --git a/src/pipeline.rs b/src/pipeline.rs index 2898ebc..53a5695 100644 --- a/src/pipeline.rs +++ b/src/pipeline.rs @@ -6,16 +6,26 @@ pub enum PipeContents { LuaReference(String), } +impl PipeContents { + pub fn to_lua_string(&self) -> String { + match self { + PipeContents::HttpResponse(_) => unimplemented!(), + PipeContents::LuaReference(lref) => lref.into(), + } + } +} + #[derive(Debug)] pub enum StepCompletion { - Success { + Normal { next_index: usize, pipe_data: Option, }, - SuccessWithWarnings { + WithWarnings { next_index: usize, pipe_data: Option, }, + WithExit, } #[derive(Debug)] diff --git a/src/pipeline_action.rs b/src/pipeline_action.rs index 31cb2de..219784e 100644 --- a/src/pipeline_action.rs +++ b/src/pipeline_action.rs @@ -1,21 +1,32 @@ +use enum_dispatch::enum_dispatch; use nanoserde::DeRon; use std::collections::HashMap; use crate::config_duration::ConfigDuration; -pub type ConfigActionMap = HashMap; +pub type ConfigActionMap = HashMap; +// allow "all have same postfix" to pass since these names pass directly through to the config file +// (thus become a ux implication) +#[allow(clippy::enum_variant_names)] #[derive(Clone, Debug, DeRon)] -pub enum PipelineAction { +pub enum Combinator { + AllOf(Vec), + AnyOf(Vec), + NoneOf(Vec), +} + +#[derive(Clone, Debug, DeRon)] +pub enum ControlFlow { GoTo { index: usize, max_times: Option, }, - // this is mostly used for URL params, since those _can_ come from Lua, and thus have to be a - // PipelineAction member - Value(String), +} +#[derive(Clone, Debug, DeRon)] +pub enum Http { // http verbs. this section could be fewer LOC with macros eg // https://stackoverflow.com/a/37007315/17630058, but (1) this is still manageable (there's // only a few HTTP verbs), and (2) rust macros are cryptic enough to a passer-by that if we're @@ -53,7 +64,21 @@ pub enum PipelineAction { params: Option, timeout: Option, }, +} + +#[derive(Clone, Debug, DeRon)] +pub enum Reference { + // this is mostly used for URL params, since those _can_ come from Lua, and thus have to be a + // PipelineAction member + Value(String), + LuaValue, + LuaTableIndex(usize), + LuaTableValue(String), +} + +#[derive(Clone, Debug, DeRon)] +pub enum Validator { // validations of whatever the current thing in the pipe is. Asserts are generally fatal when // falsey, except in the context of an AnyOf or NoneOf combinator, which can "catch" the errors // as appropriate. WarnUnless validations are never fatal and likewise can never fail a @@ -65,17 +90,16 @@ pub enum PipelineAction { WarnUnlessStatusCode(u16), WarnUnlessStatusCodeInRange(u16, u16), - // basic logic. rust doesn't allow something like - // All(AssertStatusCode|AssertStatusCodeInRange), so instead, **any** PipelineAction is a valid - // member of a combinator for now, which is less than ideal ergonomically to say the least - AllOf(Vec), - AnyOf(Vec), - NoneOf(Vec), + LuaFunction(String), +} - // the "Here Be Dragons" section, for when dynamism is absolutely needed: an escape hatch to - // Lua. TODO: document the Lua APIs and semantics... +#[enum_dispatch] +#[derive(Clone, Debug, DeRon)] +pub enum PipelineAction { + Combinator, + ControlFlow, + Http, LuaFunction(String), - LuaValue, - LuaTableIndex(usize), - LuaTableValue(String), + Reference, + Validator, } diff --git a/src/shared_lua.rs b/src/shared_lua.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/shared_lua.rs @@ -0,0 +1 @@ + diff --git a/src/situation.rs b/src/situation.rs index ea04c87..e8c702a 100644 --- a/src/situation.rs +++ b/src/situation.rs @@ -97,22 +97,18 @@ impl Situation { Ok(Self { base_url: base_url.clone(), lua_file: { - // this comical chain attempts to canonicalize a given string, presuming it's a - // path to a file. if that fails, it will just pass the given string through to lua - // unchanged (perhaps we're requiring a lua library from elsewhere in the search - // path, or a native sofile, or whatever). Nones get passed all the way through, - // skipping the entire song and dance + // this attempts to canonicalize a given string, presuming it's a path to a file. + // if that fails, it will just pass the given string through to lua unchanged + // (perhaps we're requiring a lua library from elsewhere in the search path, or a + // native sofile, or whatever). Nones get passed all the way through, skipping the + // entire song and dance spec.contents.lua_file.as_ref().map(|file| { - let rel_path = { + let canon = canonicalize({ let mut rel_base = PathBuf::from(&spec.source); rel_base.pop(); rel_base.push(file); rel_base - }; - - eprintln!("rel_path: {:?}", rel_path); - - let canon = canonicalize(rel_path); + }); if let Ok(path) = canon { path diff --git a/src/step_goto.rs b/src/step_goto.rs index 2f067b4..834d675 100644 --- a/src/step_goto.rs +++ b/src/step_goto.rs @@ -1,20 +1,13 @@ -use std::collections::HashMap; - use crate::persona::Persona; use crate::pipeline::{StepCompletion, StepError}; -pub fn step( - desired_index: usize, - max_times: Option, - persona: &Persona, - goto_counters: &mut HashMap, -) -> Result { +pub fn step(desired_index: usize, persona: &Persona) -> Result { if desired_index > persona.spec.pipeline.len() { // TODO: provide details (expand enum to allow) return Err(StepError::Unclassified); } - Ok(StepCompletion::Success { + Ok(StepCompletion::Normal { next_index: desired_index, pipe_data: None, }) diff --git a/src/step_http.rs b/src/step_http.rs index 6a743bc..6f4b1b0 100644 --- a/src/step_http.rs +++ b/src/step_http.rs @@ -22,6 +22,7 @@ pub fn step_delete( params: Option<&ConfigActionMap>, timeout: Option<&ConfigDuration>, agent: &Agent, + last: Option<&PipeContents>, ) -> Result { step( Verb::Delete, @@ -32,6 +33,7 @@ pub fn step_delete( params, timeout, agent, + last, ) } @@ -43,6 +45,7 @@ pub fn step_get( params: Option<&ConfigActionMap>, timeout: Option<&ConfigDuration>, agent: &Agent, + last: Option<&PipeContents>, ) -> Result { step( Verb::Get, @@ -53,6 +56,7 @@ pub fn step_get( params, timeout, agent, + last, ) } @@ -64,6 +68,7 @@ pub fn step_head( params: Option<&ConfigActionMap>, timeout: Option<&ConfigDuration>, agent: &Agent, + last: Option<&PipeContents>, ) -> Result { step( Verb::Head, @@ -74,6 +79,7 @@ pub fn step_head( params, timeout, agent, + last, ) } @@ -85,6 +91,7 @@ pub fn step_post( params: Option<&ConfigActionMap>, timeout: Option<&ConfigDuration>, agent: &Agent, + last: Option<&PipeContents>, ) -> Result { step( Verb::Post, @@ -95,6 +102,7 @@ pub fn step_post( params, timeout, agent, + last, ) } @@ -106,6 +114,7 @@ pub fn step_put( params: Option<&ConfigActionMap>, timeout: Option<&ConfigDuration>, agent: &Agent, + last: Option<&PipeContents>, ) -> Result { step( Verb::Put, @@ -116,6 +125,7 @@ pub fn step_put( params, timeout, agent, + last, ) } @@ -128,13 +138,14 @@ fn step( params: Option<&ConfigActionMap>, timeout: Option<&ConfigDuration>, agent: &Agent, + last: Option<&PipeContents>, ) -> Result { - let stringified_path = &path.to_string(); - base_url .join(path) .map_err(StepError::UrlParsing) .and_then(|url| { + let stringified_path = &url.to_string(); + request_common( match verb { Verb::Delete => agent.delete(stringified_path), @@ -147,6 +158,7 @@ fn step( idx, headers, params, + last, ) }) } @@ -155,16 +167,25 @@ fn request_common( mut req: ureq::Request, timeout: Option<&ConfigDuration>, idx: usize, - _headers: Option<&ConfigActionMap>, - _params: Option<&ConfigActionMap>, + headers: Option<&ConfigActionMap>, + params: Option<&ConfigActionMap>, + last: Option<&PipeContents>, ) -> Result { if let Some(timeout) = timeout { req = req.timeout(timeout.into()) } + req.call() - .map(|response| StepCompletion::Success { + .map(|response| StepCompletion::Normal { next_index: idx + 1, pipe_data: Some(PipeContents::HttpResponse(response)), }) - .map_err(StepError::Http) + .or_else(|err| { + match err { + ureq::Error::Status(_, response) => Ok(StepCompletion::Normal { + next_index: idx + 1, + pipe_data: Some(PipeContents::HttpResponse(response)), + }), + ureq::Error::Transport(_) => Err(StepError::Http(err)), + }}) } diff --git a/src/step_lua.rs b/src/step_lua.rs index 8b24218..09197ca 100644 --- a/src/step_lua.rs +++ b/src/step_lua.rs @@ -2,21 +2,28 @@ use rlua::{Error as LuaError, Lua}; use crate::pipeline::{PipeContents, StepCompletion, StepError}; -pub fn step_function(idx: usize, fname: &str, lua: &mut Lua) -> Result { +pub fn step_function( + idx: usize, + fname: &str, + lua: &mut Lua, + last: Option<&PipeContents>, +) -> Result { lua.context(|ctx| { let ref_name = format!("pipeline_call_{}", fname); - let globals = ctx.globals(); - eprintln!("running lua function {}", fname); + let last_arg = match last { + Some(contents) => contents.to_lua_string(), + None => "nil".into(), + }; ctx.load(&format!( - "{} = user_script[\"{}\"]() -- TODO: add last", - ref_name, fname + "{} = user_script[\"{}\"]({})", + ref_name, fname, last_arg, )) .set_name(&format!("pipeline action<{}>", fname))? .exec()?; - Ok(StepCompletion::Success { + Ok(StepCompletion::Normal { next_index: idx + 1, pipe_data: Some(PipeContents::LuaReference(ref_name)), }) From e002aba26b9758170b058f9bba98dcb085156aa9 Mon Sep 17 00:00:00 2001 From: Josh Klar Date: Tue, 25 Jan 2022 20:28:50 -0800 Subject: [PATCH 06/14] checkpoint: lua values are being passed through to HTTP query params, these requests are legit! --- examples/simpleish/server.rs | 8 ++- src/http_response_table.rs | 105 +++++++++++++++++++++++++++++++ src/main.rs | 119 ++++++++++++++++++++++++----------- src/pipe_contents.rs | 74 ++++++++++++++++++++++ src/pipeline.rs | 33 +--------- src/pipeline_action.rs | 36 +++++++++++ src/shared_lua.rs | 20 ++++++ src/step_error.rs | 30 +++++++++ src/step_goto.rs | 3 +- src/step_http.rs | 87 ++++++++++++++++++------- src/step_lua.rs | 44 +++++++------ 11 files changed, 448 insertions(+), 111 deletions(-) create mode 100644 src/http_response_table.rs create mode 100644 src/pipe_contents.rs create mode 100644 src/step_error.rs diff --git a/examples/simpleish/server.rs b/examples/simpleish/server.rs index 1c8f62e..468f372 100644 --- a/examples/simpleish/server.rs +++ b/examples/simpleish/server.rs @@ -77,7 +77,7 @@ fn calendar(req: Request) -> Result { // some contrived esoteric format just designed to give the LuaFunction validator something // worth doing - Ok(format!( + let response = format!( "DAYS {} SYEAR {} EYEAR {} SMON {} EMON {} SDAY {} EDAY {}", diff.num_days(), start_date.year(), @@ -86,7 +86,11 @@ fn calendar(req: Request) -> Result { end_date.month(), start_date.day(), end_date.day(), - )) + ); + + eprintln!("debug: {}", response); + + Ok(response) } fn main() { diff --git a/src/http_response_table.rs b/src/http_response_table.rs new file mode 100644 index 0000000..1a9b9ab --- /dev/null +++ b/src/http_response_table.rs @@ -0,0 +1,105 @@ +use rlua::{Lua, RegistryKey}; + +use std::collections::HashMap; +use std::io::{Error as IOError, Result as IOResult}; + +use crate::pipe_contents::PipeContents; + +type HttpResponseTablePair = (&'static str, RegistryKey); + +#[derive(Clone, Debug)] +pub struct HttpResponseTable { + pub status_code: u16, + pub headers: HashMap, + pub content_type: String, + pub body: Vec, + pub body_string: Option, +} + +impl HttpResponseTable { + pub fn bind(self, lua: &Lua) -> BoundHttpResponseTable { + BoundHttpResponseTable { lua, table: self } + } +} + +impl TryFrom<&PipeContents> for HttpResponseTable { + type Error = IOError; + + fn try_from(it: &PipeContents) -> IOResult { + match it { + PipeContents::HttpResponse { + body, + content_type, + headers, + status_code, + } => Ok(Self { + body: body.clone(), + body_string: String::from_utf8(body.clone()).ok(), + content_type: content_type.clone(), + headers: headers.clone(), + status_code: *status_code, + }), + _ => unreachable!(), + } + } +} + +#[derive(Clone, Debug)] +pub struct BoundHttpResponseTable<'lua> { + lua: &'lua Lua, + table: HttpResponseTable, +} + +impl<'a> BoundHttpResponseTable<'a> { + fn iter(&'a self) -> BoundHttpResponseTableIter<'a> { + BoundHttpResponseTableIter::create(self) + } +} + +impl<'a> IntoIterator for &'a BoundHttpResponseTable<'a> { + type Item = HttpResponseTablePair; + type IntoIter = BoundHttpResponseTableIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +pub struct BoundHttpResponseTableIter<'a> { + child: &'a BoundHttpResponseTable<'a>, + iter_state: usize, +} + +impl<'a> BoundHttpResponseTableIter<'a> { + pub fn create(child: &'a BoundHttpResponseTable<'a>) -> Self { + Self { + child, + iter_state: 0, + } + } +} + +impl<'a> Iterator for BoundHttpResponseTableIter<'a> { + type Item = HttpResponseTablePair; + + fn next(&mut self) -> Option { + self.iter_state += 1; + match self.iter_state { + 1 => Some(( + "status_code", + self.child.lua.context(|ctx| { + ctx.create_registry_value(self.child.table.status_code) + .expect("should have created status_code integer in registry") + }), + )), + 2 => Some(( + "headers", + self.child.lua.context(|ctx| { + ctx.create_registry_value(self.child.table.headers.clone()) + .expect("should have created headers table in registry") + }), + )), + _ => None, + } + } +} diff --git a/src/main.rs b/src/main.rs index 14ba228..3b1a055 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ use argh::FromArgs; -use rlua::Lua; +use rlua::{Lua, RegistryKey}; use ureq::{Agent, AgentBuilder}; use url::Url; @@ -12,20 +12,25 @@ use std::time::Duration; mod config_duration; mod grunt; +mod http_response_table; mod persona; +mod pipe_contents; mod pipeline; mod pipeline_action; mod shared_lua; mod situation; +mod step_error; mod step_goto; mod step_http; mod step_lua; use crate::grunt::Grunt; use crate::persona::Persona; -use crate::pipeline::{PipeContents, StepCompletion, StepError}; -use crate::pipeline_action::{Combinator, ControlFlow, Http, PipelineAction as PA, Reference, Validator}; +use crate::pipe_contents::PipeContents; +use crate::pipeline::StepCompletion; +use crate::pipeline_action::{ControlFlow, Http, PipelineAction as PA, Reference}; use crate::situation::{Situation, SituationSpec}; +use crate::step_error::StepError; use crate::step_goto::step as do_step_goto; use crate::step_http::{ step_delete as do_step_http_delete, step_get as do_step_http_get, @@ -130,39 +135,54 @@ fn grunt_worker( grunt: &Grunt, tx: mpsc::Sender, ) { - let mut lua = Lua::new(); - - if let Some(file) = situation.lua_file.as_ref() { - let fpath = if let Some(parent) = file.parent() { - let mut ret = parent.to_path_buf(); - ret.push("?.lua"); - ret.to_string_lossy().into_owned() - } else { - file.to_string_lossy().into_owned() - }; - let fname = file - .file_stem() - .unwrap_or_else(|| file.as_os_str()) - .to_string_lossy(); - - // TODO: something cleaner than unwrap() here - lua.context(|ctx| { - ctx.load(&format!( - "package.path = package.path .. \";{}\"; user_script = require('{}')", - fpath, fname - )) - .set_name(&format!("user_script<{}, {}>", grunt.name, fpath))? - .exec() + let lua = Lua::new(); + + let user_script_registry_key = situation + .lua_file + .as_ref() + .map(|file| { + let fpath = if let Some(parent) = file.parent() { + let mut ret = parent.to_path_buf(); + ret.push("?.lua"); + ret.to_string_lossy().into_owned() + } else { + file.to_string_lossy().into_owned() + }; + let fname = file + .file_stem() + .unwrap_or_else(|| file.as_os_str()) + .to_string_lossy(); + + // TODO: something cleaner than unwrap() here + lua.context(|ctx| { + ctx + .load(&format!( + "package.path = package.path .. \";{}\"; _user_script = require('{}')", + fpath, fname + )) + .set_name(&format!("user_script<{}, {}>", grunt.name, fpath))? + .exec()?; + + let user_script = ctx.globals().get::<_, rlua::Table>("_user_script")?; + + Ok(ctx + .create_registry_value(user_script) + .expect("should have stored user script in registry")) + }) + .unwrap_or_else(|err: rlua::Error| { + eprintln!("[{}] aborting due to lua error", grunt.name); + eprintln!("[{}] err was: {}", grunt.name, err); + panic!(); + }) }) - .unwrap_or_else(|err| { - eprintln!("[{}] aborting due to lua error", grunt.name); - eprintln!("[{}] err was: {}", grunt.name, err); - panic!(); - }); - } + .unwrap(); // TODO: remove and handle non-extant case let persona = &situation.personas[grunt.persona_idx]; let agent = AgentBuilder::new() + .user_agent(&format!( + "seatrial/grunt={}/persona={}", + grunt.name, persona.name + )) .timeout((&persona.spec.timeout).into()) .build(); let vals = vec![]; @@ -187,7 +207,8 @@ fn grunt_worker( current_pipe_idx, &situation.base_url, persona, - &mut lua, + &lua, + &user_script_registry_key, &agent, current_pipe_contents.as_ref(), &mut goto_counters, @@ -235,6 +256,12 @@ fn grunt_worker( eprintln!("[{}] step was: {:?}", grunt.name, step); break; } + Err(StepError::IO(err)) => { + eprintln!("[{}] aborting due to internal IO error", grunt.name); + eprintln!("[{}] err was: {}", grunt.name, err); + eprintln!("[{}] step was: {:?}", grunt.name, step); + break; + } Err(StepError::LuaException(err)) => { eprintln!("[{}] aborting due to lua error", grunt.name); eprintln!("[{}] err was: {}", grunt.name, err); @@ -253,6 +280,19 @@ fn grunt_worker( eprintln!("[{}] step was: {:?}", grunt.name, step); break; } + Err(StepError::RefuseToStringifyComplexLuaValue) => { + eprintln!("[{}] aborting attempt to stringify complex lua value", grunt.name); + eprintln!("[{}] step was: {:?}", grunt.name, step); + break; + } + // TODO: FIXME this messaging is extremely hard to grok, I'd be pounding my head + // into the keyboard screaming obscenities if a tool offered me this as the sole + // debug output + Err(StepError::RequestedLuaValueWhereNoneExists) => { + eprintln!("[{}] aborting attempt to pass non-existent value to lua context", grunt.name); + eprintln!("[{}] step was: {:?}", grunt.name, step); + break; + } } } else { grunt_exit(grunt); @@ -270,12 +310,16 @@ fn grunt_exit(grunt: &Grunt) { eprintln!("[{}] reached end of pipeline, goodbye!", grunt.name); } -fn do_step( +fn do_step<'a>( step: &PA, idx: usize, base_url: &Url, persona: &Persona, - lua: &mut Lua, + + // TODO: merge into a combo struct + lua: &'a Lua, + user_script_registry_key: &'a RegistryKey, + agent: &Agent, last: Option<&PipeContents>, goto_counters: &mut HashMap, @@ -306,7 +350,9 @@ fn do_step( PA::Reference(Reference::LuaTableIndex(..)) | PA::Reference(Reference::LuaTableValue(..)) | PA::Reference(Reference::LuaValue) => Err(StepError::InvalidActionInContext), - PA::LuaFunction(fname) => do_step_lua_function(idx, fname, lua, last), + PA::LuaFunction(fname) => { + do_step_lua_function(idx, fname, lua, user_script_registry_key, last) + } act @ (PA::Http(Http::Delete { url, headers, @@ -355,6 +401,7 @@ fn do_step( timeout.as_ref(), agent, last, + lua, ) } // TODO: remove diff --git a/src/pipe_contents.rs b/src/pipe_contents.rs new file mode 100644 index 0000000..a234138 --- /dev/null +++ b/src/pipe_contents.rs @@ -0,0 +1,74 @@ +use rlua::{Lua, RegistryKey, Value as LuaValue}; + +use std::collections::HashMap; +use std::io::{Error as IOError, Read, Result as IOResult}; +use std::rc::Rc; + +use crate::http_response_table::HttpResponseTable; +use crate::step_error::StepError; + +#[derive(Debug)] +pub enum PipeContents { + HttpResponse { + body: Vec, + content_type: String, + headers: HashMap, + status_code: u16, + }, + LuaReference(Rc), +} + +impl PipeContents { + pub fn to_lua(&self, lua: &Lua) -> Result>, StepError> { + match self { + PipeContents::LuaReference(lref) => Ok(Some(lref.clone())), + res @ PipeContents::HttpResponse { .. } => lua.context(|ctx| { + let arg_table = ctx.create_table()?; + for (key, val_rkey) in &HttpResponseTable::try_from(res)?.bind(lua) { + arg_table.set(key, ctx.registry_value::(&val_rkey)?)?; + } + let registry_key = ctx.create_registry_value(arg_table)?; + Ok(Some(Rc::new(registry_key))) + }), + } + } +} + +impl TryFrom for PipeContents { + type Error = IOError; + + fn try_from(res: ureq::Response) -> IOResult { + Ok(Self::HttpResponse { + content_type: res.content_type().into(), + status_code: res.status(), + headers: { + let headers_names = res.headers_names(); + let mut headers = HashMap::with_capacity(headers_names.len()); + + for header_name in headers_names { + let header = res.header(&header_name); + + if let Some(header_val) = header { + headers.insert(header_name, header_val.into()); + } + } + + headers + }, + body: { + let len: Option = + res.header("Content-Length").and_then(|cl| cl.parse().ok()); + + let mut body: Vec = if let Some(capacity) = len { + Vec::with_capacity(capacity) + } else { + Vec::new() + }; + + res.into_reader().read_to_end(&mut body)?; + + body + }, + }) + } +} diff --git a/src/pipeline.rs b/src/pipeline.rs index 53a5695..0c8298e 100644 --- a/src/pipeline.rs +++ b/src/pipeline.rs @@ -1,19 +1,4 @@ -use rlua::Error as LuaError; - -#[derive(Debug)] -pub enum PipeContents { - HttpResponse(ureq::Response), - LuaReference(String), -} - -impl PipeContents { - pub fn to_lua_string(&self) -> String { - match self { - PipeContents::HttpResponse(_) => unimplemented!(), - PipeContents::LuaReference(lref) => lref.into(), - } - } -} +use crate::pipe_contents::PipeContents; #[derive(Debug)] pub enum StepCompletion { @@ -27,19 +12,3 @@ pub enum StepCompletion { }, WithExit, } - -#[derive(Debug)] -pub enum StepError { - // TODO: this is a placeholder to replace former empty struct init, remove - Unclassified, - InvalidActionInContext, - LuaException(LuaError), - UrlParsing(url::ParseError), - Http(ureq::Error), -} - -impl From for StepError { - fn from(src: LuaError) -> Self { - Self::LuaException(src) - } -} diff --git a/src/pipeline_action.rs b/src/pipeline_action.rs index 219784e..70f10c9 100644 --- a/src/pipeline_action.rs +++ b/src/pipeline_action.rs @@ -1,9 +1,13 @@ use enum_dispatch::enum_dispatch; use nanoserde::DeRon; +use rlua::Lua; use std::collections::HashMap; use crate::config_duration::ConfigDuration; +use crate::pipe_contents::PipeContents as PC; +use crate::shared_lua::try_stringify_lua_value; +use crate::step_error::StepError; pub type ConfigActionMap = HashMap; @@ -77,6 +81,38 @@ pub enum Reference { LuaTableValue(String), } +impl Reference { + pub fn try_into_string_given_pipe_data(&self, lua: &Lua, pipe_data: Option<&PC>) -> Result { + match self { + Reference::Value(it) => Ok(it.clone()), + Reference::LuaValue => match pipe_data { + None => Err(StepError::RequestedLuaValueWhereNoneExists), + // TODO: as with Unclassified itself, change this + Some(PC::HttpResponse { .. }) => Err(StepError::Unclassified), + Some(PC::LuaReference(rkey)) => lua.context(|ctx| { + try_stringify_lua_value(ctx.registry_value::(&rkey)) + }), + }, + Reference::LuaTableIndex(idx) => match pipe_data { + None => Err(StepError::RequestedLuaValueWhereNoneExists), + // TODO: as with Unclassified itself, change this + Some(PC::HttpResponse { .. }) => Err(StepError::Unclassified), + Some(PC::LuaReference(rkey)) => lua.context(|ctx| { + try_stringify_lua_value(ctx.registry_value::(&rkey)?.get(*idx)) + }), + }, + Reference::LuaTableValue(key) => match pipe_data { + None => Err(StepError::RequestedLuaValueWhereNoneExists), + // TODO: as with Unclassified itself, change this + Some(PC::HttpResponse { .. }) => Err(StepError::Unclassified), + Some(PC::LuaReference(rkey)) => lua.context(|ctx| { + try_stringify_lua_value(ctx.registry_value::(&rkey)?.get(key.clone())) + }), + }, + } + } +} + #[derive(Clone, Debug, DeRon)] pub enum Validator { // validations of whatever the current thing in the pipe is. Asserts are generally fatal when diff --git a/src/shared_lua.rs b/src/shared_lua.rs index 8b13789..16fbef7 100644 --- a/src/shared_lua.rs +++ b/src/shared_lua.rs @@ -1 +1,21 @@ +use rlua::{Error as LuaError, Value as LuaValue}; +use crate::step_error::StepError; + +pub fn try_stringify_lua_value(it: Result) -> Result { + match it { + Ok(LuaValue::Nil) => Err(StepError::RequestedLuaValueWhereNoneExists), + Ok(LuaValue::Boolean(val)) => Ok(val.to_string()), + Ok(LuaValue::Integer(val)) => Ok(val.to_string()), + Ok(LuaValue::Number(val)) => Ok(val.to_string()), + Ok(LuaValue::String(val)) => Ok(val.to_str()?.into()), + Ok( + LuaValue::Table(..) + | LuaValue::Function(..) + | LuaValue::UserData(..) + | LuaValue::LightUserData(..), + ) => Err(StepError::RefuseToStringifyComplexLuaValue), + Ok(LuaValue::Thread { .. } | LuaValue::Error {..}) => Err(StepError::RefuseToStringifyComplexLuaValue), + Err(err) => Err(err.into()), + } +} diff --git a/src/step_error.rs b/src/step_error.rs new file mode 100644 index 0000000..bb2e4b5 --- /dev/null +++ b/src/step_error.rs @@ -0,0 +1,30 @@ +use rlua::Error as LuaError; + +use std::io::Error as IOError; + +#[derive(Debug)] +pub enum StepError { + Http(ureq::Error), + IO(IOError), + InvalidActionInContext, + LuaException(LuaError), + RefuseToStringifyComplexLuaValue, + RequestedLuaValueWhereNoneExists, + + // TODO: this is a placeholder to replace former empty struct init, remove + Unclassified, + + UrlParsing(url::ParseError), +} + +impl From for StepError { + fn from(err: IOError) -> Self { + Self::IO(err) + } +} + +impl From for StepError { + fn from(src: LuaError) -> Self { + Self::LuaException(src) + } +} diff --git a/src/step_goto.rs b/src/step_goto.rs index 834d675..e93c731 100644 --- a/src/step_goto.rs +++ b/src/step_goto.rs @@ -1,5 +1,6 @@ use crate::persona::Persona; -use crate::pipeline::{StepCompletion, StepError}; +use crate::pipeline::StepCompletion; +use crate::step_error::StepError; pub fn step(desired_index: usize, persona: &Persona) -> Result { if desired_index > persona.spec.pipeline.len() { diff --git a/src/step_http.rs b/src/step_http.rs index 6f4b1b0..42cb691 100644 --- a/src/step_http.rs +++ b/src/step_http.rs @@ -1,9 +1,14 @@ -use ureq::Agent; +use rlua::Lua; +use ureq::{Agent, Request}; use url::Url; +use std::collections::HashMap; + use crate::config_duration::ConfigDuration; -use crate::pipeline::{PipeContents, StepCompletion, StepError}; +use crate::pipe_contents::PipeContents as PC; +use crate::pipeline::StepCompletion; use crate::pipeline_action::ConfigActionMap; +use crate::step_error::StepError; #[derive(Debug)] enum Verb { @@ -22,7 +27,8 @@ pub fn step_delete( params: Option<&ConfigActionMap>, timeout: Option<&ConfigDuration>, agent: &Agent, - last: Option<&PipeContents>, + last: Option<&PC>, + lua: &Lua, ) -> Result { step( Verb::Delete, @@ -34,6 +40,7 @@ pub fn step_delete( timeout, agent, last, + lua, ) } @@ -45,7 +52,8 @@ pub fn step_get( params: Option<&ConfigActionMap>, timeout: Option<&ConfigDuration>, agent: &Agent, - last: Option<&PipeContents>, + last: Option<&PC>, + lua: &Lua, ) -> Result { step( Verb::Get, @@ -57,6 +65,7 @@ pub fn step_get( timeout, agent, last, + lua, ) } @@ -68,7 +77,8 @@ pub fn step_head( params: Option<&ConfigActionMap>, timeout: Option<&ConfigDuration>, agent: &Agent, - last: Option<&PipeContents>, + last: Option<&PC>, + lua: &Lua, ) -> Result { step( Verb::Head, @@ -80,6 +90,7 @@ pub fn step_head( timeout, agent, last, + lua, ) } @@ -91,7 +102,8 @@ pub fn step_post( params: Option<&ConfigActionMap>, timeout: Option<&ConfigDuration>, agent: &Agent, - last: Option<&PipeContents>, + last: Option<&PC>, + lua: &Lua, ) -> Result { step( Verb::Post, @@ -103,6 +115,7 @@ pub fn step_post( timeout, agent, last, + lua, ) } @@ -114,7 +127,8 @@ pub fn step_put( params: Option<&ConfigActionMap>, timeout: Option<&ConfigDuration>, agent: &Agent, - last: Option<&PipeContents>, + last: Option<&PC>, + lua: &Lua, ) -> Result { step( Verb::Put, @@ -126,6 +140,7 @@ pub fn step_put( timeout, agent, last, + lua, ) } @@ -138,7 +153,8 @@ fn step( params: Option<&ConfigActionMap>, timeout: Option<&ConfigDuration>, agent: &Agent, - last: Option<&PipeContents>, + last: Option<&PC>, + lua: &Lua, ) -> Result { base_url .join(path) @@ -159,33 +175,62 @@ fn step( headers, params, last, + lua, ) }) } fn request_common( - mut req: ureq::Request, + mut req: Request, timeout: Option<&ConfigDuration>, idx: usize, headers: Option<&ConfigActionMap>, params: Option<&ConfigActionMap>, - last: Option<&PipeContents>, + last: Option<&PC>, + lua: &Lua, ) -> Result { if let Some(timeout) = timeout { req = req.timeout(timeout.into()) } + for (key, val) in build_request_hashmap(headers, lua, last)? { + req = req.set(&key, &val); + } + + for (key, val) in build_request_hashmap(params, lua, last)? { + req = req.query(&key, &val); + } + req.call() - .map(|response| StepCompletion::Normal { - next_index: idx + 1, - pipe_data: Some(PipeContents::HttpResponse(response)), + .and_then(|response| { + Ok(StepCompletion::Normal { + next_index: idx + 1, + pipe_data: Some(response.try_into()?), + }) + }) + .or_else(|err| match err { + ureq::Error::Status(_, response) => Ok(StepCompletion::Normal { + next_index: idx + 1, + pipe_data: Some(response.try_into()?), + }), + ureq::Error::Transport(_) => Err(StepError::Http(err)), }) - .or_else(|err| { - match err { - ureq::Error::Status(_, response) => Ok(StepCompletion::Normal { - next_index: idx + 1, - pipe_data: Some(PipeContents::HttpResponse(response)), - }), - ureq::Error::Transport(_) => Err(StepError::Http(err)), - }}) +} + +fn build_request_hashmap( + base_spec: Option<&ConfigActionMap>, + lua: &Lua, + pipe_data: Option<&PC>, +) -> Result, StepError> { + if let Some(base) = base_spec { + let mut ret = HashMap::with_capacity(base.len()); + + for (key, href) in base { + ret.insert(key.clone(), href.try_into_string_given_pipe_data(lua, pipe_data)?); + } + + Ok(ret) + } else { + Ok(HashMap::new()) + } } diff --git a/src/step_lua.rs b/src/step_lua.rs index 09197ca..ad16767 100644 --- a/src/step_lua.rs +++ b/src/step_lua.rs @@ -1,32 +1,38 @@ -use rlua::{Error as LuaError, Lua}; +use rlua::{Lua, RegistryKey}; -use crate::pipeline::{PipeContents, StepCompletion, StepError}; +use std::rc::Rc; -pub fn step_function( +use crate::pipe_contents::PipeContents; +use crate::pipeline::StepCompletion; +use crate::step_error::StepError; + +pub fn step_function<'a>( idx: usize, fname: &str, - lua: &mut Lua, - last: Option<&PipeContents>, + + // TODO: merge into a combo struct + lua: &'a Lua, + user_script_registry_key: &'a RegistryKey, + + last: Option<&'a PipeContents>, ) -> Result { lua.context(|ctx| { - let ref_name = format!("pipeline_call_{}", fname); - - let last_arg = match last { - Some(contents) => contents.to_lua_string(), - None => "nil".into(), + let lua_func = ctx + .registry_value::(user_script_registry_key)? + .get::<_, rlua::Function>(fname)?; + let script_arg = match last { + Some(lval) => match lval.to_lua(lua)? { + Some(rkey) => ctx.registry_value::(&rkey)?, + None => rlua::Nil, + }, + None => rlua::Nil, }; - - ctx.load(&format!( - "{} = user_script[\"{}\"]({})", - ref_name, fname, last_arg, - )) - .set_name(&format!("pipeline action<{}>", fname))? - .exec()?; + let result = lua_func.call::(script_arg)?; + let registry_key = ctx.create_registry_value(result)?; Ok(StepCompletion::Normal { next_index: idx + 1, - pipe_data: Some(PipeContents::LuaReference(ref_name)), + pipe_data: Some(PipeContents::LuaReference(Rc::new(registry_key))), }) }) - .map_err(|err: LuaError| err.into()) } From 50b3dedbc96a03d76c8386fdceb7d74876c2e4f6 Mon Sep 17 00:00:00 2001 From: Josh Klar Date: Tue, 25 Jan 2022 20:58:53 -0800 Subject: [PATCH 07/14] fix clippy lints --- src/pipeline_action.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pipeline_action.rs b/src/pipeline_action.rs index 70f10c9..cbe1bc3 100644 --- a/src/pipeline_action.rs +++ b/src/pipeline_action.rs @@ -90,7 +90,7 @@ impl Reference { // TODO: as with Unclassified itself, change this Some(PC::HttpResponse { .. }) => Err(StepError::Unclassified), Some(PC::LuaReference(rkey)) => lua.context(|ctx| { - try_stringify_lua_value(ctx.registry_value::(&rkey)) + try_stringify_lua_value(ctx.registry_value::(rkey)) }), }, Reference::LuaTableIndex(idx) => match pipe_data { @@ -98,7 +98,7 @@ impl Reference { // TODO: as with Unclassified itself, change this Some(PC::HttpResponse { .. }) => Err(StepError::Unclassified), Some(PC::LuaReference(rkey)) => lua.context(|ctx| { - try_stringify_lua_value(ctx.registry_value::(&rkey)?.get(*idx)) + try_stringify_lua_value(ctx.registry_value::(rkey)?.get(*idx)) }), }, Reference::LuaTableValue(key) => match pipe_data { @@ -106,7 +106,7 @@ impl Reference { // TODO: as with Unclassified itself, change this Some(PC::HttpResponse { .. }) => Err(StepError::Unclassified), Some(PC::LuaReference(rkey)) => lua.context(|ctx| { - try_stringify_lua_value(ctx.registry_value::(&rkey)?.get(key.clone())) + try_stringify_lua_value(ctx.registry_value::(rkey)?.get(key.clone())) }), }, } From 83185974dd5d9bd57daf013f950130cd99fd8e46 Mon Sep 17 00:00:00 2001 From: Josh Klar Date: Tue, 25 Jan 2022 21:01:52 -0800 Subject: [PATCH 08/14] rustfmt --- src/main.rs | 23 ++++++++++++++--------- src/pipeline_action.rs | 10 ++++++++-- src/shared_lua.rs | 4 +++- src/step_http.rs | 5 ++++- 4 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/main.rs b/src/main.rs index 3b1a055..fad1686 100644 --- a/src/main.rs +++ b/src/main.rs @@ -155,13 +155,12 @@ fn grunt_worker( // TODO: something cleaner than unwrap() here lua.context(|ctx| { - ctx - .load(&format!( - "package.path = package.path .. \";{}\"; _user_script = require('{}')", - fpath, fname - )) - .set_name(&format!("user_script<{}, {}>", grunt.name, fpath))? - .exec()?; + ctx.load(&format!( + "package.path = package.path .. \";{}\"; _user_script = require('{}')", + fpath, fname + )) + .set_name(&format!("user_script<{}, {}>", grunt.name, fpath))? + .exec()?; let user_script = ctx.globals().get::<_, rlua::Table>("_user_script")?; @@ -281,7 +280,10 @@ fn grunt_worker( break; } Err(StepError::RefuseToStringifyComplexLuaValue) => { - eprintln!("[{}] aborting attempt to stringify complex lua value", grunt.name); + eprintln!( + "[{}] aborting attempt to stringify complex lua value", + grunt.name + ); eprintln!("[{}] step was: {:?}", grunt.name, step); break; } @@ -289,7 +291,10 @@ fn grunt_worker( // into the keyboard screaming obscenities if a tool offered me this as the sole // debug output Err(StepError::RequestedLuaValueWhereNoneExists) => { - eprintln!("[{}] aborting attempt to pass non-existent value to lua context", grunt.name); + eprintln!( + "[{}] aborting attempt to pass non-existent value to lua context", + grunt.name + ); eprintln!("[{}] step was: {:?}", grunt.name, step); break; } diff --git a/src/pipeline_action.rs b/src/pipeline_action.rs index cbe1bc3..b6320a8 100644 --- a/src/pipeline_action.rs +++ b/src/pipeline_action.rs @@ -82,7 +82,11 @@ pub enum Reference { } impl Reference { - pub fn try_into_string_given_pipe_data(&self, lua: &Lua, pipe_data: Option<&PC>) -> Result { + pub fn try_into_string_given_pipe_data( + &self, + lua: &Lua, + pipe_data: Option<&PC>, + ) -> Result { match self { Reference::Value(it) => Ok(it.clone()), Reference::LuaValue => match pipe_data { @@ -106,7 +110,9 @@ impl Reference { // TODO: as with Unclassified itself, change this Some(PC::HttpResponse { .. }) => Err(StepError::Unclassified), Some(PC::LuaReference(rkey)) => lua.context(|ctx| { - try_stringify_lua_value(ctx.registry_value::(rkey)?.get(key.clone())) + try_stringify_lua_value( + ctx.registry_value::(rkey)?.get(key.clone()), + ) }), }, } diff --git a/src/shared_lua.rs b/src/shared_lua.rs index 16fbef7..757ff0b 100644 --- a/src/shared_lua.rs +++ b/src/shared_lua.rs @@ -15,7 +15,9 @@ pub fn try_stringify_lua_value(it: Result) -> Result Err(StepError::RefuseToStringifyComplexLuaValue), - Ok(LuaValue::Thread { .. } | LuaValue::Error {..}) => Err(StepError::RefuseToStringifyComplexLuaValue), + Ok(LuaValue::Thread { .. } | LuaValue::Error { .. }) => { + Err(StepError::RefuseToStringifyComplexLuaValue) + } Err(err) => Err(err.into()), } } diff --git a/src/step_http.rs b/src/step_http.rs index 42cb691..4ab149e 100644 --- a/src/step_http.rs +++ b/src/step_http.rs @@ -226,7 +226,10 @@ fn build_request_hashmap( let mut ret = HashMap::with_capacity(base.len()); for (key, href) in base { - ret.insert(key.clone(), href.try_into_string_given_pipe_data(lua, pipe_data)?); + ret.insert( + key.clone(), + href.try_into_string_given_pipe_data(lua, pipe_data)?, + ); } Ok(ret) From 55cf80b1d9f375462451b01f7119460108e26d18 Mon Sep 17 00:00:00 2001 From: Josh Klar Date: Tue, 25 Jan 2022 21:51:28 -0800 Subject: [PATCH 09/14] goto should stop after max_times --- src/main.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index fad1686..d718649 100644 --- a/src/main.rs +++ b/src/main.rs @@ -345,11 +345,12 @@ fn do_step<'a>( } } None => { - goto_counters.insert(*index, times - 1); + goto_counters.insert(*index, *times); } }; } + goto_counters.insert(*index, goto_counters.get(index).unwrap() - 1); do_step_goto(*index, persona) } PA::Reference(Reference::LuaTableIndex(..)) From 201f133449ef76f588f9be2c5c0f48e2dd1505ce Mon Sep 17 00:00:00 2001 From: Josh Klar Date: Tue, 25 Jan 2022 23:28:17 -0800 Subject: [PATCH 10/14] derp --- examples/simpleish/seatrial.ron | 2 +- src/main.rs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/simpleish/seatrial.ron b/examples/simpleish/seatrial.ron index 77d905c..0eae098 100644 --- a/examples/simpleish/seatrial.ron +++ b/examples/simpleish/seatrial.ron @@ -27,7 +27,7 @@ WarnUnlessHeaderExists("X-Never-Gonna-Give-You-Up"), LuaFunction("was_valid_esoteric_format"), ])), - ControlFlow(GoTo(index: 0)), + ControlFlow(GoTo(index: 0, max_times: 2)), ], ), }, diff --git a/src/main.rs b/src/main.rs index d718649..2e38196 100644 --- a/src/main.rs +++ b/src/main.rs @@ -348,9 +348,10 @@ fn do_step<'a>( goto_counters.insert(*index, *times); } }; + + goto_counters.insert(*index, goto_counters.get(index).unwrap() - 1); } - goto_counters.insert(*index, goto_counters.get(index).unwrap() - 1); do_step_goto(*index, persona) } PA::Reference(Reference::LuaTableIndex(..)) From b33d8d0e9005b2ef764b723053478fd08477d9bd Mon Sep 17 00:00:00 2001 From: Josh Klar Date: Tue, 25 Jan 2022 23:37:36 -0800 Subject: [PATCH 11/14] trivial --- src/pipe_contents.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pipe_contents.rs b/src/pipe_contents.rs index a234138..02e2546 100644 --- a/src/pipe_contents.rs +++ b/src/pipe_contents.rs @@ -46,9 +46,7 @@ impl TryFrom for PipeContents { let mut headers = HashMap::with_capacity(headers_names.len()); for header_name in headers_names { - let header = res.header(&header_name); - - if let Some(header_val) = header { + if let Some(header_val) = res.header(&header_name) { headers.insert(header_name, header_val.into()); } } From 5be94656864ebc0d4126b96b1f46f531dd1820b1 Mon Sep 17 00:00:00 2001 From: Josh Klar Date: Wed, 26 Jan 2022 00:19:39 -0800 Subject: [PATCH 12/14] shed an unused dependency, update the rest --- Cargo.lock | 68 +++++++++++++++++++----------------------- Cargo.toml | 4 +-- src/pipe_contents.rs | 5 ++-- src/pipeline_action.rs | 12 ++++---- 4 files changed, 41 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0997f07..657c5a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -100,9 +100,9 @@ checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" [[package]] name = "crc32fast" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "738c290dfaea84fc1ca15ad9c168d083b05a714e1efddd8edaab678dc28d2836" +checksum = "a2209c310e29876f7f0b2721e7e26b84aff178aa3da5d091f9bfbf47669e60e3" dependencies = [ "cfg-if", ] @@ -127,18 +127,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "enum_dispatch" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd53b3fde38a39a06b2e66dc282f3e86191e53bd04cc499929c15742beae3df8" -dependencies = [ - "once_cell", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "flate2" version = "1.0.22" @@ -203,9 +191,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.55" +version = "0.3.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04" dependencies = [ "wasm-bindgen", ] @@ -218,9 +206,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.112" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" +checksum = "b0005d08a8f7b65fb8073cb697aa0b12b631ed251ce73d862ce50eeb52ce3b50" [[package]] name = "libc-strftime" @@ -318,6 +306,12 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +[[package]] +name = "pico-args" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468" + [[package]] name = "ppv-lite86" version = "0.2.16" @@ -335,9 +329,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47aa80447ce4daf1717500037052af176af5d38cc3e571d9ec1c7353fc10c87d" +checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" dependencies = [ "proc-macro2", ] @@ -438,8 +432,8 @@ version = "0.1.0" dependencies = [ "argh", "chrono", - "enum_dispatch", "nanoserde", + "pico-args", "rand", "rlua", "strfmt", @@ -462,9 +456,9 @@ checksum = "b278b244ef7aa5852b277f52dd0c6cac3a109919e1f6d699adde63251227a30f" [[package]] name = "syn" -version = "1.0.85" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a684ac3dcd8913827e18cd09a68384ee66c1de24157e3c556c9ab16d85695fb7" +checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" dependencies = [ "proc-macro2", "quote", @@ -577,15 +571,15 @@ checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" [[package]] name = "wasi" -version = "0.10.3+wasi-snapshot-preview1" +version = "0.10.2+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a2e384a3f170b0c7543787a91411175b71afd56ba4d3a0ae5678d4e2243c0e" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" [[package]] name = "wasm-bindgen" -version = "0.2.78" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" +checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -593,9 +587,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.78" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" +checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca" dependencies = [ "bumpalo", "lazy_static", @@ -608,9 +602,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.78" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" +checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -618,9 +612,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.78" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" +checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc" dependencies = [ "proc-macro2", "quote", @@ -631,15 +625,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.78" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" +checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2" [[package]] name = "web-sys" -version = "0.3.55" +version = "0.3.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" +checksum = "c060b319f29dd25724f09a2ba1418f142f539b2be99fbf4d2d5a8f7330afb8eb" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index 6472f3a..a27168e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,9 +5,9 @@ edition = "2021" rust-version = "1.58" [dependencies] -argh = "0.1" -enum_dispatch = "0.3" +argh = "0.1" # TODO: try to remove to get rid of syn compile time nanoserde = "0.1" +pico-args = "0.4" rlua = "0.18" strfmt = "0.1" ureq = "2.4" diff --git a/src/pipe_contents.rs b/src/pipe_contents.rs index 02e2546..48e581f 100644 --- a/src/pipe_contents.rs +++ b/src/pipe_contents.rs @@ -1,4 +1,5 @@ use rlua::{Lua, RegistryKey, Value as LuaValue}; +use ureq::Response; use std::collections::HashMap; use std::io::{Error as IOError, Read, Result as IOResult}; @@ -34,10 +35,10 @@ impl PipeContents { } } -impl TryFrom for PipeContents { +impl TryFrom for PipeContents { type Error = IOError; - fn try_from(res: ureq::Response) -> IOResult { + fn try_from(res: Response) -> IOResult { Ok(Self::HttpResponse { content_type: res.content_type().into(), status_code: res.status(), diff --git a/src/pipeline_action.rs b/src/pipeline_action.rs index b6320a8..f089c26 100644 --- a/src/pipeline_action.rs +++ b/src/pipeline_action.rs @@ -1,4 +1,3 @@ -use enum_dispatch::enum_dispatch; use nanoserde::DeRon; use rlua::Lua; @@ -135,13 +134,12 @@ pub enum Validator { LuaFunction(String), } -#[enum_dispatch] #[derive(Clone, Debug, DeRon)] pub enum PipelineAction { - Combinator, - ControlFlow, - Http, + Combinator(Combinator), + ControlFlow(ControlFlow), + Http(Http), LuaFunction(String), - Reference, - Validator, + Reference(Reference), + Validator(Validator), } From a62281775ed0f3b56fe2eb9c49c52c987937fc44 Mon Sep 17 00:00:00 2001 From: Josh Klar Date: Wed, 26 Jan 2022 00:34:33 -0800 Subject: [PATCH 13/14] attempt to add clippy linting to CI --- .github/workflows/lint.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..2baa74b --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,13 @@ +on: push +name: Clippy +jobs: + clippy_check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - run: rustup install 1.58.0 # keep in sync with MSRV in README.md/Cargo.toml + - run: rustup component add clippy + - uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --all-features From b5f00891be0feea0fd1390151e64b6119d7924a1 Mon Sep 17 00:00:00 2001 From: Josh Klar Date: Wed, 26 Jan 2022 01:00:27 -0800 Subject: [PATCH 14/14] add some trivial, but still functional, tests --- src/config_duration.rs | 19 +++++++++++++++++++ src/grunt.rs | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/config_duration.rs b/src/config_duration.rs index 764a7c7..8807e31 100644 --- a/src/config_duration.rs +++ b/src/config_duration.rs @@ -8,6 +8,12 @@ pub enum ConfigDuration { Seconds(u64), } +impl From for Duration { + fn from(src: ConfigDuration) -> Self { + (&src).into() + } +} + impl From<&ConfigDuration> for Duration { fn from(src: &ConfigDuration) -> Self { match src { @@ -16,3 +22,16 @@ impl From<&ConfigDuration> for Duration { } } } + +#[test] +fn test_seconds() { + assert_eq!(Duration::from_secs(10), ConfigDuration::Seconds(10).into()); +} + +#[test] +fn test_milliseconds() { + assert_eq!( + Duration::from_millis(100), + ConfigDuration::Milliseconds(100).into() + ); +} diff --git a/src/grunt.rs b/src/grunt.rs index cef5783..dbeb61c 100644 --- a/src/grunt.rs +++ b/src/grunt.rs @@ -31,3 +31,36 @@ impl GruntSpec { self.count.unwrap_or(1) } } + +#[test] +fn test_formatted_name() { + let spec = GruntSpec { + base_name: Some("Jimbo Gruntseph".into()), + persona: "blahblah".into(), + count: None, + }; + + assert_eq!("Jimbo Gruntseph 1", spec.formatted_name(1)); +} + +#[test] +fn test_formatted_name_no_base() { + let spec = GruntSpec { + base_name: None, + persona: "blahblah".into(), + count: None, + }; + + assert_eq!("Grunt 1", spec.formatted_name(1)); +} + +#[test] +fn test_real_count() { + let spec = GruntSpec { + base_name: None, + persona: "blahblah".into(), + count: None, + }; + + assert_eq!(1, spec.real_count()); +}