Skip to content

Commit

Permalink
feat: begin lua-side stdlib, implement combinators & validators
Browse files Browse the repository at this point in the history
  • Loading branch information
klardotsh committed Feb 11, 2022
1 parent 909ad7b commit 3618f82
Show file tree
Hide file tree
Showing 17 changed files with 798 additions and 79 deletions.
11 changes: 11 additions & 0 deletions examples/simpleish/seatrial.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
-- uses https://tieske.github.io/date/, a pure-Lua date library
local date = require('date')

local ESOTERIC_FORMAT_REGEX = "^DAYS (%d+) SYEAR (%d+) EYEAR (%d+) SMON (%d+) EMON (%d+) SDAY (%d+) EDAY (%d+)$"

function generate_30_day_range()
local today = date(true)
local plus30 = today:copy():adddays(30)
Expand All @@ -10,6 +12,15 @@ function generate_30_day_range()
}
end

function was_valid_esoteric_format(arg)
if arg.body_string:match(ESOTERIC_FORMAT_REGEX) == nil then
return ValidationResult.Error("server responded with malformed body")
end

return ValidationResult.Ok()
end

return {
generate_30_day_range = generate_30_day_range,
was_valid_esoteric_format = was_valid_esoteric_format,
}
56 changes: 56 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use argh::FromArgs;

use crate::situation::SituationSpec;

/// situational-mock-based load testing
#[derive(FromArgs)]
struct CmdArgsBase {
/// 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<SituationSpec>,
}

#[derive(Clone, Debug)]
pub struct CmdArgs {
/// integral multiplier for grunt counts (minimum 1)
pub multiplier: usize,

/// base URL for all situations in this run
pub base_url: String,

/// paths to RON files in seatrial(5) situation config format
pub situations: Vec<SituationSpec>,
}

/// flatten situations into a single vec (see docs about CmdArgsBase::req_situation)
impl From<CmdArgsBase> for CmdArgs {
fn from(mut it: CmdArgsBase) -> Self {
it.situations.insert(0, it.req_situation.clone());

Self {
multiplier: it.multiplier,
base_url: it.base_url,
situations: it.situations,
}
}
}

pub fn parse_args() -> CmdArgs {
argh::from_env::<CmdArgsBase>().into()
}
17 changes: 7 additions & 10 deletions src/config_duration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,27 @@ pub enum ConfigDuration {
Seconds(u64),
}

impl From<ConfigDuration> for Duration {
fn from(src: ConfigDuration) -> Self {
(&src).into()
}
}

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),
ConfigDuration::Milliseconds(ms) => Self::from_millis(*ms),
ConfigDuration::Seconds(ms) => Self::from_secs(*ms),
}
}
}

#[test]
fn test_seconds() {
assert_eq!(Duration::from_secs(10), ConfigDuration::Seconds(10).into());
assert_eq!(
Duration::from_secs(10),
(&ConfigDuration::Seconds(10)).into()
);
}

#[test]
fn test_milliseconds() {
assert_eq!(
Duration::from_millis(100),
ConfigDuration::Milliseconds(100).into()
(&ConfigDuration::Milliseconds(100)).into()
);
}
21 changes: 21 additions & 0 deletions src/http_response_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,27 @@ impl<'a> Iterator for BoundHttpResponseTableIter<'a> {
.expect("should have created headers table in registry")
}),
)),
3 => Some((
"content_type",
self.child.lua.context(|ctx| {
ctx.create_registry_value(self.child.table.content_type.clone())
.expect("should have created content_type string in registry")
}),
)),
4 => Some((
"body",
self.child.lua.context(|ctx| {
ctx.create_registry_value(self.child.table.body.clone())
.expect("should have created body table in registry")
}),
)),
5 => Some((
"body_string",
self.child.lua.context(|ctx| {
ctx.create_registry_value(self.child.table.body_string.clone())
.expect("should have created body_string nilable-string in registry")
}),
)),
_ => None,
}
}
Expand Down
84 changes: 49 additions & 35 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use argh::FromArgs;
use rlua::{Lua, RegistryKey};
use ureq::{Agent, AgentBuilder};
use url::Url;
Expand All @@ -10,6 +9,7 @@ use std::thread;
use std::thread::JoinHandle;
use std::time::Duration;

mod cli;
mod config_duration;
mod grunt;
mod http_response_table;
Expand All @@ -19,55 +19,32 @@ mod pipeline;
mod pipeline_action;
mod shared_lua;
mod situation;
mod step_combinator;
mod step_error;
mod step_goto;
mod step_http;
mod step_lua;
mod step_validator;

use crate::cli::parse_args;
use crate::grunt::Grunt;
use crate::persona::Persona;
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::shared_lua::attach_seatrial_stdlib;
use crate::situation::Situation;
use crate::step_combinator::step as do_step_combinator;
use crate::step_error::{StepError, StepResult};
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 {
/// 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<SituationSpec>,
}

