Skip to content

Commit 3618f82

Browse files
committed
feat: begin lua-side stdlib, implement combinators & validators
1 parent 909ad7b commit 3618f82

17 files changed

+798
-79
lines changed

examples/simpleish/seatrial.lua

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
-- uses https://tieske.github.io/date/, a pure-Lua date library
22
local date = require('date')
33

4+
local ESOTERIC_FORMAT_REGEX = "^DAYS (%d+) SYEAR (%d+) EYEAR (%d+) SMON (%d+) EMON (%d+) SDAY (%d+) EDAY (%d+)$"
5+
46
function generate_30_day_range()
57
local today = date(true)
68
local plus30 = today:copy():adddays(30)
@@ -10,6 +12,15 @@ function generate_30_day_range()
1012
}
1113
end
1214

15+
function was_valid_esoteric_format(arg)
16+
if arg.body_string:match(ESOTERIC_FORMAT_REGEX) == nil then
17+
return ValidationResult.Error("server responded with malformed body")
18+
end
19+
20+
return ValidationResult.Ok()
21+
end
22+
1323
return {
1424
generate_30_day_range = generate_30_day_range,
25+
was_valid_esoteric_format = was_valid_esoteric_format,
1526
}

src/cli.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
use argh::FromArgs;
2+
3+
use crate::situation::SituationSpec;
4+
5+
/// situational-mock-based load testing
6+
#[derive(FromArgs)]
7+
struct CmdArgsBase {
8+
/// integral multiplier for grunt counts (minimum 1)
9+
#[argh(option, short = 'm', default = "1")]
10+
multiplier: usize,
11+
12+
/// base URL for all situations in this run
13+
#[argh(positional)]
14+
base_url: String,
15+
16+
// work around https://github.com/google/argh/issues/13 wherein repeatable positional arguments
17+
// (situations, in this struct) allow any vec length 0+, where we require a vec length 1+. this
18+
// could be hacked around with some From magic and a custom Vec, but this is more
19+
// straightforward
20+
/// path to a RON file in seatrial(5) situation config format
21+
#[argh(positional)]
22+
req_situation: SituationSpec,
23+
24+
/// optional paths to additional RON files in seatrial(5) situation config format
25+
#[argh(positional)]
26+
situations: Vec<SituationSpec>,
27+
}
28+
29+
#[derive(Clone, Debug)]
30+
pub struct CmdArgs {
31+
/// integral multiplier for grunt counts (minimum 1)
32+
pub multiplier: usize,
33+
34+
/// base URL for all situations in this run
35+
pub base_url: String,
36+
37+
/// paths to RON files in seatrial(5) situation config format
38+
pub situations: Vec<SituationSpec>,
39+
}
40+
41+
/// flatten situations into a single vec (see docs about CmdArgsBase::req_situation)
42+
impl From<CmdArgsBase> for CmdArgs {
43+
fn from(mut it: CmdArgsBase) -> Self {
44+
it.situations.insert(0, it.req_situation.clone());
45+
46+
Self {
47+
multiplier: it.multiplier,
48+
base_url: it.base_url,
49+
situations: it.situations,
50+
}
51+
}
52+
}
53+
54+
pub fn parse_args() -> CmdArgs {
55+
argh::from_env::<CmdArgsBase>().into()
56+
}

src/config_duration.rs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,30 +8,27 @@ pub enum ConfigDuration {
88
Seconds(u64),
99
}
1010

11-
impl From<ConfigDuration> for Duration {
12-
fn from(src: ConfigDuration) -> Self {
13-
(&src).into()
14-
}
15-
}
16-
1711
impl From<&ConfigDuration> for Duration {
1812
fn from(src: &ConfigDuration) -> Self {
1913
match src {
20-
ConfigDuration::Milliseconds(ms) => Duration::from_millis(*ms),
21-
ConfigDuration::Seconds(ms) => Duration::from_secs(*ms),
14+
ConfigDuration::Milliseconds(ms) => Self::from_millis(*ms),
15+
ConfigDuration::Seconds(ms) => Self::from_secs(*ms),
2216
}
2317
}
2418
}
2519

