diff --git a/README.md b/README.md index a634df10..86741623 100644 --- a/README.md +++ b/README.md @@ -26,16 +26,10 @@ Changes: - Major changes: Javascript scripting! - Updated config-wasm to parse legacy and scripting yaml files - New binary pewpew-config-updater will attempt to convert legacy config yamls to the new version. If it can't convert the code it will leave in PLACEHOLDERS and TODO - - Known issues: + - Known issues in the config-updater: - Expressions in vars will not wrap environment variables in the expected `${e:VAR}` - vars in `logs` and `provides` will not have the prepended `_v.` before the var name. -Bugs: -- Collect returns an array of strings regardless of input type. Workaround, use scripting to `.map(parseInt)`. -- Declare expressions that create strings will escape out any json/quotes. No workaround currently. -- Vars cannot be decimal point values. Ex `peakLoad: 0.87`. Workaround: `peakLoad: ${x:0.87}` -- global loggers may not be running in try script - Bug fixes: ### v0.5.13 diff --git a/examples/declare.yaml b/examples/declare.yaml new file mode 100644 index 00000000..5880a281 --- /dev/null +++ b/examples/declare.yaml @@ -0,0 +1,75 @@ +load_pattern: + - !linear + to: 100% + over: 15s +loggers: + l: + to: !stdout +vars: + var_string: "foo" + var_array: ["foo", "foo", "foo"] + port: "${e:PORT}" + # a var that is an an object as a string. + # Putting this in directly will escape out all the quotes. You must wrap it as ${x:(${v:var_string_json})} to avoid escaping + var_string_json: '{"d":234,"e":"pqr","f":2.34}' + # a var that is a an object as an object + var_object: {"d":890,"e":"xyz","f":7.89} +providers: + provider_range: + !range + provider_list_objects: # List of objects + !list + values: + - a: 123 + b: 'abc' + c: 1.23 + - a: 456 + b: 'def' + c: 4.56 + - a: 789 + b: 'ghi' + c: 7.89 + - a: 101112 + b: 'jkl' + c: 10.1112 + repeat: true +endpoints: + - method: POST + declare: + # Collects the string into an array + collect_strings: !c + collects: + - take: 3 + from: '${v:var_string}' + as: _b + then: ${p:_b} + # Collects the objects into an array + collect_objects: !c + collects: + - take: 2 + from: '${p:provider_list_objects}' + as: _c + then: ${p:_c} + declare_string_json: !x '[{"provider_range": ${p:provider_range}, "var_string": "${v:var_string}","provider_list_objects": ${p:provider_list_objects}, "h": "tuv" }]' + url: http://localhost:${v:port}/ + body: !str >- + { + "provider_range": ${p:provider_range}, + "var_string": "${v:var_string}", + "provider_list_objects": ${p:provider_list_objects}, + "collect_objects": ${p:collect_objects}, + "collect_strings": ${p:collect_strings}, + "var_array": ${v:var_array}, + "var_string_json": ${x:(${v:var_string_json})}, + "var_object": ${v:var_object}, + "declare_string_json": ${p:declare_string_json}, + "const_string": "test" + } + peak_load: 1.1hps + headers: + Content-Type: application/json + logs: + l: + select: + status: response.status + body: response.body diff --git a/lib/config/src/configv1/convert_helper.rs b/lib/config/src/configv1/convert_helper.rs index ded96caf..d56daa51 100644 --- a/lib/config/src/configv1/convert_helper.rs +++ b/lib/config/src/configv1/convert_helper.rs @@ -272,7 +272,7 @@ fn map_vars(v: PreVar) -> Result, ConfigUpdaterError> { match v { json::Value::Null => unimplemented!("null var"), json::Value::Bool(b) => Ok(VarValue::Bool(b)), - json::Value::Number(n) => Ok(VarValue::Num(n.as_i64().unwrap_or_default())), + json::Value::Number(n) => Ok(VarValue::Num(n.as_f64().unwrap_or_default())), json::Value::String(s) => { let t = TemplateV1::new( &s, diff --git a/lib/config/src/configv2/endpoints/declare.rs b/lib/config/src/configv2/endpoints/declare.rs index 381d7ebf..33f89f1a 100644 --- a/lib/config/src/configv2/endpoints/declare.rs +++ b/lib/config/src/configv2/endpoints/declare.rs @@ -6,6 +6,7 @@ use crate::{ }; use ether::Either; use futures::{Stream, StreamExt, TryStreamExt}; +use log::debug; use serde::Deserialize; use std::{ collections::{BTreeMap, BTreeSet}, @@ -177,6 +178,11 @@ impl Declare { Ar: Clone + Send + Unpin + 'static, E: StdError + Send + Clone + Unpin + 'static + From, { + debug!( + "Declare into_stream declare={:?}, providers={:?}", + self, + providers.keys() + ); // poll_fn stream is not Clone, so an abstraction is made over a clonable stream vs a // function that returns a non-clonable stream fn make_stream S2, S2>(e: &Either) -> Either { @@ -193,6 +199,10 @@ impl Declare { .map(|Collect { take, from, r#as }| { let providers = providers.clone(); let stream = { + debug!( + "Declare Collects take={:?}, from={:?} as={}", + take, from, r#as + ); match from.as_static().map(ToOwned::to_owned) { // collect does not need providers, so just repeat the same value // as needed diff --git a/lib/config/src/configv2/error.rs b/lib/config/src/configv2/error.rs index 2f80650b..642d266e 100644 --- a/lib/config/src/configv2/error.rs +++ b/lib/config/src/configv2/error.rs @@ -14,6 +14,8 @@ pub enum LoadTestGenError { LibLoad(#[from] Arc), #[error("endpoints are required")] NoEndpoints(), + #[error("error missing providers: {0:?}")] + MissingProviders(Vec>), // Used by the config-wasm when only passing back a V1 error #[error("error {0}")] OtherErr(String), diff --git a/lib/config/src/configv2/mod.rs b/lib/config/src/configv2/mod.rs index 2102c225..4f84a261 100644 --- a/lib/config/src/configv2/mod.rs +++ b/lib/config/src/configv2/mod.rs @@ -57,12 +57,12 @@ pub struct LoadTest { pub(crate) type Vars = BTreeMap, VarValue>; -#[derive(Debug, Deserialize, Clone, PartialEq, Eq)] +#[derive(Debug, Deserialize, Clone, PartialEq)] #[serde(untagged)] #[derive(serde::Serialize)] pub(crate) enum VarValue { Map(Vars), - Num(i64), + Num(f64), Bool(bool), Str(Template), List(Vec), @@ -72,7 +72,7 @@ impl From> for serde_json::Value { fn from(value: VarValue) -> Self { match value { VarValue::Bool(b) => Self::Bool(b), - VarValue::Num(n) => Self::Number(n.into()), + VarValue::Num(n) => Self::Number(serde_json::Number::from_f64(n).unwrap()), VarValue::Str(mut t) => Self::String(std::mem::take(t.get_mut())), VarValue::List(l) => l.into_iter().map(Into::into).collect::>().into(), VarValue::Map(m) => Self::Object( @@ -95,9 +95,14 @@ impl Display for VarValue { } Self::Map(m) => { write!(f, "{{")?; + let mut multiple_entries = false; for (k, v) in m.iter() { + if multiple_entries { + // We need to not write a trailing slash, so only add this before the second + write!(f, ",")?; + } write!(f, "\"{}\": {}", k.escape_default(), v)?; - write!(f, ",")?; + multiple_entries = true; } write!(f, "}}") } @@ -166,10 +171,10 @@ impl LoadTest { file_path: Arc, env_vars: &BTreeMap, ) -> Result { - use LoadTestGenError::NoEndpoints; + use LoadTestGenError::{MissingProviders, NoEndpoints}; // TODO: Why isn't this causing errors on empty let mut pre_envs: LoadTest = serde_yaml::from_str(yaml)?; - log::debug!("from_yaml pre_envs: {:?}", pre_envs); + log::debug!("LoadTest::from_yaml pre_envs: {:?}", pre_envs); // init lib js scripting::set_source(std::mem::take(&mut pre_envs.lib_src))?; @@ -179,34 +184,47 @@ impl LoadTest { .iter_mut() .for_each(|e| e.insert_path(Arc::clone(&file_path))); let vars = std::mem::take(&mut pre_vars.vars); - let mut lt = pre_vars.insert_vars(&vars)?; + let mut loadtest = pre_vars.insert_vars(&vars)?; // Check if providers all exists for the templates - let missing = lt + let missing = loadtest .get_required_providers() .into_iter() - .filter(|p| !lt.providers.contains_key::(p)) + .filter(|p| !loadtest.providers.contains_key::(p)) .collect::>(); if !missing.is_empty() { - todo!("error on missing providers: {missing:?}"); + return Err(MissingProviders(missing)); } - let lp = <.load_pattern; - let ep = &mut lt.endpoints; - let headers = <.config.client.headers; + let loggers = &loadtest.loggers; + let load_pattern = &loadtest.load_pattern; + let endpoints = &mut loadtest.endpoints; + let headers = &loadtest.config.client.headers; // Check for no endpoints - if ep.is_empty() { + if endpoints.is_empty() { return Err(NoEndpoints()); } - ep.iter_mut().enumerate().for_each(|(id, endpoint)| { - endpoint.insert_load_pattern(lp.as_ref()); + endpoints.iter_mut().enumerate().for_each(|(id, endpoint)| { + endpoint.insert_load_pattern(load_pattern.as_ref()); endpoint.insert_special_tags(id); endpoint.insert_global_headers(headers); + // This was done in the `from_config`` in v1. + // We need to add all loggers to all endpoints if they have a query/select + for (name, logger) in loggers { + if let Some(query) = &logger.query { + endpoint.logs.push(( + name.clone(), + EndpointLogs { + query: query.clone(), + }, + )) + } + } }); - lt.lt_err = lt.make_lt_err(); + loadtest.lt_err = loadtest.make_lt_err(); - Ok(lt) + Ok(loadtest) } fn get_required_providers(&self) -> BTreeSet> { diff --git a/lib/config/src/configv2/templating.rs b/lib/config/src/configv2/templating.rs index ed5af583..8d41bf67 100644 --- a/lib/config/src/configv2/templating.rs +++ b/lib/config/src/configv2/templating.rs @@ -21,6 +21,7 @@ use ether::{Either, Either3}; use futures::{Stream, TryStreamExt}; pub use helpers::*; use itertools::Itertools; +use log::debug; use serde::Deserialize; use std::{ borrow::Cow, @@ -96,7 +97,7 @@ where } => template, Template::PreVars { template, .. } => template, // probably won't be needed, as serialization is done with `` templates. - Template::NeedsProviders { .. } => todo!(), + Template::NeedsProviders { .. } => todo!("TemplatedString FromStr NeedsProviders todo"), } } } @@ -205,6 +206,11 @@ impl> Template { Ar: Clone + Send + Unpin + 'static, E: StdError + Send + Clone + Unpin + 'static + From, { + debug!( + "Template into_stream template={:?}, providers={:?}", + self, + providers.keys() + ); self.into_stream_with(move |p| providers.get(p).map(|p| p.as_stream())) } @@ -234,9 +240,18 @@ impl> Template { use futures::stream::repeat; Ok(match self { Self::Literal { value } => { + debug!( + "Template::Literal into_stream_with value='{}', json='{}'", + value, + serde_json::Value::String(value.clone()) + ); Either::A(repeat(Ok((serde_json::Value::String(value), vec![])))) } Self::NeedsProviders { script, .. } => { + debug!( + "Template::NeedsProviders into_stream_with script={:?}", + script + ); let streams = script .into_iter() .map(|s| match s { @@ -254,13 +269,42 @@ impl> Template { .collect::, _>>()?; Either::B(zip_all::zip_all(streams).map_ok(|js| { js.into_iter() - .map(|(j, ar)| (j.to_string().trim_matches('"').to_owned(), ar)) + .map(|(j, ar)| { + match j { + serde_json::Value::String(s) => { + // We don't want to stringify the json::Value::String or it will escape out quotes, etc. + // Get the internal string and use it. + debug!("Template into_stream_with zip_all json::string s={}, to_string={}", s, s.to_string()); + (s.trim_matches('"').to_owned(), ar) + }, + other => { + debug!("Template into_stream_with zip_all json::other j={}, to_string={}", other, other.to_string()); + (other.to_string().trim_matches('"').to_owned(), ar) + }, + } + }) .reduce(|mut acc, e| { acc.0.push_str(&e.0); acc.1.extend(e.1); acc }) - .map(|(s, ar)| (serde_json::Value::String(s), ar)) + .map(|(s, ar)| { + // If it's an object we're turning it into a string here + ( + match serde_json::from_str(&s) { + Ok(serde_json::Value::String(s)) => { + log::debug!("Template into_stream_with Using literal string {s:?} as the json value"); + serde_json::Value::String(s) + } + Ok(v) => v, + Err(e) => { + log::debug!("Template into_stream_with String {s:?} is not valid JSON ({e}); reusing same string value"); + serde_json::Value::String(s) + } + }, + ar, + ) + }) .unwrap_or_default() })) } @@ -931,7 +975,7 @@ mod tests { ("a".to_owned().into(), VarValue::Bool(true)), ( "b".to_owned().into(), - VarValue::List(vec![VarValue::Num(45), VarValue::Num(23)]), + VarValue::List(vec![VarValue::Num(45.0), VarValue::Num(23.0)]), ), ( "c".to_owned().into(), @@ -941,8 +985,8 @@ mod tests { "d".to_owned().into(), VarValue::Str(Template::new_literal("77".to_owned())), ), - ("e".to_owned().into(), VarValue::Num(12)), - ("e1".to_owned().into(), VarValue::Num(999)), + ("e".to_owned().into(), VarValue::Num(12.0)), + ("e1".to_owned().into(), VarValue::Num(999.0)), ] .into(), ), diff --git a/tests/integration.yaml b/tests/integration.yaml index 5f67e029..02a16517 100644 --- a/tests/integration.yaml +++ b/tests/integration.yaml @@ -17,6 +17,8 @@ vars: b: "foo" e2: ["foo", "foo", "foo"] port: "${e:PORT}" + int: 12345 + float: 1.2345 # Make sure these can be parsed providers: a: !range