fn main() -> std::io::Result<()> {
let args = {
let mut args: CmdArgs = argh::from_env();
args.situations.insert(0, args.req_situation.clone());
args
};
let args = parse_args();

// TODO: no unwrap, which will also kill the nasty parens
let base_url = (if args.base_url.ends_with('/') {
Expand Down Expand Up @@ -135,7 +112,9 @@ fn grunt_worker(
grunt: &Grunt,
tx: mpsc::Sender<String>,
) {
let lua = Lua::new();
let lua = Lua::default();
// TODO: no unwrap
attach_seatrial_stdlib(&lua).unwrap();

let user_script_registry_key = situation
.lua_file
Expand Down Expand Up @@ -226,8 +205,18 @@ fn grunt_worker(
Ok(StepCompletion::WithWarnings {
next_index,
pipe_data,
warnings,
}) => {
// TODO: log event for warnings
// TODO: in addition to printing, we need to track structured events (not just
// for these warnings, but for all sorts of pipeline actions)

for warning in warnings {
eprintln!(
"[{}] warning issued during pipeline step completion: {}",
grunt.name, warning
);
}

current_pipe_contents = pipe_data;
current_pipe_idx = next_index;
}
Expand All @@ -243,6 +232,28 @@ fn grunt_worker(
eprintln!("[{}] step was: {:?}", grunt.name, step);
break;
}
Err(StepError::Validation(err)) => {
eprintln!(
"[{}] aborting due to validation error in pipeline",
grunt.name
);
eprintln!("[{}] err was: {}", grunt.name, err);
eprintln!("[{}] step was: {:?}", grunt.name, step);
break;
}
// TODO: more details - we're just not plumbing the details around
Err(StepError::ValidationSucceededUnexpectedly) => {
eprintln!(
"[{}] aborting because a validation succeeded where we expected a failure",
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",
Expand Down Expand Up @@ -328,7 +339,7 @@ fn do_step<'a>(
agent: &Agent,
last: Option<&PipeContents>,
goto_counters: &mut HashMap<usize, usize>,
) -> Result<StepCompletion, StepError> {
) -> StepResult {
match step {
PA::ControlFlow(ControlFlow::GoTo { index, max_times }) => {
if let Some(times) = max_times {
Expand Down Expand Up @@ -411,6 +422,9 @@ fn do_step<'a>(
lua,
)
}
PA::Combinator(combo) => {
do_step_combinator(idx, combo, lua, user_script_registry_key, last)
}
// TODO: remove
_ => Ok(StepCompletion::Normal {
next_index: idx + 1,
Expand Down
2 changes: 1 addition & 1 deletion src/pipe_contents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::rc::Rc;
use crate::http_response_table::HttpResponseTable;
use crate::step_error::StepError;

#[derive(Debug)]
#[derive(Clone, Debug)]
pub enum PipeContents {
HttpResponse {
body: Vec<u8>,
Expand Down
2 changes: 2 additions & 0 deletions src/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ pub enum StepCompletion {
WithWarnings {
next_index: usize,
pipe_data: Option<PipeContents>,
// TODO should this be a stronger type than just a string?
warnings: Vec<String>,
},
WithExit,
}
2 changes: 2 additions & 0 deletions src/pipeline_action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,11 @@ pub enum Validator {
// 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
AssertHeaderEquals(String, String),
AssertHeaderExists(String),
AssertStatusCode(u16),
AssertStatusCodeInRange(u16, u16),
WarnUnlessHeaderEquals(String, String),
WarnUnlessHeaderExists(String),
WarnUnlessStatusCode(u16),
WarnUnlessStatusCodeInRange(u16, u16),
Expand Down
34 changes: 33 additions & 1 deletion src/shared_lua.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
use rlua::{Error as LuaError, Value as LuaValue};
use rlua::{Error as LuaError, Lua, RegistryKey, Value as LuaValue};

use std::rc::Rc;

use crate::pipe_contents::PipeContents;
use crate::step_error::StepError;

pub mod stdlib;
pub use stdlib::attach_seatrial_stdlib;

pub fn try_stringify_lua_value(it: Result<LuaValue, LuaError>) -> Result<String, StepError> {
match it {
Ok(LuaValue::Nil) => Err(StepError::RequestedLuaValueWhereNoneExists),
Expand All @@ -21,3 +27,29 @@ pub fn try_stringify_lua_value(it: Result<LuaValue, LuaError>) -> Result<String,
Err(err) => Err(err.into()),
}
}

pub fn run_user_script_function<'a>(
fname: &str,

// TODO: merge into a combo struct
lua: &'a Lua,
user_script_registry_key: &'a RegistryKey,

last: Option<&'a PipeContents>,
) -> Result<Rc<RegistryKey>, StepError> {
lua.context(|ctx| {
let lua_func = ctx
.registry_value::<rlua::Table>(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::<rlua::Value>(&rkey)?,
None => rlua::Nil,
},
None => rlua::Nil,
};
let result = lua_func.call::<rlua::Value, rlua::Value>(script_arg)?;
let registry_key = ctx.create_registry_value(result)?;
Ok(Rc::new(registry_key))
})
}
10 changes: 10 additions & 0 deletions src/shared_lua/stdlib/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use rlua::{Lua, Result as LuaResult};

pub mod validation_result;

pub use validation_result::{attach_validationresult, ValidationResult};

pub fn attach_seatrial_stdlib<'a>(lua: &'a Lua) -> LuaResult<()> {
attach_validationresult(lua)?;
Ok(())
}
Loading

0 comments on commit 3618f82

Please sign in to comment.