2620
#[test]
2721
fn test_seconds() {
28-
assert_eq!(Duration::from_secs(10), ConfigDuration::Seconds(10).into());
22+
assert_eq!(
23+
Duration::from_secs(10),
24+
(&ConfigDuration::Seconds(10)).into()
25+
);
2926
}
3027

3128
#[test]
3229
fn test_milliseconds() {
3330
assert_eq!(
3431
Duration::from_millis(100),
35-
ConfigDuration::Milliseconds(100).into()
32+
(&ConfigDuration::Milliseconds(100)).into()
3633
);
3734
}

src/http_response_table.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,27 @@ impl<'a> Iterator for BoundHttpResponseTableIter<'a> {
9999
.expect("should have created headers table in registry")
100100
}),
101101
)),
102+
3 => Some((
103+
"content_type",
104+
self.child.lua.context(|ctx| {
105+
ctx.create_registry_value(self.child.table.content_type.clone())
106+
.expect("should have created content_type string in registry")
107+
}),
108+
)),
109+
4 => Some((
110+
"body",
111+
self.child.lua.context(|ctx| {
112+
ctx.create_registry_value(self.child.table.body.clone())
113+
.expect("should have created body table in registry")
114+
}),
115+
)),
116+
5 => Some((
117+
"body_string",
118+
self.child.lua.context(|ctx| {
119+
ctx.create_registry_value(self.child.table.body_string.clone())
120+
.expect("should have created body_string nilable-string in registry")
121+
}),
122+
)),
102123
_ => None,
103124
}
104125
}

src/main.rs

Lines changed: 49 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use argh::FromArgs;
21
use rlua::{Lua, RegistryKey};
32
use ureq::{Agent, AgentBuilder};
43
use url::Url;
@@ -10,6 +9,7 @@ use std::thread;
109
use std::thread::JoinHandle;
1110
use std::time::Duration;
1211

12+
mod cli;
1313
mod config_duration;
1414
mod grunt;
1515
mod http_response_table;
@@ -19,55 +19,32 @@ mod pipeline;
1919
mod pipeline_action;
2020
mod shared_lua;
2121
mod situation;
22+
mod step_combinator;
2223
mod step_error;
2324
mod step_goto;
2425
mod step_http;
2526
mod step_lua;
27+
mod step_validator;
2628

29+
use crate::cli::parse_args;
2730
use crate::grunt::Grunt;
2831
use crate::persona::Persona;
2932
use crate::pipe_contents::PipeContents;
3033
use crate::pipeline::StepCompletion;
3134
use crate::pipeline_action::{ControlFlow, Http, PipelineAction as PA, Reference};
32-
use crate::situation::{Situation, SituationSpec};
33-
use crate::step_error::StepError;
35+
use crate::shared_lua::attach_seatrial_stdlib;
36+
use crate::situation::Situation;
37+
use crate::step_combinator::step as do_step_combinator;
38+
use crate::step_error::{StepError, StepResult};
3439
use crate::step_goto::step as do_step_goto;
3540
use crate::step_http::{
3641
step_delete as do_step_http_delete, step_get as do_step_http_get,
3742
step_head as do_step_http_head, step_post as do_step_http_post, step_put as do_step_http_put,
3843
};
3944
use crate::step_lua::step_function as do_step_lua_function;
4045

41-
/// situational-mock-based load testing
42-
#[derive(FromArgs)]
43-
struct CmdArgs {
44-
/// integral multiplier for grunt counts (minimum 1)
45-
#[argh(option, short = 'm', default = "1")]
46-
multiplier: usize,
47-
48-
/// base URL for all situations in this run
49-
#[argh(positional)]
50-
base_url: String,
51-
52-
// work around https://github.com/google/argh/issues/13 wherein repeatable positional arguments
53-
// (situations, in this struct) allow any vec length 0+, where we require a vec length 1+. this
54-
// could be hacked around with some From magic and a custom Vec, but this is more
55-
// straightforward
56-
/// path to a RON file in seatrial(5) situation config format
57-
#[argh(positional)]
58-
req_situation: SituationSpec,
59-
60-
/// optional paths to additional RON files in seatrial(5) situation config format
61-
#[argh(positional)]
62-
situations: Vec<SituationSpec>,
63-
}
64-
6546
fn main() -> std::io::Result<()> {
66-
let args = {
67-
let mut args: CmdArgs = argh::from_env();
68-
args.situations.insert(0, args.req_situation.clone());
69-
args
70-
};
47+
let args = parse_args();
7148

7249
// TODO: no unwrap, which will also kill the nasty parens
7350
let base_url = (if args.base_url.ends_with('/') {
@@ -135,7 +112,9 @@ fn grunt_worker(
135112
grunt: &Grunt,
136113
tx: mpsc::Sender<String>,
137114
) {
138-
let lua = Lua::new();
115+
let lua = Lua::default();
116+
// TODO: no unwrap
117+
attach_seatrial_stdlib(&lua).unwrap();
139118

140119
let user_script_registry_key = situation
141120
.lua_file
@@ -226,8 +205,18 @@ fn grunt_worker(
226205
Ok(StepCompletion::WithWarnings {
227206
next_index,
228207
pipe_data,
208+
warnings,
229209
}) => {
230-
// TODO: log event for warnings
210+
// TODO: in addition to printing, we need to track structured events (not just
211+
// for these warnings, but for all sorts of pipeline actions)
212+
213+
for warning in warnings {
214+
eprintln!(
215+
"[{}] warning issued during pipeline step completion: {}",
216+
grunt.name, warning
217+
);
218+
}
219+
231220
current_pipe_contents = pipe_data;
232221
current_pipe_idx = next_index;
233222
}
@@ -243,6 +232,28 @@ fn grunt_worker(
243232
eprintln!("[{}] step was: {:?}", grunt.name, step);
244233
break;
245234
}
235+
Err(StepError::Validation(err)) => {
236+
eprintln!(
237+
"[{}] aborting due to validation error in pipeline",
238+
grunt.name
239+
);
240+
eprintln!("[{}] err was: {}", grunt.name, err);
241+
eprintln!("[{}] step was: {:?}", grunt.name, step);
242+
break;
243+
}
244+
// TODO: more details - we're just not plumbing the details around
245+
Err(StepError::ValidationSucceededUnexpectedly) => {
246+
eprintln!(
247+
"[{}] aborting because a validation succeeded where we expected a failure",
248+
grunt.name
249+
);
250+
eprintln!(
251+
"[{}] this is an error in seatrial - TODO fix this",
252+
grunt.name
253+
);
254+
eprintln!("[{}] step was: {:?}", grunt.name, step);
255+
break;
256+
}
246257
Err(StepError::InvalidActionInContext) => {
247258
eprintln!(
248259
"[{}] aborting due to invalid action definition in the given context",
@@ -328,7 +339,7 @@ fn do_step<'a>(
328339
agent: &Agent,
329340
last: Option<&PipeContents>,
330341
goto_counters: &mut HashMap<usize, usize>,
331-
) -> Result<StepCompletion, StepError> {
342+
) -> StepResult {
332343
match step {
333344
PA::ControlFlow(ControlFlow::GoTo { index, max_times }) => {
334345
if let Some(times) = max_times {
@@ -411,6 +422,9 @@ fn do_step<'a>(
411422
lua,
412423
)
413424
}
425+
PA::Combinator(combo) => {
426+
do_step_combinator(idx, combo, lua, user_script_registry_key, last)
427+
}
414428
// TODO: remove
415429
_ => Ok(StepCompletion::Normal {
416430
next_index: idx + 1,

src/pipe_contents.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use std::rc::Rc;
88
use crate::http_response_table::HttpResponseTable;
99
use crate::step_error::StepError;
1010

11-
#[derive(Debug)]
11+
#[derive(Clone, Debug)]
1212
pub enum PipeContents {
1313
HttpResponse {
1414
body: Vec<u8>,

src/pipeline.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ pub enum StepCompletion {
99
WithWarnings {
1010
next_index: usize,
1111
pipe_data: Option<PipeContents>,
12+
// TODO should this be a stronger type than just a string?
13+
warnings: Vec<String>,
1214
},
1315
WithExit,
1416
}

src/pipeline_action.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,11 @@ pub enum Validator {
124124
// falsey, except in the context of an AnyOf or NoneOf combinator, which can "catch" the errors
125125
// as appropriate. WarnUnless validations are never fatal and likewise can never fail a
126126
// combinator
127+
AssertHeaderEquals(String, String),
127128
AssertHeaderExists(String),
128129
AssertStatusCode(u16),
129130
AssertStatusCodeInRange(u16, u16),
131+
WarnUnlessHeaderEquals(String, String),
130132
WarnUnlessHeaderExists(String),
131133
WarnUnlessStatusCode(u16),
132134
WarnUnlessStatusCodeInRange(u16, u16),

src/shared_lua.rs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1-
use rlua::{Error as LuaError, Value as LuaValue};
1+
use rlua::{Error as LuaError, Lua, RegistryKey, Value as LuaValue};
22

3+
use std::rc::Rc;
4+
5+
use crate::pipe_contents::PipeContents;
36
use crate::step_error::StepError;
47

8+
pub mod stdlib;
9+
pub use stdlib::attach_seatrial_stdlib;
10+
511
pub fn try_stringify_lua_value(it: Result<LuaValue, LuaError>) -> Result<String, StepError> {
612
match it {
713
Ok(LuaValue::Nil) => Err(StepError::RequestedLuaValueWhereNoneExists),
@@ -21,3 +27,29 @@ pub fn try_stringify_lua_value(it: Result<LuaValue, LuaError>) -> Result<String,
2127
Err(err) => Err(err.into()),
2228
}
2329
}
30+
31+
pub fn run_user_script_function<'a>(
32+
fname: &str,
33+
34+
// TODO: merge into a combo struct
35+
lua: &'a Lua,
36+
user_script_registry_key: &'a RegistryKey,
37+
38+
last: Option<&'a PipeContents>,
39+
) -> Result<Rc<RegistryKey>, StepError> {
40+
lua.context(|ctx| {
41+
let lua_func = ctx
42+
.registry_value::<rlua::Table>(user_script_registry_key)?
43+
.get::<_, rlua::Function>(fname)?;
44+
let script_arg = match last {
45+
Some(lval) => match lval.to_lua(lua)? {
46+
Some(rkey) => ctx.registry_value::<rlua::Value>(&rkey)?,
47+
None => rlua::Nil,
48+
},
49+
None => rlua::Nil,
50+
};
51+
let result = lua_func.call::<rlua::Value, rlua::Value>(script_arg)?;
52+
let registry_key = ctx.create_registry_value(result)?;
53+
Ok(Rc::new(registry_key))
54+
})
55+
}

src/shared_lua/stdlib/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
use rlua::{Lua, Result as LuaResult};
2+
3+
pub mod validation_result;
4+
5+
pub use validation_result::{attach_validationresult, ValidationResult};
6+
7+
pub fn attach_seatrial_stdlib<'a>(lua: &'a Lua) -> LuaResult<()> {
8+
attach_validationresult(lua)?;
9+
Ok(())
10+
}

0 commit comments

Comments
 (0)