From 9ae72b529b30722fa42c691e72936e52de48ac6f Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Fri, 14 Feb 2020 13:09:43 +1000 Subject: [PATCH 001/127] Start of yaml test runner implementation WIP This commit is the start of the yaml test runner binary. - Download yaml tests from GitHub to local filesystem - Read yaml tests --- .gitignore | 1 + Cargo.toml | 3 +- yaml_test_runner/Cargo.toml | 19 ++++ yaml_test_runner/src/generator.rs | 106 ++++++++++++++++++ yaml_test_runner/src/github.rs | 173 ++++++++++++++++++++++++++++++ yaml_test_runner/src/main.rs | 36 +++++++ 6 files changed, 337 insertions(+), 1 deletion(-) create mode 100644 yaml_test_runner/Cargo.toml create mode 100644 yaml_test_runner/src/generator.rs create mode 100644 yaml_test_runner/src/github.rs create mode 100644 yaml_test_runner/src/main.rs diff --git a/.gitignore b/.gitignore index 41e64d17..71ee2d0d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ Cargo.lock .idea .vscode/ *.log +yaml_test_runner/yaml/ diff --git a/Cargo.toml b/Cargo.toml index 56f48afc..1dccee9b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ "api_generator", - "elasticsearch" + "elasticsearch", + "yaml_test_runner" ] \ No newline at end of file diff --git a/yaml_test_runner/Cargo.toml b/yaml_test_runner/Cargo.toml new file mode 100644 index 00000000..85e2aa50 --- /dev/null +++ b/yaml_test_runner/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "yaml_test_runner" +version = "7.6.0-alpha.1" +edition = "2018" +authors = ["Elastic and Contributors"] +description = "Generates and runs tests from Elasticsearch's YAML test specs" +repository = "https://github.com/elastic/elasticsearch-rs" + +[dependencies] +elasticsearch = { version = "7.6.0-alpha.1", path = "../elasticsearch" } + +clap = "~2" +failure = "0.1.6" +quote = "~0.3" +reqwest = "~0.9" +serde = "~1" +serde_json = "~1" +syn = { version = "~0.11", features = ["full"] } +yaml-rust = "0.4.3" \ No newline at end of file diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs new file mode 100644 index 00000000..86b7c3f5 --- /dev/null +++ b/yaml_test_runner/src/generator.rs @@ -0,0 +1,106 @@ +use quote::Tokens; +use std::fs; +use std::path::PathBuf; +use yaml_rust::{ + yaml::{Array, Hash}, + Yaml, YamlLoader, +}; + +pub fn generate_tests_from_yaml(download_dir: &PathBuf) -> Result<(), failure::Error> { + let paths = fs::read_dir(download_dir).unwrap(); + for entry in paths { + if let Ok(entry) = entry { + if let Ok(file_type) = entry.file_type() { + if file_type.is_dir() { + generate_tests_from_yaml(&entry.path())?; + } else if file_type.is_file() { + let file_name = entry.file_name().to_string_lossy().into_owned(); + if !file_name.ends_with(".yml") && !file_name.ends_with(".yaml") { + continue; + } + + let yaml = fs::read_to_string(&entry.path()).unwrap(); + let docs = YamlLoader::load_from_str(&yaml).unwrap(); + let mut setup: Option = None; + let mut teardown: Option = None; + let mut tests: Vec<(String, Tokens)> = Vec::with_capacity(docs.len()); + + for doc in docs { + println!("{:?}", &entry.path()); + if let Some(mut hash) = doc.into_hash() { + let entries = hash.entries(); + let first = entries.into_iter().next().unwrap(); + match (first.key(), first.get()) { + (Yaml::String(name), Yaml::Array(steps)) => { + let tokens = read_steps(steps)?; + match name.as_str() { + "setup" => setup = Some(tokens), + "teardown" => teardown = Some(tokens), + name => { + tests.push((name.to_owned(), tokens)); + } + }; + } + (k, v) => { + return Err(failure::err_msg(format!( + "{:?} and {:?} in {:?} is not a string and array", + &k, + &v, + &entry.path() + ))) + } + } + } else { + return Err(failure::err_msg(format!( + "{:?} is not a hash", + &entry.path() + ))); + } + } + } + } + } + } + + Ok(()) +} + +fn read_steps(steps: &Array) -> Result { + let mut tokens = Tokens::new(); + for step in steps { + if let Some(mut hash) = step.clone().into_hash() { + let mut entries = hash.entries(); + let first = entries.next().unwrap(); + + match (first.key().as_str().unwrap(), first.get()) { + ("skip", Yaml::Hash(h)) => {} + ("do", Yaml::Hash(h)) => { + read_do(h, &mut tokens)?; + } + ("set", Yaml::Hash(h)) => {} + ("transform_and_set", Yaml::Hash(h)) => {} + ("match", Yaml::Hash(h)) => {} + ("contains", Yaml::Hash(h)) => {} + ("is_true", Yaml::Hash(h)) => {} + ("is_true", Yaml::String(s)) => {} + ("is_false", Yaml::Hash(h)) => {} + ("is_false", Yaml::String(s)) => {} + ("length", Yaml::Hash(h)) => {} + ("eq", Yaml::Hash(h)) => {} + ("gte", Yaml::Hash(h)) => {} + ("lte", Yaml::Hash(h)) => {} + ("gt", Yaml::Hash(h)) => {} + ("lt", Yaml::Hash(h)) => {} + (op, _) => return Err(failure::err_msg(format!("unknown step operation: {}", op))), + } + } else { + return Err(failure::err_msg(format!("{:?} is not a hash", &step))); + } + } + + Ok(tokens) +} + +fn read_do(hash: &Hash, tokens: &mut Tokens) -> Result<(), failure::Error> { + Ok(()) +} diff --git a/yaml_test_runner/src/github.rs b/yaml_test_runner/src/github.rs new file mode 100644 index 00000000..17065722 --- /dev/null +++ b/yaml_test_runner/src/github.rs @@ -0,0 +1,173 @@ +use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION}; +use serde::Deserialize; +use std::error::Error as StdError; +use std::fmt::Formatter; +use std::fs::File; +use std::path::PathBuf; +use std::{fs, io}; + +struct YamlTestSuite { + dir: String, + branch: String, + url: String, +} + +#[derive(Deserialize, Debug)] +struct Links { + #[serde(rename = "self")] + self_link: String, + git: String, + html: String, +} + +#[derive(Deserialize, Debug)] +struct GitHubContent { + name: String, + path: String, + sha: String, + size: i32, + url: String, + html_url: String, + git_url: String, + download_url: Option, + #[serde(rename = "type")] + ty: String, + #[serde(rename = "_links")] + links: Links, +} + +pub fn download_test_suites(token: &str, branch: &str, download_dir: &PathBuf) { + let test_suite_map = [ + ("oss".to_string(), "https://api.github.com/repos/elastic/elasticsearch/contents/rest-api-spec/src/main/resources/rest-api-spec/test".to_string()), + ("xpack".to_string(), "https://api.github.com/repos/elastic/elasticsearch/contents/x-pack/plugin/src/test/resources/rest-api-spec/test".to_string())]; + + let test_suites: Vec = test_suite_map + .iter() + .map(|(dir, template_url)| { + let url = format!("{}?ref={}", template_url, branch); + YamlTestSuite { + dir: dir.to_string(), + branch: branch.to_string(), + url, + } + }) + .collect(); + + let mut headers = HeaderMap::new(); + let token_value = format!("token {}", token); + headers.append(AUTHORIZATION, HeaderValue::from_str(&token_value).unwrap()); + let client = reqwest::ClientBuilder::new() + .default_headers(headers) + .build() + .unwrap(); + + fs::create_dir_all(download_dir).unwrap(); + for suite in test_suites { + download_tests(&client, &suite, &download_dir).unwrap(); + } +} + +fn download_tests( + client: &reqwest::Client, + suite: &YamlTestSuite, + download_dir: &PathBuf, +) -> Result<(), DownloadError> { + let suite_dir = { + let mut d = download_dir.clone(); + d.push(&suite.dir); + d + }; + + fs::create_dir_all(&suite_dir)?; + println!("Downloading {} tests from {}", &suite.dir, &suite.branch); + download(client, &suite.url, &suite_dir)?; + println!( + "Done downloading {} tests from {}", + &suite.dir, &suite.branch + ); + + Ok(()) +} + +fn download( + client: &reqwest::Client, + url: &str, + download_dir: &PathBuf, +) -> Result<(), DownloadError> { + let mut response = client.get(url).send()?; + + let remaining_rate_limit: i32 = response + .headers() + .get("X-RateLimit-Remaining") + .unwrap() + .to_str() + .unwrap() + .parse() + .unwrap(); + + if remaining_rate_limit < 10 { + println!("Remaining rate limit: {}", remaining_rate_limit); + } + + let contents: Vec = response.json()?; + for content in contents { + let content_path = { + let mut d = download_dir.clone(); + d.push(&content.name); + d + }; + + match content.ty.as_str() { + "file" => { + let mut file = File::create(content_path)?; + // no need to send the token for downloading content + let mut file_response = reqwest::get(&content.download_url.unwrap())?; + io::copy(&mut file_response, &mut file)?; + } + "dir" => { + fs::create_dir_all(&content_path)?; + download(client, &content.url, &content_path)?; + } + t => panic!(format!("Unexpected GitHub content type: {}", t)), + } + } + + Ok(()) +} + +#[derive(Debug)] +pub enum DownloadError { + IoErr(io::Error), + HttpError(reqwest::Error), +} + +impl std::fmt::Display for DownloadError { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + match self { + DownloadError::IoErr(err) => write!(f, "IoErr {}", err), + DownloadError::HttpError(err) => write!(f, "HttpError {}", err), + } + } +} + +impl StdError for DownloadError { + #[allow(warnings)] + fn description(&self) -> &str { + match self { + DownloadError::IoErr(err) => err.description(), + DownloadError::HttpError(err) => err.description(), + } + } +} + +impl From for DownloadError { + fn from(e: io::Error) -> Self { + DownloadError::IoErr(e) + } +} + +impl From for DownloadError { + fn from(e: reqwest::Error) -> Self { + DownloadError::HttpError(e) + } +} diff --git a/yaml_test_runner/src/main.rs b/yaml_test_runner/src/main.rs new file mode 100644 index 00000000..663ab655 --- /dev/null +++ b/yaml_test_runner/src/main.rs @@ -0,0 +1,36 @@ +use clap::{App, Arg}; +use std::path::PathBuf; + +mod generator; +mod github; + +fn main() { + let matches = App::new(env!("CARGO_PKG_NAME")) + .about(env!("CARGO_PKG_DESCRIPTION")) + .arg(Arg::with_name("branch") + .short("b") + .long("branch") + .value_name("BRANCH") + .help("The git branch in the Elasticsearch repository from which to download yaml tests") + .required(true) + .default_value("master") + .takes_value(true)) + .arg(Arg::with_name("token") + .short("t") + .long("token") + .value_name("TOKEN") + .help("The GitHub access token. Required to increase the rate limit to be able to download all yaml tests") + .required(true) + .takes_value(true)) + .get_matches(); + + let branch = matches + .value_of("branch") + .expect("missing 'branch' argument"); + let token = matches.value_of("token").expect("missing 'token' argument"); + let download_dir = PathBuf::from("./yaml_test_runner/yaml"); + + //github::download_test_suites(token, branch, &download_dir); + + generator::generate_tests_from_yaml(&download_dir).unwrap(); +} From 65c83b47853539e6321d671cb7fc817f9b3ccbea Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Wed, 26 Feb 2020 10:43:17 +1000 Subject: [PATCH 002/127] start of read_do --- yaml_test_runner/src/generator.rs | 75 ++++++++++++++++++++++++++----- yaml_test_runner/src/main.rs | 3 ++ 2 files changed, 66 insertions(+), 12 deletions(-) diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 86b7c3f5..9cda11b8 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -1,4 +1,5 @@ use quote::Tokens; + use std::fs; use std::path::PathBuf; use yaml_rust::{ @@ -6,6 +7,22 @@ use yaml_rust::{ Yaml, YamlLoader, }; +struct YamlTest { + setup: Option, + teardown: Option, + tests: Vec<(String, Tokens)> +} + +impl YamlTest { + pub fn new(len: usize) -> Self { + Self { + setup: None, + teardown: None, + tests: Vec::with_capacity(len), + } + } +} + pub fn generate_tests_from_yaml(download_dir: &PathBuf) -> Result<(), failure::Error> { let paths = fs::read_dir(download_dir).unwrap(); for entry in paths { @@ -21,9 +38,7 @@ pub fn generate_tests_from_yaml(download_dir: &PathBuf) -> Result<(), failure::E let yaml = fs::read_to_string(&entry.path()).unwrap(); let docs = YamlLoader::load_from_str(&yaml).unwrap(); - let mut setup: Option = None; - let mut teardown: Option = None; - let mut tests: Vec<(String, Tokens)> = Vec::with_capacity(docs.len()); + let mut test = YamlTest::new(docs.len()); for doc in docs { println!("{:?}", &entry.path()); @@ -34,10 +49,10 @@ pub fn generate_tests_from_yaml(download_dir: &PathBuf) -> Result<(), failure::E (Yaml::String(name), Yaml::Array(steps)) => { let tokens = read_steps(steps)?; match name.as_str() { - "setup" => setup = Some(tokens), - "teardown" => teardown = Some(tokens), + "setup" => test.setup = Some(tokens), + "teardown" => test.teardown = Some(tokens), name => { - tests.push((name.to_owned(), tokens)); + test.tests.push((name.to_owned(), tokens)); } }; } @@ -57,6 +72,8 @@ pub fn generate_tests_from_yaml(download_dir: &PathBuf) -> Result<(), failure::E ))); } } + + println!("{:?}", test.tests); } } } @@ -70,11 +87,14 @@ fn read_steps(steps: &Array) -> Result { for step in steps { if let Some(mut hash) = step.clone().into_hash() { let mut entries = hash.entries(); - let first = entries.next().unwrap(); + let mut first = entries.next().unwrap(); + + let mut key = first.key().as_str().unwrap(); + let mut value = first.get().clone(); - match (first.key().as_str().unwrap(), first.get()) { + match (key, value) { ("skip", Yaml::Hash(h)) => {} - ("do", Yaml::Hash(h)) => { + ("do", Yaml::Hash(ref mut h)) => { read_do(h, &mut tokens)?; } ("set", Yaml::Hash(h)) => {} @@ -101,6 +121,37 @@ fn read_steps(steps: &Array) -> Result { Ok(tokens) } -fn read_do(hash: &Hash, tokens: &mut Tokens) -> Result<(), failure::Error> { - Ok(()) -} +fn read_do(hash: &mut Hash, tokens: &mut Tokens) -> Result<(), failure::Error> { + + let mut entries = hash.entries(); + + let headers = String::from("headers"); + let catch = String::from("catch"); + let node_selector = String::from("node_selector"); + + let result: Result, _> = entries + .into_iter() + .map(|entry| { + match (entry.key(), entry.get()) { + (Yaml::String(headers), Yaml::Hash(h)) => Ok(()), + (Yaml::String(catch), Yaml::String(s)) => Ok(()), + (Yaml::String(node_selector), _) => Ok(()), + (Yaml::String(s), Yaml::Hash(h)) => { + let fn_name = s.clone().replace(".", "()."); + let fn_name_ident = syn::Ident::from(fn_name); + + tokens.append(quote! { + let response = client.#fn_name_ident().await?; + }); + Ok(()) + }, + (k, v) => Err(failure::err_msg(format!("{:?} and {:?} are not a string and hash", &k, &v))), + } + }) + .collect(); + + match result { + Ok(_) => Ok(()), + Err(e) => Err(e) + } +} \ No newline at end of file diff --git a/yaml_test_runner/src/main.rs b/yaml_test_runner/src/main.rs index 663ab655..c502ced8 100644 --- a/yaml_test_runner/src/main.rs +++ b/yaml_test_runner/src/main.rs @@ -1,3 +1,6 @@ +#[macro_use] +extern crate quote; + use clap::{App, Arg}; use std::path::PathBuf; From ed27b0aede0bf807fffc284f14eed275bf6ca7fc Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Mon, 2 Mar 2020 16:43:59 +1000 Subject: [PATCH 003/127] Reference api_generator from yaml_test_runner This commit changes the api_generator package to expose it as a library to yaml_test_runner, whilst also still compiling it as a runnable binary. This allows the generator modules to be used in the yaml_test_runner where they will be needed to generate tests, by inspecting the ASTs produced by the generator. Move last_downloaded_version marker file into rest_specs directory so that the yaml_test_runner can use/reuse downloaded rest specs when running locally, by easily inspecting the downloaded version. Renamed api_generator module in api_generator to simply generator. --- .../{ => rest_specs}/last_downloaded_version | 0 api_generator/src/{main.rs => bin/run.rs} | 16 +++---- .../code_gen/mod.rs | 2 +- .../code_gen/namespace_clients.rs | 6 +-- .../code_gen/params.rs | 2 +- .../code_gen/request/mod.rs | 0 .../code_gen/request/request_builder.rs | 4 +- .../code_gen/root.rs | 6 +-- .../code_gen/url/enum_builder.rs | 8 ++-- .../code_gen/url/mod.rs | 0 .../code_gen/url/url_builder.rs | 4 +- .../src/{api_generator => generator}/mod.rs | 12 +++--- api_generator/src/lib.rs | 9 ++++ api_generator/src/rest_spec/mod.rs | 2 - yaml_test_runner/Cargo.toml | 1 + yaml_test_runner/src/generator.rs | 43 ++++++++++--------- yaml_test_runner/src/github.rs | 19 ++++++++ yaml_test_runner/src/main.rs | 36 +++++++++++++++- 18 files changed, 112 insertions(+), 58 deletions(-) rename api_generator/{ => rest_specs}/last_downloaded_version (100%) rename api_generator/src/{main.rs => bin/run.rs} (90%) rename api_generator/src/{api_generator => generator}/code_gen/mod.rs (99%) rename api_generator/src/{api_generator => generator}/code_gen/namespace_clients.rs (94%) rename api_generator/src/{api_generator => generator}/code_gen/params.rs (98%) rename api_generator/src/{api_generator => generator}/code_gen/request/mod.rs (100%) rename api_generator/src/{api_generator => generator}/code_gen/request/request_builder.rs (99%) rename api_generator/src/{api_generator => generator}/code_gen/root.rs (84%) rename api_generator/src/{api_generator => generator}/code_gen/url/enum_builder.rs (97%) rename api_generator/src/{api_generator => generator}/code_gen/url/mod.rs (100%) rename api_generator/src/{api_generator => generator}/code_gen/url/url_builder.rs (99%) rename api_generator/src/{api_generator => generator}/mod.rs (97%) create mode 100644 api_generator/src/lib.rs diff --git a/api_generator/last_downloaded_version b/api_generator/rest_specs/last_downloaded_version similarity index 100% rename from api_generator/last_downloaded_version rename to api_generator/rest_specs/last_downloaded_version diff --git a/api_generator/src/main.rs b/api_generator/src/bin/run.rs similarity index 90% rename from api_generator/src/main.rs rename to api_generator/src/bin/run.rs index 0c48b32e..21ddba8f 100644 --- a/api_generator/src/main.rs +++ b/api_generator/src/bin/run.rs @@ -1,10 +1,6 @@ extern crate dialoguer; -#[macro_use] -extern crate lazy_static; - -#[macro_use] -extern crate quote; +extern crate api_generator; use dialoguer::Input; use std::path::PathBuf; @@ -14,15 +10,15 @@ use std::{ path::Path, }; -mod api_generator; -mod error; -mod rest_spec; +use api_generator::generator; +use api_generator::error; +use api_generator::rest_spec; fn main() { // This must be run from the src root directory, with cargo run -p api_generator let download_dir = fs::canonicalize(PathBuf::from("./api_generator/rest_specs")).unwrap(); let generated_dir = fs::canonicalize(PathBuf::from("./elasticsearch/src/generated")).unwrap(); - let last_downloaded_version = "./api_generator/last_downloaded_version"; + let last_downloaded_version = "./api_generator/rest_specs/last_downloaded_version"; let mut download_specs = false; let mut answer = String::new(); @@ -96,7 +92,7 @@ fn main() { fs::create_dir_all(&generated_dir).unwrap(); - api_generator::generate(&branch, &download_dir, &generated_dir).unwrap(); + generator::generate(&branch, &download_dir, &generated_dir).unwrap(); } } } diff --git a/api_generator/src/api_generator/code_gen/mod.rs b/api_generator/src/generator/code_gen/mod.rs similarity index 99% rename from api_generator/src/api_generator/code_gen/mod.rs rename to api_generator/src/generator/code_gen/mod.rs index 6b7cff4d..b4b781e0 100644 --- a/api_generator/src/api_generator/code_gen/mod.rs +++ b/api_generator/src/generator/code_gen/mod.rs @@ -4,7 +4,7 @@ pub mod request; pub mod root; pub mod url; -use crate::api_generator::TypeKind; +use crate::generator::TypeKind; use inflector::Inflector; use quote::Tokens; use std::str; diff --git a/api_generator/src/api_generator/code_gen/namespace_clients.rs b/api_generator/src/generator/code_gen/namespace_clients.rs similarity index 94% rename from api_generator/src/api_generator/code_gen/namespace_clients.rs rename to api_generator/src/generator/code_gen/namespace_clients.rs index 695941bf..dfff2bd2 100644 --- a/api_generator/src/api_generator/code_gen/namespace_clients.rs +++ b/api_generator/src/generator/code_gen/namespace_clients.rs @@ -1,7 +1,7 @@ -use crate::api_generator::*; +use crate::generator::*; -use crate::api_generator::code_gen::request::request_builder::RequestBuilder; -use crate::api_generator::code_gen::*; +use crate::generator::code_gen::request::request_builder::RequestBuilder; +use crate::generator::code_gen::*; use inflector::Inflector; use quote::Tokens; diff --git a/api_generator/src/api_generator/code_gen/params.rs b/api_generator/src/generator/code_gen/params.rs similarity index 98% rename from api_generator/src/api_generator/code_gen/params.rs rename to api_generator/src/generator/code_gen/params.rs index 78687a8a..04f37efa 100644 --- a/api_generator/src/api_generator/code_gen/params.rs +++ b/api_generator/src/generator/code_gen/params.rs @@ -1,4 +1,4 @@ -use crate::api_generator::*; +use crate::generator::*; use inflector::Inflector; use quote::Tokens; diff --git a/api_generator/src/api_generator/code_gen/request/mod.rs b/api_generator/src/generator/code_gen/request/mod.rs similarity index 100% rename from api_generator/src/api_generator/code_gen/request/mod.rs rename to api_generator/src/generator/code_gen/request/mod.rs diff --git a/api_generator/src/api_generator/code_gen/request/request_builder.rs b/api_generator/src/generator/code_gen/request/request_builder.rs similarity index 99% rename from api_generator/src/api_generator/code_gen/request/request_builder.rs rename to api_generator/src/generator/code_gen/request/request_builder.rs index 7aa958d7..5947b5c2 100644 --- a/api_generator/src/api_generator/code_gen/request/request_builder.rs +++ b/api_generator/src/generator/code_gen/request/request_builder.rs @@ -1,4 +1,4 @@ -use crate::api_generator::{ +use crate::generator::{ code_gen, code_gen::url::enum_builder::EnumBuilder, code_gen::*, ApiEndpoint, HttpMethod, Type, TypeKind, }; @@ -367,13 +367,11 @@ impl<'a> RequestBuilder<'a> { enum_builder: &EnumBuilder, accepts_nd_body: bool, ) -> Tokens { - // TODO: lazy_static! for this? let mut common_fields: Vec = common_params .iter() .map(Self::create_struct_field) .collect(); - // TODO: lazy_static! for this? let mut common_builder_fns: Vec = common_params.iter().map(Self::create_impl_fn).collect(); diff --git a/api_generator/src/api_generator/code_gen/root.rs b/api_generator/src/generator/code_gen/root.rs similarity index 84% rename from api_generator/src/api_generator/code_gen/root.rs rename to api_generator/src/generator/code_gen/root.rs index 880e9cbd..69ccb41f 100644 --- a/api_generator/src/api_generator/code_gen/root.rs +++ b/api_generator/src/generator/code_gen/root.rs @@ -1,7 +1,7 @@ -use crate::api_generator::*; +use crate::generator::*; -use crate::api_generator::code_gen::request::request_builder::RequestBuilder; -use crate::api_generator::code_gen::*; +use crate::generator::code_gen::request::request_builder::RequestBuilder; +use crate::generator::code_gen::*; use inflector::Inflector; use quote::Tokens; diff --git a/api_generator/src/api_generator/code_gen/url/enum_builder.rs b/api_generator/src/generator/code_gen/url/enum_builder.rs similarity index 97% rename from api_generator/src/api_generator/code_gen/url/enum_builder.rs rename to api_generator/src/generator/code_gen/url/enum_builder.rs index 17217edd..d1b6e7d8 100644 --- a/api_generator/src/api_generator/code_gen/url/enum_builder.rs +++ b/api_generator/src/generator/code_gen/url/enum_builder.rs @@ -15,8 +15,8 @@ * limitations under the License. */ -use crate::api_generator::code_gen::url::url_builder::{IntoExpr, UrlBuilder}; -use crate::api_generator::{code_gen::*, ApiEndpoint, Path}; +use crate::generator::code_gen::url::url_builder::{IntoExpr, UrlBuilder}; +use crate::generator::{code_gen::*, ApiEndpoint, Path}; use inflector::Inflector; /// Builder for request url parts enum @@ -289,9 +289,9 @@ mod tests { #![cfg_attr(rustfmt, rustfmt_skip)] use super::*; - use crate::api_generator::{Url, Path, HttpMethod, Body, Deprecated, Type, TypeKind, Documentation, ast_eq}; + use crate::generator::{Url, Path, HttpMethod, Body, Deprecated, Type, TypeKind, Documentation, ast_eq}; use std::collections::BTreeMap; - use crate::api_generator::code_gen::url::url_builder::PathString; + use crate::generator::code_gen::url::url_builder::PathString; #[test] fn generate_parts_enum_from_endpoint() { diff --git a/api_generator/src/api_generator/code_gen/url/mod.rs b/api_generator/src/generator/code_gen/url/mod.rs similarity index 100% rename from api_generator/src/api_generator/code_gen/url/mod.rs rename to api_generator/src/generator/code_gen/url/mod.rs diff --git a/api_generator/src/api_generator/code_gen/url/url_builder.rs b/api_generator/src/generator/code_gen/url/url_builder.rs similarity index 99% rename from api_generator/src/api_generator/code_gen/url/url_builder.rs rename to api_generator/src/generator/code_gen/url/url_builder.rs index 84654ea3..27f0502d 100644 --- a/api_generator/src/api_generator/code_gen/url/url_builder.rs +++ b/api_generator/src/generator/code_gen/url/url_builder.rs @@ -15,8 +15,8 @@ * limitations under the License. */ -use crate::api_generator::code_gen::*; -use crate::api_generator::{Path, Type, TypeKind}; +use crate::generator::code_gen::*; +use crate::generator::{Path, Type, TypeKind}; use quote::ToTokens; use serde::{Deserialize, Deserializer}; use std::{collections::BTreeMap, fmt, iter::Iterator, str}; diff --git a/api_generator/src/api_generator/mod.rs b/api_generator/src/generator/mod.rs similarity index 97% rename from api_generator/src/api_generator/mod.rs rename to api_generator/src/generator/mod.rs index edd94051..0e216fcd 100644 --- a/api_generator/src/api_generator/mod.rs +++ b/api_generator/src/generator/mod.rs @@ -1,4 +1,4 @@ -use crate::api_generator::code_gen::url::url_builder::PathString; +use crate::generator::code_gen::url::url_builder::PathString; use rustfmt_nightly::{Config, Edition, EmitMode, Input, Session}; use serde::{Deserialize, Deserializer}; use serde_json::Value; @@ -148,7 +148,7 @@ pub struct Body { } lazy_static! { - static ref MAJOR_MINOR_VERSION: Version = + static ref VERSION: Version = semver::Version::parse(env!("CARGO_PKG_VERSION")).unwrap(); } @@ -185,7 +185,7 @@ impl DocumentationUrlString { "/master", format!( "/{}.{}", - MAJOR_MINOR_VERSION.major, MAJOR_MINOR_VERSION.minor + VERSION.major, VERSION.minor ) .as_str(), ) @@ -198,7 +198,7 @@ impl DocumentationUrlString { "/current", format!( "/{}.{}", - MAJOR_MINOR_VERSION.major, MAJOR_MINOR_VERSION.minor + VERSION.major, VERSION.minor ) .as_str(), ) @@ -420,7 +420,7 @@ fn write_file(input: String, dir: &PathBuf, file: &str) -> Result<(), failure::E } /// Reads Api from a directory of REST Api specs -fn read_api(branch: &str, download_dir: &PathBuf) -> Result { +pub fn read_api(branch: &str, download_dir: &PathBuf) -> Result { let paths = fs::read_dir(download_dir)?; let mut namespaces = BTreeMap::new(); let mut enums: HashSet = HashSet::new(); @@ -434,7 +434,7 @@ fn read_api(branch: &str, download_dir: &PathBuf) -> Result if name .unwrap() - .map(|name| !name.starts_with('_')) + .map(|name| name.ends_with(".json") && !name.starts_with('_')) .unwrap_or(true) { let mut file = File::open(&path)?; diff --git a/api_generator/src/lib.rs b/api_generator/src/lib.rs new file mode 100644 index 00000000..be78fe12 --- /dev/null +++ b/api_generator/src/lib.rs @@ -0,0 +1,9 @@ +#[macro_use] +extern crate lazy_static; + +#[macro_use] +extern crate quote; + +pub mod generator; +pub mod error; +pub mod rest_spec; \ No newline at end of file diff --git a/api_generator/src/rest_spec/mod.rs b/api_generator/src/rest_spec/mod.rs index 3380b75c..2b6f0d0c 100644 --- a/api_generator/src/rest_spec/mod.rs +++ b/api_generator/src/rest_spec/mod.rs @@ -1,5 +1,3 @@ -extern crate reqwest; - mod parallel_downloading; use parallel_downloading::download_specs_to_dir; diff --git a/yaml_test_runner/Cargo.toml b/yaml_test_runner/Cargo.toml index 85e2aa50..81ad3e44 100644 --- a/yaml_test_runner/Cargo.toml +++ b/yaml_test_runner/Cargo.toml @@ -8,6 +8,7 @@ repository = "https://github.com/elastic/elasticsearch-rs" [dependencies] elasticsearch = { version = "7.6.0-alpha.1", path = "../elasticsearch" } +api_generator = { version = "7.6.0-alpha.1", path = "../api_generator" } clap = "~2" failure = "0.1.6" diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 9cda11b8..a8a8b54c 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -6,11 +6,12 @@ use yaml_rust::{ yaml::{Array, Hash}, Yaml, YamlLoader, }; +use api_generator::generator::Api; struct YamlTest { setup: Option, teardown: Option, - tests: Vec<(String, Tokens)> + tests: Vec<(String, Tokens)>, } impl YamlTest { @@ -23,13 +24,13 @@ impl YamlTest { } } -pub fn generate_tests_from_yaml(download_dir: &PathBuf) -> Result<(), failure::Error> { +pub fn generate_tests_from_yaml(download_dir: &PathBuf, api: &Api) -> Result<(), failure::Error> { let paths = fs::read_dir(download_dir).unwrap(); for entry in paths { if let Ok(entry) = entry { if let Ok(file_type) = entry.file_type() { if file_type.is_dir() { - generate_tests_from_yaml(&entry.path())?; + generate_tests_from_yaml(&entry.path(), api)?; } else if file_type.is_file() { let file_name = entry.file_name().to_string_lossy().into_owned(); if !file_name.ends_with(".yml") && !file_name.ends_with(".yaml") { @@ -122,7 +123,6 @@ fn read_steps(steps: &Array) -> Result { } fn read_do(hash: &mut Hash, tokens: &mut Tokens) -> Result<(), failure::Error> { - let mut entries = hash.entries(); let headers = String::from("headers"); @@ -131,27 +131,28 @@ fn read_do(hash: &mut Hash, tokens: &mut Tokens) -> Result<(), failure::Error> { let result: Result, _> = entries .into_iter() - .map(|entry| { - match (entry.key(), entry.get()) { - (Yaml::String(headers), Yaml::Hash(h)) => Ok(()), - (Yaml::String(catch), Yaml::String(s)) => Ok(()), - (Yaml::String(node_selector), _) => Ok(()), - (Yaml::String(s), Yaml::Hash(h)) => { - let fn_name = s.clone().replace(".", "()."); - let fn_name_ident = syn::Ident::from(fn_name); - - tokens.append(quote! { - let response = client.#fn_name_ident().await?; - }); - Ok(()) - }, - (k, v) => Err(failure::err_msg(format!("{:?} and {:?} are not a string and hash", &k, &v))), + .map(|entry| match (entry.key(), entry.get()) { + (Yaml::String(headers), Yaml::Hash(h)) => Ok(()), + (Yaml::String(catch), Yaml::String(s)) => Ok(()), + (Yaml::String(node_selector), _) => Ok(()), + (Yaml::String(s), Yaml::Hash(h)) => { + let fn_name = s.clone().replace(".", "()."); + let fn_name_ident = syn::Ident::from(fn_name); + + tokens.append(quote! { + let response = client.#fn_name_ident().await?; + }); + Ok(()) } + (k, v) => Err(failure::err_msg(format!( + "{:?} and {:?} are not a string and hash", + &k, &v + ))), }) .collect(); match result { Ok(_) => Ok(()), - Err(e) => Err(e) + Err(e) => Err(e), } -} \ No newline at end of file +} diff --git a/yaml_test_runner/src/github.rs b/yaml_test_runner/src/github.rs index 17065722..5d479968 100644 --- a/yaml_test_runner/src/github.rs +++ b/yaml_test_runner/src/github.rs @@ -5,6 +5,7 @@ use std::fmt::Formatter; use std::fs::File; use std::path::PathBuf; use std::{fs, io}; +use io::Write; struct YamlTestSuite { dir: String, @@ -36,7 +37,20 @@ struct GitHubContent { links: Links, } +/// Downloads the yaml tests if not already downloaded pub fn download_test_suites(token: &str, branch: &str, download_dir: &PathBuf) { + + let mut last_downloaded_version = download_dir.clone(); + last_downloaded_version.push("last_downloaded_version"); + if last_downloaded_version.exists() { + let version = fs::read_to_string(&last_downloaded_version) + .expect("Unable to read last_downloaded_version of yaml tests"); + if version == branch { + println!("yaml tests for branch {} already downloaded", branch); + return; + } + } + let test_suite_map = [ ("oss".to_string(), "https://api.github.com/repos/elastic/elasticsearch/contents/rest-api-spec/src/main/resources/rest-api-spec/test".to_string()), ("xpack".to_string(), "https://api.github.com/repos/elastic/elasticsearch/contents/x-pack/plugin/src/test/resources/rest-api-spec/test".to_string())]; @@ -65,6 +79,11 @@ pub fn download_test_suites(token: &str, branch: &str, download_dir: &PathBuf) { for suite in test_suites { download_tests(&client, &suite, &download_dir).unwrap(); } + + File::create(last_downloaded_version) + .expect("failed to create last_downloaded_version file") + .write_all(branch.as_bytes()) + .expect("unable to write branch to last_downloaded_version file"); } fn download_tests( diff --git a/yaml_test_runner/src/main.rs b/yaml_test_runner/src/main.rs index c502ced8..76c02092 100644 --- a/yaml_test_runner/src/main.rs +++ b/yaml_test_runner/src/main.rs @@ -1,8 +1,11 @@ #[macro_use] extern crate quote; +extern crate api_generator; + use clap::{App, Arg}; use std::path::PathBuf; +use std::fs; mod generator; mod github; @@ -25,15 +28,44 @@ fn main() { .help("The GitHub access token. Required to increase the rate limit to be able to download all yaml tests") .required(true) .takes_value(true)) + .arg(Arg::with_name("path") + .short("p") + .long("path") + .value_name("PATH") + .help("The path to the rest API specs. Required to build a representation of the client API.") + .required(true) + .takes_value(true)) .get_matches(); let branch = matches .value_of("branch") .expect("missing 'branch' argument"); let token = matches.value_of("token").expect("missing 'token' argument"); + let path = matches.value_of("path").expect("missing 'path' argument"); + let rest_specs_dir = PathBuf::from(path); let download_dir = PathBuf::from("./yaml_test_runner/yaml"); - //github::download_test_suites(token, branch, &download_dir); + github::download_test_suites(token, branch, &download_dir); + + let mut last_downloaded_version = rest_specs_dir.clone(); + last_downloaded_version.push("last_downloaded_version"); + + let mut download_rest_specs = true; + if last_downloaded_version.exists() { + let version = fs::read_to_string(last_downloaded_version) + .expect("Could not rest specs last_downloaded version into string"); + + if version == branch { + println!("rest specs for branch {} already downloaded in {:?}", branch, &rest_specs_dir); + download_rest_specs = false; + } + } + + if download_rest_specs { + api_generator::rest_spec::download_specs(branch, &rest_specs_dir) + } + + let api = api_generator::generator::read_api(branch, &rest_specs_dir).unwrap(); - generator::generate_tests_from_yaml(&download_dir).unwrap(); + generator::generate_tests_from_yaml(&download_dir, &api).unwrap(); } From e4ff4e4e05528d976a8e96aa3479b3e45255ffb0 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 3 Mar 2020 08:37:36 +1000 Subject: [PATCH 004/127] return Result types This commit simplifies some functions by changing return types to Result<(), failure::Error> --- api_generator/src/bin/run.rs | 33 +++++++++++++----------------- api_generator/src/rest_spec/mod.rs | 14 +++++++------ yaml_test_runner/src/main.rs | 10 +++++---- 3 files changed, 28 insertions(+), 29 deletions(-) diff --git a/api_generator/src/bin/run.rs b/api_generator/src/bin/run.rs index 21ddba8f..869d3e93 100644 --- a/api_generator/src/bin/run.rs +++ b/api_generator/src/bin/run.rs @@ -7,23 +7,21 @@ use std::path::PathBuf; use std::{ fs::{self, File}, io::Write, - path::Path, }; use api_generator::generator; -use api_generator::error; use api_generator::rest_spec; -fn main() { +fn main() -> Result<(), failure::Error> { // This must be run from the src root directory, with cargo run -p api_generator - let download_dir = fs::canonicalize(PathBuf::from("./api_generator/rest_specs")).unwrap(); - let generated_dir = fs::canonicalize(PathBuf::from("./elasticsearch/src/generated")).unwrap(); - let last_downloaded_version = "./api_generator/rest_specs/last_downloaded_version"; + let download_dir = fs::canonicalize(PathBuf::from("./api_generator/rest_specs"))?; + let generated_dir = fs::canonicalize(PathBuf::from("./elasticsearch/src/generated"))?; + let last_downloaded_version = PathBuf::from("./api_generator/rest_specs/last_downloaded_version"); let mut download_specs = false; let mut answer = String::new(); - let default_branch = if Path::new(last_downloaded_version).exists() { - fs::read_to_string(last_downloaded_version).expect("Could not read branch into string") + let default_branch = if last_downloaded_version.exists() { + fs::read_to_string(&last_downloaded_version)? } else { String::from("master") }; @@ -54,13 +52,9 @@ fn main() { .interact() .unwrap(); - fs::remove_dir_all(&download_dir).unwrap(); - rest_spec::download_specs(&branch, &download_dir); - - File::create(last_downloaded_version) - .expect("failed to create last_downloaded_version file") - .write_all(branch.as_bytes()) - .expect("unable to write branch to last_downloaded_version file"); + fs::remove_dir_all(&download_dir)?; + rest_spec::download_specs(&branch, &download_dir)?; + File::create(&last_downloaded_version)?.write_all(branch.as_bytes())?; } // only offer to generate if there are downloaded specs @@ -87,12 +81,13 @@ fn main() { if generate_code { // delete existing generated files if the exist if generated_dir.exists() { - fs::remove_dir_all(&generated_dir).unwrap(); + fs::remove_dir_all(&generated_dir)?; } - fs::create_dir_all(&generated_dir).unwrap(); - - generator::generate(&branch, &download_dir, &generated_dir).unwrap(); + fs::create_dir_all(&generated_dir)?; + generator::generate(&branch, &download_dir, &generated_dir)?; } } + + Ok(()) } diff --git a/api_generator/src/rest_spec/mod.rs b/api_generator/src/rest_spec/mod.rs index 2b6f0d0c..a415e3b4 100644 --- a/api_generator/src/rest_spec/mod.rs +++ b/api_generator/src/rest_spec/mod.rs @@ -35,7 +35,7 @@ struct RestApiSpec { links: Links, } -pub fn download_specs(branch: &str, download_dir: &PathBuf) { +pub fn download_specs(branch: &str, download_dir: &PathBuf) -> Result<(), failure::Error> { let spec_urls = [ ("core".to_string(), "https://api.github.com/repos/elastic/elasticsearch/contents/rest-api-spec/src/main/resources/rest-api-spec/api".to_string()), ("xpack".to_string(), "https://api.github.com/repos/elastic/elasticsearch/contents/x-pack/plugin/src/test/resources/rest-api-spec/api".to_string())]; @@ -52,17 +52,19 @@ pub fn download_specs(branch: &str, download_dir: &PathBuf) { }) .collect(); - fs::create_dir_all(download_dir).unwrap(); + fs::create_dir_all(download_dir)?; for spec in specs { - download_endpoints(&spec, &download_dir); + download_endpoints(&spec, &download_dir)?; } + + Ok(()) } -fn download_endpoints(spec: &GitHubSpec, download_dir: &PathBuf) { +fn download_endpoints(spec: &GitHubSpec, download_dir: &PathBuf) -> Result<(), failure::Error> { let mut response = reqwest::get(&spec.url).unwrap(); let rest_api_specs: Vec = response.json().unwrap(); - println!("Downloading {} specs from {}", spec.dir, spec.branch); - download_specs_to_dir(rest_api_specs.as_slice(), download_dir).unwrap(); + download_specs_to_dir(rest_api_specs.as_slice(), download_dir)?; println!("Done downloading {} specs from {}", spec.dir, spec.branch); + Ok(()) } diff --git a/yaml_test_runner/src/main.rs b/yaml_test_runner/src/main.rs index 76c02092..dc9d95c3 100644 --- a/yaml_test_runner/src/main.rs +++ b/yaml_test_runner/src/main.rs @@ -10,7 +10,7 @@ use std::fs; mod generator; mod github; -fn main() { +fn main() -> Result<(), failure::Error> { let matches = App::new(env!("CARGO_PKG_NAME")) .about(env!("CARGO_PKG_DESCRIPTION")) .arg(Arg::with_name("branch") @@ -62,10 +62,12 @@ fn main() { } if download_rest_specs { - api_generator::rest_spec::download_specs(branch, &rest_specs_dir) + api_generator::rest_spec::download_specs(branch, &rest_specs_dir)?; } - let api = api_generator::generator::read_api(branch, &rest_specs_dir).unwrap(); + let api = api_generator::generator::read_api(branch, &rest_specs_dir)?; - generator::generate_tests_from_yaml(&download_dir, &api).unwrap(); + generator::generate_tests_from_yaml(&download_dir, &api)?; + + Ok(()) } From 22ab24b44d74c2f0f229149a7c5899305be7f139 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 3 Mar 2020 11:31:53 +1000 Subject: [PATCH 005/127] tidy up --- yaml_test_runner/src/generator.rs | 95 ++++++++++++++++++------------- 1 file changed, 56 insertions(+), 39 deletions(-) diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index a8a8b54c..9eee9f04 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -33,42 +33,45 @@ pub fn generate_tests_from_yaml(download_dir: &PathBuf, api: &Api) -> Result<(), generate_tests_from_yaml(&entry.path(), api)?; } else if file_type.is_file() { let file_name = entry.file_name().to_string_lossy().into_owned(); + + // skip non-yaml files if !file_name.ends_with(".yml") && !file_name.ends_with(".yaml") { continue; } let yaml = fs::read_to_string(&entry.path()).unwrap(); + + // a yaml test can contain multiple yaml docs let docs = YamlLoader::load_from_str(&yaml).unwrap(); let mut test = YamlTest::new(docs.len()); for doc in docs { - println!("{:?}", &entry.path()); - if let Some(mut hash) = doc.into_hash() { - let entries = hash.entries(); - let first = entries.into_iter().next().unwrap(); - match (first.key(), first.get()) { + //println!("{:?}", &entry.path()); + if let Some(mut hash) = doc.as_hash() { + + let (first_key, first_value) = hash.iter().next().unwrap(); + match (first_key, first_value) { (Yaml::String(name), Yaml::Array(steps)) => { let tokens = read_steps(steps)?; match name.as_str() { "setup" => test.setup = Some(tokens), "teardown" => test.teardown = Some(tokens), - name => { - test.tests.push((name.to_owned(), tokens)); - } + name => test.tests.push((name.to_owned(), tokens)), }; } (k, v) => { return Err(failure::err_msg(format!( - "{:?} and {:?} in {:?} is not a string and array", + "Expected string key and array value in {:?}, but found {:?} and {:?}", + &entry.path(), &k, &v, - &entry.path() - ))) + ))); } } } else { return Err(failure::err_msg(format!( - "{:?} is not a hash", + "Expected hash but found {:?} in {:?}", + &doc, &entry.path() ))); } @@ -123,36 +126,50 @@ fn read_steps(steps: &Array) -> Result { } fn read_do(hash: &mut Hash, tokens: &mut Tokens) -> Result<(), failure::Error> { - let mut entries = hash.entries(); - - let headers = String::from("headers"); - let catch = String::from("catch"); - let node_selector = String::from("node_selector"); - - let result: Result, _> = entries - .into_iter() - .map(|entry| match (entry.key(), entry.get()) { - (Yaml::String(headers), Yaml::Hash(h)) => Ok(()), - (Yaml::String(catch), Yaml::String(s)) => Ok(()), - (Yaml::String(node_selector), _) => Ok(()), - (Yaml::String(s), Yaml::Hash(h)) => { - let fn_name = s.clone().replace(".", "()."); - let fn_name_ident = syn::Ident::from(fn_name); - - tokens.append(quote! { - let response = client.#fn_name_ident().await?; - }); - Ok(()) + let results: Vec> = hash + .iter() + .map(|(k, v)| { + match k.as_str() { + Some(key) => { + match key { + "headers" => Ok(()), + "catch" => Ok(()), + "node_selector" => Ok(()), + api_call => { + let fn_name = api_call.clone().replace(".", "()."); + let fn_name_ident = syn::Ident::from(fn_name); + + tokens.append(quote! { + let response = client.#fn_name_ident().await?; + }); + Ok(()) + }, + } + }, + None => Err(failure::err_msg(format!("expected string key but found {:?}", k))) } - (k, v) => Err(failure::err_msg(format!( - "{:?} and {:?} are not a string and hash", - &k, &v - ))), }) .collect(); - match result { - Ok(_) => Ok(()), - Err(e) => Err(e), + ok_or_accumulate(results) +} + +fn ok_or_accumulate( + results: Vec>, +) -> Result<(), failure::Error> { + let errs = results + .into_iter() + .filter_map(|r| r.err()) + .collect::>(); + if errs.is_empty() { + Ok(()) + } else { + let msg = errs + .iter() + .map(|e| e.to_string()) + .collect::>() + .join("\n"); + + Err(failure::err_msg(msg)) } } From bd7dd71824afea3c8b82f940858b57ad1a2e7a09 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 3 Mar 2020 15:38:36 +1000 Subject: [PATCH 006/127] Start of test file generation --- .gitignore | 1 + api_generator/src/generator/mod.rs | 12 +- yaml_test_runner/Cargo.toml | 1 + yaml_test_runner/src/generator.rs | 198 +++++++++++++++++++++++++---- yaml_test_runner/src/main.rs | 3 +- 5 files changed, 182 insertions(+), 33 deletions(-) diff --git a/.gitignore b/.gitignore index 71ee2d0d..ee9798ac 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ Cargo.lock .vscode/ *.log yaml_test_runner/yaml/ +yaml_test_runner/src/generated/ diff --git a/api_generator/src/generator/mod.rs b/api_generator/src/generator/mod.rs index 0e216fcd..95117aac 100644 --- a/api_generator/src/generator/mod.rs +++ b/api_generator/src/generator/mod.rs @@ -136,7 +136,7 @@ pub struct Path { /// The URL components of an API endpoint #[derive(Debug, PartialEq, Deserialize, Clone)] pub struct Url { - paths: Vec, + pub paths: Vec, } /// Body of an API endpoint @@ -284,11 +284,11 @@ where pub struct ApiEndpoint { #[serde(deserialize_with = "string_or_struct")] documentation: Documentation, - stability: String, - url: Url, + pub stability: String, + pub url: Url, #[serde(default = "BTreeMap::new")] - params: BTreeMap, - body: Option, + pub params: BTreeMap, + pub body: Option, } impl ApiEndpoint { @@ -546,7 +546,7 @@ where /// formats tokens using rustfmt /// https://github.com/bcmyers/num-format/blob/b7a99480b8087924d291887b13d8c38b7ce43a36/num-format-dev/src/rustfmt.rs -fn rust_fmt(module: S) -> Result +pub fn rust_fmt(module: S) -> Result where S: Into, { diff --git a/yaml_test_runner/Cargo.toml b/yaml_test_runner/Cargo.toml index 81ad3e44..afe74650 100644 --- a/yaml_test_runner/Cargo.toml +++ b/yaml_test_runner/Cargo.toml @@ -15,6 +15,7 @@ failure = "0.1.6" quote = "~0.3" reqwest = "~0.9" serde = "~1" +serde_yaml = "0.8.11" serde_json = "~1" syn = { version = "~0.11", features = ["full"] } yaml-rust = "0.4.3" \ No newline at end of file diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 9eee9f04..ec209895 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -2,11 +2,10 @@ use quote::Tokens; use std::fs; use std::path::PathBuf; -use yaml_rust::{ - yaml::{Array, Hash}, - Yaml, YamlLoader, -}; -use api_generator::generator::Api; +use yaml_rust::{yaml::{Array, Hash}, Yaml, YamlLoader, YamlEmitter}; +use api_generator::generator::{Api, ApiEndpoint}; +use std::fs::{File, OpenOptions}; +use std::io::Write; struct YamlTest { setup: Option, @@ -24,13 +23,13 @@ impl YamlTest { } } -pub fn generate_tests_from_yaml(download_dir: &PathBuf, api: &Api) -> Result<(), failure::Error> { +pub fn generate_tests_from_yaml(api: &Api, base_download_dir: &PathBuf, download_dir: &PathBuf, generated_dir: &PathBuf) -> Result<(), failure::Error> { let paths = fs::read_dir(download_dir).unwrap(); for entry in paths { if let Ok(entry) = entry { if let Ok(file_type) = entry.file_type() { if file_type.is_dir() { - generate_tests_from_yaml(&entry.path(), api)?; + generate_tests_from_yaml(api, base_download_dir, &entry.path(), generated_dir)?; } else if file_type.is_file() { let file_name = entry.file_name().to_string_lossy().into_owned(); @@ -42,17 +41,22 @@ pub fn generate_tests_from_yaml(download_dir: &PathBuf, api: &Api) -> Result<(), let yaml = fs::read_to_string(&entry.path()).unwrap(); // a yaml test can contain multiple yaml docs - let docs = YamlLoader::load_from_str(&yaml).unwrap(); + let result = YamlLoader::load_from_str(&yaml); + if result.is_err() { + println!("error reading {:?}: {}", &entry.path(), result.err().unwrap().to_string()); + continue; + } + + let docs = result.unwrap(); let mut test = YamlTest::new(docs.len()); for doc in docs { - //println!("{:?}", &entry.path()); - if let Some(mut hash) = doc.as_hash() { + if let Some(hash) = doc.as_hash() { let (first_key, first_value) = hash.iter().next().unwrap(); match (first_key, first_value) { (Yaml::String(name), Yaml::Array(steps)) => { - let tokens = read_steps(steps)?; + let tokens = read_steps(api, steps)?; match name.as_str() { "setup" => test.setup = Some(tokens), "teardown" => test.teardown = Some(tokens), @@ -77,7 +81,7 @@ pub fn generate_tests_from_yaml(download_dir: &PathBuf, api: &Api) -> Result<(), } } - println!("{:?}", test.tests); + write_test(test, &entry.path(), base_download_dir, generated_dir)?; } } } @@ -86,24 +90,57 @@ pub fn generate_tests_from_yaml(download_dir: &PathBuf, api: &Api) -> Result<(), Ok(()) } -fn read_steps(steps: &Array) -> Result { +fn write_test(test: YamlTest, path: &PathBuf, base_download_dir: &PathBuf, generated_dir: &PathBuf) -> Result<(), failure::Error> { + let path = { + let yaml_file: String = path.to_string_lossy().into_owned(); + let file = yaml_file.replace(base_download_dir.to_str().unwrap(), generated_dir.to_str().unwrap()); + let mut path = PathBuf::from(file); + path.set_extension("rs"); + path + }; + + fs::create_dir_all(&path.parent().unwrap())?; + let mut file = OpenOptions::new().create(true).append(true).open(&path)?; + file.write_all( + "// ----------------------------------------------- +// ███╗ ██╗ ██████╗ ████████╗██╗ ██████╗███████╗ +// ████╗ ██║██╔═══██╗╚══██╔══╝██║██╔════╝██╔════╝ +// ██╔██╗ ██║██║ ██║ ██║ ██║██║ █████╗ +// ██║╚██╗██║██║ ██║ ██║ ██║██║ ██╔══╝ +// ██║ ╚████║╚██████╔╝ ██║ ██║╚██████╗███████╗ +// ╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝╚══════╝ +// ----------------------------------------------- +// +// This file is generated, +// Please do not edit it manually. +// Run the following in the root of the repo: +// +// cargo run -p yaml_test_runner -- --branch --token --path +// +// ----------------------------------------------- +" + .as_bytes(), + )?; + + Ok(()) +} + + + +fn read_steps(api: &Api, steps: &Array) -> Result { let mut tokens = Tokens::new(); for step in steps { - if let Some(mut hash) = step.clone().into_hash() { - let mut entries = hash.entries(); - let mut first = entries.next().unwrap(); + if let Some(hash) = step.as_hash() { + let (k, v) = hash.iter().next().unwrap(); - let mut key = first.key().as_str().unwrap(); - let mut value = first.get().clone(); + let key = k.as_str().unwrap(); - match (key, value) { + match (key, v) { ("skip", Yaml::Hash(h)) => {} - ("do", Yaml::Hash(ref mut h)) => { - read_do(h, &mut tokens)?; - } + ("do", Yaml::Hash(h)) => read_do(api,h, &mut tokens)?, ("set", Yaml::Hash(h)) => {} ("transform_and_set", Yaml::Hash(h)) => {} - ("match", Yaml::Hash(h)) => {} + ("match", Yaml::Hash(h)) => read_match(api, h, &mut tokens)?, ("contains", Yaml::Hash(h)) => {} ("is_true", Yaml::Hash(h)) => {} ("is_true", Yaml::String(s)) => {} @@ -125,7 +162,11 @@ fn read_steps(steps: &Array) -> Result { Ok(tokens) } -fn read_do(hash: &mut Hash, tokens: &mut Tokens) -> Result<(), failure::Error> { +fn read_match(api: &Api, hash: &Hash, tokens: &mut Tokens) -> Result<(), failure::Error> { + Ok(()) +} + +fn read_do(api: &Api, hash: &Hash, tokens: &mut Tokens) -> Result<(), failure::Error> { let results: Vec> = hash .iter() .map(|(k, v)| { @@ -135,12 +176,70 @@ fn read_do(hash: &mut Hash, tokens: &mut Tokens) -> Result<(), failure::Error> { "headers" => Ok(()), "catch" => Ok(()), "node_selector" => Ok(()), + "warnings" => Ok(()), api_call => { - let fn_name = api_call.clone().replace(".", "()."); + + let c = v.as_hash(); + if c.is_none() { + return Err(failure::err_msg(format!("Expected hash but found {:?}", v))); + } + + let endpoint = endpoint_from_api_call(api, &api_call); + + if endpoint.is_none() { + return Ok(()); + } + + let endpoint = endpoint.unwrap(); + let mut parts: Vec<(&str, &Yaml)> = vec![]; + let mut params: Vec<(&str, &Yaml)> = vec![]; + let mut body_call: Option = None; + + for (k,v) in c.unwrap().iter() { + let key = k.as_str().unwrap(); + + if endpoint.params.contains_key(key) { + params.push((key, v)); + } else if key == "body" { + body_call = create_body_call(v); + } else { + parts.push((key, v)); + } + } + + let fn_name = api_call.replace(".", "()."); let fn_name_ident = syn::Ident::from(fn_name); + let params_calls = match params.len() { + 0 => None, + _ => { + let mut tokens = Tokens::new(); + for (n, v) in params { + let param_ident = syn::Ident::from(n); + + match v { + Yaml::String(s) => tokens.append(quote! { + .#param_ident(#s) + }), + Yaml::Boolean(b) => tokens.append(quote! { + .#param_ident(#b) + }), + Yaml::Integer(i) => tokens.append(quote! { + .#param_ident(#i) + }), + _ => println!("Unsupported value {:?}", v), + } + } + + Some(tokens) + } + }; + tokens.append(quote! { - let response = client.#fn_name_ident().await?; + let response = client.#fn_name_ident() + #params_calls + #body_call + .await?; }); Ok(()) }, @@ -154,6 +253,53 @@ fn read_do(hash: &mut Hash, tokens: &mut Tokens) -> Result<(), failure::Error> { ok_or_accumulate(results) } +fn create_body_call(v: &Yaml) -> Option { + match v { + Yaml::String(s) => Some(quote!(.body(json!(#s)))), + _ => { + let mut s = String::new(); + { + let mut emitter = YamlEmitter::new(&mut s); + emitter.dump(v).unwrap(); + } + let v: serde_json::Value = serde_yaml::from_str(&s).unwrap(); + let json = serde_json::to_string(&v).unwrap(); + let ident = syn::Ident::from(json); + Some(quote!(.body(json!(#ident)))) + } + } +} + +fn endpoint_from_api_call<'a>(api: &'a Api, api_call: &str) -> Option<&'a ApiEndpoint> { + let api_call_path: Vec<&str> = api_call.split('.').collect(); + match api_call_path.len() { + 1 => match api.root.get(api_call_path[0]) { + Some(endpoint) => Some(endpoint), + None => { + println!("No ApiEndpoint found for {}. skipping", api_call_path[0]); + None + } + }, + _ => { + match api.namespaces.get(api_call_path[0]) { + Some(namespace) => { + match namespace.get(api_call_path[1]) { + Some(endpoint) => Some(endpoint), + None => { + println!("No ApiEndpoint found for {:?}. skipping", &api_call_path); + None + } + } + }, + None => { + println!("No ApiEndpoint found for {:?}. skipping", &api_call_path); + None + } + } + }, + } +} + fn ok_or_accumulate( results: Vec>, ) -> Result<(), failure::Error> { diff --git a/yaml_test_runner/src/main.rs b/yaml_test_runner/src/main.rs index dc9d95c3..7ff90579 100644 --- a/yaml_test_runner/src/main.rs +++ b/yaml_test_runner/src/main.rs @@ -44,6 +44,7 @@ fn main() -> Result<(), failure::Error> { let path = matches.value_of("path").expect("missing 'path' argument"); let rest_specs_dir = PathBuf::from(path); let download_dir = PathBuf::from("./yaml_test_runner/yaml"); + let generated_dir = PathBuf::from("./yaml_test_runner/src/generated"); github::download_test_suites(token, branch, &download_dir); @@ -67,7 +68,7 @@ fn main() -> Result<(), failure::Error> { let api = api_generator::generator::read_api(branch, &rest_specs_dir)?; - generator::generate_tests_from_yaml(&download_dir, &api)?; + generator::generate_tests_from_yaml(&api, &download_dir, &download_dir, &generated_dir)?; Ok(()) } From 06784f02bde85b07a0ae2a3113193733fba34441 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 3 Mar 2020 22:45:43 +1000 Subject: [PATCH 007/127] Generate tests WIP This commit starts generating tests from the steps read from yaml so far. --- api_generator/src/bin/run.rs | 8 +- api_generator/src/generator/code_gen/mod.rs | 2 +- api_generator/src/generator/mod.rs | 2 +- yaml_test_runner/Cargo.toml | 8 +- yaml_test_runner/src/client.rs | 46 ++++++++ yaml_test_runner/src/generator.rs | 114 ++++++++++++++++++-- yaml_test_runner/src/main.rs | 9 ++ 7 files changed, 173 insertions(+), 16 deletions(-) create mode 100644 yaml_test_runner/src/client.rs diff --git a/api_generator/src/bin/run.rs b/api_generator/src/bin/run.rs index 869d3e93..bc64f48f 100644 --- a/api_generator/src/bin/run.rs +++ b/api_generator/src/bin/run.rs @@ -1,7 +1,10 @@ extern crate dialoguer; - extern crate api_generator; +use api_generator::{ + generator, + rest_spec +}; use dialoguer::Input; use std::path::PathBuf; use std::{ @@ -9,9 +12,6 @@ use std::{ io::Write, }; -use api_generator::generator; -use api_generator::rest_spec; - fn main() -> Result<(), failure::Error> { // This must be run from the src root directory, with cargo run -p api_generator let download_dir = fs::canonicalize(PathBuf::from("./api_generator/rest_specs"))?; diff --git a/api_generator/src/generator/code_gen/mod.rs b/api_generator/src/generator/code_gen/mod.rs index b4b781e0..361a44c6 100644 --- a/api_generator/src/generator/code_gen/mod.rs +++ b/api_generator/src/generator/code_gen/mod.rs @@ -54,7 +54,7 @@ pub fn parse_expr(input: quote::Tokens) -> syn::Expr { } /// Ensures that the name generated is one that is valid for Rust -fn valid_name(s: &str) -> &str { +pub fn valid_name(s: &str) -> &str { match s { "type" => "ty", s => s, diff --git a/api_generator/src/generator/mod.rs b/api_generator/src/generator/mod.rs index 95117aac..e1108cbf 100644 --- a/api_generator/src/generator/mod.rs +++ b/api_generator/src/generator/mod.rs @@ -19,7 +19,7 @@ use std::marker::PhantomData; use std::str::FromStr; use void::Void; -mod code_gen; +pub mod code_gen; /// A complete API specification parsed from the REST API specs pub struct Api { diff --git a/yaml_test_runner/Cargo.toml b/yaml_test_runner/Cargo.toml index afe74650..c8a458e2 100644 --- a/yaml_test_runner/Cargo.toml +++ b/yaml_test_runner/Cargo.toml @@ -12,10 +12,16 @@ api_generator = { version = "7.6.0-alpha.1", path = "../api_generator" } clap = "~2" failure = "0.1.6" +Inflector = "0.11.4" quote = "~0.3" reqwest = "~0.9" serde = "~1" serde_yaml = "0.8.11" serde_json = "~1" syn = { version = "~0.11", features = ["full"] } -yaml-rust = "0.4.3" \ No newline at end of file +sysinfo = "0.9.6" +url = "2.1.1" +yaml-rust = "0.4.3" + +[dev-dependencies] +tokio = { version = "0.2.0", default-features = false, features = ["macros", "tcp", "time"] } diff --git a/yaml_test_runner/src/client.rs b/yaml_test_runner/src/client.rs new file mode 100644 index 00000000..828536e5 --- /dev/null +++ b/yaml_test_runner/src/client.rs @@ -0,0 +1,46 @@ +use elasticsearch::{ + auth::Credentials, + http::{ + transport::{ + TransportBuilder, + SingleNodeConnectionPool + } + }, + Elasticsearch, DEFAULT_ADDRESS, + cert::CertificateValidation +}; +use sysinfo::SystemExt; +use url::Url; + +fn cluster_addr() -> String { + match std::env::var("ES_TEST_SERVER") { + Ok(server) => server, + Err(_) => DEFAULT_ADDRESS.into(), + } +} + +fn running_proxy() -> bool { + let system = sysinfo::System::new(); + !system.get_process_by_name("Fiddler").is_empty() +} + +pub fn create() -> Elasticsearch { + let url = Url::parse(cluster_addr().as_ref()).unwrap(); + let conn_pool = SingleNodeConnectionPool::new(url.clone()); + let mut builder = TransportBuilder::new(conn_pool); + + if url.scheme() == "https" { + builder = builder + .auth(Credentials::Basic("elastic".into(), "changeme".into())) + .cert_validation(CertificateValidation::None) + } + + if running_proxy() { + let proxy_url = Url::parse("http://localhost:8888").unwrap(); + builder = builder.proxy(proxy_url, None, None); + } + + let transport = builder.build().unwrap(); + Elasticsearch::new(transport) +} + diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index ec209895..ec33e36d 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -1,3 +1,4 @@ +use inflector::Inflector; use quote::Tokens; use std::fs; @@ -87,20 +88,57 @@ pub fn generate_tests_from_yaml(api: &Api, base_download_dir: &PathBuf, download } } + write_mod_files(&generated_dir)?; + + Ok(()) +} + +fn write_mod_files(generated_dir: &PathBuf) -> Result<(), failure::Error> { + let paths = fs::read_dir(generated_dir).unwrap(); + let mut mods = vec![]; + for path in paths { + if let Ok(entry) = path { + let file_type = entry.file_type().unwrap(); + let path = entry.path(); + let name = path.file_stem().unwrap().to_string_lossy(); + if name.into_owned() != "mod" { + mods.push(format!("pub mod {};", path.file_stem().unwrap().to_string_lossy())); + } + + if file_type.is_dir() { + write_mod_files(&entry.path())?; + } + } + } + + let mut path = generated_dir.clone(); + path.push("mod.rs"); + let mut file = File::create(&path)?; + let generated_mods: String = mods.join("\n"); + file.write_all(generated_mods.as_bytes())?; Ok(()) } fn write_test(test: YamlTest, path: &PathBuf, base_download_dir: &PathBuf, generated_dir: &PathBuf) -> Result<(), failure::Error> { let path = { - let yaml_file: String = path.to_string_lossy().into_owned(); - let file = yaml_file.replace(base_download_dir.to_str().unwrap(), generated_dir.to_str().unwrap()); + let file: String = { + let yaml_file: String = path.to_string_lossy().into_owned() + .replace(base_download_dir.to_str().unwrap(), generated_dir.to_str().unwrap()); + yaml_file + //let path = fs::canonicalize(PathBuf::from(yaml_file))?; + //path.to_string_lossy().into_owned().replace(".", "_") + }; + + println!("{}", file); let mut path = PathBuf::from(file); path.set_extension("rs"); + // modules can't start with a number + path.set_file_name(format!("_{}", &path.file_name().unwrap().to_string_lossy().into_owned())); path }; fs::create_dir_all(&path.parent().unwrap())?; - let mut file = OpenOptions::new().create(true).append(true).open(&path)?; + let mut file = File::create(&path)?; file.write_all( "// ----------------------------------------------- // ███╗ ██╗ ██████╗ ████████╗██╗ ██████╗███████╗ @@ -122,10 +160,67 @@ fn write_test(test: YamlTest, path: &PathBuf, base_download_dir: &PathBuf, gener .as_bytes(), )?; - Ok(()) -} + let (setup_fn, setup_call) = if let Some(s) = &test.setup { + (Some(quote!{ + async fn setup(client: &Elasticsearch) -> Result<(), failure::Error> { + #s + } + }), + Some(quote!{ setup(&client).await?; })) + } else { + (None, None) + }; + let (teardown_fn, teardown_call) = if let Some(t) = &test.teardown { + (Some(quote!{ + async fn teardown(client: &Elasticsearch) -> Result<(), failure::Error> { + #t + } + }), Some(quote!{ teardown(&client).await?; })) + } else { + (None, None) + }; + + let tests: Vec = test.tests + .iter() + .map(|(name, steps)| { + let method_name = name + .replace(" ", "_") + .to_lowercase() + .to_snake_case(); + + let method_name_ident = syn::Ident::from(method_name); + quote! { + #[tokio::test] + async fn #method_name_ident() -> Result<(), failure::Error> { + let client = client::create(); + #setup_call + #steps + #teardown_call + Ok(()) + } + } + }) + .collect(); + let tokens = quote! { + #[cfg(test)] + pub mod tests { + use crate::client; + + #setup_fn + #teardown_fn + #(#tests)* + } + }; + + let generated = api_generator::generator::rust_fmt(tokens.to_string())?; + let mut file = OpenOptions::new().append(true).open(&path)?; + file.write_all(generated.as_bytes())?; + file.write_all(b"\n")?; + + Ok(()) +} fn read_steps(api: &Api, steps: &Array) -> Result { let mut tokens = Tokens::new(); @@ -163,6 +258,7 @@ fn read_steps(api: &Api, steps: &Array) -> Result { } fn read_match(api: &Api, hash: &Hash, tokens: &mut Tokens) -> Result<(), failure::Error> { + // TODO: implement Ok(()) } @@ -215,7 +311,7 @@ fn read_do(api: &Api, hash: &Hash, tokens: &mut Tokens) -> Result<(), failure::E _ => { let mut tokens = Tokens::new(); for (n, v) in params { - let param_ident = syn::Ident::from(n); + let param_ident = syn::Ident::from(api_generator::generator::code_gen::valid_name(n)); match v { Yaml::String(s) => tokens.append(quote! { @@ -276,7 +372,7 @@ fn endpoint_from_api_call<'a>(api: &'a Api, api_call: &str) -> Option<&'a ApiEnd 1 => match api.root.get(api_call_path[0]) { Some(endpoint) => Some(endpoint), None => { - println!("No ApiEndpoint found for {}. skipping", api_call_path[0]); + println!("No ApiEndpoint found for {}. skipping", &api_call); None } }, @@ -286,13 +382,13 @@ fn endpoint_from_api_call<'a>(api: &'a Api, api_call: &str) -> Option<&'a ApiEnd match namespace.get(api_call_path[1]) { Some(endpoint) => Some(endpoint), None => { - println!("No ApiEndpoint found for {:?}. skipping", &api_call_path); + println!("No ApiEndpoint found for {}. skipping", &api_call); None } } }, None => { - println!("No ApiEndpoint found for {:?}. skipping", &api_call_path); + println!("No ApiEndpoint found for {}. skipping", &api_call); None } } diff --git a/yaml_test_runner/src/main.rs b/yaml_test_runner/src/main.rs index 7ff90579..07048402 100644 --- a/yaml_test_runner/src/main.rs +++ b/yaml_test_runner/src/main.rs @@ -10,6 +10,11 @@ use std::fs; mod generator; mod github; +#[cfg(test)] +mod generated; + +pub mod client; + fn main() -> Result<(), failure::Error> { let matches = App::new(env!("CARGO_PKG_NAME")) .about(env!("CARGO_PKG_DESCRIPTION")) @@ -68,6 +73,10 @@ fn main() -> Result<(), failure::Error> { let api = api_generator::generator::read_api(branch, &rest_specs_dir)?; + // delete existing generated files first + if generated_dir.exists() { + fs::remove_dir_all(&generated_dir)?; + } generator::generate_tests_from_yaml(&api, &download_dir, &download_dir, &generated_dir)?; Ok(()) From 46215212a1ac0c1fdf9236f93e57839495689677 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Wed, 4 Mar 2020 08:14:21 +1000 Subject: [PATCH 008/127] Fix directory and module names --- yaml_test_runner/src/generator.rs | 19 +++++++++---------- yaml_test_runner/src/main.rs | 4 +++- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index ec33e36d..19235062 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -121,19 +121,18 @@ fn write_mod_files(generated_dir: &PathBuf) -> Result<(), failure::Error> { fn write_test(test: YamlTest, path: &PathBuf, base_download_dir: &PathBuf, generated_dir: &PathBuf) -> Result<(), failure::Error> { let path = { - let file: String = { - let yaml_file: String = path.to_string_lossy().into_owned() - .replace(base_download_dir.to_str().unwrap(), generated_dir.to_str().unwrap()); - yaml_file - //let path = fs::canonicalize(PathBuf::from(yaml_file))?; - //path.to_string_lossy().into_owned().replace(".", "_") - }; - - println!("{}", file); - let mut path = PathBuf::from(file); + let mut relative = path.strip_prefix(&base_download_dir)?.to_path_buf(); + relative.set_extension(""); + // directories and files will form the module names so ensure they're valid + let clean: String = relative.to_string_lossy().into_owned().replace(".", "_").replace("-", "_"); + relative = PathBuf::from(clean); + + let mut path = generated_dir.join(relative); path.set_extension("rs"); // modules can't start with a number path.set_file_name(format!("_{}", &path.file_name().unwrap().to_string_lossy().into_owned())); + + println!("{:?}", path); path }; diff --git a/yaml_test_runner/src/main.rs b/yaml_test_runner/src/main.rs index 07048402..8b02b911 100644 --- a/yaml_test_runner/src/main.rs +++ b/yaml_test_runner/src/main.rs @@ -1,7 +1,9 @@ #[macro_use] extern crate quote; - extern crate api_generator; +#[macro_use] +#[cfg(test)] +extern crate serde_json; use clap::{App, Arg}; use std::path::PathBuf; From a089ed86e6e8e8afafecc50ad144ed229a273984 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Wed, 4 Mar 2020 09:03:47 +1000 Subject: [PATCH 009/127] Handle param string arrays --- yaml_test_runner/src/generator.rs | 157 +++++++++++++++++++----------- 1 file changed, 98 insertions(+), 59 deletions(-) diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 19235062..aca2e962 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -25,7 +25,7 @@ impl YamlTest { } pub fn generate_tests_from_yaml(api: &Api, base_download_dir: &PathBuf, download_dir: &PathBuf, generated_dir: &PathBuf) -> Result<(), failure::Error> { - let paths = fs::read_dir(download_dir).unwrap(); + let paths = fs::read_dir(download_dir)?; for entry in paths { if let Ok(entry) = entry { if let Ok(file_type) = entry.file_type() { @@ -40,49 +40,90 @@ pub fn generate_tests_from_yaml(api: &Api, base_download_dir: &PathBuf, download } let yaml = fs::read_to_string(&entry.path()).unwrap(); - // a yaml test can contain multiple yaml docs let result = YamlLoader::load_from_str(&yaml); if result.is_err() { - println!("error reading {:?}: {}", &entry.path(), result.err().unwrap().to_string()); + println!("error reading {:?}: {}. skipping", &entry.path(), result.err().unwrap().to_string()); continue; } let docs = result.unwrap(); let mut test = YamlTest::new(docs.len()); - for doc in docs { - if let Some(hash) = doc.as_hash() { - - let (first_key, first_value) = hash.iter().next().unwrap(); - match (first_key, first_value) { - (Yaml::String(name), Yaml::Array(steps)) => { - let tokens = read_steps(api, steps)?; - match name.as_str() { - "setup" => test.setup = Some(tokens), - "teardown" => test.teardown = Some(tokens), - name => test.tests.push((name.to_owned(), tokens)), - }; - } - (k, v) => { - return Err(failure::err_msg(format!( - "Expected string key and array value in {:?}, but found {:?} and {:?}", - &entry.path(), - &k, - &v, - ))); + let results : Vec> = docs + .iter() + .map(|doc| { + if let Some(hash) = doc.as_hash() { + let (first_key, first_value) = hash.iter().next().unwrap(); + match (first_key, first_value) { + (Yaml::String(name), Yaml::Array(steps)) => { + let tokens = read_steps(api, steps)?; + match name.as_str() { + "setup" => test.setup = Some(tokens), + "teardown" => test.teardown = Some(tokens), + name => test.tests.push((name.to_owned(), tokens)), + }; + Ok(()) + } + (k, v) => { + Err(failure::err_msg(format!( + "expected string key and array value in {:?}, but found {:?} and {:?}", + &entry.path(), + &k, + &v, + ))) + } } + } else { + Err(failure::err_msg(format!( + "expected hash but found {:?}", + &doc + ))) } - } else { - return Err(failure::err_msg(format!( - "Expected hash but found {:?} in {:?}", - &doc, - &entry.path() - ))); + + + }) + .collect(); + + //if there has been an Err in any step of the file, don't writ + match ok_or_accumulate(results) { + Ok(_) => write_test_file(test, &entry.path(), base_download_dir, generated_dir)?, + Err(e) => println!("error(s) creating test file for {:?}\n{}", &entry.path(), e) } - } - write_test(test, &entry.path(), base_download_dir, generated_dir)?; + + + // for doc in docs { + // if let Some(hash) = doc.as_hash() { + // let (first_key, first_value) = hash.iter().next().unwrap(); + // match (first_key, first_value) { + // (Yaml::String(name), Yaml::Array(steps)) => { + // let tokens = read_steps(api, steps)?; + // match name.as_str() { + // "setup" => test.setup = Some(tokens), + // "teardown" => test.teardown = Some(tokens), + // name => test.tests.push((name.to_owned(), tokens)), + // }; + // } + // (k, v) => { + // return Err(failure::err_msg(format!( + // "Expected string key and array value in {:?}, but found {:?} and {:?}", + // &entry.path(), + // &k, + // &v, + // ))); + // } + // } + // } else { + // return Err(failure::err_msg(format!( + // "Expected hash but found {:?} in {:?}", + // &doc, + // &entry.path() + // ))); + // } + // } + + } } } @@ -119,7 +160,7 @@ fn write_mod_files(generated_dir: &PathBuf) -> Result<(), failure::Error> { Ok(()) } -fn write_test(test: YamlTest, path: &PathBuf, base_download_dir: &PathBuf, generated_dir: &PathBuf) -> Result<(), failure::Error> { +fn write_test_file(test: YamlTest, path: &PathBuf, base_download_dir: &PathBuf, generated_dir: &PathBuf) -> Result<(), failure::Error> { let path = { let mut relative = path.strip_prefix(&base_download_dir)?.to_path_buf(); relative.set_extension(""); @@ -131,8 +172,6 @@ fn write_test(test: YamlTest, path: &PathBuf, base_download_dir: &PathBuf, gener path.set_extension("rs"); // modules can't start with a number path.set_file_name(format!("_{}", &path.file_name().unwrap().to_string_lossy().into_owned())); - - println!("{:?}", path); path }; @@ -273,26 +312,18 @@ fn read_do(api: &Api, hash: &Hash, tokens: &mut Tokens) -> Result<(), failure::E "node_selector" => Ok(()), "warnings" => Ok(()), api_call => { - let c = v.as_hash(); if c.is_none() { - return Err(failure::err_msg(format!("Expected hash but found {:?}", v))); - } - - let endpoint = endpoint_from_api_call(api, &api_call); - - if endpoint.is_none() { - return Ok(()); + return Err(failure::err_msg(format!("expected hash value for {} but found {:?}", &api_call, v))); } - let endpoint = endpoint.unwrap(); + let endpoint = endpoint_from_api_call(api, &api_call)?; let mut parts: Vec<(&str, &Yaml)> = vec![]; let mut params: Vec<(&str, &Yaml)> = vec![]; let mut body_call: Option = None; for (k,v) in c.unwrap().iter() { let key = k.as_str().unwrap(); - if endpoint.params.contains_key(key) { params.push((key, v)); } else if key == "body" { @@ -322,6 +353,23 @@ fn read_do(api: &Api, hash: &Hash, tokens: &mut Tokens) -> Result<(), failure::E Yaml::Integer(i) => tokens.append(quote! { .#param_ident(#i) }), + Yaml::Array(arr) => { + // only support param string arrays for now + let result: Vec<_> = arr + .iter() + .map(|i| { + match i { + Yaml::String(s) => Ok(s), + y => Err(failure::err_msg(format!("Unsupported array value {:?}", y))) + } + }) + .filter_map(Result::ok) + .collect(); + + tokens.append(quote! { + .#param_ident(&[#(#result,)*]) + }); + } _ => println!("Unsupported value {:?}", v), } } @@ -365,31 +413,22 @@ fn create_body_call(v: &Yaml) -> Option { } } -fn endpoint_from_api_call<'a>(api: &'a Api, api_call: &str) -> Option<&'a ApiEndpoint> { +fn endpoint_from_api_call<'a>(api: &'a Api, api_call: &str) -> Result<&'a ApiEndpoint, failure::Error> { let api_call_path: Vec<&str> = api_call.split('.').collect(); match api_call_path.len() { 1 => match api.root.get(api_call_path[0]) { - Some(endpoint) => Some(endpoint), - None => { - println!("No ApiEndpoint found for {}. skipping", &api_call); - None - } + Some(endpoint) => Ok(endpoint), + None => Err(failure::err_msg(format!("No ApiEndpoint found for {}. skipping", &api_call))) }, _ => { match api.namespaces.get(api_call_path[0]) { Some(namespace) => { match namespace.get(api_call_path[1]) { - Some(endpoint) => Some(endpoint), - None => { - println!("No ApiEndpoint found for {}. skipping", &api_call); - None - } + Some(endpoint) => Ok(endpoint), + None => Err(failure::err_msg(format!("No ApiEndpoint found for {}. skipping", &api_call))) } }, - None => { - println!("No ApiEndpoint found for {}. skipping", &api_call); - None - } + None => Err(failure::err_msg(format!("No ApiEndpoint found for {}. skipping", &api_call))) } }, } From 2ea69b478784ac28b20773d4e79cbd1f2fcc6494 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Wed, 4 Mar 2020 12:37:08 +1000 Subject: [PATCH 010/127] start building parts variant WIP --- yaml_test_runner/Cargo.toml | 1 + yaml_test_runner/src/generator.rs | 181 ++++++++++++++++++++++-------- 2 files changed, 135 insertions(+), 47 deletions(-) diff --git a/yaml_test_runner/Cargo.toml b/yaml_test_runner/Cargo.toml index c8a458e2..e7fda6fb 100644 --- a/yaml_test_runner/Cargo.toml +++ b/yaml_test_runner/Cargo.toml @@ -12,6 +12,7 @@ api_generator = { version = "7.6.0-alpha.1", path = "../api_generator" } clap = "~2" failure = "0.1.6" +itertools = "0.8.2" Inflector = "0.11.4" quote = "~0.3" reqwest = "~0.9" diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index aca2e962..8b487d58 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -7,6 +7,8 @@ use yaml_rust::{yaml::{Array, Hash}, Yaml, YamlLoader, YamlEmitter}; use api_generator::generator::{Api, ApiEndpoint}; use std::fs::{File, OpenOptions}; use std::io::Write; +use itertools::Itertools; +use syn::parse::path; struct YamlTest { setup: Option, @@ -85,45 +87,11 @@ pub fn generate_tests_from_yaml(api: &Api, base_download_dir: &PathBuf, download }) .collect(); - //if there has been an Err in any step of the file, don't writ - match ok_or_accumulate(results) { - Ok(_) => write_test_file(test, &entry.path(), base_download_dir, generated_dir)?, - Err(e) => println!("error(s) creating test file for {:?}\n{}", &entry.path(), e) - } - - - - // for doc in docs { - // if let Some(hash) = doc.as_hash() { - // let (first_key, first_value) = hash.iter().next().unwrap(); - // match (first_key, first_value) { - // (Yaml::String(name), Yaml::Array(steps)) => { - // let tokens = read_steps(api, steps)?; - // match name.as_str() { - // "setup" => test.setup = Some(tokens), - // "teardown" => test.teardown = Some(tokens), - // name => test.tests.push((name.to_owned(), tokens)), - // }; - // } - // (k, v) => { - // return Err(failure::err_msg(format!( - // "Expected string key and array value in {:?}, but found {:?} and {:?}", - // &entry.path(), - // &k, - // &v, - // ))); - // } - // } - // } else { - // return Err(failure::err_msg(format!( - // "Expected hash but found {:?} in {:?}", - // &doc, - // &entry.path() - // ))); - // } - // } - - + //if there has been an Err in any step of the yaml test file, don't create a test for it + match ok_or_accumulate(results) { + Ok(_) => write_test_file(test, &entry.path(), base_download_dir, generated_dir)?, + Err(e) => println!("error(s) creating test file for {:?}\n{}", &entry.path(), e) + } } } } @@ -244,6 +212,7 @@ fn write_test_file(test: YamlTest, path: &PathBuf, base_download_dir: &PathBuf, let tokens = quote! { #[cfg(test)] pub mod tests { + use elasticsearch::*; use crate::client; #setup_fn @@ -307,10 +276,22 @@ fn read_do(api: &Api, hash: &Hash, tokens: &mut Tokens) -> Result<(), failure::E match k.as_str() { Some(key) => { match key { - "headers" => Ok(()), - "catch" => Ok(()), - "node_selector" => Ok(()), - "warnings" => Ok(()), + "headers" => { + // TODO: implement + Ok(()) + }, + "catch" => { + // TODO: implement + Ok(()) + }, + "node_selector" => { + // TODO: implement + Ok(()) + }, + "warnings" => { + // TODO: implement + Ok(()) + }, api_call => { let c = v.as_hash(); if c.is_none() { @@ -322,17 +303,25 @@ fn read_do(api: &Api, hash: &Hash, tokens: &mut Tokens) -> Result<(), failure::E let mut params: Vec<(&str, &Yaml)> = vec![]; let mut body_call: Option = None; + // TODO: use ignore + let mut ignore: Option = None; + for (k,v) in c.unwrap().iter() { let key = k.as_str().unwrap(); - if endpoint.params.contains_key(key) { + if endpoint.params.contains_key(key) || api.common_params.contains_key(key) { params.push((key, v)); } else if key == "body" { body_call = create_body_call(v); - } else { + } else if key == "ignore" { + ignore = Some(v.as_i64().unwrap()); + } + else { parts.push((key, v)); } } + let parts_variant = parts_variant(api_call, &parts, endpoint)?; + let fn_name = api_call.replace(".", "()."); let fn_name_ident = syn::Ident::from(fn_name); @@ -354,7 +343,7 @@ fn read_do(api: &Api, hash: &Hash, tokens: &mut Tokens) -> Result<(), failure::E .#param_ident(#i) }), Yaml::Array(arr) => { - // only support param string arrays for now + // only support param string arrays let result: Vec<_> = arr .iter() .map(|i| { @@ -379,9 +368,10 @@ fn read_do(api: &Api, hash: &Hash, tokens: &mut Tokens) -> Result<(), failure::E }; tokens.append(quote! { - let response = client.#fn_name_ident() + let response = client.#fn_name_ident(#parts_variant) #params_calls #body_call + .send() .await?; }); Ok(()) @@ -396,6 +386,103 @@ fn read_do(api: &Api, hash: &Hash, tokens: &mut Tokens) -> Result<(), failure::E ok_or_accumulate(results) } +fn parts_variant(api_call: &str, parts: &Vec<(&str, &Yaml)>, endpoint: &ApiEndpoint) -> Result, failure::Error> { + // TODO: ideally, this should share the logic from EnumBuilder + let enum_name = { + let name = api_call.to_pascal_case().replace(".", ""); + syn::Ident::from(format!("{}Parts", name)) + }; + + if parts.is_empty() { + return match endpoint.url.paths.len() { + 1 => Ok(None), + _ => Ok(Some(quote!(#enum_name::None))) + } + } + + let path_parts = match endpoint.url.paths.len() { + 1 => Some(endpoint.url.paths[0].path.params()), + _ => { + let paths: Vec> = endpoint.url.paths + .iter() + .map(|p| p.path.params()) + .collect(); + + // get the matching path parts + let matching_path_parts = paths + .into_iter() + .filter(|p| { + if p.len() != parts.len() { + return false; + } + + let contains = parts.iter().filter_map(|i| { + if p.contains(&i.0) { + Some(()) + } else { + None + } + }).collect::>(); + contains.len() == parts.len() + }) + .collect::>(); + + match matching_path_parts.len() { + 0 => None, + _ => Some(matching_path_parts[0].clone()) + } + } + }; + + if path_parts.is_none() { + return Err(failure::err_msg(format!("No path_parts for {} with parts {:?}", &api_call, parts))); + } + + let path_parts = path_parts.unwrap(); + let variant_name = { + let j = path_parts + .iter() + .map(|k| k.to_pascal_case()) + .collect::>() + .join(""); + syn::Ident::from(j) + }; + + let part_tokens = parts + .clone() + .into_iter() + .sorted_by(|(p, v), (p2, v2)| { + let f = path_parts.iter().position(|x| x == p).unwrap(); + let s = path_parts.iter().position(|x| x == p2).unwrap(); + f.cmp(&s) + }) + .map(|(p,v)| { + match v { + Yaml::String(s) => quote! { #s }, + Yaml::Boolean(b) => quote! { #b }, + Yaml::Integer(i) => quote! { #i }, + Yaml::Array(arr) => { + // only support param string arrays + let result: Vec<_> = arr + .iter() + .map(|i| { + match i { + Yaml::String(s) => Ok(s), + y => Err(failure::err_msg(format!("Unsupported array value {:?}", y))) + } + }) + .filter_map(Result::ok) + .collect(); + quote! { &[#(#result,)*] } + }, + _ => panic!(format!("Unsupported value {:?}", v)), + } + }) + .collect::>(); + + Ok(Some(quote!{ #enum_name::#variant_name(#(#part_tokens,)*) })) +} + fn create_body_call(v: &Yaml) -> Option { match v { Yaml::String(s) => Some(quote!(.body(json!(#s)))), From 559682f2076835ead8e8c80cf858336904c5079b Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Wed, 4 Mar 2020 14:28:43 +1000 Subject: [PATCH 011/127] Refactor components into ApiCall run cargo fmt indent accumulated errors --- api_generator/src/bin/run.rs | 10 +- api_generator/src/generator/mod.rs | 15 +- api_generator/src/lib.rs | 4 +- yaml_test_runner/src/client.rs | 10 +- yaml_test_runner/src/generator.rs | 316 ++++++++++++++++++----------- yaml_test_runner/src/github.rs | 3 +- yaml_test_runner/src/main.rs | 7 +- 7 files changed, 216 insertions(+), 149 deletions(-) diff --git a/api_generator/src/bin/run.rs b/api_generator/src/bin/run.rs index bc64f48f..db4a9f66 100644 --- a/api_generator/src/bin/run.rs +++ b/api_generator/src/bin/run.rs @@ -1,10 +1,7 @@ -extern crate dialoguer; extern crate api_generator; +extern crate dialoguer; -use api_generator::{ - generator, - rest_spec -}; +use api_generator::{generator, rest_spec}; use dialoguer::Input; use std::path::PathBuf; use std::{ @@ -16,7 +13,8 @@ fn main() -> Result<(), failure::Error> { // This must be run from the src root directory, with cargo run -p api_generator let download_dir = fs::canonicalize(PathBuf::from("./api_generator/rest_specs"))?; let generated_dir = fs::canonicalize(PathBuf::from("./elasticsearch/src/generated"))?; - let last_downloaded_version = PathBuf::from("./api_generator/rest_specs/last_downloaded_version"); + let last_downloaded_version = + PathBuf::from("./api_generator/rest_specs/last_downloaded_version"); let mut download_specs = false; let mut answer = String::new(); diff --git a/api_generator/src/generator/mod.rs b/api_generator/src/generator/mod.rs index e1108cbf..27d28545 100644 --- a/api_generator/src/generator/mod.rs +++ b/api_generator/src/generator/mod.rs @@ -148,8 +148,7 @@ pub struct Body { } lazy_static! { - static ref VERSION: Version = - semver::Version::parse(env!("CARGO_PKG_VERSION")).unwrap(); + static ref VERSION: Version = semver::Version::parse(env!("CARGO_PKG_VERSION")).unwrap(); } /// Wraps the URL string to replace master or current in URL path with the @@ -183,11 +182,7 @@ impl DocumentationUrlString { u.path() .replace( "/master", - format!( - "/{}.{}", - VERSION.major, VERSION.minor - ) - .as_str(), + format!("/{}.{}", VERSION.major, VERSION.minor).as_str(), ) .as_str(), ); @@ -196,11 +191,7 @@ impl DocumentationUrlString { u.path() .replace( "/current", - format!( - "/{}.{}", - VERSION.major, VERSION.minor - ) - .as_str(), + format!("/{}.{}", VERSION.major, VERSION.minor).as_str(), ) .as_str(), ); diff --git a/api_generator/src/lib.rs b/api_generator/src/lib.rs index be78fe12..f2634bda 100644 --- a/api_generator/src/lib.rs +++ b/api_generator/src/lib.rs @@ -4,6 +4,6 @@ extern crate lazy_static; #[macro_use] extern crate quote; -pub mod generator; pub mod error; -pub mod rest_spec; \ No newline at end of file +pub mod generator; +pub mod rest_spec; diff --git a/yaml_test_runner/src/client.rs b/yaml_test_runner/src/client.rs index 828536e5..b863ed53 100644 --- a/yaml_test_runner/src/client.rs +++ b/yaml_test_runner/src/client.rs @@ -1,13 +1,8 @@ use elasticsearch::{ auth::Credentials, - http::{ - transport::{ - TransportBuilder, - SingleNodeConnectionPool - } - }, + cert::CertificateValidation, + http::transport::{SingleNodeConnectionPool, TransportBuilder}, Elasticsearch, DEFAULT_ADDRESS, - cert::CertificateValidation }; use sysinfo::SystemExt; use url::Url; @@ -43,4 +38,3 @@ pub fn create() -> Elasticsearch { let transport = builder.build().unwrap(); Elasticsearch::new(transport) } - diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 8b487d58..cb95353c 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -1,14 +1,17 @@ use inflector::Inflector; use quote::Tokens; -use std::fs; -use std::path::PathBuf; -use yaml_rust::{yaml::{Array, Hash}, Yaml, YamlLoader, YamlEmitter}; use api_generator::generator::{Api, ApiEndpoint}; +use itertools::Itertools; +use std::fs; use std::fs::{File, OpenOptions}; use std::io::Write; -use itertools::Itertools; +use std::path::PathBuf; use syn::parse::path; +use yaml_rust::{ + yaml::{Array, Hash}, + Yaml, YamlEmitter, YamlLoader, +}; struct YamlTest { setup: Option, @@ -26,7 +29,20 @@ impl YamlTest { } } -pub fn generate_tests_from_yaml(api: &Api, base_download_dir: &PathBuf, download_dir: &PathBuf, generated_dir: &PathBuf) -> Result<(), failure::Error> { +/// The components of an API call +struct ApiCall<'a> { + parts: Vec<(&'a str, &'a Yaml)>, + params: Vec<(&'a str, &'a Yaml)>, + body_call: Option, + ignore: Option +} + +pub fn generate_tests_from_yaml( + api: &Api, + base_download_dir: &PathBuf, + download_dir: &PathBuf, + generated_dir: &PathBuf, +) -> Result<(), failure::Error> { let paths = fs::read_dir(download_dir)?; for entry in paths { if let Ok(entry) = entry { @@ -45,7 +61,11 @@ pub fn generate_tests_from_yaml(api: &Api, base_download_dir: &PathBuf, download // a yaml test can contain multiple yaml docs let result = YamlLoader::load_from_str(&yaml); if result.is_err() { - println!("error reading {:?}: {}. skipping", &entry.path(), result.err().unwrap().to_string()); + println!( + "error reading {:?}: {}. skipping", + &entry.path(), + result.err().unwrap().to_string() + ); continue; } @@ -88,9 +108,13 @@ pub fn generate_tests_from_yaml(api: &Api, base_download_dir: &PathBuf, download .collect(); //if there has been an Err in any step of the yaml test file, don't create a test for it - match ok_or_accumulate(results) { - Ok(_) => write_test_file(test, &entry.path(), base_download_dir, generated_dir)?, - Err(e) => println!("error(s) creating test file for {:?}\n{}", &entry.path(), e) + match ok_or_accumulate(results, 1) { + Ok(_) => { + write_test_file(test, &entry.path(), base_download_dir, generated_dir)? + } + Err(e) => { + println!("error(s) creating test file for {:?}\n{}", &entry.path(), e) + } } } } @@ -111,7 +135,10 @@ fn write_mod_files(generated_dir: &PathBuf) -> Result<(), failure::Error> { let path = entry.path(); let name = path.file_stem().unwrap().to_string_lossy(); if name.into_owned() != "mod" { - mods.push(format!("pub mod {};", path.file_stem().unwrap().to_string_lossy())); + mods.push(format!( + "pub mod {};", + path.file_stem().unwrap().to_string_lossy() + )); } if file_type.is_dir() { @@ -128,18 +155,30 @@ fn write_mod_files(generated_dir: &PathBuf) -> Result<(), failure::Error> { Ok(()) } -fn write_test_file(test: YamlTest, path: &PathBuf, base_download_dir: &PathBuf, generated_dir: &PathBuf) -> Result<(), failure::Error> { +fn write_test_file( + test: YamlTest, + path: &PathBuf, + base_download_dir: &PathBuf, + generated_dir: &PathBuf, +) -> Result<(), failure::Error> { let path = { let mut relative = path.strip_prefix(&base_download_dir)?.to_path_buf(); relative.set_extension(""); // directories and files will form the module names so ensure they're valid - let clean: String = relative.to_string_lossy().into_owned().replace(".", "_").replace("-", "_"); + let clean: String = relative + .to_string_lossy() + .into_owned() + .replace(".", "_") + .replace("-", "_"); relative = PathBuf::from(clean); let mut path = generated_dir.join(relative); path.set_extension("rs"); // modules can't start with a number - path.set_file_name(format!("_{}", &path.file_name().unwrap().to_string_lossy().into_owned())); + path.set_file_name(format!( + "_{}", + &path.file_name().unwrap().to_string_lossy().into_owned() + )); path }; @@ -163,37 +202,40 @@ fn write_test_file(test: YamlTest, path: &PathBuf, base_download_dir: &PathBuf, // // ----------------------------------------------- " - .as_bytes(), + .as_bytes(), )?; let (setup_fn, setup_call) = if let Some(s) = &test.setup { - (Some(quote!{ - async fn setup(client: &Elasticsearch) -> Result<(), failure::Error> { - #s - } - }), - Some(quote!{ setup(&client).await?; })) + ( + Some(quote! { + async fn setup(client: &Elasticsearch) -> Result<(), failure::Error> { + #s + } + }), + Some(quote! { setup(&client).await?; }), + ) } else { (None, None) }; let (teardown_fn, teardown_call) = if let Some(t) = &test.teardown { - (Some(quote!{ - async fn teardown(client: &Elasticsearch) -> Result<(), failure::Error> { - #t - } - }), Some(quote!{ teardown(&client).await?; })) + ( + Some(quote! { + async fn teardown(client: &Elasticsearch) -> Result<(), failure::Error> { + #t + } + }), + Some(quote! { teardown(&client).await?; }), + ) } else { (None, None) }; - let tests: Vec = test.tests + let tests: Vec = test + .tests .iter() .map(|(name, steps)| { - let method_name = name - .replace(" ", "_") - .to_lowercase() - .to_snake_case(); + let method_name = name.replace(" ", "_").to_lowercase().to_snake_case(); let method_name_ident = syn::Ident::from(method_name); quote! { @@ -239,7 +281,7 @@ fn read_steps(api: &Api, steps: &Array) -> Result { match (key, v) { ("skip", Yaml::Hash(h)) => {} - ("do", Yaml::Hash(h)) => read_do(api,h, &mut tokens)?, + ("do", Yaml::Hash(h)) => read_do(api, h, &mut tokens)?, ("set", Yaml::Hash(h)) => {} ("transform_and_set", Yaml::Hash(h)) => {} ("match", Yaml::Hash(h)) => read_match(api, h, &mut tokens)?, @@ -279,58 +321,41 @@ fn read_do(api: &Api, hash: &Hash, tokens: &mut Tokens) -> Result<(), failure::E "headers" => { // TODO: implement Ok(()) - }, + } "catch" => { // TODO: implement Ok(()) - }, + } "node_selector" => { // TODO: implement Ok(()) - }, + } "warnings" => { // TODO: implement Ok(()) - }, + } api_call => { - let c = v.as_hash(); - if c.is_none() { - return Err(failure::err_msg(format!("expected hash value for {} but found {:?}", &api_call, v))); + let hash = v.as_hash(); + if hash.is_none() { + return Err(failure::err_msg(format!( + "expected hash value for {} but found {:?}", + &api_call, v + ))); } let endpoint = endpoint_from_api_call(api, &api_call)?; - let mut parts: Vec<(&str, &Yaml)> = vec![]; - let mut params: Vec<(&str, &Yaml)> = vec![]; - let mut body_call: Option = None; - - // TODO: use ignore - let mut ignore: Option = None; - - for (k,v) in c.unwrap().iter() { - let key = k.as_str().unwrap(); - if endpoint.params.contains_key(key) || api.common_params.contains_key(key) { - params.push((key, v)); - } else if key == "body" { - body_call = create_body_call(v); - } else if key == "ignore" { - ignore = Some(v.as_i64().unwrap()); - } - else { - parts.push((key, v)); - } - } - - let parts_variant = parts_variant(api_call, &parts, endpoint)?; + let components = api_call_components(api, endpoint, hash.unwrap()); - let fn_name = api_call.replace(".", "()."); - let fn_name_ident = syn::Ident::from(fn_name); + let parts_variant = parts_variant(api_call, &components.parts, endpoint)?; - let params_calls = match params.len() { + let params_calls = match &components.params.len() { 0 => None, _ => { let mut tokens = Tokens::new(); - for (n, v) in params { - let param_ident = syn::Ident::from(api_generator::generator::code_gen::valid_name(n)); + for (n, v) in &components.params { + let param_ident = syn::Ident::from( + api_generator::generator::code_gen::valid_name(n), + ); match v { Yaml::String(s) => tokens.append(quote! { @@ -346,11 +371,12 @@ fn read_do(api: &Api, hash: &Hash, tokens: &mut Tokens) -> Result<(), failure::E // only support param string arrays let result: Vec<_> = arr .iter() - .map(|i| { - match i { - Yaml::String(s) => Ok(s), - y => Err(failure::err_msg(format!("Unsupported array value {:?}", y))) - } + .map(|i| match i { + Yaml::String(s) => Ok(s), + y => Err(failure::err_msg(format!( + "Unsupported array value {:?}", + y + ))), }) .filter_map(Result::ok) .collect(); @@ -367,46 +393,85 @@ fn read_do(api: &Api, hash: &Hash, tokens: &mut Tokens) -> Result<(), failure::E } }; + let fn_call = syn::Ident::from(api_call.replace(".", "().")); + let body_call = components.body_call; + tokens.append(quote! { - let response = client.#fn_name_ident(#parts_variant) + let response = client.#fn_call(#parts_variant) #params_calls #body_call .send() .await?; }); Ok(()) - }, + } } - }, - None => Err(failure::err_msg(format!("expected string key but found {:?}", k))) + } + None => Err(failure::err_msg(format!( + "expected string key but found {:?}", + k + ))), } }) .collect(); - ok_or_accumulate(results) + ok_or_accumulate(results, 0) } -fn parts_variant(api_call: &str, parts: &Vec<(&str, &Yaml)>, endpoint: &ApiEndpoint) -> Result, failure::Error> { +fn api_call_components<'a>(api: &'a Api, endpoint: &'a ApiEndpoint, hash: &'a Hash) -> ApiCall<'a> { + let mut parts: Vec<(&str, &Yaml)> = vec![]; + let mut params: Vec<(&str, &Yaml)> = vec![]; + let mut body_call: Option = None; + // TODO: use ignore value in the test + let mut ignore: Option = None; + + for (k, v) in hash.iter() { + let key = k.as_str().unwrap(); + if endpoint.params.contains_key(key) + || api.common_params.contains_key(key) + { + params.push((key, v)); + } else if key == "body" { + body_call = create_body_call(v); + } else if key == "ignore" { + ignore = Some(v.as_i64().unwrap()); + } else { + parts.push((key, v)); + } + } + + ApiCall { + parts, + params, + body_call, + ignore + } +} + +fn parts_variant( + api_call: &str, + parts: &Vec<(&str, &Yaml)>, + endpoint: &ApiEndpoint, +) -> Result, failure::Error> { // TODO: ideally, this should share the logic from EnumBuilder let enum_name = { let name = api_call.to_pascal_case().replace(".", ""); syn::Ident::from(format!("{}Parts", name)) }; + // Enum variants containing no URL parts where there is only a single API URL are not + // required to be passed in the API if parts.is_empty() { return match endpoint.url.paths.len() { 1 => Ok(None), - _ => Ok(Some(quote!(#enum_name::None))) - } + _ => Ok(Some(quote!(#enum_name::None))), + }; } let path_parts = match endpoint.url.paths.len() { 1 => Some(endpoint.url.paths[0].path.params()), _ => { - let paths: Vec> = endpoint.url.paths - .iter() - .map(|p| p.path.params()) - .collect(); + let paths: Vec> = endpoint.url.paths.iter().map(|p| p.path.params()).collect(); // get the matching path parts let matching_path_parts = paths @@ -416,47 +481,48 @@ fn parts_variant(api_call: &str, parts: &Vec<(&str, &Yaml)>, endpoint: &ApiEndpo return false; } - let contains = parts.iter().filter_map(|i| { - if p.contains(&i.0) { - Some(()) - } else { - None - } - }).collect::>(); + let contains = parts + .iter() + .filter_map(|i| if p.contains(&i.0) { Some(()) } else { None }) + .collect::>(); contains.len() == parts.len() }) .collect::>(); match matching_path_parts.len() { 0 => None, - _ => Some(matching_path_parts[0].clone()) + _ => Some(matching_path_parts[0].clone()), } } }; if path_parts.is_none() { - return Err(failure::err_msg(format!("No path_parts for {} with parts {:?}", &api_call, parts))); + return Err(failure::err_msg(format!( + "No path_parts for {} with parts {:?}", + &api_call, parts + ))); } let path_parts = path_parts.unwrap(); let variant_name = { - let j = path_parts + let v = path_parts .iter() .map(|k| k.to_pascal_case()) .collect::>() .join(""); - syn::Ident::from(j) + syn::Ident::from(v) }; let part_tokens = parts .clone() .into_iter() + // don't rely on URL parts being ordered in the yaml test .sorted_by(|(p, v), (p2, v2)| { let f = path_parts.iter().position(|x| x == p).unwrap(); let s = path_parts.iter().position(|x| x == p2).unwrap(); f.cmp(&s) }) - .map(|(p,v)| { + .map(|(p, v)| { match v { Yaml::String(s) => quote! { #s }, Yaml::Boolean(b) => quote! { #b }, @@ -465,24 +531,29 @@ fn parts_variant(api_call: &str, parts: &Vec<(&str, &Yaml)>, endpoint: &ApiEndpo // only support param string arrays let result: Vec<_> = arr .iter() - .map(|i| { - match i { - Yaml::String(s) => Ok(s), - y => Err(failure::err_msg(format!("Unsupported array value {:?}", y))) - } + .map(|i| match i { + Yaml::String(s) => Ok(s), + y => Err(failure::err_msg(format!("Unsupported array value {:?}", y))), }) .filter_map(Result::ok) .collect(); quote! { &[#(#result,)*] } - }, + } _ => panic!(format!("Unsupported value {:?}", v)), } }) .collect::>(); - Ok(Some(quote!{ #enum_name::#variant_name(#(#part_tokens,)*) })) + Ok(Some( + quote! { #enum_name::#variant_name(#(#part_tokens,)*) }, + )) } +/// Creates the body function call from a YAML value. +/// +/// When reading a body from the YAML test, it'll be converted to a Yaml variant, +/// usually a Hash. To get the JSON representation back requires converting +/// back to JSON fn create_body_call(v: &Yaml) -> Option { match v { Yaml::String(s) => Some(quote!(.body(json!(#s)))), @@ -500,30 +571,41 @@ fn create_body_call(v: &Yaml) -> Option { } } -fn endpoint_from_api_call<'a>(api: &'a Api, api_call: &str) -> Result<&'a ApiEndpoint, failure::Error> { +/// Find the right ApiEndpoint from the REST API specs for the API call +/// defined in the YAML test. +/// +/// The REST API specs model only the stable APIs +/// currently, so no endpoint will be found for experimental or beta APIs +fn endpoint_from_api_call<'a>( + api: &'a Api, + api_call: &str, +) -> Result<&'a ApiEndpoint, failure::Error> { let api_call_path: Vec<&str> = api_call.split('.').collect(); match api_call_path.len() { 1 => match api.root.get(api_call_path[0]) { Some(endpoint) => Ok(endpoint), - None => Err(failure::err_msg(format!("No ApiEndpoint found for {}. skipping", &api_call))) + None => Err(failure::err_msg(format!( + "No ApiEndpoint found for {}. skipping", + &api_call + ))), }, - _ => { - match api.namespaces.get(api_call_path[0]) { - Some(namespace) => { - match namespace.get(api_call_path[1]) { - Some(endpoint) => Ok(endpoint), - None => Err(failure::err_msg(format!("No ApiEndpoint found for {}. skipping", &api_call))) - } - }, - None => Err(failure::err_msg(format!("No ApiEndpoint found for {}. skipping", &api_call))) - } + _ => match api.namespaces.get(api_call_path[0]) { + Some(namespace) => match namespace.get(api_call_path[1]) { + Some(endpoint) => Ok(endpoint), + None => Err(failure::err_msg(format!( + "No ApiEndpoint found for {}. skipping", + &api_call + ))), + }, + None => Err(failure::err_msg(format!( + "No ApiEndpoint found for {}. skipping", + &api_call + ))), }, } } -fn ok_or_accumulate( - results: Vec>, -) -> Result<(), failure::Error> { +fn ok_or_accumulate(results: Vec>, indent: usize) -> Result<(), failure::Error> { let errs = results .into_iter() .filter_map(|r| r.err()) @@ -533,7 +615,7 @@ fn ok_or_accumulate( } else { let msg = errs .iter() - .map(|e| e.to_string()) + .map(|e| format!("{}{}", "\t".to_string().repeat(indent), e.to_string())) .collect::>() .join("\n"); diff --git a/yaml_test_runner/src/github.rs b/yaml_test_runner/src/github.rs index 5d479968..ad71d054 100644 --- a/yaml_test_runner/src/github.rs +++ b/yaml_test_runner/src/github.rs @@ -1,3 +1,4 @@ +use io::Write; use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION}; use serde::Deserialize; use std::error::Error as StdError; @@ -5,7 +6,6 @@ use std::fmt::Formatter; use std::fs::File; use std::path::PathBuf; use std::{fs, io}; -use io::Write; struct YamlTestSuite { dir: String, @@ -39,7 +39,6 @@ struct GitHubContent { /// Downloads the yaml tests if not already downloaded pub fn download_test_suites(token: &str, branch: &str, download_dir: &PathBuf) { - let mut last_downloaded_version = download_dir.clone(); last_downloaded_version.push("last_downloaded_version"); if last_downloaded_version.exists() { diff --git a/yaml_test_runner/src/main.rs b/yaml_test_runner/src/main.rs index 8b02b911..5a8e150d 100644 --- a/yaml_test_runner/src/main.rs +++ b/yaml_test_runner/src/main.rs @@ -6,8 +6,8 @@ extern crate api_generator; extern crate serde_json; use clap::{App, Arg}; -use std::path::PathBuf; use std::fs; +use std::path::PathBuf; mod generator; mod github; @@ -64,7 +64,10 @@ fn main() -> Result<(), failure::Error> { .expect("Could not rest specs last_downloaded version into string"); if version == branch { - println!("rest specs for branch {} already downloaded in {:?}", branch, &rest_specs_dir); + println!( + "rest specs for branch {} already downloaded in {:?}", + branch, &rest_specs_dir + ); download_rest_specs = false; } } From ee2a4d24f77f72242154a424e0f8e6c490e683de Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Wed, 4 Mar 2020 14:52:11 +1000 Subject: [PATCH 012/127] Handle ignore number or array --- yaml_test_runner/src/generator.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index cb95353c..bd74383d 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -432,9 +432,12 @@ fn api_call_components<'a>(api: &'a Api, endpoint: &'a ApiEndpoint, hash: &'a Ha { params.push((key, v)); } else if key == "body" { - body_call = create_body_call(v); + body_call = create_body_call(endpoint, v); } else if key == "ignore" { - ignore = Some(v.as_i64().unwrap()); + ignore = match v.as_i64() { + Some(i) => Some(i), + None => v.as_vec().unwrap()[0].as_i64() + } } else { parts.push((key, v)); } From 1f8656586ea1d4fc5014f4086ece0a8e3c97c6f8 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Wed, 4 Mar 2020 16:26:26 +1000 Subject: [PATCH 013/127] Propagate unsupported values --- yaml_test_runner/src/generator.rs | 90 ++++++++++++++++++++++--------- 1 file changed, 65 insertions(+), 25 deletions(-) diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index bd74383d..da2127dc 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -62,7 +62,7 @@ pub fn generate_tests_from_yaml( let result = YamlLoader::load_from_str(&yaml); if result.is_err() { println!( - "error reading {:?}: {}. skipping", + "Error reading {:?}. skipping:\n\t{}", &entry.path(), result.err().unwrap().to_string() ); @@ -108,12 +108,12 @@ pub fn generate_tests_from_yaml( .collect(); //if there has been an Err in any step of the yaml test file, don't create a test for it - match ok_or_accumulate(results, 1) { + match ok_or_accumulate(&results, 1) { Ok(_) => { write_test_file(test, &entry.path(), base_download_dir, generated_dir)? } Err(e) => { - println!("error(s) creating test file for {:?}\n{}", &entry.path(), e) + println!("Error creating test file for {:?}:\n{}", &entry.path(), e) } } } @@ -415,7 +415,7 @@ fn read_do(api: &Api, hash: &Hash, tokens: &mut Tokens) -> Result<(), failure::E }) .collect(); - ok_or_accumulate(results, 0) + ok_or_accumulate(&results, 0) } fn api_call_components<'a>(api: &'a Api, endpoint: &'a ApiEndpoint, hash: &'a Hash) -> ApiCall<'a> { @@ -462,8 +462,8 @@ fn parts_variant( syn::Ident::from(format!("{}Parts", name)) }; - // Enum variants containing no URL parts where there is only a single API URL are not - // required to be passed in the API + // Enum variants containing no URL parts where there is only a single API URL, + // are not required to be passed in the API if parts.is_empty() { return match endpoint.url.paths.len() { 1 => Ok(None), @@ -501,7 +501,7 @@ fn parts_variant( if path_parts.is_none() { return Err(failure::err_msg(format!( - "No path_parts for {} with parts {:?}", + "No path_parts for '{}' API with parts {:?}", &api_call, parts ))); } @@ -516,7 +516,7 @@ fn parts_variant( syn::Ident::from(v) }; - let part_tokens = parts + let part_tokens: Vec> = parts .clone() .into_iter() // don't rely on URL parts being ordered in the yaml test @@ -527,9 +527,9 @@ fn parts_variant( }) .map(|(p, v)| { match v { - Yaml::String(s) => quote! { #s }, - Yaml::Boolean(b) => quote! { #b }, - Yaml::Integer(i) => quote! { #i }, + Yaml::String(s) => Ok(quote! { #s }), + Yaml::Boolean(b) => Ok(quote! { #b }), + Yaml::Integer(i) => Ok(quote! { #i }), Yaml::Array(arr) => { // only support param string arrays let result: Vec<_> = arr @@ -538,18 +538,36 @@ fn parts_variant( Yaml::String(s) => Ok(s), y => Err(failure::err_msg(format!("Unsupported array value {:?}", y))), }) - .filter_map(Result::ok) .collect(); - quote! { &[#(#result,)*] } + + match ok_or_accumulate(&result, 0) { + Ok(_) => { + let result: Vec<_> = result + .into_iter() + .filter_map(Result::ok) + .collect(); + Ok(quote! { &[#(#result,)*] }) + }, + Err(e) => Err(failure::err_msg(e)) + } } - _ => panic!(format!("Unsupported value {:?}", v)), + _ => Err(failure::err_msg(format!("Unsupported value {:?}", v))), } }) - .collect::>(); + .collect(); - Ok(Some( - quote! { #enum_name::#variant_name(#(#part_tokens,)*) }, - )) + match ok_or_accumulate(&part_tokens, 0) { + Ok(_) => { + let part_tokens: Vec = part_tokens + .into_iter() + .filter_map(Result::ok) + .collect(); + Ok(Some( + quote! { #enum_name::#variant_name(#(#part_tokens,)*) }, + )) + }, + Err(e) => Err(failure::err_msg(e)) + } } /// Creates the body function call from a YAML value. @@ -557,7 +575,7 @@ fn parts_variant( /// When reading a body from the YAML test, it'll be converted to a Yaml variant, /// usually a Hash. To get the JSON representation back requires converting /// back to JSON -fn create_body_call(v: &Yaml) -> Option { +fn create_body_call(endpoint: &ApiEndpoint, v: &Yaml) -> Option { match v { Yaml::String(s) => Some(quote!(.body(json!(#s)))), _ => { @@ -566,10 +584,32 @@ fn create_body_call(v: &Yaml) -> Option { let mut emitter = YamlEmitter::new(&mut s); emitter.dump(v).unwrap(); } - let v: serde_json::Value = serde_yaml::from_str(&s).unwrap(); - let json = serde_json::to_string(&v).unwrap(); - let ident = syn::Ident::from(json); - Some(quote!(.body(json!(#ident)))) + + let accepts_nd_body = match &endpoint.body { + Some(b) => match &b.serialize { + Some(s) => s == "bulk", + _ => false, + }, + None => false, + }; + + if accepts_nd_body { + let values: Vec = serde_yaml::from_str(&s).unwrap(); + let json: Vec = values + .iter() + .map(|value| { + let json = serde_json::to_string(&value).unwrap(); + let ident = syn::Ident::from(json); + quote!(json!(#ident)) + }) + .collect(); + Some(quote!(.body(vec![ #(#json),* ]))) + } else { + let value: serde_json::Value = serde_yaml::from_str(&s).unwrap(); + let json = serde_json::to_string(&value).unwrap(); + let ident = syn::Ident::from(json); + Some(quote!(.body(json!(#ident)))) + } } } } @@ -608,10 +648,10 @@ fn endpoint_from_api_call<'a>( } } -fn ok_or_accumulate(results: Vec>, indent: usize) -> Result<(), failure::Error> { +fn ok_or_accumulate(results: &[Result], indent: usize) -> Result<(), failure::Error> { let errs = results .into_iter() - .filter_map(|r| r.err()) + .filter_map(|r| r.as_ref().err()) .collect::>(); if errs.is_empty() { Ok(()) From e3d5a4092af32ef6b754e691b5ce699064b0e642 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Wed, 4 Mar 2020 17:04:14 +1000 Subject: [PATCH 014/127] Handle array parts --- yaml_test_runner/src/generator.rs | 44 ++++++++++++++++++------------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index da2127dc..05ef64b8 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -1,7 +1,7 @@ use inflector::Inflector; use quote::Tokens; -use api_generator::generator::{Api, ApiEndpoint}; +use api_generator::generator::{Api, ApiEndpoint, TypeKind}; use itertools::Itertools; use std::fs; use std::fs::{File, OpenOptions}; @@ -113,7 +113,7 @@ pub fn generate_tests_from_yaml( write_test_file(test, &entry.path(), base_download_dir, generated_dir)? } Err(e) => { - println!("Error creating test file for {:?}:\n{}", &entry.path(), e) + println!("Error creating test file for {:?}. skipping:\n{}", &entry.path(), e) } } } @@ -346,8 +346,10 @@ fn read_do(api: &Api, hash: &Hash, tokens: &mut Tokens) -> Result<(), failure::E let endpoint = endpoint_from_api_call(api, &api_call)?; let components = api_call_components(api, endpoint, hash.unwrap()); + // TODO: move into components construction let parts_variant = parts_variant(api_call, &components.parts, endpoint)?; + // TODO: move into components construction let params_calls = match &components.params.len() { 0 => None, _ => { @@ -471,15 +473,14 @@ fn parts_variant( }; } - let path_parts = match endpoint.url.paths.len() { - 1 => Some(endpoint.url.paths[0].path.params()), + let path = match endpoint.url.paths.len() { + 1 => Some(&endpoint.url.paths[0]), _ => { - let paths: Vec> = endpoint.url.paths.iter().map(|p| p.path.params()).collect(); - // get the matching path parts - let matching_path_parts = paths - .into_iter() - .filter(|p| { + let matching_path_parts = endpoint.url.paths + .iter() + .filter(|path| { + let p = path.path.params(); if p.len() != parts.len() { return false; } @@ -494,19 +495,20 @@ fn parts_variant( match matching_path_parts.len() { 0 => None, - _ => Some(matching_path_parts[0].clone()), + _ => Some(matching_path_parts[0]), } } }; - if path_parts.is_none() { + if path.is_none() { return Err(failure::err_msg(format!( - "No path_parts for '{}' API with parts {:?}", + "No path for '{}' API with URL parts {:?}", &api_call, parts ))); } - let path_parts = path_parts.unwrap(); + let path = path.unwrap(); + let path_parts = path.path.params(); let variant_name = { let v = path_parts .iter() @@ -520,14 +522,18 @@ fn parts_variant( .clone() .into_iter() // don't rely on URL parts being ordered in the yaml test - .sorted_by(|(p, v), (p2, v2)| { + .sorted_by(|(p, _), (p2, _)| { let f = path_parts.iter().position(|x| x == p).unwrap(); let s = path_parts.iter().position(|x| x == p2).unwrap(); f.cmp(&s) }) .map(|(p, v)| { + let ty = path.parts.get(p).unwrap(); match v { - Yaml::String(s) => Ok(quote! { #s }), + Yaml::String(s) => match ty.ty { + TypeKind::List => Ok(quote! { &[#s] }), + _ => Ok(quote! { #s }) + }, Yaml::Boolean(b) => Ok(quote! { #b }), Yaml::Integer(i) => Ok(quote! { #i }), Yaml::Array(arr) => { @@ -628,7 +634,7 @@ fn endpoint_from_api_call<'a>( 1 => match api.root.get(api_call_path[0]) { Some(endpoint) => Ok(endpoint), None => Err(failure::err_msg(format!( - "No ApiEndpoint found for {}. skipping", + "No ApiEndpoint found for {}", &api_call ))), }, @@ -636,18 +642,20 @@ fn endpoint_from_api_call<'a>( Some(namespace) => match namespace.get(api_call_path[1]) { Some(endpoint) => Ok(endpoint), None => Err(failure::err_msg(format!( - "No ApiEndpoint found for {}. skipping", + "No ApiEndpoint found for {}", &api_call ))), }, None => Err(failure::err_msg(format!( - "No ApiEndpoint found for {}. skipping", + "No ApiEndpoint found for {}", &api_call ))), }, } } +/// Checks whether there are any Errs in the collection, and accumulates them into one +/// error message if there are. fn ok_or_accumulate(results: &[Result], indent: usize) -> Result<(), failure::Error> { let errs = results .into_iter() From 2c33d603c283560367e9a2e935dee589663cb5a2 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Wed, 4 Mar 2020 21:12:17 +1000 Subject: [PATCH 015/127] Handle enum bool params --- elasticsearch/src/http/request.rs | 2 +- yaml_test_runner/src/generator.rs | 114 +++++++++++++++++++----------- 2 files changed, 75 insertions(+), 41 deletions(-) diff --git a/elasticsearch/src/http/request.rs b/elasticsearch/src/http/request.rs index 2e667034..602bb5e7 100644 --- a/elasticsearch/src/http/request.rs +++ b/elasticsearch/src/http/request.rs @@ -11,7 +11,7 @@ use serde::Serialize; /// expect JSON, however, there are some APIs that expect newline-delimited JSON (NDJSON). /// The [Body] trait allows modelling different API body implementations. pub trait Body { - /// An existing immutable buffer that can be used to avoid writing + /// An existing immutable buffer that can be used to avoid /// having to write to another buffer that will then be written to the request stream. /// /// If this method returns `Some`, the bytes must be the same as diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 05ef64b8..e89f6ba6 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -1,7 +1,7 @@ use inflector::Inflector; use quote::Tokens; -use api_generator::generator::{Api, ApiEndpoint, TypeKind}; +use api_generator::generator::{Api, ApiEndpoint, TypeKind, ApiEnum}; use itertools::Itertools; use std::fs; use std::fs::{File, OpenOptions}; @@ -34,7 +34,7 @@ struct ApiCall<'a> { parts: Vec<(&'a str, &'a Yaml)>, params: Vec<(&'a str, &'a Yaml)>, body_call: Option, - ignore: Option + ignore: Option, } pub fn generate_tests_from_yaml( @@ -112,9 +112,11 @@ pub fn generate_tests_from_yaml( Ok(_) => { write_test_file(test, &entry.path(), base_download_dir, generated_dir)? } - Err(e) => { - println!("Error creating test file for {:?}. skipping:\n{}", &entry.path(), e) - } + Err(e) => println!( + "Error creating test file for {:?}. skipping:\n{}", + &entry.path(), + e + ), } } } @@ -255,6 +257,7 @@ fn write_test_file( #[cfg(test)] pub mod tests { use elasticsearch::*; + use elasticsearch::params::*; use crate::client; #setup_fn @@ -347,7 +350,8 @@ fn read_do(api: &Api, hash: &Hash, tokens: &mut Tokens) -> Result<(), failure::E let components = api_call_components(api, endpoint, hash.unwrap()); // TODO: move into components construction - let parts_variant = parts_variant(api_call, &components.parts, endpoint)?; + let parts_variant = + parts_variant(api_call, &components.parts, endpoint)?; // TODO: move into components construction let params_calls = match &components.params.len() { @@ -359,13 +363,35 @@ fn read_do(api: &Api, hash: &Hash, tokens: &mut Tokens) -> Result<(), failure::E api_generator::generator::code_gen::valid_name(n), ); + let ty = match endpoint.params.get(*n) { + Some(t) => Ok(t), + None => match api.common_params.get(*n) { + Some(t) => Ok(t), + None => Err(failure::err_msg(format!("No param found for {}", n))) + } + }?; + + let kind = ty.ty; + match v { - Yaml::String(s) => tokens.append(quote! { + Yaml::String(s) => + tokens.append(quote! { .#param_ident(#s) }), - Yaml::Boolean(b) => tokens.append(quote! { - .#param_ident(#b) - }), + Yaml::Boolean(b) => { + match kind { + TypeKind::Enum => { + let enum_name = syn::Ident::from(n.to_pascal_case()); + let variant = syn::Ident::from(b.to_string().to_pascal_case()); + tokens.append(quote! { + .#param_ident(#enum_name::#variant) + }) + } + _ => tokens.append(quote! { + .#param_ident(#b) + }), + } + }, Yaml::Integer(i) => tokens.append(quote! { .#param_ident(#i) }), @@ -429,16 +455,14 @@ fn api_call_components<'a>(api: &'a Api, endpoint: &'a ApiEndpoint, hash: &'a Ha for (k, v) in hash.iter() { let key = k.as_str().unwrap(); - if endpoint.params.contains_key(key) - || api.common_params.contains_key(key) - { + if endpoint.params.contains_key(key) || api.common_params.contains_key(key) { params.push((key, v)); } else if key == "body" { body_call = create_body_call(endpoint, v); } else if key == "ignore" { ignore = match v.as_i64() { Some(i) => Some(i), - None => v.as_vec().unwrap()[0].as_i64() + None => v.as_vec().unwrap()[0].as_i64(), } } else { parts.push((key, v)); @@ -449,7 +473,7 @@ fn api_call_components<'a>(api: &'a Api, endpoint: &'a ApiEndpoint, hash: &'a Ha parts, params, body_call, - ignore + ignore, } } @@ -477,7 +501,9 @@ fn parts_variant( 1 => Some(&endpoint.url.paths[0]), _ => { // get the matching path parts - let matching_path_parts = endpoint.url.paths + let matching_path_parts = endpoint + .url + .paths .iter() .filter(|path| { let p = path.path.params(); @@ -528,11 +554,15 @@ fn parts_variant( f.cmp(&s) }) .map(|(p, v)| { - let ty = path.parts.get(p).unwrap(); + let ty = match path.parts.get(p) { + Some(t) => Ok(t), + None => Err(failure::err_msg(format!("No URL part found for {} in {}", p, &path.path))) + }?; + match v { Yaml::String(s) => match ty.ty { TypeKind::List => Ok(quote! { &[#s] }), - _ => Ok(quote! { #s }) + _ => Ok(quote! { #s }), }, Yaml::Boolean(b) => Ok(quote! { #b }), Yaml::Integer(i) => Ok(quote! { #i }), @@ -548,13 +578,11 @@ fn parts_variant( match ok_or_accumulate(&result, 0) { Ok(_) => { - let result: Vec<_> = result - .into_iter() - .filter_map(Result::ok) - .collect(); + let result: Vec<_> = + result.into_iter().filter_map(Result::ok).collect(); Ok(quote! { &[#(#result,)*] }) - }, - Err(e) => Err(failure::err_msg(e)) + } + Err(e) => Err(failure::err_msg(e)), } } _ => Err(failure::err_msg(format!("Unsupported value {:?}", v))), @@ -564,15 +592,12 @@ fn parts_variant( match ok_or_accumulate(&part_tokens, 0) { Ok(_) => { - let part_tokens: Vec = part_tokens - .into_iter() - .filter_map(Result::ok) - .collect(); + let part_tokens: Vec = part_tokens.into_iter().filter_map(Result::ok).collect(); Ok(Some( quote! { #enum_name::#variant_name(#(#part_tokens,)*) }, )) - }, - Err(e) => Err(failure::err_msg(e)) + } + Err(e) => Err(failure::err_msg(e)), } } @@ -582,8 +607,22 @@ fn parts_variant( /// usually a Hash. To get the JSON representation back requires converting /// back to JSON fn create_body_call(endpoint: &ApiEndpoint, v: &Yaml) -> Option { + let accepts_nd_body = match &endpoint.body { + Some(b) => match &b.serialize { + Some(s) => s == "bulk", + _ => false, + }, + None => false, + }; + match v { - Yaml::String(s) => Some(quote!(.body(json!(#s)))), + Yaml::String(s) => { + if accepts_nd_body { + Some(quote!(.body(vec![#s]))) + } else { + Some(quote!(.body(#s))) + } + }, _ => { let mut s = String::new(); { @@ -591,14 +630,6 @@ fn create_body_call(endpoint: &ApiEndpoint, v: &Yaml) -> Option { emitter.dump(v).unwrap(); } - let accepts_nd_body = match &endpoint.body { - Some(b) => match &b.serialize { - Some(s) => s == "bulk", - _ => false, - }, - None => false, - }; - if accepts_nd_body { let values: Vec = serde_yaml::from_str(&s).unwrap(); let json: Vec = values @@ -656,7 +687,10 @@ fn endpoint_from_api_call<'a>( /// Checks whether there are any Errs in the collection, and accumulates them into one /// error message if there are. -fn ok_or_accumulate(results: &[Result], indent: usize) -> Result<(), failure::Error> { +fn ok_or_accumulate( + results: &[Result], + indent: usize, +) -> Result<(), failure::Error> { let errs = results .into_iter() .filter_map(|r| r.as_ref().err()) From d56c228dbc5908f7e8bba72dbdc6feaac34d12a9 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 5 Mar 2020 06:58:29 +1000 Subject: [PATCH 016/127] Add reqired client modules to generated tests --- yaml_test_runner/src/generator.rs | 36 +++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index e89f6ba6..e82fb1b3 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -12,8 +12,10 @@ use yaml_rust::{ yaml::{Array, Hash}, Yaml, YamlEmitter, YamlLoader, }; +use std::collections::HashSet; struct YamlTest { + namespaces: HashSet, setup: Option, teardown: Option, tests: Vec<(String, Tokens)>, @@ -22,6 +24,7 @@ struct YamlTest { impl YamlTest { pub fn new(len: usize) -> Self { Self { + namespaces: HashSet::with_capacity(len), setup: None, teardown: None, tests: Vec::with_capacity(len), @@ -31,6 +34,7 @@ impl YamlTest { /// The components of an API call struct ApiCall<'a> { + namespace: Option<&'a str>, parts: Vec<(&'a str, &'a Yaml)>, params: Vec<(&'a str, &'a Yaml)>, body_call: Option, @@ -79,7 +83,7 @@ pub fn generate_tests_from_yaml( let (first_key, first_value) = hash.iter().next().unwrap(); match (first_key, first_value) { (Yaml::String(name), Yaml::Array(steps)) => { - let tokens = read_steps(api, steps)?; + let tokens = read_steps(api, &mut test, steps)?; match name.as_str() { "setup" => test.setup = Some(tokens), "teardown" => test.teardown = Some(tokens), @@ -253,11 +257,20 @@ fn write_test_file( }) .collect(); + let namespaces: Vec = test.namespaces + .iter() + .map(|n| { + let ident = syn::Ident::from(n.as_str()); + quote!(use elasticsearch::#ident::*;) + }) + .collect(); + let tokens = quote! { #[cfg(test)] pub mod tests { use elasticsearch::*; use elasticsearch::params::*; + #(#namespaces)* use crate::client; #setup_fn @@ -274,7 +287,7 @@ fn write_test_file( Ok(()) } -fn read_steps(api: &Api, steps: &Array) -> Result { +fn read_steps(api: &Api, test: &mut YamlTest, steps: &Array) -> Result { let mut tokens = Tokens::new(); for step in steps { if let Some(hash) = step.as_hash() { @@ -284,7 +297,7 @@ fn read_steps(api: &Api, steps: &Array) -> Result { match (key, v) { ("skip", Yaml::Hash(h)) => {} - ("do", Yaml::Hash(h)) => read_do(api, h, &mut tokens)?, + ("do", Yaml::Hash(h)) => read_do(api, test, h, &mut tokens)?, ("set", Yaml::Hash(h)) => {} ("transform_and_set", Yaml::Hash(h)) => {} ("match", Yaml::Hash(h)) => read_match(api, h, &mut tokens)?, @@ -314,7 +327,7 @@ fn read_match(api: &Api, hash: &Hash, tokens: &mut Tokens) -> Result<(), failure Ok(()) } -fn read_do(api: &Api, hash: &Hash, tokens: &mut Tokens) -> Result<(), failure::Error> { +fn read_do(api: &Api, test: &mut YamlTest, hash: &Hash, tokens: &mut Tokens) -> Result<(), failure::Error> { let results: Vec> = hash .iter() .map(|(k, v)| { @@ -347,7 +360,11 @@ fn read_do(api: &Api, hash: &Hash, tokens: &mut Tokens) -> Result<(), failure::E } let endpoint = endpoint_from_api_call(api, &api_call)?; - let components = api_call_components(api, endpoint, hash.unwrap()); + let components = api_call_components(api, endpoint, api_call, hash.unwrap()); + + if let Some(n) = components.namespace { + test.namespaces.insert(n.to_owned()); + } // TODO: move into components construction let parts_variant = @@ -446,7 +463,7 @@ fn read_do(api: &Api, hash: &Hash, tokens: &mut Tokens) -> Result<(), failure::E ok_or_accumulate(&results, 0) } -fn api_call_components<'a>(api: &'a Api, endpoint: &'a ApiEndpoint, hash: &'a Hash) -> ApiCall<'a> { +fn api_call_components<'a>(api: &'a Api, endpoint: &'a ApiEndpoint, api_call: &'a str, hash: &'a Hash) -> ApiCall<'a> { let mut parts: Vec<(&str, &Yaml)> = vec![]; let mut params: Vec<(&str, &Yaml)> = vec![]; let mut body_call: Option = None; @@ -469,7 +486,14 @@ fn api_call_components<'a>(api: &'a Api, endpoint: &'a ApiEndpoint, hash: &'a Ha } } + let namespace: Option<&str> = if api_call.contains(".") { + Some(api_call.splitn(2, ".").collect::>()[0]) + } else { + None + }; + ApiCall { + namespace, parts, params, body_call, From f009b9eea2c7d114c24382a039c2fdbfefe5b5ef Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 5 Mar 2020 10:38:36 +1000 Subject: [PATCH 017/127] Handle string params enums and lists --- yaml_test_runner/src/generator.rs | 42 +++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index e82fb1b3..aba62aab 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -391,10 +391,39 @@ fn read_do(api: &Api, test: &mut YamlTest, hash: &Hash, tokens: &mut Tokens) -> let kind = ty.ty; match v { - Yaml::String(s) => - tokens.append(quote! { - .#param_ident(#s) - }), + Yaml::String(s) => { + match kind { + TypeKind::Enum => { + let e: String = n.to_pascal_case(); + let enum_name = syn::Ident::from(e.as_str()); + let variant = if s.is_empty() { + // TODO: Should we simply omit empty Refresh tests? + if e == "Refresh" { + syn::Ident::from("True") + } else if e == "Size" { + syn::Ident::from("Unspecified") + } else { + panic!(format!("Unhandled empty value for {}", &e)); + } + } else { + syn::Ident::from(s.to_pascal_case().replace("_", "")) + }; + + tokens.append(quote! { + .#param_ident(#enum_name::#variant) + }) + }, + TypeKind::List => { + let values: Vec<&str> = s.split(',').collect(); + tokens.append(quote! { + .#param_ident(&[#(#values),*]) + }) + }, + _ => tokens.append(quote! { + .#param_ident(#s) + }), + } + }, Yaml::Boolean(b) => { match kind { TypeKind::Enum => { @@ -585,7 +614,10 @@ fn parts_variant( match v { Yaml::String(s) => match ty.ty { - TypeKind::List => Ok(quote! { &[#s] }), + TypeKind::List => { + let values: Vec<&str> = s.split(',').collect(); + Ok(quote! { &[#(#values),*] }) + }, _ => Ok(quote! { #s }), }, Yaml::Boolean(b) => Ok(quote! { #b }), From 70a73c8932b6583acd1bf293a7e69e5f95e635af Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 5 Mar 2020 10:38:59 +1000 Subject: [PATCH 018/127] Refactor setup/teardown generation into function --- yaml_test_runner/src/generator.rs | 50 ++++++++++++++----------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index aba62aab..975361de 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -170,7 +170,7 @@ fn write_test_file( let path = { let mut relative = path.strip_prefix(&base_download_dir)?.to_path_buf(); relative.set_extension(""); - // directories and files will form the module names so ensure they're valid + // directories and files will form the module names so ensure they're valid module names let clean: String = relative .to_string_lossy() .into_owned() @@ -180,7 +180,7 @@ fn write_test_file( let mut path = generated_dir.join(relative); path.set_extension("rs"); - // modules can't start with a number + // modules can't start with a number so prefix with underscore path.set_file_name(format!( "_{}", &path.file_name().unwrap().to_string_lossy().into_owned() @@ -211,31 +211,10 @@ fn write_test_file( .as_bytes(), )?; - let (setup_fn, setup_call) = if let Some(s) = &test.setup { - ( - Some(quote! { - async fn setup(client: &Elasticsearch) -> Result<(), failure::Error> { - #s - } - }), - Some(quote! { setup(&client).await?; }), - ) - } else { - (None, None) - }; - - let (teardown_fn, teardown_call) = if let Some(t) = &test.teardown { - ( - Some(quote! { - async fn teardown(client: &Elasticsearch) -> Result<(), failure::Error> { - #t - } - }), - Some(quote! { teardown(&client).await?; }), - ) - } else { - (None, None) - }; + let (setup_fn, setup_call) = + generate_fixture("setup", &test.setup); + let (teardown_fn, teardown_call) = + generate_fixture("teardown", &test.teardown); let tests: Vec = test .tests @@ -287,6 +266,23 @@ fn write_test_file( Ok(()) } +fn generate_fixture(name: &str, tokens: &Option) -> (Option, Option) { + if let Some(t) = tokens { + let ident = syn::Ident::from(name); + ( + Some(quote! { + async fn #ident(client: &Elasticsearch) -> Result<(), failure::Error> { + #t + Ok(()) + } + }), + Some(quote! { #ident(&client).await?; }), + ) + } else { + (None, None) + } +} + fn read_steps(api: &Api, test: &mut YamlTest, steps: &Array) -> Result { let mut tokens = Tokens::new(); for step in steps { From 37502635e6623021e973559328d0dd5d26550ace Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 5 Mar 2020 11:28:35 +1000 Subject: [PATCH 019/127] Deduplicate generated method names --- yaml_test_runner/Cargo.toml | 2 ++ yaml_test_runner/src/generator.rs | 27 +++++++++++++++++++++++---- yaml_test_runner/src/main.rs | 2 ++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/yaml_test_runner/Cargo.toml b/yaml_test_runner/Cargo.toml index e7fda6fb..56ad8cf8 100644 --- a/yaml_test_runner/Cargo.toml +++ b/yaml_test_runner/Cargo.toml @@ -14,7 +14,9 @@ clap = "~2" failure = "0.1.6" itertools = "0.8.2" Inflector = "0.11.4" +lazy_static = "1.4.0" quote = "~0.3" +regex = "1.3.1" reqwest = "~0.9" serde = "~1" serde_yaml = "0.8.11" diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 975361de..bc2753a5 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -3,6 +3,7 @@ use quote::Tokens; use api_generator::generator::{Api, ApiEndpoint, TypeKind, ApiEnum}; use itertools::Itertools; +use regex::Regex; use std::fs; use std::fs::{File, OpenOptions}; use std::io::Write; @@ -216,16 +217,34 @@ fn write_test_file( let (teardown_fn, teardown_call) = generate_fixture("teardown", &test.teardown); + let mut method_names = HashSet::new(); + let tests: Vec = test .tests .iter() .map(|(name, steps)| { - let method_name = name.replace(" ", "_").to_lowercase().to_snake_case(); - - let method_name_ident = syn::Ident::from(method_name); + let method_name = { + let mut method_name = name.replace(" ", "_").to_lowercase().to_snake_case(); + + // some method descriptions are the same in YAML tests, which would result in + // duplicate generated test function names. Deduplicate by appending incrementing number + while !method_names.insert(method_name.clone()) { + lazy_static! { + static ref ENDING_DIGITS_REGEX: Regex = Regex::new(r"^(.*?)_(\d*?)$").unwrap(); + } + if let Some(c) = ENDING_DIGITS_REGEX.captures(&method_name) { + let name = c.get(1).unwrap().as_str(); + let n = c.get(2).unwrap().as_str().parse::().unwrap(); + method_name = format!("{}_{}", name, n + 1); + } else { + method_name = format!("{}_2", method_name); + } + } + syn::Ident::from(method_name) + }; quote! { #[tokio::test] - async fn #method_name_ident() -> Result<(), failure::Error> { + async fn #method_name() -> Result<(), failure::Error> { let client = client::create(); #setup_call #steps diff --git a/yaml_test_runner/src/main.rs b/yaml_test_runner/src/main.rs index 5a8e150d..783e9606 100644 --- a/yaml_test_runner/src/main.rs +++ b/yaml_test_runner/src/main.rs @@ -1,4 +1,6 @@ #[macro_use] +extern crate lazy_static; +#[macro_use] extern crate quote; extern crate api_generator; #[macro_use] From 9807b2964843c8640089581c855924dae672eb7a Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 5 Mar 2020 12:21:07 +1000 Subject: [PATCH 020/127] to_string API Url parts of bool and integer --- yaml_test_runner/src/generator.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index bc2753a5..bdcebd37 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -635,8 +635,14 @@ fn parts_variant( }, _ => Ok(quote! { #s }), }, - Yaml::Boolean(b) => Ok(quote! { #b }), - Yaml::Integer(i) => Ok(quote! { #i }), + Yaml::Boolean(b) => { + let s = b.to_string(); + Ok(quote! { #s }) + }, + Yaml::Integer(i) => { + let s = i.to_string(); + Ok(quote! { #s }) + }, Yaml::Array(arr) => { // only support param string arrays let result: Vec<_> = arr @@ -651,7 +657,7 @@ fn parts_variant( Ok(_) => { let result: Vec<_> = result.into_iter().filter_map(Result::ok).collect(); - Ok(quote! { &[#(#result,)*] }) + Ok(quote! { &[#(#result),*] }) } Err(e) => Err(failure::err_msg(e)), } @@ -665,7 +671,7 @@ fn parts_variant( Ok(_) => { let part_tokens: Vec = part_tokens.into_iter().filter_map(Result::ok).collect(); Ok(Some( - quote! { #enum_name::#variant_name(#(#part_tokens,)*) }, + quote! { #enum_name::#variant_name(#(#part_tokens),*) }, )) } Err(e) => Err(failure::err_msg(e)), From a65b3bd82748c10832eccb2ba4dfd4c631216b6f Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 5 Mar 2020 12:53:43 +1000 Subject: [PATCH 021/127] Refactor api call components --- yaml_test_runner/src/generator.rs | 253 +++++++++++++++--------------- 1 file changed, 129 insertions(+), 124 deletions(-) diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index bdcebd37..387eb1ca 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -36,9 +36,10 @@ impl YamlTest { /// The components of an API call struct ApiCall<'a> { namespace: Option<&'a str>, - parts: Vec<(&'a str, &'a Yaml)>, - params: Vec<(&'a str, &'a Yaml)>, - body_call: Option, + function: syn::Ident, + parts: Option, + params: Option, + body: Option, ignore: Option, } @@ -375,120 +376,23 @@ fn read_do(api: &Api, test: &mut YamlTest, hash: &Hash, tokens: &mut Tokens) -> } let endpoint = endpoint_from_api_call(api, &api_call)?; - let components = api_call_components(api, endpoint, api_call, hash.unwrap()); - - if let Some(n) = components.namespace { + let ApiCall { + namespace, + function, + parts, + params, + body, + ignore } = api_call_components(api, endpoint, api_call, hash.unwrap())?; + + // capture any namespaces used in the test + if let Some(n) = namespace { test.namespaces.insert(n.to_owned()); } - // TODO: move into components construction - let parts_variant = - parts_variant(api_call, &components.parts, endpoint)?; - - // TODO: move into components construction - let params_calls = match &components.params.len() { - 0 => None, - _ => { - let mut tokens = Tokens::new(); - for (n, v) in &components.params { - let param_ident = syn::Ident::from( - api_generator::generator::code_gen::valid_name(n), - ); - - let ty = match endpoint.params.get(*n) { - Some(t) => Ok(t), - None => match api.common_params.get(*n) { - Some(t) => Ok(t), - None => Err(failure::err_msg(format!("No param found for {}", n))) - } - }?; - - let kind = ty.ty; - - match v { - Yaml::String(s) => { - match kind { - TypeKind::Enum => { - let e: String = n.to_pascal_case(); - let enum_name = syn::Ident::from(e.as_str()); - let variant = if s.is_empty() { - // TODO: Should we simply omit empty Refresh tests? - if e == "Refresh" { - syn::Ident::from("True") - } else if e == "Size" { - syn::Ident::from("Unspecified") - } else { - panic!(format!("Unhandled empty value for {}", &e)); - } - } else { - syn::Ident::from(s.to_pascal_case().replace("_", "")) - }; - - tokens.append(quote! { - .#param_ident(#enum_name::#variant) - }) - }, - TypeKind::List => { - let values: Vec<&str> = s.split(',').collect(); - tokens.append(quote! { - .#param_ident(&[#(#values),*]) - }) - }, - _ => tokens.append(quote! { - .#param_ident(#s) - }), - } - }, - Yaml::Boolean(b) => { - match kind { - TypeKind::Enum => { - let enum_name = syn::Ident::from(n.to_pascal_case()); - let variant = syn::Ident::from(b.to_string().to_pascal_case()); - tokens.append(quote! { - .#param_ident(#enum_name::#variant) - }) - } - _ => tokens.append(quote! { - .#param_ident(#b) - }), - } - }, - Yaml::Integer(i) => tokens.append(quote! { - .#param_ident(#i) - }), - Yaml::Array(arr) => { - // only support param string arrays - let result: Vec<_> = arr - .iter() - .map(|i| match i { - Yaml::String(s) => Ok(s), - y => Err(failure::err_msg(format!( - "Unsupported array value {:?}", - y - ))), - }) - .filter_map(Result::ok) - .collect(); - - tokens.append(quote! { - .#param_ident(&[#(#result,)*]) - }); - } - _ => println!("Unsupported value {:?}", v), - } - } - - Some(tokens) - } - }; - - let fn_call = syn::Ident::from(api_call.replace(".", "().")); - let body_call = components.body_call; - tokens.append(quote! { - let response = client.#fn_call(#parts_variant) - #params_calls - #body_call + let response = client.#function(#parts) + #params + #body .send() .await?; }); @@ -507,11 +411,108 @@ fn read_do(api: &Api, test: &mut YamlTest, hash: &Hash, tokens: &mut Tokens) -> ok_or_accumulate(&results, 0) } -fn api_call_components<'a>(api: &'a Api, endpoint: &'a ApiEndpoint, api_call: &'a str, hash: &'a Hash) -> ApiCall<'a> { +fn generated_params(api: &Api, endpoint: &ApiEndpoint, params: &[(&str, &Yaml)]) -> Result, failure::Error> { + match params.len() { + 0 => Ok(None), + _ => { + let mut tokens = Tokens::new(); + for (n, v) in params { + let param_ident = syn::Ident::from( + api_generator::generator::code_gen::valid_name(n), + ); + + let ty = match endpoint.params.get(*n) { + Some(t) => Ok(t), + None => match api.common_params.get(*n) { + Some(t) => Ok(t), + None => Err(failure::err_msg(format!("No param found for {}", n))) + } + }?; + + let kind = ty.ty; + + match v { + Yaml::String(s) => { + match kind { + TypeKind::Enum => { + let e: String = n.to_pascal_case(); + let enum_name = syn::Ident::from(e.as_str()); + let variant = if s.is_empty() { + // TODO: Should we simply omit empty Refresh tests? + if e == "Refresh" { + syn::Ident::from("True") + } else if e == "Size" { + syn::Ident::from("Unspecified") + } else { + panic!(format!("Unhandled empty value for {}", &e)); + } + } else { + syn::Ident::from(s.to_pascal_case().replace("_", "")) + }; + + tokens.append(quote! { + .#param_ident(#enum_name::#variant) + }) + }, + TypeKind::List => { + let values: Vec<&str> = s.split(',').collect(); + tokens.append(quote! { + .#param_ident(&[#(#values),*]) + }) + }, + _ => tokens.append(quote! { + .#param_ident(#s) + }), + } + }, + Yaml::Boolean(b) => { + match kind { + TypeKind::Enum => { + let enum_name = syn::Ident::from(n.to_pascal_case()); + let variant = syn::Ident::from(b.to_string().to_pascal_case()); + tokens.append(quote! { + .#param_ident(#enum_name::#variant) + }) + } + _ => tokens.append(quote! { + .#param_ident(#b) + }), + } + }, + Yaml::Integer(i) => tokens.append(quote! { + .#param_ident(#i) + }), + Yaml::Array(arr) => { + // only support param string arrays + let result: Vec<_> = arr + .iter() + .map(|i| match i { + Yaml::String(s) => Ok(s), + y => Err(failure::err_msg(format!( + "Unsupported array value {:?}", + y + ))), + }) + .filter_map(Result::ok) + .collect(); + + tokens.append(quote! { + .#param_ident(&[#(#result),*]) + }); + } + _ => println!("Unsupported value {:?}", v), + } + } + + Ok(Some(tokens)) + } + } +} + +fn api_call_components<'a>(api: &'a Api, endpoint: &'a ApiEndpoint, api_call: &'a str, hash: &'a Hash) -> Result, failure::Error> { let mut parts: Vec<(&str, &Yaml)> = vec![]; let mut params: Vec<(&str, &Yaml)> = vec![]; - let mut body_call: Option = None; - // TODO: use ignore value in the test + let mut body: Option = None; let mut ignore: Option = None; for (k, v) in hash.iter() { @@ -519,7 +520,7 @@ fn api_call_components<'a>(api: &'a Api, endpoint: &'a ApiEndpoint, api_call: &' if endpoint.params.contains_key(key) || api.common_params.contains_key(key) { params.push((key, v)); } else if key == "body" { - body_call = create_body_call(endpoint, v); + body = generate_body(endpoint, v); } else if key == "ignore" { ignore = match v.as_i64() { Some(i) => Some(i), @@ -530,25 +531,29 @@ fn api_call_components<'a>(api: &'a Api, endpoint: &'a ApiEndpoint, api_call: &' } } + let parts = generate_parts(api_call, endpoint, &parts)?; + let params = generated_params(api, endpoint, ¶ms)?; + let function = syn::Ident::from(api_call.replace(".", "().")); let namespace: Option<&str> = if api_call.contains(".") { Some(api_call.splitn(2, ".").collect::>()[0]) } else { None }; - ApiCall { + Ok(ApiCall { namespace, + function, parts, params, - body_call, + body, ignore, - } + }) } -fn parts_variant( +fn generate_parts( api_call: &str, - parts: &Vec<(&str, &Yaml)>, endpoint: &ApiEndpoint, + parts: &[(&str, &Yaml)], ) -> Result, failure::Error> { // TODO: ideally, this should share the logic from EnumBuilder let enum_name = { @@ -622,7 +627,7 @@ fn parts_variant( f.cmp(&s) }) .map(|(p, v)| { - let ty = match path.parts.get(p) { + let ty = match path.parts.get(*p) { Some(t) => Ok(t), None => Err(failure::err_msg(format!("No URL part found for {} in {}", p, &path.path))) }?; @@ -683,7 +688,7 @@ fn parts_variant( /// When reading a body from the YAML test, it'll be converted to a Yaml variant, /// usually a Hash. To get the JSON representation back requires converting /// back to JSON -fn create_body_call(endpoint: &ApiEndpoint, v: &Yaml) -> Option { +fn generate_body(endpoint: &ApiEndpoint, v: &Yaml) -> Option { let accepts_nd_body = match &endpoint.body { Some(b) => match &b.serialize { Some(s) => s == "bulk", From cf7dd111a85dbb3a32499ea8c9838955dcf3f2fb Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 5 Mar 2020 17:52:38 +1000 Subject: [PATCH 022/127] move endpoint lookup onto Api impl --- api_generator/src/generator/mod.rs | 18 ++++++++++++ yaml_test_runner/src/generator.rs | 45 ++++++------------------------ 2 files changed, 27 insertions(+), 36 deletions(-) diff --git a/api_generator/src/generator/mod.rs b/api_generator/src/generator/mod.rs index 27d28545..bf1c5f71 100644 --- a/api_generator/src/generator/mod.rs +++ b/api_generator/src/generator/mod.rs @@ -34,6 +34,24 @@ pub struct Api { pub enums: Vec, } +impl Api { + /// Find the right ApiEndpoint from the REST API specs for the API call + /// defined in the YAML test. + /// + /// The REST API specs model only the stable APIs + /// currently, so no endpoint will be found for experimental or beta APIs + pub fn endpoint_for_api_call<'a>(&self, api_call: &str) -> Option<&ApiEndpoint> { + let api_call_path: Vec<&str> = api_call.split('.').collect(); + match api_call_path.len() { + 1 => self.root.get(api_call_path[0]), + _ => match self.namespaces.get(api_call_path[0]) { + Some(namespace) => namespace.get(api_call_path[1]), + None => None, + }, + } + } +} + /// A HTTP method in the REST API spec #[derive(Debug, Eq, PartialEq, Deserialize, Clone, Copy, Ord, PartialOrd)] pub enum HttpMethod { diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 387eb1ca..0d8e40df 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -375,14 +375,21 @@ fn read_do(api: &Api, test: &mut YamlTest, hash: &Hash, tokens: &mut Tokens) -> ))); } - let endpoint = endpoint_from_api_call(api, &api_call)?; + let endpoint = match api.endpoint_for_api_call(api_call) { + Some(e) => Ok(e), + None => { + Err(failure::err_msg(format!("no API found for {}", &api_call))) + } + }?; + let ApiCall { namespace, function, parts, params, body, - ignore } = api_call_components(api, endpoint, api_call, hash.unwrap())?; + ignore, + } = api_call_components(api, endpoint, api_call, hash.unwrap())?; // capture any namespaces used in the test if let Some(n) = namespace { @@ -733,40 +740,6 @@ fn generate_body(endpoint: &ApiEndpoint, v: &Yaml) -> Option { } } -/// Find the right ApiEndpoint from the REST API specs for the API call -/// defined in the YAML test. -/// -/// The REST API specs model only the stable APIs -/// currently, so no endpoint will be found for experimental or beta APIs -fn endpoint_from_api_call<'a>( - api: &'a Api, - api_call: &str, -) -> Result<&'a ApiEndpoint, failure::Error> { - let api_call_path: Vec<&str> = api_call.split('.').collect(); - match api_call_path.len() { - 1 => match api.root.get(api_call_path[0]) { - Some(endpoint) => Ok(endpoint), - None => Err(failure::err_msg(format!( - "No ApiEndpoint found for {}", - &api_call - ))), - }, - _ => match api.namespaces.get(api_call_path[0]) { - Some(namespace) => match namespace.get(api_call_path[1]) { - Some(endpoint) => Ok(endpoint), - None => Err(failure::err_msg(format!( - "No ApiEndpoint found for {}", - &api_call - ))), - }, - None => Err(failure::err_msg(format!( - "No ApiEndpoint found for {}", - &api_call - ))), - }, - } -} - /// Checks whether there are any Errs in the collection, and accumulates them into one /// error message if there are. fn ok_or_accumulate( From 0683c5bea1a5506821d212438b4e06f6e25cf899 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 5 Mar 2020 18:19:32 +1000 Subject: [PATCH 023/127] impl From<> for TrackTotalHits for i64 and bool --- elasticsearch/src/params/mod.rs | 13 +++ yaml_test_runner/src/generator.rs | 158 +++++++++++++++++++++--------- 2 files changed, 124 insertions(+), 47 deletions(-) diff --git a/elasticsearch/src/params/mod.rs b/elasticsearch/src/params/mod.rs index 468e74cc..2ee96a29 100644 --- a/elasticsearch/src/params/mod.rs +++ b/elasticsearch/src/params/mod.rs @@ -19,6 +19,18 @@ pub enum TrackTotalHits { Count(i64), } +impl From for TrackTotalHits { + fn from(b: bool) -> Self { + TrackTotalHits::Track(b) + } +} + +impl From for TrackTotalHits { + fn from(i: i64) -> Self { + TrackTotalHits::Count(i) + } +} + /// Control how the `_source` field is returned with every hit. /// /// By default operations return the contents of the `_source` field @@ -98,3 +110,4 @@ impl<'a> From<(Vec<&'a str>, Vec<&'a str>)> for SourceFilter { } } } + diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 0d8e40df..8f7b5d5c 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -1,9 +1,10 @@ use inflector::Inflector; use quote::Tokens; -use api_generator::generator::{Api, ApiEndpoint, TypeKind, ApiEnum}; +use api_generator::generator::{Api, ApiEndpoint, ApiEnum, TypeKind}; use itertools::Itertools; use regex::Regex; +use std::collections::HashSet; use std::fs; use std::fs::{File, OpenOptions}; use std::io::Write; @@ -13,7 +14,6 @@ use yaml_rust::{ yaml::{Array, Hash}, Yaml, YamlEmitter, YamlLoader, }; -use std::collections::HashSet; struct YamlTest { namespaces: HashSet, @@ -213,10 +213,8 @@ fn write_test_file( .as_bytes(), )?; - let (setup_fn, setup_call) = - generate_fixture("setup", &test.setup); - let (teardown_fn, teardown_call) = - generate_fixture("teardown", &test.teardown); + let (setup_fn, setup_call) = generate_fixture("setup", &test.setup); + let (teardown_fn, teardown_call) = generate_fixture("teardown", &test.teardown); let mut method_names = HashSet::new(); @@ -231,7 +229,8 @@ fn write_test_file( // duplicate generated test function names. Deduplicate by appending incrementing number while !method_names.insert(method_name.clone()) { lazy_static! { - static ref ENDING_DIGITS_REGEX: Regex = Regex::new(r"^(.*?)_(\d*?)$").unwrap(); + static ref ENDING_DIGITS_REGEX: Regex = + Regex::new(r"^(.*?)_(\d*?)$").unwrap(); } if let Some(c) = ENDING_DIGITS_REGEX.captures(&method_name) { let name = c.get(1).unwrap().as_str(); @@ -256,7 +255,8 @@ fn write_test_file( }) .collect(); - let namespaces: Vec = test.namespaces + let namespaces: Vec = test + .namespaces .iter() .map(|n| { let ident = syn::Ident::from(n.as_str()); @@ -265,9 +265,11 @@ fn write_test_file( .collect(); let tokens = quote! { + #![allow(unused_imports)] #[cfg(test)] pub mod tests { use elasticsearch::*; + use elasticsearch::http::request::JsonBody; use elasticsearch::params::*; #(#namespaces)* use crate::client; @@ -343,7 +345,12 @@ fn read_match(api: &Api, hash: &Hash, tokens: &mut Tokens) -> Result<(), failure Ok(()) } -fn read_do(api: &Api, test: &mut YamlTest, hash: &Hash, tokens: &mut Tokens) -> Result<(), failure::Error> { +fn read_do( + api: &Api, + test: &mut YamlTest, + hash: &Hash, + tokens: &mut Tokens, +) -> Result<(), failure::Error> { let results: Vec> = hash .iter() .map(|(k, v)| { @@ -418,28 +425,31 @@ fn read_do(api: &Api, test: &mut YamlTest, hash: &Hash, tokens: &mut Tokens) -> ok_or_accumulate(&results, 0) } -fn generated_params(api: &Api, endpoint: &ApiEndpoint, params: &[(&str, &Yaml)]) -> Result, failure::Error> { +fn generate_params( + api: &Api, + endpoint: &ApiEndpoint, + params: &[(&str, &Yaml)], +) -> Result, failure::Error> { match params.len() { 0 => Ok(None), _ => { let mut tokens = Tokens::new(); for (n, v) in params { - let param_ident = syn::Ident::from( - api_generator::generator::code_gen::valid_name(n), - ); + let param_ident = + syn::Ident::from(api_generator::generator::code_gen::valid_name(n)); let ty = match endpoint.params.get(*n) { Some(t) => Ok(t), None => match api.common_params.get(*n) { Some(t) => Ok(t), - None => Err(failure::err_msg(format!("No param found for {}", n))) - } + None => Err(failure::err_msg(format!("No param found for {}", n))), + }, }?; let kind = ty.ty; match v { - Yaml::String(s) => { + Yaml::String(ref s) => { match kind { TypeKind::Enum => { let e: String = n.to_pascal_case(); @@ -451,6 +461,7 @@ fn generated_params(api: &Api, endpoint: &ApiEndpoint, params: &[(&str, &Yaml)]) } else if e == "Size" { syn::Ident::from("Unspecified") } else { + //TODO: propagate as Err panic!(format!("Unhandled empty value for {}", &e)); } } else { @@ -458,37 +469,78 @@ fn generated_params(api: &Api, endpoint: &ApiEndpoint, params: &[(&str, &Yaml)]) }; tokens.append(quote! { - .#param_ident(#enum_name::#variant) - }) - }, + .#param_ident(#enum_name::#variant) + }) + } TypeKind::List => { let values: Vec<&str> = s.split(',').collect(); tokens.append(quote! { - .#param_ident(&[#(#values),*]) - }) + .#param_ident(&[#(#values),*]) + }) + } + TypeKind::Boolean => { + let b = s.parse::()?; + tokens.append(quote! { + .#param_ident(#b) + }); }, _ => tokens.append(quote! { - .#param_ident(#s) - }), + .#param_ident(#s) + }), } - }, - Yaml::Boolean(b) => { - match kind { - TypeKind::Enum => { - let enum_name = syn::Ident::from(n.to_pascal_case()); - let variant = syn::Ident::from(b.to_string().to_pascal_case()); + } + Yaml::Boolean(ref b) => match kind { + TypeKind::Enum => { + let enum_name = syn::Ident::from(n.to_pascal_case()); + let variant = syn::Ident::from(b.to_string().to_pascal_case()); + tokens.append(quote! { + .#param_ident(#enum_name::#variant) + }) + } + TypeKind::List => { + // TODO: _source filter can be true|false|list of strings + let s = b.to_string(); + tokens.append(quote! { + .#param_ident(&[#s]) + }) + } + _ => { + if n == &"track_total_hits" { tokens.append(quote! { - .#param_ident(#enum_name::#variant) - }) + .#param_ident(#b.into()) + }); + } else { + tokens.append(quote! { + .#param_ident(#b) + }); } - _ => tokens.append(quote! { - .#param_ident(#b) - }), + }, + }, + Yaml::Integer(ref i) => match kind { + TypeKind::String => { + let s = i.to_string(); + tokens.append(quote! { + .#param_ident(#s) + }) } + TypeKind::Integer | TypeKind::Number => { + let int = *i as i32; + tokens.append(quote! { + .#param_ident(#int) + }); + }, + _ => { + if n == &"track_total_hits" { + tokens.append(quote! { + .#param_ident(#i.into()) + }); + } else { + tokens.append(quote! { + .#param_ident(#i) + }); + } + }, }, - Yaml::Integer(i) => tokens.append(quote! { - .#param_ident(#i) - }), Yaml::Array(arr) => { // only support param string arrays let result: Vec<_> = arr @@ -504,8 +556,8 @@ fn generated_params(api: &Api, endpoint: &ApiEndpoint, params: &[(&str, &Yaml)]) .collect(); tokens.append(quote! { - .#param_ident(&[#(#result),*]) - }); + .#param_ident(&[#(#result),*]) + }); } _ => println!("Unsupported value {:?}", v), } @@ -516,7 +568,12 @@ fn generated_params(api: &Api, endpoint: &ApiEndpoint, params: &[(&str, &Yaml)]) } } -fn api_call_components<'a>(api: &'a Api, endpoint: &'a ApiEndpoint, api_call: &'a str, hash: &'a Hash) -> Result, failure::Error> { +fn api_call_components<'a>( + api: &'a Api, + endpoint: &'a ApiEndpoint, + api_call: &'a str, + hash: &'a Hash, +) -> Result, failure::Error> { let mut parts: Vec<(&str, &Yaml)> = vec![]; let mut params: Vec<(&str, &Yaml)> = vec![]; let mut body: Option = None; @@ -539,7 +596,7 @@ fn api_call_components<'a>(api: &'a Api, endpoint: &'a ApiEndpoint, api_call: &' } let parts = generate_parts(api_call, endpoint, &parts)?; - let params = generated_params(api, endpoint, ¶ms)?; + let params = generate_params(api, endpoint, ¶ms)?; let function = syn::Ident::from(api_call.replace(".", "().")); let namespace: Option<&str> = if api_call.contains(".") { Some(api_call.splitn(2, ".").collect::>()[0]) @@ -636,7 +693,10 @@ fn generate_parts( .map(|(p, v)| { let ty = match path.parts.get(*p) { Some(t) => Ok(t), - None => Err(failure::err_msg(format!("No URL part found for {} in {}", p, &path.path))) + None => Err(failure::err_msg(format!( + "No URL part found for {} in {}", + p, &path.path + ))), }?; match v { @@ -644,17 +704,17 @@ fn generate_parts( TypeKind::List => { let values: Vec<&str> = s.split(',').collect(); Ok(quote! { &[#(#values),*] }) - }, + } _ => Ok(quote! { #s }), }, Yaml::Boolean(b) => { let s = b.to_string(); Ok(quote! { #s }) - }, + } Yaml::Integer(i) => { let s = i.to_string(); Ok(quote! { #s }) - }, + } Yaml::Array(arr) => { // only support param string arrays let result: Vec<_> = arr @@ -711,7 +771,7 @@ fn generate_body(endpoint: &ApiEndpoint, v: &Yaml) -> Option { } else { Some(quote!(.body(#s))) } - }, + } _ => { let mut s = String::new(); { @@ -726,7 +786,11 @@ fn generate_body(endpoint: &ApiEndpoint, v: &Yaml) -> Option { .map(|value| { let json = serde_json::to_string(&value).unwrap(); let ident = syn::Ident::from(json); - quote!(json!(#ident)) + if value.is_string() { + quote!(#ident) + } else { + quote!(JsonBody::from(json!(#ident))) + } }) .collect(); Some(quote!(.body(vec![ #(#json),* ]))) From 4f335715f8655fe0488bd2622cd63045b9fda15d Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Fri, 6 Mar 2020 11:34:55 +1000 Subject: [PATCH 024/127] Refactor api call construction into try_from function --- api_generator/src/generator/mod.rs | 2 + yaml_test_runner/src/generator.rs | 816 +++++++++++++++-------------- 2 files changed, 426 insertions(+), 392 deletions(-) diff --git a/api_generator/src/generator/mod.rs b/api_generator/src/generator/mod.rs index bf1c5f71..cce69750 100644 --- a/api_generator/src/generator/mod.rs +++ b/api_generator/src/generator/mod.rs @@ -291,6 +291,7 @@ where /// An API endpoint defined in the REST API specs #[derive(Debug, PartialEq, Deserialize, Clone)] pub struct ApiEndpoint { + pub full_name: Option, #[serde(deserialize_with = "string_or_struct")] documentation: Documentation, pub stability: String, @@ -532,6 +533,7 @@ where // get the first (and only) endpoint name and endpoint body let mut first_endpoint = endpoint.into_iter().next().unwrap(); + first_endpoint.1.full_name = Some(first_endpoint.0.clone()); // sort the HTTP methods so that we can easily pattern match on them later for path in first_endpoint.1.url.paths.iter_mut() { diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 8f7b5d5c..6592abd6 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -9,12 +9,12 @@ use std::fs; use std::fs::{File, OpenOptions}; use std::io::Write; use std::path::PathBuf; -use syn::parse::path; use yaml_rust::{ yaml::{Array, Hash}, Yaml, YamlEmitter, YamlLoader, }; +/// The components of a test file, constructed from a yaml file struct YamlTest { namespaces: HashSet, setup: Option, @@ -43,6 +43,416 @@ struct ApiCall<'a> { ignore: Option, } +// TODO: continue moving API call parts to this impl +impl<'a> ApiCall<'a> { + pub fn try_from( + api: &'a Api, + endpoint: &'a ApiEndpoint, + hash: &'a Hash, + ) -> Result, failure::Error> { + let mut parts: Vec<(&str, &Yaml)> = vec![]; + let mut params: Vec<(&str, &Yaml)> = vec![]; + let mut body: Option = None; + let mut ignore: Option = None; + + // work out what's a URL part and what's a param in the supplied + // arguments for the API call + for (k, v) in hash.iter() { + let key = k.as_str().unwrap(); + if endpoint.params.contains_key(key) || api.common_params.contains_key(key) { + params.push((key, v)); + } else if key == "body" { + body = Self::generate_body(endpoint, v); + } else if key == "ignore" { + ignore = match v.as_i64() { + Some(i) => Some(i), + // handle ignore as an array of i64 + None => v.as_vec().unwrap()[0].as_i64(), + } + } else { + parts.push((key, v)); + } + } + + let api_call = endpoint.full_name.as_ref().unwrap(); + let parts = Self::generate_parts(api_call, endpoint, &parts)?; + let params = Self::generate_params(api, endpoint, ¶ms)?; + let function = syn::Ident::from(api_call.replace(".", "().")); + let namespace: Option<&str> = if api_call.contains(".") { + Some(api_call.splitn(2, ".").collect::>()[0]) + } else { + None + }; + + Ok(ApiCall { + namespace, + function, + parts, + params, + body, + ignore, + }) + } + + fn generate_params( + api: &Api, + endpoint: &ApiEndpoint, + params: &[(&str, &Yaml)], + ) -> Result, failure::Error> { + match params.len() { + 0 => Ok(None), + _ => { + let mut tokens = Tokens::new(); + for (n, v) in params { + let param_ident = + syn::Ident::from(api_generator::generator::code_gen::valid_name(n)); + + let ty = match endpoint.params.get(*n) { + Some(t) => Ok(t), + None => match api.common_params.get(*n) { + Some(t) => Ok(t), + None => Err(failure::err_msg(format!("No param found for {}", n))), + }, + }?; + + let kind = ty.ty; + + match v { + Yaml::String(ref s) => { + match kind { + TypeKind::Enum => { + let e: String = n.to_pascal_case(); + let enum_name = syn::Ident::from(e.as_str()); + let variant = if s.is_empty() { + // TODO: Should we simply omit empty Refresh tests? + if e == "Refresh" { + syn::Ident::from("True") + } else if e == "Size" { + syn::Ident::from("Unspecified") + } else { + //TODO: propagate as Err + panic!(format!("Unhandled empty value for {}", &e)); + } + } else { + syn::Ident::from(s.to_pascal_case().replace("_", "")) + }; + + tokens.append(quote! { + .#param_ident(#enum_name::#variant) + }) + } + TypeKind::List => { + let values: Vec<&str> = s.split(',').collect(); + tokens.append(quote! { + .#param_ident(&[#(#values),*]) + }) + } + TypeKind::Boolean => { + let b = s.parse::()?; + tokens.append(quote! { + .#param_ident(#b) + }); + } + TypeKind::Double => { + let f = s.parse::()?; + tokens.append(quote! { + .#param_ident(#f) + }); + } + _ => tokens.append(quote! { + .#param_ident(#s) + }), + } + } + Yaml::Boolean(ref b) => match kind { + TypeKind::Enum => { + let enum_name = syn::Ident::from(n.to_pascal_case()); + let variant = syn::Ident::from(b.to_string().to_pascal_case()); + tokens.append(quote! { + .#param_ident(#enum_name::#variant) + }) + } + TypeKind::List => { + // TODO: _source filter can be true|false|list of strings + let s = b.to_string(); + tokens.append(quote! { + .#param_ident(&[#s]) + }) + } + _ => { + if n == &"track_total_hits" { + tokens.append(quote! { + .#param_ident(#b.into()) + }); + } else { + tokens.append(quote! { + .#param_ident(#b) + }); + } + } + }, + Yaml::Integer(ref i) => match kind { + TypeKind::String => { + let s = i.to_string(); + tokens.append(quote! { + .#param_ident(#s) + }) + } + TypeKind::Integer => { + // yaml-rust parses all as i64 + let int = *i as i32; + tokens.append(quote! { + .#param_ident(#int) + }); + } + TypeKind::Float => { + // yaml-rust parses all as i64 + let f = *i as f32; + tokens.append(quote! { + .#param_ident(#f) + }); + } + TypeKind::Double => { + // yaml-rust parses all as i64 + let f = *i as f64; + tokens.append(quote! { + .#param_ident(#f) + }); + } + _ => { + if n == &"track_total_hits" { + tokens.append(quote! { + .#param_ident(#i.into()) + }); + } else { + tokens.append(quote! { + .#param_ident(#i) + }); + } + } + }, + Yaml::Array(arr) => { + // only support param string arrays + let result: Vec<_> = arr + .iter() + .map(|i| match i { + Yaml::String(s) => Ok(s), + y => Err(failure::err_msg(format!( + "Unsupported array value {:?}", + y + ))), + }) + .filter_map(Result::ok) + .collect(); + + tokens.append(quote! { + .#param_ident(&[#(#result),*]) + }); + } + _ => println!("Unsupported value {:?}", v), + } + } + + Ok(Some(tokens)) + } + } + } + + fn generate_parts( + api_call: &str, + endpoint: &ApiEndpoint, + parts: &[(&str, &Yaml)], + ) -> Result, failure::Error> { + // TODO: ideally, this should share the logic from EnumBuilder + let enum_name = { + let name = api_call.to_pascal_case().replace(".", ""); + syn::Ident::from(format!("{}Parts", name)) + }; + + // Enum variants containing no URL parts where there is only a single API URL, + // are not required to be passed in the API + if parts.is_empty() { + return match endpoint.url.paths.len() { + 1 => Ok(None), + _ => Ok(Some(quote!(#enum_name::None))), + }; + } + + let path = match endpoint.url.paths.len() { + 1 => Some(&endpoint.url.paths[0]), + _ => { + // get the matching path parts + let matching_path_parts = endpoint + .url + .paths + .iter() + .filter(|path| { + let p = path.path.params(); + if p.len() != parts.len() { + return false; + } + + let contains = parts + .iter() + .filter_map(|i| if p.contains(&i.0) { Some(()) } else { None }) + .collect::>(); + contains.len() == parts.len() + }) + .collect::>(); + + match matching_path_parts.len() { + 0 => None, + _ => Some(matching_path_parts[0]), + } + } + }; + + if path.is_none() { + return Err(failure::err_msg(format!( + "No path for '{}' API with URL parts {:?}", + &api_call, parts + ))); + } + + let path = path.unwrap(); + let path_parts = path.path.params(); + let variant_name = { + let v = path_parts + .iter() + .map(|k| k.to_pascal_case()) + .collect::>() + .join(""); + syn::Ident::from(v) + }; + + let part_tokens: Vec> = parts + .clone() + .into_iter() + // don't rely on URL parts being ordered in the yaml test + .sorted_by(|(p, _), (p2, _)| { + let f = path_parts.iter().position(|x| x == p).unwrap(); + let s = path_parts.iter().position(|x| x == p2).unwrap(); + f.cmp(&s) + }) + .map(|(p, v)| { + let ty = match path.parts.get(*p) { + Some(t) => Ok(t), + None => Err(failure::err_msg(format!( + "No URL part found for {} in {}", + p, &path.path + ))), + }?; + + match v { + Yaml::String(s) => match ty.ty { + TypeKind::List => { + let values: Vec<&str> = s.split(',').collect(); + Ok(quote! { &[#(#values),*] }) + } + _ => Ok(quote! { #s }), + }, + Yaml::Boolean(b) => { + let s = b.to_string(); + Ok(quote! { #s }) + } + Yaml::Integer(i) => { + let s = i.to_string(); + Ok(quote! { #s }) + } + Yaml::Array(arr) => { + // only support param string arrays + let result: Vec<_> = arr + .iter() + .map(|i| match i { + Yaml::String(s) => Ok(s), + y => Err(failure::err_msg(format!( + "Unsupported array value {:?}", + y + ))), + }) + .collect(); + + match ok_or_accumulate(&result, 0) { + Ok(_) => { + let result: Vec<_> = + result.into_iter().filter_map(Result::ok).collect(); + Ok(quote! { &[#(#result),*] }) + } + Err(e) => Err(failure::err_msg(e)), + } + } + _ => Err(failure::err_msg(format!("Unsupported value {:?}", v))), + } + }) + .collect(); + + match ok_or_accumulate(&part_tokens, 0) { + Ok(_) => { + let part_tokens: Vec = + part_tokens.into_iter().filter_map(Result::ok).collect(); + Ok(Some( + quote! { #enum_name::#variant_name(#(#part_tokens),*) }, + )) + } + Err(e) => Err(failure::err_msg(e)), + } + } + + /// Creates the body function call from a YAML value. + /// + /// When reading a body from the YAML test, it'll be converted to a Yaml variant, + /// usually a Hash. To get the JSON representation back requires converting + /// back to JSON + fn generate_body(endpoint: &ApiEndpoint, v: &Yaml) -> Option { + let accepts_nd_body = match &endpoint.body { + Some(b) => match &b.serialize { + Some(s) => s == "bulk", + _ => false, + }, + None => false, + }; + + match v { + Yaml::String(s) => { + if accepts_nd_body { + Some(quote!(.body(vec![#s]))) + } else { + Some(quote!(.body(#s))) + } + } + _ => { + let mut s = String::new(); + { + let mut emitter = YamlEmitter::new(&mut s); + emitter.dump(v).unwrap(); + } + + if accepts_nd_body { + let values: Vec = serde_yaml::from_str(&s).unwrap(); + let json: Vec = values + .iter() + .map(|value| { + let json = serde_json::to_string(&value).unwrap(); + let ident = syn::Ident::from(json); + if value.is_string() { + quote!(#ident) + } else { + quote!(JsonBody::from(json!(#ident))) + } + }) + .collect(); + Some(quote!(.body(vec![ #(#json),* ]))) + } else { + let value: serde_json::Value = serde_yaml::from_str(&s).unwrap(); + let json = serde_json::to_string(&value).unwrap(); + let ident = syn::Ident::from(json); + Some(quote!(.body(json!(#ident)))) + } + } + } + } +} + pub fn generate_tests_from_yaml( api: &Api, base_download_dir: &PathBuf, @@ -85,7 +495,7 @@ pub fn generate_tests_from_yaml( let (first_key, first_value) = hash.iter().next().unwrap(); match (first_key, first_value) { (Yaml::String(name), Yaml::Array(steps)) => { - let tokens = read_steps(api, &mut test, steps)?; + let tokens = parse_steps(api, &mut test, steps)?; match name.as_str() { "setup" => test.setup = Some(tokens), "teardown" => test.teardown = Some(tokens), @@ -134,6 +544,7 @@ pub fn generate_tests_from_yaml( Ok(()) } +/// Writes a mod.rs file in each generated directory fn write_mod_files(generated_dir: &PathBuf) -> Result<(), failure::Error> { let paths = fs::read_dir(generated_dir).unwrap(); let mut mods = vec![]; @@ -215,8 +626,7 @@ fn write_test_file( let (setup_fn, setup_call) = generate_fixture("setup", &test.setup); let (teardown_fn, teardown_call) = generate_fixture("teardown", &test.teardown); - - let mut method_names = HashSet::new(); + let mut seen_method_names = HashSet::new(); let tests: Vec = test .tests @@ -227,7 +637,7 @@ fn write_test_file( // some method descriptions are the same in YAML tests, which would result in // duplicate generated test function names. Deduplicate by appending incrementing number - while !method_names.insert(method_name.clone()) { + while !seen_method_names.insert(method_name.clone()) { lazy_static! { static ref ENDING_DIGITS_REGEX: Regex = Regex::new(r"^(.*?)_(\d*?)$").unwrap(); @@ -237,7 +647,7 @@ fn write_test_file( let n = c.get(2).unwrap().as_str().parse::().unwrap(); method_name = format!("{}_{}", name, n + 1); } else { - method_name = format!("{}_2", method_name); + method_name.push_str("_2"); } } syn::Ident::from(method_name) @@ -288,6 +698,7 @@ fn write_test_file( Ok(()) } +/// Generates the AST for the fixture fn and its invocation fn generate_fixture(name: &str, tokens: &Option) -> (Option, Option) { if let Some(t) = tokens { let ident = syn::Ident::from(name); @@ -305,7 +716,7 @@ fn generate_fixture(name: &str, tokens: &Option) -> (Option, Opt } } -fn read_steps(api: &Api, test: &mut YamlTest, steps: &Array) -> Result { +fn parse_steps(api: &Api, test: &mut YamlTest, steps: &Array) -> Result { let mut tokens = Tokens::new(); for step in steps { if let Some(hash) = step.as_hash() { @@ -315,10 +726,10 @@ fn read_steps(api: &Api, test: &mut YamlTest, steps: &Array) -> Result {} - ("do", Yaml::Hash(h)) => read_do(api, test, h, &mut tokens)?, + ("do", Yaml::Hash(h)) => parse_do(api, test, h, &mut tokens)?, ("set", Yaml::Hash(h)) => {} ("transform_and_set", Yaml::Hash(h)) => {} - ("match", Yaml::Hash(h)) => read_match(api, h, &mut tokens)?, + ("match", Yaml::Hash(h)) => parse_match(api, h, &mut tokens)?, ("contains", Yaml::Hash(h)) => {} ("is_true", Yaml::Hash(h)) => {} ("is_true", Yaml::String(s)) => {} @@ -340,12 +751,12 @@ fn read_steps(api: &Api, test: &mut YamlTest, steps: &Array) -> Result Result<(), failure::Error> { +fn parse_match(api: &Api, hash: &Hash, tokens: &mut Tokens) -> Result<(), failure::Error> { // TODO: implement Ok(()) } -fn read_do( +fn parse_do( api: &Api, test: &mut YamlTest, hash: &Hash, @@ -385,7 +796,7 @@ fn read_do( let endpoint = match api.endpoint_for_api_call(api_call) { Some(e) => Ok(e), None => { - Err(failure::err_msg(format!("no API found for {}", &api_call))) + Err(failure::err_msg(format!("no API found for {}", api_call))) } }?; @@ -396,7 +807,7 @@ fn read_do( params, body, ignore, - } = api_call_components(api, endpoint, api_call, hash.unwrap())?; + } = ApiCall::try_from(api, endpoint, hash.unwrap())?; // capture any namespaces used in the test if let Some(n) = namespace { @@ -425,385 +836,6 @@ fn read_do( ok_or_accumulate(&results, 0) } -fn generate_params( - api: &Api, - endpoint: &ApiEndpoint, - params: &[(&str, &Yaml)], -) -> Result, failure::Error> { - match params.len() { - 0 => Ok(None), - _ => { - let mut tokens = Tokens::new(); - for (n, v) in params { - let param_ident = - syn::Ident::from(api_generator::generator::code_gen::valid_name(n)); - - let ty = match endpoint.params.get(*n) { - Some(t) => Ok(t), - None => match api.common_params.get(*n) { - Some(t) => Ok(t), - None => Err(failure::err_msg(format!("No param found for {}", n))), - }, - }?; - - let kind = ty.ty; - - match v { - Yaml::String(ref s) => { - match kind { - TypeKind::Enum => { - let e: String = n.to_pascal_case(); - let enum_name = syn::Ident::from(e.as_str()); - let variant = if s.is_empty() { - // TODO: Should we simply omit empty Refresh tests? - if e == "Refresh" { - syn::Ident::from("True") - } else if e == "Size" { - syn::Ident::from("Unspecified") - } else { - //TODO: propagate as Err - panic!(format!("Unhandled empty value for {}", &e)); - } - } else { - syn::Ident::from(s.to_pascal_case().replace("_", "")) - }; - - tokens.append(quote! { - .#param_ident(#enum_name::#variant) - }) - } - TypeKind::List => { - let values: Vec<&str> = s.split(',').collect(); - tokens.append(quote! { - .#param_ident(&[#(#values),*]) - }) - } - TypeKind::Boolean => { - let b = s.parse::()?; - tokens.append(quote! { - .#param_ident(#b) - }); - }, - _ => tokens.append(quote! { - .#param_ident(#s) - }), - } - } - Yaml::Boolean(ref b) => match kind { - TypeKind::Enum => { - let enum_name = syn::Ident::from(n.to_pascal_case()); - let variant = syn::Ident::from(b.to_string().to_pascal_case()); - tokens.append(quote! { - .#param_ident(#enum_name::#variant) - }) - } - TypeKind::List => { - // TODO: _source filter can be true|false|list of strings - let s = b.to_string(); - tokens.append(quote! { - .#param_ident(&[#s]) - }) - } - _ => { - if n == &"track_total_hits" { - tokens.append(quote! { - .#param_ident(#b.into()) - }); - } else { - tokens.append(quote! { - .#param_ident(#b) - }); - } - }, - }, - Yaml::Integer(ref i) => match kind { - TypeKind::String => { - let s = i.to_string(); - tokens.append(quote! { - .#param_ident(#s) - }) - } - TypeKind::Integer | TypeKind::Number => { - let int = *i as i32; - tokens.append(quote! { - .#param_ident(#int) - }); - }, - _ => { - if n == &"track_total_hits" { - tokens.append(quote! { - .#param_ident(#i.into()) - }); - } else { - tokens.append(quote! { - .#param_ident(#i) - }); - } - }, - }, - Yaml::Array(arr) => { - // only support param string arrays - let result: Vec<_> = arr - .iter() - .map(|i| match i { - Yaml::String(s) => Ok(s), - y => Err(failure::err_msg(format!( - "Unsupported array value {:?}", - y - ))), - }) - .filter_map(Result::ok) - .collect(); - - tokens.append(quote! { - .#param_ident(&[#(#result),*]) - }); - } - _ => println!("Unsupported value {:?}", v), - } - } - - Ok(Some(tokens)) - } - } -} - -fn api_call_components<'a>( - api: &'a Api, - endpoint: &'a ApiEndpoint, - api_call: &'a str, - hash: &'a Hash, -) -> Result, failure::Error> { - let mut parts: Vec<(&str, &Yaml)> = vec![]; - let mut params: Vec<(&str, &Yaml)> = vec![]; - let mut body: Option = None; - let mut ignore: Option = None; - - for (k, v) in hash.iter() { - let key = k.as_str().unwrap(); - if endpoint.params.contains_key(key) || api.common_params.contains_key(key) { - params.push((key, v)); - } else if key == "body" { - body = generate_body(endpoint, v); - } else if key == "ignore" { - ignore = match v.as_i64() { - Some(i) => Some(i), - None => v.as_vec().unwrap()[0].as_i64(), - } - } else { - parts.push((key, v)); - } - } - - let parts = generate_parts(api_call, endpoint, &parts)?; - let params = generate_params(api, endpoint, ¶ms)?; - let function = syn::Ident::from(api_call.replace(".", "().")); - let namespace: Option<&str> = if api_call.contains(".") { - Some(api_call.splitn(2, ".").collect::>()[0]) - } else { - None - }; - - Ok(ApiCall { - namespace, - function, - parts, - params, - body, - ignore, - }) -} - -fn generate_parts( - api_call: &str, - endpoint: &ApiEndpoint, - parts: &[(&str, &Yaml)], -) -> Result, failure::Error> { - // TODO: ideally, this should share the logic from EnumBuilder - let enum_name = { - let name = api_call.to_pascal_case().replace(".", ""); - syn::Ident::from(format!("{}Parts", name)) - }; - - // Enum variants containing no URL parts where there is only a single API URL, - // are not required to be passed in the API - if parts.is_empty() { - return match endpoint.url.paths.len() { - 1 => Ok(None), - _ => Ok(Some(quote!(#enum_name::None))), - }; - } - - let path = match endpoint.url.paths.len() { - 1 => Some(&endpoint.url.paths[0]), - _ => { - // get the matching path parts - let matching_path_parts = endpoint - .url - .paths - .iter() - .filter(|path| { - let p = path.path.params(); - if p.len() != parts.len() { - return false; - } - - let contains = parts - .iter() - .filter_map(|i| if p.contains(&i.0) { Some(()) } else { None }) - .collect::>(); - contains.len() == parts.len() - }) - .collect::>(); - - match matching_path_parts.len() { - 0 => None, - _ => Some(matching_path_parts[0]), - } - } - }; - - if path.is_none() { - return Err(failure::err_msg(format!( - "No path for '{}' API with URL parts {:?}", - &api_call, parts - ))); - } - - let path = path.unwrap(); - let path_parts = path.path.params(); - let variant_name = { - let v = path_parts - .iter() - .map(|k| k.to_pascal_case()) - .collect::>() - .join(""); - syn::Ident::from(v) - }; - - let part_tokens: Vec> = parts - .clone() - .into_iter() - // don't rely on URL parts being ordered in the yaml test - .sorted_by(|(p, _), (p2, _)| { - let f = path_parts.iter().position(|x| x == p).unwrap(); - let s = path_parts.iter().position(|x| x == p2).unwrap(); - f.cmp(&s) - }) - .map(|(p, v)| { - let ty = match path.parts.get(*p) { - Some(t) => Ok(t), - None => Err(failure::err_msg(format!( - "No URL part found for {} in {}", - p, &path.path - ))), - }?; - - match v { - Yaml::String(s) => match ty.ty { - TypeKind::List => { - let values: Vec<&str> = s.split(',').collect(); - Ok(quote! { &[#(#values),*] }) - } - _ => Ok(quote! { #s }), - }, - Yaml::Boolean(b) => { - let s = b.to_string(); - Ok(quote! { #s }) - } - Yaml::Integer(i) => { - let s = i.to_string(); - Ok(quote! { #s }) - } - Yaml::Array(arr) => { - // only support param string arrays - let result: Vec<_> = arr - .iter() - .map(|i| match i { - Yaml::String(s) => Ok(s), - y => Err(failure::err_msg(format!("Unsupported array value {:?}", y))), - }) - .collect(); - - match ok_or_accumulate(&result, 0) { - Ok(_) => { - let result: Vec<_> = - result.into_iter().filter_map(Result::ok).collect(); - Ok(quote! { &[#(#result),*] }) - } - Err(e) => Err(failure::err_msg(e)), - } - } - _ => Err(failure::err_msg(format!("Unsupported value {:?}", v))), - } - }) - .collect(); - - match ok_or_accumulate(&part_tokens, 0) { - Ok(_) => { - let part_tokens: Vec = part_tokens.into_iter().filter_map(Result::ok).collect(); - Ok(Some( - quote! { #enum_name::#variant_name(#(#part_tokens),*) }, - )) - } - Err(e) => Err(failure::err_msg(e)), - } -} - -/// Creates the body function call from a YAML value. -/// -/// When reading a body from the YAML test, it'll be converted to a Yaml variant, -/// usually a Hash. To get the JSON representation back requires converting -/// back to JSON -fn generate_body(endpoint: &ApiEndpoint, v: &Yaml) -> Option { - let accepts_nd_body = match &endpoint.body { - Some(b) => match &b.serialize { - Some(s) => s == "bulk", - _ => false, - }, - None => false, - }; - - match v { - Yaml::String(s) => { - if accepts_nd_body { - Some(quote!(.body(vec![#s]))) - } else { - Some(quote!(.body(#s))) - } - } - _ => { - let mut s = String::new(); - { - let mut emitter = YamlEmitter::new(&mut s); - emitter.dump(v).unwrap(); - } - - if accepts_nd_body { - let values: Vec = serde_yaml::from_str(&s).unwrap(); - let json: Vec = values - .iter() - .map(|value| { - let json = serde_json::to_string(&value).unwrap(); - let ident = syn::Ident::from(json); - if value.is_string() { - quote!(#ident) - } else { - quote!(JsonBody::from(json!(#ident))) - } - }) - .collect(); - Some(quote!(.body(vec![ #(#json),* ]))) - } else { - let value: serde_json::Value = serde_yaml::from_str(&s).unwrap(); - let json = serde_json::to_string(&value).unwrap(); - let ident = syn::Ident::from(json); - Some(quote!(.body(json!(#ident)))) - } - } - } -} - /// Checks whether there are any Errs in the collection, and accumulates them into one /// error message if there are. fn ok_or_accumulate( From 2a794137229b6e6a44be89eacb8a77bc880135fb Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Fri, 6 Mar 2020 13:20:20 +1000 Subject: [PATCH 025/127] Handle arrays that maps to string params --- yaml_test_runner/src/generator.rs | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 6592abd6..9d1f2946 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -1,7 +1,7 @@ use inflector::Inflector; use quote::Tokens; -use api_generator::generator::{Api, ApiEndpoint, ApiEnum, TypeKind}; +use api_generator::generator::{Api, ApiEndpoint, TypeKind}; use itertools::Itertools; use regex::Regex; use std::collections::HashSet; @@ -348,16 +348,21 @@ impl<'a> ApiCall<'a> { TypeKind::List => { let values: Vec<&str> = s.split(',').collect(); Ok(quote! { &[#(#values),*] }) - } + }, _ => Ok(quote! { #s }), }, Yaml::Boolean(b) => { let s = b.to_string(); Ok(quote! { #s }) } - Yaml::Integer(i) => { - let s = i.to_string(); - Ok(quote! { #s }) + Yaml::Integer(i) => match ty.ty { + TypeKind::Long => { + Ok(quote!{ #i }) + }, + _ => { + let s = i.to_string(); + Ok(quote! { #s }) + }, } Yaml::Array(arr) => { // only support param string arrays @@ -376,7 +381,18 @@ impl<'a> ApiCall<'a> { Ok(_) => { let result: Vec<_> = result.into_iter().filter_map(Result::ok).collect(); - Ok(quote! { &[#(#result),*] }) + + match ty.ty { + // Some APIs specify a part is a string in the REST API spec + // but is really a list, which is what a YAML test might pass + // e.g. security.get_role_mapping. + // see https://github.com/elastic/elasticsearch/pull/53207 + TypeKind::String => { + let s = result.iter().join(","); + Ok(quote! { #s }) + }, + _ => Ok(quote! { &[#(#result),*] }) + } } Err(e) => Err(failure::err_msg(e)), } From 8475d77267059ea10f96fbf6f68d1cd867dd22ce Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Fri, 6 Mar 2020 14:44:54 +1000 Subject: [PATCH 026/127] Define expand_wildcards as a collection --- api_generator/src/generator/code_gen/mod.rs | 20 ++- .../generated/namespace_clients/cluster.rs | 12 +- .../generated/namespace_clients/indices.rs | 162 +++++++++--------- elasticsearch/src/generated/root.rs | 42 ++--- 4 files changed, 125 insertions(+), 111 deletions(-) diff --git a/api_generator/src/generator/code_gen/mod.rs b/api_generator/src/generator/code_gen/mod.rs index 361a44c6..056e1dae 100644 --- a/api_generator/src/generator/code_gen/mod.rs +++ b/api_generator/src/generator/code_gen/mod.rs @@ -128,12 +128,26 @@ fn typekind_to_ty(name: &str, kind: TypeKind, required: bool) -> syn::Ty { let str_type = "&'b str"; match kind { TypeKind::None => v.push_str(str_type), - TypeKind::List => v.push_str(format!("&'b [{}]", str_type).as_ref()), - TypeKind::Enum => v.push_str(name.to_pascal_case().as_str()), + TypeKind::List => { + v.push_str("&'b ["); + v.push_str(str_type); + v.push_str("]"); + }, + TypeKind::Enum => match name { + // opened https://github.com/elastic/elasticsearch/issues/53212 + // to discuss whether this really should be a collection + "expand_wildcards" => { + // Expand wildcards should + v.push_str("&'b ["); + v.push_str(name.to_pascal_case().as_str()); + v.push_str("]"); + }, + _ => v.push_str(name.to_pascal_case().as_str()) + }, TypeKind::String => v.push_str(str_type), TypeKind::Text => v.push_str(str_type), TypeKind::Boolean => match name { - "track_total_hits" => v.push_str("TrackTotalHits"), + "track_total_hits" => v.push_str(name.to_pascal_case().as_str()), _ => v.push_str("bool"), }, TypeKind::Number => v.push_str("i64"), diff --git a/elasticsearch/src/generated/namespace_clients/cluster.rs b/elasticsearch/src/generated/namespace_clients/cluster.rs index 1a996860..019f487a 100644 --- a/elasticsearch/src/generated/namespace_clients/cluster.rs +++ b/elasticsearch/src/generated/namespace_clients/cluster.rs @@ -364,7 +364,7 @@ pub struct ClusterHealth<'a, 'b> { client: &'a Elasticsearch, parts: ClusterHealthParts<'b>, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, filter_path: Option<&'b [&'b str]>, headers: HeaderMap, human: Option, @@ -412,7 +412,7 @@ impl<'a, 'b> ClusterHealth<'a, 'b> { self } #[doc = "Whether to expand wildcard expression to concrete indices that are open, closed or both."] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -506,7 +506,7 @@ impl<'a, 'b> ClusterHealth<'a, 'b> { #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", serialize_with = "crate::client::serialize_coll_qs" @@ -1212,7 +1212,7 @@ pub struct ClusterState<'a, 'b> { parts: ClusterStateParts<'b>, allow_no_indices: Option, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, filter_path: Option<&'b [&'b str]>, flat_settings: Option, headers: HeaderMap, @@ -1258,7 +1258,7 @@ impl<'a, 'b> ClusterState<'a, 'b> { self } #[doc = "Whether to expand wildcard expression to concrete indices that are open, closed or both."] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -1331,7 +1331,7 @@ impl<'a, 'b> ClusterState<'a, 'b> { #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", serialize_with = "crate::client::serialize_coll_qs" diff --git a/elasticsearch/src/generated/namespace_clients/indices.rs b/elasticsearch/src/generated/namespace_clients/indices.rs index 4ebd3d77..0320fa72 100644 --- a/elasticsearch/src/generated/namespace_clients/indices.rs +++ b/elasticsearch/src/generated/namespace_clients/indices.rs @@ -215,7 +215,7 @@ pub struct IndicesClearCache<'a, 'b, B> { allow_no_indices: Option, body: Option, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, fielddata: Option, fields: Option<&'b [&'b str]>, filter_path: Option<&'b [&'b str]>, @@ -290,7 +290,7 @@ where self } #[doc = "Whether to expand wildcard expression to concrete indices that are open, closed or both."] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -363,7 +363,7 @@ where #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde(rename = "fielddata")] fielddata: Option, #[serde(rename = "fields", serialize_with = "crate::client::serialize_coll_qs")] @@ -614,7 +614,7 @@ pub struct IndicesClose<'a, 'b, B> { allow_no_indices: Option, body: Option, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, filter_path: Option<&'b [&'b str]>, headers: HeaderMap, human: Option, @@ -683,7 +683,7 @@ where self } #[doc = "Whether to expand wildcard expression to concrete indices that are open, closed or both."] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -746,7 +746,7 @@ where #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", serialize_with = "crate::client::serialize_coll_qs" @@ -998,7 +998,7 @@ pub struct IndicesDelete<'a, 'b> { parts: IndicesDeleteParts<'b>, allow_no_indices: Option, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, filter_path: Option<&'b [&'b str]>, headers: HeaderMap, human: Option, @@ -1038,7 +1038,7 @@ impl<'a, 'b> IndicesDelete<'a, 'b> { self } #[doc = "Whether wildcard expressions should get expanded to open or closed indices (default: open)"] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -1096,7 +1096,7 @@ impl<'a, 'b> IndicesDelete<'a, 'b> { #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", serialize_with = "crate::client::serialize_coll_qs" @@ -1438,7 +1438,7 @@ pub struct IndicesExists<'a, 'b> { parts: IndicesExistsParts<'b>, allow_no_indices: Option, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, filter_path: Option<&'b [&'b str]>, flat_settings: Option, headers: HeaderMap, @@ -1480,7 +1480,7 @@ impl<'a, 'b> IndicesExists<'a, 'b> { self } #[doc = "Whether wildcard expressions should get expanded to open or closed indices (default: open)"] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -1543,7 +1543,7 @@ impl<'a, 'b> IndicesExists<'a, 'b> { #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", serialize_with = "crate::client::serialize_coll_qs" @@ -1626,7 +1626,7 @@ pub struct IndicesExistsAlias<'a, 'b> { parts: IndicesExistsAliasParts<'b>, allow_no_indices: Option, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, filter_path: Option<&'b [&'b str]>, headers: HeaderMap, human: Option, @@ -1664,7 +1664,7 @@ impl<'a, 'b> IndicesExistsAlias<'a, 'b> { self } #[doc = "Whether to expand wildcard expression to concrete indices that are open, closed or both."] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -1717,7 +1717,7 @@ impl<'a, 'b> IndicesExistsAlias<'a, 'b> { #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", serialize_with = "crate::client::serialize_coll_qs" @@ -1931,7 +1931,7 @@ pub struct IndicesExistsType<'a, 'b> { parts: IndicesExistsTypeParts<'b>, allow_no_indices: Option, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, filter_path: Option<&'b [&'b str]>, headers: HeaderMap, human: Option, @@ -1969,7 +1969,7 @@ impl<'a, 'b> IndicesExistsType<'a, 'b> { self } #[doc = "Whether to expand wildcard expression to concrete indices that are open, closed or both."] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -2022,7 +2022,7 @@ impl<'a, 'b> IndicesExistsType<'a, 'b> { #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", serialize_with = "crate::client::serialize_coll_qs" @@ -2092,7 +2092,7 @@ pub struct IndicesFlush<'a, 'b, B> { allow_no_indices: Option, body: Option, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, filter_path: Option<&'b [&'b str]>, force: Option, headers: HeaderMap, @@ -2158,7 +2158,7 @@ where self } #[doc = "Whether to expand wildcard expression to concrete indices that are open, closed or both."] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -2219,7 +2219,7 @@ where #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", serialize_with = "crate::client::serialize_coll_qs" @@ -2292,7 +2292,7 @@ pub struct IndicesFlushSynced<'a, 'b, B> { allow_no_indices: Option, body: Option, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, filter_path: Option<&'b [&'b str]>, headers: HeaderMap, human: Option, @@ -2352,7 +2352,7 @@ where self } #[doc = "Whether to expand wildcard expression to concrete indices that are open, closed or both."] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -2403,7 +2403,7 @@ where #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", serialize_with = "crate::client::serialize_coll_qs" @@ -2470,7 +2470,7 @@ pub struct IndicesForcemerge<'a, 'b, B> { allow_no_indices: Option, body: Option, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, filter_path: Option<&'b [&'b str]>, flush: Option, headers: HeaderMap, @@ -2539,7 +2539,7 @@ where self } #[doc = "Whether to expand wildcard expression to concrete indices that are open, closed or both."] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -2602,7 +2602,7 @@ where #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", serialize_with = "crate::client::serialize_coll_qs" @@ -2674,7 +2674,7 @@ pub struct IndicesFreeze<'a, 'b, B> { allow_no_indices: Option, body: Option, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, filter_path: Option<&'b [&'b str]>, headers: HeaderMap, human: Option, @@ -2743,7 +2743,7 @@ where self } #[doc = "Whether to expand wildcard expression to concrete indices that are open, closed or both."] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -2806,7 +2806,7 @@ where #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", serialize_with = "crate::client::serialize_coll_qs" @@ -2877,7 +2877,7 @@ pub struct IndicesGet<'a, 'b> { parts: IndicesGetParts<'b>, allow_no_indices: Option, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, filter_path: Option<&'b [&'b str]>, flat_settings: Option, headers: HeaderMap, @@ -2923,7 +2923,7 @@ impl<'a, 'b> IndicesGet<'a, 'b> { self } #[doc = "Whether wildcard expressions should get expanded to open or closed indices (default: open)"] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -2996,7 +2996,7 @@ impl<'a, 'b> IndicesGet<'a, 'b> { #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", serialize_with = "crate::client::serialize_coll_qs" @@ -3098,7 +3098,7 @@ pub struct IndicesGetAlias<'a, 'b> { parts: IndicesGetAliasParts<'b>, allow_no_indices: Option, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, filter_path: Option<&'b [&'b str]>, headers: HeaderMap, human: Option, @@ -3136,7 +3136,7 @@ impl<'a, 'b> IndicesGetAlias<'a, 'b> { self } #[doc = "Whether to expand wildcard expression to concrete indices that are open, closed or both."] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -3189,7 +3189,7 @@ impl<'a, 'b> IndicesGetAlias<'a, 'b> { #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", serialize_with = "crate::client::serialize_coll_qs" @@ -3295,7 +3295,7 @@ pub struct IndicesGetFieldMapping<'a, 'b> { parts: IndicesGetFieldMappingParts<'b>, allow_no_indices: Option, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, filter_path: Option<&'b [&'b str]>, headers: HeaderMap, human: Option, @@ -3337,7 +3337,7 @@ impl<'a, 'b> IndicesGetFieldMapping<'a, 'b> { self } #[doc = "Whether to expand wildcard expression to concrete indices that are open, closed or both."] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -3400,7 +3400,7 @@ impl<'a, 'b> IndicesGetFieldMapping<'a, 'b> { #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", serialize_with = "crate::client::serialize_coll_qs" @@ -3496,7 +3496,7 @@ pub struct IndicesGetMapping<'a, 'b> { parts: IndicesGetMappingParts<'b>, allow_no_indices: Option, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, filter_path: Option<&'b [&'b str]>, headers: HeaderMap, human: Option, @@ -3538,7 +3538,7 @@ impl<'a, 'b> IndicesGetMapping<'a, 'b> { self } #[doc = "Whether to expand wildcard expression to concrete indices that are open, closed or both."] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -3601,7 +3601,7 @@ impl<'a, 'b> IndicesGetMapping<'a, 'b> { #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", serialize_with = "crate::client::serialize_coll_qs" @@ -3697,7 +3697,7 @@ pub struct IndicesGetSettings<'a, 'b> { parts: IndicesGetSettingsParts<'b>, allow_no_indices: Option, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, filter_path: Option<&'b [&'b str]>, flat_settings: Option, headers: HeaderMap, @@ -3741,7 +3741,7 @@ impl<'a, 'b> IndicesGetSettings<'a, 'b> { self } #[doc = "Whether to expand wildcard expression to concrete indices that are open, closed or both."] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -3809,7 +3809,7 @@ impl<'a, 'b> IndicesGetSettings<'a, 'b> { #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", serialize_with = "crate::client::serialize_coll_qs" @@ -4046,7 +4046,7 @@ pub struct IndicesGetUpgrade<'a, 'b> { parts: IndicesGetUpgradeParts<'b>, allow_no_indices: Option, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, filter_path: Option<&'b [&'b str]>, headers: HeaderMap, human: Option, @@ -4082,7 +4082,7 @@ impl<'a, 'b> IndicesGetUpgrade<'a, 'b> { self } #[doc = "Whether to expand wildcard expression to concrete indices that are open, closed or both."] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -4130,7 +4130,7 @@ impl<'a, 'b> IndicesGetUpgrade<'a, 'b> { #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", serialize_with = "crate::client::serialize_coll_qs" @@ -4194,7 +4194,7 @@ pub struct IndicesOpen<'a, 'b, B> { allow_no_indices: Option, body: Option, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, filter_path: Option<&'b [&'b str]>, headers: HeaderMap, human: Option, @@ -4263,7 +4263,7 @@ where self } #[doc = "Whether to expand wildcard expression to concrete indices that are open, closed or both."] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -4326,7 +4326,7 @@ where #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", serialize_with = "crate::client::serialize_coll_qs" @@ -4581,7 +4581,7 @@ pub struct IndicesPutMapping<'a, 'b, B> { allow_no_indices: Option, body: Option, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, filter_path: Option<&'b [&'b str]>, headers: HeaderMap, human: Option, @@ -4650,7 +4650,7 @@ where self } #[doc = "Whether to expand wildcard expression to concrete indices that are open, closed or both."] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -4713,7 +4713,7 @@ where #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", serialize_with = "crate::client::serialize_coll_qs" @@ -4789,7 +4789,7 @@ pub struct IndicesPutSettings<'a, 'b, B> { allow_no_indices: Option, body: Option, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, filter_path: Option<&'b [&'b str]>, flat_settings: Option, headers: HeaderMap, @@ -4861,7 +4861,7 @@ where self } #[doc = "Whether to expand wildcard expression to concrete indices that are open, closed or both."] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -4929,7 +4929,7 @@ where #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", serialize_with = "crate::client::serialize_coll_qs" @@ -5351,7 +5351,7 @@ pub struct IndicesRefresh<'a, 'b, B> { allow_no_indices: Option, body: Option, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, filter_path: Option<&'b [&'b str]>, headers: HeaderMap, human: Option, @@ -5411,7 +5411,7 @@ where self } #[doc = "Whether to expand wildcard expression to concrete indices that are open, closed or both."] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -5462,7 +5462,7 @@ where #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", serialize_with = "crate::client::serialize_coll_qs" @@ -5526,7 +5526,7 @@ pub struct IndicesReloadSearchAnalyzers<'a, 'b, B> { allow_no_indices: Option, body: Option, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, filter_path: Option<&'b [&'b str]>, headers: HeaderMap, human: Option, @@ -5586,7 +5586,7 @@ where self } #[doc = "Whether to expand wildcard expression to concrete indices that are open, closed or both."] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -5637,7 +5637,7 @@ where #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", serialize_with = "crate::client::serialize_coll_qs" @@ -5906,7 +5906,7 @@ pub struct IndicesSegments<'a, 'b> { parts: IndicesSegmentsParts<'b>, allow_no_indices: Option, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, filter_path: Option<&'b [&'b str]>, headers: HeaderMap, human: Option, @@ -5944,7 +5944,7 @@ impl<'a, 'b> IndicesSegments<'a, 'b> { self } #[doc = "Whether to expand wildcard expression to concrete indices that are open, closed or both."] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -5997,7 +5997,7 @@ impl<'a, 'b> IndicesSegments<'a, 'b> { #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", serialize_with = "crate::client::serialize_coll_qs" @@ -6066,7 +6066,7 @@ pub struct IndicesShardStores<'a, 'b> { parts: IndicesShardStoresParts<'b>, allow_no_indices: Option, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, filter_path: Option<&'b [&'b str]>, headers: HeaderMap, human: Option, @@ -6104,7 +6104,7 @@ impl<'a, 'b> IndicesShardStores<'a, 'b> { self } #[doc = "Whether to expand wildcard expression to concrete indices that are open, closed or both."] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -6157,7 +6157,7 @@ impl<'a, 'b> IndicesShardStores<'a, 'b> { #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", serialize_with = "crate::client::serialize_coll_qs" @@ -6613,7 +6613,7 @@ pub struct IndicesStats<'a, 'b> { parts: IndicesStatsParts<'b>, completion_fields: Option<&'b [&'b str]>, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, fielddata_fields: Option<&'b [&'b str]>, fields: Option<&'b [&'b str]>, filter_path: Option<&'b [&'b str]>, @@ -6663,7 +6663,7 @@ impl<'a, 'b> IndicesStats<'a, 'b> { self } #[doc = "Whether to expand wildcard expression to concrete indices that are open, closed or both."] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -6749,7 +6749,7 @@ impl<'a, 'b> IndicesStats<'a, 'b> { #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "fielddata_fields", serialize_with = "crate::client::serialize_coll_qs" @@ -6836,7 +6836,7 @@ pub struct IndicesUnfreeze<'a, 'b, B> { allow_no_indices: Option, body: Option, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, filter_path: Option<&'b [&'b str]>, headers: HeaderMap, human: Option, @@ -6905,7 +6905,7 @@ where self } #[doc = "Whether to expand wildcard expression to concrete indices that are open, closed or both."] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -6968,7 +6968,7 @@ where #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", serialize_with = "crate::client::serialize_coll_qs" @@ -7198,7 +7198,7 @@ pub struct IndicesUpgrade<'a, 'b, B> { allow_no_indices: Option, body: Option, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, filter_path: Option<&'b [&'b str]>, headers: HeaderMap, human: Option, @@ -7264,7 +7264,7 @@ where self } #[doc = "Whether to expand wildcard expression to concrete indices that are open, closed or both."] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -7322,7 +7322,7 @@ where #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", serialize_with = "crate::client::serialize_coll_qs" @@ -7413,7 +7413,7 @@ pub struct IndicesValidateQuery<'a, 'b, B> { default_operator: Option, df: Option<&'b str>, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, explain: Option, filter_path: Option<&'b [&'b str]>, headers: HeaderMap, @@ -7520,7 +7520,7 @@ where self } #[doc = "Whether to expand wildcard expression to concrete indices that are open, closed or both."] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -7601,7 +7601,7 @@ where #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde(rename = "explain")] explain: Option, #[serde( diff --git a/elasticsearch/src/generated/root.rs b/elasticsearch/src/generated/root.rs index 0b93b35e..0d5bda56 100644 --- a/elasticsearch/src/generated/root.rs +++ b/elasticsearch/src/generated/root.rs @@ -478,7 +478,7 @@ pub struct Count<'a, 'b, B> { default_operator: Option, df: Option<&'b str>, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, filter_path: Option<&'b [&'b str]>, headers: HeaderMap, human: Option, @@ -587,7 +587,7 @@ where self } #[doc = "Whether to expand wildcard expression to concrete indices that are open, closed or both."] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -681,7 +681,7 @@ where #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", serialize_with = "crate::client::serialize_coll_qs" @@ -1231,7 +1231,7 @@ pub struct DeleteByQuery<'a, 'b, B> { default_operator: Option, df: Option<&'b str>, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, filter_path: Option<&'b [&'b str]>, from: Option, headers: HeaderMap, @@ -1406,7 +1406,7 @@ where self } #[doc = "Whether to expand wildcard expression to concrete indices that are open, closed or both."] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -1587,7 +1587,7 @@ where #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", serialize_with = "crate::client::serialize_coll_qs" @@ -2783,7 +2783,7 @@ pub struct FieldCaps<'a, 'b, B> { allow_no_indices: Option, body: Option, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, fields: Option<&'b [&'b str]>, filter_path: Option<&'b [&'b str]>, headers: HeaderMap, @@ -2849,7 +2849,7 @@ where self } #[doc = "Whether to expand wildcard expression to concrete indices that are open, closed or both."] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -2910,7 +2910,7 @@ where #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde(rename = "fields", serialize_with = "crate::client::serialize_coll_qs")] fields: Option<&'b [&'b str]>, #[serde( @@ -5949,7 +5949,7 @@ pub struct Search<'a, 'b, B> { df: Option<&'b str>, docvalue_fields: Option<&'b [&'b str]>, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, explain: Option, filter_path: Option<&'b [&'b str]>, from: Option, @@ -6170,7 +6170,7 @@ where self } #[doc = "Whether to expand wildcard expression to concrete indices that are open, closed or both."] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -6395,7 +6395,7 @@ where #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde(rename = "explain")] explain: Option, #[serde( @@ -6563,7 +6563,7 @@ pub struct SearchShards<'a, 'b, B> { allow_no_indices: Option, body: Option, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, filter_path: Option<&'b [&'b str]>, headers: HeaderMap, human: Option, @@ -6632,7 +6632,7 @@ where self } #[doc = "Whether to expand wildcard expression to concrete indices that are open, closed or both."] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -6698,7 +6698,7 @@ where #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", serialize_with = "crate::client::serialize_coll_qs" @@ -6787,7 +6787,7 @@ pub struct SearchTemplate<'a, 'b, B> { allow_no_indices: Option, body: Option, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, explain: Option, filter_path: Option<&'b [&'b str]>, headers: HeaderMap, @@ -6874,7 +6874,7 @@ where self } #[doc = "Whether to expand wildcard expression to concrete indices that are open, closed or both."] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -6970,7 +6970,7 @@ where #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde(rename = "explain")] explain: Option, #[serde( @@ -7663,7 +7663,7 @@ pub struct UpdateByQuery<'a, 'b, B> { default_operator: Option, df: Option<&'b str>, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, filter_path: Option<&'b [&'b str]>, from: Option, headers: HeaderMap, @@ -7851,7 +7851,7 @@ where self } #[doc = "Whether to expand wildcard expression to concrete indices that are open, closed or both."] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -8044,7 +8044,7 @@ where #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", serialize_with = "crate::client::serialize_coll_qs" From 0ee11bafc121e1eb160eadf6f1e263f96abb7822 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Fri, 6 Mar 2020 16:18:08 +1000 Subject: [PATCH 027/127] Extract enum creation out into local fn --- yaml_test_runner/src/generator.rs | 76 +++++++++++++++++++++---------- 1 file changed, 53 insertions(+), 23 deletions(-) diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 9d1f2946..5b8cf465 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -43,8 +43,8 @@ struct ApiCall<'a> { ignore: Option, } -// TODO: continue moving API call parts to this impl impl<'a> ApiCall<'a> { + /// Try to create an API call pub fn try_from( api: &'a Api, endpoint: &'a ApiEndpoint, @@ -117,29 +117,48 @@ impl<'a> ApiCall<'a> { let kind = ty.ty; + fn create_enum(n: &str, s: &str) -> Tokens { + let e: String = n.to_pascal_case(); + let enum_name = syn::Ident::from(e.as_str()); + let variant = if s.is_empty() { + // TODO: Should we simply omit empty Refresh tests? + if e == "Refresh" { + syn::Ident::from("True") + } else if e == "Size" { + syn::Ident::from("Unspecified") + } else { + //TODO: propagate as Err + panic!(format!("Unhandled empty value for {}", &e)); + } + } else { + syn::Ident::from(s.to_pascal_case()) + }; + + quote!(#enum_name::#variant) + } + match v { Yaml::String(ref s) => { match kind { TypeKind::Enum => { - let e: String = n.to_pascal_case(); - let enum_name = syn::Ident::from(e.as_str()); - let variant = if s.is_empty() { - // TODO: Should we simply omit empty Refresh tests? - if e == "Refresh" { - syn::Ident::from("True") - } else if e == "Size" { - syn::Ident::from("Unspecified") - } else { - //TODO: propagate as Err - panic!(format!("Unhandled empty value for {}", &e)); - } + if n == &"expand_wildcards" { + // expand_wildcards might be defined as a comma-separated + // string. e.g. + let idents: Vec = s.split(',') + .collect::>() + .iter() + .map(|e| create_enum(n,e)) + .collect(); + + tokens.append(quote! { + .#param_ident(&[#(#idents),*]) + }); } else { - syn::Ident::from(s.to_pascal_case().replace("_", "")) - }; - - tokens.append(quote! { - .#param_ident(#enum_name::#variant) - }) + let e = create_enum(n, s.as_str()); + tokens.append(quote! { + .#param_ident(#e) + }); + } } TypeKind::List => { let values: Vec<&str> = s.split(',').collect(); @@ -233,7 +252,7 @@ impl<'a> ApiCall<'a> { }, Yaml::Array(arr) => { // only support param string arrays - let result: Vec<_> = arr + let result: Vec<&String> = arr .iter() .map(|i| match i { Yaml::String(s) => Ok(s), @@ -245,9 +264,20 @@ impl<'a> ApiCall<'a> { .filter_map(Result::ok) .collect(); - tokens.append(quote! { - .#param_ident(&[#(#result),*]) - }); + if n == &"expand_wildcards" { + let result : Vec = result + .iter() + .map(|s| create_enum(n, s.as_str())) + .collect(); + + tokens.append(quote! { + .#param_ident(&[#(#result),*]) + }); + } else { + tokens.append(quote! { + .#param_ident(&[#(#result),*]) + }); + } } _ => println!("Unsupported value {:?}", v), } From b10d05e6a2e06e3462bd1934e20b69e18372b567 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Fri, 6 Mar 2020 17:28:15 +1000 Subject: [PATCH 028/127] Fix category_id represented as string See https://github.com/elastic/elasticsearch/blob/0d38626d8e6e9e2620a7a446b617a2ac42852461/x-pack/plugin/src/test/resources/rest-api-spec/test/ml/jobs_get_result_categories.yml#L115 --- yaml_test_runner/src/generator.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 5b8cf465..d8338aa0 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -379,6 +379,10 @@ impl<'a> ApiCall<'a> { let values: Vec<&str> = s.split(',').collect(); Ok(quote! { &[#(#values),*] }) }, + TypeKind::Long => { + let l = s.parse::().unwrap(); + Ok(quote! { #l }) + }, _ => Ok(quote! { #s }), }, Yaml::Boolean(b) => { From c0e34ba953ec76cd38f9c824a8d22c224367e3ae Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Mon, 9 Mar 2020 09:52:57 +1000 Subject: [PATCH 029/127] Cehck enum options contains passed values --- yaml_test_runner/src/generator.rs | 61 +++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index d8338aa0..9da3f777 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -117,24 +117,27 @@ impl<'a> ApiCall<'a> { let kind = ty.ty; - fn create_enum(n: &str, s: &str) -> Tokens { - let e: String = n.to_pascal_case(); + fn create_enum(enum_name: &str, variant: &str, options: &[serde_json::Value]) -> Result { + if !variant.is_empty() && !options.contains(&serde_json::Value::String(variant.to_owned())) { + return Err(failure::err_msg(format!("options {:?} does not contain value {}", &options, variant))); + } + + let e: String = enum_name.to_pascal_case(); let enum_name = syn::Ident::from(e.as_str()); - let variant = if s.is_empty() { + let variant = if variant.is_empty() { // TODO: Should we simply omit empty Refresh tests? if e == "Refresh" { syn::Ident::from("True") } else if e == "Size" { syn::Ident::from("Unspecified") } else { - //TODO: propagate as Err - panic!(format!("Unhandled empty value for {}", &e)); + return Err(failure::err_msg(format!("Unhandled empty value for {}", &e))); } } else { - syn::Ident::from(s.to_pascal_case()) + syn::Ident::from(variant.to_pascal_case()) }; - quote!(#enum_name::#variant) + Ok(quote!(#enum_name::#variant)) } match v { @@ -144,17 +147,27 @@ impl<'a> ApiCall<'a> { if n == &"expand_wildcards" { // expand_wildcards might be defined as a comma-separated // string. e.g. - let idents: Vec = s.split(',') + let idents: Vec> = s.split(',') .collect::>() .iter() - .map(|e| create_enum(n,e)) + .map(|e| create_enum(n,e, &ty.options)) .collect(); - tokens.append(quote! { - .#param_ident(&[#(#idents),*]) - }); + match ok_or_accumulate(&idents, 0) { + Ok(_) => { + let idents: Vec = idents + .into_iter() + .filter_map(Result::ok) + .collect(); + + tokens.append(quote! { + .#param_ident(&[#(#idents),*]) + }); + }, + Err(e) => return Err(failure::err_msg(e)) + } } else { - let e = create_enum(n, s.as_str()); + let e = create_enum(n, s.as_str(), &ty.options)?; tokens.append(quote! { .#param_ident(#e) }); @@ -163,7 +176,7 @@ impl<'a> ApiCall<'a> { TypeKind::List => { let values: Vec<&str> = s.split(',').collect(); tokens.append(quote! { - .#param_ident(&[#(#values),*]) + .#param_ident(&[#(#values),*]) }) } TypeKind::Boolean => { @@ -265,14 +278,24 @@ impl<'a> ApiCall<'a> { .collect(); if n == &"expand_wildcards" { - let result : Vec = result + let result : Vec> = result .iter() - .map(|s| create_enum(n, s.as_str())) + .map(|s| create_enum(n, s.as_str(), &ty.options)) .collect(); - tokens.append(quote! { - .#param_ident(&[#(#result),*]) - }); + match ok_or_accumulate(&result, 0) { + Ok(_) => { + let result: Vec = result + .into_iter() + .filter_map(Result::ok) + .collect(); + + tokens.append(quote! { + .#param_ident(&[#(#result),*]) + }); + }, + Err(e) => return Err(failure::err_msg(e)) + } } else { tokens.append(quote! { .#param_ident(&[#(#result),*]) From f25cd30ca81347487ab85dcfbb59c71dc0e3b2a2 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Mon, 9 Mar 2020 10:31:20 +1000 Subject: [PATCH 030/127] change track_total_hits to Into --- api_generator/src/generator/code_gen/mod.rs | 19 +++-- .../code_gen/request/request_builder.rs | 36 +++++++-- .../generator/code_gen/url/enum_builder.rs | 2 +- elasticsearch/src/generated/root.rs | 4 +- yaml_test_runner/src/generator.rs | 81 +++++++++---------- 5 files changed, 86 insertions(+), 56 deletions(-) diff --git a/api_generator/src/generator/code_gen/mod.rs b/api_generator/src/generator/code_gen/mod.rs index 056e1dae..0ea61702 100644 --- a/api_generator/src/generator/code_gen/mod.rs +++ b/api_generator/src/generator/code_gen/mod.rs @@ -97,7 +97,7 @@ impl GetPath for syn::Ty { fn get_path(&self) -> &syn::Path { match *self { syn::Ty::Path(_, ref p) => &p, - _ => panic!("Only path types are supported."), + ref p => panic!(format!("Expected syn::Ty::Path, but found {:?}", p)), } } } @@ -119,7 +119,8 @@ impl GetIdent for T { } /// Gets the Ty syntax token for a TypeKind -fn typekind_to_ty(name: &str, kind: TypeKind, required: bool) -> syn::Ty { +/// TODO: This function is serving too many purposes. Refactor it +fn typekind_to_ty(name: &str, kind: TypeKind, required: bool, fn_arg: bool) -> syn::Ty { let mut v = String::new(); if !required { v.push_str("Option<"); @@ -132,7 +133,7 @@ fn typekind_to_ty(name: &str, kind: TypeKind, required: bool) -> syn::Ty { v.push_str("&'b ["); v.push_str(str_type); v.push_str("]"); - }, + } TypeKind::Enum => match name { // opened https://github.com/elastic/elasticsearch/issues/53212 // to discuss whether this really should be a collection @@ -141,13 +142,19 @@ fn typekind_to_ty(name: &str, kind: TypeKind, required: bool) -> syn::Ty { v.push_str("&'b ["); v.push_str(name.to_pascal_case().as_str()); v.push_str("]"); - }, - _ => v.push_str(name.to_pascal_case().as_str()) + } + _ => v.push_str(name.to_pascal_case().as_str()), }, TypeKind::String => v.push_str(str_type), TypeKind::Text => v.push_str(str_type), TypeKind::Boolean => match name { - "track_total_hits" => v.push_str(name.to_pascal_case().as_str()), + "track_total_hits" => { + if fn_arg { + v.push_str(format!("Into<{}>", name.to_pascal_case()).as_str()) + } else { + v.push_str(name.to_pascal_case().as_str()) + } + } _ => v.push_str("bool"), }, TypeKind::Number => v.push_str("i64"), diff --git a/api_generator/src/generator/code_gen/request/request_builder.rs b/api_generator/src/generator/code_gen/request/request_builder.rs index 5947b5c2..1ffea4ff 100644 --- a/api_generator/src/generator/code_gen/request/request_builder.rs +++ b/api_generator/src/generator/code_gen/request/request_builder.rs @@ -6,7 +6,7 @@ use inflector::Inflector; use quote::{ToTokens, Tokens}; use reqwest::Url; use std::{collections::BTreeMap, str}; -use syn::{Field, FieldValue, ImplItem}; +use syn::{Field, FieldValue, ImplItem, TraitBoundModifier, TyParamBound}; /// Builder that generates the AST for a request builder struct pub struct RequestBuilder<'a> { @@ -310,10 +310,36 @@ impl<'a> RequestBuilder<'a> { /// Creates the AST for a builder fn for a builder impl fn create_impl_fn(f: (&String, &Type)) -> syn::ImplItem { let name = valid_name(&f.0).to_lowercase(); + let (ty, value_ident, fn_generics) = { + let ty = typekind_to_ty(&f.0, f.1.ty, true, true); + match ty { + syn::Ty::Path(ref _q, ref p) => { + if p.get_ident().as_ref() == "Into" { + let ty = syn::parse_type("T").unwrap(); + let ident = code_gen::ident(format!("{}.into()", &name)); + let ty_param = syn::TyParam { + ident: code_gen::ident("T"), + default: None, + attrs: vec![], + bounds: vec![TyParamBound::Trait( + syn::PolyTraitRef { + trait_ref: p.clone(), + bound_lifetimes: vec![], + }, + TraitBoundModifier::None, + )], + }; + let generics = generics(vec![], vec![ty_param]); + (ty, ident, generics) + } else { + (ty, ident(&name), generics_none()) + } + } + _ => (ty, ident(&name), generics_none()), + } + }; let impl_ident = ident(&name); let field_ident = ident(&name); - let value_ident = ident(&name); - let ty = typekind_to_ty(&f.0, f.1.ty, true); let doc_attr = match &f.1.description { Some(docs) => vec![doc(docs)], _ => vec![], @@ -340,7 +366,7 @@ impl<'a> RequestBuilder<'a> { output: syn::FunctionRetTy::Ty(code_gen::ty("Self")), variadic: false, }, - generics: generics_none(), + generics: fn_generics, }, // generates a fn body of the form // -------- @@ -599,7 +625,7 @@ impl<'a> RequestBuilder<'a> { ident: Some(ident(valid_name(&f.0).to_lowercase())), vis: syn::Visibility::Inherited, attrs: vec![], - ty: typekind_to_ty(&f.0, f.1.ty, false), + ty: typekind_to_ty(&f.0, f.1.ty, false, false), } } diff --git a/api_generator/src/generator/code_gen/url/enum_builder.rs b/api_generator/src/generator/code_gen/url/enum_builder.rs index d1b6e7d8..89864878 100644 --- a/api_generator/src/generator/code_gen/url/enum_builder.rs +++ b/api_generator/src/generator/code_gen/url/enum_builder.rs @@ -122,7 +122,7 @@ impl<'a> EnumBuilder<'a> { ident: None, vis: syn::Visibility::Inherited, attrs: vec![], - ty: typekind_to_ty(p, ty, true), + ty: typekind_to_ty(p, ty, true, false), } }) .collect(), diff --git a/elasticsearch/src/generated/root.rs b/elasticsearch/src/generated/root.rs index 0d5bda56..36094119 100644 --- a/elasticsearch/src/generated/root.rs +++ b/elasticsearch/src/generated/root.rs @@ -6330,8 +6330,8 @@ where self } #[doc = "Indicate if the number of documents that match the query should be tracked"] - pub fn track_total_hits(mut self, track_total_hits: TrackTotalHits) -> Self { - self.track_total_hits = Some(track_total_hits); + pub fn track_total_hits>(mut self, track_total_hits: T) -> Self { + self.track_total_hits = Some(track_total_hits.into()); self } #[doc = "Specify whether aggregation and suggester names should be prefixed by their respective types in the response"] diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 9da3f777..0d4dbc13 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -117,9 +117,18 @@ impl<'a> ApiCall<'a> { let kind = ty.ty; - fn create_enum(enum_name: &str, variant: &str, options: &[serde_json::Value]) -> Result { - if !variant.is_empty() && !options.contains(&serde_json::Value::String(variant.to_owned())) { - return Err(failure::err_msg(format!("options {:?} does not contain value {}", &options, variant))); + fn create_enum( + enum_name: &str, + variant: &str, + options: &[serde_json::Value], + ) -> Result { + if !variant.is_empty() + && !options.contains(&serde_json::Value::String(variant.to_owned())) + { + return Err(failure::err_msg(format!( + "options {:?} does not contain value {}", + &options, variant + ))); } let e: String = enum_name.to_pascal_case(); @@ -131,7 +140,10 @@ impl<'a> ApiCall<'a> { } else if e == "Size" { syn::Ident::from("Unspecified") } else { - return Err(failure::err_msg(format!("Unhandled empty value for {}", &e))); + return Err(failure::err_msg(format!( + "Unhandled empty value for {}", + &e + ))); } } else { syn::Ident::from(variant.to_pascal_case()) @@ -147,10 +159,11 @@ impl<'a> ApiCall<'a> { if n == &"expand_wildcards" { // expand_wildcards might be defined as a comma-separated // string. e.g. - let idents: Vec> = s.split(',') + let idents: Vec> = s + .split(',') .collect::>() .iter() - .map(|e| create_enum(n,e, &ty.options)) + .map(|e| create_enum(n, e, &ty.options)) .collect(); match ok_or_accumulate(&idents, 0) { @@ -163,8 +176,8 @@ impl<'a> ApiCall<'a> { tokens.append(quote! { .#param_ident(&[#(#idents),*]) }); - }, - Err(e) => return Err(failure::err_msg(e)) + } + Err(e) => return Err(failure::err_msg(e)), } } else { let e = create_enum(n, s.as_str(), &ty.options)?; @@ -212,15 +225,9 @@ impl<'a> ApiCall<'a> { }) } _ => { - if n == &"track_total_hits" { - tokens.append(quote! { - .#param_ident(#b.into()) - }); - } else { - tokens.append(quote! { - .#param_ident(#b) - }); - } + tokens.append(quote! { + .#param_ident(#b) + }); } }, Yaml::Integer(ref i) => match kind { @@ -252,15 +259,9 @@ impl<'a> ApiCall<'a> { }); } _ => { - if n == &"track_total_hits" { - tokens.append(quote! { - .#param_ident(#i.into()) - }); - } else { - tokens.append(quote! { - .#param_ident(#i) - }); - } + tokens.append(quote! { + .#param_ident(#i) + }); } }, Yaml::Array(arr) => { @@ -278,23 +279,21 @@ impl<'a> ApiCall<'a> { .collect(); if n == &"expand_wildcards" { - let result : Vec> = result + let result: Vec> = result .iter() .map(|s| create_enum(n, s.as_str(), &ty.options)) .collect(); match ok_or_accumulate(&result, 0) { Ok(_) => { - let result: Vec = result - .into_iter() - .filter_map(Result::ok) - .collect(); + let result: Vec = + result.into_iter().filter_map(Result::ok).collect(); tokens.append(quote! { .#param_ident(&[#(#result),*]) }); - }, - Err(e) => return Err(failure::err_msg(e)) + } + Err(e) => return Err(failure::err_msg(e)), } } else { tokens.append(quote! { @@ -401,11 +400,11 @@ impl<'a> ApiCall<'a> { TypeKind::List => { let values: Vec<&str> = s.split(',').collect(); Ok(quote! { &[#(#values),*] }) - }, + } TypeKind::Long => { let l = s.parse::().unwrap(); Ok(quote! { #l }) - }, + } _ => Ok(quote! { #s }), }, Yaml::Boolean(b) => { @@ -413,14 +412,12 @@ impl<'a> ApiCall<'a> { Ok(quote! { #s }) } Yaml::Integer(i) => match ty.ty { - TypeKind::Long => { - Ok(quote!{ #i }) - }, + TypeKind::Long => Ok(quote! { #i }), _ => { let s = i.to_string(); Ok(quote! { #s }) - }, - } + } + }, Yaml::Array(arr) => { // only support param string arrays let result: Vec<_> = arr @@ -447,8 +444,8 @@ impl<'a> ApiCall<'a> { TypeKind::String => { let s = result.iter().join(","); Ok(quote! { #s }) - }, - _ => Ok(quote! { &[#(#result),*] }) + } + _ => Ok(quote! { &[#(#result),*] }), } } Err(e) => Err(failure::err_msg(e)), From 6897cb30cc70c8a93341597b512c6af8e463d182 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Mon, 9 Mar 2020 11:32:05 +1000 Subject: [PATCH 031/127] Ensure matching path for collected API call parts --- yaml_test_runner/src/generator.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 0d4dbc13..10c8e859 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -324,6 +324,15 @@ impl<'a> ApiCall<'a> { // Enum variants containing no URL parts where there is only a single API URL, // are not required to be passed in the API if parts.is_empty() { + let param_counts = endpoint.url.paths + .iter() + .map(|p| p.path.params().len()) + .collect::>(); + + if !param_counts.contains(&0) { + return Err(failure::err_msg(format!("No path for '{}' API with no URL parts", api_call))); + } + return match endpoint.url.paths.len() { 1 => Ok(None), _ => Ok(Some(quote!(#enum_name::None))), @@ -331,7 +340,14 @@ impl<'a> ApiCall<'a> { } let path = match endpoint.url.paths.len() { - 1 => Some(&endpoint.url.paths[0]), + 1 => { + let path = &endpoint.url.paths[0]; + if path.path.params().len() == parts.len() { + Some(path) + } else { + None + } + }, _ => { // get the matching path parts let matching_path_parts = endpoint From 673583d44559e35cdec224a6b267e582a685d20d Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Mon, 9 Mar 2020 11:42:52 +1000 Subject: [PATCH 032/127] Handle string -> integer param --- yaml_test_runner/src/generator.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 10c8e859..5cf9bdfe 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -203,6 +203,12 @@ impl<'a> ApiCall<'a> { tokens.append(quote! { .#param_ident(#f) }); + }, + TypeKind::Integer => { + let i = s.parse::()?; + tokens.append(quote! { + .#param_ident(#i) + }); } _ => tokens.append(quote! { .#param_ident(#s) From b10f3ce4f086ef0c28e56dd2c58ba313ae59cfe0 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Mon, 9 Mar 2020 12:38:31 +1000 Subject: [PATCH 033/127] fix clippy warnings --- api_generator/src/generator/mod.rs | 2 +- yaml_test_runner/src/generator.rs | 45 +++++++++++++++--------------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/api_generator/src/generator/mod.rs b/api_generator/src/generator/mod.rs index cce69750..b6dfa9d9 100644 --- a/api_generator/src/generator/mod.rs +++ b/api_generator/src/generator/mod.rs @@ -40,7 +40,7 @@ impl Api { /// /// The REST API specs model only the stable APIs /// currently, so no endpoint will be found for experimental or beta APIs - pub fn endpoint_for_api_call<'a>(&self, api_call: &str) -> Option<&ApiEndpoint> { + pub fn endpoint_for_api_call(&self, api_call: &str) -> Option<&ApiEndpoint> { let api_call_path: Vec<&str> = api_call.split('.').collect(); match api_call_path.len() { 1 => self.root.get(api_call_path[0]), diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 5cf9bdfe..68255d3e 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -10,7 +10,7 @@ use std::fs::{File, OpenOptions}; use std::io::Write; use std::path::PathBuf; use yaml_rust::{ - yaml::{Array, Hash}, + yaml::{Hash}, Yaml, YamlEmitter, YamlLoader, }; @@ -78,8 +78,8 @@ impl<'a> ApiCall<'a> { let parts = Self::generate_parts(api_call, endpoint, &parts)?; let params = Self::generate_params(api, endpoint, ¶ms)?; let function = syn::Ident::from(api_call.replace(".", "().")); - let namespace: Option<&str> = if api_call.contains(".") { - Some(api_call.splitn(2, ".").collect::>()[0]) + let namespace: Option<&str> = if api_call.contains('.') { + Some(api_call.splitn(2, '.').collect::>()[0]) } else { None }; @@ -400,8 +400,7 @@ impl<'a> ApiCall<'a> { }; let part_tokens: Vec> = parts - .clone() - .into_iter() + .iter() // don't rely on URL parts being ordered in the yaml test .sorted_by(|(p, _), (p2, _)| { let f = path_parts.iter().position(|x| x == p).unwrap(); @@ -808,7 +807,7 @@ fn generate_fixture(name: &str, tokens: &Option) -> (Option, Opt } } -fn parse_steps(api: &Api, test: &mut YamlTest, steps: &Array) -> Result { +fn parse_steps(api: &Api, test: &mut YamlTest, steps: &[Yaml]) -> Result { let mut tokens = Tokens::new(); for step in steps { if let Some(hash) = step.as_hash() { @@ -817,22 +816,22 @@ fn parse_steps(api: &Api, test: &mut YamlTest, steps: &Array) -> Result {} + ("skip", Yaml::Hash(_h)) => {} ("do", Yaml::Hash(h)) => parse_do(api, test, h, &mut tokens)?, - ("set", Yaml::Hash(h)) => {} - ("transform_and_set", Yaml::Hash(h)) => {} + ("set", Yaml::Hash(_h)) => {} + ("transform_and_set", Yaml::Hash(_h)) => {} ("match", Yaml::Hash(h)) => parse_match(api, h, &mut tokens)?, - ("contains", Yaml::Hash(h)) => {} - ("is_true", Yaml::Hash(h)) => {} - ("is_true", Yaml::String(s)) => {} - ("is_false", Yaml::Hash(h)) => {} - ("is_false", Yaml::String(s)) => {} - ("length", Yaml::Hash(h)) => {} - ("eq", Yaml::Hash(h)) => {} - ("gte", Yaml::Hash(h)) => {} - ("lte", Yaml::Hash(h)) => {} - ("gt", Yaml::Hash(h)) => {} - ("lt", Yaml::Hash(h)) => {} + ("contains", Yaml::Hash(_h)) => {} + ("is_true", Yaml::Hash(_h)) => {} + ("is_true", Yaml::String(_s)) => {} + ("is_false", Yaml::Hash(_h)) => {} + ("is_false", Yaml::String(_s)) => {} + ("length", Yaml::Hash(_h)) => {} + ("eq", Yaml::Hash(_h)) => {} + ("gte", Yaml::Hash(_h)) => {} + ("lte", Yaml::Hash(_h)) => {} + ("gt", Yaml::Hash(_h)) => {} + ("lt", Yaml::Hash(_h)) => {} (op, _) => return Err(failure::err_msg(format!("unknown step operation: {}", op))), } } else { @@ -843,7 +842,7 @@ fn parse_steps(api: &Api, test: &mut YamlTest, steps: &Array) -> Result Result<(), failure::Error> { +fn parse_match(_api: &Api, _hash: &Hash, _tokens: &mut Tokens) -> Result<(), failure::Error> { // TODO: implement Ok(()) } @@ -898,7 +897,7 @@ fn parse_do( parts, params, body, - ignore, + ignore: _ignore, } = ApiCall::try_from(api, endpoint, hash.unwrap())?; // capture any namespaces used in the test @@ -935,7 +934,7 @@ fn ok_or_accumulate( indent: usize, ) -> Result<(), failure::Error> { let errs = results - .into_iter() + .iter() .filter_map(|r| r.as_ref().err()) .collect::>(); if errs.is_empty() { From 05a2a268ecf0f6409878907d2f87adf5e1db9e55 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Fri, 13 Mar 2020 14:30:11 +1000 Subject: [PATCH 034/127] Bump runner to 7.6.1-alpha.1 --- yaml_test_runner/Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yaml_test_runner/Cargo.toml b/yaml_test_runner/Cargo.toml index 56ad8cf8..c467d07b 100644 --- a/yaml_test_runner/Cargo.toml +++ b/yaml_test_runner/Cargo.toml @@ -1,14 +1,14 @@ [package] name = "yaml_test_runner" -version = "7.6.0-alpha.1" +version = "7.6.1-alpha.1" edition = "2018" authors = ["Elastic and Contributors"] description = "Generates and runs tests from Elasticsearch's YAML test specs" repository = "https://github.com/elastic/elasticsearch-rs" [dependencies] -elasticsearch = { version = "7.6.0-alpha.1", path = "../elasticsearch" } -api_generator = { version = "7.6.0-alpha.1", path = "../api_generator" } +elasticsearch = { version = "7.6.1-alpha.1", path = "../elasticsearch" } +api_generator = { version = "7.6.1-alpha.1", path = "../api_generator" } clap = "~2" failure = "0.1.6" From dc7134925949ee90f4cccc161afa1225bdc86921 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Fri, 13 Mar 2020 14:57:44 +1000 Subject: [PATCH 035/127] Delete existing YAML tests when downloading --- yaml_test_runner/src/github.rs | 16 ++++++++++++---- yaml_test_runner/src/main.rs | 10 +++++----- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/yaml_test_runner/src/github.rs b/yaml_test_runner/src/github.rs index ad71d054..2ce334c1 100644 --- a/yaml_test_runner/src/github.rs +++ b/yaml_test_runner/src/github.rs @@ -38,7 +38,7 @@ struct GitHubContent { } /// Downloads the yaml tests if not already downloaded -pub fn download_test_suites(token: &str, branch: &str, download_dir: &PathBuf) { +pub fn download_test_suites(token: &str, branch: &str, download_dir: &PathBuf) -> Result<(), failure::Error> { let mut last_downloaded_version = download_dir.clone(); last_downloaded_version.push("last_downloaded_version"); if last_downloaded_version.exists() { @@ -46,7 +46,7 @@ pub fn download_test_suites(token: &str, branch: &str, download_dir: &PathBuf) { .expect("Unable to read last_downloaded_version of yaml tests"); if version == branch { println!("yaml tests for branch {} already downloaded", branch); - return; + return Ok(()); } } @@ -74,15 +74,23 @@ pub fn download_test_suites(token: &str, branch: &str, download_dir: &PathBuf) { .build() .unwrap(); - fs::create_dir_all(download_dir).unwrap(); + // delete existing yaml tests + if download_dir.exists() { + fs::remove_dir_all(&download_dir)?; + } + + fs::create_dir_all(download_dir)?; + for suite in test_suites { - download_tests(&client, &suite, &download_dir).unwrap(); + download_tests(&client, &suite, &download_dir)?; } File::create(last_downloaded_version) .expect("failed to create last_downloaded_version file") .write_all(branch.as_bytes()) .expect("unable to write branch to last_downloaded_version file"); + + Ok(()) } fn download_tests( diff --git a/yaml_test_runner/src/main.rs b/yaml_test_runner/src/main.rs index 783e9606..de466f76 100644 --- a/yaml_test_runner/src/main.rs +++ b/yaml_test_runner/src/main.rs @@ -55,14 +55,14 @@ fn main() -> Result<(), failure::Error> { let download_dir = PathBuf::from("./yaml_test_runner/yaml"); let generated_dir = PathBuf::from("./yaml_test_runner/src/generated"); - github::download_test_suites(token, branch, &download_dir); + github::download_test_suites(token, branch, &download_dir)?; - let mut last_downloaded_version = rest_specs_dir.clone(); - last_downloaded_version.push("last_downloaded_version"); + let mut last_downloaded_rest_spec_version = rest_specs_dir.clone(); + last_downloaded_rest_spec_version.push("last_downloaded_version"); let mut download_rest_specs = true; - if last_downloaded_version.exists() { - let version = fs::read_to_string(last_downloaded_version) + if last_downloaded_rest_spec_version.exists() { + let version = fs::read_to_string(last_downloaded_rest_spec_version) .expect("Could not rest specs last_downloaded version into string"); if version == branch { From 53a435196d71bb951ce6b83a57a58bee8f5b42a6 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Fri, 13 Mar 2020 17:55:04 +1000 Subject: [PATCH 036/127] Initial parse skip implementation WIP. Needs refactoring to pull test components out into a struct such that decisions about skipping, catching, etc, can be handled once all steps are parsed --- api_generator/src/generator/mod.rs | 7 +++ yaml_test_runner/Cargo.toml | 1 + yaml_test_runner/src/generator.rs | 80 ++++++++++++++++++++++++++++-- yaml_test_runner/src/github.rs | 5 +- yaml_test_runner/src/main.rs | 2 +- 5 files changed, 90 insertions(+), 5 deletions(-) diff --git a/api_generator/src/generator/mod.rs b/api_generator/src/generator/mod.rs index b6dfa9d9..354597c3 100644 --- a/api_generator/src/generator/mod.rs +++ b/api_generator/src/generator/mod.rs @@ -35,6 +35,13 @@ pub struct Api { } impl Api { + /// Attempt to parse the version from the commit tag, which typically + /// will be of the form e.g. v7.6.1 + pub fn version(&self) -> Option { + let v = self.commit.trim_start_matches('v'); + semver::Version::parse(v).ok() + } + /// Find the right ApiEndpoint from the REST API specs for the API call /// defined in the YAML test. /// diff --git a/yaml_test_runner/Cargo.toml b/yaml_test_runner/Cargo.toml index c467d07b..aa6f43ba 100644 --- a/yaml_test_runner/Cargo.toml +++ b/yaml_test_runner/Cargo.toml @@ -18,6 +18,7 @@ lazy_static = "1.4.0" quote = "~0.3" regex = "1.3.1" reqwest = "~0.9" +semver = "0.9.0" serde = "~1" serde_yaml = "0.8.11" serde_json = "~1" diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 68255d3e..4a25ccd3 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -816,7 +816,21 @@ fn parse_steps(api: &Api, test: &mut YamlTest, steps: &[Yaml]) -> Result {} + ("skip", Yaml::Hash(h)) => { + match parse_skip(h) { + Ok(skip) => { + if let Some(api_version) = api.version() { + if skip.matches(&api_version) { + let reason = skip.reason.unwrap_or(""); + + // TODO: Communicate this in a different way - Don't use an error. Probably need to push components of a test into its own struct + return Err(failure::err_msg(format!("Skipping test because skip version '{}' are met. {}", skip.version.unwrap(), reason))) + } + } + } + Err(e) => return Err(failure::err_msg(e.to_string())) + } + } ("do", Yaml::Hash(h)) => parse_do(api, test, h, &mut tokens)?, ("set", Yaml::Hash(_h)) => {} ("transform_and_set", Yaml::Hash(_h)) => {} @@ -834,6 +848,8 @@ fn parse_steps(api: &Api, test: &mut YamlTest, steps: &[Yaml]) -> Result {} (op, _) => return Err(failure::err_msg(format!("unknown step operation: {}", op))), } + + } else { return Err(failure::err_msg(format!("{:?} is not a hash", &step))); } @@ -842,8 +858,66 @@ fn parse_steps(api: &Api, test: &mut YamlTest, steps: &[Yaml]) -> Result Result<(), failure::Error> { - // TODO: implement +struct Skip<'a> { + version_requirements: Option, + version: Option<&'a str>, + reason: Option<&'a str>, + features: Option<&'a str>, +} + +impl<'a> Skip<'a> { + pub fn matches(&self, version: &semver::Version) -> bool { + match &self.version_requirements { + Some(r) => r.matches(version), + None => false + } + } +} + +fn parse_skip(hash: &Hash) -> Result { + let version = hash.get(&Yaml::from_str("version")).map_or_else(|| None,|y| y.as_str()); + let reason = hash.get(&Yaml::from_str("reason")).map_or_else(|| None,|y| y.as_str()); + let features = hash.get(&Yaml::from_str("features")).map_or_else(|| None,|y| y.as_str()); + let version_requirements = if let Some(v) = version { + lazy_static!{ + static ref VERSION_REGEX: Regex = Regex::new(r"^([\w\.]+)?\s*?\-\s*?([\w\.]+)?$").unwrap(); + } + + if let Some(c) = VERSION_REGEX.captures(v) { + match (c.get(1), c.get(2)) { + (Some(start), Some(end)) => { + Some(semver::VersionReq::parse(format!(">={},<={}", start.as_str(), end.as_str()).as_ref()).unwrap()) + }, + (Some(start), None) => { + Some(semver::VersionReq::parse(format!(">={}", start.as_str()).as_ref()).unwrap()) + }, + (None, Some(end)) => { + Some(semver::VersionReq::parse(format!("<={}", end.as_str()).as_ref()).unwrap()) + }, + (None, None) => { + None + } + } + } else { + None + } + } else { + None + }; + + Ok(Skip { + version, + version_requirements, + reason, + features + }) +} + +fn parse_match(_api: &Api, _hash: &Hash, tokens: &mut Tokens) -> Result<(), failure::Error> { + + + + Ok(()) } diff --git a/yaml_test_runner/src/github.rs b/yaml_test_runner/src/github.rs index 2ce334c1..c39bde5f 100644 --- a/yaml_test_runner/src/github.rs +++ b/yaml_test_runner/src/github.rs @@ -154,7 +154,7 @@ fn download( fs::create_dir_all(&content_path)?; download(client, &content.url, &content_path)?; } - t => panic!(format!("Unexpected GitHub content type: {}", t)), + t => return Err(DownloadError::InvalidType(format!("Unexpected GitHub content type: {}", t))), } } @@ -165,6 +165,7 @@ fn download( pub enum DownloadError { IoErr(io::Error), HttpError(reqwest::Error), + InvalidType(String) } impl std::fmt::Display for DownloadError { @@ -172,6 +173,7 @@ impl std::fmt::Display for DownloadError { match self { DownloadError::IoErr(err) => write!(f, "IoErr {}", err), DownloadError::HttpError(err) => write!(f, "HttpError {}", err), + DownloadError::InvalidType(s) => write!(f, "InvalidType {}", s), } } } @@ -182,6 +184,7 @@ impl StdError for DownloadError { match self { DownloadError::IoErr(err) => err.description(), DownloadError::HttpError(err) => err.description(), + DownloadError::InvalidType(s) => s.as_ref(), } } } diff --git a/yaml_test_runner/src/main.rs b/yaml_test_runner/src/main.rs index de466f76..458a9c89 100644 --- a/yaml_test_runner/src/main.rs +++ b/yaml_test_runner/src/main.rs @@ -63,7 +63,7 @@ fn main() -> Result<(), failure::Error> { let mut download_rest_specs = true; if last_downloaded_rest_spec_version.exists() { let version = fs::read_to_string(last_downloaded_rest_spec_version) - .expect("Could not rest specs last_downloaded version into string"); + .expect("Could not read rest specs last_downloaded version into string"); if version == branch { println!( From 87309e844e8f7e617e0f7efd561e262c296ff72f Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Mon, 16 Mar 2020 14:29:34 +1000 Subject: [PATCH 037/127] fix test --- api_generator/src/generator/code_gen/url/enum_builder.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/api_generator/src/generator/code_gen/url/enum_builder.rs b/api_generator/src/generator/code_gen/url/enum_builder.rs index 89864878..7a44a468 100644 --- a/api_generator/src/generator/code_gen/url/enum_builder.rs +++ b/api_generator/src/generator/code_gen/url/enum_builder.rs @@ -298,6 +298,7 @@ mod tests { let endpoint = ( "search".to_string(), ApiEndpoint { + full_name: Some("search".to_string()), documentation: Documentation { description: None, url: None, From 2837f5e59e637811620da14793dcda7c71da6809 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Mon, 16 Mar 2020 14:30:27 +1000 Subject: [PATCH 038/127] Enable arbitrary_precision and raw_value features --- yaml_test_runner/Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yaml_test_runner/Cargo.toml b/yaml_test_runner/Cargo.toml index aa6f43ba..7e17230a 100644 --- a/yaml_test_runner/Cargo.toml +++ b/yaml_test_runner/Cargo.toml @@ -7,8 +7,8 @@ description = "Generates and runs tests from Elasticsearch's YAML test specs" repository = "https://github.com/elastic/elasticsearch-rs" [dependencies] -elasticsearch = { version = "7.6.1-alpha.1", path = "../elasticsearch" } -api_generator = { version = "7.6.1-alpha.1", path = "../api_generator" } +elasticsearch = { path = "../elasticsearch" } +api_generator = { path = "../api_generator" } clap = "~2" failure = "0.1.6" @@ -21,7 +21,7 @@ reqwest = "~0.9" semver = "0.9.0" serde = "~1" serde_yaml = "0.8.11" -serde_json = "~1" +serde_json = { version = "~1", features = ["arbitrary_precision", "raw_value"] } syn = { version = "~0.11", features = ["full"] } sysinfo = "0.9.6" url = "2.1.1" From 8a67b8145a475795d049a17ae54fb1f91e424f5e Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Mon, 16 Mar 2020 14:32:44 +1000 Subject: [PATCH 039/127] Refactor test generation Don't emit tests where a numeric value can't be parsed from a string. This removes all tests that use $set features. Emit json body json as string. Some tests include numeric values that overflow i32 when constructing a serde_json::Value from a whole number. There may be another way of handling this, but for now, emitting strings will work. --- yaml_test_runner/src/generator.rs | 230 +++++++++++++++++------------- 1 file changed, 134 insertions(+), 96 deletions(-) diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 4a25ccd3..b9d3e2ce 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -15,14 +15,14 @@ use yaml_rust::{ }; /// The components of a test file, constructed from a yaml file -struct YamlTest { +struct YamlTests { namespaces: HashSet, setup: Option, teardown: Option, - tests: Vec<(String, Tokens)>, + tests: Vec, } -impl YamlTest { +impl YamlTests { pub fn new(len: usize) -> Self { Self { namespaces: HashSet::with_capacity(len), @@ -31,6 +31,119 @@ impl YamlTest { tests: Vec::with_capacity(len), } } + + pub fn build(self) -> Tokens { + let (setup_fn, setup_call) = self.setup_impl(); + let (teardown_fn, teardown_call) = self.teardown_impl(); + let tests: Vec = self.fn_impls(setup_call, teardown_call); + + let namespaces: Vec = self + .namespaces + .iter() + .map(|n| { + let ident = syn::Ident::from(n.as_str()); + quote!(use elasticsearch::#ident::*;) + }) + .collect(); + + quote! { + #[allow(unused_imports, unused_variables)] + #[cfg(test)] + pub mod tests { + use elasticsearch::*; + use elasticsearch::http::request::JsonBody; + use elasticsearch::params::*; + #(#namespaces)* + use crate::client; + + #setup_fn + #teardown_fn + #(#tests)* + } + } + } + + fn fn_impls(&self, setup_call: Option, teardown_call: Option) -> Vec { + let mut seen_method_names = HashSet::new(); + + self + .tests + .iter() + .map(|test_fn| { + // some function descriptions are the same in YAML tests, which would result in + // duplicate generated test function names. Deduplicate by appending incrementing number + let fn_name = { + let mut fn_name = test_fn.fn_name(); + + while !seen_method_names.insert(fn_name.clone()) { + lazy_static! { + static ref ENDING_DIGITS_REGEX: Regex = + Regex::new(r"^(.*?)_(\d*?)$").unwrap(); + } + if let Some(c) = ENDING_DIGITS_REGEX.captures(&fn_name) { + let name = c.get(1).unwrap().as_str(); + let n = c.get(2).unwrap().as_str().parse::().unwrap(); + fn_name = format!("{}_{}", name, n + 1); + } else { + fn_name.push_str("_2"); + } + } + syn::Ident::from(fn_name) + }; + + let body = &test_fn.body; + + quote! { + #[tokio::test] + async fn #fn_name() -> Result<(), failure::Error> { + let client = client::create(); + #setup_call + #body + #teardown_call + Ok(()) + } + } + }) + .collect() + } + + fn setup_impl(&self) -> (Option, Option) { + Self::generate_fixture("setup", &self.setup) + } + + fn teardown_impl(&self) -> (Option, Option) { + Self::generate_fixture("teardown", &self.teardown) + } + + /// Generates the AST for the fixture fn and its invocation + fn generate_fixture(name: &str, tokens: &Option) -> (Option, Option) { + if let Some(t) = tokens { + let ident = syn::Ident::from(name); + ( + Some(quote! { + async fn #ident(client: &Elasticsearch) -> Result<(), failure::Error> { + #t + Ok(()) + } + }), + Some(quote! { #ident(&client).await?; }), + ) + } else { + (None, None) + } + } +} + +/// A test function +struct YamlTestFn { + name: String, + body: Tokens, +} + +impl YamlTestFn { + pub fn fn_name(&self) -> String { + self.name.replace(" ", "_").to_lowercase().to_snake_case() + } } /// The components of an API call @@ -204,7 +317,7 @@ impl<'a> ApiCall<'a> { .#param_ident(#f) }); }, - TypeKind::Integer => { + TypeKind::Integer | TypeKind::Number => { let i = s.parse::()?; tokens.append(quote! { .#param_ident(#i) @@ -535,9 +648,11 @@ impl<'a> ApiCall<'a> { Some(quote!(.body(vec![ #(#json),* ]))) } else { let value: serde_json::Value = serde_yaml::from_str(&s).unwrap(); - let json = serde_json::to_string(&value).unwrap(); - let ident = syn::Ident::from(json); - Some(quote!(.body(json!(#ident)))) + let json = serde_json::to_string_pretty(&value).unwrap(); + + //let ident = syn::Ident::from(json); + + Some(quote!(.body(#json))) } } } @@ -577,7 +692,7 @@ pub fn generate_tests_from_yaml( } let docs = result.unwrap(); - let mut test = YamlTest::new(docs.len()); + let mut test = YamlTests::new(docs.len()); let results : Vec> = docs .iter() @@ -590,7 +705,13 @@ pub fn generate_tests_from_yaml( match name.as_str() { "setup" => test.setup = Some(tokens), "teardown" => test.teardown = Some(tokens), - name => test.tests.push((name.to_owned(), tokens)), + name => { + let test_fn = YamlTestFn { + name: name.to_owned(), + body: tokens + }; + test.tests.push(test_fn) + }, }; Ok(()) } @@ -666,7 +787,7 @@ fn write_mod_files(generated_dir: &PathBuf) -> Result<(), failure::Error> { } fn write_test_file( - test: YamlTest, + test: YamlTests, path: &PathBuf, base_download_dir: &PathBuf, generated_dir: &PathBuf, @@ -715,72 +836,7 @@ fn write_test_file( .as_bytes(), )?; - let (setup_fn, setup_call) = generate_fixture("setup", &test.setup); - let (teardown_fn, teardown_call) = generate_fixture("teardown", &test.teardown); - let mut seen_method_names = HashSet::new(); - - let tests: Vec = test - .tests - .iter() - .map(|(name, steps)| { - let method_name = { - let mut method_name = name.replace(" ", "_").to_lowercase().to_snake_case(); - - // some method descriptions are the same in YAML tests, which would result in - // duplicate generated test function names. Deduplicate by appending incrementing number - while !seen_method_names.insert(method_name.clone()) { - lazy_static! { - static ref ENDING_DIGITS_REGEX: Regex = - Regex::new(r"^(.*?)_(\d*?)$").unwrap(); - } - if let Some(c) = ENDING_DIGITS_REGEX.captures(&method_name) { - let name = c.get(1).unwrap().as_str(); - let n = c.get(2).unwrap().as_str().parse::().unwrap(); - method_name = format!("{}_{}", name, n + 1); - } else { - method_name.push_str("_2"); - } - } - syn::Ident::from(method_name) - }; - quote! { - #[tokio::test] - async fn #method_name() -> Result<(), failure::Error> { - let client = client::create(); - #setup_call - #steps - #teardown_call - Ok(()) - } - } - }) - .collect(); - - let namespaces: Vec = test - .namespaces - .iter() - .map(|n| { - let ident = syn::Ident::from(n.as_str()); - quote!(use elasticsearch::#ident::*;) - }) - .collect(); - - let tokens = quote! { - #![allow(unused_imports)] - #[cfg(test)] - pub mod tests { - use elasticsearch::*; - use elasticsearch::http::request::JsonBody; - use elasticsearch::params::*; - #(#namespaces)* - use crate::client; - - #setup_fn - #teardown_fn - #(#tests)* - } - }; - + let tokens = test.build(); let generated = api_generator::generator::rust_fmt(tokens.to_string())?; let mut file = OpenOptions::new().append(true).open(&path)?; file.write_all(generated.as_bytes())?; @@ -789,25 +845,7 @@ fn write_test_file( Ok(()) } -/// Generates the AST for the fixture fn and its invocation -fn generate_fixture(name: &str, tokens: &Option) -> (Option, Option) { - if let Some(t) = tokens { - let ident = syn::Ident::from(name); - ( - Some(quote! { - async fn #ident(client: &Elasticsearch) -> Result<(), failure::Error> { - #t - Ok(()) - } - }), - Some(quote! { #ident(&client).await?; }), - ) - } else { - (None, None) - } -} - -fn parse_steps(api: &Api, test: &mut YamlTest, steps: &[Yaml]) -> Result { +fn parse_steps(api: &Api, test: &mut YamlTests, steps: &[Yaml]) -> Result { let mut tokens = Tokens::new(); for step in steps { if let Some(hash) = step.as_hash() { @@ -923,7 +961,7 @@ fn parse_match(_api: &Api, _hash: &Hash, tokens: &mut Tokens) -> Result<(), fail fn parse_do( api: &Api, - test: &mut YamlTest, + test: &mut YamlTests, hash: &Hash, tokens: &mut Tokens, ) -> Result<(), failure::Error> { From 3d0d075b74af0534d47849fb60cea556901f0987 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 17 Mar 2020 12:43:04 +1000 Subject: [PATCH 040/127] cargo fmt --- api_generator/src/rest_spec/mod.rs | 2 +- elasticsearch/src/auth.rs | 2 +- elasticsearch/src/cert.rs | 14 ++++++++++---- elasticsearch/src/http/transport.rs | 16 +++++++--------- elasticsearch/src/params/mod.rs | 1 - yaml_test_runner/src/github.rs | 15 ++++++++++++--- 6 files changed, 31 insertions(+), 19 deletions(-) diff --git a/api_generator/src/rest_spec/mod.rs b/api_generator/src/rest_spec/mod.rs index 29030172..ca7229fe 100644 --- a/api_generator/src/rest_spec/mod.rs +++ b/api_generator/src/rest_spec/mod.rs @@ -69,7 +69,7 @@ fn download_endpoints(spec: &GitHubSpec, download_dir: &PathBuf) -> Result<(), f let response = client.get(&spec.url).send().unwrap(); let rest_api_specs: Vec = response.json().unwrap(); println!("Downloading {} specs from {}", spec.dir, spec.branch); - download_specs_to_dir(client,rest_api_specs.as_slice(), download_dir).unwrap(); + download_specs_to_dir(client, rest_api_specs.as_slice(), download_dir).unwrap(); println!("Done downloading {} specs from {}", spec.dir, spec.branch); Ok(()) } diff --git a/elasticsearch/src/auth.rs b/elasticsearch/src/auth.rs index 0efd8c1d..df73e14d 100644 --- a/elasticsearch/src/auth.rs +++ b/elasticsearch/src/auth.rs @@ -40,7 +40,7 @@ pub enum ClientCertificate { /// /// This requires the `rustls-tls` feature to be enabled. #[cfg(feature = "rustls-tls")] - Pem(Vec) + Pem(Vec), } #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] diff --git a/elasticsearch/src/cert.rs b/elasticsearch/src/cert.rs index 7dcad978..fcf9a7fb 100644 --- a/elasticsearch/src/cert.rs +++ b/elasticsearch/src/cert.rs @@ -19,7 +19,9 @@ pub use reqwest::Certificate; /// With Elasticsearch running at `https://example.com`, configured to use a certificate generated /// with your own Certificate Authority (CA), and where the certificate contains a CommonName (CN) /// or Subject Alternative Name (SAN) that matches the hostname of Elasticsearch -#[cfg_attr(any(feature = "native-tls", feature = "rustls-tls"), doc = r##" +#[cfg_attr( + any(feature = "native-tls", feature = "rustls-tls"), + doc = r##" ```rust,norun # use elasticsearch::{ # auth::Credentials, @@ -48,14 +50,17 @@ let _response = client.ping().send().await?; # Ok(()) # } ``` -"##)] +"## +)] /// ## Certificate validation /// /// This requires the `native-tls` feature to be enabled. /// /// With Elasticsearch running at `https://example.com`, configured to use a certificate generated /// with your own Certificate Authority (CA) -#[cfg_attr(feature = "native-tls", doc = r##" +#[cfg_attr( + feature = "native-tls", + doc = r##" ```rust,norun # use elasticsearch::{ # auth::Credentials, @@ -83,7 +88,8 @@ let _response = client.ping().send().await?; # Ok(()) # } ``` -"##)] +"## +)] /// ## No validation /// /// No validation is performed on the certificate provided by the server. diff --git a/elasticsearch/src/http/transport.rs b/elasticsearch/src/http/transport.rs index 1ce186a4..fe902f8b 100644 --- a/elasticsearch/src/http/transport.rs +++ b/elasticsearch/src/http/transport.rs @@ -15,6 +15,7 @@ use crate::{ }, }; +use crate::auth::ClientCertificate; use base64::write::EncoderWriter as Base64Encoder; use bytes::BytesMut; use serde::Serialize; @@ -23,7 +24,6 @@ use std::fmt; use std::fmt::Debug; use std::io::{self, Write}; use url::Url; -use crate::auth::ClientCertificate; /// Error that can occur when building a [Transport] #[derive(Debug)] @@ -155,16 +155,16 @@ impl TransportBuilder { ClientCertificate::Pkcs12(b, p) => { let password = match p { Some(pass) => pass.as_str(), - None => "" + None => "", }; let pkcs12 = reqwest::Identity::from_pkcs12_der(b, password)?; client_builder.identity(pkcs12) - }, + } #[cfg(feature = "rustls-tls")] ClientCertificate::Pem(b) => { let pem = reqwest::Identity::from_pem(b)?; client_builder.identity(pem) - }, + } } } }; @@ -176,11 +176,9 @@ impl TransportBuilder { #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] CertificateValidation::Full(c) => client_builder.add_root_certificate(c), #[cfg(feature = "native-tls")] - CertificateValidation::Certificate(c) => { - client_builder - .add_root_certificate(c) - .danger_accept_invalid_hostnames(true) - } + CertificateValidation::Certificate(c) => client_builder + .add_root_certificate(c) + .danger_accept_invalid_hostnames(true), CertificateValidation::None => client_builder.danger_accept_invalid_certs(true), } } diff --git a/elasticsearch/src/params/mod.rs b/elasticsearch/src/params/mod.rs index 2ee96a29..acd93980 100644 --- a/elasticsearch/src/params/mod.rs +++ b/elasticsearch/src/params/mod.rs @@ -110,4 +110,3 @@ impl<'a> From<(Vec<&'a str>, Vec<&'a str>)> for SourceFilter { } } } - diff --git a/yaml_test_runner/src/github.rs b/yaml_test_runner/src/github.rs index c39bde5f..b48500c2 100644 --- a/yaml_test_runner/src/github.rs +++ b/yaml_test_runner/src/github.rs @@ -38,7 +38,11 @@ struct GitHubContent { } /// Downloads the yaml tests if not already downloaded -pub fn download_test_suites(token: &str, branch: &str, download_dir: &PathBuf) -> Result<(), failure::Error> { +pub fn download_test_suites( + token: &str, + branch: &str, + download_dir: &PathBuf, +) -> Result<(), failure::Error> { let mut last_downloaded_version = download_dir.clone(); last_downloaded_version.push("last_downloaded_version"); if last_downloaded_version.exists() { @@ -154,7 +158,12 @@ fn download( fs::create_dir_all(&content_path)?; download(client, &content.url, &content_path)?; } - t => return Err(DownloadError::InvalidType(format!("Unexpected GitHub content type: {}", t))), + t => { + return Err(DownloadError::InvalidType(format!( + "Unexpected GitHub content type: {}", + t + ))) + } } } @@ -165,7 +174,7 @@ fn download( pub enum DownloadError { IoErr(io::Error), HttpError(reqwest::Error), - InvalidType(String) + InvalidType(String), } impl std::fmt::Display for DownloadError { From db4dce075b404c21807761e6c8afd842e96d7b65 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 17 Mar 2020 12:43:39 +1000 Subject: [PATCH 041/127] Deprecating warnings fn on Response --- elasticsearch/src/http/response.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/elasticsearch/src/http/response.rs b/elasticsearch/src/http/response.rs index 6e917732..a68dda1a 100644 --- a/elasticsearch/src/http/response.rs +++ b/elasticsearch/src/http/response.rs @@ -42,7 +42,17 @@ impl Response { self.0.headers() } + /// Deprecation warning response headers + pub fn warning_headers(&self) -> impl Iterator { + self.headers() + .get_all("Warning") + .iter() + .map(|w| w.to_str().unwrap()) + } + /// Asynchronously read the response body + /// + /// Reading the response body consumes `self` pub async fn read_body(self) -> Result where B: DeserializeOwned, From 9ff6c859a8e3b30645df332803379e36cec46d04 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 17 Mar 2020 12:46:10 +1000 Subject: [PATCH 042/127] start of parse_match impl WIP. Need to delay reading response body until we know what form the body should take i.e. serde_json::Value in majority of cases, but may be a String when performing a regex match on text/plain such as in cat APIs --- yaml_test_runner/src/generator.rs | 188 ++++++++++++++++++++++-------- 1 file changed, 139 insertions(+), 49 deletions(-) diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index b9d3e2ce..5a6d05d1 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -5,14 +5,12 @@ use api_generator::generator::{Api, ApiEndpoint, TypeKind}; use itertools::Itertools; use regex::Regex; use std::collections::HashSet; +use std::fmt::Write as FormatWrite; use std::fs; use std::fs::{File, OpenOptions}; use std::io::Write; use std::path::PathBuf; -use yaml_rust::{ - yaml::{Hash}, - Yaml, YamlEmitter, YamlLoader, -}; +use yaml_rust::{yaml::Hash, Yaml, YamlEmitter, YamlLoader}; /// The components of a test file, constructed from a yaml file struct YamlTests { @@ -54,6 +52,8 @@ impl YamlTests { use elasticsearch::http::request::JsonBody; use elasticsearch::params::*; #(#namespaces)* + use regex; + use serde_json::Value; use crate::client; #setup_fn @@ -66,8 +66,7 @@ impl YamlTests { fn fn_impls(&self, setup_call: Option, teardown_call: Option) -> Vec { let mut seen_method_names = HashSet::new(); - self - .tests + self.tests .iter() .map(|test_fn| { // some function descriptions are the same in YAML tests, which would result in @@ -77,9 +76,9 @@ impl YamlTests { while !seen_method_names.insert(fn_name.clone()) { lazy_static! { - static ref ENDING_DIGITS_REGEX: Regex = - Regex::new(r"^(.*?)_(\d*?)$").unwrap(); - } + static ref ENDING_DIGITS_REGEX: Regex = + Regex::new(r"^(.*?)_(\d*?)$").unwrap(); + } if let Some(c) = ENDING_DIGITS_REGEX.captures(&fn_name) { let name = c.get(1).unwrap().as_str(); let n = c.get(2).unwrap().as_str().parse::().unwrap(); @@ -121,11 +120,11 @@ impl YamlTests { let ident = syn::Ident::from(name); ( Some(quote! { - async fn #ident(client: &Elasticsearch) -> Result<(), failure::Error> { - #t - Ok(()) - } - }), + async fn #ident(client: &Elasticsearch) -> Result<(), failure::Error> { + #t + Ok(()) + } + }), Some(quote! { #ident(&client).await?; }), ) } else { @@ -316,7 +315,7 @@ impl<'a> ApiCall<'a> { tokens.append(quote! { .#param_ident(#f) }); - }, + } TypeKind::Integer | TypeKind::Number => { let i = s.parse::()?; tokens.append(quote! { @@ -443,13 +442,18 @@ impl<'a> ApiCall<'a> { // Enum variants containing no URL parts where there is only a single API URL, // are not required to be passed in the API if parts.is_empty() { - let param_counts = endpoint.url.paths + let param_counts = endpoint + .url + .paths .iter() .map(|p| p.path.params().len()) .collect::>(); if !param_counts.contains(&0) { - return Err(failure::err_msg(format!("No path for '{}' API with no URL parts", api_call))); + return Err(failure::err_msg(format!( + "No path for '{}' API with no URL parts", + api_call + ))); } return match endpoint.url.paths.len() { @@ -466,7 +470,7 @@ impl<'a> ApiCall<'a> { } else { None } - }, + } _ => { // get the matching path parts let matching_path_parts = endpoint @@ -648,7 +652,7 @@ impl<'a> ApiCall<'a> { Some(quote!(.body(vec![ #(#json),* ]))) } else { let value: serde_json::Value = serde_yaml::from_str(&s).unwrap(); - let json = serde_json::to_string_pretty(&value).unwrap(); + let json = serde_json::to_string(&value).unwrap(); //let ident = syn::Ident::from(json); @@ -862,11 +866,15 @@ fn parse_steps(api: &Api, test: &mut YamlTests, steps: &[Yaml]) -> Result return Err(failure::err_msg(e.to_string())) + Err(e) => return Err(failure::err_msg(e.to_string())), } } ("do", Yaml::Hash(h)) => parse_do(api, test, h, &mut tokens)?, @@ -886,8 +894,6 @@ fn parse_steps(api: &Api, test: &mut YamlTests, steps: &[Yaml]) -> Result {} (op, _) => return Err(failure::err_msg(format!("unknown step operation: {}", op))), } - - } else { return Err(failure::err_msg(format!("{:?} is not a hash", &step))); } @@ -907,37 +913,48 @@ impl<'a> Skip<'a> { pub fn matches(&self, version: &semver::Version) -> bool { match &self.version_requirements { Some(r) => r.matches(version), - None => false + None => false, } } } fn parse_skip(hash: &Hash) -> Result { - let version = hash.get(&Yaml::from_str("version")).map_or_else(|| None,|y| y.as_str()); - let reason = hash.get(&Yaml::from_str("reason")).map_or_else(|| None,|y| y.as_str()); - let features = hash.get(&Yaml::from_str("features")).map_or_else(|| None,|y| y.as_str()); + let version = hash + .get(&Yaml::from_str("version")) + .map_or_else(|| None, |y| y.as_str()); + let reason = hash + .get(&Yaml::from_str("reason")) + .map_or_else(|| None, |y| y.as_str()); + let features = hash + .get(&Yaml::from_str("features")) + .map_or_else(|| None, |y| y.as_str()); let version_requirements = if let Some(v) = version { - lazy_static!{ - static ref VERSION_REGEX: Regex = Regex::new(r"^([\w\.]+)?\s*?\-\s*?([\w\.]+)?$").unwrap(); - } - - if let Some(c) = VERSION_REGEX.captures(v) { - match (c.get(1), c.get(2)) { - (Some(start), Some(end)) => { - Some(semver::VersionReq::parse(format!(">={},<={}", start.as_str(), end.as_str()).as_ref()).unwrap()) - }, - (Some(start), None) => { - Some(semver::VersionReq::parse(format!(">={}", start.as_str()).as_ref()).unwrap()) - }, - (None, Some(end)) => { - Some(semver::VersionReq::parse(format!("<={}", end.as_str()).as_ref()).unwrap()) - }, - (None, None) => { - None + if v.to_lowercase() == "all" { + Some(semver::VersionReq::any()) + } else { + lazy_static! { + static ref VERSION_REGEX: Regex = + Regex::new(r"^([\w\.]+)?\s*?\-\s*?([\w\.]+)?$").unwrap(); + } + if let Some(c) = VERSION_REGEX.captures(v) { + match (c.get(1), c.get(2)) { + (Some(start), Some(end)) => Some( + semver::VersionReq::parse( + format!(">={},<={}", start.as_str(), end.as_str()).as_ref(), + ) + .unwrap(), + ), + (Some(start), None) => Some( + semver::VersionReq::parse(format!(">={}", start.as_str()).as_ref()).unwrap(), + ), + (None, Some(end)) => { + Some(semver::VersionReq::parse(format!("<={}", end.as_str()).as_ref()).unwrap()) + } + (None, None) => None, } + } else { + None } - } else { - None } } else { None @@ -947,14 +964,85 @@ fn parse_skip(hash: &Hash) -> Result { version, version_requirements, reason, - features + features, }) } -fn parse_match(_api: &Api, _hash: &Hash, tokens: &mut Tokens) -> Result<(), failure::Error> { - +fn parse_match(api: &Api, hash: &Hash, tokens: &mut Tokens) -> Result<(), failure::Error> { + // match hashes only have one entry + let (k, v) = hash.iter().next().unwrap(); + let key = k.as_str().unwrap().trim(); + let expr = { + if key == "$body" { + key.into() + } else { + let mut values = Vec::new(); + let mut value = String::new(); + let mut chars = key.chars(); + while let Some(ch) = chars.next() { + match ch { + '\\' => { + // consume the next character too + if let Some(next) = chars.next() { + value.push(next); + } + } + '.' => { + values.push(value); + value = String::new(); + } + _ => { + value.push(ch); + } + } + } + values.push(value); + let mut expr = String::new(); + for s in values { + if s.chars().all(char::is_numeric) { + write!(expr, "[{}]", s).unwrap(); + } else { + write!(expr, "[\"{}\"]", s).unwrap(); + } + }; + expr + } + }; + match v { + Yaml::Real(_) => {}, + Yaml::Integer(_) => {}, + Yaml::String(s) => { + if s.starts_with('/') { + let s = s.trim().trim_matches('/'); + if expr == "$body" { + tokens.append(quote! { + let string_response_body = serde_json::to_string(&response_body).unwrap(); + let regex = regex::Regex::new(#s)?; + assert!(regex.is_match(&string_response_body), "expected:\n\n{}\n\nto match regex:\n\n{}", &string_response_body, #s); + }); + } else { + let ident = syn::Ident::from(expr); + tokens.append(quote! { + let regex = regex::Regex::new(#s)?; + assert!(regex.is_match(response_body#ident.as_str().unwrap()), "expected value at #ident:\n\n{}\n\nto match regex:\n\n{}", response_body#ident.as_str().unwrap(), #s); + }); + } + } else { + let ident = syn::Ident::from(expr); + tokens.append(quote! { + assert_eq!(response_body#ident.as_str().unwrap(), #s, "expected value {} but was {}", #s, response_body#ident.as_str().unwrap()); + }) + } + }, + Yaml::Boolean(_) => {}, + Yaml::Array(_) => {}, + Yaml::Hash(_) => {}, + Yaml::Alias(_) => {}, + Yaml::Null => {}, + Yaml::BadValue => {}, + } Ok(()) } @@ -1023,6 +1111,8 @@ fn parse_do( #body .send() .await?; + + let response_body = response.read_body::().await?; }); Ok(()) } From 97130c5e95fcceba9d4a25614b24e4affca78c3d Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Wed, 18 Mar 2020 16:52:55 +1000 Subject: [PATCH 043/127] Refactor step parsing into types This commit is the start of refactoring the approach to parse steps into types that can be collected as a vec of enums with variants for each type. This approach is required because certain steps may require information from previous steps to determine what action should be taken, and thus what rust code should be generated for the steps of a test --- yaml_test_runner/src/generator.rs | 703 ++++++++++++++++++++---------- 1 file changed, 464 insertions(+), 239 deletions(-) diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 5a6d05d1..b7ea6c60 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -1,42 +1,86 @@ use inflector::Inflector; -use quote::Tokens; +use quote::{Tokens, ToTokens}; use api_generator::generator::{Api, ApiEndpoint, TypeKind}; use itertools::Itertools; use regex::Regex; -use std::collections::HashSet; +use std::collections::{HashSet, BTreeMap}; use std::fmt::Write as FormatWrite; use std::fs; use std::fs::{File, OpenOptions}; use std::io::Write; use std::path::PathBuf; use yaml_rust::{yaml::Hash, Yaml, YamlEmitter, YamlLoader}; +use semver::Version; /// The components of a test file, constructed from a yaml file struct YamlTests { - namespaces: HashSet, - setup: Option, - teardown: Option, + version: Option, + directives: HashSet, + setup: Option>, + teardown: Option>, tests: Vec, } impl YamlTests { - pub fn new(len: usize) -> Self { + pub fn new(version: Option, len: usize) -> Self { Self { - namespaces: HashSet::with_capacity(len), + version, + directives: HashSet::with_capacity(len), setup: None, teardown: None, tests: Vec::with_capacity(len), } } + /// Collects the use directives required for all steps in + /// the test + fn use_directives_from_steps(steps: &[Step]) -> Vec { + steps + .iter() + .filter_map(Step::r#do) + .filter_map(|d| d.api_call.namespace.as_ref()) + .map(|s| s.to_string()) + .collect() + } + + pub fn add_setup(&mut self, steps: Vec) -> &mut Self { + let directives = Self::use_directives_from_steps(&steps); + for directive in directives { + self.directives.insert(directive); + } + + self.setup = Some(steps); + self + } + + pub fn add_teardown(&mut self, steps: Vec) -> &mut Self { + let directives = Self::use_directives_from_steps(&steps); + for directive in directives { + self.directives.insert(directive); + } + + self.teardown = Some(steps); + self + } + + pub fn add_test_fn(&mut self, test_fn: YamlTestFn) -> &mut Self { + let directives = Self::use_directives_from_steps(&test_fn.steps); + for directive in directives { + self.directives.insert(directive); + } + + self.tests.push(test_fn); + self + } + pub fn build(self) -> Tokens { let (setup_fn, setup_call) = self.setup_impl(); let (teardown_fn, teardown_call) = self.teardown_impl(); let tests: Vec = self.fn_impls(setup_call, teardown_call); - let namespaces: Vec = self - .namespaces + let directives: Vec = self + .directives .iter() .map(|n| { let ident = syn::Ident::from(n.as_str()); @@ -51,7 +95,7 @@ impl YamlTests { use elasticsearch::*; use elasticsearch::http::request::JsonBody; use elasticsearch::params::*; - #(#namespaces)* + #(#directives)* use regex; use serde_json::Value; use crate::client; @@ -63,43 +107,86 @@ impl YamlTests { } } + /// some function descriptions are the same in YAML tests, which would result in + /// duplicate generated test function names. Deduplicate by appending incrementing number + fn unique_fn_name(name: &str, seen_method_names: &mut HashSet) -> syn::Ident { + let mut fn_name = name.to_string(); + while !seen_method_names.insert(fn_name.clone()) { + lazy_static! { + static ref ENDING_DIGITS_REGEX: Regex = + Regex::new(r"^(.*?)_(\d*?)$").unwrap(); + } + if let Some(c) = ENDING_DIGITS_REGEX.captures(&fn_name) { + let name = c.get(1).unwrap().as_str(); + let n = c.get(2).unwrap().as_str().parse::().unwrap(); + fn_name = format!("{}_{}", name, n + 1); + } else { + fn_name.push_str("_2"); + } + } + syn::Ident::from(fn_name) + } + fn fn_impls(&self, setup_call: Option, teardown_call: Option) -> Vec { let mut seen_method_names = HashSet::new(); self.tests .iter() .map(|test_fn| { - // some function descriptions are the same in YAML tests, which would result in - // duplicate generated test function names. Deduplicate by appending incrementing number - let fn_name = { - let mut fn_name = test_fn.fn_name(); - - while !seen_method_names.insert(fn_name.clone()) { - lazy_static! { - static ref ENDING_DIGITS_REGEX: Regex = - Regex::new(r"^(.*?)_(\d*?)$").unwrap(); - } - if let Some(c) = ENDING_DIGITS_REGEX.captures(&fn_name) { - let name = c.get(1).unwrap().as_str(); - let n = c.get(2).unwrap().as_str().parse::().unwrap(); - fn_name = format!("{}_{}", name, n + 1); - } else { - fn_name.push_str("_2"); + let fn_name = + Self::unique_fn_name(test_fn.fn_name().as_ref(), &mut seen_method_names); + + let mut body = Tokens::new(); + let mut skip = Option::<&Skip>::None; + let mut read_response = false; + + for step in &test_fn.steps { + match step { + Step::Skip(s) => { + skip = if let Some(v) = &self.version { + if s.matches(v) { + let reason = match s.reason.as_ref() { + Some(s) => s.to_string(), + None => String::new() + }; + println!( + "Skipping test because skip version '{}' are met. {}", + s.version.as_ref().unwrap(), + reason + ); + Some(s) + } else { + None + } + } else { + None + } + }, + Step::Do(d) => d.to_tokens(&mut body), + Step::Match(m) => { + if !read_response { + body.append(quote! { + let response_body = response.read_body::().await?; + }); + read_response = true; + } + m.to_tokens(&mut body); } } - syn::Ident::from(fn_name) - }; - - let body = &test_fn.body; - - quote! { - #[tokio::test] - async fn #fn_name() -> Result<(), failure::Error> { - let client = client::create(); - #setup_call - #body - #teardown_call - Ok(()) + } + + // TODO: surface this some other way, other than returning empty tokens + match skip { + Some(_) => Tokens::new(), + None => quote! { + #[tokio::test] + async fn #fn_name() -> Result<(), failure::Error> { + let client = client::create(); + #setup_call + #body + #teardown_call + Ok(()) + } } } }) @@ -115,13 +202,25 @@ impl YamlTests { } /// Generates the AST for the fixture fn and its invocation - fn generate_fixture(name: &str, tokens: &Option) -> (Option, Option) { - if let Some(t) = tokens { + fn generate_fixture(name: &str, steps: &Option>) -> (Option, Option) { + if let Some(s) = steps { let ident = syn::Ident::from(name); + + // TODO: collect up the do calls for now. We do also need to handle skip, etc. + let tokens = s + .iter() + .filter_map(Step::r#do) + .map(|d| { + let mut tokens = Tokens::new(); + d.to_tokens(&mut tokens); + tokens + }) + .collect::>(); + ( Some(quote! { async fn #ident(client: &Elasticsearch) -> Result<(), failure::Error> { - #t + #(#tokens)* Ok(()) } }), @@ -136,18 +235,25 @@ impl YamlTests { /// A test function struct YamlTestFn { name: String, - body: Tokens, + steps: Vec, } impl YamlTestFn { + pub fn new>(name: S, steps: Vec) -> Self { + Self { + name: name.into(), + steps + } + } + pub fn fn_name(&self) -> String { self.name.replace(" ", "_").to_lowercase().to_snake_case() } } /// The components of an API call -struct ApiCall<'a> { - namespace: Option<&'a str>, +struct ApiCall { + namespace: Option, function: syn::Ident, parts: Option, params: Option, @@ -155,13 +261,30 @@ struct ApiCall<'a> { ignore: Option, } -impl<'a> ApiCall<'a> { +impl ToTokens for ApiCall { + fn to_tokens(&self, tokens: &mut Tokens) { + let function = &self.function; + let parts = &self.parts; + let params = &self.params; + let body = &self.body; + + tokens.append(quote! { + let response = client.#function(#parts) + #params + #body + .send() + .await?; + }); + } +} + +impl ApiCall { /// Try to create an API call pub fn try_from( - api: &'a Api, - endpoint: &'a ApiEndpoint, - hash: &'a Hash, - ) -> Result, failure::Error> { + api: &Api, + endpoint: &ApiEndpoint, + hash: &Hash, + ) -> Result { let mut parts: Vec<(&str, &Yaml)> = vec![]; let mut params: Vec<(&str, &Yaml)> = vec![]; let mut body: Option = None; @@ -190,8 +313,9 @@ impl<'a> ApiCall<'a> { let parts = Self::generate_parts(api_call, endpoint, &parts)?; let params = Self::generate_params(api, endpoint, ¶ms)?; let function = syn::Ident::from(api_call.replace(".", "().")); - let namespace: Option<&str> = if api_call.contains('.') { - Some(api_call.splitn(2, '.').collect::>()[0]) + let namespace: Option = if api_call.contains('.') { + let namespaces: Vec<&str> = api_call.splitn(2, '.').collect(); + Some(namespaces[0].to_string()) } else { None }; @@ -696,7 +820,7 @@ pub fn generate_tests_from_yaml( } let docs = result.unwrap(); - let mut test = YamlTests::new(docs.len()); + let mut test = YamlTests::new(api.version(), docs.len()); let results : Vec> = docs .iter() @@ -705,16 +829,13 @@ pub fn generate_tests_from_yaml( let (first_key, first_value) = hash.iter().next().unwrap(); match (first_key, first_value) { (Yaml::String(name), Yaml::Array(steps)) => { - let tokens = parse_steps(api, &mut test, steps)?; + let steps = parse_steps(api, steps)?; match name.as_str() { - "setup" => test.setup = Some(tokens), - "teardown" => test.teardown = Some(tokens), + "setup" => test.add_setup(steps), + "teardown" => test.add_teardown(steps), name => { - let test_fn = YamlTestFn { - name: name.to_owned(), - body: tokens - }; - test.tests.push(test_fn) + let test_fn = YamlTestFn::new(name, steps); + test.add_test_fn(test_fn) }, }; Ok(()) @@ -849,38 +970,30 @@ fn write_test_file( Ok(()) } -fn parse_steps(api: &Api, test: &mut YamlTests, steps: &[Yaml]) -> Result { - let mut tokens = Tokens::new(); +fn parse_steps(api: &Api, steps: &[Yaml]) -> Result, failure::Error> { + let mut parsed_steps : Vec = Vec::new(); for step in steps { if let Some(hash) = step.as_hash() { let (k, v) = hash.iter().next().unwrap(); let key = k.as_str().unwrap(); + // TODO: pass v directly to try_parse step and handle conversion to yaml type inside match (key, v) { ("skip", Yaml::Hash(h)) => { - match parse_skip(h) { - Ok(skip) => { - if let Some(api_version) = api.version() { - if skip.matches(&api_version) { - let reason = skip.reason.unwrap_or(""); - - // TODO: Communicate this in a different way - Don't use an error. Probably need to push components of a test into its own struct - return Err(failure::err_msg(format!( - "Skipping test because skip version '{}' are met. {}", - skip.version.unwrap(), - reason - ))); - } - } - } - Err(e) => return Err(failure::err_msg(e.to_string())), - } + let skip = Skip::try_parse(h)?; + parsed_steps.push(skip.into()); } - ("do", Yaml::Hash(h)) => parse_do(api, test, h, &mut tokens)?, + ("do", Yaml::Hash(h)) => { + let d = Do::try_parse(api, h)?; + parsed_steps.push(d.into()) + }, ("set", Yaml::Hash(_h)) => {} ("transform_and_set", Yaml::Hash(_h)) => {} - ("match", Yaml::Hash(h)) => parse_match(api, h, &mut tokens)?, + ("match", Yaml::Hash(h)) => { + let m = Match::try_parse(h)?; + parsed_steps.push(m.into()); + }, ("contains", Yaml::Hash(_h)) => {} ("is_true", Yaml::Hash(_h)) => {} ("is_true", Yaml::String(_s)) => {} @@ -899,81 +1012,118 @@ fn parse_steps(api: &Api, test: &mut YamlTests, steps: &[Yaml]) -> Result { +pub struct Skip { version_requirements: Option, - version: Option<&'a str>, - reason: Option<&'a str>, - features: Option<&'a str>, + version: Option, + reason: Option, + features: Option>, } -impl<'a> Skip<'a> { - pub fn matches(&self, version: &semver::Version) -> bool { - match &self.version_requirements { - Some(r) => r.matches(version), - None => false, - } +impl From for Step { + fn from(skip: Skip) -> Self { + Step::Skip(skip) } } -fn parse_skip(hash: &Hash) -> Result { - let version = hash - .get(&Yaml::from_str("version")) - .map_or_else(|| None, |y| y.as_str()); - let reason = hash - .get(&Yaml::from_str("reason")) - .map_or_else(|| None, |y| y.as_str()); - let features = hash - .get(&Yaml::from_str("features")) - .map_or_else(|| None, |y| y.as_str()); - let version_requirements = if let Some(v) = version { - if v.to_lowercase() == "all" { - Some(semver::VersionReq::any()) - } else { - lazy_static! { - static ref VERSION_REGEX: Regex = - Regex::new(r"^([\w\.]+)?\s*?\-\s*?([\w\.]+)?$").unwrap(); - } - if let Some(c) = VERSION_REGEX.captures(v) { - match (c.get(1), c.get(2)) { - (Some(start), Some(end)) => Some( - semver::VersionReq::parse( - format!(">={},<={}", start.as_str(), end.as_str()).as_ref(), +impl Skip { + fn try_parse(hash: &Hash) -> Result { + + fn string_value(hash: &Hash, name: &str) -> Option { + hash + .get(&Yaml::from_str(name)) + .map_or_else(|| None, |y| { + y.as_str().map(|s| s.to_string()) + }) + } + + fn array_value(hash: &Hash, name: &str) -> Option> { + hash + .get(&Yaml::from_str(name)) + .map_or_else(|| None, |y| { + match y.as_str() { + Some(s) => Some(vec![s.to_string()]), + None => y.as_vec().map(|arr| + arr + .iter() + .map(|a| a.as_str().map(|s| s.to_string()).unwrap()) + .collect() ) - .unwrap(), - ), - (Some(start), None) => Some( - semver::VersionReq::parse(format!(">={}", start.as_str()).as_ref()).unwrap(), - ), - (None, Some(end)) => { - Some(semver::VersionReq::parse(format!("<={}", end.as_str()).as_ref()).unwrap()) } - (None, None) => None, - } + + }) + } + + let version = string_value(hash, "version"); + let reason = string_value(hash, "reason"); + let features = array_value(hash, "features"); + + let version_requirements = if let Some(v) = &version { + if v.to_lowercase() == "all" { + Some(semver::VersionReq::any()) } else { - None + lazy_static! { + static ref VERSION_REGEX: Regex = + Regex::new(r"^([\w\.]+)?\s*?\-\s*?([\w\.]+)?$").unwrap(); + } + if let Some(c) = VERSION_REGEX.captures(v) { + match (c.get(1), c.get(2)) { + (Some(start), Some(end)) => Some( + semver::VersionReq::parse( + format!(">={},<={}", start.as_str(), end.as_str()).as_ref(), + ).unwrap(), + ), + (Some(start), None) => Some( + semver::VersionReq::parse(format!(">={}", start.as_str()).as_ref()).unwrap(), + ), + (None, Some(end)) => Some( + semver::VersionReq::parse(format!("<={}", end.as_str()).as_ref()).unwrap() + ), + (None, None) => None, + } + } else { + None + } } + } else { + None + }; + + Ok(Skip { + version, + version_requirements, + reason, + features, + }) + } + + pub fn matches(&self, version: &semver::Version) -> bool { + match &self.version_requirements { + Some(r) => r.matches(version), + None => false, } - } else { - None - }; + } +} + +pub struct Match { + hash: Hash +} - Ok(Skip { - version, - version_requirements, - reason, - features, - }) +impl From for Step { + fn from(m: Match) -> Self { + Step::Match(m) + } } -fn parse_match(api: &Api, hash: &Hash, tokens: &mut Tokens) -> Result<(), failure::Error> { +impl Match { + pub fn try_parse(hash: &Hash) -> Result { + Ok(Match { hash: hash.clone() }) + } - // match hashes only have one entry - let (k, v) = hash.iter().next().unwrap(); - let key = k.as_str().unwrap().trim(); - let expr = { + /// Builds an indexer expression from the match key + fn get_expr(key: &str) -> String { if key == "$body" { key.into() } else { @@ -1008,125 +1158,200 @@ fn parse_match(api: &Api, hash: &Hash, tokens: &mut Tokens) -> Result<(), failur }; expr } - }; + } +} - match v { - Yaml::Real(_) => {}, - Yaml::Integer(_) => {}, - Yaml::String(s) => { - if s.starts_with('/') { - let s = s.trim().trim_matches('/'); - if expr == "$body" { +impl ToTokens for Match { + // TODO: Move this parsing out into Match::try_parse + fn to_tokens(&self, tokens: &mut Tokens) { + let (k, v) = self.hash.iter().next().unwrap(); + let key = k.as_str().unwrap().trim(); + let expr = Self::get_expr(key); + + match v { + Yaml::String(s) => { + if s.starts_with('/') { + let s = s.trim().trim_matches('/'); + if expr == "$body" { + tokens.append(quote! { + let string_response_body = serde_json::to_string(&response_body).unwrap(); + let regex = regex::Regex::new(#s)?; + assert!( + regex.is_match(&string_response_body), + "expected $body:\n\n{}\n\nto match regex:\n\n{}", + &string_response_body, + #s + ); + }); + } else { + let ident = syn::Ident::from(expr.clone()); + tokens.append(quote! { + let regex = regex::Regex::new(#s)?; + assert!( + regex.is_match(response_body#ident.as_str().unwrap()), + "expected value at {}:\n\n{}\n\nto match regex:\n\n{}", + #expr, + response_body#ident.as_str().unwrap(), + #s + ); + }); + } + } else { + let ident = syn::Ident::from(expr.clone()); tokens.append(quote! { - let string_response_body = serde_json::to_string(&response_body).unwrap(); - let regex = regex::Regex::new(#s)?; - assert!(regex.is_match(&string_response_body), "expected:\n\n{}\n\nto match regex:\n\n{}", &string_response_body, #s); - }); + assert_eq!( + response_body#ident.as_str().unwrap(), + #s, + "expected value at {} to be {} but was {}", + #expr, + #s, + response_body#ident.as_str().unwrap() + ); + }) + } + }, + Yaml::Integer(i) => { + if expr == "$body" { + panic!("match on $body with integer"); } else { - let ident = syn::Ident::from(expr); + let ident = syn::Ident::from(expr.clone()); tokens.append(quote! { - let regex = regex::Regex::new(#s)?; - assert!(regex.is_match(response_body#ident.as_str().unwrap()), "expected value at #ident:\n\n{}\n\nto match regex:\n\n{}", response_body#ident.as_str().unwrap(), #s); + assert_eq!( + response_body#ident.as_i64().unwrap(), + #i, + "expected value at {} to be {} but was {}", + #expr, + #i, + response_body#ident.as_i64().unwrap() + ); }); } - } else { - let ident = syn::Ident::from(expr); - tokens.append(quote! { - assert_eq!(response_body#ident.as_str().unwrap(), #s, "expected value {} but was {}", #s, response_body#ident.as_str().unwrap()); - }) } - }, - Yaml::Boolean(_) => {}, - Yaml::Array(_) => {}, - Yaml::Hash(_) => {}, - Yaml::Alias(_) => {}, - Yaml::Null => {}, - Yaml::BadValue => {}, + // TODO: handle hashes, etc. + _ => {} + } } +} - Ok(()) +pub enum Step { + Skip(Skip), + Do(Do), + Match(Match), } -fn parse_do( - api: &Api, - test: &mut YamlTests, - hash: &Hash, - tokens: &mut Tokens, -) -> Result<(), failure::Error> { - let results: Vec> = hash - .iter() - .map(|(k, v)| { - match k.as_str() { - Some(key) => { - match key { - "headers" => { - // TODO: implement - Ok(()) - } - "catch" => { - // TODO: implement - Ok(()) - } - "node_selector" => { - // TODO: implement - Ok(()) - } - "warnings" => { - // TODO: implement - Ok(()) - } - api_call => { - let hash = v.as_hash(); - if hash.is_none() { - return Err(failure::err_msg(format!( - "expected hash value for {} but found {:?}", - &api_call, v - ))); - } +impl Step { + pub fn r#do(&self) -> Option<&Do> { + match self { + Step::Do(d) => Some(d), + _ => None + } + } +} + +pub struct Do { + headers: BTreeMap, + catch: Option, + api_call: ApiCall, + warnings: Vec, +} + +impl ToTokens for Do { + fn to_tokens(&self, tokens: &mut Tokens) { + + // TODO: Add in catch, headers, warnings + &self.api_call.to_tokens(tokens); + } +} + +impl From for Step { + fn from(d: Do) -> Self { + Step::Do(d) + } +} + +impl Do { + pub fn try_parse( + api: &Api, + hash: &Hash, + ) -> Result { + let mut api_call: Option = None; + let mut headers = BTreeMap::new(); + let mut warnings: Vec = Vec::new(); + let mut catch = None; + + let results: Vec> = hash + .iter() + .map(|(k, v)| { + match k.as_str() { + Some(key) => { + match key { + "headers" => { + match v.as_hash() { + Some(h) => { + //for (k, v) in h.iter() {} + - let endpoint = match api.endpoint_for_api_call(api_call) { - Some(e) => Ok(e), - None => { - Err(failure::err_msg(format!("no API found for {}", api_call))) + Ok(()) + }, + None => Err(failure::err_msg(format!( + "expected hash but found {:?}", + v + ))), } - }?; - - let ApiCall { - namespace, - function, - parts, - params, - body, - ignore: _ignore, - } = ApiCall::try_from(api, endpoint, hash.unwrap())?; - - // capture any namespaces used in the test - if let Some(n) = namespace { - test.namespaces.insert(n.to_owned()); } + "catch" => { + catch = v.as_str().map(|s| s.to_string()); + Ok(()) + } + "node_selector" => { + // TODO: implement + Ok(()) + } + "warnings" => { + warnings = v + .as_vec() + .map(|a| a.iter().map(|y| y.as_str().unwrap().to_string()).collect()) + .unwrap(); + Ok(()) + } + call => { + let hash = v.as_hash(); + if hash.is_none() { + return Err(failure::err_msg(format!( + "expected hash value for {} but found {:?}", + &call, v + ))); + } - tokens.append(quote! { - let response = client.#function(#parts) - #params - #body - .send() - .await?; + let endpoint = match api.endpoint_for_api_call(call) { + Some(e) => Ok(e), + None => { + Err(failure::err_msg(format!("no API found for {}", call))) + } + }?; - let response_body = response.read_body::().await?; - }); - Ok(()) + api_call = Some(ApiCall::try_from(api, endpoint, hash.unwrap())?); + Ok(()) + } } } + None => Err(failure::err_msg(format!( + "expected string key but found {:?}", + k + ))), } - None => Err(failure::err_msg(format!( - "expected string key but found {:?}", - k - ))), - } - }) - .collect(); + }) + .collect(); + + ok_or_accumulate(&results, 0)?; - ok_or_accumulate(&results, 0) + Ok(Do { + api_call: api_call.unwrap(), + catch, + headers, + warnings, + }) + } } /// Checks whether there are any Errs in the collection, and accumulates them into one From 734096bc750b3e9f0ad8db739d1d536ab83950bd Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Fri, 20 Mar 2020 15:25:17 +1000 Subject: [PATCH 044/127] Refactor steps into separate module --- yaml_test_runner/src/generator.rs | 1076 ++-------------------------- yaml_test_runner/src/main.rs | 1 + yaml_test_runner/src/step/do.rs | 640 +++++++++++++++++ yaml_test_runner/src/step/match.rs | 134 ++++ yaml_test_runner/src/step/mod.rs | 94 +++ yaml_test_runner/src/step/skip.rs | 84 +++ 6 files changed, 1013 insertions(+), 1016 deletions(-) create mode 100644 yaml_test_runner/src/step/do.rs create mode 100644 yaml_test_runner/src/step/match.rs create mode 100644 yaml_test_runner/src/step/mod.rs create mode 100644 yaml_test_runner/src/step/skip.rs diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index b7ea6c60..caa9431a 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -1,25 +1,24 @@ use inflector::Inflector; -use quote::{Tokens, ToTokens}; +use quote::{ToTokens, Tokens}; -use api_generator::generator::{Api, ApiEndpoint, TypeKind}; -use itertools::Itertools; +use crate::step::*; +use api_generator::generator::Api; use regex::Regex; -use std::collections::{HashSet, BTreeMap}; -use std::fmt::Write as FormatWrite; +use semver::Version; +use std::collections::HashSet; use std::fs; use std::fs::{File, OpenOptions}; use std::io::Write; use std::path::PathBuf; -use yaml_rust::{yaml::Hash, Yaml, YamlEmitter, YamlLoader}; -use semver::Version; +use yaml_rust::{Yaml, YamlLoader}; /// The components of a test file, constructed from a yaml file struct YamlTests { version: Option, directives: HashSet, - setup: Option>, - teardown: Option>, - tests: Vec, + setup: Option, + teardown: Option, + tests: Vec, } impl YamlTests { @@ -37,34 +36,34 @@ impl YamlTests { /// the test fn use_directives_from_steps(steps: &[Step]) -> Vec { steps - .iter() - .filter_map(Step::r#do) - .filter_map(|d| d.api_call.namespace.as_ref()) - .map(|s| s.to_string()) - .collect() + .iter() + .filter_map(Step::r#do) + .filter_map(|d| d.api_call.namespace.as_ref()) + .map(|s| s.to_string()) + .collect() } - pub fn add_setup(&mut self, steps: Vec) -> &mut Self { - let directives = Self::use_directives_from_steps(&steps); + pub fn add_setup(&mut self, setup: TestFn) -> &mut Self { + let directives = Self::use_directives_from_steps(&setup.steps); for directive in directives { self.directives.insert(directive); } - self.setup = Some(steps); + self.setup = Some(setup); self } - pub fn add_teardown(&mut self, steps: Vec) -> &mut Self { - let directives = Self::use_directives_from_steps(&steps); + pub fn add_teardown(&mut self, teardown: TestFn) -> &mut Self { + let directives = Self::use_directives_from_steps(&teardown.steps); for directive in directives { self.directives.insert(directive); } - self.teardown = Some(steps); + self.teardown = Some(teardown); self } - pub fn add_test_fn(&mut self, test_fn: YamlTestFn) -> &mut Self { + pub fn add_test_fn(&mut self, test_fn: TestFn) -> &mut Self { let directives = Self::use_directives_from_steps(&test_fn.steps); for directive in directives { self.directives.insert(directive); @@ -75,8 +74,8 @@ impl YamlTests { } pub fn build(self) -> Tokens { - let (setup_fn, setup_call) = self.setup_impl(); - let (teardown_fn, teardown_call) = self.teardown_impl(); + let (setup_fn, setup_call) = Self::generate_fixture(&self.setup); + let (teardown_fn, teardown_call) = Self::generate_fixture(&self.teardown); let tests: Vec = self.fn_impls(setup_call, teardown_call); let directives: Vec = self @@ -113,8 +112,7 @@ impl YamlTests { let mut fn_name = name.to_string(); while !seen_method_names.insert(fn_name.clone()) { lazy_static! { - static ref ENDING_DIGITS_REGEX: Regex = - Regex::new(r"^(.*?)_(\d*?)$").unwrap(); + static ref ENDING_DIGITS_REGEX: Regex = Regex::new(r"^(.*?)_(\d*?)$").unwrap(); } if let Some(c) = ENDING_DIGITS_REGEX.captures(&fn_name) { let name = c.get(1).unwrap().as_str(); @@ -147,7 +145,7 @@ impl YamlTests { if s.matches(v) { let reason = match s.reason.as_ref() { Some(s) => s.to_string(), - None => String::new() + None => String::new(), }; println!( "Skipping test because skip version '{}' are met. {}", @@ -161,7 +159,7 @@ impl YamlTests { } else { None } - }, + } Step::Do(d) => d.to_tokens(&mut body), Step::Match(m) => { if !read_response { @@ -187,27 +185,20 @@ impl YamlTests { #teardown_call Ok(()) } - } + }, } }) .collect() } - fn setup_impl(&self) -> (Option, Option) { - Self::generate_fixture("setup", &self.setup) - } - - fn teardown_impl(&self) -> (Option, Option) { - Self::generate_fixture("teardown", &self.teardown) - } - /// Generates the AST for the fixture fn and its invocation - fn generate_fixture(name: &str, steps: &Option>) -> (Option, Option) { - if let Some(s) = steps { - let ident = syn::Ident::from(name); + fn generate_fixture(test_fn: &Option) -> (Option, Option) { + if let Some(t) = test_fn { + let ident = syn::Ident::from(t.name.as_str()); // TODO: collect up the do calls for now. We do also need to handle skip, etc. - let tokens = s + let tokens = t + .steps .iter() .filter_map(Step::r#do) .map(|d| { @@ -233,16 +224,16 @@ impl YamlTests { } /// A test function -struct YamlTestFn { +struct TestFn { name: String, steps: Vec, } -impl YamlTestFn { +impl TestFn { pub fn new>(name: S, steps: Vec) -> Self { Self { name: name.into(), - steps + steps, } } @@ -251,542 +242,6 @@ impl YamlTestFn { } } -/// The components of an API call -struct ApiCall { - namespace: Option, - function: syn::Ident, - parts: Option, - params: Option, - body: Option, - ignore: Option, -} - -impl ToTokens for ApiCall { - fn to_tokens(&self, tokens: &mut Tokens) { - let function = &self.function; - let parts = &self.parts; - let params = &self.params; - let body = &self.body; - - tokens.append(quote! { - let response = client.#function(#parts) - #params - #body - .send() - .await?; - }); - } -} - -impl ApiCall { - /// Try to create an API call - pub fn try_from( - api: &Api, - endpoint: &ApiEndpoint, - hash: &Hash, - ) -> Result { - let mut parts: Vec<(&str, &Yaml)> = vec![]; - let mut params: Vec<(&str, &Yaml)> = vec![]; - let mut body: Option = None; - let mut ignore: Option = None; - - // work out what's a URL part and what's a param in the supplied - // arguments for the API call - for (k, v) in hash.iter() { - let key = k.as_str().unwrap(); - if endpoint.params.contains_key(key) || api.common_params.contains_key(key) { - params.push((key, v)); - } else if key == "body" { - body = Self::generate_body(endpoint, v); - } else if key == "ignore" { - ignore = match v.as_i64() { - Some(i) => Some(i), - // handle ignore as an array of i64 - None => v.as_vec().unwrap()[0].as_i64(), - } - } else { - parts.push((key, v)); - } - } - - let api_call = endpoint.full_name.as_ref().unwrap(); - let parts = Self::generate_parts(api_call, endpoint, &parts)?; - let params = Self::generate_params(api, endpoint, ¶ms)?; - let function = syn::Ident::from(api_call.replace(".", "().")); - let namespace: Option = if api_call.contains('.') { - let namespaces: Vec<&str> = api_call.splitn(2, '.').collect(); - Some(namespaces[0].to_string()) - } else { - None - }; - - Ok(ApiCall { - namespace, - function, - parts, - params, - body, - ignore, - }) - } - - fn generate_params( - api: &Api, - endpoint: &ApiEndpoint, - params: &[(&str, &Yaml)], - ) -> Result, failure::Error> { - match params.len() { - 0 => Ok(None), - _ => { - let mut tokens = Tokens::new(); - for (n, v) in params { - let param_ident = - syn::Ident::from(api_generator::generator::code_gen::valid_name(n)); - - let ty = match endpoint.params.get(*n) { - Some(t) => Ok(t), - None => match api.common_params.get(*n) { - Some(t) => Ok(t), - None => Err(failure::err_msg(format!("No param found for {}", n))), - }, - }?; - - let kind = ty.ty; - - fn create_enum( - enum_name: &str, - variant: &str, - options: &[serde_json::Value], - ) -> Result { - if !variant.is_empty() - && !options.contains(&serde_json::Value::String(variant.to_owned())) - { - return Err(failure::err_msg(format!( - "options {:?} does not contain value {}", - &options, variant - ))); - } - - let e: String = enum_name.to_pascal_case(); - let enum_name = syn::Ident::from(e.as_str()); - let variant = if variant.is_empty() { - // TODO: Should we simply omit empty Refresh tests? - if e == "Refresh" { - syn::Ident::from("True") - } else if e == "Size" { - syn::Ident::from("Unspecified") - } else { - return Err(failure::err_msg(format!( - "Unhandled empty value for {}", - &e - ))); - } - } else { - syn::Ident::from(variant.to_pascal_case()) - }; - - Ok(quote!(#enum_name::#variant)) - } - - match v { - Yaml::String(ref s) => { - match kind { - TypeKind::Enum => { - if n == &"expand_wildcards" { - // expand_wildcards might be defined as a comma-separated - // string. e.g. - let idents: Vec> = s - .split(',') - .collect::>() - .iter() - .map(|e| create_enum(n, e, &ty.options)) - .collect(); - - match ok_or_accumulate(&idents, 0) { - Ok(_) => { - let idents: Vec = idents - .into_iter() - .filter_map(Result::ok) - .collect(); - - tokens.append(quote! { - .#param_ident(&[#(#idents),*]) - }); - } - Err(e) => return Err(failure::err_msg(e)), - } - } else { - let e = create_enum(n, s.as_str(), &ty.options)?; - tokens.append(quote! { - .#param_ident(#e) - }); - } - } - TypeKind::List => { - let values: Vec<&str> = s.split(',').collect(); - tokens.append(quote! { - .#param_ident(&[#(#values),*]) - }) - } - TypeKind::Boolean => { - let b = s.parse::()?; - tokens.append(quote! { - .#param_ident(#b) - }); - } - TypeKind::Double => { - let f = s.parse::()?; - tokens.append(quote! { - .#param_ident(#f) - }); - } - TypeKind::Integer | TypeKind::Number => { - let i = s.parse::()?; - tokens.append(quote! { - .#param_ident(#i) - }); - } - _ => tokens.append(quote! { - .#param_ident(#s) - }), - } - } - Yaml::Boolean(ref b) => match kind { - TypeKind::Enum => { - let enum_name = syn::Ident::from(n.to_pascal_case()); - let variant = syn::Ident::from(b.to_string().to_pascal_case()); - tokens.append(quote! { - .#param_ident(#enum_name::#variant) - }) - } - TypeKind::List => { - // TODO: _source filter can be true|false|list of strings - let s = b.to_string(); - tokens.append(quote! { - .#param_ident(&[#s]) - }) - } - _ => { - tokens.append(quote! { - .#param_ident(#b) - }); - } - }, - Yaml::Integer(ref i) => match kind { - TypeKind::String => { - let s = i.to_string(); - tokens.append(quote! { - .#param_ident(#s) - }) - } - TypeKind::Integer => { - // yaml-rust parses all as i64 - let int = *i as i32; - tokens.append(quote! { - .#param_ident(#int) - }); - } - TypeKind::Float => { - // yaml-rust parses all as i64 - let f = *i as f32; - tokens.append(quote! { - .#param_ident(#f) - }); - } - TypeKind::Double => { - // yaml-rust parses all as i64 - let f = *i as f64; - tokens.append(quote! { - .#param_ident(#f) - }); - } - _ => { - tokens.append(quote! { - .#param_ident(#i) - }); - } - }, - Yaml::Array(arr) => { - // only support param string arrays - let result: Vec<&String> = arr - .iter() - .map(|i| match i { - Yaml::String(s) => Ok(s), - y => Err(failure::err_msg(format!( - "Unsupported array value {:?}", - y - ))), - }) - .filter_map(Result::ok) - .collect(); - - if n == &"expand_wildcards" { - let result: Vec> = result - .iter() - .map(|s| create_enum(n, s.as_str(), &ty.options)) - .collect(); - - match ok_or_accumulate(&result, 0) { - Ok(_) => { - let result: Vec = - result.into_iter().filter_map(Result::ok).collect(); - - tokens.append(quote! { - .#param_ident(&[#(#result),*]) - }); - } - Err(e) => return Err(failure::err_msg(e)), - } - } else { - tokens.append(quote! { - .#param_ident(&[#(#result),*]) - }); - } - } - _ => println!("Unsupported value {:?}", v), - } - } - - Ok(Some(tokens)) - } - } - } - - fn generate_parts( - api_call: &str, - endpoint: &ApiEndpoint, - parts: &[(&str, &Yaml)], - ) -> Result, failure::Error> { - // TODO: ideally, this should share the logic from EnumBuilder - let enum_name = { - let name = api_call.to_pascal_case().replace(".", ""); - syn::Ident::from(format!("{}Parts", name)) - }; - - // Enum variants containing no URL parts where there is only a single API URL, - // are not required to be passed in the API - if parts.is_empty() { - let param_counts = endpoint - .url - .paths - .iter() - .map(|p| p.path.params().len()) - .collect::>(); - - if !param_counts.contains(&0) { - return Err(failure::err_msg(format!( - "No path for '{}' API with no URL parts", - api_call - ))); - } - - return match endpoint.url.paths.len() { - 1 => Ok(None), - _ => Ok(Some(quote!(#enum_name::None))), - }; - } - - let path = match endpoint.url.paths.len() { - 1 => { - let path = &endpoint.url.paths[0]; - if path.path.params().len() == parts.len() { - Some(path) - } else { - None - } - } - _ => { - // get the matching path parts - let matching_path_parts = endpoint - .url - .paths - .iter() - .filter(|path| { - let p = path.path.params(); - if p.len() != parts.len() { - return false; - } - - let contains = parts - .iter() - .filter_map(|i| if p.contains(&i.0) { Some(()) } else { None }) - .collect::>(); - contains.len() == parts.len() - }) - .collect::>(); - - match matching_path_parts.len() { - 0 => None, - _ => Some(matching_path_parts[0]), - } - } - }; - - if path.is_none() { - return Err(failure::err_msg(format!( - "No path for '{}' API with URL parts {:?}", - &api_call, parts - ))); - } - - let path = path.unwrap(); - let path_parts = path.path.params(); - let variant_name = { - let v = path_parts - .iter() - .map(|k| k.to_pascal_case()) - .collect::>() - .join(""); - syn::Ident::from(v) - }; - - let part_tokens: Vec> = parts - .iter() - // don't rely on URL parts being ordered in the yaml test - .sorted_by(|(p, _), (p2, _)| { - let f = path_parts.iter().position(|x| x == p).unwrap(); - let s = path_parts.iter().position(|x| x == p2).unwrap(); - f.cmp(&s) - }) - .map(|(p, v)| { - let ty = match path.parts.get(*p) { - Some(t) => Ok(t), - None => Err(failure::err_msg(format!( - "No URL part found for {} in {}", - p, &path.path - ))), - }?; - - match v { - Yaml::String(s) => match ty.ty { - TypeKind::List => { - let values: Vec<&str> = s.split(',').collect(); - Ok(quote! { &[#(#values),*] }) - } - TypeKind::Long => { - let l = s.parse::().unwrap(); - Ok(quote! { #l }) - } - _ => Ok(quote! { #s }), - }, - Yaml::Boolean(b) => { - let s = b.to_string(); - Ok(quote! { #s }) - } - Yaml::Integer(i) => match ty.ty { - TypeKind::Long => Ok(quote! { #i }), - _ => { - let s = i.to_string(); - Ok(quote! { #s }) - } - }, - Yaml::Array(arr) => { - // only support param string arrays - let result: Vec<_> = arr - .iter() - .map(|i| match i { - Yaml::String(s) => Ok(s), - y => Err(failure::err_msg(format!( - "Unsupported array value {:?}", - y - ))), - }) - .collect(); - - match ok_or_accumulate(&result, 0) { - Ok(_) => { - let result: Vec<_> = - result.into_iter().filter_map(Result::ok).collect(); - - match ty.ty { - // Some APIs specify a part is a string in the REST API spec - // but is really a list, which is what a YAML test might pass - // e.g. security.get_role_mapping. - // see https://github.com/elastic/elasticsearch/pull/53207 - TypeKind::String => { - let s = result.iter().join(","); - Ok(quote! { #s }) - } - _ => Ok(quote! { &[#(#result),*] }), - } - } - Err(e) => Err(failure::err_msg(e)), - } - } - _ => Err(failure::err_msg(format!("Unsupported value {:?}", v))), - } - }) - .collect(); - - match ok_or_accumulate(&part_tokens, 0) { - Ok(_) => { - let part_tokens: Vec = - part_tokens.into_iter().filter_map(Result::ok).collect(); - Ok(Some( - quote! { #enum_name::#variant_name(#(#part_tokens),*) }, - )) - } - Err(e) => Err(failure::err_msg(e)), - } - } - - /// Creates the body function call from a YAML value. - /// - /// When reading a body from the YAML test, it'll be converted to a Yaml variant, - /// usually a Hash. To get the JSON representation back requires converting - /// back to JSON - fn generate_body(endpoint: &ApiEndpoint, v: &Yaml) -> Option { - let accepts_nd_body = match &endpoint.body { - Some(b) => match &b.serialize { - Some(s) => s == "bulk", - _ => false, - }, - None => false, - }; - - match v { - Yaml::String(s) => { - if accepts_nd_body { - Some(quote!(.body(vec![#s]))) - } else { - Some(quote!(.body(#s))) - } - } - _ => { - let mut s = String::new(); - { - let mut emitter = YamlEmitter::new(&mut s); - emitter.dump(v).unwrap(); - } - - if accepts_nd_body { - let values: Vec = serde_yaml::from_str(&s).unwrap(); - let json: Vec = values - .iter() - .map(|value| { - let json = serde_json::to_string(&value).unwrap(); - let ident = syn::Ident::from(json); - if value.is_string() { - quote!(#ident) - } else { - quote!(JsonBody::from(json!(#ident))) - } - }) - .collect(); - Some(quote!(.body(vec![ #(#json),* ]))) - } else { - let value: serde_json::Value = serde_yaml::from_str(&s).unwrap(); - let json = serde_json::to_string(&value).unwrap(); - - //let ident = syn::Ident::from(json); - - Some(quote!(.body(#json))) - } - } - } - } -} - pub fn generate_tests_from_yaml( api: &Api, base_download_dir: &PathBuf, @@ -825,38 +280,34 @@ pub fn generate_tests_from_yaml( let results : Vec> = docs .iter() .map(|doc| { - if let Some(hash) = doc.as_hash() { - let (first_key, first_value) = hash.iter().next().unwrap(); - match (first_key, first_value) { - (Yaml::String(name), Yaml::Array(steps)) => { - let steps = parse_steps(api, steps)?; - match name.as_str() { - "setup" => test.add_setup(steps), - "teardown" => test.add_teardown(steps), - name => { - let test_fn = YamlTestFn::new(name, steps); - test.add_test_fn(test_fn) - }, - }; - Ok(()) - } - (k, v) => { - Err(failure::err_msg(format!( - "expected string key and array value in {:?}, but found {:?} and {:?}", - &entry.path(), - &k, - &v, - ))) - } - } - } else { - Err(failure::err_msg(format!( + let hash = doc + .as_hash() + .ok_or_else(|| failure::err_msg(format!( "expected hash but found {:?}", &doc - ))) + )))?; + + let (key, value) = hash.iter().next().unwrap(); + match (key, value) { + (Yaml::String(name), Yaml::Array(steps)) => { + let steps = parse_steps(api, steps)?; + let test_fn = TestFn::new(name, steps); + match name.as_str() { + "setup" => test.add_setup(test_fn), + "teardown" => test.add_teardown(test_fn), + _ => test.add_test_fn(test_fn), + }; + Ok(()) + } + (k, v) => { + Err(failure::err_msg(format!( + "expected string key and array value in {:?}, but found {:?} and {:?}", + &entry.path(), + &k, + &v, + ))) + } } - - }) .collect(); @@ -969,410 +420,3 @@ fn write_test_file( Ok(()) } - -fn parse_steps(api: &Api, steps: &[Yaml]) -> Result, failure::Error> { - let mut parsed_steps : Vec = Vec::new(); - for step in steps { - if let Some(hash) = step.as_hash() { - let (k, v) = hash.iter().next().unwrap(); - - let key = k.as_str().unwrap(); - - // TODO: pass v directly to try_parse step and handle conversion to yaml type inside - match (key, v) { - ("skip", Yaml::Hash(h)) => { - let skip = Skip::try_parse(h)?; - parsed_steps.push(skip.into()); - } - ("do", Yaml::Hash(h)) => { - let d = Do::try_parse(api, h)?; - parsed_steps.push(d.into()) - }, - ("set", Yaml::Hash(_h)) => {} - ("transform_and_set", Yaml::Hash(_h)) => {} - ("match", Yaml::Hash(h)) => { - let m = Match::try_parse(h)?; - parsed_steps.push(m.into()); - }, - ("contains", Yaml::Hash(_h)) => {} - ("is_true", Yaml::Hash(_h)) => {} - ("is_true", Yaml::String(_s)) => {} - ("is_false", Yaml::Hash(_h)) => {} - ("is_false", Yaml::String(_s)) => {} - ("length", Yaml::Hash(_h)) => {} - ("eq", Yaml::Hash(_h)) => {} - ("gte", Yaml::Hash(_h)) => {} - ("lte", Yaml::Hash(_h)) => {} - ("gt", Yaml::Hash(_h)) => {} - ("lt", Yaml::Hash(_h)) => {} - (op, _) => return Err(failure::err_msg(format!("unknown step operation: {}", op))), - } - } else { - return Err(failure::err_msg(format!("{:?} is not a hash", &step))); - } - } - - Ok(parsed_steps) -} - -pub struct Skip { - version_requirements: Option, - version: Option, - reason: Option, - features: Option>, -} - -impl From for Step { - fn from(skip: Skip) -> Self { - Step::Skip(skip) - } -} - -impl Skip { - fn try_parse(hash: &Hash) -> Result { - - fn string_value(hash: &Hash, name: &str) -> Option { - hash - .get(&Yaml::from_str(name)) - .map_or_else(|| None, |y| { - y.as_str().map(|s| s.to_string()) - }) - } - - fn array_value(hash: &Hash, name: &str) -> Option> { - hash - .get(&Yaml::from_str(name)) - .map_or_else(|| None, |y| { - match y.as_str() { - Some(s) => Some(vec![s.to_string()]), - None => y.as_vec().map(|arr| - arr - .iter() - .map(|a| a.as_str().map(|s| s.to_string()).unwrap()) - .collect() - ) - } - - }) - } - - let version = string_value(hash, "version"); - let reason = string_value(hash, "reason"); - let features = array_value(hash, "features"); - - let version_requirements = if let Some(v) = &version { - if v.to_lowercase() == "all" { - Some(semver::VersionReq::any()) - } else { - lazy_static! { - static ref VERSION_REGEX: Regex = - Regex::new(r"^([\w\.]+)?\s*?\-\s*?([\w\.]+)?$").unwrap(); - } - if let Some(c) = VERSION_REGEX.captures(v) { - match (c.get(1), c.get(2)) { - (Some(start), Some(end)) => Some( - semver::VersionReq::parse( - format!(">={},<={}", start.as_str(), end.as_str()).as_ref(), - ).unwrap(), - ), - (Some(start), None) => Some( - semver::VersionReq::parse(format!(">={}", start.as_str()).as_ref()).unwrap(), - ), - (None, Some(end)) => Some( - semver::VersionReq::parse(format!("<={}", end.as_str()).as_ref()).unwrap() - ), - (None, None) => None, - } - } else { - None - } - } - } else { - None - }; - - Ok(Skip { - version, - version_requirements, - reason, - features, - }) - } - - pub fn matches(&self, version: &semver::Version) -> bool { - match &self.version_requirements { - Some(r) => r.matches(version), - None => false, - } - } -} - -pub struct Match { - hash: Hash -} - -impl From for Step { - fn from(m: Match) -> Self { - Step::Match(m) - } -} - -impl Match { - pub fn try_parse(hash: &Hash) -> Result { - Ok(Match { hash: hash.clone() }) - } - - /// Builds an indexer expression from the match key - fn get_expr(key: &str) -> String { - if key == "$body" { - key.into() - } else { - let mut values = Vec::new(); - let mut value = String::new(); - let mut chars = key.chars(); - while let Some(ch) = chars.next() { - match ch { - '\\' => { - // consume the next character too - if let Some(next) = chars.next() { - value.push(next); - } - } - '.' => { - values.push(value); - value = String::new(); - } - _ => { - value.push(ch); - } - } - } - values.push(value); - let mut expr = String::new(); - for s in values { - if s.chars().all(char::is_numeric) { - write!(expr, "[{}]", s).unwrap(); - } else { - write!(expr, "[\"{}\"]", s).unwrap(); - } - }; - expr - } - } -} - -impl ToTokens for Match { - // TODO: Move this parsing out into Match::try_parse - fn to_tokens(&self, tokens: &mut Tokens) { - let (k, v) = self.hash.iter().next().unwrap(); - let key = k.as_str().unwrap().trim(); - let expr = Self::get_expr(key); - - match v { - Yaml::String(s) => { - if s.starts_with('/') { - let s = s.trim().trim_matches('/'); - if expr == "$body" { - tokens.append(quote! { - let string_response_body = serde_json::to_string(&response_body).unwrap(); - let regex = regex::Regex::new(#s)?; - assert!( - regex.is_match(&string_response_body), - "expected $body:\n\n{}\n\nto match regex:\n\n{}", - &string_response_body, - #s - ); - }); - } else { - let ident = syn::Ident::from(expr.clone()); - tokens.append(quote! { - let regex = regex::Regex::new(#s)?; - assert!( - regex.is_match(response_body#ident.as_str().unwrap()), - "expected value at {}:\n\n{}\n\nto match regex:\n\n{}", - #expr, - response_body#ident.as_str().unwrap(), - #s - ); - }); - } - } else { - let ident = syn::Ident::from(expr.clone()); - tokens.append(quote! { - assert_eq!( - response_body#ident.as_str().unwrap(), - #s, - "expected value at {} to be {} but was {}", - #expr, - #s, - response_body#ident.as_str().unwrap() - ); - }) - } - }, - Yaml::Integer(i) => { - if expr == "$body" { - panic!("match on $body with integer"); - } else { - let ident = syn::Ident::from(expr.clone()); - tokens.append(quote! { - assert_eq!( - response_body#ident.as_i64().unwrap(), - #i, - "expected value at {} to be {} but was {}", - #expr, - #i, - response_body#ident.as_i64().unwrap() - ); - }); - } - } - // TODO: handle hashes, etc. - _ => {} - } - } -} - -pub enum Step { - Skip(Skip), - Do(Do), - Match(Match), -} - -impl Step { - pub fn r#do(&self) -> Option<&Do> { - match self { - Step::Do(d) => Some(d), - _ => None - } - } -} - -pub struct Do { - headers: BTreeMap, - catch: Option, - api_call: ApiCall, - warnings: Vec, -} - -impl ToTokens for Do { - fn to_tokens(&self, tokens: &mut Tokens) { - - // TODO: Add in catch, headers, warnings - &self.api_call.to_tokens(tokens); - } -} - -impl From for Step { - fn from(d: Do) -> Self { - Step::Do(d) - } -} - -impl Do { - pub fn try_parse( - api: &Api, - hash: &Hash, - ) -> Result { - let mut api_call: Option = None; - let mut headers = BTreeMap::new(); - let mut warnings: Vec = Vec::new(); - let mut catch = None; - - let results: Vec> = hash - .iter() - .map(|(k, v)| { - match k.as_str() { - Some(key) => { - match key { - "headers" => { - match v.as_hash() { - Some(h) => { - //for (k, v) in h.iter() {} - - - Ok(()) - }, - None => Err(failure::err_msg(format!( - "expected hash but found {:?}", - v - ))), - } - } - "catch" => { - catch = v.as_str().map(|s| s.to_string()); - Ok(()) - } - "node_selector" => { - // TODO: implement - Ok(()) - } - "warnings" => { - warnings = v - .as_vec() - .map(|a| a.iter().map(|y| y.as_str().unwrap().to_string()).collect()) - .unwrap(); - Ok(()) - } - call => { - let hash = v.as_hash(); - if hash.is_none() { - return Err(failure::err_msg(format!( - "expected hash value for {} but found {:?}", - &call, v - ))); - } - - let endpoint = match api.endpoint_for_api_call(call) { - Some(e) => Ok(e), - None => { - Err(failure::err_msg(format!("no API found for {}", call))) - } - }?; - - api_call = Some(ApiCall::try_from(api, endpoint, hash.unwrap())?); - Ok(()) - } - } - } - None => Err(failure::err_msg(format!( - "expected string key but found {:?}", - k - ))), - } - }) - .collect(); - - ok_or_accumulate(&results, 0)?; - - Ok(Do { - api_call: api_call.unwrap(), - catch, - headers, - warnings, - }) - } -} - -/// Checks whether there are any Errs in the collection, and accumulates them into one -/// error message if there are. -fn ok_or_accumulate( - results: &[Result], - indent: usize, -) -> Result<(), failure::Error> { - let errs = results - .iter() - .filter_map(|r| r.as_ref().err()) - .collect::>(); - if errs.is_empty() { - Ok(()) - } else { - let msg = errs - .iter() - .map(|e| format!("{}{}", "\t".to_string().repeat(indent), e.to_string())) - .collect::>() - .join("\n"); - - Err(failure::err_msg(msg)) - } -} diff --git a/yaml_test_runner/src/main.rs b/yaml_test_runner/src/main.rs index 458a9c89..76ebc4d4 100644 --- a/yaml_test_runner/src/main.rs +++ b/yaml_test_runner/src/main.rs @@ -13,6 +13,7 @@ use std::path::PathBuf; mod generator; mod github; +pub mod step; #[cfg(test)] mod generated; diff --git a/yaml_test_runner/src/step/do.rs b/yaml_test_runner/src/step/do.rs new file mode 100644 index 00000000..56b4db81 --- /dev/null +++ b/yaml_test_runner/src/step/do.rs @@ -0,0 +1,640 @@ +use inflector::Inflector; +use quote::{ToTokens, Tokens}; + +use super::{ok_or_accumulate, Step}; +use api_generator::generator::{Api, ApiEndpoint, TypeKind}; +use itertools::Itertools; +use std::collections::BTreeMap; +use yaml_rust::{yaml::Hash, Yaml, YamlEmitter}; + +pub struct Do { + headers: BTreeMap, + catch: Option, + pub api_call: ApiCall, + warnings: Vec, +} + +impl ToTokens for Do { + fn to_tokens(&self, tokens: &mut Tokens) { + // TODO: Add in catch, headers, warnings + &self.api_call.to_tokens(tokens); + } +} + +impl From for Step { + fn from(d: Do) -> Self { + Step::Do(d) + } +} + +impl Do { + pub fn try_parse(api: &Api, yaml: &Yaml) -> Result { + let hash = yaml + .as_hash() + .ok_or_else(|| failure::err_msg(format!("Expected hash but found {:?}", yaml)))?; + + let mut api_call: Option = None; + let mut headers = BTreeMap::new(); + let mut warnings: Vec = Vec::new(); + let mut catch = None; + + let results: Vec> = hash + .iter() + .map(|(k, v)| { + let key = k.as_str().ok_or_else(|| { + failure::err_msg(format!("expected string key but found {:?}", k)) + })?; + + match key { + "headers" => { + match v.as_hash() { + Some(h) => { + //for (k, v) in h.iter() {} + + Ok(()) + } + None => { + Err(failure::err_msg(format!("expected hash but found {:?}", v))) + } + } + } + "catch" => { + catch = v.as_str().map(|s| s.to_string()); + Ok(()) + } + "node_selector" => { + // TODO: implement + Ok(()) + } + "warnings" => { + warnings = v + .as_vec() + .map(|a| a.iter().map(|y| y.as_str().unwrap().to_string()).collect()) + .unwrap(); + Ok(()) + } + call => { + let hash = v.as_hash().ok_or_else(|| { + failure::err_msg(format!( + "expected hash value for {} but found {:?}", + &call, v + )) + })?; + + let endpoint = api.endpoint_for_api_call(call).ok_or_else(|| { + failure::err_msg(format!("no API found for {}", call)) + })?; + + api_call = Some(ApiCall::try_from(api, endpoint, hash)?); + Ok(()) + } + } + }) + .collect(); + + ok_or_accumulate(&results, 0)?; + + Ok(Do { + api_call: api_call.unwrap(), + catch, + headers, + warnings, + }) + } +} + +/// The components of an API call +pub struct ApiCall { + pub namespace: Option, + function: syn::Ident, + parts: Option, + params: Option, + body: Option, + ignore: Option, +} + +impl ToTokens for ApiCall { + fn to_tokens(&self, tokens: &mut Tokens) { + let function = &self.function; + let parts = &self.parts; + let params = &self.params; + let body = &self.body; + + tokens.append(quote! { + let response = client.#function(#parts) + #params + #body + .send() + .await?; + }); + } +} + +impl ApiCall { + /// Try to create an API call + pub fn try_from( + api: &Api, + endpoint: &ApiEndpoint, + hash: &Hash, + ) -> Result { + let mut parts: Vec<(&str, &Yaml)> = vec![]; + let mut params: Vec<(&str, &Yaml)> = vec![]; + let mut body: Option = None; + let mut ignore: Option = None; + + // work out what's a URL part and what's a param in the supplied + // arguments for the API call + for (k, v) in hash.iter() { + let key = k.as_str().unwrap(); + if endpoint.params.contains_key(key) || api.common_params.contains_key(key) { + params.push((key, v)); + } else if key == "body" { + body = Self::generate_body(endpoint, v); + } else if key == "ignore" { + ignore = match v.as_i64() { + Some(i) => Some(i), + // handle ignore as an array of i64 + None => v.as_vec().unwrap()[0].as_i64(), + } + } else { + parts.push((key, v)); + } + } + + let api_call = endpoint.full_name.as_ref().unwrap(); + let parts = Self::generate_parts(api_call, endpoint, &parts)?; + let params = Self::generate_params(api, endpoint, ¶ms)?; + let function = syn::Ident::from(api_call.replace(".", "().")); + let namespace: Option = if api_call.contains('.') { + let namespaces: Vec<&str> = api_call.splitn(2, '.').collect(); + Some(namespaces[0].to_string()) + } else { + None + }; + + Ok(ApiCall { + namespace, + function, + parts, + params, + body, + ignore, + }) + } + + fn generate_params( + api: &Api, + endpoint: &ApiEndpoint, + params: &[(&str, &Yaml)], + ) -> Result, failure::Error> { + match params.len() { + 0 => Ok(None), + _ => { + let mut tokens = Tokens::new(); + for (n, v) in params { + let param_ident = + syn::Ident::from(api_generator::generator::code_gen::valid_name(n)); + + let ty = match endpoint.params.get(*n) { + Some(t) => Ok(t), + None => match api.common_params.get(*n) { + Some(t) => Ok(t), + None => Err(failure::err_msg(format!("No param found for {}", n))), + }, + }?; + + let kind = ty.ty; + + fn create_enum( + enum_name: &str, + variant: &str, + options: &[serde_json::Value], + ) -> Result { + if !variant.is_empty() + && !options.contains(&serde_json::Value::String(variant.to_owned())) + { + return Err(failure::err_msg(format!( + "options {:?} does not contain value {}", + &options, variant + ))); + } + + let e: String = enum_name.to_pascal_case(); + let enum_name = syn::Ident::from(e.as_str()); + let variant = if variant.is_empty() { + // TODO: Should we simply omit empty Refresh tests? + if e == "Refresh" { + syn::Ident::from("True") + } else if e == "Size" { + syn::Ident::from("Unspecified") + } else { + return Err(failure::err_msg(format!( + "Unhandled empty value for {}", + &e + ))); + } + } else { + syn::Ident::from(variant.to_pascal_case()) + }; + + Ok(quote!(#enum_name::#variant)) + } + + match v { + Yaml::String(ref s) => { + match kind { + TypeKind::Enum => { + if n == &"expand_wildcards" { + // expand_wildcards might be defined as a comma-separated + // string. e.g. + let idents: Vec> = s + .split(',') + .collect::>() + .iter() + .map(|e| create_enum(n, e, &ty.options)) + .collect(); + + match ok_or_accumulate(&idents, 0) { + Ok(_) => { + let idents: Vec = idents + .into_iter() + .filter_map(Result::ok) + .collect(); + + tokens.append(quote! { + .#param_ident(&[#(#idents),*]) + }); + } + Err(e) => return Err(failure::err_msg(e)), + } + } else { + let e = create_enum(n, s.as_str(), &ty.options)?; + tokens.append(quote! { + .#param_ident(#e) + }); + } + } + TypeKind::List => { + let values: Vec<&str> = s.split(',').collect(); + tokens.append(quote! { + .#param_ident(&[#(#values),*]) + }) + } + TypeKind::Boolean => { + let b = s.parse::()?; + tokens.append(quote! { + .#param_ident(#b) + }); + } + TypeKind::Double => { + let f = s.parse::()?; + tokens.append(quote! { + .#param_ident(#f) + }); + } + TypeKind::Integer | TypeKind::Number => { + let i = s.parse::()?; + tokens.append(quote! { + .#param_ident(#i) + }); + } + _ => tokens.append(quote! { + .#param_ident(#s) + }), + } + } + Yaml::Boolean(ref b) => match kind { + TypeKind::Enum => { + let enum_name = syn::Ident::from(n.to_pascal_case()); + let variant = syn::Ident::from(b.to_string().to_pascal_case()); + tokens.append(quote! { + .#param_ident(#enum_name::#variant) + }) + } + TypeKind::List => { + // TODO: _source filter can be true|false|list of strings + let s = b.to_string(); + tokens.append(quote! { + .#param_ident(&[#s]) + }) + } + _ => { + tokens.append(quote! { + .#param_ident(#b) + }); + } + }, + Yaml::Integer(ref i) => match kind { + TypeKind::String => { + let s = i.to_string(); + tokens.append(quote! { + .#param_ident(#s) + }) + } + TypeKind::Integer => { + // yaml-rust parses all as i64 + let int = *i as i32; + tokens.append(quote! { + .#param_ident(#int) + }); + } + TypeKind::Float => { + // yaml-rust parses all as i64 + let f = *i as f32; + tokens.append(quote! { + .#param_ident(#f) + }); + } + TypeKind::Double => { + // yaml-rust parses all as i64 + let f = *i as f64; + tokens.append(quote! { + .#param_ident(#f) + }); + } + _ => { + tokens.append(quote! { + .#param_ident(#i) + }); + } + }, + Yaml::Array(arr) => { + // only support param string arrays + let result: Vec<&String> = arr + .iter() + .map(|i| match i { + Yaml::String(s) => Ok(s), + y => Err(failure::err_msg(format!( + "Unsupported array value {:?}", + y + ))), + }) + .filter_map(Result::ok) + .collect(); + + if n == &"expand_wildcards" { + let result: Vec> = result + .iter() + .map(|s| create_enum(n, s.as_str(), &ty.options)) + .collect(); + + match ok_or_accumulate(&result, 0) { + Ok(_) => { + let result: Vec = + result.into_iter().filter_map(Result::ok).collect(); + + tokens.append(quote! { + .#param_ident(&[#(#result),*]) + }); + } + Err(e) => return Err(failure::err_msg(e)), + } + } else { + tokens.append(quote! { + .#param_ident(&[#(#result),*]) + }); + } + } + _ => println!("Unsupported value {:?}", v), + } + } + + Ok(Some(tokens)) + } + } + } + + fn generate_parts( + api_call: &str, + endpoint: &ApiEndpoint, + parts: &[(&str, &Yaml)], + ) -> Result, failure::Error> { + // TODO: ideally, this should share the logic from EnumBuilder + let enum_name = { + let name = api_call.to_pascal_case().replace(".", ""); + syn::Ident::from(format!("{}Parts", name)) + }; + + // Enum variants containing no URL parts where there is only a single API URL, + // are not required to be passed in the API + if parts.is_empty() { + let param_counts = endpoint + .url + .paths + .iter() + .map(|p| p.path.params().len()) + .collect::>(); + + if !param_counts.contains(&0) { + return Err(failure::err_msg(format!( + "No path for '{}' API with no URL parts", + api_call + ))); + } + + return match endpoint.url.paths.len() { + 1 => Ok(None), + _ => Ok(Some(quote!(#enum_name::None))), + }; + } + + let path = match endpoint.url.paths.len() { + 1 => { + let path = &endpoint.url.paths[0]; + if path.path.params().len() == parts.len() { + Some(path) + } else { + None + } + } + _ => { + // get the matching path parts + let matching_path_parts = endpoint + .url + .paths + .iter() + .filter(|path| { + let p = path.path.params(); + if p.len() != parts.len() { + return false; + } + + let contains = parts + .iter() + .filter_map(|i| if p.contains(&i.0) { Some(()) } else { None }) + .collect::>(); + contains.len() == parts.len() + }) + .collect::>(); + + match matching_path_parts.len() { + 0 => None, + _ => Some(matching_path_parts[0]), + } + } + }; + + if path.is_none() { + return Err(failure::err_msg(format!( + "No path for '{}' API with URL parts {:?}", + &api_call, parts + ))); + } + + let path = path.unwrap(); + let path_parts = path.path.params(); + let variant_name = { + let v = path_parts + .iter() + .map(|k| k.to_pascal_case()) + .collect::>() + .join(""); + syn::Ident::from(v) + }; + + let part_tokens: Vec> = parts + .iter() + // don't rely on URL parts being ordered in the yaml test + .sorted_by(|(p, _), (p2, _)| { + let f = path_parts.iter().position(|x| x == p).unwrap(); + let s = path_parts.iter().position(|x| x == p2).unwrap(); + f.cmp(&s) + }) + .map(|(p, v)| { + let ty = match path.parts.get(*p) { + Some(t) => Ok(t), + None => Err(failure::err_msg(format!( + "No URL part found for {} in {}", + p, &path.path + ))), + }?; + + match v { + Yaml::String(s) => match ty.ty { + TypeKind::List => { + let values: Vec<&str> = s.split(',').collect(); + Ok(quote! { &[#(#values),*] }) + } + TypeKind::Long => { + let l = s.parse::().unwrap(); + Ok(quote! { #l }) + } + _ => Ok(quote! { #s }), + }, + Yaml::Boolean(b) => { + let s = b.to_string(); + Ok(quote! { #s }) + } + Yaml::Integer(i) => match ty.ty { + TypeKind::Long => Ok(quote! { #i }), + _ => { + let s = i.to_string(); + Ok(quote! { #s }) + } + }, + Yaml::Array(arr) => { + // only support param string arrays + let result: Vec<_> = arr + .iter() + .map(|i| match i { + Yaml::String(s) => Ok(s), + y => Err(failure::err_msg(format!( + "Unsupported array value {:?}", + y + ))), + }) + .collect(); + + match ok_or_accumulate(&result, 0) { + Ok(_) => { + let result: Vec<_> = + result.into_iter().filter_map(Result::ok).collect(); + + match ty.ty { + // Some APIs specify a part is a string in the REST API spec + // but is really a list, which is what a YAML test might pass + // e.g. security.get_role_mapping. + // see https://github.com/elastic/elasticsearch/pull/53207 + TypeKind::String => { + let s = result.iter().join(","); + Ok(quote! { #s }) + } + _ => Ok(quote! { &[#(#result),*] }), + } + } + Err(e) => Err(failure::err_msg(e)), + } + } + _ => Err(failure::err_msg(format!("Unsupported value {:?}", v))), + } + }) + .collect(); + + match ok_or_accumulate(&part_tokens, 0) { + Ok(_) => { + let part_tokens: Vec = + part_tokens.into_iter().filter_map(Result::ok).collect(); + Ok(Some( + quote! { #enum_name::#variant_name(#(#part_tokens),*) }, + )) + } + Err(e) => Err(failure::err_msg(e)), + } + } + + /// Creates the body function call from a YAML value. + /// + /// When reading a body from the YAML test, it'll be converted to a Yaml variant, + /// usually a Hash. To get the JSON representation back requires converting + /// back to JSON + fn generate_body(endpoint: &ApiEndpoint, v: &Yaml) -> Option { + let accepts_nd_body = match &endpoint.body { + Some(b) => match &b.serialize { + Some(s) => s == "bulk", + _ => false, + }, + None => false, + }; + + match v { + Yaml::String(s) => { + if accepts_nd_body { + Some(quote!(.body(vec![#s]))) + } else { + Some(quote!(.body(#s))) + } + } + _ => { + let mut s = String::new(); + { + let mut emitter = YamlEmitter::new(&mut s); + emitter.dump(v).unwrap(); + } + + if accepts_nd_body { + let values: Vec = serde_yaml::from_str(&s).unwrap(); + let json: Vec = values + .iter() + .map(|value| { + let json = serde_json::to_string(&value).unwrap(); + let ident = syn::Ident::from(json); + if value.is_string() { + quote!(#ident) + } else { + quote!(JsonBody::from(json!(#ident))) + } + }) + .collect(); + Some(quote!(.body(vec![ #(#json),* ]))) + } else { + let value: serde_json::Value = serde_yaml::from_str(&s).unwrap(); + let json = serde_json::to_string(&value).unwrap(); + + //let ident = syn::Ident::from(json); + + Some(quote!(.body(#json))) + } + } + } + } +} diff --git a/yaml_test_runner/src/step/match.rs b/yaml_test_runner/src/step/match.rs new file mode 100644 index 00000000..7251967a --- /dev/null +++ b/yaml_test_runner/src/step/match.rs @@ -0,0 +1,134 @@ +use super::Step; +use quote::{ToTokens, Tokens}; +use std::fmt::Write as FormatWrite; +use yaml_rust::{yaml::Hash, Yaml}; + +pub struct Match { + hash: Hash, +} + +impl From for Step { + fn from(m: Match) -> Self { + Step::Match(m) + } +} + +impl Match { + pub fn try_parse(yaml: &Yaml) -> Result { + let hash = yaml + .as_hash() + .ok_or_else(|| failure::err_msg(format!("Expected hash but found {:?}", yaml)))?; + + Ok(Match { hash: hash.clone() }) + } + + /// Builds an indexer expression from the match key + fn get_expr(key: &str) -> String { + if key == "$body" { + key.into() + } else { + let mut values = Vec::new(); + let mut value = String::new(); + let mut chars = key.chars(); + while let Some(ch) = chars.next() { + match ch { + '\\' => { + // consume the next character too + if let Some(next) = chars.next() { + value.push(next); + } + } + '.' => { + values.push(value); + value = String::new(); + } + _ => { + value.push(ch); + } + } + } + values.push(value); + let mut expr = String::new(); + for s in values { + if s.chars().all(char::is_numeric) { + write!(expr, "[{}]", s).unwrap(); + } else { + write!(expr, "[\"{}\"]", s).unwrap(); + } + } + expr + } + } +} + +impl ToTokens for Match { + // TODO: Move this parsing out into Match::try_parse + fn to_tokens(&self, tokens: &mut Tokens) { + let (k, v) = self.hash.iter().next().unwrap(); + let key = k.as_str().unwrap().trim(); + let expr = Self::get_expr(key); + + match v { + Yaml::String(s) => { + if s.starts_with('/') { + let s = s.trim().trim_matches('/'); + if expr == "$body" { + tokens.append(quote! { + let string_response_body = serde_json::to_string(&response_body).unwrap(); + let regex = regex::Regex::new(#s)?; + assert!( + regex.is_match(&string_response_body), + "expected $body:\n\n{}\n\nto match regex:\n\n{}", + &string_response_body, + #s + ); + }); + } else { + let ident = syn::Ident::from(expr.clone()); + tokens.append(quote! { + let regex = regex::Regex::new(#s)?; + assert!( + regex.is_match(response_body#ident.as_str().unwrap()), + "expected value at {}:\n\n{}\n\nto match regex:\n\n{}", + #expr, + response_body#ident.as_str().unwrap(), + #s + ); + }); + } + } else { + let ident = syn::Ident::from(expr.clone()); + tokens.append(quote! { + assert_eq!( + response_body#ident.as_str().unwrap(), + #s, + "expected value at {} to be {} but was {}", + #expr, + #s, + response_body#ident.as_str().unwrap() + ); + }) + } + } + Yaml::Integer(i) => { + if expr == "$body" { + panic!("match on $body with integer"); + } else { + let ident = syn::Ident::from(expr.clone()); + tokens.append(quote! { + assert_eq!( + response_body#ident.as_i64().unwrap(), + #i, + "expected value at {} to be {} but was {}", + #expr, + #i, + response_body#ident.as_i64().unwrap() + ); + }); + } + } + // TODO: handle hashes, etc. + _ => {} + } + } +} diff --git a/yaml_test_runner/src/step/mod.rs b/yaml_test_runner/src/step/mod.rs new file mode 100644 index 00000000..860013b5 --- /dev/null +++ b/yaml_test_runner/src/step/mod.rs @@ -0,0 +1,94 @@ +use api_generator::generator::Api; +use yaml_rust::Yaml; + +mod r#do; +mod r#match; +mod skip; +pub use r#do::*; +pub use r#match::*; +pub use skip::*; + +pub fn parse_steps(api: &Api, steps: &[Yaml]) -> Result, failure::Error> { + let mut parsed_steps: Vec = Vec::new(); + for step in steps { + let hash = step + .as_hash() + .ok_or_else(|| failure::err_msg(format!("Expected hash but found {:?}", step)))?; + + let (key, value) = { + let (k, yaml) = hash.iter().next().unwrap(); + let key = k.as_str().ok_or_else(|| { + failure::err_msg(format!("Expected string key but found {:?}", k)) + })?; + + (key, yaml) + }; + + match key { + "skip" => { + let skip = Skip::try_parse(value)?; + parsed_steps.push(skip.into()); + } + "do" => { + let d = Do::try_parse(api, value)?; + parsed_steps.push(d.into()) + } + "set" => {} + "transform_and_set" => {} + "match" => { + let m = Match::try_parse(value)?; + parsed_steps.push(m.into()); + } + "contains" => {} + "is_true" => {} + "is_false" => {} + "length" => {} + "eq" => {} + "gte" => {} + "lte" => {} + "gt" => {} + "lt" => {} + op => return Err(failure::err_msg(format!("unknown step operation: {}", op))), + } + } + + Ok(parsed_steps) +} + +pub enum Step { + Skip(Skip), + Do(Do), + Match(Match), +} + +impl Step { + pub fn r#do(&self) -> Option<&Do> { + match self { + Step::Do(d) => Some(d), + _ => None, + } + } +} + +/// Checks whether there are any Errs in the collection, and accumulates them into one +/// error message if there are. +pub fn ok_or_accumulate( + results: &[Result], + indent: usize, +) -> Result<(), failure::Error> { + let errs = results + .iter() + .filter_map(|r| r.as_ref().err()) + .collect::>(); + if errs.is_empty() { + Ok(()) + } else { + let msg = errs + .iter() + .map(|e| format!("{}{}", "\t".to_string().repeat(indent), e.to_string())) + .collect::>() + .join("\n"); + + Err(failure::err_msg(msg)) + } +} diff --git a/yaml_test_runner/src/step/skip.rs b/yaml_test_runner/src/step/skip.rs new file mode 100644 index 00000000..0838a64f --- /dev/null +++ b/yaml_test_runner/src/step/skip.rs @@ -0,0 +1,84 @@ +use super::Step; +use regex::Regex; +use yaml_rust::Yaml; + +pub struct Skip { + version_requirements: Option, + pub version: Option, + pub reason: Option, + features: Option>, +} + +impl From for Step { + fn from(skip: Skip) -> Self { + Step::Skip(skip) + } +} + +impl Skip { + pub fn try_parse(yaml: &Yaml) -> Result { + let version = yaml["version"] + .as_str() + .map_or_else(|| None, |y| Some(y.to_string())); + let reason = yaml["reason"] + .as_str() + .map_or_else(|| None, |y| Some(y.to_string())); + let features = match &yaml["features"] { + Yaml::String(s) => Some(vec![s.to_string()]), + Yaml::Array(a) => Some( + a.iter() + .map(|y| y.as_str().map(|s| s.to_string()).unwrap()) + .collect(), + ), + _ => None, + }; + + let version_requirements = if let Some(v) = &version { + if v.to_lowercase() == "all" { + Some(semver::VersionReq::any()) + } else { + lazy_static! { + static ref VERSION_REGEX: Regex = + Regex::new(r"^([\w\.]+)?\s*?\-\s*?([\w\.]+)?$").unwrap(); + } + if let Some(c) = VERSION_REGEX.captures(v) { + match (c.get(1), c.get(2)) { + (Some(start), Some(end)) => Some( + semver::VersionReq::parse( + format!(">={},<={}", start.as_str(), end.as_str()).as_ref(), + ) + .unwrap(), + ), + (Some(start), None) => Some( + semver::VersionReq::parse(format!(">={}", start.as_str()).as_ref()) + .unwrap(), + ), + (None, Some(end)) => Some( + semver::VersionReq::parse(format!("<={}", end.as_str()).as_ref()) + .unwrap(), + ), + (None, None) => None, + } + } else { + None + } + } + } else { + None + }; + + Ok(Skip { + version, + version_requirements, + reason, + features, + }) + } + + pub fn matches(&self, version: &semver::Version) -> bool { + match &self.version_requirements { + Some(r) => r.matches(version), + None => false, + } + } +} From dbb3604ed88e02b1e84ea9d6dd41694f4f7e5736 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Fri, 20 Mar 2020 16:21:28 +1000 Subject: [PATCH 045/127] Include headers in do step API calls --- yaml_test_runner/src/generator.rs | 7 ++- yaml_test_runner/src/step/do.rs | 75 ++++++++++++++++++------------- 2 files changed, 48 insertions(+), 34 deletions(-) diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index caa9431a..38f09ca5 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -38,7 +38,7 @@ impl YamlTests { steps .iter() .filter_map(Step::r#do) - .filter_map(|d| d.api_call.namespace.as_ref()) + .filter_map(|d| d.namespace()) .map(|s| s.to_string()) .collect() } @@ -92,7 +92,10 @@ impl YamlTests { #[cfg(test)] pub mod tests { use elasticsearch::*; - use elasticsearch::http::request::JsonBody; + use elasticsearch::http::{ + headers::{HeaderName, HeaderValue}, + request::JsonBody + }; use elasticsearch::params::*; #(#directives)* use regex; diff --git a/yaml_test_runner/src/step/do.rs b/yaml_test_runner/src/step/do.rs index 56b4db81..e206763b 100644 --- a/yaml_test_runner/src/step/do.rs +++ b/yaml_test_runner/src/step/do.rs @@ -5,18 +5,17 @@ use super::{ok_or_accumulate, Step}; use api_generator::generator::{Api, ApiEndpoint, TypeKind}; use itertools::Itertools; use std::collections::BTreeMap; -use yaml_rust::{yaml::Hash, Yaml, YamlEmitter}; +use yaml_rust::{Yaml, YamlEmitter}; pub struct Do { - headers: BTreeMap, - catch: Option, - pub api_call: ApiCall, + api_call: ApiCall, warnings: Vec, + catch: Option, } impl ToTokens for Do { fn to_tokens(&self, tokens: &mut Tokens) { - // TODO: Add in catch, headers, warnings + // TODO: Add in catch and warnings &self.api_call.to_tokens(tokens); } } @@ -31,9 +30,9 @@ impl Do { pub fn try_parse(api: &Api, yaml: &Yaml) -> Result { let hash = yaml .as_hash() - .ok_or_else(|| failure::err_msg(format!("Expected hash but found {:?}", yaml)))?; + .ok_or_else(|| failure::err_msg(format!("expected hash but found {:?}", yaml)))?; - let mut api_call: Option = None; + let mut call: Option<(&str, &Yaml)> = None; let mut headers = BTreeMap::new(); let mut warnings: Vec = Vec::new(); let mut catch = None; @@ -47,16 +46,16 @@ impl Do { match key { "headers" => { - match v.as_hash() { - Some(h) => { - //for (k, v) in h.iter() {} - - Ok(()) - } - None => { - Err(failure::err_msg(format!("expected hash but found {:?}", v))) - } + let hash = v.as_hash() + .ok_or_else(|| failure::err_msg(format!("expected hash but found {:?}", v)))?; + for (hk, hv) in hash.iter() { + let h = hk.as_str() + .ok_or_else(|| failure::err_msg(format!("expected str but found {:?}", hk)))?; + let v = hv.as_str() + .ok_or_else(|| failure::err_msg(format!("expected str but found {:?}", hv)))?; + headers.insert(h.into(), v.into()); } + Ok(()) } "catch" => { catch = v.as_str().map(|s| s.to_string()); @@ -73,19 +72,8 @@ impl Do { .unwrap(); Ok(()) } - call => { - let hash = v.as_hash().ok_or_else(|| { - failure::err_msg(format!( - "expected hash value for {} but found {:?}", - &call, v - )) - })?; - - let endpoint = api.endpoint_for_api_call(call).ok_or_else(|| { - failure::err_msg(format!("no API found for {}", call)) - })?; - - api_call = Some(ApiCall::try_from(api, endpoint, hash)?); + api_call => { + call = Some((api_call, v)); Ok(()) } } @@ -94,13 +82,21 @@ impl Do { ok_or_accumulate(&results, 0)?; + let (call, value) = call.ok_or_else(|| failure::err_msg("no API found in do"))?; + let endpoint = api.endpoint_for_api_call(call) + .ok_or_else(|| failure::err_msg(format!("no API found for '{}'", call)))?; + let api_call = ApiCall::try_from(api, endpoint, value, headers)?; + Ok(Do { - api_call: api_call.unwrap(), + api_call, catch, - headers, warnings, }) } + + pub fn namespace(&self) -> Option<&String> { + self.api_call.namespace.as_ref() + } } /// The components of an API call @@ -109,6 +105,7 @@ pub struct ApiCall { function: syn::Ident, parts: Option, params: Option, + headers: BTreeMap, body: Option, ignore: Option, } @@ -120,8 +117,17 @@ impl ToTokens for ApiCall { let params = &self.params; let body = &self.body; + // TODO: handle "set" values + let headers: Vec = self.headers + .iter() + .map(|(k,v)| quote! { + .header(HeaderName::from_static(#k), HeaderValue::from_static(#v)) + }) + .collect(); + tokens.append(quote! { let response = client.#function(#parts) + #(#headers)* #params #body .send() @@ -135,8 +141,12 @@ impl ApiCall { pub fn try_from( api: &Api, endpoint: &ApiEndpoint, - hash: &Hash, + yaml: &Yaml, + headers: BTreeMap ) -> Result { + let hash = yaml.as_hash() + .ok_or_else(|| failure::err_msg(format!("expected hash but found {:?}", yaml)))?; + let mut parts: Vec<(&str, &Yaml)> = vec![]; let mut params: Vec<(&str, &Yaml)> = vec![]; let mut body: Option = None; @@ -177,6 +187,7 @@ impl ApiCall { function, parts, params, + headers, body, ignore, }) From ffb7d41acafdeb5c9f2c739ca9c9bfb2128e9ace Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 24 Mar 2020 19:29:16 +1000 Subject: [PATCH 046/127] Implement set step --- yaml_test_runner/Cargo.toml | 2 +- yaml_test_runner/src/generator.rs | 9 +++ yaml_test_runner/src/step/do.rs | 105 +++++++++++++++++------------ yaml_test_runner/src/step/match.rs | 44 ++---------- yaml_test_runner/src/step/mod.rs | 49 +++++++++++++- yaml_test_runner/src/step/set.rs | 57 ++++++++++++++++ 6 files changed, 181 insertions(+), 85 deletions(-) create mode 100644 yaml_test_runner/src/step/set.rs diff --git a/yaml_test_runner/Cargo.toml b/yaml_test_runner/Cargo.toml index 7e17230a..30f70c57 100644 --- a/yaml_test_runner/Cargo.toml +++ b/yaml_test_runner/Cargo.toml @@ -21,7 +21,7 @@ reqwest = "~0.9" semver = "0.9.0" serde = "~1" serde_yaml = "0.8.11" -serde_json = { version = "~1", features = ["arbitrary_precision", "raw_value"] } +serde_json = { version = "~1", features = ["arbitrary_precision"] } syn = { version = "~0.11", features = ["full"] } sysinfo = "0.9.6" url = "2.1.1" diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 38f09ca5..fdc11b1d 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -172,6 +172,15 @@ impl YamlTests { read_response = true; } m.to_tokens(&mut body); + }, + Step::Set(s) => { + if !read_response { + body.append(quote! { + let response_body = response.read_body::().await?; + }); + read_response = true; + } + s.to_tokens(&mut body); } } } diff --git a/yaml_test_runner/src/step/do.rs b/yaml_test_runner/src/step/do.rs index e206763b..ded37b32 100644 --- a/yaml_test_runner/src/step/do.rs +++ b/yaml_test_runner/src/step/do.rs @@ -4,8 +4,11 @@ use quote::{ToTokens, Tokens}; use super::{ok_or_accumulate, Step}; use api_generator::generator::{Api, ApiEndpoint, TypeKind}; use itertools::Itertools; +use regex::Regex; use std::collections::BTreeMap; use yaml_rust::{Yaml, YamlEmitter}; +use syn::StrStyle; +use syn::parse::ident; pub struct Do { api_call: ApiCall, @@ -193,6 +196,41 @@ impl ApiCall { }) } + fn generate_enum( + enum_name: &str, + variant: &str, + options: &[serde_json::Value], + ) -> Result { + if !variant.is_empty() + && !options.contains(&serde_json::Value::String(variant.to_owned())) + { + return Err(failure::err_msg(format!( + "options {:?} does not contain value {}", + &options, variant + ))); + } + + let e: String = enum_name.to_pascal_case(); + let enum_name = syn::Ident::from(e.as_str()); + let variant = if variant.is_empty() { + // TODO: Should we simply omit empty Refresh tests? + if e == "Refresh" { + syn::Ident::from("True") + } else if e == "Size" { + syn::Ident::from("Unspecified") + } else { + return Err(failure::err_msg(format!( + "Unhandled empty value for {}", + &e + ))); + } + } else { + syn::Ident::from(variant.to_pascal_case()) + }; + + Ok(quote!(#enum_name::#variant)) + } + fn generate_params( api: &Api, endpoint: &ApiEndpoint, @@ -216,41 +254,6 @@ impl ApiCall { let kind = ty.ty; - fn create_enum( - enum_name: &str, - variant: &str, - options: &[serde_json::Value], - ) -> Result { - if !variant.is_empty() - && !options.contains(&serde_json::Value::String(variant.to_owned())) - { - return Err(failure::err_msg(format!( - "options {:?} does not contain value {}", - &options, variant - ))); - } - - let e: String = enum_name.to_pascal_case(); - let enum_name = syn::Ident::from(e.as_str()); - let variant = if variant.is_empty() { - // TODO: Should we simply omit empty Refresh tests? - if e == "Refresh" { - syn::Ident::from("True") - } else if e == "Size" { - syn::Ident::from("Unspecified") - } else { - return Err(failure::err_msg(format!( - "Unhandled empty value for {}", - &e - ))); - } - } else { - syn::Ident::from(variant.to_pascal_case()) - }; - - Ok(quote!(#enum_name::#variant)) - } - match v { Yaml::String(ref s) => { match kind { @@ -262,7 +265,7 @@ impl ApiCall { .split(',') .collect::>() .iter() - .map(|e| create_enum(n, e, &ty.options)) + .map(|e| Self::generate_enum(n, e, &ty.options)) .collect(); match ok_or_accumulate(&idents, 0) { @@ -279,7 +282,7 @@ impl ApiCall { Err(e) => return Err(failure::err_msg(e)), } } else { - let e = create_enum(n, s.as_str(), &ty.options)?; + let e = Self::generate_enum(n, s.as_str(), &ty.options)?; tokens.append(quote! { .#param_ident(#e) }); @@ -386,7 +389,7 @@ impl ApiCall { if n == &"expand_wildcards" { let result: Vec> = result .iter() - .map(|s| create_enum(n, s.as_str(), &ty.options)) + .map(|s| Self::generate_enum(n, s.as_str(), &ty.options)) .collect(); match ok_or_accumulate(&result, 0) { @@ -593,6 +596,21 @@ impl ApiCall { } } + fn replace_values(s: &str) -> String { + lazy_static! { + // replace usages of "$.*" with the captured value + static ref SET_REGEX: Regex = + Regex::new(r#""\$(.*?)""#).unwrap(); + + // include i64 suffix on whole numbers + static ref INT_REGEX: Regex = + regex::Regex::new(r"(:\s?)(\d+?)([,\s?|\s*?}])").unwrap(); + } + + let c = SET_REGEX.replace_all(s, "$1").into_owned(); + INT_REGEX.replace_all(&c, "${1}${2}i64${3}").into_owned() + } + /// Creates the body function call from a YAML value. /// /// When reading a body from the YAML test, it'll be converted to a Yaml variant, @@ -627,7 +645,8 @@ impl ApiCall { let json: Vec = values .iter() .map(|value| { - let json = serde_json::to_string(&value).unwrap(); + let mut json = serde_json::to_string(&value).unwrap(); + json = Self::replace_values(&json); let ident = syn::Ident::from(json); if value.is_string() { quote!(#ident) @@ -639,11 +658,11 @@ impl ApiCall { Some(quote!(.body(vec![ #(#json),* ]))) } else { let value: serde_json::Value = serde_yaml::from_str(&s).unwrap(); - let json = serde_json::to_string(&value).unwrap(); - - //let ident = syn::Ident::from(json); + let mut json = serde_json::to_string_pretty(&value).unwrap(); + json = Self::replace_values(&json); + let ident = syn::Ident::from(json); - Some(quote!(.body(#json))) + Some(quote!(.body(json!{#ident}))) } } } diff --git a/yaml_test_runner/src/step/match.rs b/yaml_test_runner/src/step/match.rs index 7251967a..42dcb3fa 100644 --- a/yaml_test_runner/src/step/match.rs +++ b/yaml_test_runner/src/step/match.rs @@ -1,7 +1,7 @@ use super::Step; use quote::{ToTokens, Tokens}; -use std::fmt::Write as FormatWrite; use yaml_rust::{yaml::Hash, Yaml}; +use crate::step::BodyExpr; pub struct Match { hash: Hash, @@ -13,6 +13,8 @@ impl From for Step { } } +impl BodyExpr for Match {} + impl Match { pub fn try_parse(yaml: &Yaml) -> Result { let hash = yaml @@ -21,44 +23,6 @@ impl Match { Ok(Match { hash: hash.clone() }) } - - /// Builds an indexer expression from the match key - fn get_expr(key: &str) -> String { - if key == "$body" { - key.into() - } else { - let mut values = Vec::new(); - let mut value = String::new(); - let mut chars = key.chars(); - while let Some(ch) = chars.next() { - match ch { - '\\' => { - // consume the next character too - if let Some(next) = chars.next() { - value.push(next); - } - } - '.' => { - values.push(value); - value = String::new(); - } - _ => { - value.push(ch); - } - } - } - values.push(value); - let mut expr = String::new(); - for s in values { - if s.chars().all(char::is_numeric) { - write!(expr, "[{}]", s).unwrap(); - } else { - write!(expr, "[\"{}\"]", s).unwrap(); - } - } - expr - } - } } impl ToTokens for Match { @@ -66,7 +30,7 @@ impl ToTokens for Match { fn to_tokens(&self, tokens: &mut Tokens) { let (k, v) = self.hash.iter().next().unwrap(); let key = k.as_str().unwrap().trim(); - let expr = Self::get_expr(key); + let expr = self.body_expr(key); match v { Yaml::String(s) => { diff --git a/yaml_test_runner/src/step/mod.rs b/yaml_test_runner/src/step/mod.rs index 860013b5..4c568df5 100644 --- a/yaml_test_runner/src/step/mod.rs +++ b/yaml_test_runner/src/step/mod.rs @@ -1,11 +1,14 @@ use api_generator::generator::Api; use yaml_rust::Yaml; +use std::fmt::Write; mod r#do; mod r#match; +mod set; mod skip; pub use r#do::*; pub use r#match::*; +pub use set::*; pub use skip::*; pub fn parse_steps(api: &Api, steps: &[Yaml]) -> Result, failure::Error> { @@ -33,7 +36,10 @@ pub fn parse_steps(api: &Api, steps: &[Yaml]) -> Result, failure::Erro let d = Do::try_parse(api, value)?; parsed_steps.push(d.into()) } - "set" => {} + "set" => { + let s = Set::try_parse(value)?; + parsed_steps.push(s.into()); + } "transform_and_set" => {} "match" => { let m = Match::try_parse(value)?; @@ -55,8 +61,49 @@ pub fn parse_steps(api: &Api, steps: &[Yaml]) -> Result, failure::Erro Ok(parsed_steps) } +pub trait BodyExpr { + /// Builds an indexer expression from the match key + fn body_expr(&self, key: &str) -> String { + if key == "$body" { + key.into() + } else { + let mut values = Vec::new(); + let mut value = String::new(); + let mut chars = key.chars(); + while let Some(ch) = chars.next() { + match ch { + '\\' => { + // consume the next character too + if let Some(next) = chars.next() { + value.push(next); + } + } + '.' => { + values.push(value); + value = String::new(); + } + _ => { + value.push(ch); + } + } + } + values.push(value); + let mut expr = String::new(); + for s in values { + if s.chars().all(char::is_numeric) { + write!(expr, "[{}]", s).unwrap(); + } else { + write!(expr, "[\"{}\"]", s).unwrap(); + } + } + expr + } + } +} + pub enum Step { Skip(Skip), + Set(Set), Do(Do), Match(Match), } diff --git a/yaml_test_runner/src/step/set.rs b/yaml_test_runner/src/step/set.rs new file mode 100644 index 00000000..6716d76b --- /dev/null +++ b/yaml_test_runner/src/step/set.rs @@ -0,0 +1,57 @@ +use inflector::Inflector; +use quote::{ToTokens, Tokens}; + +use super::Step; +use yaml_rust::Yaml; +use crate::step::BodyExpr; + +pub struct Set { + ident: syn::Ident, + expr: String, +} + +impl From for Step { + fn from(set: Set) -> Self { + Step::Set(set) + } +} + +impl BodyExpr for Set {} + +impl Set { + pub fn try_parse(yaml: &Yaml) -> Result { + let hash = yaml + .as_hash() + .ok_or_else(|| failure::err_msg(format!("expected hash but found {:?}", yaml)))?; + + let (expr, id) = { + let (k, yaml) = hash.iter().next().unwrap(); + let key = k.as_str().ok_or_else(|| { + failure::err_msg(format!("Expected string key but found {:?}", k)) + })?; + + let id = yaml.as_str().ok_or_else(|| { + failure::err_msg(format!("Expected string value but found {:?}", k)) + })?; + + (key, id) + }; + + Ok(Set { + ident: syn::Ident::from(id), + expr: expr.into() + }) + } +} + +impl ToTokens for Set { + fn to_tokens(&self, tokens: &mut Tokens) { + let ident = &self.ident; + let expr = syn::Ident::from(self.body_expr(&self.expr)); + + // TODO: Unwrap serde_json value here, or in the usage? + tokens.append(quote!{ + let #ident = response_body#expr.clone(); + }); + } +} \ No newline at end of file From 85a814b62072b8b5a338b10caf19f60fa55d8ece Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Wed, 25 Mar 2020 09:18:00 +1000 Subject: [PATCH 047/127] set values in API parts --- yaml_test_runner/src/step/do.rs | 57 ++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/yaml_test_runner/src/step/do.rs b/yaml_test_runner/src/step/do.rs index ded37b32..b4b71720 100644 --- a/yaml_test_runner/src/step/do.rs +++ b/yaml_test_runner/src/step/do.rs @@ -486,16 +486,11 @@ impl ApiCall { _ => Some(matching_path_parts[0]), } } - }; - - if path.is_none() { - return Err(failure::err_msg(format!( - "No path for '{}' API with URL parts {:?}", - &api_call, parts - ))); - } + }.ok_or_else(|| failure::err_msg(format!( + "No path for '{}' API with URL parts {:?}", + &api_call, parts + )))?; - let path = path.unwrap(); let path_parts = path.path.params(); let variant_name = { let v = path_parts @@ -515,25 +510,37 @@ impl ApiCall { f.cmp(&s) }) .map(|(p, v)| { - let ty = match path.parts.get(*p) { - Some(t) => Ok(t), - None => Err(failure::err_msg(format!( - "No URL part found for {} in {}", - p, &path.path - ))), - }?; + let ty = path.parts.get(*p) + .ok_or_else(|| failure::err_msg(format!( + "No URL part found for {} in {}", + p, &path.path + )))?; match v { - Yaml::String(s) => match ty.ty { - TypeKind::List => { - let values: Vec<&str> = s.split(',').collect(); - Ok(quote! { &[#(#values),*] }) - } - TypeKind::Long => { - let l = s.parse::().unwrap(); - Ok(quote! { #l }) + Yaml::String(s) => { + let is_set_value = s.starts_with('$'); + + match ty.ty { + TypeKind::List => { + let values: Vec<&str> = s.split(',').collect(); + Ok(quote! { &[#(#values),*] }) + } + TypeKind::Long => { + let l = s.parse::().unwrap(); + Ok(quote! { #l }) + } + _ => { + if is_set_value { + let t = s.trim_start_matches('$') + .trim_start_matches('{') + .trim_end_matches('}'); + let ident = syn::Ident::from(t); + Ok(quote! { #ident.as_str().unwrap() }) + } else { + Ok(quote! { #s }) + } + }, } - _ => Ok(quote! { #s }), }, Yaml::Boolean(b) => { let s = b.to_string(); From e61c2d930fb5f560ba05b7176e10b40bd0e6150e Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Wed, 25 Mar 2020 12:22:33 +1000 Subject: [PATCH 048/127] Remove $body from beginning of body expressions --- yaml_test_runner/src/step/mod.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/yaml_test_runner/src/step/mod.rs b/yaml_test_runner/src/step/mod.rs index 4c568df5..20468dc8 100644 --- a/yaml_test_runner/src/step/mod.rs +++ b/yaml_test_runner/src/step/mod.rs @@ -88,10 +88,23 @@ pub trait BodyExpr { } } values.push(value); + + // some APIs specify the response body as the first part of the path + // which should be removed. + if values[0] == "$body".to_string() { + values.remove(0); + } + let mut expr = String::new(); for s in values { if s.chars().all(char::is_numeric) { write!(expr, "[{}]", s).unwrap(); + } else if s.starts_with('$') { + // handle set values + let t = s.trim_start_matches('$') + .trim_start_matches('{') + .trim_end_matches('}'); + write!(expr, "[{}.as_str().unwrap()]", t).unwrap(); } else { write!(expr, "[\"{}\"]", s).unwrap(); } From cf54120e610d840aa3c14273d133ba0500a8ccf9 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Wed, 25 Mar 2020 12:42:43 +1000 Subject: [PATCH 049/127] handle set values in API params --- yaml_test_runner/src/step/do.rs | 87 +++++++++++++++++++++++++----- yaml_test_runner/src/step/match.rs | 16 +++++- 2 files changed, 87 insertions(+), 16 deletions(-) diff --git a/yaml_test_runner/src/step/do.rs b/yaml_test_runner/src/step/do.rs index b4b71720..d648f4a8 100644 --- a/yaml_test_runner/src/step/do.rs +++ b/yaml_test_runner/src/step/do.rs @@ -256,6 +256,8 @@ impl ApiCall { match v { Yaml::String(ref s) => { + let is_set_value = s.starts_with('$'); + match kind { TypeKind::Enum => { if n == &"expand_wildcards" { @@ -307,14 +309,47 @@ impl ApiCall { }); } TypeKind::Integer | TypeKind::Number => { - let i = s.parse::()?; - tokens.append(quote! { - .#param_ident(#i) - }); + if is_set_value { + let t = Self::from_set_value(s); + let ident = syn::Ident::from(t); + tokens.append(quote! { + .#param_ident(#ident.as_i32().unwrap()) + }); + } else { + let i = s.parse::()?; + tokens.append(quote! { + .#param_ident(#i) + }); + } + }, + TypeKind::Long => { + if is_set_value { + let t = Self::from_set_value(s); + let ident = syn::Ident::from(t); + tokens.append(quote! { + .#param_ident(#ident.as_i64().unwrap()) + }); + } else { + let i = s.parse::()?; + tokens.append(quote! { + .#param_ident(#i) + }); + } } - _ => tokens.append(quote! { - .#param_ident(#s) - }), + _ => { + // handle set values + let t = if is_set_value { + let t = Self::from_set_value(s); + let ident = syn::Ident::from(t); + quote!{ #ident.as_str().unwrap() } + } else { + quote! { #s } + }; + + tokens.append(quote! { + .#param_ident(#t) + }) + }, } } Yaml::Boolean(ref b) => match kind { @@ -418,6 +453,12 @@ impl ApiCall { } } + fn from_set_value(s: &str) -> &str { + s.trim_start_matches('$') + .trim_start_matches('{') + .trim_end_matches('}') + } + fn generate_parts( api_call: &str, endpoint: &ApiEndpoint, @@ -522,18 +563,33 @@ impl ApiCall { match ty.ty { TypeKind::List => { - let values: Vec<&str> = s.split(',').collect(); + let values: Vec = s + .split(',') + .map(|s| { + if is_set_value { + let t = Self::from_set_value(s); + let ident = syn::Ident::from(t); + quote! { #ident.as_str().unwrap() } + } else { + quote!{ #s } + } + }) + .collect(); Ok(quote! { &[#(#values),*] }) } TypeKind::Long => { - let l = s.parse::().unwrap(); - Ok(quote! { #l }) + if is_set_value { + let t = Self::from_set_value(s); + let ident = syn::Ident::from(t); + Ok(quote! { #ident.as_i64().unwrap() }) + } else { + let l = s.parse::().unwrap(); + Ok(quote! { #l }) + } } _ => { if is_set_value { - let t = s.trim_start_matches('$') - .trim_start_matches('{') - .trim_end_matches('}'); + let t = Self::from_set_value(s); let ident = syn::Ident::from(t); Ok(quote! { #ident.as_str().unwrap() }) } else { @@ -569,7 +625,10 @@ impl ApiCall { match ok_or_accumulate(&result, 0) { Ok(_) => { let result: Vec<_> = - result.into_iter().filter_map(Result::ok).collect(); + result.into_iter() + .filter_map(Result::ok) + + .collect(); match ty.ty { // Some APIs specify a part is a string in the REST API spec diff --git a/yaml_test_runner/src/step/match.rs b/yaml_test_runner/src/step/match.rs index 42dcb3fa..3bb68a73 100644 --- a/yaml_test_runner/src/step/match.rs +++ b/yaml_test_runner/src/step/match.rs @@ -62,13 +62,25 @@ impl ToTokens for Match { } } else { let ident = syn::Ident::from(expr.clone()); + + // handle set values + let t = if s.starts_with('$') { + let t = s.trim_start_matches('$') + .trim_start_matches('{') + .trim_end_matches('}'); + let ident = syn::Ident::from(t); + quote!{ #ident.as_str().unwrap() } + } else { + quote! { #s } + }; + tokens.append(quote! { assert_eq!( response_body#ident.as_str().unwrap(), - #s, + #t, "expected value at {} to be {} but was {}", #expr, - #s, + #t, response_body#ident.as_str().unwrap() ); }) From 9b540b001b8da081c09ee0cd3f2db6bdc3db510b Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Wed, 25 Mar 2020 12:43:27 +1000 Subject: [PATCH 050/127] Handle replacements in newline delimited string bodies --- yaml_test_runner/src/step/do.rs | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/yaml_test_runner/src/step/do.rs b/yaml_test_runner/src/step/do.rs index d648f4a8..2e559ec4 100644 --- a/yaml_test_runner/src/step/do.rs +++ b/yaml_test_runner/src/step/do.rs @@ -693,10 +693,34 @@ impl ApiCall { match v { Yaml::String(s) => { + let json = Self::replace_values(s.as_str()); if accepts_nd_body { - Some(quote!(.body(vec![#s]))) + // a newline delimited API body may be expressed + // as a scalar string literal style where line breaks are significant (using |) + // or where lines breaks are folded to an empty space unless it ends on an + // empty or a more-indented line (using >) + // see https://yaml.org/spec/1.2/spec.html#id2760844 + // + // need to trim the trailing newline to differentiate... + let contains_newlines = json.trim_end_matches('\n').contains('\n'); + let split = if contains_newlines { + json.split('\n').collect::>() + } else { + json.split(char::is_whitespace).collect::>() + }; + + let values: Vec = split + .into_iter() + .filter(|s| !s.is_empty()) + .map(|s| { + let ident = syn::Ident::from(s); + quote! { JsonBody::from(json!(#ident)) } + }) + .collect(); + Some(quote!(.body(vec![#(#values),*]))) } else { - Some(quote!(.body(#s))) + let ident = syn::Ident::from(json); + Some(quote!(.body(json!{#ident}))) } } _ => { @@ -713,6 +737,7 @@ impl ApiCall { .map(|value| { let mut json = serde_json::to_string(&value).unwrap(); json = Self::replace_values(&json); + let ident = syn::Ident::from(json); if value.is_string() { quote!(#ident) From d5788e6e59fa8903693d1f0e49ec1ee0320f24aa Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Wed, 25 Mar 2020 12:43:56 +1000 Subject: [PATCH 051/127] Reset read response at each do step --- yaml_test_runner/src/generator.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index fdc11b1d..31ab6574 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -163,7 +163,10 @@ impl YamlTests { None } } - Step::Do(d) => d.to_tokens(&mut body), + Step::Do(d) => { + read_response = false; + d.to_tokens(&mut body); + }, Step::Match(m) => { if !read_response { body.append(quote! { From f1260879317b43cdcb66fb088b260dbfc5b7b0ed Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Wed, 25 Mar 2020 13:20:10 +1000 Subject: [PATCH 052/127] handle number params as i64 --- yaml_test_runner/src/step/do.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yaml_test_runner/src/step/do.rs b/yaml_test_runner/src/step/do.rs index 2e559ec4..ec2df69b 100644 --- a/yaml_test_runner/src/step/do.rs +++ b/yaml_test_runner/src/step/do.rs @@ -308,12 +308,12 @@ impl ApiCall { .#param_ident(#f) }); } - TypeKind::Integer | TypeKind::Number => { + TypeKind::Integer => { if is_set_value { let t = Self::from_set_value(s); let ident = syn::Ident::from(t); tokens.append(quote! { - .#param_ident(#ident.as_i32().unwrap()) + .#param_ident(#ident.as_i64().unwrap() as i32) }); } else { let i = s.parse::()?; @@ -322,7 +322,7 @@ impl ApiCall { }); } }, - TypeKind::Long => { + TypeKind::Number | TypeKind::Long => { if is_set_value { let t = Self::from_set_value(s); let ident = syn::Ident::from(t); From 62240625aab3b47aa3432fc83de509e6c7729470 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Fri, 27 Mar 2020 13:04:13 +1000 Subject: [PATCH 053/127] implement length step --- elasticsearch/src/http/response.rs | 21 +++++++--- yaml_test_runner/src/generator.rs | 41 ++++++++++++------ yaml_test_runner/src/main.rs | 1 + yaml_test_runner/src/step/do.rs | 2 - yaml_test_runner/src/step/length.rs | 64 +++++++++++++++++++++++++++++ yaml_test_runner/src/step/match.rs | 25 ++++++----- yaml_test_runner/src/step/mod.rs | 16 ++++++-- yaml_test_runner/src/step/set.rs | 1 - yaml_test_runner/src/util.rs | 19 +++++++++ 9 files changed, 157 insertions(+), 33 deletions(-) create mode 100644 yaml_test_runner/src/step/length.rs create mode 100644 yaml_test_runner/src/util.rs diff --git a/elasticsearch/src/http/response.rs b/elasticsearch/src/http/response.rs index a68dda1a..dae1f785 100644 --- a/elasticsearch/src/http/response.rs +++ b/elasticsearch/src/http/response.rs @@ -16,12 +16,23 @@ impl Response { Self(response) } - /// The HTTP status code of the response + /// Get the content-length of this response, if known. + /// + /// Reasons it may not be known: + /// + /// - The server didn't send a `content-length` header. + /// - The response is compressed and automatically decoded (thus changing + /// the actual decoded length). + pub fn content_length(&self) -> Option { + self.0.content_length() + } + + /// Get the HTTP status code of the response pub fn status_code(&self) -> StatusCode { self.0.status() } - /// Turn the response into an `Error` if Elasticsearch returned an error. + /// Turn the response into an [Error] if Elasticsearch returned an error. pub fn error_for_status_code(self) -> Result { match self.0.error_for_status_ref() { Ok(_) => Ok(self), @@ -29,7 +40,7 @@ impl Response { } } - /// Turn the response into an `Error` if Elasticsearch returned an error. + /// Turn the response into an [Error] if Elasticsearch returned an error. pub fn error_for_status_code_ref(&self) -> Result<&Self, Error> { match self.0.error_for_status_ref() { Ok(_) => Ok(self), @@ -37,12 +48,12 @@ impl Response { } } - /// The response headers + /// Get the response headers pub fn headers(&self) -> &HeaderMap { self.0.headers() } - /// Deprecation warning response headers + /// Get the deprecation warning response headers pub fn warning_headers(&self) -> impl Iterator { self.headers() .get_all("Warning") diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 31ab6574..9085b4d6 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -101,6 +101,7 @@ impl YamlTests { use regex; use serde_json::Value; use crate::client; + use crate::util; #setup_fn #teardown_fn @@ -128,6 +129,24 @@ impl YamlTests { syn::Ident::from(fn_name) } + fn read_response(read_response: bool, is_body_expr: bool, body: &mut Tokens) -> bool { + if !read_response { + // TODO: string_response_body needs to use response.read_body_as_text() + if is_body_expr { + body.append(quote! { + let response_body = response.read_body::().await?; + let string_response_body = serde_json::to_string(&response_body).unwrap(); + }); + } else { + body.append(quote! { + let response_body = response.read_body::().await?; + }); + } + } + + true + } + fn fn_impls(&self, setup_call: Option, teardown_call: Option) -> Vec { let mut seen_method_names = HashSet::new(); @@ -168,22 +187,20 @@ impl YamlTests { d.to_tokens(&mut body); }, Step::Match(m) => { - if !read_response { - body.append(quote! { - let response_body = response.read_body::().await?; - }); - read_response = true; - } + read_response = + Self::read_response(read_response, m.is_body_expr(&m.expr), &mut body); m.to_tokens(&mut body); }, Step::Set(s) => { - if !read_response { - body.append(quote! { - let response_body = response.read_body::().await?; - }); - read_response = true; - } + // TODO: is "set" ever is_body_expr? + read_response = + Self::read_response(read_response, false, &mut body); s.to_tokens(&mut body); + }, + Step::Length(l) => { + read_response = + Self::read_response(read_response, l.is_body_expr(&l.expr), &mut body); + l.to_tokens(&mut body); } } } diff --git a/yaml_test_runner/src/main.rs b/yaml_test_runner/src/main.rs index 76ebc4d4..699df9b9 100644 --- a/yaml_test_runner/src/main.rs +++ b/yaml_test_runner/src/main.rs @@ -19,6 +19,7 @@ pub mod step; mod generated; pub mod client; +pub mod util; fn main() -> Result<(), failure::Error> { let matches = App::new(env!("CARGO_PKG_NAME")) diff --git a/yaml_test_runner/src/step/do.rs b/yaml_test_runner/src/step/do.rs index ec2df69b..a40401db 100644 --- a/yaml_test_runner/src/step/do.rs +++ b/yaml_test_runner/src/step/do.rs @@ -7,8 +7,6 @@ use itertools::Itertools; use regex::Regex; use std::collections::BTreeMap; use yaml_rust::{Yaml, YamlEmitter}; -use syn::StrStyle; -use syn::parse::ident; pub struct Do { api_call: ApiCall, diff --git a/yaml_test_runner/src/step/length.rs b/yaml_test_runner/src/step/length.rs new file mode 100644 index 00000000..15db7662 --- /dev/null +++ b/yaml_test_runner/src/step/length.rs @@ -0,0 +1,64 @@ +use quote::{ToTokens, Tokens}; + +use super::Step; +use yaml_rust::Yaml; +use crate::step::BodyExpr; + +pub struct Length { + len: usize, + pub(crate) expr: String, +} + +impl From for Step { + fn from(length: Length) -> Self { + Step::Length(length) + } +} + +impl BodyExpr for Length {} + +impl Length { + pub fn try_parse(yaml: &Yaml) -> Result { + let hash = yaml + .as_hash() + .ok_or_else(|| failure::err_msg(format!("expected hash but found {:?}", yaml)))?; + + let (expr, len) = { + let (k, v) = hash.iter().next().unwrap(); + let key = k.as_str().ok_or_else(|| { + failure::err_msg(format!("expected string key but found {:?}", k)) + })?; + + let len = v.as_i64().ok_or_else(|| { + failure::err_msg(format!("expected i64 but found {:?}", v)) + })?; + + (key, len) + }; + + Ok(Length { + len: len as usize, + expr: expr.into() + }) + } +} + +impl ToTokens for Length { + fn to_tokens(&self, tokens: &mut Tokens) { + let len = self.len; + let expr = self.body_expr(&self.expr); + + if self.is_body_expr(&expr) { + tokens.append(quote!{ + let len = string_response_body.len(); + assert_eq!(#len, len); + }); + } else { + let ident = syn::Ident::from(expr); + tokens.append(quote!{ + let len = util::len_from_value(&response_body#ident)?; + assert_eq!(#len, len); + }); + } + } +} \ No newline at end of file diff --git a/yaml_test_runner/src/step/match.rs b/yaml_test_runner/src/step/match.rs index 3bb68a73..01a85176 100644 --- a/yaml_test_runner/src/step/match.rs +++ b/yaml_test_runner/src/step/match.rs @@ -1,10 +1,11 @@ use super::Step; use quote::{ToTokens, Tokens}; -use yaml_rust::{yaml::Hash, Yaml}; +use yaml_rust::Yaml; use crate::step::BodyExpr; pub struct Match { - hash: Hash, + pub expr: String, + value: Yaml, } impl From for Step { @@ -21,22 +22,26 @@ impl Match { .as_hash() .ok_or_else(|| failure::err_msg(format!("Expected hash but found {:?}", yaml)))?; - Ok(Match { hash: hash.clone() }) + let (expr, value) = { + let (k, v) = hash.iter().next().unwrap(); + let key = k.as_str().unwrap().trim().to_string(); + (key, v.clone()) + }; + + Ok(Match { expr, value }) } } impl ToTokens for Match { - // TODO: Move this parsing out into Match::try_parse fn to_tokens(&self, tokens: &mut Tokens) { - let (k, v) = self.hash.iter().next().unwrap(); - let key = k.as_str().unwrap().trim(); - let expr = self.body_expr(key); + let expr = self.body_expr(&self.expr); - match v { + match &self.value { Yaml::String(s) => { if s.starts_with('/') { let s = s.trim().trim_matches('/'); - if expr == "$body" { + if self.is_body_expr(&expr) { + // TODO: string_response_body needs to use response.read_body_as_text() tokens.append(quote! { let string_response_body = serde_json::to_string(&response_body).unwrap(); let regex = regex::Regex::new(#s)?; @@ -87,7 +92,7 @@ impl ToTokens for Match { } } Yaml::Integer(i) => { - if expr == "$body" { + if self.is_body_expr(&expr) { panic!("match on $body with integer"); } else { let ident = syn::Ident::from(expr.clone()); diff --git a/yaml_test_runner/src/step/mod.rs b/yaml_test_runner/src/step/mod.rs index 20468dc8..51525ef2 100644 --- a/yaml_test_runner/src/step/mod.rs +++ b/yaml_test_runner/src/step/mod.rs @@ -6,10 +6,12 @@ mod r#do; mod r#match; mod set; mod skip; +mod length; pub use r#do::*; pub use r#match::*; pub use set::*; pub use skip::*; +pub use length::*; pub fn parse_steps(api: &Api, steps: &[Yaml]) -> Result, failure::Error> { let mut parsed_steps: Vec = Vec::new(); @@ -48,7 +50,10 @@ pub fn parse_steps(api: &Api, steps: &[Yaml]) -> Result, failure::Erro "contains" => {} "is_true" => {} "is_false" => {} - "length" => {} + "length" => { + let l = Length::try_parse(value)?; + parsed_steps.push(l.into()) + } "eq" => {} "gte" => {} "lte" => {} @@ -62,9 +67,13 @@ pub fn parse_steps(api: &Api, steps: &[Yaml]) -> Result, failure::Erro } pub trait BodyExpr { + fn is_body_expr(&self, key: &str) -> bool { + key == "$body" + } + /// Builds an indexer expression from the match key fn body_expr(&self, key: &str) -> String { - if key == "$body" { + if self.is_body_expr(key) { key.into() } else { let mut values = Vec::new(); @@ -91,7 +100,7 @@ pub trait BodyExpr { // some APIs specify the response body as the first part of the path // which should be removed. - if values[0] == "$body".to_string() { + if self.is_body_expr(values[0].as_ref()) { values.remove(0); } @@ -119,6 +128,7 @@ pub enum Step { Set(Set), Do(Do), Match(Match), + Length(Length), } impl Step { diff --git a/yaml_test_runner/src/step/set.rs b/yaml_test_runner/src/step/set.rs index 6716d76b..3de68754 100644 --- a/yaml_test_runner/src/step/set.rs +++ b/yaml_test_runner/src/step/set.rs @@ -1,4 +1,3 @@ -use inflector::Inflector; use quote::{ToTokens, Tokens}; use super::Step; diff --git a/yaml_test_runner/src/util.rs b/yaml_test_runner/src/util.rs new file mode 100644 index 00000000..84b2ab91 --- /dev/null +++ b/yaml_test_runner/src/util.rs @@ -0,0 +1,19 @@ +use serde_json::Value; + +pub fn len_from_value(value: &Value) -> Result { + match value { + Value::Number(n) => { + Ok(n.as_i64().unwrap() as usize) + }, + Value::String(s) => { + Ok(s.len()) + }, + Value::Array(a) => { + Ok(a.len()) + }, + Value::Object(o) => { + Ok(o.len()) + }, + v => Err(failure::err_msg(format!("Cannot get length from {:?}", v))) + } +} From c3128b084a7483e70e727b10bdb0a31e6ca9e5bb Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 5 May 2020 15:32:46 +1000 Subject: [PATCH 054/127] re-run api_generator This commit re-runs the api_generator to update the generated client with changes from this branch --- .../src/generated/namespace_clients/async_search.rs | 10 +++++----- elasticsearch/src/generated/namespace_clients/cat.rs | 12 ++++++------ elasticsearch/src/generated/namespace_clients/ml.rs | 12 ++++++------ 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/elasticsearch/src/generated/namespace_clients/async_search.rs b/elasticsearch/src/generated/namespace_clients/async_search.rs index aa2d5e75..7669204a 100644 --- a/elasticsearch/src/generated/namespace_clients/async_search.rs +++ b/elasticsearch/src/generated/namespace_clients/async_search.rs @@ -332,7 +332,7 @@ pub struct AsyncSearchSubmit<'a, 'b, B> { df: Option<&'b str>, docvalue_fields: Option<&'b [&'b str]>, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, explain: Option, filter_path: Option<&'b [&'b str]>, from: Option, @@ -547,7 +547,7 @@ where self } #[doc = "Whether to expand wildcard expression to concrete indices that are open, closed or both."] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -702,8 +702,8 @@ where self } #[doc = "Indicate if the number of documents that match the query should be tracked"] - pub fn track_total_hits(mut self, track_total_hits: TrackTotalHits) -> Self { - self.track_total_hits = Some(track_total_hits); + pub fn track_total_hits>(mut self, track_total_hits: T) -> Self { + self.track_total_hits = Some(track_total_hits.into()); self } #[doc = "Specify whether aggregation and suggester names should be prefixed by their respective types in the response"] @@ -767,7 +767,7 @@ where #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde(rename = "explain")] explain: Option, #[serde( diff --git a/elasticsearch/src/generated/namespace_clients/cat.rs b/elasticsearch/src/generated/namespace_clients/cat.rs index 60d72f6b..86d6ce12 100644 --- a/elasticsearch/src/generated/namespace_clients/cat.rs +++ b/elasticsearch/src/generated/namespace_clients/cat.rs @@ -57,7 +57,7 @@ pub struct CatAliases<'a, 'b> { client: &'a Elasticsearch, parts: CatAliasesParts<'b>, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, filter_path: Option<&'b [&'b str]>, format: Option<&'b str>, h: Option<&'b [&'b str]>, @@ -100,7 +100,7 @@ impl<'a, 'b> CatAliases<'a, 'b> { self } #[doc = "Whether to expand wildcard expression to concrete indices that are open, closed or both."] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -171,7 +171,7 @@ impl<'a, 'b> CatAliases<'a, 'b> { #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", serialize_with = "crate::client::serialize_coll_qs" @@ -1132,7 +1132,7 @@ pub struct CatIndices<'a, 'b> { parts: CatIndicesParts<'b>, bytes: Option, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, filter_path: Option<&'b [&'b str]>, format: Option<&'b str>, h: Option<&'b [&'b str]>, @@ -1191,7 +1191,7 @@ impl<'a, 'b> CatIndices<'a, 'b> { self } #[doc = "Whether to expand wildcard expression to concrete indices that are open, closed or both."] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -1289,7 +1289,7 @@ impl<'a, 'b> CatIndices<'a, 'b> { #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", serialize_with = "crate::client::serialize_coll_qs" diff --git a/elasticsearch/src/generated/namespace_clients/ml.rs b/elasticsearch/src/generated/namespace_clients/ml.rs index 72aa8d80..206ca246 100644 --- a/elasticsearch/src/generated/namespace_clients/ml.rs +++ b/elasticsearch/src/generated/namespace_clients/ml.rs @@ -5081,7 +5081,7 @@ pub struct MlPutDatafeed<'a, 'b, B> { allow_no_indices: Option, body: Option, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, filter_path: Option<&'b [&'b str]>, headers: HeaderMap, human: Option, @@ -5145,7 +5145,7 @@ where self } #[doc = "Whether source index expressions should get expanded to open or closed indices (default: open)"] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -5198,7 +5198,7 @@ where #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", serialize_with = "crate::client::serialize_coll_qs" @@ -6191,7 +6191,7 @@ pub struct MlUpdateDatafeed<'a, 'b, B> { allow_no_indices: Option, body: Option, error_trace: Option, - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, filter_path: Option<&'b [&'b str]>, headers: HeaderMap, human: Option, @@ -6255,7 +6255,7 @@ where self } #[doc = "Whether source index expressions should get expanded to open or closed indices (default: open)"] - pub fn expand_wildcards(mut self, expand_wildcards: ExpandWildcards) -> Self { + pub fn expand_wildcards(mut self, expand_wildcards: &'b [ExpandWildcards]) -> Self { self.expand_wildcards = Some(expand_wildcards); self } @@ -6308,7 +6308,7 @@ where #[serde(rename = "error_trace")] error_trace: Option, #[serde(rename = "expand_wildcards")] - expand_wildcards: Option, + expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", serialize_with = "crate::client::serialize_coll_qs" From 2a4e8676030d60fe135b5edbe38b61d9f134cc8b Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 5 May 2020 16:16:48 +1000 Subject: [PATCH 055/127] Use json() and text() response functions --- yaml_test_runner/src/generator.rs | 6 ++---- yaml_test_runner/src/step/length.rs | 14 ++++++++++---- yaml_test_runner/src/step/match.rs | 2 -- yaml_test_runner/src/step/mod.rs | 3 ++- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 9085b4d6..8864a438 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -131,15 +131,13 @@ impl YamlTests { fn read_response(read_response: bool, is_body_expr: bool, body: &mut Tokens) -> bool { if !read_response { - // TODO: string_response_body needs to use response.read_body_as_text() if is_body_expr { body.append(quote! { - let response_body = response.read_body::().await?; - let string_response_body = serde_json::to_string(&response_body).unwrap(); + let string_response_body = response.text().await?; }); } else { body.append(quote! { - let response_body = response.read_body::().await?; + let response_body = response.json::().await?; }); } } diff --git a/yaml_test_runner/src/step/length.rs b/yaml_test_runner/src/step/length.rs index 15db7662..ebb75801 100644 --- a/yaml_test_runner/src/step/length.rs +++ b/yaml_test_runner/src/step/length.rs @@ -15,7 +15,13 @@ impl From for Step { } } -impl BodyExpr for Length {} +impl BodyExpr for Length { + // a length step should never advertise itself as a body expression as it would + // cause the body of the preceding API call to be returned as text rather than serde::Value. + fn is_body_expr(&self, key: &str) -> bool { + false + } +} impl Length { pub fn try_parse(yaml: &Yaml) -> Result { @@ -46,14 +52,14 @@ impl Length { impl ToTokens for Length { fn to_tokens(&self, tokens: &mut Tokens) { let len = self.len; - let expr = self.body_expr(&self.expr); - if self.is_body_expr(&expr) { + if &self.expr == "$body" { tokens.append(quote!{ - let len = string_response_body.len(); + let len = util::len_from_value(&response_body)?; assert_eq!(#len, len); }); } else { + let expr = self.body_expr(&self.expr); let ident = syn::Ident::from(expr); tokens.append(quote!{ let len = util::len_from_value(&response_body#ident)?; diff --git a/yaml_test_runner/src/step/match.rs b/yaml_test_runner/src/step/match.rs index 01a85176..5913592f 100644 --- a/yaml_test_runner/src/step/match.rs +++ b/yaml_test_runner/src/step/match.rs @@ -41,9 +41,7 @@ impl ToTokens for Match { if s.starts_with('/') { let s = s.trim().trim_matches('/'); if self.is_body_expr(&expr) { - // TODO: string_response_body needs to use response.read_body_as_text() tokens.append(quote! { - let string_response_body = serde_json::to_string(&response_body).unwrap(); let regex = regex::Regex::new(#s)?; assert!( regex.is_match(&string_response_body), diff --git a/yaml_test_runner/src/step/mod.rs b/yaml_test_runner/src/step/mod.rs index 51525ef2..19be6b2e 100644 --- a/yaml_test_runner/src/step/mod.rs +++ b/yaml_test_runner/src/step/mod.rs @@ -71,7 +71,8 @@ pub trait BodyExpr { key == "$body" } - /// Builds an indexer expression from the match key + /// Builds an indexer expression from the match key e.g. + /// match key `2.airline` is converted to `[2]["airline"]` fn body_expr(&self, key: &str) -> String { if self.is_body_expr(key) { key.into() From a81996cbb06230a11ab6eb880dcbb2083e844589 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 7 May 2020 12:18:29 +1000 Subject: [PATCH 056/127] Handle url serialization of enum collections This commit updates serialize_coll_qs to handle serializing a collection of types that implement Serialize to a comma separated query string value. This is only needed for enums and strings, but (mis)uses serde_json serialize to achieve this, since serde_urlencoded does not support such a format. We could implement a Serializer for this down the line. --- .../code_gen/request/request_builder.rs | 3 +- elasticsearch/src/client.rs | 25 +++- .../namespace_clients/async_search.rs | 5 +- .../src/generated/namespace_clients/cat.rs | 10 +- .../generated/namespace_clients/cluster.rs | 10 +- .../generated/namespace_clients/indices.rs | 135 ++++++++++++++---- .../src/generated/namespace_clients/ml.rs | 10 +- elasticsearch/src/generated/root.rs | 35 ++++- 8 files changed, 185 insertions(+), 48 deletions(-) diff --git a/api_generator/src/generator/code_gen/request/request_builder.rs b/api_generator/src/generator/code_gen/request/request_builder.rs index b7f66679..be1f87df 100644 --- a/api_generator/src/generator/code_gen/request/request_builder.rs +++ b/api_generator/src/generator/code_gen/request/request_builder.rs @@ -108,7 +108,8 @@ impl<'a> RequestBuilder<'a> { let struct_fields = endpoint_params.iter().map(|(param_name, param_type)| { let field = Self::create_struct_field((param_name, param_type)); let field_rename = lit(param_name); - if param_type.ty == TypeKind::List { + // TODO: we special case expand_wildcards here to be a list, but this should be fixed upstream + if param_type.ty == TypeKind::List || param_name == "expand_wildcards" { let serialize_with = lit("crate::client::serialize_coll_qs"); quote! { #[serde(rename = #field_rename, serialize_with = #serialize_with)] diff --git a/elasticsearch/src/client.rs b/elasticsearch/src/client.rs index 2dbf8d58..3d7a7300 100644 --- a/elasticsearch/src/client.rs +++ b/elasticsearch/src/client.rs @@ -5,21 +5,34 @@ use crate::{ use serde::{Serialize, Serializer}; -/// Serializes an `Option<&[&str]>` with +/// Serializes an `Option<&[Serialize]>` with /// `Some(value)` to a comma separated string of values. /// Used to serialize values within the query string -pub fn serialize_coll_qs( - value: &Option<&[&str]>, +pub(crate) fn serialize_coll_qs( + value: &Option<&[T]>, serializer: S, ) -> Result<::Ok, ::Error> where S: Serializer, + T: Serialize { let vec = value - .as_ref() .expect("attempt to serialize Option::None value"); - let joined = vec.join(","); - serializer.serialize_str(joined.as_ref()) + + // TODO: There must be a better way of serializing a Vec to a comma-separated url encoded string... + // (mis)use serde_json to_string and trim the surrounding quotes... + let serialized = vec + .iter() + .map(|v| serde_json::to_string(v).unwrap()) + .collect::>(); + + let target = serialized + .iter() + .map(|s| s.trim_matches('"')) + .collect::>() + .join(","); + + serializer.serialize_str(&target) } /// Root client for top level APIs diff --git a/elasticsearch/src/generated/namespace_clients/async_search.rs b/elasticsearch/src/generated/namespace_clients/async_search.rs index 7669204a..afcf1fcf 100644 --- a/elasticsearch/src/generated/namespace_clients/async_search.rs +++ b/elasticsearch/src/generated/namespace_clients/async_search.rs @@ -766,7 +766,10 @@ where docvalue_fields: Option<&'b [&'b str]>, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde(rename = "explain")] explain: Option, diff --git a/elasticsearch/src/generated/namespace_clients/cat.rs b/elasticsearch/src/generated/namespace_clients/cat.rs index 86d6ce12..ffa2df75 100644 --- a/elasticsearch/src/generated/namespace_clients/cat.rs +++ b/elasticsearch/src/generated/namespace_clients/cat.rs @@ -170,7 +170,10 @@ impl<'a, 'b> CatAliases<'a, 'b> { struct QueryParams<'b> { #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", @@ -1288,7 +1291,10 @@ impl<'a, 'b> CatIndices<'a, 'b> { bytes: Option, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", diff --git a/elasticsearch/src/generated/namespace_clients/cluster.rs b/elasticsearch/src/generated/namespace_clients/cluster.rs index 2114fba9..78e0df78 100644 --- a/elasticsearch/src/generated/namespace_clients/cluster.rs +++ b/elasticsearch/src/generated/namespace_clients/cluster.rs @@ -508,7 +508,10 @@ impl<'a, 'b> ClusterHealth<'a, 'b> { struct QueryParams<'b> { #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", @@ -1338,7 +1341,10 @@ impl<'a, 'b> ClusterState<'a, 'b> { allow_no_indices: Option, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", diff --git a/elasticsearch/src/generated/namespace_clients/indices.rs b/elasticsearch/src/generated/namespace_clients/indices.rs index 32261d3e..b8da8bb6 100644 --- a/elasticsearch/src/generated/namespace_clients/indices.rs +++ b/elasticsearch/src/generated/namespace_clients/indices.rs @@ -364,7 +364,10 @@ where allow_no_indices: Option, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde(rename = "fielddata")] fielddata: Option, @@ -749,7 +752,10 @@ where allow_no_indices: Option, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", @@ -1101,7 +1107,10 @@ impl<'a, 'b> IndicesDelete<'a, 'b> { allow_no_indices: Option, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", @@ -1551,7 +1560,10 @@ impl<'a, 'b> IndicesExists<'a, 'b> { allow_no_indices: Option, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", @@ -1726,7 +1738,10 @@ impl<'a, 'b> IndicesExistsAlias<'a, 'b> { allow_no_indices: Option, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", @@ -2033,7 +2048,10 @@ impl<'a, 'b> IndicesExistsType<'a, 'b> { allow_no_indices: Option, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", @@ -2231,7 +2249,10 @@ where allow_no_indices: Option, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", @@ -2416,7 +2437,10 @@ where allow_no_indices: Option, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", @@ -2616,7 +2640,10 @@ where allow_no_indices: Option, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", @@ -2821,7 +2848,10 @@ where allow_no_indices: Option, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", @@ -3012,7 +3042,10 @@ impl<'a, 'b> IndicesGet<'a, 'b> { allow_no_indices: Option, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", @@ -3206,7 +3239,10 @@ impl<'a, 'b> IndicesGetAlias<'a, 'b> { allow_no_indices: Option, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", @@ -3418,7 +3454,10 @@ impl<'a, 'b> IndicesGetFieldMapping<'a, 'b> { allow_no_indices: Option, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", @@ -3620,7 +3659,10 @@ impl<'a, 'b> IndicesGetMapping<'a, 'b> { allow_no_indices: Option, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", @@ -3829,7 +3871,10 @@ impl<'a, 'b> IndicesGetSettings<'a, 'b> { allow_no_indices: Option, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", @@ -4152,7 +4197,10 @@ impl<'a, 'b> IndicesGetUpgrade<'a, 'b> { allow_no_indices: Option, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", @@ -4349,7 +4397,10 @@ where allow_no_indices: Option, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", @@ -4738,7 +4789,10 @@ where allow_no_indices: Option, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", @@ -4955,7 +5009,10 @@ where allow_no_indices: Option, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", @@ -5469,7 +5526,10 @@ where allow_no_indices: Option, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", @@ -5645,7 +5705,10 @@ where allow_no_indices: Option, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", @@ -6007,7 +6070,10 @@ impl<'a, 'b> IndicesSegments<'a, 'b> { allow_no_indices: Option, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", @@ -6168,7 +6234,10 @@ impl<'a, 'b> IndicesShardStores<'a, 'b> { allow_no_indices: Option, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", @@ -6763,7 +6832,10 @@ impl<'a, 'b> IndicesStats<'a, 'b> { completion_fields: Option<&'b [&'b str]>, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "fielddata_fields", @@ -6983,7 +7055,10 @@ where allow_no_indices: Option, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", @@ -7339,7 +7414,10 @@ where allow_no_indices: Option, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", @@ -7619,7 +7697,10 @@ where df: Option<&'b str>, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde(rename = "explain")] explain: Option, diff --git a/elasticsearch/src/generated/namespace_clients/ml.rs b/elasticsearch/src/generated/namespace_clients/ml.rs index 206ca246..2e0bce99 100644 --- a/elasticsearch/src/generated/namespace_clients/ml.rs +++ b/elasticsearch/src/generated/namespace_clients/ml.rs @@ -5197,7 +5197,10 @@ where allow_no_indices: Option, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", @@ -6307,7 +6310,10 @@ where allow_no_indices: Option, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", diff --git a/elasticsearch/src/generated/root.rs b/elasticsearch/src/generated/root.rs index 0bf4ae12..696b5807 100644 --- a/elasticsearch/src/generated/root.rs +++ b/elasticsearch/src/generated/root.rs @@ -683,7 +683,10 @@ where df: Option<&'b str>, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", @@ -1602,7 +1605,10 @@ where df: Option<&'b str>, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", @@ -2932,7 +2938,10 @@ where allow_no_indices: Option, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde(rename = "fields", serialize_with = "crate::client::serialize_coll_qs")] fields: Option<&'b [&'b str]>, @@ -6444,7 +6453,10 @@ where docvalue_fields: Option<&'b [&'b str]>, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde(rename = "explain")] explain: Option, @@ -6748,7 +6760,10 @@ where allow_no_indices: Option, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", @@ -7031,7 +7046,10 @@ where ccs_minimize_roundtrips: Option, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde(rename = "explain")] explain: Option, @@ -8109,7 +8127,10 @@ where df: Option<&'b str>, #[serde(rename = "error_trace")] error_trace: Option, - #[serde(rename = "expand_wildcards")] + #[serde( + rename = "expand_wildcards", + serialize_with = "crate::client::serialize_coll_qs" + )] expand_wildcards: Option<&'b [ExpandWildcards]>, #[serde( rename = "filter_path", From f9dc7f0a695976f30acfdb98f02692f9825f1789 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 7 May 2020 12:18:56 +1000 Subject: [PATCH 057/127] tidy up --- yaml_test_runner/src/main.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yaml_test_runner/src/main.rs b/yaml_test_runner/src/main.rs index 699df9b9..ed4a0e80 100644 --- a/yaml_test_runner/src/main.rs +++ b/yaml_test_runner/src/main.rs @@ -59,12 +59,12 @@ fn main() -> Result<(), failure::Error> { github::download_test_suites(token, branch, &download_dir)?; - let mut last_downloaded_rest_spec_version = rest_specs_dir.clone(); - last_downloaded_rest_spec_version.push("last_downloaded_version"); + let mut last_downloaded_rest_spec_branch = rest_specs_dir.clone(); + last_downloaded_rest_spec_branch.push("last_downloaded_version"); let mut download_rest_specs = true; - if last_downloaded_rest_spec_version.exists() { - let version = fs::read_to_string(last_downloaded_rest_spec_version) + if last_downloaded_rest_spec_branch.exists() { + let version = fs::read_to_string(last_downloaded_rest_spec_branch) .expect("Could not read rest specs last_downloaded version into string"); if version == branch { From 3451c8dc91d9d6d4437f5cd5999ac16e80abe1e5 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 7 May 2020 12:20:26 +1000 Subject: [PATCH 058/127] Implement catch on do steps --- yaml_test_runner/src/generator.rs | 11 ++-- yaml_test_runner/src/step/do.rs | 85 ++++++++++++++++++++++++++++--- 2 files changed, 84 insertions(+), 12 deletions(-) diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 8864a438..149cc3ac 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -129,14 +129,14 @@ impl YamlTests { syn::Ident::from(fn_name) } - fn read_response(read_response: bool, is_body_expr: bool, body: &mut Tokens) -> bool { + fn read_response(read_response: bool, is_body_expr: bool, tokens: &mut Tokens) -> bool { if !read_response { if is_body_expr { - body.append(quote! { + tokens.append(quote! { let string_response_body = response.text().await?; }); } else { - body.append(quote! { + tokens.append(quote! { let response_body = response.json::().await?; }); } @@ -181,8 +181,7 @@ impl YamlTests { } } Step::Do(d) => { - read_response = false; - d.to_tokens(&mut body); + read_response = d.to_tokens(false, &mut body); }, Step::Match(m) => { read_response = @@ -233,7 +232,7 @@ impl YamlTests { .filter_map(Step::r#do) .map(|d| { let mut tokens = Tokens::new(); - d.to_tokens(&mut tokens); + ToTokens::to_tokens(d, &mut tokens); tokens }) .collect::>(); diff --git a/yaml_test_runner/src/step/do.rs b/yaml_test_runner/src/step/do.rs index 4adcdb84..3f186937 100644 --- a/yaml_test_runner/src/step/do.rs +++ b/yaml_test_runner/src/step/do.rs @@ -8,16 +8,67 @@ use regex::Regex; use std::collections::BTreeMap; use yaml_rust::{Yaml, YamlEmitter}; +/// A catch expression on a do step +pub struct Catch(String); + +impl Catch { + fn needs_response_body(&self) -> bool { + self.0.starts_with('/') + } +} + +impl ToTokens for Catch { + fn to_tokens(&self, tokens: &mut Tokens) { + fn http_status_code(status_code: u16, tokens: &mut Tokens) { + tokens.append(quote! { + assert_eq!(response.status_code().as_u16(), #status_code); + }); + } + + match self.0.as_ref() { + "bad_request" => http_status_code(400, tokens), + "unauthorized" => http_status_code(401, tokens), + "forbidden" => http_status_code(403, tokens), + "missing" => http_status_code(404, tokens), + "request_timeout" => http_status_code(408, tokens), + "conflict" => http_status_code(409, tokens), + "request" => { + tokens.append(quote! { + let status_code = response.status_code().as_u16(); + assert!(status_code >= 400 && status_code < 600); + }); + }, + "unavailable" => http_status_code(503, tokens), + "param" => { + // Not possible to pass a bad param to the client so ignore. + }, + s => { + // trim the enclosing forward slashes and replace escaped forward slashes + let t = s.trim_matches('/').replace("\\/", "/"); + tokens.append(quote!{ + let catch_regex = regex::Regex::new(#t)?; + assert!( + catch_regex.is_match(response_body["error"]["reason"].as_str().unwrap()), + "expected value at {}:\n\n{}\n\nto match regex:\n\n{}", + "[\"error\"][\"reason\"]", + response_body["error"]["reason"].as_str().unwrap(), + #s + ); + }); + }, + } + } +} + pub struct Do { api_call: ApiCall, warnings: Vec, - catch: Option, + catch: Option, } impl ToTokens for Do { fn to_tokens(&self, tokens: &mut Tokens) { - // TODO: Add in catch and warnings - &self.api_call.to_tokens(tokens); + let _ = self.to_tokens(false, tokens); } } @@ -28,6 +79,23 @@ impl From for Step { } impl Do { + pub fn to_tokens(&self, mut read_response: bool, tokens: &mut Tokens) -> bool { + // TODO: Add in warnings + &self.api_call.to_tokens(tokens); + + if let Some(c) = &self.catch { + if !read_response && c.needs_response_body() { + read_response = true; + tokens.append(quote! { + let response_body = response.json::().await?; + }); + } + c.to_tokens(tokens); + } + + read_response + } + pub fn try_parse(api: &Api, yaml: &Yaml) -> Result { let hash = yaml .as_hash() @@ -59,7 +127,7 @@ impl Do { Ok(()) } "catch" => { - catch = v.as_str().map(|s| s.to_string()); + catch = v.as_str().map(|s| Catch(s.to_string())); Ok(()) } "node_selector" => { @@ -119,10 +187,15 @@ impl ToTokens for ApiCall { let body = &self.body; // TODO: handle "set" values + let headers: Vec = self.headers .iter() - .map(|(k,v)| quote! { - .header(HeaderName::from_static(#k), HeaderValue::from_static(#v)) + .map(|(k,v)| { + // header names **must** be lowercase to satisfy Header lib + let k = k.to_lowercase(); + quote! { + .header(HeaderName::from_static(#k), HeaderValue::from_static(#v)) + } }) .collect(); From 292df79523057c624e15776c8d9a02028af61042 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 7 May 2020 12:20:46 +1000 Subject: [PATCH 059/127] add oss teardown --- yaml_test_runner/src/client.rs | 64 ++++++++++++++++++++++++++++++- yaml_test_runner/src/generator.rs | 9 ++++- 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/yaml_test_runner/src/client.rs b/yaml_test_runner/src/client.rs index b863ed53..e19403f2 100644 --- a/yaml_test_runner/src/client.rs +++ b/yaml_test_runner/src/client.rs @@ -2,10 +2,14 @@ use elasticsearch::{ auth::Credentials, cert::CertificateValidation, http::transport::{SingleNodeConnectionPool, TransportBuilder}, - Elasticsearch, DEFAULT_ADDRESS, + Elasticsearch, DEFAULT_ADDRESS, Error }; use sysinfo::SystemExt; use url::Url; +use elasticsearch::indices::{IndicesDeleteParts, IndicesDeleteTemplateParts}; +use elasticsearch::params::ExpandWildcards; +use elasticsearch::cat::{CatTemplatesParts, CatSnapshotsParts}; +use elasticsearch::snapshot::{SnapshotDeleteParts, SnapshotDeleteRepositoryParts}; fn cluster_addr() -> String { match std::env::var("ES_TEST_SERVER") { @@ -38,3 +42,61 @@ pub fn create() -> Elasticsearch { let transport = builder.build().unwrap(); Elasticsearch::new(transport) } + +pub async fn general_oss_teardown(client: &Elasticsearch) -> Result<(), Error> { + let _delete_response = client.indices() + .delete(IndicesDeleteParts::Index(&["*"])) + .expand_wildcards(&[ExpandWildcards::Open, ExpandWildcards::Closed, ExpandWildcards::Hidden]) + .send() + .await?; + + let cat_template_response = client.cat() + .templates(CatTemplatesParts::Name("*")) + .h(&["name"]) + .send() + .await? + .text() + .await?; + + let all_templates: Vec<&str> = cat_template_response + .split('\n') + .filter(|s| !s.is_empty() && !s.starts_with('.') && s != &"security-audit-log") + .collect(); + + for template in all_templates { + let _delete_template_response = client.indices() + .delete_template(IndicesDeleteTemplateParts::Name(&template)) + .send() + .await?; + } + + let cat_snapshot_response = client.cat() + .snapshots(CatSnapshotsParts::None) + .h(&["id", "repository"]) + .send() + .await? + .text() + .await?; + + let all_snapshots: Vec<(&str, &str)> = cat_snapshot_response + .split('\n') + .map(|s| s.split(" ").collect::>()) + .filter(|s| s.len() == 2) + .map(|s| (s[0].trim(), s[1].trim())) + .filter(|(id, repo)| !id.is_empty() && !repo.is_empty()) + .collect(); + + for (id, repo) in all_snapshots { + let _snapshot_response = client.snapshot() + .delete(SnapshotDeleteParts::RepositorySnapshot(&repo, &id)) + .send() + .await?; + } + + let _delete_repo_response = client.snapshot() + .delete_repository(SnapshotDeleteRepositoryParts::Repository(&["*"])) + .send() + .await?; + + Ok(()) +} diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 149cc3ac..abd962e4 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -76,7 +76,11 @@ impl YamlTests { pub fn build(self) -> Tokens { let (setup_fn, setup_call) = Self::generate_fixture(&self.setup); let (teardown_fn, teardown_call) = Self::generate_fixture(&self.teardown); - let tests: Vec = self.fn_impls(setup_call, teardown_call); + // TODO: create an x-pack teardown and call this when an x-pack test + let general_teardown_call = quote! { + client::general_oss_teardown(&client).await?; + }; + let tests: Vec = self.fn_impls(setup_call, teardown_call, general_teardown_call); let directives: Vec = self .directives @@ -145,7 +149,7 @@ impl YamlTests { true } - fn fn_impls(&self, setup_call: Option, teardown_call: Option) -> Vec { + fn fn_impls(&self, setup_call: Option, teardown_call: Option, general_teardown_call: Tokens) -> Vec { let mut seen_method_names = HashSet::new(); self.tests @@ -212,6 +216,7 @@ impl YamlTests { #setup_call #body #teardown_call + #general_teardown_call Ok(()) } }, From ca2aae7363ba6694e63035ce2220f21dcd643fc3 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 7 May 2020 15:05:04 +1000 Subject: [PATCH 060/127] add xpack teardown call --- yaml_test_runner/src/client.rs | 184 ++++++++++++++++++++++++++---- yaml_test_runner/src/generator.rs | 38 +++++- 2 files changed, 196 insertions(+), 26 deletions(-) diff --git a/yaml_test_runner/src/client.rs b/yaml_test_runner/src/client.rs index e19403f2..9a7aac30 100644 --- a/yaml_test_runner/src/client.rs +++ b/yaml_test_runner/src/client.rs @@ -10,6 +10,12 @@ use elasticsearch::indices::{IndicesDeleteParts, IndicesDeleteTemplateParts}; use elasticsearch::params::ExpandWildcards; use elasticsearch::cat::{CatTemplatesParts, CatSnapshotsParts}; use elasticsearch::snapshot::{SnapshotDeleteParts, SnapshotDeleteRepositoryParts}; +use elasticsearch::watcher::WatcherDeleteWatchParts; +use serde_json::Value; +use std::future::Future; +use elasticsearch::security::{SecurityGetRoleParts, SecurityDeleteRoleParts, SecurityGetUserParts, SecurityDeleteUserParts, SecurityGetPrivilegesParts, SecurityDeletePrivilegesParts}; +use elasticsearch::ml::{MlStopDatafeedParts, MlGetDatafeedsParts, MlDeleteDatafeedParts, MlCloseJobParts, MlGetJobsParts, MlDeleteJobParts}; +use elasticsearch::ilm::IlmRemovePolicyParts; fn cluster_addr() -> String { match std::env::var("ES_TEST_SERVER") { @@ -43,13 +49,7 @@ pub fn create() -> Elasticsearch { Elasticsearch::new(transport) } -pub async fn general_oss_teardown(client: &Elasticsearch) -> Result<(), Error> { - let _delete_response = client.indices() - .delete(IndicesDeleteParts::Index(&["*"])) - .expand_wildcards(&[ExpandWildcards::Open, ExpandWildcards::Closed, ExpandWildcards::Hidden]) - .send() - .await?; - +async fn delete_all_templates(client: &Elasticsearch) -> Result<(), Error> { let cat_template_response = client.cat() .templates(CatTemplatesParts::Name("*")) .h(&["name"]) @@ -70,33 +70,177 @@ pub async fn general_oss_teardown(client: &Elasticsearch) -> Result<(), Error> { .await?; } + Ok(()) +} + +/// general teardown step for an OSS yaml test +pub async fn general_oss_teardown(client: &Elasticsearch) -> Result<(), Error> { + let _delete_response = client.indices() + .delete(IndicesDeleteParts::Index(&["*"])) + .expand_wildcards(&[ExpandWildcards::Open, ExpandWildcards::Closed, ExpandWildcards::Hidden]) + .send() + .await?; + + delete_all_templates(client).await?; + let cat_snapshot_response = client.cat() .snapshots(CatSnapshotsParts::None) .h(&["id", "repository"]) .send() + .await?; + + if cat_snapshot_response.status_code().is_success() { + let cat_snapshot_text = cat_snapshot_response + .text() + .await?; + + let all_snapshots: Vec<(&str, &str)> = cat_snapshot_text + .split('\n') + .map(|s| s.split(" ").collect::>()) + .filter(|s| s.len() == 2) + .map(|s| (s[0].trim(), s[1].trim())) + .filter(|(id, repo)| !id.is_empty() && !repo.is_empty()) + .collect(); + + for (id, repo) in all_snapshots { + let _snapshot_response = client.snapshot() + .delete(SnapshotDeleteParts::RepositorySnapshot(&repo, &id)) + .send() + .await?; + } + } + + let _delete_repo_response = client.snapshot() + .delete_repository(SnapshotDeleteRepositoryParts::Repository(&["*"])) + .send() + .await?; + + Ok(()) +} + +pub async fn general_xpack_teardown(client: &Elasticsearch) -> Result<(), Error> { + delete_all_templates(client).await?; + + let _delete_watch_response = client.watcher() + .delete_watch(WatcherDeleteWatchParts::Id("my_watch")) + .send() + .await?; + + let roles_response = client.security() + .get_role(SecurityGetRoleParts::None) + .send() .await? - .text() + .json::() .await?; - let all_snapshots: Vec<(&str, &str)> = cat_snapshot_response - .split('\n') - .map(|s| s.split(" ").collect::>()) - .filter(|s| s.len() == 2) - .map(|s| (s[0].trim(), s[1].trim())) - .filter(|(id, repo)| !id.is_empty() && !repo.is_empty()) - .collect(); + for (k, v) in roles_response.as_object().unwrap() { + if let Some(b) = v["metadata"]["_reserved"].as_bool() { + if !b { + let _ = client.security() + .delete_role(SecurityDeleteRoleParts::Name(k)) + .send() + .await?; + } + } + } - for (id, repo) in all_snapshots { - let _snapshot_response = client.snapshot() - .delete(SnapshotDeleteParts::RepositorySnapshot(&repo, &id)) + let users_response = client.security() + .get_user(SecurityGetUserParts::None) + .send() + .await? + .json::() + .await?; + + for (k, v) in users_response.as_object().unwrap() { + if let Some(b) = v["metadata"]["_reserved"].as_bool() { + if !b { + let _ = client.security() + .delete_user(SecurityDeleteUserParts::Username(k)) + .send() + .await?; + } + } + } + + delete_privileges(client).await?; + stop_and_delete_datafeeds(client).await?; + + let _ = client.ilm() + .remove_policy(IlmRemovePolicyParts::Index("_all")) + .send() + .await?; + + close_and_delete_jobs(client).await?; + + Ok(()) +} + +async fn delete_privileges(client: &Elasticsearch) -> Result<(), Error> { + let privileges_response = client.security() + .get_privileges(SecurityGetPrivilegesParts::None) + .send() + .await? + .json::() + .await?; + + for (k, v) in privileges_response.as_object().unwrap() { + if let Some(b) = v["metadata"]["_reserved"].as_bool() { + if !b { + let _ = client.security() + .delete_privileges(SecurityDeletePrivilegesParts::ApplicationName(k, "_all")) + .send() + .await?; + } + } + } + + Ok(()) +} + +async fn stop_and_delete_datafeeds(client: &Elasticsearch) -> Result<(), Error> { + let _stop_data_feed_response = client.ml() + .stop_datafeed(MlStopDatafeedParts::DatafeedId("_all")) + .send() + .await?; + + let get_data_feeds_response = client.ml() + .get_datafeeds(MlGetDatafeedsParts::None) + .send() + .await? + .json::() + .await?; + + for feed in get_data_feeds_response["datafeeds"].as_array().unwrap() { + let id = feed["datafeed_id"].as_str().unwrap(); + let _ = client.ml() + .delete_datafeed(MlDeleteDatafeedParts::DatafeedId(id)) .send() .await?; } - let _delete_repo_response = client.snapshot() - .delete_repository(SnapshotDeleteRepositoryParts::Repository(&["*"])) + Ok(()) +} + +async fn close_and_delete_jobs(client: &Elasticsearch) -> Result<(), Error> { + let _ = client.ml() + .close_job(MlCloseJobParts::JobId("_all")) + .send() + .await?; + + let get_jobs_response = client.ml() + .get_jobs(MlGetJobsParts::JobId("_all")) .send() + .await? + .json::() .await?; + for job in get_jobs_response["jobs"].as_array().unwrap() { + let id = job["job_id"].as_str().unwrap(); + let _ = client.ml() + .delete_job(MlDeleteJobParts::JobId(id)) + .send() + .await?; + } + Ok(()) } diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index abd962e4..bd07d9fc 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -9,12 +9,18 @@ use std::collections::HashSet; use std::fs; use std::fs::{File, OpenOptions}; use std::io::Write; -use std::path::PathBuf; +use std::path::{PathBuf, Component}; use yaml_rust::{Yaml, YamlLoader}; +pub enum TestSuite { + Oss, + XPack +} + /// The components of a test file, constructed from a yaml file struct YamlTests { version: Option, + suite: TestSuite, directives: HashSet, setup: Option, teardown: Option, @@ -22,9 +28,10 @@ struct YamlTests { } impl YamlTests { - pub fn new(version: Option, len: usize) -> Self { + pub fn new(version: Option, suite: TestSuite, len: usize) -> Self { Self { version, + suite, directives: HashSet::with_capacity(len), setup: None, teardown: None, @@ -76,10 +83,11 @@ impl YamlTests { pub fn build(self) -> Tokens { let (setup_fn, setup_call) = Self::generate_fixture(&self.setup); let (teardown_fn, teardown_call) = Self::generate_fixture(&self.teardown); - // TODO: create an x-pack teardown and call this when an x-pack test - let general_teardown_call = quote! { - client::general_oss_teardown(&client).await?; + let general_teardown_call = match self.suite { + TestSuite::Oss => quote!(client::general_oss_teardown(&client).await?;), + TestSuite::XPack => quote!(client::general_xpack_teardown(&client).await?;), }; + let tests: Vec = self.fn_impls(setup_call, teardown_call, general_teardown_call); let directives: Vec = self @@ -308,8 +316,26 @@ pub fn generate_tests_from_yaml( continue; } + let suite = { + let path = entry.path().clone(); + let mut components = path.strip_prefix(&base_download_dir)?.components(); + let mut relative = "".to_string(); + while let Some(c) = components.next() { + if c != Component::RootDir { + relative = c.as_os_str().to_string_lossy().into_owned(); + break; + } + } + + match relative.as_str() { + "oss" => TestSuite::Oss, + "xpack" => TestSuite::XPack, + _ => panic!("Unknown test suite") + } + }; + let docs = result.unwrap(); - let mut test = YamlTests::new(api.version(), docs.len()); + let mut test = YamlTests::new(api.version(), suite, docs.len()); let results : Vec> = docs .iter() From 5adf5086a3f72e37cdb92c4842bd40643689c18c Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 7 May 2020 18:05:33 +1000 Subject: [PATCH 061/127] complete xpack teardown change to setup functions, so that they run before each test --- yaml_test_runner/src/client.rs | 207 ++++++++++++++++++++++-------- yaml_test_runner/src/generator.rs | 13 +- 2 files changed, 161 insertions(+), 59 deletions(-) diff --git a/yaml_test_runner/src/client.rs b/yaml_test_runner/src/client.rs index 9a7aac30..ac81671a 100644 --- a/yaml_test_runner/src/client.rs +++ b/yaml_test_runner/src/client.rs @@ -6,16 +6,19 @@ use elasticsearch::{ }; use sysinfo::SystemExt; use url::Url; -use elasticsearch::indices::{IndicesDeleteParts, IndicesDeleteTemplateParts}; -use elasticsearch::params::ExpandWildcards; +use elasticsearch::indices::{IndicesDeleteParts, IndicesDeleteTemplateParts, IndicesRefreshParts}; +use elasticsearch::params::{ExpandWildcards, WaitForStatus}; use elasticsearch::cat::{CatTemplatesParts, CatSnapshotsParts}; use elasticsearch::snapshot::{SnapshotDeleteParts, SnapshotDeleteRepositoryParts}; use elasticsearch::watcher::WatcherDeleteWatchParts; -use serde_json::Value; +use serde_json::{json, Value}; use std::future::Future; -use elasticsearch::security::{SecurityGetRoleParts, SecurityDeleteRoleParts, SecurityGetUserParts, SecurityDeleteUserParts, SecurityGetPrivilegesParts, SecurityDeletePrivilegesParts}; +use elasticsearch::security::{SecurityGetRoleParts, SecurityDeleteRoleParts, SecurityGetUserParts, SecurityDeleteUserParts, SecurityGetPrivilegesParts, SecurityDeletePrivilegesParts, SecurityPutUserParts}; use elasticsearch::ml::{MlStopDatafeedParts, MlGetDatafeedsParts, MlDeleteDatafeedParts, MlCloseJobParts, MlGetJobsParts, MlDeleteJobParts}; use elasticsearch::ilm::IlmRemovePolicyParts; +use elasticsearch::tasks::TasksCancelParts; +use elasticsearch::transform::{TransformGetTransformParts, TransformStopTransformParts, TransformDeleteTransformParts}; +use elasticsearch::cluster::ClusterHealthParts; fn cluster_addr() -> String { match std::env::var("ES_TEST_SERVER") { @@ -49,39 +52,10 @@ pub fn create() -> Elasticsearch { Elasticsearch::new(transport) } -async fn delete_all_templates(client: &Elasticsearch) -> Result<(), Error> { - let cat_template_response = client.cat() - .templates(CatTemplatesParts::Name("*")) - .h(&["name"]) - .send() - .await? - .text() - .await?; - - let all_templates: Vec<&str> = cat_template_response - .split('\n') - .filter(|s| !s.is_empty() && !s.starts_with('.') && s != &"security-audit-log") - .collect(); - - for template in all_templates { - let _delete_template_response = client.indices() - .delete_template(IndicesDeleteTemplateParts::Name(&template)) - .send() - .await?; - } - - Ok(()) -} - -/// general teardown step for an OSS yaml test -pub async fn general_oss_teardown(client: &Elasticsearch) -> Result<(), Error> { - let _delete_response = client.indices() - .delete(IndicesDeleteParts::Index(&["*"])) - .expand_wildcards(&[ExpandWildcards::Open, ExpandWildcards::Closed, ExpandWildcards::Hidden]) - .send() - .await?; - - delete_all_templates(client).await?; +/// general setup step for an OSS yaml test +pub async fn general_oss_setup(client: &Elasticsearch) -> Result<(), Error> { + delete_indices(client).await?; + delete_templates(client).await?; let cat_snapshot_response = client.cat() .snapshots(CatSnapshotsParts::None) @@ -118,32 +92,147 @@ pub async fn general_oss_teardown(client: &Elasticsearch) -> Result<(), Error> { Ok(()) } -pub async fn general_xpack_teardown(client: &Elasticsearch) -> Result<(), Error> { - delete_all_templates(client).await?; +/// general setup step for an xpack yaml test +pub async fn general_xpack_setup(client: &Elasticsearch) -> Result<(), Error> { + delete_templates(client).await?; let _delete_watch_response = client.watcher() .delete_watch(WatcherDeleteWatchParts::Id("my_watch")) .send() .await?; - let roles_response = client.security() - .get_role(SecurityGetRoleParts::None) + delete_roles(client).await?; + delete_users(client).await?; + delete_privileges(client).await?; + stop_and_delete_datafeeds(client).await?; + + let _ = client.ilm() + .remove_policy(IlmRemovePolicyParts::Index("_all")) + .send() + .await?; + + close_and_delete_jobs(client).await?; + + // TODO: stop and delete rollup jobs once implemented in the client + + cancel_tasks(client).await?; + stop_and_delete_transforms(client).await?; + wait_for_yellow_status(client).await?; + delete_indices(client).await?; + + let _ = client.security() + .put_user(SecurityPutUserParts::Username("x_pack_rest_user")) + .body(json!({ + "password": "x-pack-test-password", + "roles": ["superuser"] + })) + .send() + .await?; + + let _ = client.indices() + .refresh(IndicesRefreshParts::Index(&["_all"])) + .expand_wildcards(&[ExpandWildcards::Open, ExpandWildcards::Closed, ExpandWildcards::Hidden]) + .send() + .await?; + + wait_for_yellow_status(client).await?; + + Ok(()) +} + +async fn wait_for_yellow_status(client: &Elasticsearch) -> Result<(), Error> { + let _ = client.cluster() + .health(ClusterHealthParts::None) + .wait_for_status(WaitForStatus::Yellow) + .send() + .await?; + + Ok(()) +} + +async fn delete_indices(client: &Elasticsearch) -> Result<(), Error> { + let _delete_response = client.indices() + .delete(IndicesDeleteParts::Index(&["*"])) + .expand_wildcards(&[ExpandWildcards::Open, ExpandWildcards::Closed, ExpandWildcards::Hidden]) + .send() + .await?; + + Ok(()) +} + +async fn stop_and_delete_transforms(client: &Elasticsearch) -> Result<(), Error> { + let transforms_response = client.transform() + .get_transform(TransformGetTransformParts::TransformId("_all")) .send() .await? .json::() .await?; - for (k, v) in roles_response.as_object().unwrap() { - if let Some(b) = v["metadata"]["_reserved"].as_bool() { - if !b { - let _ = client.security() - .delete_role(SecurityDeleteRoleParts::Name(k)) - .send() - .await?; + for transform in transforms_response["transforms"].as_array().unwrap() { + let id = transform["id"].as_str().unwrap(); + let _ = client.transform() + .stop_transform(TransformStopTransformParts::TransformId(id)) + .send() + .await?; + + let _ = client.transform() + .delete_transform(TransformDeleteTransformParts::TransformId(id)) + .send() + .await?; + } + + Ok(()) +} + +async fn cancel_tasks(client: &Elasticsearch) -> Result<(), Error> { + let rollup_response = client.tasks() + .list() + .send() + .await? + .json::() + .await?; + + for (_node_id, nodes) in rollup_response["nodes"].as_object().unwrap() { + for (task_id, task) in nodes["tasks"].as_object().unwrap() { + if let Some(b) = task["cancellable"].as_bool() { + if b { + let _ = client.tasks() + .cancel(TasksCancelParts::TaskId(task_id)) + .send() + .await?; + } } } } + Ok(()) +} + +async fn delete_templates(client: &Elasticsearch) -> Result<(), Error> { + let cat_template_response = client.cat() + .templates(CatTemplatesParts::Name("*")) + .h(&["name"]) + .send() + .await? + .text() + .await?; + + let all_templates: Vec<&str> = cat_template_response + .split('\n') + .filter(|s| !s.is_empty() && !s.starts_with('.') && s != &"security-audit-log") + .collect(); + + for template in all_templates { + let _delete_template_response = client.indices() + .delete_template(IndicesDeleteTemplateParts::Name(&template)) + .send() + .await?; + } + + Ok(()) +} + +async fn delete_users(client: &Elasticsearch) -> Result<(), Error> { let users_response = client.security() .get_user(SecurityGetUserParts::None) .send() @@ -162,15 +251,27 @@ pub async fn general_xpack_teardown(client: &Elasticsearch) -> Result<(), Error> } } - delete_privileges(client).await?; - stop_and_delete_datafeeds(client).await?; + Ok(()) +} - let _ = client.ilm() - .remove_policy(IlmRemovePolicyParts::Index("_all")) +async fn delete_roles(client: &Elasticsearch) -> Result<(), Error> { + let roles_response = client.security() + .get_role(SecurityGetRoleParts::None) .send() + .await? + .json::() .await?; - close_and_delete_jobs(client).await?; + for (k, v) in roles_response.as_object().unwrap() { + if let Some(b) = v["metadata"]["_reserved"].as_bool() { + if !b { + let _ = client.security() + .delete_role(SecurityDeleteRoleParts::Name(k)) + .send() + .await?; + } + } + } Ok(()) } diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index bd07d9fc..8ea9934e 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -80,15 +80,16 @@ impl YamlTests { self } + /// Generates the AST for the Yaml test file pub fn build(self) -> Tokens { let (setup_fn, setup_call) = Self::generate_fixture(&self.setup); let (teardown_fn, teardown_call) = Self::generate_fixture(&self.teardown); - let general_teardown_call = match self.suite { - TestSuite::Oss => quote!(client::general_oss_teardown(&client).await?;), - TestSuite::XPack => quote!(client::general_xpack_teardown(&client).await?;), + let general_setup_call = match self.suite { + TestSuite::Oss => quote!(client::general_oss_setup(&client).await?;), + TestSuite::XPack => quote!(client::general_xpack_setup(&client).await?;), }; - let tests: Vec = self.fn_impls(setup_call, teardown_call, general_teardown_call); + let tests: Vec = self.fn_impls(general_setup_call, setup_call, teardown_call, ); let directives: Vec = self .directives @@ -157,7 +158,7 @@ impl YamlTests { true } - fn fn_impls(&self, setup_call: Option, teardown_call: Option, general_teardown_call: Tokens) -> Vec { + fn fn_impls(&self, general_setup_call: Tokens, setup_call: Option, teardown_call: Option) -> Vec { let mut seen_method_names = HashSet::new(); self.tests @@ -221,10 +222,10 @@ impl YamlTests { #[tokio::test] async fn #fn_name() -> Result<(), failure::Error> { let client = client::create(); + #general_setup_call #setup_call #body #teardown_call - #general_teardown_call Ok(()) } }, From e82723943325d8189be98674587646b1ae77de05 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 7 May 2020 18:41:58 +1000 Subject: [PATCH 062/127] Fix Match step regex --- yaml_test_runner/src/step/match.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/yaml_test_runner/src/step/match.rs b/yaml_test_runner/src/step/match.rs index 5913592f..60538acb 100644 --- a/yaml_test_runner/src/step/match.rs +++ b/yaml_test_runner/src/step/match.rs @@ -39,7 +39,8 @@ impl ToTokens for Match { match &self.value { Yaml::String(s) => { if s.starts_with('/') { - let s = s.trim().trim_matches('/'); + // trim the enclosing forward slashes and replace escaped forward slashes + let s = s.trim().trim_matches('/').replace("\\/", "/"); if self.is_body_expr(&expr) { tokens.append(quote! { let regex = regex::Regex::new(#s)?; From 9e2c991ea80625d18feb5824c67dfeb990f93239 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Fri, 8 May 2020 13:39:58 +1000 Subject: [PATCH 063/127] ignore pattern whitespace in regex --- yaml_test_runner/src/step/match.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/yaml_test_runner/src/step/match.rs b/yaml_test_runner/src/step/match.rs index 60538acb..b10db21c 100644 --- a/yaml_test_runner/src/step/match.rs +++ b/yaml_test_runner/src/step/match.rs @@ -2,6 +2,7 @@ use super::Step; use quote::{ToTokens, Tokens}; use yaml_rust::Yaml; use crate::step::BodyExpr; +use regex::Regex; pub struct Match { pub expr: String, @@ -43,7 +44,9 @@ impl ToTokens for Match { let s = s.trim().trim_matches('/').replace("\\/", "/"); if self.is_body_expr(&expr) { tokens.append(quote! { - let regex = regex::Regex::new(#s)?; + let regex = regex::RegexBuilder::new(#s) + .ignore_whitespace(true) + .build()?; assert!( regex.is_match(&string_response_body), "expected $body:\n\n{}\n\nto match regex:\n\n{}", From e165612fe93460e6e6b88d069d63faded586233c Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Fri, 8 May 2020 13:40:37 +1000 Subject: [PATCH 064/127] Determine version from ELASTICSEARCH_VERSION env var --- api_generator/src/generator/mod.rs | 7 ------- yaml_test_runner/src/client.rs | 1 + yaml_test_runner/src/generator.rs | 13 +++++++------ yaml_test_runner/src/main.rs | 19 ++++++++++++++++++- 4 files changed, 26 insertions(+), 14 deletions(-) diff --git a/api_generator/src/generator/mod.rs b/api_generator/src/generator/mod.rs index 3b90d8d3..fee6fa07 100644 --- a/api_generator/src/generator/mod.rs +++ b/api_generator/src/generator/mod.rs @@ -35,13 +35,6 @@ pub struct Api { } impl Api { - /// Attempt to parse the version from the commit tag, which typically - /// will be of the form e.g. v7.6.1 - pub fn version(&self) -> Option { - let v = self.commit.trim_start_matches('v'); - semver::Version::parse(v).ok() - } - /// Find the right ApiEndpoint from the REST API specs for the API call /// defined in the YAML test. /// diff --git a/yaml_test_runner/src/client.rs b/yaml_test_runner/src/client.rs index ac81671a..f8344ca3 100644 --- a/yaml_test_runner/src/client.rs +++ b/yaml_test_runner/src/client.rs @@ -32,6 +32,7 @@ fn running_proxy() -> bool { !system.get_process_by_name("Fiddler").is_empty() } +/// create a client to use in tests pub fn create() -> Elasticsearch { let url = Url::parse(cluster_addr().as_ref()).unwrap(); let conn_pool = SingleNodeConnectionPool::new(url.clone()); diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 8ea9934e..494388bc 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -18,8 +18,8 @@ pub enum TestSuite { } /// The components of a test file, constructed from a yaml file -struct YamlTests { - version: Option, +struct YamlTests<'a> { + version: &'a Option, suite: TestSuite, directives: HashSet, setup: Option, @@ -27,8 +27,8 @@ struct YamlTests { tests: Vec, } -impl YamlTests { - pub fn new(version: Option, suite: TestSuite, len: usize) -> Self { +impl<'a> YamlTests<'a> { + pub fn new(version: &'a Option, suite: TestSuite, len: usize) -> Self { Self { version, suite, @@ -287,6 +287,7 @@ impl TestFn { pub fn generate_tests_from_yaml( api: &Api, + version: &Option, base_download_dir: &PathBuf, download_dir: &PathBuf, generated_dir: &PathBuf, @@ -296,7 +297,7 @@ pub fn generate_tests_from_yaml( if let Ok(entry) = entry { if let Ok(file_type) = entry.file_type() { if file_type.is_dir() { - generate_tests_from_yaml(api, base_download_dir, &entry.path(), generated_dir)?; + generate_tests_from_yaml(api, version, base_download_dir, &entry.path(), generated_dir)?; } else if file_type.is_file() { let file_name = entry.file_name().to_string_lossy().into_owned(); @@ -336,7 +337,7 @@ pub fn generate_tests_from_yaml( }; let docs = result.unwrap(); - let mut test = YamlTests::new(api.version(), suite, docs.len()); + let mut test = YamlTests::new(version, suite, docs.len()); let results : Vec> = docs .iter() diff --git a/yaml_test_runner/src/main.rs b/yaml_test_runner/src/main.rs index ed4a0e80..407c1a26 100644 --- a/yaml_test_runner/src/main.rs +++ b/yaml_test_runner/src/main.rs @@ -86,7 +86,24 @@ fn main() -> Result<(), failure::Error> { if generated_dir.exists() { fs::remove_dir_all(&generated_dir)?; } - generator::generate_tests_from_yaml(&api, &download_dir, &download_dir, &generated_dir)?; + + // try to get the version from ELASTICSEARCH_VERSION environment variable, if set. + // any prerelease part needs to be trimmed because the semver crate only allows + // a version with a prerelease to match against predicates, if at least one predicate + // has a prerelease. See + // https://github.com/steveklabnik/semver/blob/afa5fc853cb4d6d2b1329579e5528f86f3b550f9/src/version_req.rs#L319-L331 + let version = match std::env::var("ELASTICSEARCH_VERSION") { + Ok(v) => { + let v = v + .trim_start_matches("elasticsearch:") + .trim_end_matches(|c: char| c.is_alphabetic() || c == '-'); + semver::Version::parse(v).ok() + }, + Err(_) => None + }; + + println!("Using version {:?} to compile tests", &version); + generator::generate_tests_from_yaml(&api, &version, &download_dir, &download_dir, &generated_dir)?; Ok(()) } From 7466930ec1f70b90a33b501bc33242ea7e4d06dc Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Fri, 8 May 2020 13:47:59 +1000 Subject: [PATCH 065/127] Introduce simple_logger --- yaml_test_runner/Cargo.toml | 2 ++ yaml_test_runner/src/client.rs | 1 - yaml_test_runner/src/main.rs | 12 +++++++++--- yaml_test_runner/src/step/match.rs | 1 - 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/yaml_test_runner/Cargo.toml b/yaml_test_runner/Cargo.toml index 9ea87477..3ffe7e39 100644 --- a/yaml_test_runner/Cargo.toml +++ b/yaml_test_runner/Cargo.toml @@ -15,6 +15,7 @@ failure = "0.1.6" itertools = "0.8.2" Inflector = "0.11.4" lazy_static = "1.4.0" +log = "0.4.8" quote = "~0.3" regex = "1.3.1" reqwest = "~0.9" @@ -22,6 +23,7 @@ semver = "0.9.0" serde = "~1" serde_yaml = "0.8.11" serde_json = { version = "~1", features = ["arbitrary_precision"] } +simple_logger = "1.6.0" syn = { version = "~0.11", features = ["full"] } sysinfo = "0.9.6" url = "2.1.1" diff --git a/yaml_test_runner/src/client.rs b/yaml_test_runner/src/client.rs index f8344ca3..bfe17c25 100644 --- a/yaml_test_runner/src/client.rs +++ b/yaml_test_runner/src/client.rs @@ -12,7 +12,6 @@ use elasticsearch::cat::{CatTemplatesParts, CatSnapshotsParts}; use elasticsearch::snapshot::{SnapshotDeleteParts, SnapshotDeleteRepositoryParts}; use elasticsearch::watcher::WatcherDeleteWatchParts; use serde_json::{json, Value}; -use std::future::Future; use elasticsearch::security::{SecurityGetRoleParts, SecurityDeleteRoleParts, SecurityGetUserParts, SecurityDeleteUserParts, SecurityGetPrivilegesParts, SecurityDeletePrivilegesParts, SecurityPutUserParts}; use elasticsearch::ml::{MlStopDatafeedParts, MlGetDatafeedsParts, MlDeleteDatafeedParts, MlCloseJobParts, MlGetJobsParts, MlDeleteJobParts}; use elasticsearch::ilm::IlmRemovePolicyParts; diff --git a/yaml_test_runner/src/main.rs b/yaml_test_runner/src/main.rs index 407c1a26..36e48389 100644 --- a/yaml_test_runner/src/main.rs +++ b/yaml_test_runner/src/main.rs @@ -1,4 +1,7 @@ #[macro_use] +extern crate log; +extern crate simple_logger; +#[macro_use] extern crate lazy_static; #[macro_use] extern crate quote; @@ -10,6 +13,7 @@ extern crate serde_json; use clap::{App, Arg}; use std::fs; use std::path::PathBuf; +use log::Level; mod generator; mod github; @@ -22,6 +26,8 @@ pub mod client; pub mod util; fn main() -> Result<(), failure::Error> { + simple_logger::init_with_level(Level::Info).unwrap(); + let matches = App::new(env!("CARGO_PKG_NAME")) .about(env!("CARGO_PKG_DESCRIPTION")) .arg(Arg::with_name("branch") @@ -68,7 +74,7 @@ fn main() -> Result<(), failure::Error> { .expect("Could not read rest specs last_downloaded version into string"); if version == branch { - println!( + info!( "rest specs for branch {} already downloaded in {:?}", branch, &rest_specs_dir ); @@ -98,11 +104,11 @@ fn main() -> Result<(), failure::Error> { .trim_start_matches("elasticsearch:") .trim_end_matches(|c: char| c.is_alphabetic() || c == '-'); semver::Version::parse(v).ok() - }, + } Err(_) => None }; - println!("Using version {:?} to compile tests", &version); + info!("Using version {:?} to compile tests", &version); generator::generate_tests_from_yaml(&api, &version, &download_dir, &download_dir, &generated_dir)?; Ok(()) diff --git a/yaml_test_runner/src/step/match.rs b/yaml_test_runner/src/step/match.rs index b10db21c..1c975fae 100644 --- a/yaml_test_runner/src/step/match.rs +++ b/yaml_test_runner/src/step/match.rs @@ -2,7 +2,6 @@ use super::Step; use quote::{ToTokens, Tokens}; use yaml_rust::Yaml; use crate::step::BodyExpr; -use regex::Regex; pub struct Match { pub expr: String, From 0786bd5ae903d3bc247ad581ddb194bac94b313e Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Mon, 11 May 2020 10:50:02 +1000 Subject: [PATCH 066/127] generates tests only for test suite This commit generates tests only for the test suite determined from ELASTICSEARCH_VERSION env var --- elasticsearch/build.rs | 2 +- elasticsearch/src/client.rs | 5 +- elasticsearch/src/http/response.rs | 2 +- elasticsearch/src/http/transport.rs | 8 +- elasticsearch/tests/cert.rs | 86 ++++++++--------- elasticsearch/tests/client.rs | 32 ++++--- yaml_test_runner/src/client.rs | 138 ++++++++++++++++---------- yaml_test_runner/src/generator.rs | 144 ++++++++++++++++------------ yaml_test_runner/src/main.rs | 59 ++++++++---- yaml_test_runner/src/step/do.rs | 135 ++++++++++++++------------ yaml_test_runner/src/step/length.rs | 19 ++-- yaml_test_runner/src/step/match.rs | 15 +-- yaml_test_runner/src/step/mod.rs | 21 +++- yaml_test_runner/src/step/set.rs | 8 +- yaml_test_runner/src/util.rs | 18 +--- 15 files changed, 397 insertions(+), 295 deletions(-) diff --git a/elasticsearch/build.rs b/elasticsearch/build.rs index 9b21a39b..a179598a 100644 --- a/elasticsearch/build.rs +++ b/elasticsearch/build.rs @@ -16,4 +16,4 @@ fn main() { println!("cargo:rustc-cfg=RUSTC_IS_DEV"); } } -} \ No newline at end of file +} diff --git a/elasticsearch/src/client.rs b/elasticsearch/src/client.rs index 3d7a7300..87648d25 100644 --- a/elasticsearch/src/client.rs +++ b/elasticsearch/src/client.rs @@ -14,10 +14,9 @@ pub(crate) fn serialize_coll_qs( ) -> Result<::Ok, ::Error> where S: Serializer, - T: Serialize + T: Serialize, { - let vec = value - .expect("attempt to serialize Option::None value"); + let vec = value.expect("attempt to serialize Option::None value"); // TODO: There must be a better way of serializing a Vec to a comma-separated url encoded string... // (mis)use serde_json to_string and trim the surrounding quotes... diff --git a/elasticsearch/src/http/response.rs b/elasticsearch/src/http/response.rs index 8ac27814..dab2059d 100644 --- a/elasticsearch/src/http/response.rs +++ b/elasticsearch/src/http/response.rs @@ -80,5 +80,5 @@ impl Response { pub async fn text(self) -> Result { let body = self.0.text().await?; Ok(body) - } + } } diff --git a/elasticsearch/src/http/transport.rs b/elasticsearch/src/http/transport.rs index 2382603d..6900feb6 100644 --- a/elasticsearch/src/http/transport.rs +++ b/elasticsearch/src/http/transport.rs @@ -6,8 +6,8 @@ use crate::{ error::Error, http::{ headers::{ - HeaderMap, HeaderName, HeaderValue, ACCEPT, AUTHORIZATION, CONTENT_TYPE, DEFAULT_ACCEPT, - DEFAULT_CONTENT_TYPE, DEFAULT_USER_AGENT, USER_AGENT, + HeaderMap, HeaderName, HeaderValue, ACCEPT, AUTHORIZATION, CONTENT_TYPE, + DEFAULT_ACCEPT, DEFAULT_CONTENT_TYPE, DEFAULT_USER_AGENT, USER_AGENT, }, request::Body, response::Response, @@ -86,7 +86,7 @@ pub struct TransportBuilder { proxy: Option, proxy_credentials: Option, disable_proxy: bool, - headers: HeaderMap + headers: HeaderMap, } impl TransportBuilder { @@ -104,7 +104,7 @@ impl TransportBuilder { proxy: None, proxy_credentials: None, disable_proxy: false, - headers: HeaderMap::new() + headers: HeaderMap::new(), } } diff --git a/elasticsearch/tests/cert.rs b/elasticsearch/tests/cert.rs index ec21b1e2..c99ac586 100644 --- a/elasticsearch/tests/cert.rs +++ b/elasticsearch/tests/cert.rs @@ -19,7 +19,7 @@ fn expected_error_message() -> String { let os = os_type::current_platform(); match os.os_type { OSType::OSX => "The certificate was not trusted".to_string(), - _ => "unable to get local issuer certificate".to_string() + _ => "unable to get local issuer certificate".to_string(), } } } @@ -96,31 +96,27 @@ async fn full_certificate_validation() -> Result<(), failure::Error> { let result = client.ping().send().await; let os_type = os_type::current_platform(); match os_type.os_type { - OSType::OSX => { - match result { - Ok(_) => Ok(()), - Err(e) => Err(failure::err_msg(e.to_string())), - } + OSType::OSX => match result { + Ok(_) => Ok(()), + Err(e) => Err(failure::err_msg(e.to_string())), }, - _ => { - match result { - Ok(response) => Err(failure::err_msg(format!( - "Expected error but response was {}", - response.status_code() - ))), - Err(e) => { - let expected = expected_error_message(); - let actual = e.to_string(); - assert!( - actual.contains(&expected), - "Expected error message to contain '{}' but was '{}'", - expected, - actual - ); - Ok(()) - } + _ => match result { + Ok(response) => Err(failure::err_msg(format!( + "Expected error but response was {}", + response.status_code() + ))), + Err(e) => { + let expected = expected_error_message(); + let actual = e.to_string(); + assert!( + actual.contains(&expected), + "Expected error message to contain '{}' but was '{}'", + expected, + actual + ); + Ok(()) } - } + }, } } @@ -148,31 +144,27 @@ async fn certificate_certificate_validation() -> Result<(), failure::Error> { let result = client.ping().send().await; let os_type = os_type::current_platform(); match os_type.os_type { - OSType::OSX => { - match result { - Ok(_) => Ok(()), - Err(e) => Err(failure::err_msg(e.to_string())), - } + OSType::OSX => match result { + Ok(_) => Ok(()), + Err(e) => Err(failure::err_msg(e.to_string())), }, - _ => { - match result { - Ok(response) => Err(failure::err_msg(format!( - "Expected error but response was {}", - response.status_code() - ))), - Err(e) => { - let expected = expected_error_message(); - let actual = e.to_string(); - assert!( - actual.contains(&expected), - "Expected error message to contain '{}' but was '{}'", - expected, - actual - ); - Ok(()) - } + _ => match result { + Ok(response) => Err(failure::err_msg(format!( + "Expected error but response was {}", + response.status_code() + ))), + Err(e) => { + let expected = expected_error_message(); + let actual = e.to_string(); + assert!( + actual.contains(&expected), + "Expected error message to contain '{}' but was '{}'", + expected, + actual + ); + Ok(()) } - } + }, } } diff --git a/elasticsearch/tests/client.rs b/elasticsearch/tests/client.rs index 8022953a..bafffa70 100644 --- a/elasticsearch/tests/client.rs +++ b/elasticsearch/tests/client.rs @@ -37,9 +37,10 @@ async fn default_header() -> Result<(), failure::Error> { http::Response::default() }); - let builder = client::create_builder(format!("http://{}", server.addr()).as_ref()) - .header(HeaderName::from_static(X_OPAQUE_ID), - HeaderValue::from_static("foo")); + let builder = client::create_builder(format!("http://{}", server.addr()).as_ref()).header( + HeaderName::from_static(X_OPAQUE_ID), + HeaderValue::from_static("foo"), + ); let client = client::create(builder); let _response = client.ping().send().await?; @@ -54,15 +55,20 @@ async fn override_default_header() -> Result<(), failure::Error> { http::Response::default() }); - let builder = client::create_builder(format!("http://{}", server.addr()).as_ref()) - .header(HeaderName::from_static(X_OPAQUE_ID), - HeaderValue::from_static("foo")); + let builder = client::create_builder(format!("http://{}", server.addr()).as_ref()).header( + HeaderName::from_static(X_OPAQUE_ID), + HeaderValue::from_static("foo"), + ); let client = client::create(builder); - let _response = client.ping() - .header(HeaderName::from_static(X_OPAQUE_ID), - HeaderValue::from_static("bar")) - .send().await?; + let _response = client + .ping() + .header( + HeaderName::from_static(X_OPAQUE_ID), + HeaderValue::from_static("bar"), + ) + .send() + .await?; Ok(()) } @@ -93,7 +99,7 @@ async fn deprecation_warning_headers() -> Result<(), failure::Error> { let _ = index_documents(&client).await?; let response = client .search(SearchParts::None) - .body(json!{ + .body(json! { { "aggs": { "titles": { @@ -125,7 +131,9 @@ async fn deprecation_warning_headers() -> Result<(), failure::Error> { let warnings = response.warning_headers().collect::>(); assert!(warnings.len() > 0); - assert!(warnings.iter().any(|&w| w.contains("Deprecated aggregation order key"))); + assert!(warnings + .iter() + .any(|&w| w.contains("Deprecated aggregation order key"))); Ok(()) } diff --git a/yaml_test_runner/src/client.rs b/yaml_test_runner/src/client.rs index bfe17c25..52dcbb4a 100644 --- a/yaml_test_runner/src/client.rs +++ b/yaml_test_runner/src/client.rs @@ -1,23 +1,31 @@ +use elasticsearch::cat::{CatSnapshotsParts, CatTemplatesParts}; +use elasticsearch::cluster::ClusterHealthParts; +use elasticsearch::ilm::IlmRemovePolicyParts; +use elasticsearch::indices::{IndicesDeleteParts, IndicesDeleteTemplateParts, IndicesRefreshParts}; +use elasticsearch::ml::{ + MlCloseJobParts, MlDeleteDatafeedParts, MlDeleteJobParts, MlGetDatafeedsParts, MlGetJobsParts, + MlStopDatafeedParts, +}; +use elasticsearch::params::{ExpandWildcards, WaitForStatus}; +use elasticsearch::security::{ + SecurityDeletePrivilegesParts, SecurityDeleteRoleParts, SecurityDeleteUserParts, + SecurityGetPrivilegesParts, SecurityGetRoleParts, SecurityGetUserParts, SecurityPutUserParts, +}; +use elasticsearch::snapshot::{SnapshotDeleteParts, SnapshotDeleteRepositoryParts}; +use elasticsearch::tasks::TasksCancelParts; +use elasticsearch::transform::{ + TransformDeleteTransformParts, TransformGetTransformParts, TransformStopTransformParts, +}; +use elasticsearch::watcher::WatcherDeleteWatchParts; use elasticsearch::{ auth::Credentials, cert::CertificateValidation, http::transport::{SingleNodeConnectionPool, TransportBuilder}, - Elasticsearch, DEFAULT_ADDRESS, Error + Elasticsearch, Error, DEFAULT_ADDRESS, }; +use serde_json::{json, Value}; use sysinfo::SystemExt; use url::Url; -use elasticsearch::indices::{IndicesDeleteParts, IndicesDeleteTemplateParts, IndicesRefreshParts}; -use elasticsearch::params::{ExpandWildcards, WaitForStatus}; -use elasticsearch::cat::{CatTemplatesParts, CatSnapshotsParts}; -use elasticsearch::snapshot::{SnapshotDeleteParts, SnapshotDeleteRepositoryParts}; -use elasticsearch::watcher::WatcherDeleteWatchParts; -use serde_json::{json, Value}; -use elasticsearch::security::{SecurityGetRoleParts, SecurityDeleteRoleParts, SecurityGetUserParts, SecurityDeleteUserParts, SecurityGetPrivilegesParts, SecurityDeletePrivilegesParts, SecurityPutUserParts}; -use elasticsearch::ml::{MlStopDatafeedParts, MlGetDatafeedsParts, MlDeleteDatafeedParts, MlCloseJobParts, MlGetJobsParts, MlDeleteJobParts}; -use elasticsearch::ilm::IlmRemovePolicyParts; -use elasticsearch::tasks::TasksCancelParts; -use elasticsearch::transform::{TransformGetTransformParts, TransformStopTransformParts, TransformDeleteTransformParts}; -use elasticsearch::cluster::ClusterHealthParts; fn cluster_addr() -> String { match std::env::var("ES_TEST_SERVER") { @@ -57,16 +65,15 @@ pub async fn general_oss_setup(client: &Elasticsearch) -> Result<(), Error> { delete_indices(client).await?; delete_templates(client).await?; - let cat_snapshot_response = client.cat() + let cat_snapshot_response = client + .cat() .snapshots(CatSnapshotsParts::None) .h(&["id", "repository"]) .send() .await?; if cat_snapshot_response.status_code().is_success() { - let cat_snapshot_text = cat_snapshot_response - .text() - .await?; + let cat_snapshot_text = cat_snapshot_response.text().await?; let all_snapshots: Vec<(&str, &str)> = cat_snapshot_text .split('\n') @@ -77,14 +84,16 @@ pub async fn general_oss_setup(client: &Elasticsearch) -> Result<(), Error> { .collect(); for (id, repo) in all_snapshots { - let _snapshot_response = client.snapshot() + let _snapshot_response = client + .snapshot() .delete(SnapshotDeleteParts::RepositorySnapshot(&repo, &id)) .send() .await?; } } - let _delete_repo_response = client.snapshot() + let _delete_repo_response = client + .snapshot() .delete_repository(SnapshotDeleteRepositoryParts::Repository(&["*"])) .send() .await?; @@ -96,7 +105,8 @@ pub async fn general_oss_setup(client: &Elasticsearch) -> Result<(), Error> { pub async fn general_xpack_setup(client: &Elasticsearch) -> Result<(), Error> { delete_templates(client).await?; - let _delete_watch_response = client.watcher() + let _delete_watch_response = client + .watcher() .delete_watch(WatcherDeleteWatchParts::Id("my_watch")) .send() .await?; @@ -106,7 +116,8 @@ pub async fn general_xpack_setup(client: &Elasticsearch) -> Result<(), Error> { delete_privileges(client).await?; stop_and_delete_datafeeds(client).await?; - let _ = client.ilm() + let _ = client + .ilm() .remove_policy(IlmRemovePolicyParts::Index("_all")) .send() .await?; @@ -120,7 +131,8 @@ pub async fn general_xpack_setup(client: &Elasticsearch) -> Result<(), Error> { wait_for_yellow_status(client).await?; delete_indices(client).await?; - let _ = client.security() + let _ = client + .security() .put_user(SecurityPutUserParts::Username("x_pack_rest_user")) .body(json!({ "password": "x-pack-test-password", @@ -129,9 +141,14 @@ pub async fn general_xpack_setup(client: &Elasticsearch) -> Result<(), Error> { .send() .await?; - let _ = client.indices() + let _ = client + .indices() .refresh(IndicesRefreshParts::Index(&["_all"])) - .expand_wildcards(&[ExpandWildcards::Open, ExpandWildcards::Closed, ExpandWildcards::Hidden]) + .expand_wildcards(&[ + ExpandWildcards::Open, + ExpandWildcards::Closed, + ExpandWildcards::Hidden, + ]) .send() .await?; @@ -141,7 +158,8 @@ pub async fn general_xpack_setup(client: &Elasticsearch) -> Result<(), Error> { } async fn wait_for_yellow_status(client: &Elasticsearch) -> Result<(), Error> { - let _ = client.cluster() + let _ = client + .cluster() .health(ClusterHealthParts::None) .wait_for_status(WaitForStatus::Yellow) .send() @@ -151,9 +169,14 @@ async fn wait_for_yellow_status(client: &Elasticsearch) -> Result<(), Error> { } async fn delete_indices(client: &Elasticsearch) -> Result<(), Error> { - let _delete_response = client.indices() + let _delete_response = client + .indices() .delete(IndicesDeleteParts::Index(&["*"])) - .expand_wildcards(&[ExpandWildcards::Open, ExpandWildcards::Closed, ExpandWildcards::Hidden]) + .expand_wildcards(&[ + ExpandWildcards::Open, + ExpandWildcards::Closed, + ExpandWildcards::Hidden, + ]) .send() .await?; @@ -161,7 +184,8 @@ async fn delete_indices(client: &Elasticsearch) -> Result<(), Error> { } async fn stop_and_delete_transforms(client: &Elasticsearch) -> Result<(), Error> { - let transforms_response = client.transform() + let transforms_response = client + .transform() .get_transform(TransformGetTransformParts::TransformId("_all")) .send() .await? @@ -170,12 +194,14 @@ async fn stop_and_delete_transforms(client: &Elasticsearch) -> Result<(), Error> for transform in transforms_response["transforms"].as_array().unwrap() { let id = transform["id"].as_str().unwrap(); - let _ = client.transform() + let _ = client + .transform() .stop_transform(TransformStopTransformParts::TransformId(id)) .send() .await?; - let _ = client.transform() + let _ = client + .transform() .delete_transform(TransformDeleteTransformParts::TransformId(id)) .send() .await?; @@ -185,18 +211,14 @@ async fn stop_and_delete_transforms(client: &Elasticsearch) -> Result<(), Error> } async fn cancel_tasks(client: &Elasticsearch) -> Result<(), Error> { - let rollup_response = client.tasks() - .list() - .send() - .await? - .json::() - .await?; + let rollup_response = client.tasks().list().send().await?.json::().await?; for (_node_id, nodes) in rollup_response["nodes"].as_object().unwrap() { for (task_id, task) in nodes["tasks"].as_object().unwrap() { if let Some(b) = task["cancellable"].as_bool() { if b { - let _ = client.tasks() + let _ = client + .tasks() .cancel(TasksCancelParts::TaskId(task_id)) .send() .await?; @@ -209,7 +231,8 @@ async fn cancel_tasks(client: &Elasticsearch) -> Result<(), Error> { } async fn delete_templates(client: &Elasticsearch) -> Result<(), Error> { - let cat_template_response = client.cat() + let cat_template_response = client + .cat() .templates(CatTemplatesParts::Name("*")) .h(&["name"]) .send() @@ -223,7 +246,8 @@ async fn delete_templates(client: &Elasticsearch) -> Result<(), Error> { .collect(); for template in all_templates { - let _delete_template_response = client.indices() + let _delete_template_response = client + .indices() .delete_template(IndicesDeleteTemplateParts::Name(&template)) .send() .await?; @@ -233,7 +257,8 @@ async fn delete_templates(client: &Elasticsearch) -> Result<(), Error> { } async fn delete_users(client: &Elasticsearch) -> Result<(), Error> { - let users_response = client.security() + let users_response = client + .security() .get_user(SecurityGetUserParts::None) .send() .await? @@ -243,7 +268,8 @@ async fn delete_users(client: &Elasticsearch) -> Result<(), Error> { for (k, v) in users_response.as_object().unwrap() { if let Some(b) = v["metadata"]["_reserved"].as_bool() { if !b { - let _ = client.security() + let _ = client + .security() .delete_user(SecurityDeleteUserParts::Username(k)) .send() .await?; @@ -255,7 +281,8 @@ async fn delete_users(client: &Elasticsearch) -> Result<(), Error> { } async fn delete_roles(client: &Elasticsearch) -> Result<(), Error> { - let roles_response = client.security() + let roles_response = client + .security() .get_role(SecurityGetRoleParts::None) .send() .await? @@ -265,7 +292,8 @@ async fn delete_roles(client: &Elasticsearch) -> Result<(), Error> { for (k, v) in roles_response.as_object().unwrap() { if let Some(b) = v["metadata"]["_reserved"].as_bool() { if !b { - let _ = client.security() + let _ = client + .security() .delete_role(SecurityDeleteRoleParts::Name(k)) .send() .await?; @@ -277,7 +305,8 @@ async fn delete_roles(client: &Elasticsearch) -> Result<(), Error> { } async fn delete_privileges(client: &Elasticsearch) -> Result<(), Error> { - let privileges_response = client.security() + let privileges_response = client + .security() .get_privileges(SecurityGetPrivilegesParts::None) .send() .await? @@ -287,7 +316,8 @@ async fn delete_privileges(client: &Elasticsearch) -> Result<(), Error> { for (k, v) in privileges_response.as_object().unwrap() { if let Some(b) = v["metadata"]["_reserved"].as_bool() { if !b { - let _ = client.security() + let _ = client + .security() .delete_privileges(SecurityDeletePrivilegesParts::ApplicationName(k, "_all")) .send() .await?; @@ -299,12 +329,14 @@ async fn delete_privileges(client: &Elasticsearch) -> Result<(), Error> { } async fn stop_and_delete_datafeeds(client: &Elasticsearch) -> Result<(), Error> { - let _stop_data_feed_response = client.ml() + let _stop_data_feed_response = client + .ml() .stop_datafeed(MlStopDatafeedParts::DatafeedId("_all")) .send() .await?; - let get_data_feeds_response = client.ml() + let get_data_feeds_response = client + .ml() .get_datafeeds(MlGetDatafeedsParts::None) .send() .await? @@ -313,7 +345,8 @@ async fn stop_and_delete_datafeeds(client: &Elasticsearch) -> Result<(), Error> for feed in get_data_feeds_response["datafeeds"].as_array().unwrap() { let id = feed["datafeed_id"].as_str().unwrap(); - let _ = client.ml() + let _ = client + .ml() .delete_datafeed(MlDeleteDatafeedParts::DatafeedId(id)) .send() .await?; @@ -323,12 +356,14 @@ async fn stop_and_delete_datafeeds(client: &Elasticsearch) -> Result<(), Error> } async fn close_and_delete_jobs(client: &Elasticsearch) -> Result<(), Error> { - let _ = client.ml() + let _ = client + .ml() .close_job(MlCloseJobParts::JobId("_all")) .send() .await?; - let get_jobs_response = client.ml() + let get_jobs_response = client + .ml() .get_jobs(MlGetJobsParts::JobId("_all")) .send() .await? @@ -337,7 +372,8 @@ async fn close_and_delete_jobs(client: &Elasticsearch) -> Result<(), Error> { for job in get_jobs_response["jobs"].as_array().unwrap() { let id = job["job_id"].as_str().unwrap(); - let _ = client.ml() + let _ = client + .ml() .delete_job(MlDeleteJobParts::JobId(id)) .send() .await?; diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 494388bc..7b4ede7d 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -9,17 +9,18 @@ use std::collections::HashSet; use std::fs; use std::fs::{File, OpenOptions}; use std::io::Write; -use std::path::{PathBuf, Component}; +use std::path::{Component, PathBuf}; use yaml_rust::{Yaml, YamlLoader}; +#[derive(Debug, PartialEq)] pub enum TestSuite { Oss, - XPack + XPack, } /// The components of a test file, constructed from a yaml file struct YamlTests<'a> { - version: &'a Option, + version: &'a Version, suite: TestSuite, directives: HashSet, setup: Option, @@ -28,7 +29,7 @@ struct YamlTests<'a> { } impl<'a> YamlTests<'a> { - pub fn new(version: &'a Option, suite: TestSuite, len: usize) -> Self { + pub fn new(version: &'a semver::Version, suite: TestSuite, len: usize) -> Self { Self { version, suite, @@ -89,7 +90,7 @@ impl<'a> YamlTests<'a> { TestSuite::XPack => quote!(client::general_xpack_setup(&client).await?;), }; - let tests: Vec = self.fn_impls(general_setup_call, setup_call, teardown_call, ); + let tests: Vec = self.fn_impls(general_setup_call, setup_call, teardown_call); let directives: Vec = self .directives @@ -158,7 +159,12 @@ impl<'a> YamlTests<'a> { true } - fn fn_impls(&self, general_setup_call: Tokens, setup_call: Option, teardown_call: Option) -> Vec { + fn fn_impls( + &self, + general_setup_call: Tokens, + setup_call: Option, + teardown_call: Option, + ) -> Vec { let mut seen_method_names = HashSet::new(); self.tests @@ -174,42 +180,43 @@ impl<'a> YamlTests<'a> { for step in &test_fn.steps { match step { Step::Skip(s) => { - skip = if let Some(v) = &self.version { - if s.matches(v) { - let reason = match s.reason.as_ref() { - Some(s) => s.to_string(), - None => String::new(), - }; - println!( - "Skipping test because skip version '{}' are met. {}", - s.version.as_ref().unwrap(), - reason - ); - Some(s) - } else { - None - } + skip = if s.matches(self.version) { + let reason = match s.reason.as_ref() { + Some(s) => s.to_string(), + None => String::new(), + }; + info!( + "Skipping test because skip version '{}' are met. {}", + s.version.as_ref().unwrap(), + reason + ); + Some(s) } else { None } } Step::Do(d) => { read_response = d.to_tokens(false, &mut body); - }, + } Step::Match(m) => { - read_response = - Self::read_response(read_response, m.is_body_expr(&m.expr), &mut body); + read_response = Self::read_response( + read_response, + m.is_body_expr(&m.expr), + &mut body, + ); m.to_tokens(&mut body); - }, + } Step::Set(s) => { // TODO: is "set" ever is_body_expr? - read_response = - Self::read_response(read_response, false, &mut body); + read_response = Self::read_response(read_response, false, &mut body); s.to_tokens(&mut body); - }, + } Step::Length(l) => { - read_response = - Self::read_response(read_response, l.is_body_expr(&l.expr), &mut body); + read_response = Self::read_response( + read_response, + l.is_body_expr(&l.expr), + &mut body, + ); l.to_tokens(&mut body); } } @@ -287,7 +294,8 @@ impl TestFn { pub fn generate_tests_from_yaml( api: &Api, - version: &Option, + suite: &TestSuite, + version: &semver::Version, base_download_dir: &PathBuf, download_dir: &PathBuf, generated_dir: &PathBuf, @@ -297,47 +305,63 @@ pub fn generate_tests_from_yaml( if let Ok(entry) = entry { if let Ok(file_type) = entry.file_type() { if file_type.is_dir() { - generate_tests_from_yaml(api, version, base_download_dir, &entry.path(), generated_dir)?; + generate_tests_from_yaml( + api, + suite, + version, + base_download_dir, + &entry.path(), + generated_dir, + )?; } else if file_type.is_file() { - let file_name = entry.file_name().to_string_lossy().into_owned(); - + let path = entry.path(); // skip non-yaml files - if !file_name.ends_with(".yml") && !file_name.ends_with(".yaml") { + let extension = path.extension().unwrap_or("".as_ref()); + if extension != "yml" && extension != "yaml" { continue; } - let yaml = fs::read_to_string(&entry.path()).unwrap(); - // a yaml test can contain multiple yaml docs - let result = YamlLoader::load_from_str(&yaml); - if result.is_err() { - println!( - "Error reading {:?}. skipping:\n\t{}", - &entry.path(), - result.err().unwrap().to_string() - ); - continue; - } - - let suite = { - let path = entry.path().clone(); - let mut components = path.strip_prefix(&base_download_dir)?.components(); - let mut relative = "".to_string(); + let relative_path = path.strip_prefix(&base_download_dir)?; + let test_suite = { + let mut components = relative_path.components(); + let mut top_dir = "".to_string(); while let Some(c) = components.next() { if c != Component::RootDir { - relative = c.as_os_str().to_string_lossy().into_owned(); + top_dir = c.as_os_str().to_string_lossy().into_owned(); break; } } - match relative.as_str() { + match top_dir.as_str() { "oss" => TestSuite::Oss, "xpack" => TestSuite::XPack, - _ => panic!("Unknown test suite") + _ => panic!("Unknown test suite"), } }; + if &test_suite != suite { + info!( + "skipping {:?}. compiling tests for {:?}", + relative_path, suite + ); + continue; + } + + let yaml = fs::read_to_string(&entry.path()).unwrap(); + + // a yaml test can contain multiple yaml docs + let result = YamlLoader::load_from_str(&yaml); + if result.is_err() { + info!( + "skipping {:?}. cannot read as Yaml: {}", + relative_path, + result.err().unwrap().to_string() + ); + continue; + } + let docs = result.unwrap(); - let mut test = YamlTests::new(version, suite, docs.len()); + let mut test = YamlTests::new(version, test_suite, docs.len()); let results : Vec> = docs .iter() @@ -375,9 +399,7 @@ pub fn generate_tests_from_yaml( //if there has been an Err in any step of the yaml test file, don't create a test for it match ok_or_accumulate(&results, 1) { - Ok(_) => { - write_test_file(test, &entry.path(), base_download_dir, generated_dir)? - } + Ok(_) => write_test_file(test, &path, base_download_dir, generated_dir)?, Err(e) => println!( "Error creating test file for {:?}. skipping:\n{}", &entry.path(), @@ -396,7 +418,11 @@ pub fn generate_tests_from_yaml( /// Writes a mod.rs file in each generated directory fn write_mod_files(generated_dir: &PathBuf) -> Result<(), failure::Error> { - let paths = fs::read_dir(generated_dir).unwrap(); + if !generated_dir.exists() { + fs::create_dir(generated_dir)?; + } + + let paths = fs::read_dir(generated_dir)?; let mut mods = vec![]; for path in paths { if let Ok(entry) = path { diff --git a/yaml_test_runner/src/main.rs b/yaml_test_runner/src/main.rs index 36e48389..d46eb9af 100644 --- a/yaml_test_runner/src/main.rs +++ b/yaml_test_runner/src/main.rs @@ -1,3 +1,6 @@ +// TODO: remove when implementation is more complete. +#![allow(dead_code)] + #[macro_use] extern crate log; extern crate simple_logger; @@ -10,10 +13,12 @@ extern crate api_generator; #[cfg(test)] extern crate serde_json; +use crate::generator::TestSuite; use clap::{App, Arg}; +use log::Level; use std::fs; use std::path::PathBuf; -use log::Level; +use std::process::exit; mod generator; mod github; @@ -54,6 +59,33 @@ fn main() -> Result<(), failure::Error> { .takes_value(true)) .get_matches(); + // Get the version from ELASTICSEARCH_VERSION environment variable, if set. + // any prerelease part needs to be trimmed because the semver crate only allows + // a version with a prerelease to match against predicates, if at least one predicate + // has a prerelease. See + // https://github.com/steveklabnik/semver/blob/afa5fc853cb4d6d2b1329579e5528f86f3b550f9/src/version_req.rs#L319-L331 + let (suite, version) = match std::env::var("ELASTICSEARCH_VERSION") { + Ok(v) => { + let suite = if v.contains("oss") { + TestSuite::Oss + } else { + TestSuite::XPack + }; + + let v = v + .trim_start_matches("elasticsearch:") + .trim_end_matches(|c: char| c.is_alphabetic() || c == '-'); + + (suite, semver::Version::parse(v)?) + } + Err(_) => { + error!("ELASTICSEARCH_VERSION environment variable must be set to compile tests"); + exit(1); + } + }; + + info!("Using version {:?} to compile tests", &version); + let branch = matches .value_of("branch") .expect("missing 'branch' argument"); @@ -93,23 +125,14 @@ fn main() -> Result<(), failure::Error> { fs::remove_dir_all(&generated_dir)?; } - // try to get the version from ELASTICSEARCH_VERSION environment variable, if set. - // any prerelease part needs to be trimmed because the semver crate only allows - // a version with a prerelease to match against predicates, if at least one predicate - // has a prerelease. See - // https://github.com/steveklabnik/semver/blob/afa5fc853cb4d6d2b1329579e5528f86f3b550f9/src/version_req.rs#L319-L331 - let version = match std::env::var("ELASTICSEARCH_VERSION") { - Ok(v) => { - let v = v - .trim_start_matches("elasticsearch:") - .trim_end_matches(|c: char| c.is_alphabetic() || c == '-'); - semver::Version::parse(v).ok() - } - Err(_) => None - }; - - info!("Using version {:?} to compile tests", &version); - generator::generate_tests_from_yaml(&api, &version, &download_dir, &download_dir, &generated_dir)?; + generator::generate_tests_from_yaml( + &api, + &suite, + &version, + &download_dir, + &download_dir, + &generated_dir, + )?; Ok(()) } diff --git a/yaml_test_runner/src/step/do.rs b/yaml_test_runner/src/step/do.rs index 3f186937..47bbac57 100644 --- a/yaml_test_runner/src/step/do.rs +++ b/yaml_test_runner/src/step/do.rs @@ -37,15 +37,15 @@ impl ToTokens for Catch { let status_code = response.status_code().as_u16(); assert!(status_code >= 400 && status_code < 600); }); - }, + } "unavailable" => http_status_code(503, tokens), "param" => { // Not possible to pass a bad param to the client so ignore. - }, + } s => { // trim the enclosing forward slashes and replace escaped forward slashes let t = s.trim_matches('/').replace("\\/", "/"); - tokens.append(quote!{ + tokens.append(quote! { let catch_regex = regex::Regex::new(#t)?; assert!( catch_regex.is_match(response_body["error"]["reason"].as_str().unwrap()), @@ -55,7 +55,7 @@ impl ToTokens for Catch { #s ); }); - }, + } } } } @@ -115,13 +115,16 @@ impl Do { match key { "headers" => { - let hash = v.as_hash() - .ok_or_else(|| failure::err_msg(format!("expected hash but found {:?}", v)))?; + let hash = v.as_hash().ok_or_else(|| { + failure::err_msg(format!("expected hash but found {:?}", v)) + })?; for (hk, hv) in hash.iter() { - let h = hk.as_str() - .ok_or_else(|| failure::err_msg(format!("expected str but found {:?}", hk)))?; - let v = hv.as_str() - .ok_or_else(|| failure::err_msg(format!("expected str but found {:?}", hv)))?; + let h = hk.as_str().ok_or_else(|| { + failure::err_msg(format!("expected str but found {:?}", hk)) + })?; + let v = hv.as_str().ok_or_else(|| { + failure::err_msg(format!("expected str but found {:?}", hv)) + })?; headers.insert(h.into(), v.into()); } Ok(()) @@ -152,7 +155,8 @@ impl Do { ok_or_accumulate(&results, 0)?; let (call, value) = call.ok_or_else(|| failure::err_msg("no API found in do"))?; - let endpoint = api.endpoint_for_api_call(call) + let endpoint = api + .endpoint_for_api_call(call) .ok_or_else(|| failure::err_msg(format!("no API found for '{}'", call)))?; let api_call = ApiCall::try_from(api, endpoint, value, headers)?; @@ -188,9 +192,10 @@ impl ToTokens for ApiCall { // TODO: handle "set" values - let headers: Vec = self.headers + let headers: Vec = self + .headers .iter() - .map(|(k,v)| { + .map(|(k, v)| { // header names **must** be lowercase to satisfy Header lib let k = k.to_lowercase(); quote! { @@ -210,15 +215,26 @@ impl ToTokens for ApiCall { } } +lazy_static! { + // replace usages of "$.*" with the captured value + static ref SET_REGEX: Regex = + Regex::new(r#""\$(.*?)""#).unwrap(); + + // include i64 suffix on whole numbers + static ref INT_REGEX: Regex = + regex::Regex::new(r"(:\s?)(\d+?)([,\s?|\s*?}])").unwrap(); +} + impl ApiCall { /// Try to create an API call pub fn try_from( api: &Api, endpoint: &ApiEndpoint, yaml: &Yaml, - headers: BTreeMap + headers: BTreeMap, ) -> Result { - let hash = yaml.as_hash() + let hash = yaml + .as_hash() .ok_or_else(|| failure::err_msg(format!("expected hash but found {:?}", yaml)))?; let mut parts: Vec<(&str, &Yaml)> = vec![]; @@ -272,8 +288,7 @@ impl ApiCall { variant: &str, options: &[serde_json::Value], ) -> Result { - if !variant.is_empty() - && !options.contains(&serde_json::Value::String(variant.to_owned())) + if !variant.is_empty() && !options.contains(&serde_json::Value::String(variant.to_owned())) { return Err(failure::err_msg(format!( "options {:?} does not contain value {}", @@ -392,7 +407,7 @@ impl ApiCall { .#param_ident(#i) }); } - }, + } TypeKind::Number | TypeKind::Long => { if is_set_value { let t = Self::from_set_value(s); @@ -412,7 +427,7 @@ impl ApiCall { let t = if is_set_value { let t = Self::from_set_value(s); let ident = syn::Ident::from(t); - quote!{ #ident.as_str().unwrap() } + quote! { #ident.as_str().unwrap() } } else { quote! { #s } }; @@ -420,7 +435,7 @@ impl ApiCall { tokens.append(quote! { .#param_ident(#t) }) - }, + } } } Yaml::Boolean(ref b) => match kind { @@ -598,10 +613,13 @@ impl ApiCall { _ => Some(matching_path_parts[0]), } } - }.ok_or_else(|| failure::err_msg(format!( - "No path for '{}' API with URL parts {:?}", - &api_call, parts - )))?; + } + .ok_or_else(|| { + failure::err_msg(format!( + "No path for '{}' API with URL parts {:?}", + &api_call, parts + )) + })?; let path_parts = path.path.params(); let variant_name = { @@ -622,11 +640,9 @@ impl ApiCall { f.cmp(&s) }) .map(|(p, v)| { - let ty = path.parts.get(*p) - .ok_or_else(|| failure::err_msg(format!( - "No URL part found for {} in {}", - p, &path.path - )))?; + let ty = path.parts.get(*p).ok_or_else(|| { + failure::err_msg(format!("No URL part found for {} in {}", p, &path.path)) + })?; match v { Yaml::String(s) => { @@ -642,7 +658,7 @@ impl ApiCall { let ident = syn::Ident::from(t); quote! { #ident.as_str().unwrap() } } else { - quote!{ #s } + quote! { #s } } }) .collect(); @@ -666,9 +682,9 @@ impl ApiCall { } else { Ok(quote! { #s }) } - }, + } } - }, + } Yaml::Boolean(b) => { let s = b.to_string(); Ok(quote! { #s }) @@ -696,10 +712,7 @@ impl ApiCall { match ok_or_accumulate(&result, 0) { Ok(_) => { let result: Vec<_> = - result.into_iter() - .filter_map(Result::ok) - - .collect(); + result.into_iter().filter_map(Result::ok).collect(); match ty.ty { // Some APIs specify a part is a string in the REST API spec @@ -733,19 +746,17 @@ impl ApiCall { } } - fn replace_values(s: &str) -> String { - lazy_static! { - // replace usages of "$.*" with the captured value - static ref SET_REGEX: Regex = - Regex::new(r#""\$(.*?)""#).unwrap(); - - // include i64 suffix on whole numbers - static ref INT_REGEX: Regex = - regex::Regex::new(r"(:\s?)(\d+?)([,\s?|\s*?}])").unwrap(); - } + /// Replaces a "set" step value with a variable + fn replace_set>(s: S) -> String { + SET_REGEX.replace_all(s.as_ref(), "$1").into_owned() + } - let c = SET_REGEX.replace_all(s, "$1").into_owned(); - INT_REGEX.replace_all(&c, "${1}${2}i64${3}").into_owned() + /// Replaces all integers in a string to suffix with i64, to ensure that numbers + /// larger than i32 will be handled correctly when passed to json! macro + fn replace_i64>(s: S) -> String { + INT_REGEX + .replace_all(s.as_ref(), "${1}${2}i64${3}") + .into_owned() } /// Creates the body function call from a YAML value. @@ -754,18 +765,13 @@ impl ApiCall { /// usually a Hash. To get the JSON representation back requires converting /// back to JSON fn generate_body(endpoint: &ApiEndpoint, v: &Yaml) -> Option { - let accepts_nd_body = match &endpoint.body { - Some(b) => match &b.serialize { - Some(s) => s == "bulk", - _ => false, - }, - None => false, - }; - match v { Yaml::String(s) => { - let json = Self::replace_values(s.as_str()); - if accepts_nd_body { + let json = { + let s = Self::replace_set(s); + Self::replace_i64(s) + }; + if endpoint.supports_nd_body() { // a newline delimited API body may be expressed // as a scalar string literal style where line breaks are significant (using |) // or where lines breaks are folded to an empty space unless it ends on an @@ -801,18 +807,20 @@ impl ApiCall { emitter.dump(v).unwrap(); } - if accepts_nd_body { + if endpoint.supports_nd_body() { let values: Vec = serde_yaml::from_str(&s).unwrap(); let json: Vec = values .iter() .map(|value| { let mut json = serde_json::to_string(&value).unwrap(); - json = Self::replace_values(&json); - - let ident = syn::Ident::from(json); if value.is_string() { + json = Self::replace_set(&json); + let ident = syn::Ident::from(json); quote!(#ident) } else { + json = Self::replace_set(json); + json = Self::replace_i64(json); + let ident = syn::Ident::from(json); quote!(JsonBody::from(json!(#ident))) } }) @@ -821,7 +829,8 @@ impl ApiCall { } else { let value: serde_json::Value = serde_yaml::from_str(&s).unwrap(); let mut json = serde_json::to_string_pretty(&value).unwrap(); - json = Self::replace_values(&json); + json = Self::replace_set(json); + json = Self::replace_i64(json); let ident = syn::Ident::from(json); Some(quote!(.body(json!{#ident}))) diff --git a/yaml_test_runner/src/step/length.rs b/yaml_test_runner/src/step/length.rs index ebb75801..e0ac5538 100644 --- a/yaml_test_runner/src/step/length.rs +++ b/yaml_test_runner/src/step/length.rs @@ -1,8 +1,8 @@ use quote::{ToTokens, Tokens}; use super::Step; -use yaml_rust::Yaml; use crate::step::BodyExpr; +use yaml_rust::Yaml; pub struct Length { len: usize, @@ -18,7 +18,8 @@ impl From for Step { impl BodyExpr for Length { // a length step should never advertise itself as a body expression as it would // cause the body of the preceding API call to be returned as text rather than serde::Value. - fn is_body_expr(&self, key: &str) -> bool { + // a serde::Value is needed as a length step on $body means counting object keys or array length. + fn is_body_expr(&self, _key: &str) -> bool { false } } @@ -35,16 +36,16 @@ impl Length { failure::err_msg(format!("expected string key but found {:?}", k)) })?; - let len = v.as_i64().ok_or_else(|| { - failure::err_msg(format!("expected i64 but found {:?}", v)) - })?; + let len = v + .as_i64() + .ok_or_else(|| failure::err_msg(format!("expected i64 but found {:?}", v)))?; (key, len) }; Ok(Length { len: len as usize, - expr: expr.into() + expr: expr.into(), }) } } @@ -54,17 +55,17 @@ impl ToTokens for Length { let len = self.len; if &self.expr == "$body" { - tokens.append(quote!{ + tokens.append(quote! { let len = util::len_from_value(&response_body)?; assert_eq!(#len, len); }); } else { let expr = self.body_expr(&self.expr); let ident = syn::Ident::from(expr); - tokens.append(quote!{ + tokens.append(quote! { let len = util::len_from_value(&response_body#ident)?; assert_eq!(#len, len); }); } } -} \ No newline at end of file +} diff --git a/yaml_test_runner/src/step/match.rs b/yaml_test_runner/src/step/match.rs index 1c975fae..c4076e42 100644 --- a/yaml_test_runner/src/step/match.rs +++ b/yaml_test_runner/src/step/match.rs @@ -1,7 +1,7 @@ use super::Step; +use crate::step::{BodyExpr, clean_regex}; use quote::{ToTokens, Tokens}; use yaml_rust::Yaml; -use crate::step::BodyExpr; pub struct Match { pub expr: String, @@ -39,8 +39,8 @@ impl ToTokens for Match { match &self.value { Yaml::String(s) => { if s.starts_with('/') { - // trim the enclosing forward slashes and replace escaped forward slashes - let s = s.trim().trim_matches('/').replace("\\/", "/"); + let s = clean_regex(s); + if self.is_body_expr(&expr) { tokens.append(quote! { let regex = regex::RegexBuilder::new(#s) @@ -56,7 +56,9 @@ impl ToTokens for Match { } else { let ident = syn::Ident::from(expr.clone()); tokens.append(quote! { - let regex = regex::Regex::new(#s)?; + let regex = regex::RegexBuilder::new(#s) + .ignore_whitespace(true) + .build()?; assert!( regex.is_match(response_body#ident.as_str().unwrap()), "expected value at {}:\n\n{}\n\nto match regex:\n\n{}", @@ -71,11 +73,12 @@ impl ToTokens for Match { // handle set values let t = if s.starts_with('$') { - let t = s.trim_start_matches('$') + let t = s + .trim_start_matches('$') .trim_start_matches('{') .trim_end_matches('}'); let ident = syn::Ident::from(t); - quote!{ #ident.as_str().unwrap() } + quote! { #ident.as_str().unwrap() } } else { quote! { #s } }; diff --git a/yaml_test_runner/src/step/mod.rs b/yaml_test_runner/src/step/mod.rs index 19be6b2e..332c1773 100644 --- a/yaml_test_runner/src/step/mod.rs +++ b/yaml_test_runner/src/step/mod.rs @@ -1,17 +1,17 @@ use api_generator::generator::Api; -use yaml_rust::Yaml; use std::fmt::Write; +use yaml_rust::Yaml; mod r#do; +mod length; mod r#match; mod set; mod skip; -mod length; +pub use length::*; pub use r#do::*; pub use r#match::*; pub use set::*; pub use skip::*; -pub use length::*; pub fn parse_steps(api: &Api, steps: &[Yaml]) -> Result, failure::Error> { let mut parsed_steps: Vec = Vec::new(); @@ -111,7 +111,8 @@ pub trait BodyExpr { write!(expr, "[{}]", s).unwrap(); } else if s.starts_with('$') { // handle set values - let t = s.trim_start_matches('$') + let t = s + .trim_start_matches('$') .trim_start_matches('{') .trim_end_matches('}'); write!(expr, "[{}.as_str().unwrap()]", t).unwrap(); @@ -163,3 +164,15 @@ pub fn ok_or_accumulate( Err(failure::err_msg(msg)) } } + +// trim the enclosing forward slashes and +// 1. replace escaped forward slashes (not needed after trimming forward slashes) +// 2. replace escaped colons (not supported by regex crate) +pub fn clean_regex>(s: S) -> String { + s.as_ref() + .trim() + .trim_matches('/') + .replace("\\/", "/") + .replace("\\:", ":") + .replace("\\#", "#") +} \ No newline at end of file diff --git a/yaml_test_runner/src/step/set.rs b/yaml_test_runner/src/step/set.rs index 3de68754..b399e554 100644 --- a/yaml_test_runner/src/step/set.rs +++ b/yaml_test_runner/src/step/set.rs @@ -1,8 +1,8 @@ use quote::{ToTokens, Tokens}; use super::Step; -use yaml_rust::Yaml; use crate::step::BodyExpr; +use yaml_rust::Yaml; pub struct Set { ident: syn::Ident, @@ -38,7 +38,7 @@ impl Set { Ok(Set { ident: syn::Ident::from(id), - expr: expr.into() + expr: expr.into(), }) } } @@ -49,8 +49,8 @@ impl ToTokens for Set { let expr = syn::Ident::from(self.body_expr(&self.expr)); // TODO: Unwrap serde_json value here, or in the usage? - tokens.append(quote!{ + tokens.append(quote! { let #ident = response_body#expr.clone(); }); } -} \ No newline at end of file +} diff --git a/yaml_test_runner/src/util.rs b/yaml_test_runner/src/util.rs index 84b2ab91..01b52084 100644 --- a/yaml_test_runner/src/util.rs +++ b/yaml_test_runner/src/util.rs @@ -2,18 +2,10 @@ use serde_json::Value; pub fn len_from_value(value: &Value) -> Result { match value { - Value::Number(n) => { - Ok(n.as_i64().unwrap() as usize) - }, - Value::String(s) => { - Ok(s.len()) - }, - Value::Array(a) => { - Ok(a.len()) - }, - Value::Object(o) => { - Ok(o.len()) - }, - v => Err(failure::err_msg(format!("Cannot get length from {:?}", v))) + Value::Number(n) => Ok(n.as_i64().unwrap() as usize), + Value::String(s) => Ok(s.len()), + Value::Array(a) => Ok(a.len()), + Value::Object(o) => Ok(o.len()), + v => Err(failure::err_msg(format!("Cannot get length from {:?}", v))), } } From 01dc846bad284c2adab6f69e9a6e4243378d603f Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Mon, 11 May 2020 10:51:28 +1000 Subject: [PATCH 067/127] Extract username/password for ES_TEST_SERVER This commit extracts the username and password from the value passsed with ES_TEST_SERVER env var, and sets Basic credentials. Credentials set before setting any other headers, to allow headers to overwrite values. --- elasticsearch/src/http/transport.rs | 45 +++++++++++++++-------------- yaml_test_runner/src/client.rs | 40 +++++++++++++++++++++---- 2 files changed, 58 insertions(+), 27 deletions(-) diff --git a/elasticsearch/src/http/transport.rs b/elasticsearch/src/http/transport.rs index 6900feb6..51fa086d 100644 --- a/elasticsearch/src/http/transport.rs +++ b/elasticsearch/src/http/transport.rs @@ -322,6 +322,30 @@ impl Transport { let reqwest_method = self.method(method); let mut request_builder = self.client.request(reqwest_method, url); + // set credentials before any headers, as credentials append to existing headers in reqwest, + // whilst setting headers() overwrites, so if an Authorization header has been specified + // on a specific request, we want it to overwrite. + if let Some(c) = &self.credentials { + request_builder = match c { + Credentials::Basic(u, p) => request_builder.basic_auth(u, Some(p)), + Credentials::Bearer(t) => request_builder.bearer_auth(t), + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + Credentials::Certificate(_) => request_builder, + Credentials::ApiKey(i, k) => { + let mut header_value = b"ApiKey ".to_vec(); + { + let mut encoder = Base64Encoder::new(&mut header_value, base64::STANDARD); + write!(encoder, "{}:", i).unwrap(); + write!(encoder, "{}", k).unwrap(); + } + request_builder.header( + AUTHORIZATION, + HeaderValue::from_bytes(&header_value).unwrap(), + ) + } + } + } + // default headers first, overwrite with any provided let mut request_headers = HeaderMap::with_capacity(3 + headers.len()); request_headers.insert(CONTENT_TYPE, HeaderValue::from_static(DEFAULT_CONTENT_TYPE)); @@ -349,27 +373,6 @@ impl Transport { request_builder = request_builder.query(q); } - if let Some(c) = &self.credentials { - request_builder = match c { - Credentials::Basic(u, p) => request_builder.basic_auth(u, Some(p)), - Credentials::Bearer(t) => request_builder.bearer_auth(t), - #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] - Credentials::Certificate(_) => request_builder, - Credentials::ApiKey(i, k) => { - let mut header_value = b"ApiKey ".to_vec(); - { - let mut encoder = Base64Encoder::new(&mut header_value, base64::STANDARD); - write!(encoder, "{}:", i).unwrap(); - write!(encoder, "{}", k).unwrap(); - } - request_builder.header( - AUTHORIZATION, - HeaderValue::from_bytes(&header_value).unwrap(), - ) - } - } - } - let response = request_builder.send().await; match response { Ok(r) => Ok(Response::new(r)), diff --git a/yaml_test_runner/src/client.rs b/yaml_test_runner/src/client.rs index 52dcbb4a..9e2a9d07 100644 --- a/yaml_test_runner/src/client.rs +++ b/yaml_test_runner/src/client.rs @@ -41,15 +41,43 @@ fn running_proxy() -> bool { /// create a client to use in tests pub fn create() -> Elasticsearch { - let url = Url::parse(cluster_addr().as_ref()).unwrap(); + let mut url = Url::parse(cluster_addr().as_ref()).unwrap(); + + // if the url is https and specifies a username and password, remove from the url and set credentials + let credentials = if url.scheme() == "https" { + let username = if !url.username().is_empty() { + let u = url.username().to_string(); + url.set_username("").unwrap(); + u + } else { + "elastic".into() + }; + + let password = match url.password() { + Some(p) => { + let pass = p.to_string(); + url.set_password(None).unwrap(); + pass + }, + None => "changeme".into() + }; + + Some(Credentials::Basic(username, password)) + } else { + None + }; + let conn_pool = SingleNodeConnectionPool::new(url.clone()); let mut builder = TransportBuilder::new(conn_pool); - if url.scheme() == "https" { - builder = builder - .auth(Credentials::Basic("elastic".into(), "changeme".into())) - .cert_validation(CertificateValidation::None) - } + builder = match credentials { + Some(c) => { + builder + .auth(c) + .cert_validation(CertificateValidation::None) + }, + None => builder + }; if running_proxy() { let proxy_url = Url::parse("http://localhost:8888").unwrap(); From 11f6b6fd97224881bec47f83c12c9c6649a7c730 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Mon, 11 May 2020 10:51:49 +1000 Subject: [PATCH 068/127] "set" value replacement in headers --- yaml_test_runner/src/step/do.rs | 54 +++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/yaml_test_runner/src/step/do.rs b/yaml_test_runner/src/step/do.rs index 47bbac57..d662623a 100644 --- a/yaml_test_runner/src/step/do.rs +++ b/yaml_test_runner/src/step/do.rs @@ -7,6 +7,20 @@ use itertools::Itertools; use regex::Regex; use std::collections::BTreeMap; use yaml_rust::{Yaml, YamlEmitter}; +use crate::step::clean_regex; + +lazy_static! { + // replace usages of "$.*" with the captured value + static ref SET_REGEX: Regex = + Regex::new(r#""\$(.*?)""#).unwrap(); + + static ref SET_DELIMITED_REGEX: Regex = + Regex::new(r#"\$\{(.*?)\}"#).unwrap(); + + // include i64 suffix on whole numbers + static ref INT_REGEX: Regex = + regex::Regex::new(r"(:\s?)(\d+?)([,\s?|\s*?}])").unwrap(); +} /// A catch expression on a do step pub struct Catch(String); @@ -43,15 +57,15 @@ impl ToTokens for Catch { // Not possible to pass a bad param to the client so ignore. } s => { - // trim the enclosing forward slashes and replace escaped forward slashes - let t = s.trim_matches('/').replace("\\/", "/"); + let t = clean_regex(s); tokens.append(quote! { let catch_regex = regex::Regex::new(#t)?; + let error = response_body["error"].to_string(); assert!( - catch_regex.is_match(response_body["error"]["reason"].as_str().unwrap()), - "expected value at {}:\n\n{}\n\nto match regex:\n\n{}", - "[\"error\"][\"reason\"]", - response_body["error"]["reason"].as_str().unwrap(), + catch_regex.is_match(&error), + "expected json value at {}:\n\n{}\n\nto match regex:\n\n{}", + "[\"error\"]", + &error, #s ); }); @@ -190,16 +204,26 @@ impl ToTokens for ApiCall { let params = &self.params; let body = &self.body; - // TODO: handle "set" values - let headers: Vec = self .headers .iter() .map(|(k, v)| { // header names **must** be lowercase to satisfy Header lib let k = k.to_lowercase(); - quote! { - .header(HeaderName::from_static(#k), HeaderValue::from_static(#v)) + + // handle "set" value in headers + if let Some(c) = SET_DELIMITED_REGEX.captures(v) { + let token = syn::Ident::from(c.get(1).unwrap().as_str()); + let replacement = SET_DELIMITED_REGEX.replace_all(v, "{}"); + quote! { .header( + HeaderName::from_static(#k), + HeaderValue::from_str(format!(#replacement, #token).as_ref())?) + } + } else { + quote! { .header( + HeaderName::from_static(#k), + HeaderValue::from_static(#v)) + } } }) .collect(); @@ -215,16 +239,6 @@ impl ToTokens for ApiCall { } } -lazy_static! { - // replace usages of "$.*" with the captured value - static ref SET_REGEX: Regex = - Regex::new(r#""\$(.*?)""#).unwrap(); - - // include i64 suffix on whole numbers - static ref INT_REGEX: Regex = - regex::Regex::new(r"(:\s?)(\d+?)([,\s?|\s*?}])").unwrap(); -} - impl ApiCall { /// Try to create an API call pub fn try_from( From 59ce058f556d8d508c590fe95b6ea4e48aa63c69 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Mon, 11 May 2020 13:44:57 +1000 Subject: [PATCH 069/127] replace println with logger calls --- yaml_test_runner/src/github.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/yaml_test_runner/src/github.rs b/yaml_test_runner/src/github.rs index b48500c2..e0bcbb88 100644 --- a/yaml_test_runner/src/github.rs +++ b/yaml_test_runner/src/github.rs @@ -49,7 +49,7 @@ pub fn download_test_suites( let version = fs::read_to_string(&last_downloaded_version) .expect("Unable to read last_downloaded_version of yaml tests"); if version == branch { - println!("yaml tests for branch {} already downloaded", branch); + info!("yaml tests for branch {} already downloaded", branch); return Ok(()); } } @@ -72,7 +72,7 @@ pub fn download_test_suites( let mut headers = HeaderMap::new(); let token_value = format!("token {}", token); - headers.append(AUTHORIZATION, HeaderValue::from_str(&token_value).unwrap()); + headers.append(AUTHORIZATION, HeaderValue::from_str(&token_value)?); let client = reqwest::ClientBuilder::new() .default_headers(headers) .build() @@ -109,9 +109,9 @@ fn download_tests( }; fs::create_dir_all(&suite_dir)?; - println!("Downloading {} tests from {}", &suite.dir, &suite.branch); + info!("Downloading {} tests from {}", &suite.dir, &suite.branch); download(client, &suite.url, &suite_dir)?; - println!( + info!( "Done downloading {} tests from {}", &suite.dir, &suite.branch ); @@ -136,7 +136,7 @@ fn download( .unwrap(); if remaining_rate_limit < 10 { - println!("Remaining rate limit: {}", remaining_rate_limit); + warn!("Remaining rate limit: {}", remaining_rate_limit); } let contents: Vec = response.json()?; From 9b479a75301c53cf9048856cc4c1c05cd3d89440 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Mon, 11 May 2020 15:22:24 +1000 Subject: [PATCH 070/127] introduce skip.yml to skip features and tests --- yaml_test_runner/skip.yml | 6 +++ yaml_test_runner/src/generator.rs | 54 ++++++++++++++++-------- yaml_test_runner/src/step/do.rs | 51 ++++++++++++----------- yaml_test_runner/src/step/skip.rs | 68 ++++++++++++++++++++++--------- 4 files changed, 119 insertions(+), 60 deletions(-) create mode 100644 yaml_test_runner/skip.yml diff --git a/yaml_test_runner/skip.yml b/yaml_test_runner/skip.yml new file mode 100644 index 00000000..8d524c71 --- /dev/null +++ b/yaml_test_runner/skip.yml @@ -0,0 +1,6 @@ +features: + - transform_and_set + - node_selector + - arbitrary_key + +tests: [] \ No newline at end of file diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 7b4ede7d..42919d48 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -5,11 +5,12 @@ use crate::step::*; use api_generator::generator::Api; use regex::Regex; use semver::Version; +use serde::Deserialize; use std::collections::HashSet; use std::fs; use std::fs::{File, OpenOptions}; use std::io::Write; -use std::path::{Component, PathBuf}; +use std::path::{Component, PathBuf, Path}; use yaml_rust::{Yaml, YamlLoader}; #[derive(Debug, PartialEq)] @@ -20,7 +21,9 @@ pub enum TestSuite { /// The components of a test file, constructed from a yaml file struct YamlTests<'a> { + path: &'a Path, version: &'a Version, + skips: &'a Skips, suite: TestSuite, directives: HashSet, setup: Option, @@ -29,9 +32,11 @@ struct YamlTests<'a> { } impl<'a> YamlTests<'a> { - pub fn new(version: &'a semver::Version, suite: TestSuite, len: usize) -> Self { + pub fn new(path: &'a Path, version: &'a semver::Version, skips: &'a Skips, suite: TestSuite, len: usize) -> Self { Self { + path, version, + skips, suite, directives: HashSet::with_capacity(len), setup: None, @@ -126,7 +131,7 @@ impl<'a> YamlTests<'a> { /// some function descriptions are the same in YAML tests, which would result in /// duplicate generated test function names. Deduplicate by appending incrementing number - fn unique_fn_name(name: &str, seen_method_names: &mut HashSet) -> syn::Ident { + fn unique_fn_name(name: &str, seen_method_names: &mut HashSet) -> String { let mut fn_name = name.to_string(); while !seen_method_names.insert(fn_name.clone()) { lazy_static! { @@ -140,7 +145,7 @@ impl<'a> YamlTests<'a> { fn_name.push_str("_2"); } } - syn::Ident::from(fn_name) + fn_name } fn read_response(read_response: bool, is_body_expr: bool, tokens: &mut Tokens) -> bool { @@ -170,8 +175,9 @@ impl<'a> YamlTests<'a> { self.tests .iter() .map(|test_fn| { - let fn_name = + let name = Self::unique_fn_name(test_fn.fn_name().as_ref(), &mut seen_method_names); + let fn_name = syn::Ident::from(name.as_str()); let mut body = Tokens::new(); let mut skip = Option::<&Skip>::None; @@ -180,15 +186,21 @@ impl<'a> YamlTests<'a> { for step in &test_fn.steps { match step { Step::Skip(s) => { - skip = if s.matches(self.version) { - let reason = match s.reason.as_ref() { - Some(s) => s.to_string(), - None => String::new(), - }; + skip = if s.version_matches(self.version) { info!( - "Skipping test because skip version '{}' are met. {}", - s.version.as_ref().unwrap(), - reason + "skipping {} in {:?} because version '{}' is met. {}", + &name, + self.path, + s.version(), + s.reason() + ); + Some(s) + } else if s.skip_features(&self.skips.features) { + info!( + "skipping {} in {:?} because it uses features '{:?}' which are currently not implemented", + &name, + self.path, + s.features() ); Some(s) } else { @@ -292,6 +304,12 @@ impl TestFn { } } +#[derive(Deserialize)] +struct Skips { + features: Vec, + tests: Vec, +} + pub fn generate_tests_from_yaml( api: &Api, suite: &TestSuite, @@ -300,6 +318,8 @@ pub fn generate_tests_from_yaml( download_dir: &PathBuf, generated_dir: &PathBuf, ) -> Result<(), failure::Error> { + + let skips = serde_yaml::from_str::(include_str!("./../skip.yml"))?; let paths = fs::read_dir(download_dir)?; for entry in paths { if let Ok(entry) = entry { @@ -349,7 +369,7 @@ pub fn generate_tests_from_yaml( let yaml = fs::read_to_string(&entry.path()).unwrap(); - // a yaml test can contain multiple yaml docs + // a yaml test can contain multiple yaml docs, so use yaml_rust to parse let result = YamlLoader::load_from_str(&yaml); if result.is_err() { info!( @@ -361,7 +381,7 @@ pub fn generate_tests_from_yaml( } let docs = result.unwrap(); - let mut test = YamlTests::new(version, test_suite, docs.len()); + let mut test = YamlTests::new(relative_path, version, &skips, test_suite, docs.len()); let results : Vec> = docs .iter() @@ -400,8 +420,8 @@ pub fn generate_tests_from_yaml( //if there has been an Err in any step of the yaml test file, don't create a test for it match ok_or_accumulate(&results, 1) { Ok(_) => write_test_file(test, &path, base_download_dir, generated_dir)?, - Err(e) => println!( - "Error creating test file for {:?}. skipping:\n{}", + Err(e) => info!( + "error creating test file for {:?}. skipping:\n{}", &entry.path(), e ), diff --git a/yaml_test_runner/src/step/do.rs b/yaml_test_runner/src/step/do.rs index d662623a..13abbf08 100644 --- a/yaml_test_runner/src/step/do.rs +++ b/yaml_test_runner/src/step/do.rs @@ -14,6 +14,7 @@ lazy_static! { static ref SET_REGEX: Regex = Regex::new(r#""\$(.*?)""#).unwrap(); + // replace usages of ${.*} with the captured value static ref SET_DELIMITED_REGEX: Regex = Regex::new(r#"\$\{(.*?)\}"#).unwrap(); @@ -217,7 +218,7 @@ impl ToTokens for ApiCall { let replacement = SET_DELIMITED_REGEX.replace_all(v, "{}"); quote! { .header( HeaderName::from_static(#k), - HeaderValue::from_str(format!(#replacement, #token).as_ref())?) + HeaderValue::from_str(format!(#replacement, #token.as_str().unwrap()).as_ref())?) } } else { quote! { .header( @@ -410,10 +411,9 @@ impl ApiCall { } TypeKind::Integer => { if is_set_value { - let t = Self::from_set_value(s); - let ident = syn::Ident::from(t); + let set_value = Self::from_set_value(s); tokens.append(quote! { - .#param_ident(#ident.as_i64().unwrap() as i32) + .#param_ident(#set_value.as_i64().unwrap() as i32) }); } else { let i = s.parse::()?; @@ -424,10 +424,9 @@ impl ApiCall { } TypeKind::Number | TypeKind::Long => { if is_set_value { - let t = Self::from_set_value(s); - let ident = syn::Ident::from(t); + let set_value = Self::from_set_value(s); tokens.append(quote! { - .#param_ident(#ident.as_i64().unwrap()) + .#param_ident(#set_value.as_i64().unwrap()) }); } else { let i = s.parse::()?; @@ -439,9 +438,8 @@ impl ApiCall { _ => { // handle set values let t = if is_set_value { - let t = Self::from_set_value(s); - let ident = syn::Ident::from(t); - quote! { #ident.as_str().unwrap() } + let set_value = Self::from_set_value(s); + quote! { #set_value.as_str().unwrap() } } else { quote! { #s } }; @@ -553,10 +551,18 @@ impl ApiCall { } } - fn from_set_value(s: &str) -> &str { - s.trim_start_matches('$') - .trim_start_matches('{') - .trim_end_matches('}') + fn from_set_value(s: &str) -> Tokens { + if s.starts_with('$') { + let ident = syn::Ident::from(s.trim_start_matches('$') + .trim_start_matches('{') + .trim_end_matches('}')); + quote! { #ident } + } else { + let token = syn::Ident::from(SET_DELIMITED_REGEX.captures(s).unwrap().get(1).unwrap().as_str()); + let replacement = SET_DELIMITED_REGEX.replace_all(s, "{}"); + // wrap in Value::String so that generated .as_str().unwrap() logic works the same for both branches + quote! { Value::String(format!(#replacement, #token.as_str().unwrap())) } + } } fn generate_parts( @@ -660,7 +666,7 @@ impl ApiCall { match v { Yaml::String(s) => { - let is_set_value = s.starts_with('$'); + let is_set_value = s.starts_with('$') || s.contains("${"); match ty.ty { TypeKind::List => { @@ -668,9 +674,8 @@ impl ApiCall { .split(',') .map(|s| { if is_set_value { - let t = Self::from_set_value(s); - let ident = syn::Ident::from(t); - quote! { #ident.as_str().unwrap() } + let set_value = Self::from_set_value(s); + quote! { #set_value.as_str().unwrap() } } else { quote! { #s } } @@ -680,9 +685,8 @@ impl ApiCall { } TypeKind::Long => { if is_set_value { - let t = Self::from_set_value(s); - let ident = syn::Ident::from(t); - Ok(quote! { #ident.as_i64().unwrap() }) + let set_value = Self::from_set_value(s); + Ok(quote! { #set_value.as_i64().unwrap() }) } else { let l = s.parse::().unwrap(); Ok(quote! { #l }) @@ -690,9 +694,8 @@ impl ApiCall { } _ => { if is_set_value { - let t = Self::from_set_value(s); - let ident = syn::Ident::from(t); - Ok(quote! { #ident.as_str().unwrap() }) + let set_value = Self::from_set_value(s); + Ok(quote! { #set_value.as_str().unwrap() }) } else { Ok(quote! { #s }) } diff --git a/yaml_test_runner/src/step/skip.rs b/yaml_test_runner/src/step/skip.rs index 0838a64f..f7037050 100644 --- a/yaml_test_runner/src/step/skip.rs +++ b/yaml_test_runner/src/step/skip.rs @@ -16,24 +16,25 @@ impl From for Step { } impl Skip { - pub fn try_parse(yaml: &Yaml) -> Result { - let version = yaml["version"] - .as_str() - .map_or_else(|| None, |y| Some(y.to_string())); - let reason = yaml["reason"] - .as_str() - .map_or_else(|| None, |y| Some(y.to_string())); - let features = match &yaml["features"] { - Yaml::String(s) => Some(vec![s.to_string()]), - Yaml::Array(a) => Some( - a.iter() - .map(|y| y.as_str().map(|s| s.to_string()).unwrap()) - .collect(), - ), - _ => None, - }; - let version_requirements = if let Some(v) = &version { + pub fn version(&self) -> String { + self.version.clone().unwrap_or_else(|| "".into()) + } + + pub fn reason(&self) -> String { + self.reason.clone().unwrap_or_else(|| "".into()) + } + + pub fn features(&self) -> &[String] { + match &self.features { + Some(v) => v, + None => &[] + } + } + + /// Converts the version range specified in the yaml test into a [semver::VersionReq] + fn parse_version_requirements(version: &Option) -> Option { + if let Some(v) = version { if v.to_lowercase() == "all" { Some(semver::VersionReq::any()) } else { @@ -47,7 +48,7 @@ impl Skip { semver::VersionReq::parse( format!(">={},<={}", start.as_str(), end.as_str()).as_ref(), ) - .unwrap(), + .unwrap(), ), (Some(start), None) => Some( semver::VersionReq::parse(format!(">={}", start.as_str()).as_ref()) @@ -65,8 +66,28 @@ impl Skip { } } else { None + } + } + + pub fn try_parse(yaml: &Yaml) -> Result { + let version = yaml["version"] + .as_str() + .map_or_else(|| None, |y| Some(y.to_string())); + let reason = yaml["reason"] + .as_str() + .map_or_else(|| None, |y| Some(y.to_string())); + let features = match &yaml["features"] { + Yaml::String(s) => Some(vec![s.to_string()]), + Yaml::Array(a) => Some( + a.iter() + .map(|y| y.as_str().map(|s| s.to_string()).unwrap()) + .collect(), + ), + _ => None, }; + let version_requirements = Self::parse_version_requirements(&version); + Ok(Skip { version, version_requirements, @@ -75,10 +96,19 @@ impl Skip { }) } - pub fn matches(&self, version: &semver::Version) -> bool { + /// Determines if this instance matches the version + pub fn version_matches(&self, version: &semver::Version) -> bool { match &self.version_requirements { Some(r) => r.matches(version), None => false, } } + + /// Determines if this instance matches the version + pub fn skip_features(&self, features: &Vec) -> bool { + match &self.features { + Some(test_features) => test_features.iter().any(|f| features.contains(f)), + None => false, + } + } } From 337c85bcdac5894c61faccaac8f4a4862094e429 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Mon, 11 May 2020 19:33:18 +1000 Subject: [PATCH 071/127] Skip tests specified in skip.yml --- yaml_test_runner/skip.yml | 6 +- yaml_test_runner/src/client.rs | 12 +-- yaml_test_runner/src/generator.rs | 127 +++++++++++++++++++---------- yaml_test_runner/src/step/do.rs | 19 +++-- yaml_test_runner/src/step/match.rs | 2 +- yaml_test_runner/src/step/mod.rs | 4 +- yaml_test_runner/src/step/skip.rs | 5 +- 7 files changed, 111 insertions(+), 64 deletions(-) diff --git a/yaml_test_runner/skip.yml b/yaml_test_runner/skip.yml index 8d524c71..64e54ccf 100644 --- a/yaml_test_runner/skip.yml +++ b/yaml_test_runner/skip.yml @@ -1,6 +1,10 @@ +# features not yet implemented features: - transform_and_set - node_selector - arbitrary_key -tests: [] \ No newline at end of file +# tests to skip generating and compiling a test for. +# Take the form of the generated path e.g. +# generated::xpack::security::hidden_index::_13_security_tokens_read::tests::test_get_security_tokens_index_metadata +tests: [] diff --git a/yaml_test_runner/src/client.rs b/yaml_test_runner/src/client.rs index 9e2a9d07..f1c0663f 100644 --- a/yaml_test_runner/src/client.rs +++ b/yaml_test_runner/src/client.rs @@ -58,8 +58,8 @@ pub fn create() -> Elasticsearch { let pass = p.to_string(); url.set_password(None).unwrap(); pass - }, - None => "changeme".into() + } + None => "changeme".into(), }; Some(Credentials::Basic(username, password)) @@ -71,12 +71,8 @@ pub fn create() -> Elasticsearch { let mut builder = TransportBuilder::new(conn_pool); builder = match credentials { - Some(c) => { - builder - .auth(c) - .cert_validation(CertificateValidation::None) - }, - None => builder + Some(c) => builder.auth(c).cert_validation(CertificateValidation::None), + None => builder, }; if running_proxy() { diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 42919d48..861e87c5 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -10,9 +10,10 @@ use std::collections::HashSet; use std::fs; use std::fs::{File, OpenOptions}; use std::io::Write; -use std::path::{Component, PathBuf, Path}; +use std::path::{Component, Path, PathBuf}; use yaml_rust::{Yaml, YamlLoader}; +/// The test suite to compile #[derive(Debug, PartialEq)] pub enum TestSuite { Oss, @@ -32,7 +33,13 @@ struct YamlTests<'a> { } impl<'a> YamlTests<'a> { - pub fn new(path: &'a Path, version: &'a semver::Version, skips: &'a Skips, suite: TestSuite, len: usize) -> Self { + pub fn new( + path: &'a Path, + version: &'a semver::Version, + skips: &'a Skips, + suite: TestSuite, + len: usize, + ) -> Self { Self { path, version, @@ -45,8 +52,7 @@ impl<'a> YamlTests<'a> { } } - /// Collects the use directives required for all steps in - /// the test + /// Collects the use directives required for all steps and tests fn use_directives_from_steps(steps: &[Step]) -> Vec { steps .iter() @@ -56,6 +62,7 @@ impl<'a> YamlTests<'a> { .collect() } + /// Adds a specific setup function pub fn add_setup(&mut self, setup: TestFn) -> &mut Self { let directives = Self::use_directives_from_steps(&setup.steps); for directive in directives { @@ -66,6 +73,7 @@ impl<'a> YamlTests<'a> { self } + /// Adds a specific teardown function pub fn add_teardown(&mut self, teardown: TestFn) -> &mut Self { let directives = Self::use_directives_from_steps(&teardown.steps); for directive in directives { @@ -76,6 +84,7 @@ impl<'a> YamlTests<'a> { self } + /// Adds a test to the collection of tests pub fn add_test_fn(&mut self, test_fn: TestFn) -> &mut Self { let directives = Self::use_directives_from_steps(&test_fn.steps); for directive in directives { @@ -95,7 +104,7 @@ impl<'a> YamlTests<'a> { TestSuite::XPack => quote!(client::general_xpack_setup(&client).await?;), }; - let tests: Vec = self.fn_impls(general_setup_call, setup_call, teardown_call); + let tests = self.fn_impls(general_setup_call, setup_call, teardown_call); let directives: Vec = self .directives @@ -164,12 +173,36 @@ impl<'a> YamlTests<'a> { true } + fn generated_full_name(&self, name: &str) -> String { + let mut test_file_path = test_file_path(self.path).unwrap(); + test_file_path.set_extension(""); + + let mut components = test_file_path + .components() + .into_iter() + .filter_map(|c| match c { + Component::Prefix(_) => None, + Component::RootDir => None, + Component::CurDir => None, + Component::ParentDir => None, + Component::Normal(n) => Some(n.to_string_lossy().into_owned()), + }) + .collect::>(); + + format!("generated::{}::tests::{}", components.join("::"), name) + } + + fn skip_test(&self, name: &str) -> bool { + let generated_name = self.generated_full_name(&name); + self.skips.tests.contains(&generated_name) + } + fn fn_impls( &self, general_setup_call: Tokens, setup_call: Option, teardown_call: Option, - ) -> Vec { + ) -> Vec> { let mut seen_method_names = HashSet::new(); self.tests @@ -177,8 +210,17 @@ impl<'a> YamlTests<'a> { .map(|test_fn| { let name = Self::unique_fn_name(test_fn.fn_name().as_ref(), &mut seen_method_names); - let fn_name = syn::Ident::from(name.as_str()); + if self.skip_test(&name) { + info!( + "skipping '{}' in {:?} because included in skip.yml", + &name, + self.path, + ); + return None; + } + + let fn_name = syn::Ident::from(name.as_str()); let mut body = Tokens::new(); let mut skip = Option::<&Skip>::None; let mut read_response = false; @@ -188,7 +230,7 @@ impl<'a> YamlTests<'a> { Step::Skip(s) => { skip = if s.version_matches(self.version) { info!( - "skipping {} in {:?} because version '{}' is met. {}", + "skipping '{}' in {:?} because version '{}' is met. {}", &name, self.path, s.version(), @@ -197,7 +239,7 @@ impl<'a> YamlTests<'a> { Some(s) } else if s.skip_features(&self.skips.features) { info!( - "skipping {} in {:?} because it uses features '{:?}' which are currently not implemented", + "skipping '{}' in {:?} because it needs features '{:?}' which are currently not implemented", &name, self.path, s.features() @@ -234,10 +276,9 @@ impl<'a> YamlTests<'a> { } } - // TODO: surface this some other way, other than returning empty tokens match skip { - Some(_) => Tokens::new(), - None => quote! { + Some(_) => None, + None => Some(quote! { #[tokio::test] async fn #fn_name() -> Result<(), failure::Error> { let client = client::create(); @@ -247,7 +288,7 @@ impl<'a> YamlTests<'a> { #teardown_call Ok(()) } - }, + }), } }) .collect() @@ -318,7 +359,6 @@ pub fn generate_tests_from_yaml( download_dir: &PathBuf, generated_dir: &PathBuf, ) -> Result<(), failure::Error> { - let skips = serde_yaml::from_str::(include_str!("./../skip.yml"))?; let paths = fs::read_dir(download_dir)?; for entry in paths { @@ -381,7 +421,8 @@ pub fn generate_tests_from_yaml( } let docs = result.unwrap(); - let mut test = YamlTests::new(relative_path, version, &skips, test_suite, docs.len()); + let mut test = + YamlTests::new(relative_path, version, &skips, test_suite, docs.len()); let results : Vec> = docs .iter() @@ -408,7 +449,7 @@ pub fn generate_tests_from_yaml( (k, v) => { Err(failure::err_msg(format!( "expected string key and array value in {:?}, but found {:?} and {:?}", - &entry.path(), + relative_path, &k, &v, ))) @@ -419,12 +460,8 @@ pub fn generate_tests_from_yaml( //if there has been an Err in any step of the yaml test file, don't create a test for it match ok_or_accumulate(&results, 1) { - Ok(_) => write_test_file(test, &path, base_download_dir, generated_dir)?, - Err(e) => info!( - "error creating test file for {:?}. skipping:\n{}", - &entry.path(), - e - ), + Ok(_) => write_test_file(test, relative_path, generated_dir)?, + Err(e) => info!("skipping test file for {:?}\n{}", relative_path, e), } } } @@ -470,32 +507,34 @@ fn write_mod_files(generated_dir: &PathBuf) -> Result<(), failure::Error> { Ok(()) } +fn test_file_path(relative_path: &Path) -> Result { + let mut relative = relative_path.to_path_buf(); + relative.set_extension(""); + // directories and files will form the module names so ensure they're valid module names + let clean: String = relative + .to_string_lossy() + .into_owned() + .replace(".", "_") + .replace("-", "_"); + + relative = PathBuf::from(clean); + // modules can't start with a number so prefix with underscore + relative.set_file_name(format!( + "_{}", + &relative.file_name().unwrap().to_string_lossy().into_owned() + )); + + Ok(relative) +} + fn write_test_file( test: YamlTests, - path: &PathBuf, - base_download_dir: &PathBuf, + relative_path: &Path, generated_dir: &PathBuf, ) -> Result<(), failure::Error> { - let path = { - let mut relative = path.strip_prefix(&base_download_dir)?.to_path_buf(); - relative.set_extension(""); - // directories and files will form the module names so ensure they're valid module names - let clean: String = relative - .to_string_lossy() - .into_owned() - .replace(".", "_") - .replace("-", "_"); - relative = PathBuf::from(clean); - - let mut path = generated_dir.join(relative); - path.set_extension("rs"); - // modules can't start with a number so prefix with underscore - path.set_file_name(format!( - "_{}", - &path.file_name().unwrap().to_string_lossy().into_owned() - )); - path - }; + let mut path = test_file_path(relative_path)?; + path = generated_dir.join(path); + path.set_extension("rs"); fs::create_dir_all(&path.parent().unwrap())?; let mut file = File::create(&path)?; diff --git a/yaml_test_runner/src/step/do.rs b/yaml_test_runner/src/step/do.rs index 13abbf08..ebf3053f 100644 --- a/yaml_test_runner/src/step/do.rs +++ b/yaml_test_runner/src/step/do.rs @@ -2,12 +2,12 @@ use inflector::Inflector; use quote::{ToTokens, Tokens}; use super::{ok_or_accumulate, Step}; +use crate::step::clean_regex; use api_generator::generator::{Api, ApiEndpoint, TypeKind}; use itertools::Itertools; use regex::Regex; use std::collections::BTreeMap; use yaml_rust::{Yaml, YamlEmitter}; -use crate::step::clean_regex; lazy_static! { // replace usages of "$.*" with the captured value @@ -553,12 +553,21 @@ impl ApiCall { fn from_set_value(s: &str) -> Tokens { if s.starts_with('$') { - let ident = syn::Ident::from(s.trim_start_matches('$') - .trim_start_matches('{') - .trim_end_matches('}')); + let ident = syn::Ident::from( + s.trim_start_matches('$') + .trim_start_matches('{') + .trim_end_matches('}'), + ); quote! { #ident } } else { - let token = syn::Ident::from(SET_DELIMITED_REGEX.captures(s).unwrap().get(1).unwrap().as_str()); + let token = syn::Ident::from( + SET_DELIMITED_REGEX + .captures(s) + .unwrap() + .get(1) + .unwrap() + .as_str(), + ); let replacement = SET_DELIMITED_REGEX.replace_all(s, "{}"); // wrap in Value::String so that generated .as_str().unwrap() logic works the same for both branches quote! { Value::String(format!(#replacement, #token.as_str().unwrap())) } diff --git a/yaml_test_runner/src/step/match.rs b/yaml_test_runner/src/step/match.rs index c4076e42..de008046 100644 --- a/yaml_test_runner/src/step/match.rs +++ b/yaml_test_runner/src/step/match.rs @@ -1,5 +1,5 @@ use super::Step; -use crate::step::{BodyExpr, clean_regex}; +use crate::step::{clean_regex, BodyExpr}; use quote::{ToTokens, Tokens}; use yaml_rust::Yaml; diff --git a/yaml_test_runner/src/step/mod.rs b/yaml_test_runner/src/step/mod.rs index 332c1773..aba6c3da 100644 --- a/yaml_test_runner/src/step/mod.rs +++ b/yaml_test_runner/src/step/mod.rs @@ -157,7 +157,7 @@ pub fn ok_or_accumulate( } else { let msg = errs .iter() - .map(|e| format!("{}{}", "\t".to_string().repeat(indent), e.to_string())) + .map(|e| format!("{}{}", " ".to_string().repeat(indent), e.to_string())) .collect::>() .join("\n"); @@ -175,4 +175,4 @@ pub fn clean_regex>(s: S) -> String { .replace("\\/", "/") .replace("\\:", ":") .replace("\\#", "#") -} \ No newline at end of file +} diff --git a/yaml_test_runner/src/step/skip.rs b/yaml_test_runner/src/step/skip.rs index f7037050..0f671c1f 100644 --- a/yaml_test_runner/src/step/skip.rs +++ b/yaml_test_runner/src/step/skip.rs @@ -16,7 +16,6 @@ impl From for Step { } impl Skip { - pub fn version(&self) -> String { self.version.clone().unwrap_or_else(|| "".into()) } @@ -28,7 +27,7 @@ impl Skip { pub fn features(&self) -> &[String] { match &self.features { Some(v) => v, - None => &[] + None => &[], } } @@ -48,7 +47,7 @@ impl Skip { semver::VersionReq::parse( format!(">={},<={}", start.as_str(), end.as_str()).as_ref(), ) - .unwrap(), + .unwrap(), ), (Some(start), None) => Some( semver::VersionReq::parse(format!(">={}", start.as_str()).as_ref()) From f738cb0df4d175f5ad26d14538cdc97bbba53d6c Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Mon, 11 May 2020 19:49:21 +1000 Subject: [PATCH 072/127] refactoring --- yaml_test_runner/src/client.rs | 4 +- yaml_test_runner/src/generator.rs | 86 ++++++++++++++----------------- yaml_test_runner/src/step/do.rs | 2 +- yaml_test_runner/src/step/skip.rs | 2 +- 4 files changed, 43 insertions(+), 51 deletions(-) diff --git a/yaml_test_runner/src/client.rs b/yaml_test_runner/src/client.rs index f1c0663f..73266d0b 100644 --- a/yaml_test_runner/src/client.rs +++ b/yaml_test_runner/src/client.rs @@ -67,7 +67,7 @@ pub fn create() -> Elasticsearch { None }; - let conn_pool = SingleNodeConnectionPool::new(url.clone()); + let conn_pool = SingleNodeConnectionPool::new(url); let mut builder = TransportBuilder::new(conn_pool); builder = match credentials { @@ -101,7 +101,7 @@ pub async fn general_oss_setup(client: &Elasticsearch) -> Result<(), Error> { let all_snapshots: Vec<(&str, &str)> = cat_snapshot_text .split('\n') - .map(|s| s.split(" ").collect::>()) + .map(|s| s.split(' ').collect::>()) .filter(|s| s.len() == 2) .map(|s| (s[0].trim(), s[1].trim())) .filter(|(id, repo)| !id.is_empty() && !repo.is_empty()) diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 861e87c5..dff57f99 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -138,25 +138,7 @@ impl<'a> YamlTests<'a> { } } - /// some function descriptions are the same in YAML tests, which would result in - /// duplicate generated test function names. Deduplicate by appending incrementing number - fn unique_fn_name(name: &str, seen_method_names: &mut HashSet) -> String { - let mut fn_name = name.to_string(); - while !seen_method_names.insert(fn_name.clone()) { - lazy_static! { - static ref ENDING_DIGITS_REGEX: Regex = Regex::new(r"^(.*?)_(\d*?)$").unwrap(); - } - if let Some(c) = ENDING_DIGITS_REGEX.captures(&fn_name) { - let name = c.get(1).unwrap().as_str(); - let n = c.get(2).unwrap().as_str().parse::().unwrap(); - fn_name = format!("{}_{}", name, n + 1); - } else { - fn_name.push_str("_2"); - } - } - fn_name - } - + /// Whether to emit code to read the last response, either as json or text fn read_response(read_response: bool, is_body_expr: bool, tokens: &mut Tokens) -> bool { if !read_response { if is_body_expr { @@ -173,27 +155,24 @@ impl<'a> YamlTests<'a> { true } - fn generated_full_name(&self, name: &str) -> String { - let mut test_file_path = test_file_path(self.path).unwrap(); - test_file_path.set_extension(""); - - let mut components = test_file_path - .components() - .into_iter() - .filter_map(|c| match c { - Component::Prefix(_) => None, - Component::RootDir => None, - Component::CurDir => None, - Component::ParentDir => None, - Component::Normal(n) => Some(n.to_string_lossy().into_owned()), - }) - .collect::>(); - - format!("generated::{}::tests::{}", components.join("::"), name) - } - + /// Whether the test should be skipped fn skip_test(&self, name: &str) -> bool { - let generated_name = self.generated_full_name(&name); + let generated_name = { + let mut test_file_path = test_file_path(self.path).unwrap(); + test_file_path.set_extension(""); + let components = test_file_path + .components() + .filter_map(|c| match c { + Component::Prefix(_) => None, + Component::RootDir => None, + Component::CurDir => None, + Component::ParentDir => None, + Component::Normal(n) => Some(n.to_string_lossy().into_owned()), + }) + .collect::>(); + + format!("generated::{}::tests::{}", components.join("::"), name) + }; self.skips.tests.contains(&generated_name) } @@ -203,14 +182,12 @@ impl<'a> YamlTests<'a> { setup_call: Option, teardown_call: Option, ) -> Vec> { - let mut seen_method_names = HashSet::new(); + let mut seen_names = HashSet::new(); self.tests .iter() .map(|test_fn| { - let name = - Self::unique_fn_name(test_fn.fn_name().as_ref(), &mut seen_method_names); - + let name = test_fn.unique_name(&mut seen_names); if self.skip_test(&name) { info!( "skipping '{}' in {:?} because included in skip.yml", @@ -340,8 +317,23 @@ impl TestFn { } } - pub fn fn_name(&self) -> String { - self.name.replace(" ", "_").to_lowercase().to_snake_case() + /// some function descriptions are the same in YAML tests, which would result in + /// duplicate generated test function names. Deduplicate by appending incrementing number + pub fn unique_name(&self, seen_names: &mut HashSet) -> String { + let mut fn_name = self.name.replace(" ", "_").to_lowercase().to_snake_case(); + while !seen_names.insert(fn_name.clone()) { + lazy_static! { + static ref ENDING_DIGITS_REGEX: Regex = Regex::new(r"^(.*?)_(\d*?)$").unwrap(); + } + if let Some(c) = ENDING_DIGITS_REGEX.captures(&fn_name) { + let name = c.get(1).unwrap().as_str(); + let n = c.get(2).unwrap().as_str().parse::().unwrap(); + fn_name = format!("{}_{}", name, n + 1); + } else { + fn_name.push_str("_2"); + } + } + fn_name } } @@ -376,7 +368,7 @@ pub fn generate_tests_from_yaml( } else if file_type.is_file() { let path = entry.path(); // skip non-yaml files - let extension = path.extension().unwrap_or("".as_ref()); + let extension = path.extension().unwrap_or_else(||"".as_ref()); if extension != "yml" && extension != "yaml" { continue; } @@ -385,7 +377,7 @@ pub fn generate_tests_from_yaml( let test_suite = { let mut components = relative_path.components(); let mut top_dir = "".to_string(); - while let Some(c) = components.next() { + for c in components { if c != Component::RootDir { top_dir = c.as_os_str().to_string_lossy().into_owned(); break; diff --git a/yaml_test_runner/src/step/do.rs b/yaml_test_runner/src/step/do.rs index ebf3053f..37922932 100644 --- a/yaml_test_runner/src/step/do.rs +++ b/yaml_test_runner/src/step/do.rs @@ -96,7 +96,7 @@ impl From for Step { impl Do { pub fn to_tokens(&self, mut read_response: bool, tokens: &mut Tokens) -> bool { // TODO: Add in warnings - &self.api_call.to_tokens(tokens); + self.api_call.to_tokens(tokens); if let Some(c) = &self.catch { if !read_response && c.needs_response_body() { diff --git a/yaml_test_runner/src/step/skip.rs b/yaml_test_runner/src/step/skip.rs index 0f671c1f..bf72cc02 100644 --- a/yaml_test_runner/src/step/skip.rs +++ b/yaml_test_runner/src/step/skip.rs @@ -104,7 +104,7 @@ impl Skip { } /// Determines if this instance matches the version - pub fn skip_features(&self, features: &Vec) -> bool { + pub fn skip_features(&self, features: &[String]) -> bool { match &self.features { Some(test_features) => test_features.iter().any(|f| features.contains(f)), None => false, From 095ca0570fdc99dd7e5ff865066c8217ee0e28a1 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 12 May 2020 10:02:11 +1000 Subject: [PATCH 073/127] Update TypeKind::None to TypeKind::Unknown(String) Better handle new types when introduced --- api_generator/src/generator/code_gen/mod.rs | 2 +- api_generator/src/generator/mod.rs | 47 ++++++++++----------- yaml_test_runner/src/generator.rs | 2 +- 3 files changed, 24 insertions(+), 27 deletions(-) diff --git a/api_generator/src/generator/code_gen/mod.rs b/api_generator/src/generator/code_gen/mod.rs index d9378d0c..aded86b6 100644 --- a/api_generator/src/generator/code_gen/mod.rs +++ b/api_generator/src/generator/code_gen/mod.rs @@ -128,7 +128,7 @@ fn typekind_to_ty(name: &str, kind: &TypeKind, required: bool, fn_arg: bool) -> let str_type = "&'b str"; match kind { - TypeKind::None => v.push_str(str_type), + TypeKind::Unknown(_) => v.push_str(str_type), TypeKind::List => { v.push_str("&'b ["); v.push_str(str_type); diff --git a/api_generator/src/generator/mod.rs b/api_generator/src/generator/mod.rs index fee6fa07..ac78b0d5 100644 --- a/api_generator/src/generator/mod.rs +++ b/api_generator/src/generator/mod.rs @@ -1,6 +1,9 @@ use crate::generator::code_gen::url::url_builder::PathString; use rustfmt_nightly::{Config, Edition, EmitMode, Input, Session}; -use serde::{Deserialize, Deserializer}; +use serde::{ + de::{MapAccess, Visitor}, + Deserialize, Deserializer, +}; use serde_json::Value; use std::{ collections::{BTreeMap, HashSet}, @@ -8,19 +11,22 @@ use std::{ fs::{self, File, OpenOptions}, hash::{Hash, Hasher}, io::{prelude::*, Read}, + marker::PhantomData, path::PathBuf, + str::FromStr, }; #[cfg(test)] use quote::{ToTokens, Tokens}; use semver::Version; -use serde::de::{Error, MapAccess, Visitor}; -use std::marker::PhantomData; -use std::str::FromStr; use void::Void; pub mod code_gen; +lazy_static! { + static ref VERSION: Version = semver::Version::parse(env!("CARGO_PKG_VERSION")).unwrap(); +} + /// A complete API specification parsed from the REST API specs pub struct Api { pub commit: String, @@ -101,7 +107,7 @@ pub struct Type { /// The type of the param or part #[derive(Debug, PartialEq, Clone)] pub enum TypeKind { - None, + Unknown(String), List, Enum, String, @@ -123,20 +129,7 @@ impl<'de> Deserialize<'de> for TypeKind { D: Deserializer<'de>, { let value = String::deserialize(deserializer)?; - if value.contains('|') { - let values: Vec<&str> = value.split('|').collect(); - - if values.len() > 2 { - Err(D::Error::custom( - "TypeKind union contains more than two values", - )) - } else { - let union = Box::new((TypeKind::from(values[0]), TypeKind::from(values[1]))); - Ok(TypeKind::Union(union)) - } - } else { - Ok(TypeKind::from(value.as_str())) - } + Ok(TypeKind::from(value.as_str())) } } @@ -155,14 +148,22 @@ impl From<&str> for TypeKind { "long" => TypeKind::Long, "date" => TypeKind::Date, "time" => TypeKind::Time, - n => panic!("unknown typekind {}", n), + n => { + let values: Vec<&str> = n.split('|').collect(); + if values.len() != 2 { + TypeKind::Unknown(n.to_string()) + } else { + let union = Box::new((TypeKind::from(values[0]), TypeKind::from(values[1]))); + TypeKind::Union(union) + } + } } } } impl Default for TypeKind { fn default() -> Self { - TypeKind::None + TypeKind::Unknown("".to_string()) } } @@ -197,10 +198,6 @@ pub struct Body { pub serialize: Option, } -lazy_static! { - static ref VERSION: Version = semver::Version::parse(env!("CARGO_PKG_VERSION")).unwrap(); -} - /// Wraps the URL string to replace master or current in URL path with the /// major.minor version of the api_generator. fn documentation_url_string<'de, D>(deserializer: D) -> Result diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index dff57f99..abb9c05d 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -368,7 +368,7 @@ pub fn generate_tests_from_yaml( } else if file_type.is_file() { let path = entry.path(); // skip non-yaml files - let extension = path.extension().unwrap_or_else(||"".as_ref()); + let extension = path.extension().unwrap_or_else(|| "".as_ref()); if extension != "yml" && extension != "yaml" { continue; } From af141068a09be484b3a9ae6f8bffed9e10f160ba Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 12 May 2020 20:12:14 +1000 Subject: [PATCH 074/127] Fix launching oss cluster --- .ci/run-elasticsearch.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.ci/run-elasticsearch.ps1 b/.ci/run-elasticsearch.ps1 index 33eb774d..9f4ae675 100644 --- a/.ci/run-elasticsearch.ps1 +++ b/.ci/run-elasticsearch.ps1 @@ -217,7 +217,7 @@ $volumes = @( "--volume", "${volume_name}:/usr/share/elasticsearch/data" ) -if (-not ($version -contains "oss")) { +if (-not ($version -match "oss")) { $environment += @( "--env", "ELASTIC_PASSWORD=`"$ELASTIC_PASSWORD`"", "--env", "xpack.license.self_generated.type=trial", @@ -241,7 +241,7 @@ if (-not ($version -contains "oss")) { } $url="http://$NODE_NAME" -if (-not ($version -contains "oss")) { +if (-not ($version -match "oss")) { $url="https://elastic:$ELASTIC_PASSWORD@$NODE_NAME" } From d9f09835e5251d71de5d956bc017cf8b32cd9ee3 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 12 May 2020 20:14:55 +1000 Subject: [PATCH 075/127] Implement is_true, is_false, numeric asserts Refactor BodyExpr into Expr struct --- yaml_test_runner/src/generator.rs | 47 ++++++++++---- yaml_test_runner/src/main.rs | 4 +- yaml_test_runner/src/step/comparison.rs | 83 +++++++++++++++++++++++++ yaml_test_runner/src/step/is_false.rs | 49 +++++++++++++++ yaml_test_runner/src/step/is_true.rs | 49 +++++++++++++++ yaml_test_runner/src/step/length.rs | 34 ++++------ yaml_test_runner/src/step/match.rs | 30 ++++----- yaml_test_runner/src/step/mod.rs | 81 ++++++++++++++++++------ yaml_test_runner/src/step/set.rs | 28 +++------ yaml_test_runner/src/step/skip.rs | 6 +- 10 files changed, 318 insertions(+), 93 deletions(-) create mode 100644 yaml_test_runner/src/step/comparison.rs create mode 100644 yaml_test_runner/src/step/is_false.rs create mode 100644 yaml_test_runner/src/step/is_true.rs diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index abb9c05d..c2ceac24 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -24,7 +24,7 @@ pub enum TestSuite { struct YamlTests<'a> { path: &'a Path, version: &'a Version, - skips: &'a Skips, + skip: &'a GlobalSkip, suite: TestSuite, directives: HashSet, setup: Option, @@ -36,14 +36,14 @@ impl<'a> YamlTests<'a> { pub fn new( path: &'a Path, version: &'a semver::Version, - skips: &'a Skips, + skip: &'a GlobalSkip, suite: TestSuite, len: usize, ) -> Self { Self { path, version, - skips, + skip, suite, directives: HashSet::with_capacity(len), setup: None, @@ -173,7 +173,7 @@ impl<'a> YamlTests<'a> { format!("generated::{}::tests::{}", components.join("::"), name) }; - self.skips.tests.contains(&generated_name) + self.skip.tests.contains(&generated_name) } fn fn_impls( @@ -205,7 +205,7 @@ impl<'a> YamlTests<'a> { for step in &test_fn.steps { match step { Step::Skip(s) => { - skip = if s.version_matches(self.version) { + skip = if s.skip_version(self.version) { info!( "skipping '{}' in {:?} because version '{}' is met. {}", &name, @@ -214,7 +214,7 @@ impl<'a> YamlTests<'a> { s.reason() ); Some(s) - } else if s.skip_features(&self.skips.features) { + } else if s.skip_features(&self.skip.features) { info!( "skipping '{}' in {:?} because it needs features '{:?}' which are currently not implemented", &name, @@ -232,7 +232,7 @@ impl<'a> YamlTests<'a> { Step::Match(m) => { read_response = Self::read_response( read_response, - m.is_body_expr(&m.expr), + m.expr.is_body(), &mut body, ); m.to_tokens(&mut body); @@ -245,10 +245,34 @@ impl<'a> YamlTests<'a> { Step::Length(l) => { read_response = Self::read_response( read_response, - l.is_body_expr(&l.expr), + false, &mut body, ); l.to_tokens(&mut body); + }, + Step::IsTrue(t) => { + read_response = Self::read_response( + read_response, + t.expr.is_body(), + &mut body, + ); + t.to_tokens(&mut body); + }, + Step::IsFalse(f) => { + read_response = Self::read_response( + read_response, + f.expr.is_body(), + &mut body, + ); + f.to_tokens(&mut body); + }, + Step::Comparison(c) => { + read_response = Self::read_response( + read_response, + c.expr.is_body(), + &mut body, + ); + c.to_tokens(&mut body); } } } @@ -337,8 +361,9 @@ impl TestFn { } } +/// Items to globally skip #[derive(Deserialize)] -struct Skips { +struct GlobalSkip { features: Vec, tests: Vec, } @@ -351,7 +376,7 @@ pub fn generate_tests_from_yaml( download_dir: &PathBuf, generated_dir: &PathBuf, ) -> Result<(), failure::Error> { - let skips = serde_yaml::from_str::(include_str!("./../skip.yml"))?; + let skips = serde_yaml::from_str::(include_str!("./../skip.yml"))?; let paths = fs::read_dir(download_dir)?; for entry in paths { if let Ok(entry) = entry { @@ -375,7 +400,7 @@ pub fn generate_tests_from_yaml( let relative_path = path.strip_prefix(&base_download_dir)?; let test_suite = { - let mut components = relative_path.components(); + let components = relative_path.components(); let mut top_dir = "".to_string(); for c in components { if c != Component::RootDir { diff --git a/yaml_test_runner/src/main.rs b/yaml_test_runner/src/main.rs index d46eb9af..462e294e 100644 --- a/yaml_test_runner/src/main.rs +++ b/yaml_test_runner/src/main.rs @@ -73,7 +73,9 @@ fn main() -> Result<(), failure::Error> { }; let v = v - .trim_start_matches("elasticsearch:") + .split(':') + .next_back() + .unwrap() .trim_end_matches(|c: char| c.is_alphabetic() || c == '-'); (suite, semver::Version::parse(v)?) diff --git a/yaml_test_runner/src/step/comparison.rs b/yaml_test_runner/src/step/comparison.rs new file mode 100644 index 00000000..d996ed85 --- /dev/null +++ b/yaml_test_runner/src/step/comparison.rs @@ -0,0 +1,83 @@ +use quote::{ToTokens, Tokens}; + +use super::Step; +use crate::step::{Expr}; +use yaml_rust::Yaml; + +pub const OPERATORS: [&'static str; 4] = ["lt", "lte", "gt", "gte"]; + +pub struct Comparison { + pub(crate) expr: Expr, + value: Yaml, + op: String, +} + +impl From for Step { + fn from(comparison: Comparison) -> Self { + Step::Comparison(comparison) + } +} + +impl Comparison { + pub fn try_parse(yaml: &Yaml, op: &str) -> Result { + let hash = yaml + .as_hash() + .ok_or_else(|| failure::err_msg(format!("expected hash but found {:?}", yaml)))?; + + let (k, v) = hash.iter().next().unwrap(); + let expr = k.as_str().ok_or_else(|| { + failure::err_msg(format!("expected string key but found {:?}", k)) + })?; + + Ok(Comparison { + expr: expr.into(), + value: v.clone(), + op: op.into() + }) + } + + fn assert(&self, t: T, expr: &str, op: &str, tokens: &mut Tokens) { + let ident = syn::Ident::from(expr); + let op_ident = syn::Ident::from(op); + let message = "Expected value at {} to be numeric but is {}"; + let comparison_message = "Expected value at {} to be {:?} {}, but is {}"; + tokens.append(quote! { + match &response_body#ident { + Value::Number(n) => { + match n.as_i64() { + Some(i) => assert!(i #op_ident #t as i64, #comparison_message, #expr, #op, #t, i), + None => match n.as_f64() { + Some(f) => assert!(f #op_ident #t as f64, #comparison_message, #expr, #op, #t, f), + None => match n.as_u64() { + Some(u) => assert!(u #op_ident #t as u64, #comparison_message, #expr, #op, #t, u), + None => assert!(false, #message, #expr, &n) + } + } + } + } + v => assert!(false, #message, #expr, &v), + } + }); + } +} + +impl ToTokens for Comparison { + fn to_tokens(&self, tokens: &mut Tokens) { + let expr = self.expr.expression(); + let op = match self.op.as_str() { + "lte" => "<=", + "lt" => "<", + "gt" => ">", + "gte" => ">=", + n => panic!("unsupported op {}", n) + }; + + match self.value.as_i64() { + Some(i) => self.assert(i, &expr, op, tokens), + None => match self.value.as_f64() { + Some(f) => self.assert(f, &expr, op, tokens), + None => panic!("Expected i64 or f64 but found {:?}", &self.value), + } + } + } +} diff --git a/yaml_test_runner/src/step/is_false.rs b/yaml_test_runner/src/step/is_false.rs new file mode 100644 index 00000000..8be7aa18 --- /dev/null +++ b/yaml_test_runner/src/step/is_false.rs @@ -0,0 +1,49 @@ +use quote::{ToTokens, Tokens}; + +use super::Step; +use crate::step::Expr; +use yaml_rust::Yaml; + +pub struct IsFalse { + pub(crate) expr: Expr, +} + +impl From for Step { + fn from(is_false: IsFalse) -> Self { + Step::IsFalse(is_false) + } +} + +impl IsFalse { + pub fn try_parse(yaml: &Yaml) -> Result { + let expr = yaml + .as_str() + .ok_or_else(|| failure::err_msg(format!("expected string key but found {:?}", &yaml)))?; + + Ok(IsFalse { + expr: expr.into(), + }) + } +} + +impl ToTokens for IsFalse { + fn to_tokens(&self, tokens: &mut Tokens) { + if self.expr.is_empty() { + tokens.append(quote! { + assert!(response_body.is_null() || response_body == json!("")); + }); + } else { + let expr = self.expr.expression(); + let ident = syn::Ident::from(expr.as_str()); + tokens.append(quote! { + match &response_body#ident { + Value::Null => assert!(true, "Expected value at {} to be false but is null", #expr), + Value::Bool(b) => assert_eq!(*b, false, "Expected value at {} to be false but is {}", #expr, b), + Value::Number(n) => assert!(n.as_f64().unwrap() == 0.0, "Expected value at {} to be false but is {}", #expr, n.as_f64().unwrap()), + Value::String(s) => assert!(s.is_empty(), "Expected value at {} to be false but is {}", #expr, &s), + v => assert!(false, "Expected value at {} to be false but is {:?}", #expr, &v), + } + }); + } + } +} diff --git a/yaml_test_runner/src/step/is_true.rs b/yaml_test_runner/src/step/is_true.rs new file mode 100644 index 00000000..3e1b8a27 --- /dev/null +++ b/yaml_test_runner/src/step/is_true.rs @@ -0,0 +1,49 @@ +use quote::{ToTokens, Tokens}; + +use super::Step; +use crate::step::Expr; +use yaml_rust::Yaml; + +pub struct IsTrue { + pub(crate) expr: Expr, +} + +impl From for Step { + fn from(is_true: IsTrue) -> Self { + Step::IsTrue(is_true) + } +} + +impl IsTrue { + pub fn try_parse(yaml: &Yaml) -> Result { + let expr = yaml + .as_str() + .ok_or_else(|| failure::err_msg(format!("expected string key but found {:?}", &yaml)))?; + + Ok(IsTrue { + expr: expr.into(), + }) + } +} + +impl ToTokens for IsTrue { + fn to_tokens(&self, tokens: &mut Tokens) { + if self.expr.is_empty() { + tokens.append(quote! { + assert!(!response_body.is_null() && response_body != json!("")); + }); + } else { + let expr = self.expr.expression(); + let ident = syn::Ident::from(expr.as_str()); + tokens.append(quote! { + match &response_body#ident { + Value::Null => assert!(false, "Expected value at {} to be true but is null", #expr), + Value::Bool(b) => assert!(*b, "Expected value at {} to be true but is {}", #expr, b), + Value::Number(n) => assert!(n.as_f64().unwrap() != 0.0, "Expected value at {} to be true but is {}", #expr, n.as_f64().unwrap()), + Value::String(s) => assert!(!s.is_empty(), "Expected value at {} to be true but is {}", #expr, &s), + v => assert!(true, "Expected value at {} to be true but is {:?}", #expr, &v), + } + }); + } + } +} diff --git a/yaml_test_runner/src/step/length.rs b/yaml_test_runner/src/step/length.rs index e0ac5538..b16f6511 100644 --- a/yaml_test_runner/src/step/length.rs +++ b/yaml_test_runner/src/step/length.rs @@ -1,12 +1,12 @@ use quote::{ToTokens, Tokens}; use super::Step; -use crate::step::BodyExpr; +use crate::step::Expr; use yaml_rust::Yaml; pub struct Length { + pub(crate) expr: Expr, len: usize, - pub(crate) expr: String, } impl From for Step { @@ -15,33 +15,21 @@ impl From for Step { } } -impl BodyExpr for Length { - // a length step should never advertise itself as a body expression as it would - // cause the body of the preceding API call to be returned as text rather than serde::Value. - // a serde::Value is needed as a length step on $body means counting object keys or array length. - fn is_body_expr(&self, _key: &str) -> bool { - false - } -} - impl Length { pub fn try_parse(yaml: &Yaml) -> Result { let hash = yaml .as_hash() .ok_or_else(|| failure::err_msg(format!("expected hash but found {:?}", yaml)))?; - let (expr, len) = { - let (k, v) = hash.iter().next().unwrap(); - let key = k.as_str().ok_or_else(|| { - failure::err_msg(format!("expected string key but found {:?}", k)) - })?; + let (k, v) = hash.iter().next().unwrap(); - let len = v - .as_i64() - .ok_or_else(|| failure::err_msg(format!("expected i64 but found {:?}", v)))?; + let expr = k + .as_str() + .ok_or_else(|| failure::err_msg(format!("expected string key but found {:?}", k)))?; - (key, len) - }; + let len = v + .as_i64() + .ok_or_else(|| failure::err_msg(format!("expected i64 but found {:?}", v)))?; Ok(Length { len: len as usize, @@ -54,13 +42,13 @@ impl ToTokens for Length { fn to_tokens(&self, tokens: &mut Tokens) { let len = self.len; - if &self.expr == "$body" { + if self.expr.is_body() { tokens.append(quote! { let len = util::len_from_value(&response_body)?; assert_eq!(#len, len); }); } else { - let expr = self.body_expr(&self.expr); + let expr = self.expr.expression(); let ident = syn::Ident::from(expr); tokens.append(quote! { let len = util::len_from_value(&response_body#ident)?; diff --git a/yaml_test_runner/src/step/match.rs b/yaml_test_runner/src/step/match.rs index de008046..14bea6a0 100644 --- a/yaml_test_runner/src/step/match.rs +++ b/yaml_test_runner/src/step/match.rs @@ -1,10 +1,10 @@ use super::Step; -use crate::step::{clean_regex, BodyExpr}; +use crate::step::{clean_regex, Expr}; use quote::{ToTokens, Tokens}; use yaml_rust::Yaml; pub struct Match { - pub expr: String, + pub expr: Expr, value: Yaml, } @@ -14,34 +14,28 @@ impl From for Step { } } -impl BodyExpr for Match {} - impl Match { pub fn try_parse(yaml: &Yaml) -> Result { let hash = yaml .as_hash() - .ok_or_else(|| failure::err_msg(format!("Expected hash but found {:?}", yaml)))?; - - let (expr, value) = { - let (k, v) = hash.iter().next().unwrap(); - let key = k.as_str().unwrap().trim().to_string(); - (key, v.clone()) - }; + .ok_or_else(|| failure::err_msg(format!("expected hash but found {:?}", yaml)))?; - Ok(Match { expr, value }) + let (k, v) = hash.iter().next().unwrap(); + let expr = k.as_str().unwrap().trim(); + Ok(Match { expr: expr.into(), value: v.clone() }) } } impl ToTokens for Match { fn to_tokens(&self, tokens: &mut Tokens) { - let expr = self.body_expr(&self.expr); + let expr = self.expr.expression(); match &self.value { Yaml::String(s) => { if s.starts_with('/') { let s = clean_regex(s); - if self.is_body_expr(&expr) { + if self.expr.is_body() { tokens.append(quote! { let regex = regex::RegexBuilder::new(#s) .ignore_whitespace(true) @@ -54,7 +48,7 @@ impl ToTokens for Match { ); }); } else { - let ident = syn::Ident::from(expr.clone()); + let ident = syn::Ident::from(expr.as_str()); tokens.append(quote! { let regex = regex::RegexBuilder::new(#s) .ignore_whitespace(true) @@ -69,7 +63,7 @@ impl ToTokens for Match { }); } } else { - let ident = syn::Ident::from(expr.clone()); + let ident = syn::Ident::from(expr.as_str()); // handle set values let t = if s.starts_with('$') { @@ -96,10 +90,10 @@ impl ToTokens for Match { } } Yaml::Integer(i) => { - if self.is_body_expr(&expr) { + if self.expr.is_body() { panic!("match on $body with integer"); } else { - let ident = syn::Ident::from(expr.clone()); + let ident = syn::Ident::from(expr.as_str()); tokens.append(quote! { assert_eq!( response_body#ident.as_i64().unwrap(), diff --git a/yaml_test_runner/src/step/mod.rs b/yaml_test_runner/src/step/mod.rs index aba6c3da..ffd709ab 100644 --- a/yaml_test_runner/src/step/mod.rs +++ b/yaml_test_runner/src/step/mod.rs @@ -7,11 +7,17 @@ mod length; mod r#match; mod set; mod skip; +mod is_true; +mod is_false; +mod comparison; pub use length::*; pub use r#do::*; pub use r#match::*; pub use set::*; pub use skip::*; +pub use is_true::*; +pub use is_false::*; +pub use comparison::{Comparison, OPERATORS}; pub fn parse_steps(api: &Api, steps: &[Yaml]) -> Result, failure::Error> { let mut parsed_steps: Vec = Vec::new(); @@ -48,17 +54,22 @@ pub fn parse_steps(api: &Api, steps: &[Yaml]) -> Result, failure::Erro parsed_steps.push(m.into()); } "contains" => {} - "is_true" => {} - "is_false" => {} + "is_true" => { + let e = IsTrue::try_parse(value)?; + parsed_steps.push(e.into()) + } + "is_false" => { + let e = IsFalse::try_parse(value)?; + parsed_steps.push(e.into()) + } "length" => { let l = Length::try_parse(value)?; parsed_steps.push(l.into()) } - "eq" => {} - "gte" => {} - "lte" => {} - "gt" => {} - "lt" => {} + op if OPERATORS.contains(&op) => { + let comp = Comparison::try_parse(value, op)?; + parsed_steps.push(comp.into()) + } op => return Err(failure::err_msg(format!("unknown step operation: {}", op))), } } @@ -66,20 +77,45 @@ pub fn parse_steps(api: &Api, steps: &[Yaml]) -> Result, failure::Erro Ok(parsed_steps) } -pub trait BodyExpr { - fn is_body_expr(&self, key: &str) -> bool { - key == "$body" +/// An expression to apply to the response. Can be the whole body ($body) or an +/// indexer expression into a JSON response. +pub struct Expr { + expr: String +} + +impl From<&str> for Expr { + fn from(s: &str) -> Self { + Expr::new(s) + } +} + +impl Expr { + pub fn new>(expr: S) -> Self { + Self { expr: expr.into() } + } + + /// Whether the expression is empty. Used in is_true and is_false + /// to represent the body + pub fn is_empty(&self) -> bool { + self.expr.is_empty() + } + + /// Whether the expression is "$body" + pub fn is_body(&self) -> bool { + Self::is_string_body(&self.expr) + } + + fn is_string_body(s: &str) -> bool { + s == "$body" } - /// Builds an indexer expression from the match key e.g. - /// match key `2.airline` is converted to `[2]["airline"]` - fn body_expr(&self, key: &str) -> String { - if self.is_body_expr(key) { - key.into() + pub fn expression(&self) -> String { + if self.is_body() { + self.expr.clone() } else { let mut values = Vec::new(); let mut value = String::new(); - let mut chars = key.chars(); + let mut chars = self.expr.chars(); while let Some(ch) = chars.next() { match ch { '\\' => { @@ -101,13 +137,15 @@ pub trait BodyExpr { // some APIs specify the response body as the first part of the path // which should be removed. - if self.is_body_expr(values[0].as_ref()) { + if Self::is_string_body(values[0].as_ref()) { values.remove(0); } let mut expr = String::new(); for s in values { - if s.chars().all(char::is_numeric) { + if s.is_empty() { + write!(expr, "[\"\"]").unwrap(); + } else if s.chars().all(char::is_numeric) { write!(expr, "[{}]", s).unwrap(); } else if s.starts_with('$') { // handle set values @@ -125,12 +163,16 @@ pub trait BodyExpr { } } +/// Steps defined in a yaml test pub enum Step { Skip(Skip), Set(Set), Do(Do), Match(Match), Length(Length), + IsTrue(IsTrue), + IsFalse(IsFalse), + Comparison(Comparison), } impl Step { @@ -167,7 +209,7 @@ pub fn ok_or_accumulate( // trim the enclosing forward slashes and // 1. replace escaped forward slashes (not needed after trimming forward slashes) -// 2. replace escaped colons (not supported by regex crate) +// 2. replace escaped colons and hashes (not supported by regex crate) pub fn clean_regex>(s: S) -> String { s.as_ref() .trim() @@ -175,4 +217,5 @@ pub fn clean_regex>(s: S) -> String { .replace("\\/", "/") .replace("\\:", ":") .replace("\\#", "#") + .replace("\\%", "%") } diff --git a/yaml_test_runner/src/step/set.rs b/yaml_test_runner/src/step/set.rs index b399e554..6e018cc6 100644 --- a/yaml_test_runner/src/step/set.rs +++ b/yaml_test_runner/src/step/set.rs @@ -1,12 +1,12 @@ use quote::{ToTokens, Tokens}; use super::Step; -use crate::step::BodyExpr; +use crate::step::Expr; use yaml_rust::Yaml; pub struct Set { ident: syn::Ident, - expr: String, + expr: Expr, } impl From for Step { @@ -15,26 +15,20 @@ impl From for Step { } } -impl BodyExpr for Set {} - impl Set { pub fn try_parse(yaml: &Yaml) -> Result { let hash = yaml .as_hash() .ok_or_else(|| failure::err_msg(format!("expected hash but found {:?}", yaml)))?; - let (expr, id) = { - let (k, yaml) = hash.iter().next().unwrap(); - let key = k.as_str().ok_or_else(|| { - failure::err_msg(format!("Expected string key but found {:?}", k)) - })?; - - let id = yaml.as_str().ok_or_else(|| { - failure::err_msg(format!("Expected string value but found {:?}", k)) - })?; + let (k, v) = hash.iter().next().unwrap(); + let expr = k + .as_str() + .ok_or_else(|| failure::err_msg(format!("expected string key but found {:?}", k)))?; - (key, id) - }; + let id = v + .as_str() + .ok_or_else(|| failure::err_msg(format!("expected string value but found {:?}", v)))?; Ok(Set { ident: syn::Ident::from(id), @@ -46,9 +40,7 @@ impl Set { impl ToTokens for Set { fn to_tokens(&self, tokens: &mut Tokens) { let ident = &self.ident; - let expr = syn::Ident::from(self.body_expr(&self.expr)); - - // TODO: Unwrap serde_json value here, or in the usage? + let expr = syn::Ident::from(self.expr.expression().as_str()); tokens.append(quote! { let #ident = response_body#expr.clone(); }); diff --git a/yaml_test_runner/src/step/skip.rs b/yaml_test_runner/src/step/skip.rs index bf72cc02..f9c0fd38 100644 --- a/yaml_test_runner/src/step/skip.rs +++ b/yaml_test_runner/src/step/skip.rs @@ -4,8 +4,8 @@ use yaml_rust::Yaml; pub struct Skip { version_requirements: Option, - pub version: Option, - pub reason: Option, + version: Option, + reason: Option, features: Option>, } @@ -96,7 +96,7 @@ impl Skip { } /// Determines if this instance matches the version - pub fn version_matches(&self, version: &semver::Version) -> bool { + pub fn skip_version(&self, version: &semver::Version) -> bool { match &self.version_requirements { Some(r) => r.matches(version), None => false, From ac09db368ecea4109ef17c126024ccd92c450a0d Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 12 May 2020 20:15:19 +1000 Subject: [PATCH 076/127] Implement content_type on response Convenience method to read content_type header --- elasticsearch/src/http/response.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/elasticsearch/src/http/response.rs b/elasticsearch/src/http/response.rs index dab2059d..d6470694 100644 --- a/elasticsearch/src/http/response.rs +++ b/elasticsearch/src/http/response.rs @@ -25,6 +25,15 @@ impl Response { self.0.content_length() } + /// Gets the content-type of this response. + pub fn content_type(&self) -> &str { + self.0 + .headers() + .get(crate::http::headers::CONTENT_TYPE) + .and_then(|value| value.to_str().ok()) + .unwrap() + } + /// Get the HTTP status code of the response pub fn status_code(&self) -> StatusCode { self.0.status() From 9f983ed07052e7594cd047c617024d86070ba606 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 12 May 2020 20:52:04 +1000 Subject: [PATCH 077/127] Rename to json and text This commit renames use of response_body and string_response_body to json and text, respectively. Always read response as text and parse to json if the response content-type is application/json --- yaml_test_runner/src/generator.rs | 52 ++++++++----------------- yaml_test_runner/src/step/comparison.rs | 2 +- yaml_test_runner/src/step/do.rs | 10 ++++- yaml_test_runner/src/step/is_false.rs | 4 +- yaml_test_runner/src/step/is_true.rs | 4 +- yaml_test_runner/src/step/length.rs | 4 +- yaml_test_runner/src/step/match.rs | 16 ++++---- yaml_test_runner/src/step/set.rs | 2 +- 8 files changed, 40 insertions(+), 54 deletions(-) diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index c2ceac24..d4adc779 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -139,17 +139,17 @@ impl<'a> YamlTests<'a> { } /// Whether to emit code to read the last response, either as json or text - fn read_response(read_response: bool, is_body_expr: bool, tokens: &mut Tokens) -> bool { + pub fn read_response(read_response: bool, tokens: &mut Tokens) -> bool { if !read_response { - if is_body_expr { - tokens.append(quote! { - let string_response_body = response.text().await?; - }); - } else { - tokens.append(quote! { - let response_body = response.json::().await?; - }); - } + tokens.append(quote! { + let is_json = response.content_type().starts_with("application/json"); + let text = response.text().await?; + let json : Value = if is_json { + serde_json::from_slice(text.as_ref())? + } else { + Value::Null + }; + }); } true @@ -230,48 +230,28 @@ impl<'a> YamlTests<'a> { read_response = d.to_tokens(false, &mut body); } Step::Match(m) => { - read_response = Self::read_response( - read_response, - m.expr.is_body(), - &mut body, - ); + read_response = Self::read_response(read_response,&mut body); m.to_tokens(&mut body); } Step::Set(s) => { // TODO: is "set" ever is_body_expr? - read_response = Self::read_response(read_response, false, &mut body); + read_response = Self::read_response(read_response, &mut body); s.to_tokens(&mut body); } Step::Length(l) => { - read_response = Self::read_response( - read_response, - false, - &mut body, - ); + read_response = Self::read_response(read_response,&mut body); l.to_tokens(&mut body); }, Step::IsTrue(t) => { - read_response = Self::read_response( - read_response, - t.expr.is_body(), - &mut body, - ); + read_response = Self::read_response(read_response,&mut body); t.to_tokens(&mut body); }, Step::IsFalse(f) => { - read_response = Self::read_response( - read_response, - f.expr.is_body(), - &mut body, - ); + read_response = Self::read_response(read_response, &mut body); f.to_tokens(&mut body); }, Step::Comparison(c) => { - read_response = Self::read_response( - read_response, - c.expr.is_body(), - &mut body, - ); + read_response = Self::read_response(read_response,&mut body); c.to_tokens(&mut body); } } diff --git a/yaml_test_runner/src/step/comparison.rs b/yaml_test_runner/src/step/comparison.rs index d996ed85..ba6d0124 100644 --- a/yaml_test_runner/src/step/comparison.rs +++ b/yaml_test_runner/src/step/comparison.rs @@ -42,7 +42,7 @@ impl Comparison { let message = "Expected value at {} to be numeric but is {}"; let comparison_message = "Expected value at {} to be {:?} {}, but is {}"; tokens.append(quote! { - match &response_body#ident { + match &json#ident { Value::Number(n) => { match n.as_i64() { Some(i) => assert!(i #op_ident #t as i64, #comparison_message, #expr, #op, #t, i), diff --git a/yaml_test_runner/src/step/do.rs b/yaml_test_runner/src/step/do.rs index 37922932..b591c4ec 100644 --- a/yaml_test_runner/src/step/do.rs +++ b/yaml_test_runner/src/step/do.rs @@ -61,7 +61,7 @@ impl ToTokens for Catch { let t = clean_regex(s); tokens.append(quote! { let catch_regex = regex::Regex::new(#t)?; - let error = response_body["error"].to_string(); + let error = json["error"].to_string(); assert!( catch_regex.is_match(&error), "expected json value at {}:\n\n{}\n\nto match regex:\n\n{}", @@ -102,7 +102,13 @@ impl Do { if !read_response && c.needs_response_body() { read_response = true; tokens.append(quote! { - let response_body = response.json::().await?; + let is_json = response.content_type().starts_with("application/json"); + let text = response.text().await?; + let json : Value = if is_json { + serde_json::from_slice(text.as_ref())? + } else { + Value::Null + }; }); } c.to_tokens(tokens); diff --git a/yaml_test_runner/src/step/is_false.rs b/yaml_test_runner/src/step/is_false.rs index 8be7aa18..5eca83e7 100644 --- a/yaml_test_runner/src/step/is_false.rs +++ b/yaml_test_runner/src/step/is_false.rs @@ -30,13 +30,13 @@ impl ToTokens for IsFalse { fn to_tokens(&self, tokens: &mut Tokens) { if self.expr.is_empty() { tokens.append(quote! { - assert!(response_body.is_null() || response_body == json!("")); + assert!(text.is_empty()); }); } else { let expr = self.expr.expression(); let ident = syn::Ident::from(expr.as_str()); tokens.append(quote! { - match &response_body#ident { + match &json#ident { Value::Null => assert!(true, "Expected value at {} to be false but is null", #expr), Value::Bool(b) => assert_eq!(*b, false, "Expected value at {} to be false but is {}", #expr, b), Value::Number(n) => assert!(n.as_f64().unwrap() == 0.0, "Expected value at {} to be false but is {}", #expr, n.as_f64().unwrap()), diff --git a/yaml_test_runner/src/step/is_true.rs b/yaml_test_runner/src/step/is_true.rs index 3e1b8a27..4ff544e6 100644 --- a/yaml_test_runner/src/step/is_true.rs +++ b/yaml_test_runner/src/step/is_true.rs @@ -30,13 +30,13 @@ impl ToTokens for IsTrue { fn to_tokens(&self, tokens: &mut Tokens) { if self.expr.is_empty() { tokens.append(quote! { - assert!(!response_body.is_null() && response_body != json!("")); + assert!(!text.is_empty()); }); } else { let expr = self.expr.expression(); let ident = syn::Ident::from(expr.as_str()); tokens.append(quote! { - match &response_body#ident { + match &json#ident { Value::Null => assert!(false, "Expected value at {} to be true but is null", #expr), Value::Bool(b) => assert!(*b, "Expected value at {} to be true but is {}", #expr, b), Value::Number(n) => assert!(n.as_f64().unwrap() != 0.0, "Expected value at {} to be true but is {}", #expr, n.as_f64().unwrap()), diff --git a/yaml_test_runner/src/step/length.rs b/yaml_test_runner/src/step/length.rs index b16f6511..d2e029dc 100644 --- a/yaml_test_runner/src/step/length.rs +++ b/yaml_test_runner/src/step/length.rs @@ -44,14 +44,14 @@ impl ToTokens for Length { if self.expr.is_body() { tokens.append(quote! { - let len = util::len_from_value(&response_body)?; + let len = util::len_from_value(&json)?; assert_eq!(#len, len); }); } else { let expr = self.expr.expression(); let ident = syn::Ident::from(expr); tokens.append(quote! { - let len = util::len_from_value(&response_body#ident)?; + let len = util::len_from_value(&json#ident)?; assert_eq!(#len, len); }); } diff --git a/yaml_test_runner/src/step/match.rs b/yaml_test_runner/src/step/match.rs index 14bea6a0..5845c4e7 100644 --- a/yaml_test_runner/src/step/match.rs +++ b/yaml_test_runner/src/step/match.rs @@ -41,9 +41,9 @@ impl ToTokens for Match { .ignore_whitespace(true) .build()?; assert!( - regex.is_match(&string_response_body), + regex.is_match(&text), "expected $body:\n\n{}\n\nto match regex:\n\n{}", - &string_response_body, + &text, #s ); }); @@ -54,10 +54,10 @@ impl ToTokens for Match { .ignore_whitespace(true) .build()?; assert!( - regex.is_match(response_body#ident.as_str().unwrap()), + regex.is_match(json#ident.as_str().unwrap()), "expected value at {}:\n\n{}\n\nto match regex:\n\n{}", #expr, - response_body#ident.as_str().unwrap(), + json#ident.as_str().unwrap(), #s ); }); @@ -79,12 +79,12 @@ impl ToTokens for Match { tokens.append(quote! { assert_eq!( - response_body#ident.as_str().unwrap(), + json#ident.as_str().unwrap(), #t, "expected value at {} to be {} but was {}", #expr, #t, - response_body#ident.as_str().unwrap() + json#ident.as_str().unwrap() ); }) } @@ -96,12 +96,12 @@ impl ToTokens for Match { let ident = syn::Ident::from(expr.as_str()); tokens.append(quote! { assert_eq!( - response_body#ident.as_i64().unwrap(), + json#ident.as_i64().unwrap(), #i, "expected value at {} to be {} but was {}", #expr, #i, - response_body#ident.as_i64().unwrap() + json#ident.as_i64().unwrap() ); }); } diff --git a/yaml_test_runner/src/step/set.rs b/yaml_test_runner/src/step/set.rs index 6e018cc6..8ddf9394 100644 --- a/yaml_test_runner/src/step/set.rs +++ b/yaml_test_runner/src/step/set.rs @@ -42,7 +42,7 @@ impl ToTokens for Set { let ident = &self.ident; let expr = syn::Ident::from(self.expr.expression().as_str()); tokens.append(quote! { - let #ident = response_body#expr.clone(); + let #ident = json#expr.clone(); }); } } From 54fc2e70ff304b946b19cbb9a647b9f46518c705 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 12 May 2020 20:52:23 +1000 Subject: [PATCH 078/127] Handle set values in numeric comparisons --- yaml_test_runner/src/step/comparison.rs | 33 ++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/yaml_test_runner/src/step/comparison.rs b/yaml_test_runner/src/step/comparison.rs index ba6d0124..eecdb8a3 100644 --- a/yaml_test_runner/src/step/comparison.rs +++ b/yaml_test_runner/src/step/comparison.rs @@ -76,7 +76,38 @@ impl ToTokens for Comparison { Some(i) => self.assert(i, &expr, op, tokens), None => match self.value.as_f64() { Some(f) => self.assert(f, &expr, op, tokens), - None => panic!("Expected i64 or f64 but found {:?}", &self.value), + None => { + match self.value.as_str() { + Some(s) if s.starts_with('$') => { + let s = s.trim_start_matches('$') + .trim_start_matches('{') + .trim_end_matches('}'); + let expr_ident = syn::Ident::from(expr.as_str()); + let ident = syn::Ident::from(s); + let op_ident = syn::Ident::from(op); + let message = "Expected value at {} to be numeric but is {}"; + let comparison_message = "Expected value at {} to be {:?} {}, but is {}"; + tokens.append(quote! { + match &json#expr_ident { + Value::Number(n) => { + match n.as_i64() { + Some(i) => assert!(i #op_ident #ident.as_i64().unwrap(), #comparison_message, #expr, #op, #ident.as_i64().unwrap(), i), + None => match n.as_f64() { + Some(f) => assert!(f #op_ident #ident.as_f64().unwrap(), #comparison_message, #expr, #op, #ident.as_f64().unwrap(), f), + None => match n.as_u64() { + Some(u) => assert!(u #op_ident #ident.as_u64().unwrap(), #comparison_message, #expr, #op, #ident.as_u64().unwrap(), u), + None => assert!(false, #message, #expr, &n) + } + } + } + } + v => assert!(false, #message, #expr, &v), + } + }); + } + _ => panic!("Expected i64 or f64 but found {:?}", &self.value), + } + } } } } From 589e683f7d0cc8c692047f927d8fc82ed3baa244 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Wed, 13 May 2020 10:11:42 +1000 Subject: [PATCH 079/127] Add assertions in deleting indices setup --- yaml_test_runner/src/client.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/yaml_test_runner/src/client.rs b/yaml_test_runner/src/client.rs index 73266d0b..1cc8ab6b 100644 --- a/yaml_test_runner/src/client.rs +++ b/yaml_test_runner/src/client.rs @@ -182,18 +182,22 @@ pub async fn general_xpack_setup(client: &Elasticsearch) -> Result<(), Error> { } async fn wait_for_yellow_status(client: &Elasticsearch) -> Result<(), Error> { - let _ = client + let cluster_health = client .cluster() .health(ClusterHealthParts::None) .wait_for_status(WaitForStatus::Yellow) .send() .await?; + assert!( + cluster_health.status_code().is_success(), + "cluster health returned {}", cluster_health.status_code().as_u16()); + Ok(()) } async fn delete_indices(client: &Elasticsearch) -> Result<(), Error> { - let _delete_response = client + let delete_response = client .indices() .delete(IndicesDeleteParts::Index(&["*"])) .expand_wildcards(&[ @@ -204,6 +208,10 @@ async fn delete_indices(client: &Elasticsearch) -> Result<(), Error> { .send() .await?; + assert!( + delete_response.status_code().is_success(), + "deleting indices returned {}", delete_response.status_code().as_u16()); + Ok(()) } From 99b3c86c27eb899e65a25a5a35b52ee41b6af5f1 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Wed, 13 May 2020 10:16:09 +1000 Subject: [PATCH 080/127] Simplify assertions --- yaml_test_runner/src/step/comparison.rs | 1 + yaml_test_runner/src/step/is_false.rs | 2 +- yaml_test_runner/src/step/is_true.rs | 2 +- yaml_test_runner/src/step/skip.rs | 3 +++ 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/yaml_test_runner/src/step/comparison.rs b/yaml_test_runner/src/step/comparison.rs index eecdb8a3..c96d51f4 100644 --- a/yaml_test_runner/src/step/comparison.rs +++ b/yaml_test_runner/src/step/comparison.rs @@ -78,6 +78,7 @@ impl ToTokens for Comparison { Some(f) => self.assert(f, &expr, op, tokens), None => { match self.value.as_str() { + // handle "set" values Some(s) if s.starts_with('$') => { let s = s.trim_start_matches('$') .trim_start_matches('{') diff --git a/yaml_test_runner/src/step/is_false.rs b/yaml_test_runner/src/step/is_false.rs index 5eca83e7..08e08a69 100644 --- a/yaml_test_runner/src/step/is_false.rs +++ b/yaml_test_runner/src/step/is_false.rs @@ -39,7 +39,7 @@ impl ToTokens for IsFalse { match &json#ident { Value::Null => assert!(true, "Expected value at {} to be false but is null", #expr), Value::Bool(b) => assert_eq!(*b, false, "Expected value at {} to be false but is {}", #expr, b), - Value::Number(n) => assert!(n.as_f64().unwrap() == 0.0, "Expected value at {} to be false but is {}", #expr, n.as_f64().unwrap()), + Value::Number(n) => assert_eq!(n.as_f64().unwrap(), 0.0, "Expected value at {} to be false but is {}", #expr, n.as_f64().unwrap()), Value::String(s) => assert!(s.is_empty(), "Expected value at {} to be false but is {}", #expr, &s), v => assert!(false, "Expected value at {} to be false but is {:?}", #expr, &v), } diff --git a/yaml_test_runner/src/step/is_true.rs b/yaml_test_runner/src/step/is_true.rs index 4ff544e6..928256bb 100644 --- a/yaml_test_runner/src/step/is_true.rs +++ b/yaml_test_runner/src/step/is_true.rs @@ -39,7 +39,7 @@ impl ToTokens for IsTrue { match &json#ident { Value::Null => assert!(false, "Expected value at {} to be true but is null", #expr), Value::Bool(b) => assert!(*b, "Expected value at {} to be true but is {}", #expr, b), - Value::Number(n) => assert!(n.as_f64().unwrap() != 0.0, "Expected value at {} to be true but is {}", #expr, n.as_f64().unwrap()), + Value::Number(n) => assert_ne!(n.as_f64().unwrap(), 0.0, "Expected value at {} to be true but is {}", #expr, n.as_f64().unwrap()), Value::String(s) => assert!(!s.is_empty(), "Expected value at {} to be true but is {}", #expr, &s), v => assert!(true, "Expected value at {} to be true but is {:?}", #expr, &v), } diff --git a/yaml_test_runner/src/step/skip.rs b/yaml_test_runner/src/step/skip.rs index f9c0fd38..6a008eae 100644 --- a/yaml_test_runner/src/step/skip.rs +++ b/yaml_test_runner/src/step/skip.rs @@ -16,14 +16,17 @@ impl From for Step { } impl Skip { + /// Gets the version. Returns empty if no version pub fn version(&self) -> String { self.version.clone().unwrap_or_else(|| "".into()) } + /// Gets the reason. Returns empty string if no reason pub fn reason(&self) -> String { self.reason.clone().unwrap_or_else(|| "".into()) } + /// Gets the features. Returns empty slice if no features pub fn features(&self) -> &[String] { match &self.features { Some(v) => v, From 43a97a4b4d85946e4226008bf085cc9264cd592f Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Wed, 13 May 2020 10:54:56 +1000 Subject: [PATCH 081/127] Handle _arbitrary_key_ --- yaml_test_runner/src/step/mod.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/yaml_test_runner/src/step/mod.rs b/yaml_test_runner/src/step/mod.rs index ffd709ab..9cc26a08 100644 --- a/yaml_test_runner/src/step/mod.rs +++ b/yaml_test_runner/src/step/mod.rs @@ -154,6 +154,10 @@ impl Expr { .trim_start_matches('{') .trim_end_matches('}'); write!(expr, "[{}.as_str().unwrap()]", t).unwrap(); + } else if s.as_str() == "_arbitrary_key_" { + // handle _arbitrary_key_. + // wrap in Value::String to allow uniform unwrapping in subsequent steps + write!(expr, ".as_object().unwrap().iter().next().map(|(k, _)| json!(k)).unwrap()").unwrap(); } else { write!(expr, "[\"{}\"]", s).unwrap(); } From 3e5f19e4ba2528c915fa51df4777963515ddbc0c Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Wed, 13 May 2020 15:16:05 +1000 Subject: [PATCH 082/127] Handle transform_and_set --- yaml_test_runner/Cargo.toml | 1 + yaml_test_runner/skip.yml | 2 - yaml_test_runner/src/generator.rs | 24 ++-- yaml_test_runner/src/main.rs | 1 + yaml_test_runner/src/step/do.rs | 59 ++++++++-- yaml_test_runner/src/step/mod.rs | 11 +- .../src/step/transform_and_set.rs | 110 ++++++++++++++++++ yaml_test_runner/src/transform.rs | 12 ++ 8 files changed, 197 insertions(+), 23 deletions(-) create mode 100644 yaml_test_runner/src/step/transform_and_set.rs create mode 100644 yaml_test_runner/src/transform.rs diff --git a/yaml_test_runner/Cargo.toml b/yaml_test_runner/Cargo.toml index 3ffe7e39..8f08283f 100644 --- a/yaml_test_runner/Cargo.toml +++ b/yaml_test_runner/Cargo.toml @@ -10,6 +10,7 @@ repository = "https://github.com/elastic/elasticsearch-rs" elasticsearch = { path = "../elasticsearch" } api_generator = { path = "../api_generator" } +base64 = "^0.11" clap = "~2" failure = "0.1.6" itertools = "0.8.2" diff --git a/yaml_test_runner/skip.yml b/yaml_test_runner/skip.yml index 64e54ccf..9266cb51 100644 --- a/yaml_test_runner/skip.yml +++ b/yaml_test_runner/skip.yml @@ -1,8 +1,6 @@ # features not yet implemented features: - - transform_and_set - node_selector - - arbitrary_key # tests to skip generating and compiling a test for. # Take the form of the generated path e.g. diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index d4adc779..8362f91f 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -129,6 +129,7 @@ impl<'a> YamlTests<'a> { use regex; use serde_json::Value; use crate::client; + use crate::transform; use crate::util; #setup_fn @@ -138,16 +139,20 @@ impl<'a> YamlTests<'a> { } } - /// Whether to emit code to read the last response, either as json or text + /// Whether to emit code to read the last response, as text and optionally json pub fn read_response(read_response: bool, tokens: &mut Tokens) -> bool { if !read_response { tokens.append(quote! { - let is_json = response.content_type().starts_with("application/json"); - let text = response.text().await?; - let json : Value = if is_json { - serde_json::from_slice(text.as_ref())? - } else { - Value::Null + let (text, json) = { + let is_json = response.content_type().starts_with("application/json"); + let text = response.text().await?; + let json = if is_json { + serde_json::from_slice::(text.as_ref())? + } else { + Value::Null + }; + + (text, json) }; }); } @@ -234,7 +239,6 @@ impl<'a> YamlTests<'a> { m.to_tokens(&mut body); } Step::Set(s) => { - // TODO: is "set" ever is_body_expr? read_response = Self::read_response(read_response, &mut body); s.to_tokens(&mut body); } @@ -253,6 +257,10 @@ impl<'a> YamlTests<'a> { Step::Comparison(c) => { read_response = Self::read_response(read_response,&mut body); c.to_tokens(&mut body); + }, + Step::TransformAndSet(t) => { + read_response = Self::read_response(read_response,&mut body); + t.to_tokens(&mut body); } } } diff --git a/yaml_test_runner/src/main.rs b/yaml_test_runner/src/main.rs index 462e294e..29c30915 100644 --- a/yaml_test_runner/src/main.rs +++ b/yaml_test_runner/src/main.rs @@ -29,6 +29,7 @@ mod generated; pub mod client; pub mod util; +pub mod transform; fn main() -> Result<(), failure::Error> { simple_logger::init_with_level(Level::Info).unwrap(); diff --git a/yaml_test_runner/src/step/do.rs b/yaml_test_runner/src/step/do.rs index b591c4ec..e4b6d1ef 100644 --- a/yaml_test_runner/src/step/do.rs +++ b/yaml_test_runner/src/step/do.rs @@ -14,6 +14,10 @@ lazy_static! { static ref SET_REGEX: Regex = Regex::new(r#""\$(.*?)""#).unwrap(); + // replace usages of ${.*} with the captured value + static ref SET_QUOTED_DELIMITED_REGEX: Regex = + Regex::new(r#""\$\{(.*?)\}""#).unwrap(); + // replace usages of ${.*} with the captured value static ref SET_DELIMITED_REGEX: Regex = Regex::new(r#"\$\{(.*?)\}"#).unwrap(); @@ -36,7 +40,10 @@ impl ToTokens for Catch { fn to_tokens(&self, tokens: &mut Tokens) { fn http_status_code(status_code: u16, tokens: &mut Tokens) { tokens.append(quote! { - assert_eq!(response.status_code().as_u16(), #status_code); + assert_eq!( + response.status_code().as_u16(), + #status_code, + "Expected status code {} but was {}", #status_code, response.status_code().as_u16()); }); } @@ -49,8 +56,10 @@ impl ToTokens for Catch { "conflict" => http_status_code(409, tokens), "request" => { tokens.append(quote! { - let status_code = response.status_code().as_u16(); - assert!(status_code >= 400 && status_code < 600); + let status_code = response.status_code().as_u16(); + assert!( + status_code >= 400 && status_code < 600, + "Expected status code 400-599 but was {}", response.status_code().as_u16()); }); } "unavailable" => http_status_code(503, tokens), @@ -61,12 +70,10 @@ impl ToTokens for Catch { let t = clean_regex(s); tokens.append(quote! { let catch_regex = regex::Regex::new(#t)?; - let error = json["error"].to_string(); assert!( - catch_regex.is_match(&error), - "expected json value at {}:\n\n{}\n\nto match regex:\n\n{}", - "[\"error\"]", - &error, + catch_regex.is_match(&text), + "expected text:\n\n{}\n\nto match regex:\n\n{}", + &text, #s ); }); @@ -558,6 +565,7 @@ impl ApiCall { } fn from_set_value(s: &str) -> Tokens { + // check if the entire string is a token if s.starts_with('$') { let ident = syn::Ident::from( s.trim_start_matches('$') @@ -566,6 +574,7 @@ impl ApiCall { ); quote! { #ident } } else { + // only part of the string is a token, so substitute let token = syn::Ident::from( SET_DELIMITED_REGEX .captures(s) @@ -592,8 +601,13 @@ impl ApiCall { }; // Enum variants containing no URL parts where there is only a single API URL, - // are not required to be passed in the API - if parts.is_empty() { + // are not required to be passed in the API. + // + // Also, short circuit for tests where the only parts specified are null + // e.g. security API test. It seems these should simply omit the value though... + if parts.is_empty() || parts + .iter() + .all(|(_, v)| v.is_null()) { let param_counts = endpoint .url .paths @@ -601,6 +615,7 @@ impl ApiCall { .map(|p| p.path.params().len()) .collect::>(); + // check there's actually a None value if !param_counts.contains(&0) { return Err(failure::err_msg(format!( "No path for '{}' API with no URL parts", @@ -712,6 +727,8 @@ impl ApiCall { let set_value = Self::from_set_value(s); Ok(quote! { #set_value.as_str().unwrap() }) } else { + + Ok(quote! { #s }) } } @@ -778,6 +795,16 @@ impl ApiCall { } } + /// Replaces a "set" step value with a variable + fn replace_set_quoted_delimited>(s: S) -> String { + SET_QUOTED_DELIMITED_REGEX.replace_all(s.as_ref(), "$1").into_owned() + } + + /// Replaces a "set" step value with a variable + fn replace_set_delimited>(s: S) -> String { + SET_DELIMITED_REGEX.replace_all(s.as_ref(), "$1").into_owned() + } + /// Replaces a "set" step value with a variable fn replace_set>(s: S) -> String { SET_REGEX.replace_all(s.as_ref(), "$1").into_owned() @@ -800,8 +827,10 @@ impl ApiCall { match v { Yaml::String(s) => { let json = { - let s = Self::replace_set(s); - Self::replace_i64(s) + let mut json = Self::replace_set_quoted_delimited(s); + json = Self::replace_set_delimited(json); + json = Self::replace_set(json); + Self::replace_i64(json) }; if endpoint.supports_nd_body() { // a newline delimited API body may be expressed @@ -846,10 +875,14 @@ impl ApiCall { .map(|value| { let mut json = serde_json::to_string(&value).unwrap(); if value.is_string() { + json = Self::replace_set_quoted_delimited(json); + json = Self::replace_set_delimited(json); json = Self::replace_set(&json); let ident = syn::Ident::from(json); quote!(#ident) } else { + json = Self::replace_set_quoted_delimited(json); + json = Self::replace_set_delimited(json); json = Self::replace_set(json); json = Self::replace_i64(json); let ident = syn::Ident::from(json); @@ -861,6 +894,8 @@ impl ApiCall { } else { let value: serde_json::Value = serde_yaml::from_str(&s).unwrap(); let mut json = serde_json::to_string_pretty(&value).unwrap(); + json = Self::replace_set_quoted_delimited(json); + json = Self::replace_set_delimited(json); json = Self::replace_set(json); json = Self::replace_i64(json); let ident = syn::Ident::from(json); diff --git a/yaml_test_runner/src/step/mod.rs b/yaml_test_runner/src/step/mod.rs index 9cc26a08..88355951 100644 --- a/yaml_test_runner/src/step/mod.rs +++ b/yaml_test_runner/src/step/mod.rs @@ -7,6 +7,7 @@ mod length; mod r#match; mod set; mod skip; +mod transform_and_set; mod is_true; mod is_false; mod comparison; @@ -18,6 +19,7 @@ pub use skip::*; pub use is_true::*; pub use is_false::*; pub use comparison::{Comparison, OPERATORS}; +pub use transform_and_set::*; pub fn parse_steps(api: &Api, steps: &[Yaml]) -> Result, failure::Error> { let mut parsed_steps: Vec = Vec::new(); @@ -48,7 +50,10 @@ pub fn parse_steps(api: &Api, steps: &[Yaml]) -> Result, failure::Erro let s = Set::try_parse(value)?; parsed_steps.push(s.into()); } - "transform_and_set" => {} + "transform_and_set" => { + let t = TransformAndSet::try_parse(value)?; + parsed_steps.push(t.into()); + } "match" => { let m = Match::try_parse(value)?; parsed_steps.push(m.into()); @@ -177,9 +182,11 @@ pub enum Step { IsTrue(IsTrue), IsFalse(IsFalse), Comparison(Comparison), + TransformAndSet(TransformAndSet), } impl Step { + /// Gets a Do step pub fn r#do(&self) -> Option<&Do> { match self { Step::Do(d) => Some(d), @@ -214,6 +221,7 @@ pub fn ok_or_accumulate( // trim the enclosing forward slashes and // 1. replace escaped forward slashes (not needed after trimming forward slashes) // 2. replace escaped colons and hashes (not supported by regex crate) +// TODO: create wrapper struct pub fn clean_regex>(s: S) -> String { s.as_ref() .trim() @@ -222,4 +230,5 @@ pub fn clean_regex>(s: S) -> String { .replace("\\:", ":") .replace("\\#", "#") .replace("\\%", "%") + .replace("\\'", "'") } diff --git a/yaml_test_runner/src/step/transform_and_set.rs b/yaml_test_runner/src/step/transform_and_set.rs new file mode 100644 index 00000000..b7bfce19 --- /dev/null +++ b/yaml_test_runner/src/step/transform_and_set.rs @@ -0,0 +1,110 @@ +use quote::{ToTokens, Tokens}; + +use super::Step; +use crate::step::Expr; +use inflector::Inflector; +use yaml_rust::Yaml; + +pub struct Transformation { + raw: String, + function: String, + exprs: Vec, +} + +impl Transformation { + pub fn transform(&self) -> syn::Ident { + let mut transform = String::new(); + transform.push_str(&self.function); + transform.push('('); + for expr in &self.exprs { + transform.push_str("json"); + transform.push_str(expr.expression().as_str()); + transform.push_str(".as_str().unwrap()"); + transform.push(','); + } + transform.push(')'); + syn::Ident::from(transform.as_str()) + } +} + +impl From<&str> for Transformation { + fn from(t: &str) -> Self { + let raw = t.to_string(); + let mut function = None; + let mut exprs = Vec::new(); + let mut value = String::new(); + let mut chars = t.chars(); + while let Some(ch) = chars.next() { + match ch { + '#' => { + continue; + } + '(' => { + let name = format!("transform::{}", value.as_str().to_snake_case()); + function = Some(name); + value = String::new(); + }, + ',' | ')' => { + let expr = value.trim(); + exprs.push(Expr::new(expr)); + value = String::new(); + } + _ => { + value.push(ch); + } + } + } + + Self { + raw, + function: function.unwrap(), + exprs, + } + } +} + +pub struct TransformAndSet { + ident: syn::Ident, + transformation: Transformation, +} + +impl From for Step { + fn from(transform_and_set: TransformAndSet) -> Self { + Step::TransformAndSet(transform_and_set) + } +} + +impl TransformAndSet { + pub fn try_parse(yaml: &Yaml) -> Result { + let hash = yaml + .as_hash() + .ok_or_else(|| failure::err_msg(format!("expected hash but found {:?}", yaml)))?; + + let (k, v) = hash.iter().next().unwrap(); + let ident = k + .as_str() + .ok_or_else(|| failure::err_msg(format!("expected string key but found {:?}", k)))?; + + let transformation = v + .as_str() + .ok_or_else(|| failure::err_msg(format!("expected string value but found {:?}", v)))?; + + Ok(TransformAndSet { + ident: syn::Ident::from(ident), + transformation: transformation.into(), + }) + } +} + +impl ToTokens for TransformAndSet { + fn to_tokens(&self, tokens: &mut Tokens) { + let ident = &self.ident; + let transform = &self.transformation.transform(); + tokens.append(quote! { + let #ident = { + let transform = #transform; + json!(transform) + }; + }); + } +} diff --git a/yaml_test_runner/src/transform.rs b/yaml_test_runner/src/transform.rs new file mode 100644 index 00000000..16f7de66 --- /dev/null +++ b/yaml_test_runner/src/transform.rs @@ -0,0 +1,12 @@ +use base64::write::EncoderWriter as Base64Encoder; +use std::io::Write; + +pub fn base_64_encode_credentials(user: &str, password: &str) -> String { + let mut value = Vec::new(); + { + let mut encoder = Base64Encoder::new(&mut value, base64::STANDARD); + write!(encoder, "{}:", user).unwrap(); + write!(encoder, "{}", password).unwrap(); + }; + String::from_utf8(value).unwrap() +} \ No newline at end of file From 7a615cba9b78225442a6fed7930adc3bcdba4933 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 14 May 2020 15:53:36 +1000 Subject: [PATCH 083/127] Extract method, statuscode, text and json from response This commit extracts pertinent info from the response on which assertions can be made. --- api_generator/src/lib.rs | 3 +++ yaml_test_runner/src/generator.rs | 20 ++++---------------- yaml_test_runner/src/util.rs | 15 +++++++++++++++ 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/api_generator/src/lib.rs b/api_generator/src/lib.rs index f2634bda..7efdd0d9 100644 --- a/api_generator/src/lib.rs +++ b/api_generator/src/lib.rs @@ -1,3 +1,6 @@ +// needed for quote! +#![recursion_limit="256"] + #[macro_use] extern crate lazy_static; diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 8362f91f..8c2b0f18 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -122,7 +122,8 @@ impl<'a> YamlTests<'a> { use elasticsearch::*; use elasticsearch::http::{ headers::{HeaderName, HeaderValue}, - request::JsonBody + request::JsonBody, + Method, }; use elasticsearch::params::*; #(#directives)* @@ -143,17 +144,7 @@ impl<'a> YamlTests<'a> { pub fn read_response(read_response: bool, tokens: &mut Tokens) -> bool { if !read_response { tokens.append(quote! { - let (text, json) = { - let is_json = response.content_type().starts_with("application/json"); - let text = response.text().await?; - let json = if is_json { - serde_json::from_slice::(text.as_ref())? - } else { - Value::Null - }; - - (text, json) - }; + let (method, status_code, text, json) = util::read_response(response).await?; }); } @@ -168,11 +159,8 @@ impl<'a> YamlTests<'a> { let components = test_file_path .components() .filter_map(|c| match c { - Component::Prefix(_) => None, - Component::RootDir => None, - Component::CurDir => None, - Component::ParentDir => None, Component::Normal(n) => Some(n.to_string_lossy().into_owned()), + _ => None, }) .collect::>(); diff --git a/yaml_test_runner/src/util.rs b/yaml_test_runner/src/util.rs index 01b52084..6fa93a9c 100644 --- a/yaml_test_runner/src/util.rs +++ b/yaml_test_runner/src/util.rs @@ -1,4 +1,5 @@ use serde_json::Value; +use elasticsearch::http::{Method, StatusCode, response::Response}; pub fn len_from_value(value: &Value) -> Result { match value { @@ -9,3 +10,17 @@ pub fn len_from_value(value: &Value) -> Result { v => Err(failure::err_msg(format!("Cannot get length from {:?}", v))), } } + +pub async fn read_response(response: Response) -> Result<(Method, StatusCode, String, Value), failure::Error> { + let is_json = response.content_type().starts_with("application/json"); + let method = response.method(); + let status_code = response.status_code(); + let text = response.text().await?; + let json = if is_json && !text.is_empty() { + serde_json::from_slice::(text.as_ref())? + } else { + Value::Null + }; + + Ok((method, status_code, text, json)) +} From 16fac648e5ff083081210aeb128be131c3463a80 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 14 May 2020 15:54:13 +1000 Subject: [PATCH 084/127] Skip ssl certificates test Can never pass as the CA cert is returned first, which won't match the assertion --- yaml_test_runner/skip.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/yaml_test_runner/skip.yml b/yaml_test_runner/skip.yml index 9266cb51..6ea5503e 100644 --- a/yaml_test_runner/skip.yml +++ b/yaml_test_runner/skip.yml @@ -1,3 +1,8 @@ +# Skip file of features and tests to skip. +# This is used at compilation time, when compiling tests from the YAML tests, to not generate tests that match +# on name or features defined below that should be skipped. Accordingly, changing values in this file requires +# recompiling tests in order for the changes to take effect. + # features not yet implemented features: - node_selector @@ -5,4 +10,6 @@ features: # tests to skip generating and compiling a test for. # Take the form of the generated path e.g. # generated::xpack::security::hidden_index::_13_security_tokens_read::tests::test_get_security_tokens_index_metadata -tests: [] +tests: + # this test returns the CA cert before the cert, so always fails + - generated::xpack::ssl::_10_basic::tests::test_get_ssl_certificates From 67b66b50a2f9b62e6305aac8d21e52a6d9f6c462 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 14 May 2020 15:54:39 +1000 Subject: [PATCH 085/127] Update is_true and is_false logic to use request method --- yaml_test_runner/src/step/is_false.rs | 12 ++++++------ yaml_test_runner/src/step/is_true.rs | 16 ++++++++++------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/yaml_test_runner/src/step/is_false.rs b/yaml_test_runner/src/step/is_false.rs index 08e08a69..22d17eab 100644 --- a/yaml_test_runner/src/step/is_false.rs +++ b/yaml_test_runner/src/step/is_false.rs @@ -30,18 +30,18 @@ impl ToTokens for IsFalse { fn to_tokens(&self, tokens: &mut Tokens) { if self.expr.is_empty() { tokens.append(quote! { - assert!(text.is_empty()); + assert!(text.is_empty(), "expected value to be empty but was {}", &text); }); } else { let expr = self.expr.expression(); let ident = syn::Ident::from(expr.as_str()); tokens.append(quote! { match &json#ident { - Value::Null => assert!(true, "Expected value at {} to be false but is null", #expr), - Value::Bool(b) => assert_eq!(*b, false, "Expected value at {} to be false but is {}", #expr, b), - Value::Number(n) => assert_eq!(n.as_f64().unwrap(), 0.0, "Expected value at {} to be false but is {}", #expr, n.as_f64().unwrap()), - Value::String(s) => assert!(s.is_empty(), "Expected value at {} to be false but is {}", #expr, &s), - v => assert!(false, "Expected value at {} to be false but is {:?}", #expr, &v), + Value::Null => {}, + Value::Bool(b) => assert_eq!(*b, false, "expected value at {} to be false but was {}", #expr, b), + Value::Number(n) => assert_eq!(n.as_f64().unwrap(), 0.0, "expected value at {} to be false (0) but was {}", #expr, n.as_f64().unwrap()), + Value::String(s) => assert!(s.is_empty(), "expected value at {} to be false (empty) but was {}", #expr, &s), + v => assert!(false, "expected value at {} to be false but was {:?}", #expr, &v), } }); } diff --git a/yaml_test_runner/src/step/is_true.rs b/yaml_test_runner/src/step/is_true.rs index 928256bb..dd0edfbb 100644 --- a/yaml_test_runner/src/step/is_true.rs +++ b/yaml_test_runner/src/step/is_true.rs @@ -29,19 +29,23 @@ impl IsTrue { impl ToTokens for IsTrue { fn to_tokens(&self, tokens: &mut Tokens) { if self.expr.is_empty() { + // for a HEAD request, the body is expected to be empty, so check the status code instead. tokens.append(quote! { - assert!(!text.is_empty()); + match method { + Method::Head => assert!(status_code.is_success(), "expected successful response for HEAD request but was {}", status_code.as_u16()), + _ => assert!(!text.is_empty(), "expected value to be true (not empty) but was {}", &text), + } }); } else { let expr = self.expr.expression(); let ident = syn::Ident::from(expr.as_str()); tokens.append(quote! { match &json#ident { - Value::Null => assert!(false, "Expected value at {} to be true but is null", #expr), - Value::Bool(b) => assert!(*b, "Expected value at {} to be true but is {}", #expr, b), - Value::Number(n) => assert_ne!(n.as_f64().unwrap(), 0.0, "Expected value at {} to be true but is {}", #expr, n.as_f64().unwrap()), - Value::String(s) => assert!(!s.is_empty(), "Expected value at {} to be true but is {}", #expr, &s), - v => assert!(true, "Expected value at {} to be true but is {:?}", #expr, &v), + Value::Null => assert!(false, "expected value at {} to be true (not null) but was null", #expr), + Value::Bool(b) => assert!(*b, "expected value at {} to be true but was false", #expr), + Value::Number(n) => assert_ne!(n.as_f64().unwrap(), 0.0, "expected value at {} to be true (not 0) but was {}", #expr, n.as_f64().unwrap()), + Value::String(s) => assert!(!s.is_empty(), "expected value at {} to be true (not empty) but was {}", #expr, &s), + v => {}, } }); } From f049ffc6accafdd660be68bec5e90b997649bd3a Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 14 May 2020 21:56:03 +1000 Subject: [PATCH 086/127] match on all json values --- yaml_test_runner/src/step/match.rs | 93 ++++++++++++++++++++++++++++-- 1 file changed, 89 insertions(+), 4 deletions(-) diff --git a/yaml_test_runner/src/step/match.rs b/yaml_test_runner/src/step/match.rs index 5845c4e7..99df52e2 100644 --- a/yaml_test_runner/src/step/match.rs +++ b/yaml_test_runner/src/step/match.rs @@ -1,7 +1,7 @@ use super::Step; use crate::step::{clean_regex, Expr}; use quote::{ToTokens, Tokens}; -use yaml_rust::Yaml; +use yaml_rust::{Yaml, YamlEmitter}; pub struct Match { pub expr: Expr, @@ -91,7 +91,7 @@ impl ToTokens for Match { } Yaml::Integer(i) => { if self.expr.is_body() { - panic!("match on $body with integer"); + panic!("match on $body with i64"); } else { let ident = syn::Ident::from(expr.as_str()); tokens.append(quote! { @@ -106,8 +106,93 @@ impl ToTokens for Match { }); } } - // TODO: handle hashes, etc. - _ => {} + Yaml::Real(r) => { + let f = r.parse::().unwrap(); + if self.expr.is_body() { + panic!("match on $body with f64"); + } else { + let ident = syn::Ident::from(expr.as_str()); + tokens.append(quote! { + assert_eq!( + json#ident.as_f64().unwrap(), + #f, + "expected value at {} to be {} but was {}", + #expr, + #f, + json#ident.as_f64().unwrap() + ); + }); + } + } + Yaml::Null => { + if self.expr.is_body() { + tokens.append(quote! { + assert!(text.is_empty(), "expected response to be null (empty) but was {}", &text); + }); + } else { + let ident = syn::Ident::from(expr.as_str()); + tokens.append(quote! { + assert!( + json#ident.is_null(), + "expected value at {} to be null but was {}", + #expr, + json#ident.to_string(), + ); + }); + } + } + Yaml::Boolean(b) => { + if self.expr.is_body() { + panic!("match on $body with bool"); + } else { + let ident = syn::Ident::from(expr.as_str()); + tokens.append(quote! { + assert_eq!( + json#ident.as_bool().unwrap(), + #b, + "expected value at {} to be {} but was {}", + #expr, + #b, + json#ident.as_bool().unwrap(), + ); + }); + } + } + yaml if yaml.is_array() || yaml.as_hash().is_some() => { + let mut s = String::new(); + { + let mut emitter = YamlEmitter::new(&mut s); + emitter.dump(yaml).unwrap(); + } + + let value: serde_json::Value = serde_yaml::from_str(&s).unwrap(); + let json = syn::Ident::from(value.to_string()); + + if self.expr.is_body() { + tokens.append(quote! { + assert_eq!( + json, + json!(#json), + "expected response to be {} but was {}", + json!(#json).to_string(), + json.to_string()); + }); + } else { + let ident = syn::Ident::from(expr.as_str()); + tokens.append(quote! { + assert_eq!( + json#ident, + json!(#json), + "expected value at {} to be {} but was {}", + #expr, + json!(#json).to_string(), + json#ident.to_string()); + }); + } + } + yaml => { + panic!("Bad yaml value {:?}", &yaml); + } } } } From 5a4cd40cce4ca840510030573f58379de289a111 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 14 May 2020 21:56:13 +1000 Subject: [PATCH 087/127] extract regex to module --- yaml_test_runner/src/main.rs | 1 + yaml_test_runner/src/regex.rs | 43 +++++++++++++++++++ yaml_test_runner/src/step/do.rs | 73 ++++++++------------------------- 3 files changed, 60 insertions(+), 57 deletions(-) create mode 100644 yaml_test_runner/src/regex.rs diff --git a/yaml_test_runner/src/main.rs b/yaml_test_runner/src/main.rs index 29c30915..03e206cd 100644 --- a/yaml_test_runner/src/main.rs +++ b/yaml_test_runner/src/main.rs @@ -30,6 +30,7 @@ mod generated; pub mod client; pub mod util; pub mod transform; +pub mod regex; fn main() -> Result<(), failure::Error> { simple_logger::init_with_level(Level::Info).unwrap(); diff --git a/yaml_test_runner/src/regex.rs b/yaml_test_runner/src/regex.rs new file mode 100644 index 00000000..83f93849 --- /dev/null +++ b/yaml_test_runner/src/regex.rs @@ -0,0 +1,43 @@ +use lazy_static; +use regex::Regex; + +lazy_static! { + // replace usages of "$.*" with the captured value + pub static ref SET_REGEX: Regex = + Regex::new(r#""\$(.*?)""#).unwrap(); + + // replace usages of ${.*} with the captured value + pub static ref SET_QUOTED_DELIMITED_REGEX: Regex = + Regex::new(r#""\$\{(.*?)\}""#).unwrap(); + + // replace usages of ${.*} with the captured value + pub static ref SET_DELIMITED_REGEX: Regex = + Regex::new(r#"\$\{(.*?)\}"#).unwrap(); + + // include i64 suffix on whole numbers + pub static ref INT_REGEX: Regex = + regex::Regex::new(r"(:\s?)(\d+?)([,\s?|\s*?}])").unwrap(); +} + +/// Replaces a "set" step value with a variable +pub fn replace_set_quoted_delimited>(s: S) -> String { + SET_QUOTED_DELIMITED_REGEX.replace_all(s.as_ref(), "$1").into_owned() +} + +/// Replaces a "set" step value with a variable +pub fn replace_set_delimited>(s: S) -> String { + SET_DELIMITED_REGEX.replace_all(s.as_ref(), "$1").into_owned() +} + +/// Replaces a "set" step value with a variable +pub fn replace_set>(s: S) -> String { + SET_REGEX.replace_all(s.as_ref(), "$1").into_owned() +} + +/// Replaces all integers in a string to suffix with i64, to ensure that numbers +/// larger than i32 will be handled correctly when passed to json! macro +pub fn replace_i64>(s: S) -> String { + INT_REGEX + .replace_all(s.as_ref(), "${1}${2}i64${3}") + .into_owned() +} \ No newline at end of file diff --git a/yaml_test_runner/src/step/do.rs b/yaml_test_runner/src/step/do.rs index e4b6d1ef..92f5434b 100644 --- a/yaml_test_runner/src/step/do.rs +++ b/yaml_test_runner/src/step/do.rs @@ -5,27 +5,9 @@ use super::{ok_or_accumulate, Step}; use crate::step::clean_regex; use api_generator::generator::{Api, ApiEndpoint, TypeKind}; use itertools::Itertools; -use regex::Regex; use std::collections::BTreeMap; use yaml_rust::{Yaml, YamlEmitter}; - -lazy_static! { - // replace usages of "$.*" with the captured value - static ref SET_REGEX: Regex = - Regex::new(r#""\$(.*?)""#).unwrap(); - - // replace usages of ${.*} with the captured value - static ref SET_QUOTED_DELIMITED_REGEX: Regex = - Regex::new(r#""\$\{(.*?)\}""#).unwrap(); - - // replace usages of ${.*} with the captured value - static ref SET_DELIMITED_REGEX: Regex = - Regex::new(r#"\$\{(.*?)\}"#).unwrap(); - - // include i64 suffix on whole numbers - static ref INT_REGEX: Regex = - regex::Regex::new(r"(:\s?)(\d+?)([,\s?|\s*?}])").unwrap(); -} +use crate::regex::*; /// A catch expression on a do step pub struct Catch(String); @@ -795,29 +777,6 @@ impl ApiCall { } } - /// Replaces a "set" step value with a variable - fn replace_set_quoted_delimited>(s: S) -> String { - SET_QUOTED_DELIMITED_REGEX.replace_all(s.as_ref(), "$1").into_owned() - } - - /// Replaces a "set" step value with a variable - fn replace_set_delimited>(s: S) -> String { - SET_DELIMITED_REGEX.replace_all(s.as_ref(), "$1").into_owned() - } - - /// Replaces a "set" step value with a variable - fn replace_set>(s: S) -> String { - SET_REGEX.replace_all(s.as_ref(), "$1").into_owned() - } - - /// Replaces all integers in a string to suffix with i64, to ensure that numbers - /// larger than i32 will be handled correctly when passed to json! macro - fn replace_i64>(s: S) -> String { - INT_REGEX - .replace_all(s.as_ref(), "${1}${2}i64${3}") - .into_owned() - } - /// Creates the body function call from a YAML value. /// /// When reading a body from the YAML test, it'll be converted to a Yaml variant, @@ -827,10 +786,10 @@ impl ApiCall { match v { Yaml::String(s) => { let json = { - let mut json = Self::replace_set_quoted_delimited(s); - json = Self::replace_set_delimited(json); - json = Self::replace_set(json); - Self::replace_i64(json) + let mut json = replace_set_quoted_delimited(s); + json = replace_set_delimited(json); + json = replace_set(json); + replace_i64(json) }; if endpoint.supports_nd_body() { // a newline delimited API body may be expressed @@ -875,16 +834,16 @@ impl ApiCall { .map(|value| { let mut json = serde_json::to_string(&value).unwrap(); if value.is_string() { - json = Self::replace_set_quoted_delimited(json); - json = Self::replace_set_delimited(json); - json = Self::replace_set(&json); + json = replace_set_quoted_delimited(json); + json = replace_set_delimited(json); + json = replace_set(&json); let ident = syn::Ident::from(json); quote!(#ident) } else { - json = Self::replace_set_quoted_delimited(json); - json = Self::replace_set_delimited(json); - json = Self::replace_set(json); - json = Self::replace_i64(json); + json = replace_set_quoted_delimited(json); + json = replace_set_delimited(json); + json = replace_set(json); + json = replace_i64(json); let ident = syn::Ident::from(json); quote!(JsonBody::from(json!(#ident))) } @@ -894,10 +853,10 @@ impl ApiCall { } else { let value: serde_json::Value = serde_yaml::from_str(&s).unwrap(); let mut json = serde_json::to_string_pretty(&value).unwrap(); - json = Self::replace_set_quoted_delimited(json); - json = Self::replace_set_delimited(json); - json = Self::replace_set(json); - json = Self::replace_i64(json); + json = replace_set_quoted_delimited(json); + json = replace_set_delimited(json); + json = replace_set(json); + json = replace_i64(json); let ident = syn::Ident::from(json); Some(quote!(.body(json!{#ident}))) From b6dbd7e092a90ecb57e447692b7d56349cb56049 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 14 May 2020 22:17:03 +1000 Subject: [PATCH 088/127] Don't emit body() call for null --- yaml_test_runner/src/step/do.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/yaml_test_runner/src/step/do.rs b/yaml_test_runner/src/step/do.rs index 92f5434b..49e16996 100644 --- a/yaml_test_runner/src/step/do.rs +++ b/yaml_test_runner/src/step/do.rs @@ -784,6 +784,7 @@ impl ApiCall { /// back to JSON fn generate_body(endpoint: &ApiEndpoint, v: &Yaml) -> Option { match v { + Yaml::Null => None, Yaml::String(s) => { let json = { let mut json = replace_set_quoted_delimited(s); From e664cda6582840a92c520107f742c751e7aca165 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 14 May 2020 22:32:07 +1000 Subject: [PATCH 089/127] write i64 suffix only for values larger than i32::max_value() --- yaml_test_runner/src/regex.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/yaml_test_runner/src/regex.rs b/yaml_test_runner/src/regex.rs index 83f93849..383a846e 100644 --- a/yaml_test_runner/src/regex.rs +++ b/yaml_test_runner/src/regex.rs @@ -1,5 +1,5 @@ use lazy_static; -use regex::Regex; +use regex::{Regex, Captures}; lazy_static! { // replace usages of "$.*" with the captured value @@ -38,6 +38,11 @@ pub fn replace_set>(s: S) -> String { /// larger than i32 will be handled correctly when passed to json! macro pub fn replace_i64>(s: S) -> String { INT_REGEX - .replace_all(s.as_ref(), "${1}${2}i64${3}") + .replace_all(s.as_ref(), |c: &Captures| { + match &c[2].parse::() { + Ok(i) if *i > i32::max_value() as i64 => format!("{}{}i64{}", &c[1], &c[2], &c[3]), + _ => format!("{}", &c[0]) + } + }) .into_owned() } \ No newline at end of file From 5d1e236264473647a0384fd76a369ecd962c53f2 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 14 May 2020 23:02:46 +1000 Subject: [PATCH 090/127] handle assertion of i64 value in test to f64 value in response --- yaml_test_runner/src/step/match.rs | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/yaml_test_runner/src/step/match.rs b/yaml_test_runner/src/step/match.rs index 99df52e2..c6e2c19e 100644 --- a/yaml_test_runner/src/step/match.rs +++ b/yaml_test_runner/src/step/match.rs @@ -94,15 +94,27 @@ impl ToTokens for Match { panic!("match on $body with i64"); } else { let ident = syn::Ident::from(expr.as_str()); + // handle the case where the YAML test asserts a match against an integer value + // but a floating point value is returned from Elasticsearch tokens.append(quote! { - assert_eq!( - json#ident.as_i64().unwrap(), - #i, - "expected value at {} to be {} but was {}", - #expr, - #i, - json#ident.as_i64().unwrap() - ); + match json#ident.as_i64() { + Some(i) => assert_eq!( + i, + #i, + "expected value at {} to be {} but was {}", + #expr, + #i, + i + ), + None => assert_eq!( + json#ident.as_f64().unwrap(), + #i as f64, + "expected value at {} to be {} but was {}", + #expr, + #i as f64, + json#ident.as_f64().unwrap() + ) + } }); } } From d9a6d2feadd339ed3fd6460f196ea397bc31e45f Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 14 May 2020 23:03:10 +1000 Subject: [PATCH 091/127] Add consistent failing test and unsupported features --- yaml_test_runner/skip.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/yaml_test_runner/skip.yml b/yaml_test_runner/skip.yml index 6ea5503e..9c18e6f8 100644 --- a/yaml_test_runner/skip.yml +++ b/yaml_test_runner/skip.yml @@ -6,6 +6,9 @@ # features not yet implemented features: - node_selector + - contains + - stash_path_replace + - embedded_stash_key # tests to skip generating and compiling a test for. # Take the form of the generated path e.g. @@ -13,3 +16,7 @@ features: tests: # this test returns the CA cert before the cert, so always fails - generated::xpack::ssl::_10_basic::tests::test_get_ssl_certificates + + # this test always returns "exponential_avg_checkpoint_duration_ms": 0.0 . seems like it might be missing data in + # the setup, fires quicker than any documents are processed, or the delay of 1m is too high? + - generated::xpack::transform::_transforms_stats_continuous::tests::test_get_continuous_transform_stats \ No newline at end of file From 90bdfef5056208d75790f5eb152f88fff741f1ce Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Fri, 15 May 2020 10:13:37 +1000 Subject: [PATCH 092/127] Handle warnings and ignore --- yaml_test_runner/src/step/do.rs | 103 ++++++++++++++++++++++---------- 1 file changed, 70 insertions(+), 33 deletions(-) diff --git a/yaml_test_runner/src/step/do.rs b/yaml_test_runner/src/step/do.rs index 49e16996..1c640314 100644 --- a/yaml_test_runner/src/step/do.rs +++ b/yaml_test_runner/src/step/do.rs @@ -2,12 +2,12 @@ use inflector::Inflector; use quote::{ToTokens, Tokens}; use super::{ok_or_accumulate, Step}; +use crate::regex::*; use crate::step::clean_regex; use api_generator::generator::{Api, ApiEndpoint, TypeKind}; use itertools::Itertools; use std::collections::BTreeMap; use yaml_rust::{Yaml, YamlEmitter}; -use crate::regex::*; /// A catch expression on a do step pub struct Catch(String); @@ -25,7 +25,7 @@ impl ToTokens for Catch { assert_eq!( response.status_code().as_u16(), #status_code, - "Expected status code {} but was {}", #status_code, response.status_code().as_u16()); + "expected response status code to be {} but was {}", #status_code, response.status_code().as_u16()); }); } @@ -67,6 +67,7 @@ impl ToTokens for Catch { pub struct Do { api_call: ApiCall, warnings: Vec, + allowed_warnings: Vec, catch: Option, } @@ -84,23 +85,54 @@ impl From for Step { impl Do { pub fn to_tokens(&self, mut read_response: bool, tokens: &mut Tokens) -> bool { - // TODO: Add in warnings self.api_call.to_tokens(tokens); + // only assert that there are no warnings if expected warnings is empty and not allowing warnings + if !self.warnings.is_empty() { + tokens.append(quote! { + let warnings: Vec<&str> = response.warning_headers().collect(); + }); + for warning in &self.warnings { + tokens.append(quote! { + assert!( + warnings.iter().any(|w| w.contains(#warning)), + "expected warnings to contain '{}' but did not", + #warning); + }); + } + } else if !self.allowed_warnings.is_empty() { + tokens.append(quote! { + let warnings: Vec<&str> = response.warning_headers().collect(); + assert!(warnings.is_empty(), "expected warnings to be empty but found {:?}", &warnings); + }); + } + if let Some(c) = &self.catch { if !read_response && c.needs_response_body() { read_response = true; tokens.append(quote! { - let is_json = response.content_type().starts_with("application/json"); - let text = response.text().await?; - let json : Value = if is_json { - serde_json::from_slice(text.as_ref())? - } else { - Value::Null - }; + let (method, status_code, text, json) = util::read_response(response).await?; }); } c.to_tokens(tokens); + } else { + match &self.api_call.ignore { + Some(i) => { + tokens.append(quote! { + assert!( + response.status_code().is_success() || response.status_code().as_u16() == #i, + "expected response to be successful or {} but was {}", + #i, + response.status_code().as_u16()); + }); + } + None => tokens.append(quote! { + assert!( + response.status_code().is_success(), + "expected response to be successful but was {}", + response.status_code().as_u16()); + }), + } } read_response @@ -114,8 +146,15 @@ impl Do { let mut call: Option<(&str, &Yaml)> = None; let mut headers = BTreeMap::new(); let mut warnings: Vec = Vec::new(); + let mut allowed_warnings: Vec = Vec::new(); let mut catch = None; + fn to_string_vec(v: &Yaml) -> Vec { + v.as_vec() + .map(|a| a.iter().map(|y| y.as_str().unwrap().to_string()).collect()) + .unwrap() + } + let results: Vec> = hash .iter() .map(|(k, v)| { @@ -148,10 +187,11 @@ impl Do { Ok(()) } "warnings" => { - warnings = v - .as_vec() - .map(|a| a.iter().map(|y| y.as_str().unwrap().to_string()).collect()) - .unwrap(); + warnings = to_string_vec(v); + Ok(()) + } + "allowed_warnings" => { + allowed_warnings = to_string_vec(v); Ok(()) } api_call => { @@ -174,6 +214,7 @@ impl Do { api_call, catch, warnings, + allowed_warnings, }) } @@ -190,7 +231,7 @@ pub struct ApiCall { params: Option, headers: BTreeMap, body: Option, - ignore: Option, + ignore: Option, } impl ToTokens for ApiCall { @@ -250,24 +291,24 @@ impl ApiCall { let mut parts: Vec<(&str, &Yaml)> = vec![]; let mut params: Vec<(&str, &Yaml)> = vec![]; let mut body: Option = None; - let mut ignore: Option = None; + let mut ignore: Option = None; // work out what's a URL part and what's a param in the supplied // arguments for the API call for (k, v) in hash.iter() { - let key = k.as_str().unwrap(); - if endpoint.params.contains_key(key) || api.common_params.contains_key(key) { - params.push((key, v)); - } else if key == "body" { - body = Self::generate_body(endpoint, v); - } else if key == "ignore" { - ignore = match v.as_i64() { - Some(i) => Some(i), - // handle ignore as an array of i64 - None => v.as_vec().unwrap()[0].as_i64(), + match k.as_str().unwrap() { + "body" => body = Self::generate_body(endpoint, v), + "ignore" => { + ignore = match v.as_i64() { + Some(i) => Some(i as u16), + // handle ignore as an array of i64 + None => Some(v.as_vec().unwrap()[0].as_i64().unwrap() as u16), + } } - } else { - parts.push((key, v)); + key if endpoint.params.contains_key(key) || api.common_params.contains_key(key) => { + params.push((key, v)) + } + key => parts.push((key, v)), } } @@ -587,9 +628,7 @@ impl ApiCall { // // Also, short circuit for tests where the only parts specified are null // e.g. security API test. It seems these should simply omit the value though... - if parts.is_empty() || parts - .iter() - .all(|(_, v)| v.is_null()) { + if parts.is_empty() || parts.iter().all(|(_, v)| v.is_null()) { let param_counts = endpoint .url .paths @@ -709,8 +748,6 @@ impl ApiCall { let set_value = Self::from_set_value(s); Ok(quote! { #set_value.as_str().unwrap() }) } else { - - Ok(quote! { #s }) } } From 62dd0265c89aa8e707ef6b372d1d0d4242e2d26b Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Fri, 15 May 2020 12:38:31 +1000 Subject: [PATCH 093/127] code format --- .../generator/code_gen/namespace_clients.rs | 2 +- api_generator/src/generator/code_gen/root.rs | 2 +- .../src/generator/code_gen/url/url_builder.rs | 42 +++++++------------ api_generator/src/generator/mod.rs | 16 +++---- api_generator/src/lib.rs | 2 +- elasticsearch/src/http/response.rs | 2 +- yaml_test_runner/src/client.rs | 8 +++- yaml_test_runner/src/main.rs | 4 +- yaml_test_runner/src/regex.rs | 20 +++++---- yaml_test_runner/src/step/comparison.rs | 20 +++++---- yaml_test_runner/src/step/is_false.rs | 10 ++--- yaml_test_runner/src/step/is_true.rs | 10 ++--- yaml_test_runner/src/step/match.rs | 5 ++- yaml_test_runner/src/step/mod.rs | 20 +++++---- .../src/step/transform_and_set.rs | 2 +- yaml_test_runner/src/transform.rs | 2 +- yaml_test_runner/src/util.rs | 6 ++- 17 files changed, 86 insertions(+), 87 deletions(-) diff --git a/api_generator/src/generator/code_gen/namespace_clients.rs b/api_generator/src/generator/code_gen/namespace_clients.rs index dfa285b9..25e66335 100644 --- a/api_generator/src/generator/code_gen/namespace_clients.rs +++ b/api_generator/src/generator/code_gen/namespace_clients.rs @@ -16,9 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -use crate::generator::*; use crate::generator::code_gen::request::request_builder::RequestBuilder; use crate::generator::code_gen::*; +use crate::generator::*; use inflector::Inflector; use quote::Tokens; use std::path::PathBuf; diff --git a/api_generator/src/generator/code_gen/root.rs b/api_generator/src/generator/code_gen/root.rs index 59279fbb..bcb9d559 100644 --- a/api_generator/src/generator/code_gen/root.rs +++ b/api_generator/src/generator/code_gen/root.rs @@ -16,9 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -use crate::generator::*; use crate::generator::code_gen::request::request_builder::RequestBuilder; use crate::generator::code_gen::*; +use crate::generator::*; use inflector::Inflector; use quote::Tokens; use std::path::PathBuf; diff --git a/api_generator/src/generator/code_gen/url/url_builder.rs b/api_generator/src/generator/code_gen/url/url_builder.rs index 614c868d..3343e7cc 100644 --- a/api_generator/src/generator/code_gen/url/url_builder.rs +++ b/api_generator/src/generator/code_gen/url/url_builder.rs @@ -170,8 +170,6 @@ impl<'a> UrlBuilder<'a> { /// Build the AST for an allocated url from the path literals and params. fn build_owned(self) -> syn::Block { - - // collection of let {name}_str = [self.]{name}.[join(",")|to_string()]; let let_params_exprs = Self::let_parameters_exprs(&self.path, &self.parts); @@ -181,8 +179,8 @@ impl<'a> UrlBuilder<'a> { let len_expr = { let lit_len_expr = Self::literal_length_expr(&self.path); let mut params_len_exprs = Self::parameter_length_exprs(&self.path); - let mut len_exprs = vec![lit_len_expr]; - len_exprs.append(&mut params_len_exprs); + let mut len_exprs = vec![lit_len_expr]; + len_exprs.append(&mut params_len_exprs); Self::summed_length_expr(len_exprs) }; let let_stmt = Self::let_p_stmt(url_ident.clone(), len_expr); @@ -228,37 +226,30 @@ impl<'a> UrlBuilder<'a> { } /// Creates the AST for a let expression to percent encode path parts - fn let_encoded_exprs( - url: &[PathPart<'a>], - parts: &BTreeMap, - ) -> Vec { + fn let_encoded_exprs(url: &[PathPart<'a>], parts: &BTreeMap) -> Vec { url.iter() .filter_map(|p| match *p { PathPart::Param(p) => { let name = valid_name(p); let path_expr = match &parts[p].ty { TypeKind::String => path_none(name).into_expr(), - _ => path_none(format!("{}_str", name).as_str()).into_expr() + _ => path_none(format!("{}_str", name).as_str()).into_expr(), }; let encoded_ident = ident(format!("encoded_{}", name)); let percent_encode_call: syn::Expr = syn::ExprKind::Call( Box::new(path_none("percent_encode").into_expr()), vec![ - syn::ExprKind::MethodCall( - ident("as_bytes"), - vec![], - vec![path_expr] - ).into(), - path_none("PARTS_ENCODED").into_expr() + syn::ExprKind::MethodCall(ident("as_bytes"), vec![], vec![path_expr]) + .into(), + path_none("PARTS_ENCODED").into_expr(), ], - ).into(); + ) + .into(); - let into_call: syn::Expr = syn::ExprKind::MethodCall( - ident("into"), - vec![], - vec![percent_encode_call] - ).into(); + let into_call: syn::Expr = + syn::ExprKind::MethodCall(ident("into"), vec![], vec![percent_encode_call]) + .into(); Some(syn::Stmt::Local(Box::new(syn::Local { pat: Box::new(syn::Pat::Ident( @@ -346,9 +337,7 @@ impl<'a> UrlBuilder<'a> { } /// Get an expression to find the number of chars in each parameter part for the url. - fn parameter_length_exprs( - url: &[PathPart<'a>], - ) -> Vec { + fn parameter_length_exprs(url: &[PathPart<'a>]) -> Vec { url.iter() .filter_map(|p| match *p { PathPart::Param(p) => { @@ -413,10 +402,7 @@ impl<'a> UrlBuilder<'a> { } /// Get a list of statements that append each part to a `String` in order. - fn push_str_stmts( - url_ident: syn::Ident, - url: &[PathPart<'a>] - ) -> Vec { + fn push_str_stmts(url_ident: syn::Ident, url: &[PathPart<'a>]) -> Vec { url.iter() .map(|p| match *p { PathPart::Literal(p) => { diff --git a/api_generator/src/generator/mod.rs b/api_generator/src/generator/mod.rs index 64ef167d..c63cf2ad 100644 --- a/api_generator/src/generator/mod.rs +++ b/api_generator/src/generator/mod.rs @@ -147,9 +147,9 @@ impl<'de> Deserialize<'de> for TypeKind { D: Deserializer<'de>, { let value = String::deserialize(deserializer)?; - Ok(TypeKind::from(value.as_str())) - } + Ok(TypeKind::from(value.as_str())) } +} impl From<&str> for TypeKind { fn from(s: &str) -> Self { @@ -173,11 +173,11 @@ impl From<&str> for TypeKind { } else { let union = Box::new((TypeKind::from(values[0]), TypeKind::from(values[1]))); TypeKind::Union(union) + } + } } } } - } -} impl Default for TypeKind { fn default() -> Self { @@ -248,8 +248,8 @@ impl DocumentationUrlString { .replace( "/master", format!("/{}.{}", VERSION.major, VERSION.minor).as_str(), - ) - .as_str(), + ) + .as_str(), ); } else if u.path().contains("/current") { u.set_path( @@ -257,8 +257,8 @@ impl DocumentationUrlString { .replace( "/current", format!("/{}.{}", VERSION.major, VERSION.minor).as_str(), - ) - .as_str(), + ) + .as_str(), ); } u.into_string() diff --git a/api_generator/src/lib.rs b/api_generator/src/lib.rs index 7efdd0d9..f55d4e67 100644 --- a/api_generator/src/lib.rs +++ b/api_generator/src/lib.rs @@ -1,5 +1,5 @@ // needed for quote! -#![recursion_limit="256"] +#![recursion_limit = "256"] #[macro_use] extern crate lazy_static; diff --git a/elasticsearch/src/http/response.rs b/elasticsearch/src/http/response.rs index 8de54c5e..e07835dc 100644 --- a/elasticsearch/src/http/response.rs +++ b/elasticsearch/src/http/response.rs @@ -104,7 +104,7 @@ impl Response { /// Gets the request URL pub fn url(&self) -> &Url { self.0.url() -} + } /// Gets the Deprecation warning response headers /// diff --git a/yaml_test_runner/src/client.rs b/yaml_test_runner/src/client.rs index 1cc8ab6b..e0e3ff24 100644 --- a/yaml_test_runner/src/client.rs +++ b/yaml_test_runner/src/client.rs @@ -191,7 +191,9 @@ async fn wait_for_yellow_status(client: &Elasticsearch) -> Result<(), Error> { assert!( cluster_health.status_code().is_success(), - "cluster health returned {}", cluster_health.status_code().as_u16()); + "cluster health returned {}", + cluster_health.status_code().as_u16() + ); Ok(()) } @@ -210,7 +212,9 @@ async fn delete_indices(client: &Elasticsearch) -> Result<(), Error> { assert!( delete_response.status_code().is_success(), - "deleting indices returned {}", delete_response.status_code().as_u16()); + "deleting indices returned {}", + delete_response.status_code().as_u16() + ); Ok(()) } diff --git a/yaml_test_runner/src/main.rs b/yaml_test_runner/src/main.rs index 03e206cd..247fdabd 100644 --- a/yaml_test_runner/src/main.rs +++ b/yaml_test_runner/src/main.rs @@ -28,9 +28,9 @@ pub mod step; mod generated; pub mod client; -pub mod util; -pub mod transform; pub mod regex; +pub mod transform; +pub mod util; fn main() -> Result<(), failure::Error> { simple_logger::init_with_level(Level::Info).unwrap(); diff --git a/yaml_test_runner/src/regex.rs b/yaml_test_runner/src/regex.rs index 383a846e..e09103ab 100644 --- a/yaml_test_runner/src/regex.rs +++ b/yaml_test_runner/src/regex.rs @@ -1,5 +1,5 @@ use lazy_static; -use regex::{Regex, Captures}; +use regex::{Captures, Regex}; lazy_static! { // replace usages of "$.*" with the captured value @@ -21,12 +21,16 @@ lazy_static! { /// Replaces a "set" step value with a variable pub fn replace_set_quoted_delimited>(s: S) -> String { - SET_QUOTED_DELIMITED_REGEX.replace_all(s.as_ref(), "$1").into_owned() + SET_QUOTED_DELIMITED_REGEX + .replace_all(s.as_ref(), "$1") + .into_owned() } /// Replaces a "set" step value with a variable pub fn replace_set_delimited>(s: S) -> String { - SET_DELIMITED_REGEX.replace_all(s.as_ref(), "$1").into_owned() + SET_DELIMITED_REGEX + .replace_all(s.as_ref(), "$1") + .into_owned() } /// Replaces a "set" step value with a variable @@ -38,11 +42,9 @@ pub fn replace_set>(s: S) -> String { /// larger than i32 will be handled correctly when passed to json! macro pub fn replace_i64>(s: S) -> String { INT_REGEX - .replace_all(s.as_ref(), |c: &Captures| { - match &c[2].parse::() { - Ok(i) if *i > i32::max_value() as i64 => format!("{}{}i64{}", &c[1], &c[2], &c[3]), - _ => format!("{}", &c[0]) - } + .replace_all(s.as_ref(), |c: &Captures| match &c[2].parse::() { + Ok(i) if *i > i32::max_value() as i64 => format!("{}{}i64{}", &c[1], &c[2], &c[3]), + _ => format!("{}", &c[0]), }) .into_owned() -} \ No newline at end of file +} diff --git a/yaml_test_runner/src/step/comparison.rs b/yaml_test_runner/src/step/comparison.rs index c96d51f4..94d4cdc1 100644 --- a/yaml_test_runner/src/step/comparison.rs +++ b/yaml_test_runner/src/step/comparison.rs @@ -1,7 +1,7 @@ use quote::{ToTokens, Tokens}; use super::Step; -use crate::step::{Expr}; +use crate::step::Expr; use yaml_rust::Yaml; pub const OPERATORS: [&'static str; 4] = ["lt", "lte", "gt", "gte"]; @@ -25,14 +25,14 @@ impl Comparison { .ok_or_else(|| failure::err_msg(format!("expected hash but found {:?}", yaml)))?; let (k, v) = hash.iter().next().unwrap(); - let expr = k.as_str().ok_or_else(|| { - failure::err_msg(format!("expected string key but found {:?}", k)) - })?; + let expr = k + .as_str() + .ok_or_else(|| failure::err_msg(format!("expected string key but found {:?}", k)))?; Ok(Comparison { expr: expr.into(), value: v.clone(), - op: op.into() + op: op.into(), }) } @@ -69,7 +69,7 @@ impl ToTokens for Comparison { "lt" => "<", "gt" => ">", "gte" => ">=", - n => panic!("unsupported op {}", n) + n => panic!("unsupported op {}", n), }; match self.value.as_i64() { @@ -80,14 +80,16 @@ impl ToTokens for Comparison { match self.value.as_str() { // handle "set" values Some(s) if s.starts_with('$') => { - let s = s.trim_start_matches('$') + let s = s + .trim_start_matches('$') .trim_start_matches('{') .trim_end_matches('}'); let expr_ident = syn::Ident::from(expr.as_str()); let ident = syn::Ident::from(s); let op_ident = syn::Ident::from(op); let message = "Expected value at {} to be numeric but is {}"; - let comparison_message = "Expected value at {} to be {:?} {}, but is {}"; + let comparison_message = + "Expected value at {} to be {:?} {}, but is {}"; tokens.append(quote! { match &json#expr_ident { Value::Number(n) => { @@ -109,7 +111,7 @@ impl ToTokens for Comparison { _ => panic!("Expected i64 or f64 but found {:?}", &self.value), } } - } + }, } } } diff --git a/yaml_test_runner/src/step/is_false.rs b/yaml_test_runner/src/step/is_false.rs index 22d17eab..7a5056bf 100644 --- a/yaml_test_runner/src/step/is_false.rs +++ b/yaml_test_runner/src/step/is_false.rs @@ -16,13 +16,11 @@ impl From for Step { impl IsFalse { pub fn try_parse(yaml: &Yaml) -> Result { - let expr = yaml - .as_str() - .ok_or_else(|| failure::err_msg(format!("expected string key but found {:?}", &yaml)))?; + let expr = yaml.as_str().ok_or_else(|| { + failure::err_msg(format!("expected string key but found {:?}", &yaml)) + })?; - Ok(IsFalse { - expr: expr.into(), - }) + Ok(IsFalse { expr: expr.into() }) } } diff --git a/yaml_test_runner/src/step/is_true.rs b/yaml_test_runner/src/step/is_true.rs index dd0edfbb..f385aace 100644 --- a/yaml_test_runner/src/step/is_true.rs +++ b/yaml_test_runner/src/step/is_true.rs @@ -16,13 +16,11 @@ impl From for Step { impl IsTrue { pub fn try_parse(yaml: &Yaml) -> Result { - let expr = yaml - .as_str() - .ok_or_else(|| failure::err_msg(format!("expected string key but found {:?}", &yaml)))?; + let expr = yaml.as_str().ok_or_else(|| { + failure::err_msg(format!("expected string key but found {:?}", &yaml)) + })?; - Ok(IsTrue { - expr: expr.into(), - }) + Ok(IsTrue { expr: expr.into() }) } } diff --git a/yaml_test_runner/src/step/match.rs b/yaml_test_runner/src/step/match.rs index c6e2c19e..990573a1 100644 --- a/yaml_test_runner/src/step/match.rs +++ b/yaml_test_runner/src/step/match.rs @@ -22,7 +22,10 @@ impl Match { let (k, v) = hash.iter().next().unwrap(); let expr = k.as_str().unwrap().trim(); - Ok(Match { expr: expr.into(), value: v.clone() }) + Ok(Match { + expr: expr.into(), + value: v.clone(), + }) } } diff --git a/yaml_test_runner/src/step/mod.rs b/yaml_test_runner/src/step/mod.rs index 88355951..1e597679 100644 --- a/yaml_test_runner/src/step/mod.rs +++ b/yaml_test_runner/src/step/mod.rs @@ -2,23 +2,23 @@ use api_generator::generator::Api; use std::fmt::Write; use yaml_rust::Yaml; +mod comparison; mod r#do; +mod is_false; +mod is_true; mod length; mod r#match; mod set; mod skip; mod transform_and_set; -mod is_true; -mod is_false; -mod comparison; +pub use comparison::{Comparison, OPERATORS}; +pub use is_false::*; +pub use is_true::*; pub use length::*; pub use r#do::*; pub use r#match::*; pub use set::*; pub use skip::*; -pub use is_true::*; -pub use is_false::*; -pub use comparison::{Comparison, OPERATORS}; pub use transform_and_set::*; pub fn parse_steps(api: &Api, steps: &[Yaml]) -> Result, failure::Error> { @@ -85,7 +85,7 @@ pub fn parse_steps(api: &Api, steps: &[Yaml]) -> Result, failure::Erro /// An expression to apply to the response. Can be the whole body ($body) or an /// indexer expression into a JSON response. pub struct Expr { - expr: String + expr: String, } impl From<&str> for Expr { @@ -162,7 +162,11 @@ impl Expr { } else if s.as_str() == "_arbitrary_key_" { // handle _arbitrary_key_. // wrap in Value::String to allow uniform unwrapping in subsequent steps - write!(expr, ".as_object().unwrap().iter().next().map(|(k, _)| json!(k)).unwrap()").unwrap(); + write!( + expr, + ".as_object().unwrap().iter().next().map(|(k, _)| json!(k)).unwrap()" + ) + .unwrap(); } else { write!(expr, "[\"{}\"]", s).unwrap(); } diff --git a/yaml_test_runner/src/step/transform_and_set.rs b/yaml_test_runner/src/step/transform_and_set.rs index b7bfce19..5f33dfc6 100644 --- a/yaml_test_runner/src/step/transform_and_set.rs +++ b/yaml_test_runner/src/step/transform_and_set.rs @@ -43,7 +43,7 @@ impl From<&str> for Transformation { let name = format!("transform::{}", value.as_str().to_snake_case()); function = Some(name); value = String::new(); - }, + } ',' | ')' => { let expr = value.trim(); exprs.push(Expr::new(expr)); diff --git a/yaml_test_runner/src/transform.rs b/yaml_test_runner/src/transform.rs index 16f7de66..9ebf3513 100644 --- a/yaml_test_runner/src/transform.rs +++ b/yaml_test_runner/src/transform.rs @@ -9,4 +9,4 @@ pub fn base_64_encode_credentials(user: &str, password: &str) -> String { write!(encoder, "{}", password).unwrap(); }; String::from_utf8(value).unwrap() -} \ No newline at end of file +} diff --git a/yaml_test_runner/src/util.rs b/yaml_test_runner/src/util.rs index 6fa93a9c..fc86c0fe 100644 --- a/yaml_test_runner/src/util.rs +++ b/yaml_test_runner/src/util.rs @@ -1,5 +1,5 @@ +use elasticsearch::http::{response::Response, Method, StatusCode}; use serde_json::Value; -use elasticsearch::http::{Method, StatusCode, response::Response}; pub fn len_from_value(value: &Value) -> Result { match value { @@ -11,7 +11,9 @@ pub fn len_from_value(value: &Value) -> Result { } } -pub async fn read_response(response: Response) -> Result<(Method, StatusCode, String, Value), failure::Error> { +pub async fn read_response( + response: Response, +) -> Result<(Method, StatusCode, String, Value), failure::Error> { let is_json = response.content_type().starts_with("application/json"); let method = response.method(); let status_code = response.status_code(); From bb9fc21462de369a203f6fe4df2a33ade8a67ffb Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Fri, 15 May 2020 12:38:45 +1000 Subject: [PATCH 094/127] introduce macros --- yaml_test_runner/src/macros.rs | 25 +++++++++++++++++++++++++ yaml_test_runner/src/main.rs | 2 ++ yaml_test_runner/src/step/do.rs | 17 ++++------------- 3 files changed, 31 insertions(+), 13 deletions(-) create mode 100644 yaml_test_runner/src/macros.rs diff --git a/yaml_test_runner/src/macros.rs b/yaml_test_runner/src/macros.rs new file mode 100644 index 00000000..eb1853eb --- /dev/null +++ b/yaml_test_runner/src/macros.rs @@ -0,0 +1,25 @@ +#![allow(unused_macros)] +#![macro_use] + +#[macro_export] +macro_rules! assert_response_success { + ($response:ident) => {{ + assert!( + $response.status_code().is_success(), + "expected response to be successful but was {}", + $response.status_code().as_u16() + ); + }}; +} + +#[macro_export] +macro_rules! assert_response_success_or { + ($response:ident, $status:expr) => {{ + assert!( + $response.status_code().is_success() || $response.status_code().as_u16() == $status, + "expected response to be successful or {} but was {}", + $status, + $response.status_code().as_u16() + ); + }}; +} \ No newline at end of file diff --git a/yaml_test_runner/src/main.rs b/yaml_test_runner/src/main.rs index 247fdabd..702b5c38 100644 --- a/yaml_test_runner/src/main.rs +++ b/yaml_test_runner/src/main.rs @@ -1,6 +1,8 @@ // TODO: remove when implementation is more complete. #![allow(dead_code)] +pub mod macros; + #[macro_use] extern crate log; extern crate simple_logger; diff --git a/yaml_test_runner/src/step/do.rs b/yaml_test_runner/src/step/do.rs index 1c640314..a19c2f80 100644 --- a/yaml_test_runner/src/step/do.rs +++ b/yaml_test_runner/src/step/do.rs @@ -117,20 +117,11 @@ impl Do { c.to_tokens(tokens); } else { match &self.api_call.ignore { - Some(i) => { - tokens.append(quote! { - assert!( - response.status_code().is_success() || response.status_code().as_u16() == #i, - "expected response to be successful or {} but was {}", - #i, - response.status_code().as_u16()); - }); - } + Some(i) => tokens.append(quote! { + assert_response_success_or!(response, #i); + }), None => tokens.append(quote! { - assert!( - response.status_code().is_success(), - "expected response to be successful but was {}", - response.status_code().as_u16()); + assert_response_success!(response); }), } } From b5ea611d8d47456d9f5e75fdeb72999a3bfb5dfa Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Fri, 15 May 2020 12:45:29 +1000 Subject: [PATCH 095/127] skip overall buckets test --- yaml_test_runner/skip.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/yaml_test_runner/skip.yml b/yaml_test_runner/skip.yml index 9c18e6f8..eef8c0fe 100644 --- a/yaml_test_runner/skip.yml +++ b/yaml_test_runner/skip.yml @@ -19,4 +19,7 @@ tests: # this test always returns "exponential_avg_checkpoint_duration_ms": 0.0 . seems like it might be missing data in # the setup, fires quicker than any documents are processed, or the delay of 1m is too high? - - generated::xpack::transform::_transforms_stats_continuous::tests::test_get_continuous_transform_stats \ No newline at end of file + - generated::xpack::transform::_transforms_stats_continuous::tests::test_get_continuous_transform_stats + + # this test always returns 3 buckets where 1 is expected + - generated::xpack::ml::_jobs_get_result_overall_buckets::tests::test_overall_buckets_given_overall_score_filter \ No newline at end of file From f50e714928b9730377f0da4c9d14db9570d4addb Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Fri, 15 May 2020 14:11:56 +1000 Subject: [PATCH 096/127] assert_match and assert_regex_match macros --- yaml_test_runner/src/macros.rs | 32 ++++++++++ yaml_test_runner/src/step/match.rs | 95 ++++-------------------------- 2 files changed, 45 insertions(+), 82 deletions(-) diff --git a/yaml_test_runner/src/macros.rs b/yaml_test_runner/src/macros.rs index eb1853eb..c3ac12ae 100644 --- a/yaml_test_runner/src/macros.rs +++ b/yaml_test_runner/src/macros.rs @@ -22,4 +22,36 @@ macro_rules! assert_response_success_or { $response.status_code().as_u16() ); }}; +} + +#[macro_export] +macro_rules! assert_match { + ($expected:expr, $($actual:tt)+) => {{ + assert_eq!($expected, json!($($actual)+), + "expected value {} to match {:?} but was {:?}", stringify!($expected), json!($($actual)+), $expected + ); + }}; +} + +#[macro_export] +macro_rules! assert_null { + ($expected:expr) => {{ + assert!($expected.is_null(), "expected value {} to be null but was {:?}", stringify!($expected), $expected); + }}; +} + +#[macro_export] +macro_rules! assert_regex_match { + ($expected:expr, $regex:expr) => {{ + let regex = regex::RegexBuilder::new($regex) + .ignore_whitespace(true) + .build()?; + assert!( + regex.is_match($expected), + "expected value {} to match regex\n\n{}\n\nbut was\n\n{}", + stringify!($expected), + $regex, + $expected + ); + }}; } \ No newline at end of file diff --git a/yaml_test_runner/src/step/match.rs b/yaml_test_runner/src/step/match.rs index 990573a1..169b08b9 100644 --- a/yaml_test_runner/src/step/match.rs +++ b/yaml_test_runner/src/step/match.rs @@ -37,37 +37,17 @@ impl ToTokens for Match { Yaml::String(s) => { if s.starts_with('/') { let s = clean_regex(s); - if self.expr.is_body() { tokens.append(quote! { - let regex = regex::RegexBuilder::new(#s) - .ignore_whitespace(true) - .build()?; - assert!( - regex.is_match(&text), - "expected $body:\n\n{}\n\nto match regex:\n\n{}", - &text, - #s - ); + assert_regex_match!(&text, #s); }); } else { let ident = syn::Ident::from(expr.as_str()); tokens.append(quote! { - let regex = regex::RegexBuilder::new(#s) - .ignore_whitespace(true) - .build()?; - assert!( - regex.is_match(json#ident.as_str().unwrap()), - "expected value at {}:\n\n{}\n\nto match regex:\n\n{}", - #expr, - json#ident.as_str().unwrap(), - #s - ); + assert_regex_match!(json#ident.as_str().unwrap(), #s); }); } } else { - let ident = syn::Ident::from(expr.as_str()); - // handle set values let t = if s.starts_with('$') { let t = s @@ -80,15 +60,9 @@ impl ToTokens for Match { quote! { #s } }; + let ident = syn::Ident::from(expr.as_str()); tokens.append(quote! { - assert_eq!( - json#ident.as_str().unwrap(), - #t, - "expected value at {} to be {} but was {}", - #expr, - #t, - json#ident.as_str().unwrap() - ); + assert_match!(json#ident, #t); }) } } @@ -100,23 +74,10 @@ impl ToTokens for Match { // handle the case where the YAML test asserts a match against an integer value // but a floating point value is returned from Elasticsearch tokens.append(quote! { - match json#ident.as_i64() { - Some(i) => assert_eq!( - i, - #i, - "expected value at {} to be {} but was {}", - #expr, - #i, - i - ), - None => assert_eq!( - json#ident.as_f64().unwrap(), - #i as f64, - "expected value at {} to be {} but was {}", - #expr, - #i as f64, - json#ident.as_f64().unwrap() - ) + if json#ident.is_i64() { + assert_match!(json#ident, #i); + } else { + assert_match!(json#ident, #i as f64); } }); } @@ -128,14 +89,7 @@ impl ToTokens for Match { } else { let ident = syn::Ident::from(expr.as_str()); tokens.append(quote! { - assert_eq!( - json#ident.as_f64().unwrap(), - #f, - "expected value at {} to be {} but was {}", - #expr, - #f, - json#ident.as_f64().unwrap() - ); + assert_match!(json#ident, #f); }); } } @@ -147,12 +101,7 @@ impl ToTokens for Match { } else { let ident = syn::Ident::from(expr.as_str()); tokens.append(quote! { - assert!( - json#ident.is_null(), - "expected value at {} to be null but was {}", - #expr, - json#ident.to_string(), - ); + assert_null!(json#ident); }); } } @@ -162,14 +111,7 @@ impl ToTokens for Match { } else { let ident = syn::Ident::from(expr.as_str()); tokens.append(quote! { - assert_eq!( - json#ident.as_bool().unwrap(), - #b, - "expected value at {} to be {} but was {}", - #expr, - #b, - json#ident.as_bool().unwrap(), - ); + assert_match!(json#ident, #b); }); } } @@ -185,23 +127,12 @@ impl ToTokens for Match { if self.expr.is_body() { tokens.append(quote! { - assert_eq!( - json, - json!(#json), - "expected response to be {} but was {}", - json!(#json).to_string(), - json.to_string()); + assert_match!(json, #json); }); } else { let ident = syn::Ident::from(expr.as_str()); tokens.append(quote! { - assert_eq!( - json#ident, - json!(#json), - "expected value at {} to be {} but was {}", - #expr, - json!(#json).to_string(), - json#ident.to_string()); + assert_match!(json#ident, #json); }); } } From 3c2e719ef6e7217c35cfc87338cde1ce2b8569c8 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Fri, 15 May 2020 15:28:50 +1000 Subject: [PATCH 097/127] Wrap numeric match branching in macro --- yaml_test_runner/src/macros.rs | 14 ++++++++++++++ yaml_test_runner/src/step/match.rs | 8 +------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/yaml_test_runner/src/macros.rs b/yaml_test_runner/src/macros.rs index c3ac12ae..28316638 100644 --- a/yaml_test_runner/src/macros.rs +++ b/yaml_test_runner/src/macros.rs @@ -33,6 +33,19 @@ macro_rules! assert_match { }}; } +/// handle the case where the YAML test asserts a match against an integer value +/// but a floating point value is returned from Elasticsearch +#[macro_export] +macro_rules! assert_numeric_match { + ($expected:expr, $actual:expr) => {{ + if $expected.is_i64() { + assert_match!($expected, $actual); + } else { + assert_match!($expected, $actual as f64); + } + }}; +} + #[macro_export] macro_rules! assert_null { ($expected:expr) => {{ @@ -40,6 +53,7 @@ macro_rules! assert_null { }}; } +/// Asserts that the first string value matches the second string regular expression #[macro_export] macro_rules! assert_regex_match { ($expected:expr, $regex:expr) => {{ diff --git a/yaml_test_runner/src/step/match.rs b/yaml_test_runner/src/step/match.rs index 169b08b9..927a5d56 100644 --- a/yaml_test_runner/src/step/match.rs +++ b/yaml_test_runner/src/step/match.rs @@ -71,14 +71,8 @@ impl ToTokens for Match { panic!("match on $body with i64"); } else { let ident = syn::Ident::from(expr.as_str()); - // handle the case where the YAML test asserts a match against an integer value - // but a floating point value is returned from Elasticsearch tokens.append(quote! { - if json#ident.is_i64() { - assert_match!(json#ident, #i); - } else { - assert_match!(json#ident, #i as f64); - } + assert_numeric_match!(json#ident, #i); }); } } From 2f7e52931291b600a48fb57889bae2a1c9092154 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Fri, 15 May 2020 15:29:30 +1000 Subject: [PATCH 098/127] Add assert_length macro --- yaml_test_runner/src/macros.rs | 28 ++++++++++++++++++++++++++++ yaml_test_runner/src/step/length.rs | 6 ++---- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/yaml_test_runner/src/macros.rs b/yaml_test_runner/src/macros.rs index 28316638..f4c0f731 100644 --- a/yaml_test_runner/src/macros.rs +++ b/yaml_test_runner/src/macros.rs @@ -68,4 +68,32 @@ macro_rules! assert_regex_match { $expected ); }}; +} + +/// Asserts that the length of a [serde_json::Value] type matches the expected length. +/// A length is calculated from the value based on the variant e.g. +/// - string length +/// - array length +/// - number of keys in object +/// - numeric value +#[macro_export] +macro_rules! assert_length { + ($expr:expr, $len:expr) => {{ + let len = match $expr { + Value::Number(n) => n.as_i64().unwrap() as usize, + Value::String(s) => s.len(), + Value::Array(a) => a.len(), + Value::Object(o) => o.len(), + v => panic!("Cannot get length from {:?}", v) + }; + + assert_eq!( + $len, + len, + "expected value {} to have length {} but was {}", + stringify!($expr), + $len, + len + ); + }}; } \ No newline at end of file diff --git a/yaml_test_runner/src/step/length.rs b/yaml_test_runner/src/step/length.rs index d2e029dc..1af3ff56 100644 --- a/yaml_test_runner/src/step/length.rs +++ b/yaml_test_runner/src/step/length.rs @@ -44,15 +44,13 @@ impl ToTokens for Length { if self.expr.is_body() { tokens.append(quote! { - let len = util::len_from_value(&json)?; - assert_eq!(#len, len); + assert_length!(&json, #len); }); } else { let expr = self.expr.expression(); let ident = syn::Ident::from(expr); tokens.append(quote! { - let len = util::len_from_value(&json#ident)?; - assert_eq!(#len, len); + assert_length!(&json#ident, #len); }); } } From a7e822d197512c490701c5f5bc610207782a2bc3 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Fri, 15 May 2020 15:34:39 +1000 Subject: [PATCH 099/127] Don't assert successful response automatically This commit reverts the asserting of a successful response if there's no catch or ignore. Some tests may return a 404 but are not set to ignore it: generated::xpack::ml::_index_layout::tests::test_crud_on_two_jobs_in_shar ed_index --- yaml_test_runner/src/regex.rs | 2 +- yaml_test_runner/src/step/do.rs | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/yaml_test_runner/src/regex.rs b/yaml_test_runner/src/regex.rs index e09103ab..6c93391c 100644 --- a/yaml_test_runner/src/regex.rs +++ b/yaml_test_runner/src/regex.rs @@ -6,7 +6,7 @@ lazy_static! { pub static ref SET_REGEX: Regex = Regex::new(r#""\$(.*?)""#).unwrap(); - // replace usages of ${.*} with the captured value + // replace usages of "${.*}" with the captured value pub static ref SET_QUOTED_DELIMITED_REGEX: Regex = Regex::new(r#""\$\{(.*?)\}""#).unwrap(); diff --git a/yaml_test_runner/src/step/do.rs b/yaml_test_runner/src/step/do.rs index a19c2f80..331b03a4 100644 --- a/yaml_test_runner/src/step/do.rs +++ b/yaml_test_runner/src/step/do.rs @@ -115,15 +115,13 @@ impl Do { }); } c.to_tokens(tokens); - } else { - match &self.api_call.ignore { - Some(i) => tokens.append(quote! { + } + + match &self.api_call.ignore { + Some(i) => tokens.append(quote! { assert_response_success_or!(response, #i); }), - None => tokens.append(quote! { - assert_response_success!(response); - }), - } + None => (), } read_response From 66ed1a96bdeaddc8b2dbfe6d2165320e73f06f98 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Fri, 15 May 2020 15:34:51 +1000 Subject: [PATCH 100/127] skip snapshot test --- yaml_test_runner/skip.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/yaml_test_runner/skip.yml b/yaml_test_runner/skip.yml index eef8c0fe..084753c7 100644 --- a/yaml_test_runner/skip.yml +++ b/yaml_test_runner/skip.yml @@ -22,4 +22,7 @@ tests: - generated::xpack::transform::_transforms_stats_continuous::tests::test_get_continuous_transform_stats # this test always returns 3 buckets where 1 is expected - - generated::xpack::ml::_jobs_get_result_overall_buckets::tests::test_overall_buckets_given_overall_score_filter \ No newline at end of file + - generated::xpack::ml::_jobs_get_result_overall_buckets::tests::test_overall_buckets_given_overall_score_filter + + # this test fails because it can't access snapshot to restore it + - generated::xpack::snapshot::_10_basic::tests::create_a_source_only_snapshot_and_then_restore_it \ No newline at end of file From 2a38faf724d97d882c6cf63fe732a21cbd302e03 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Fri, 15 May 2020 16:47:45 +1000 Subject: [PATCH 101/127] handle additional i64 replace cases and simplify regex --- yaml_test_runner/src/regex.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/yaml_test_runner/src/regex.rs b/yaml_test_runner/src/regex.rs index 6c93391c..2a48c23f 100644 --- a/yaml_test_runner/src/regex.rs +++ b/yaml_test_runner/src/regex.rs @@ -14,9 +14,11 @@ lazy_static! { pub static ref SET_DELIMITED_REGEX: Regex = Regex::new(r#"\$\{(.*?)\}"#).unwrap(); - // include i64 suffix on whole numbers + // include i64 suffix on whole numbers larger than i32 + // will match on numbers with 10 or more digits, with the replace + // call testing against i32::max_value pub static ref INT_REGEX: Regex = - regex::Regex::new(r"(:\s?)(\d+?)([,\s?|\s*?}])").unwrap(); + regex::Regex::new(r"([,:\[{]\s*)(\d{10,}?)(\s*[,}\]])").unwrap(); } /// Replaces a "set" step value with a variable From 3eac83c80280ab798067869d95f4994c6ea8dc1f Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Fri, 15 May 2020 16:48:33 +1000 Subject: [PATCH 102/127] Replace set values and i64 in match against object and array --- yaml_test_runner/src/step/match.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/yaml_test_runner/src/step/match.rs b/yaml_test_runner/src/step/match.rs index 927a5d56..3f6e3731 100644 --- a/yaml_test_runner/src/step/match.rs +++ b/yaml_test_runner/src/step/match.rs @@ -2,6 +2,7 @@ use super::Step; use crate::step::{clean_regex, Expr}; use quote::{ToTokens, Tokens}; use yaml_rust::{Yaml, YamlEmitter}; +use crate::regex::*; pub struct Match { pub expr: Expr, @@ -117,9 +118,17 @@ impl ToTokens for Match { } let value: serde_json::Value = serde_yaml::from_str(&s).unwrap(); - let json = syn::Ident::from(value.to_string()); - if self.expr.is_body() { + let json = { + let mut json = value.to_string(); + json = replace_set_quoted_delimited(json); + json = replace_set_delimited(json); + json = replace_set(json); + json = replace_i64(json); + syn::Ident::from(json) + }; + + if self.expr.is_body() || self.expr.is_empty() { tokens.append(quote! { assert_match!(json, #json); }); From b45a3a65b1a73b3499c8861c5ccdaea18889bffe Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Mon, 18 May 2020 11:09:08 +1000 Subject: [PATCH 103/127] assert status code macros --- yaml_test_runner/src/client.rs | 14 ++------------ yaml_test_runner/src/macros.rs | 33 ++++++++++++++++++++++++++++++-- yaml_test_runner/src/step/do.rs | 23 ++++++---------------- yaml_test_runner/src/step/mod.rs | 2 +- 4 files changed, 40 insertions(+), 32 deletions(-) diff --git a/yaml_test_runner/src/client.rs b/yaml_test_runner/src/client.rs index e0e3ff24..afcec04c 100644 --- a/yaml_test_runner/src/client.rs +++ b/yaml_test_runner/src/client.rs @@ -189,12 +189,7 @@ async fn wait_for_yellow_status(client: &Elasticsearch) -> Result<(), Error> { .send() .await?; - assert!( - cluster_health.status_code().is_success(), - "cluster health returned {}", - cluster_health.status_code().as_u16() - ); - + assert_response_success!(cluster_health); Ok(()) } @@ -210,12 +205,7 @@ async fn delete_indices(client: &Elasticsearch) -> Result<(), Error> { .send() .await?; - assert!( - delete_response.status_code().is_success(), - "deleting indices returned {}", - delete_response.status_code().as_u16() - ); - + assert_response_success!(delete_response); Ok(()) } diff --git a/yaml_test_runner/src/macros.rs b/yaml_test_runner/src/macros.rs index f4c0f731..2e9dfdcb 100644 --- a/yaml_test_runner/src/macros.rs +++ b/yaml_test_runner/src/macros.rs @@ -1,6 +1,7 @@ #![allow(unused_macros)] #![macro_use] +/// Asserts that a [Response] has a status code >=200 and <300 #[macro_export] macro_rules! assert_response_success { ($response:ident) => {{ @@ -12,6 +13,7 @@ macro_rules! assert_response_success { }}; } +/// Asserts that a [Response] has a status code >=200 and <300 or matches the passed status #[macro_export] macro_rules! assert_response_success_or { ($response:ident, $status:expr) => {{ @@ -24,6 +26,31 @@ macro_rules! assert_response_success_or { }}; } +#[macro_export] +macro_rules! assert_status_code { + ($status_code:expr, $expected:expr) => {{ + assert_eq!( + $expected, + $status_code.as_u16(), + "expected status code to be {} but was {}", + $expected, + $status_code.as_u16() + ); + }}; +} + +#[macro_export] +macro_rules! assert_request_status_code { + ($status_code:expr) => {{ + let status_code = $status_code.as_u16(); + assert!( + status_code >= 400 && status_code < 600, + "expected status code in range 400-599 but was {}", status_code); + }}; +} + +/// Asserts that the passed [serde_json::Value] matches the second argument. +/// The second argument is converted to a [serde_json::Value] using the `json!` macro #[macro_export] macro_rules! assert_match { ($expected:expr, $($actual:tt)+) => {{ @@ -33,7 +60,8 @@ macro_rules! assert_match { }}; } -/// handle the case where the YAML test asserts a match against an integer value +/// Asserts that the passed [serde_json::Value] matches the expected numeric value. +/// This handles the case where a YAML test asserts a match against an integer value /// but a floating point value is returned from Elasticsearch #[macro_export] macro_rules! assert_numeric_match { @@ -46,6 +74,7 @@ macro_rules! assert_numeric_match { }}; } +/// Asserts that a [serde_json::Value] is null. #[macro_export] macro_rules! assert_null { ($expected:expr) => {{ @@ -70,7 +99,7 @@ macro_rules! assert_regex_match { }}; } -/// Asserts that the length of a [serde_json::Value] type matches the expected length. +/// Asserts that the length of a [serde_json::Value] matches the expected length. /// A length is calculated from the value based on the variant e.g. /// - string length /// - array length diff --git a/yaml_test_runner/src/step/do.rs b/yaml_test_runner/src/step/do.rs index 331b03a4..ef76dd6d 100644 --- a/yaml_test_runner/src/step/do.rs +++ b/yaml_test_runner/src/step/do.rs @@ -22,10 +22,7 @@ impl ToTokens for Catch { fn to_tokens(&self, tokens: &mut Tokens) { fn http_status_code(status_code: u16, tokens: &mut Tokens) { tokens.append(quote! { - assert_eq!( - response.status_code().as_u16(), - #status_code, - "expected response status code to be {} but was {}", #status_code, response.status_code().as_u16()); + assert_status_code!(response.status_code(), #status_code); }); } @@ -38,10 +35,7 @@ impl ToTokens for Catch { "conflict" => http_status_code(409, tokens), "request" => { tokens.append(quote! { - let status_code = response.status_code().as_u16(); - assert!( - status_code >= 400 && status_code < 600, - "Expected status code 400-599 but was {}", response.status_code().as_u16()); + assert_request_status_code!(response.status_code()); }); } "unavailable" => http_status_code(503, tokens), @@ -51,13 +45,7 @@ impl ToTokens for Catch { s => { let t = clean_regex(s); tokens.append(quote! { - let catch_regex = regex::Regex::new(#t)?; - assert!( - catch_regex.is_match(&text), - "expected text:\n\n{}\n\nto match regex:\n\n{}", - &text, - #s - ); + assert_regex_match!(&text, #t); }); } } @@ -96,8 +84,9 @@ impl Do { tokens.append(quote! { assert!( warnings.iter().any(|w| w.contains(#warning)), - "expected warnings to contain '{}' but did not", - #warning); + "expected warnings to contain '{}' but contained {:?}", + #warning, + &warnings); }); } } else if !self.allowed_warnings.is_empty() { diff --git a/yaml_test_runner/src/step/mod.rs b/yaml_test_runner/src/step/mod.rs index 1e597679..158887ad 100644 --- a/yaml_test_runner/src/step/mod.rs +++ b/yaml_test_runner/src/step/mod.rs @@ -82,7 +82,7 @@ pub fn parse_steps(api: &Api, steps: &[Yaml]) -> Result, failure::Erro Ok(parsed_steps) } -/// An expression to apply to the response. Can be the whole body ($body) or an +/// An expression to apply to the response. Can be the whole body ($body or "") or an /// indexer expression into a JSON response. pub struct Expr { expr: String, From 115795fbe3947f0f4eede9b1396bd23be5d2b6b4 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Mon, 18 May 2020 15:26:01 +1000 Subject: [PATCH 104/127] Generate yaml tests in tests directory This commit updates the generation of yaml tests to output them in a tests directory as a sibling of src, in line with integration test conventions --- .gitignore | 3 +- yaml_test_runner/src/{main.rs => bin/run.rs} | 29 ++---------- yaml_test_runner/src/generator.rs | 40 ++++++++-------- yaml_test_runner/src/lib.rs | 23 +++++++++ yaml_test_runner/src/macros.rs | 47 +++++++++++++++++-- yaml_test_runner/src/step/is_false.rs | 8 +--- yaml_test_runner/src/step/is_true.rs | 8 +--- yaml_test_runner/src/step/match.rs | 4 +- .../src/step/transform_and_set.rs | 1 + yaml_test_runner/tests/mod.rs | 1 + 10 files changed, 97 insertions(+), 67 deletions(-) rename yaml_test_runner/src/{main.rs => bin/run.rs} (88%) create mode 100644 yaml_test_runner/src/lib.rs create mode 100644 yaml_test_runner/tests/mod.rs diff --git a/.gitignore b/.gitignore index ee9798ac..410256a7 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ Cargo.lock .vscode/ *.log yaml_test_runner/yaml/ -yaml_test_runner/src/generated/ +yaml_test_runner/tests/oss +yaml_test_runner/tests/xpack diff --git a/yaml_test_runner/src/main.rs b/yaml_test_runner/src/bin/run.rs similarity index 88% rename from yaml_test_runner/src/main.rs rename to yaml_test_runner/src/bin/run.rs index 702b5c38..322633d1 100644 --- a/yaml_test_runner/src/main.rs +++ b/yaml_test_runner/src/bin/run.rs @@ -1,38 +1,15 @@ -// TODO: remove when implementation is more complete. -#![allow(dead_code)] - -pub mod macros; - #[macro_use] extern crate log; extern crate simple_logger; -#[macro_use] -extern crate lazy_static; -#[macro_use] -extern crate quote; extern crate api_generator; -#[macro_use] -#[cfg(test)] -extern crate serde_json; -use crate::generator::TestSuite; use clap::{App, Arg}; use log::Level; use std::fs; use std::path::PathBuf; use std::process::exit; -mod generator; -mod github; -pub mod step; - -#[cfg(test)] -mod generated; - -pub mod client; -pub mod regex; -pub mod transform; -pub mod util; +use yaml_test_runner::{generator::{self, TestSuite}, github}; fn main() -> Result<(), failure::Error> { simple_logger::init_with_level(Level::Info).unwrap(); @@ -98,8 +75,8 @@ fn main() -> Result<(), failure::Error> { let token = matches.value_of("token").expect("missing 'token' argument"); let path = matches.value_of("path").expect("missing 'path' argument"); let rest_specs_dir = PathBuf::from(path); - let download_dir = PathBuf::from("./yaml_test_runner/yaml"); - let generated_dir = PathBuf::from("./yaml_test_runner/src/generated"); + let download_dir = PathBuf::from(format!("./{}/yaml", env!("CARGO_PKG_NAME"))); + let generated_dir = PathBuf::from(format!("./{}/tests", env!("CARGO_PKG_NAME"))); github::download_test_suites(token, branch, &download_dir)?; diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 8c2b0f18..21dafd15 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -116,27 +116,25 @@ impl<'a> YamlTests<'a> { .collect(); quote! { - #[allow(unused_imports, unused_variables)] - #[cfg(test)] - pub mod tests { - use elasticsearch::*; - use elasticsearch::http::{ - headers::{HeaderName, HeaderValue}, - request::JsonBody, - Method, - }; - use elasticsearch::params::*; - #(#directives)* - use regex; - use serde_json::Value; - use crate::client; - use crate::transform; - use crate::util; - - #setup_fn - #teardown_fn - #(#tests)* - } + #![allow(unused_imports, unused_variables)] + use elasticsearch::*; + use elasticsearch::http::{ + headers::{HeaderName, HeaderValue}, + request::JsonBody, + Method, + }; + use elasticsearch::params::*; + #(#directives)* + use ::regex; + use serde_json::{json, Value}; + use yaml_test_runner::client; + use yaml_test_runner::transform; + use yaml_test_runner::util; + use yaml_test_runner::*; + + #setup_fn + #teardown_fn + #(#tests)* } } diff --git a/yaml_test_runner/src/lib.rs b/yaml_test_runner/src/lib.rs new file mode 100644 index 00000000..d81fe8b0 --- /dev/null +++ b/yaml_test_runner/src/lib.rs @@ -0,0 +1,23 @@ +#[macro_use] +extern crate log; +extern crate simple_logger; + +#[macro_use] +mod macros; + +#[macro_use] +extern crate lazy_static; + +extern crate serde_json; + +#[macro_use] +extern crate quote; +extern crate api_generator; + +pub mod generator; +pub mod step; +pub mod github; +pub mod client; +pub mod regex; +pub mod transform; +pub mod util; \ No newline at end of file diff --git a/yaml_test_runner/src/macros.rs b/yaml_test_runner/src/macros.rs index 2e9dfdcb..d88b3882 100644 --- a/yaml_test_runner/src/macros.rs +++ b/yaml_test_runner/src/macros.rs @@ -1,5 +1,4 @@ #![allow(unused_macros)] -#![macro_use] /// Asserts that a [Response] has a status code >=200 and <300 #[macro_export] @@ -82,12 +81,24 @@ macro_rules! assert_null { }}; } -/// Asserts that the first string value matches the second string regular expression +/// Asserts that the first string value matches the second string regular expression. An optional +/// third bool argument ignores pattern whitespace. #[macro_export] macro_rules! assert_regex_match { ($expected:expr, $regex:expr) => {{ let regex = regex::RegexBuilder::new($regex) - .ignore_whitespace(true) + .build()?; + assert!( + regex.is_match($expected), + "expected value {} to match regex\n\n{}\n\nbut was\n\n{}", + stringify!($expected), + $regex, + $expected + ); + }}; + ($expected:expr, $regex:expr, $ignore_whitespace:expr) => {{ + let regex = regex::RegexBuilder::new($regex) + .ignore_whitespace($ignore_whitespace) .build()?; assert!( regex.is_match($expected), @@ -125,4 +136,34 @@ macro_rules! assert_length { len ); }}; +} + +/// Asserts that the expression is "false" i.e. `0`, `false`, `undefined`, `null` or `""` +#[macro_export] +macro_rules! assert_is_false { + ($expr:expr) => {{ + let expr_string = stringify!($expr); + match $expr { + Value::Null => {}, + Value::Bool(b) => assert_eq!(*b, false, "expected value at {} to be false but was {}", expr_string, b), + Value::Number(n) => assert_eq!(n.as_f64().unwrap(), 0.0, "expected value at {} to be false (0) but was {}", expr_string, n.as_f64().unwrap()), + Value::String(s) => assert!(s.is_empty(), "expected value at {} to be false (empty) but was {}", expr_string, &s), + v => assert!(false, "expected value at {} to be false but was {:?}", expr_string, &v), + } + }}; +} + +/// Asserts that the expression is "true" i.e. not `0`, `false`, `undefined`, `null` or `""` +#[macro_export] +macro_rules! assert_is_true { + ($expr:expr) => {{ + let expr_string = stringify!($expr); + match $expr { + Value::Null => assert!(false, "expected value at {} to be true (not null) but was null", expr_string), + Value::Bool(b) => assert!(*b, "expected value at {} to be true but was false", expr_string), + Value::Number(n) => assert_ne!(n.as_f64().unwrap(), 0.0, "expected value at {} to be true (not 0) but was {}", expr_string, n.as_f64().unwrap()), + Value::String(s) => assert!(!s.is_empty(), "expected value at {} to be true (not empty) but was {}", expr_string, &s), + v => {}, + } + }}; } \ No newline at end of file diff --git a/yaml_test_runner/src/step/is_false.rs b/yaml_test_runner/src/step/is_false.rs index 7a5056bf..3c2d21f2 100644 --- a/yaml_test_runner/src/step/is_false.rs +++ b/yaml_test_runner/src/step/is_false.rs @@ -34,13 +34,7 @@ impl ToTokens for IsFalse { let expr = self.expr.expression(); let ident = syn::Ident::from(expr.as_str()); tokens.append(quote! { - match &json#ident { - Value::Null => {}, - Value::Bool(b) => assert_eq!(*b, false, "expected value at {} to be false but was {}", #expr, b), - Value::Number(n) => assert_eq!(n.as_f64().unwrap(), 0.0, "expected value at {} to be false (0) but was {}", #expr, n.as_f64().unwrap()), - Value::String(s) => assert!(s.is_empty(), "expected value at {} to be false (empty) but was {}", #expr, &s), - v => assert!(false, "expected value at {} to be false but was {:?}", #expr, &v), - } + assert_is_false!(&json#ident); }); } } diff --git a/yaml_test_runner/src/step/is_true.rs b/yaml_test_runner/src/step/is_true.rs index f385aace..339f4425 100644 --- a/yaml_test_runner/src/step/is_true.rs +++ b/yaml_test_runner/src/step/is_true.rs @@ -38,13 +38,7 @@ impl ToTokens for IsTrue { let expr = self.expr.expression(); let ident = syn::Ident::from(expr.as_str()); tokens.append(quote! { - match &json#ident { - Value::Null => assert!(false, "expected value at {} to be true (not null) but was null", #expr), - Value::Bool(b) => assert!(*b, "expected value at {} to be true but was false", #expr), - Value::Number(n) => assert_ne!(n.as_f64().unwrap(), 0.0, "expected value at {} to be true (not 0) but was {}", #expr, n.as_f64().unwrap()), - Value::String(s) => assert!(!s.is_empty(), "expected value at {} to be true (not empty) but was {}", #expr, &s), - v => {}, - } + assert_is_true!(&json#ident); }); } } diff --git a/yaml_test_runner/src/step/match.rs b/yaml_test_runner/src/step/match.rs index 3f6e3731..4b36147a 100644 --- a/yaml_test_runner/src/step/match.rs +++ b/yaml_test_runner/src/step/match.rs @@ -40,12 +40,12 @@ impl ToTokens for Match { let s = clean_regex(s); if self.expr.is_body() { tokens.append(quote! { - assert_regex_match!(&text, #s); + assert_regex_match!(&text, #s, true); }); } else { let ident = syn::Ident::from(expr.as_str()); tokens.append(quote! { - assert_regex_match!(json#ident.as_str().unwrap(), #s); + assert_regex_match!(json#ident.as_str().unwrap(), #s, true); }); } } else { diff --git a/yaml_test_runner/src/step/transform_and_set.rs b/yaml_test_runner/src/step/transform_and_set.rs index 5f33dfc6..1749400b 100644 --- a/yaml_test_runner/src/step/transform_and_set.rs +++ b/yaml_test_runner/src/step/transform_and_set.rs @@ -6,6 +6,7 @@ use inflector::Inflector; use yaml_rust::Yaml; pub struct Transformation { + #[allow(dead_code)] raw: String, function: String, exprs: Vec, diff --git a/yaml_test_runner/tests/mod.rs b/yaml_test_runner/tests/mod.rs new file mode 100644 index 00000000..d197b6c2 --- /dev/null +++ b/yaml_test_runner/tests/mod.rs @@ -0,0 +1 @@ +pub mod oss; \ No newline at end of file From eb010c01f695856945ed42da869c5e2f7bd49f4a Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Mon, 18 May 2020 15:58:45 +1000 Subject: [PATCH 105/127] replace backticks in regex --- yaml_test_runner/src/step/mod.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/yaml_test_runner/src/step/mod.rs b/yaml_test_runner/src/step/mod.rs index 158887ad..a2b8b1a0 100644 --- a/yaml_test_runner/src/step/mod.rs +++ b/yaml_test_runner/src/step/mod.rs @@ -222,10 +222,7 @@ pub fn ok_or_accumulate( } } -// trim the enclosing forward slashes and -// 1. replace escaped forward slashes (not needed after trimming forward slashes) -// 2. replace escaped colons and hashes (not supported by regex crate) -// TODO: create wrapper struct +// cleans up a regex as specified in YAML to one that will work with the regex crate. pub fn clean_regex>(s: S) -> String { s.as_ref() .trim() @@ -235,4 +232,5 @@ pub fn clean_regex>(s: S) -> String { .replace("\\#", "#") .replace("\\%", "%") .replace("\\'", "'") + .replace("\\`", "`") } From 82b7f47ab80adbf70ca59be18c20a3de59826016 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Mon, 18 May 2020 17:22:44 +1000 Subject: [PATCH 106/127] Move test related components to tests/common module --- elasticsearch/tests/common/mod.rs | 4 ---- yaml_test_runner/src/bin/run.rs | 20 +++++++++++++++++-- yaml_test_runner/src/generator.rs | 17 ++++++++++------ yaml_test_runner/src/lib.rs | 8 +------- .../{src => tests/common}/client.rs | 3 ++- .../{src => tests/common}/macros.rs | 2 -- yaml_test_runner/tests/common/mod.rs | 5 +++++ .../{src => tests/common}/transform.rs | 0 .../{src => tests/common}/util.rs | 0 yaml_test_runner/tests/mod.rs | 2 ++ 10 files changed, 39 insertions(+), 22 deletions(-) rename yaml_test_runner/{src => tests/common}/client.rs (99%) rename yaml_test_runner/{src => tests/common}/macros.rs (99%) create mode 100644 yaml_test_runner/tests/common/mod.rs rename yaml_test_runner/{src => tests/common}/transform.rs (100%) rename yaml_test_runner/{src => tests/common}/util.rs (100%) diff --git a/elasticsearch/tests/common/mod.rs b/elasticsearch/tests/common/mod.rs index ee878269..bd2e4292 100644 --- a/elasticsearch/tests/common/mod.rs +++ b/elasticsearch/tests/common/mod.rs @@ -16,10 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -// From reqwest crate -// Licensed under Apache License, Version 2.0 -// https://github.com/seanmonstar/reqwest/blob/master/LICENSE-APACHE - pub mod client; pub mod server; diff --git a/yaml_test_runner/src/bin/run.rs b/yaml_test_runner/src/bin/run.rs index 322633d1..26b17109 100644 --- a/yaml_test_runner/src/bin/run.rs +++ b/yaml_test_runner/src/bin/run.rs @@ -103,9 +103,25 @@ fn main() -> Result<(), failure::Error> { let api = api_generator::generator::read_api(branch, &rest_specs_dir)?; - // delete existing generated files first + // delete everything under the generated_dir except common dir if generated_dir.exists() { - fs::remove_dir_all(&generated_dir)?; + let entries = fs::read_dir(&generated_dir)?; + for entry in entries { + if let Ok(e) = entry { + match e.file_type() { + Ok(f) => { + if e.file_name() != "common" { + if f.is_dir() { + fs::remove_dir_all(e.path())?; + } else if f.is_file() { + fs::remove_file(e.path())?; + } + } + }, + Err(_) => () + }; + } + } } generator::generate_tests_from_yaml( diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 21dafd15..4e1c0bc8 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -117,6 +117,7 @@ impl<'a> YamlTests<'a> { quote! { #![allow(unused_imports, unused_variables)] + use crate::common::{client, macros, transform, util}; use elasticsearch::*; use elasticsearch::http::{ headers::{HeaderName, HeaderValue}, @@ -127,10 +128,6 @@ impl<'a> YamlTests<'a> { #(#directives)* use ::regex; use serde_json::{json, Value}; - use yaml_test_runner::client; - use yaml_test_runner::transform; - use yaml_test_runner::util; - use yaml_test_runner::*; #setup_fn #teardown_fn @@ -477,14 +474,22 @@ fn write_mod_files(generated_dir: &PathBuf) -> Result<(), failure::Error> { let file_type = entry.file_type().unwrap(); let path = entry.path(); let name = path.file_stem().unwrap().to_string_lossy(); - if name.into_owned() != "mod" { + + let is_tests_common_dir = + name.as_ref() == "common" && path.parent().unwrap().file_name().unwrap() == "tests"; + + if name.as_ref() != "mod" { + if is_tests_common_dir { + mods.push("#[macro_use]".to_string()); + } + mods.push(format!( "pub mod {};", path.file_stem().unwrap().to_string_lossy() )); } - if file_type.is_dir() { + if file_type.is_dir() && !is_tests_common_dir { write_mod_files(&entry.path())?; } } diff --git a/yaml_test_runner/src/lib.rs b/yaml_test_runner/src/lib.rs index d81fe8b0..88aa3429 100644 --- a/yaml_test_runner/src/lib.rs +++ b/yaml_test_runner/src/lib.rs @@ -2,9 +2,6 @@ extern crate log; extern crate simple_logger; -#[macro_use] -mod macros; - #[macro_use] extern crate lazy_static; @@ -17,7 +14,4 @@ extern crate api_generator; pub mod generator; pub mod step; pub mod github; -pub mod client; -pub mod regex; -pub mod transform; -pub mod util; \ No newline at end of file +pub mod regex; \ No newline at end of file diff --git a/yaml_test_runner/src/client.rs b/yaml_test_runner/tests/common/client.rs similarity index 99% rename from yaml_test_runner/src/client.rs rename to yaml_test_runner/tests/common/client.rs index afcec04c..7047f839 100644 --- a/yaml_test_runner/src/client.rs +++ b/yaml_test_runner/tests/common/client.rs @@ -34,12 +34,13 @@ fn cluster_addr() -> String { } } +/// Determines if Fiddler.exe proxy process is running fn running_proxy() -> bool { let system = sysinfo::System::new(); !system.get_process_by_name("Fiddler").is_empty() } -/// create a client to use in tests +/// Creates a client to use in tests pub fn create() -> Elasticsearch { let mut url = Url::parse(cluster_addr().as_ref()).unwrap(); diff --git a/yaml_test_runner/src/macros.rs b/yaml_test_runner/tests/common/macros.rs similarity index 99% rename from yaml_test_runner/src/macros.rs rename to yaml_test_runner/tests/common/macros.rs index d88b3882..5d0df8a4 100644 --- a/yaml_test_runner/src/macros.rs +++ b/yaml_test_runner/tests/common/macros.rs @@ -1,5 +1,3 @@ -#![allow(unused_macros)] - /// Asserts that a [Response] has a status code >=200 and <300 #[macro_export] macro_rules! assert_response_success { diff --git a/yaml_test_runner/tests/common/mod.rs b/yaml_test_runner/tests/common/mod.rs new file mode 100644 index 00000000..4bc096bb --- /dev/null +++ b/yaml_test_runner/tests/common/mod.rs @@ -0,0 +1,5 @@ +#[macro_use] +pub mod macros; +pub mod client; +pub mod transform; +pub mod util; \ No newline at end of file diff --git a/yaml_test_runner/src/transform.rs b/yaml_test_runner/tests/common/transform.rs similarity index 100% rename from yaml_test_runner/src/transform.rs rename to yaml_test_runner/tests/common/transform.rs diff --git a/yaml_test_runner/src/util.rs b/yaml_test_runner/tests/common/util.rs similarity index 100% rename from yaml_test_runner/src/util.rs rename to yaml_test_runner/tests/common/util.rs diff --git a/yaml_test_runner/tests/mod.rs b/yaml_test_runner/tests/mod.rs index d197b6c2..2c3317ff 100644 --- a/yaml_test_runner/tests/mod.rs +++ b/yaml_test_runner/tests/mod.rs @@ -1 +1,3 @@ +#[macro_use] +pub mod common; pub mod oss; \ No newline at end of file From 33068d16b9f343ef7d4044f97bf064eada6b7f4c Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 19 May 2020 10:55:30 +1000 Subject: [PATCH 107/127] read yaml into Value --- yaml_test_runner/tests/common/util.rs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/yaml_test_runner/tests/common/util.rs b/yaml_test_runner/tests/common/util.rs index fc86c0fe..3b4d010d 100644 --- a/yaml_test_runner/tests/common/util.rs +++ b/yaml_test_runner/tests/common/util.rs @@ -1,26 +1,22 @@ use elasticsearch::http::{response::Response, Method, StatusCode}; use serde_json::Value; -pub fn len_from_value(value: &Value) -> Result { - match value { - Value::Number(n) => Ok(n.as_i64().unwrap() as usize), - Value::String(s) => Ok(s.len()), - Value::Array(a) => Ok(a.len()), - Value::Object(o) => Ok(o.len()), - v => Err(failure::err_msg(format!("Cannot get length from {:?}", v))), - } -} - +/// Reads the response from Elasticsearch, returning the method, status code, text response, +/// and the response parsed from json or yaml pub async fn read_response( response: Response, ) -> Result<(Method, StatusCode, String, Value), failure::Error> { let is_json = response.content_type().starts_with("application/json"); + let is_yaml = response.content_type().starts_with("application/yaml"); let method = response.method(); let status_code = response.status_code(); let text = response.text().await?; let json = if is_json && !text.is_empty() { serde_json::from_slice::(text.as_ref())? - } else { + } else if is_yaml && !text.is_empty() { + serde_yaml::from_slice::(text.as_ref())? + } + else { Value::Null }; From 5d6d60277fc724ae47a3b8336f62a940d7d6d19e Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 19 May 2020 13:14:23 +1000 Subject: [PATCH 108/127] skip additional tests --- yaml_test_runner/skip.yml | 34 +++++++++++++++++---------- yaml_test_runner/tests/common/util.rs | 7 +++--- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/yaml_test_runner/skip.yml b/yaml_test_runner/skip.yml index 084753c7..f671ae21 100644 --- a/yaml_test_runner/skip.yml +++ b/yaml_test_runner/skip.yml @@ -10,19 +10,29 @@ features: - stash_path_replace - embedded_stash_key -# tests to skip generating and compiling a test for. -# Take the form of the generated path e.g. -# generated::xpack::security::hidden_index::_13_security_tokens_read::tests::test_get_security_tokens_index_metadata +# tests to skip generating and compiling a test for tests: - # this test returns the CA cert before the cert, so always fails - - generated::xpack::ssl::_10_basic::tests::test_get_ssl_certificates + xpack/ssl/10_basic.yml: + # this test returns the CA cert before the cert, so always fails + - "Test get SSL certificates" - # this test always returns "exponential_avg_checkpoint_duration_ms": 0.0 . seems like it might be missing data in - # the setup, fires quicker than any documents are processed, or the delay of 1m is too high? - - generated::xpack::transform::_transforms_stats_continuous::tests::test_get_continuous_transform_stats + xpack/transform/transforms_stats_continuous.yml: + # this test always returns "exponential_avg_checkpoint_duration_ms": 0.0 . seems like it might be missing data in + # the setup, fires quicker than any documents are processed, or the delay of 1m is too high? + - "Test get continuous transform stats" - # this test always returns 3 buckets where 1 is expected - - generated::xpack::ml::_jobs_get_result_overall_buckets::tests::test_overall_buckets_given_overall_score_filter + xpack/ml/jobs_get_result_overall_buckets.yml: + # this test always returns 3 buckets where 1 is expected + - "Test overall buckets given overall_score filter" - # this test fails because it can't access snapshot to restore it - - generated::xpack::snapshot::_10_basic::tests::create_a_source_only_snapshot_and_then_restore_it \ No newline at end of file + xpack/snapshot/10_basic.yml: + # this test fails because it can't access snapshot to restore it + - "Create a source only snapshot and then restore it" + + oss/cat.aliases/10_basic.yml: + # this test fails as the regex needs a \n before the ending $ + - "Multiple alias names" + + oss/cat.indices/10_basic.yml: + # this test fails as the regex needs a \n before the ending $ + - "Test cat indices using health status" \ No newline at end of file diff --git a/yaml_test_runner/tests/common/util.rs b/yaml_test_runner/tests/common/util.rs index 3b4d010d..62ecf3aa 100644 --- a/yaml_test_runner/tests/common/util.rs +++ b/yaml_test_runner/tests/common/util.rs @@ -12,11 +12,10 @@ pub async fn read_response( let status_code = response.status_code(); let text = response.text().await?; let json = if is_json && !text.is_empty() { - serde_json::from_slice::(text.as_ref())? + serde_json::from_str::(text.as_ref())? } else if is_yaml && !text.is_empty() { - serde_yaml::from_slice::(text.as_ref())? - } - else { + serde_yaml::from_str::(text.as_ref())? + } else { Value::Null }; From e6ea4f9b3027b385debb1df6633ef455f1026acb Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 19 May 2020 13:14:48 +1000 Subject: [PATCH 109/127] normalize paths across OS --- yaml_test_runner/Cargo.toml | 1 + yaml_test_runner/src/generator.rs | 59 ++++++++++++++----------- yaml_test_runner/src/step/comparison.rs | 4 +- 3 files changed, 35 insertions(+), 29 deletions(-) diff --git a/yaml_test_runner/Cargo.toml b/yaml_test_runner/Cargo.toml index 8f08283f..f8a67420 100644 --- a/yaml_test_runner/Cargo.toml +++ b/yaml_test_runner/Cargo.toml @@ -17,6 +17,7 @@ itertools = "0.8.2" Inflector = "0.11.4" lazy_static = "1.4.0" log = "0.4.8" +path-slash = "0.1.1" quote = "~0.3" regex = "1.3.1" reqwest = "~0.9" diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 4e1c0bc8..ba99aef2 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -6,12 +6,14 @@ use api_generator::generator::Api; use regex::Regex; use semver::Version; use serde::Deserialize; -use std::collections::HashSet; +use std::collections::{HashSet, BTreeMap}; use std::fs; use std::fs::{File, OpenOptions}; use std::io::Write; use std::path::{Component, Path, PathBuf}; use yaml_rust::{Yaml, YamlLoader}; +use std::borrow::Borrow; +use path_slash::PathExt; /// The test suite to compile #[derive(Debug, PartialEq)] @@ -22,7 +24,7 @@ pub enum TestSuite { /// The components of a test file, constructed from a yaml file struct YamlTests<'a> { - path: &'a Path, + path: String, version: &'a Version, skip: &'a GlobalSkip, suite: TestSuite, @@ -40,6 +42,7 @@ impl<'a> YamlTests<'a> { suite: TestSuite, len: usize, ) -> Self { + let path = path.to_slash_lossy(); Self { path, version, @@ -148,20 +151,17 @@ impl<'a> YamlTests<'a> { /// Whether the test should be skipped fn skip_test(&self, name: &str) -> bool { - let generated_name = { - let mut test_file_path = test_file_path(self.path).unwrap(); - test_file_path.set_extension(""); - let components = test_file_path - .components() - .filter_map(|c| match c { - Component::Normal(n) => Some(n.to_string_lossy().into_owned()), - _ => None, - }) - .collect::>(); - format!("generated::{}::tests::{}", components.join("::"), name) - }; - self.skip.tests.contains(&generated_name) + if self.skip.tests.contains_key(self.path.as_str()) { + let tests = self.skip.tests.get(self.path.as_str()); + + return match tests { + Some(t) => t.contains(name.to_string().borrow()), + None => true + } + } + + false } fn fn_impls( @@ -176,9 +176,9 @@ impl<'a> YamlTests<'a> { .iter() .map(|test_fn| { let name = test_fn.unique_name(&mut seen_names); - if self.skip_test(&name) { + if self.skip_test(test_fn.name()) { info!( - "skipping '{}' in {:?} because included in skip.yml", + "skipping '{}' in {} because it's included in skip.yml", &name, self.path, ); @@ -195,18 +195,18 @@ impl<'a> YamlTests<'a> { Step::Skip(s) => { skip = if s.skip_version(self.version) { info!( - "skipping '{}' in {:?} because version '{}' is met. {}", + "skipping '{}' in {} because version '{}' is met. {}", &name, - self.path, + &self.path, s.version(), s.reason() ); Some(s) } else if s.skip_features(&self.skip.features) { info!( - "skipping '{}' in {:?} because it needs features '{:?}' which are currently not implemented", + "skipping '{}' in {} because it needs features '{:?}' which are currently not implemented", &name, - self.path, + &self.path, s.features() ); Some(s) @@ -312,6 +312,11 @@ impl TestFn { } } + /// The function name as declared in yaml + pub fn name(&self) -> &str { + self.name.as_str() + } + /// some function descriptions are the same in YAML tests, which would result in /// duplicate generated test function names. Deduplicate by appending incrementing number pub fn unique_name(&self, seen_names: &mut HashSet) -> String { @@ -336,7 +341,7 @@ impl TestFn { #[derive(Deserialize)] struct GlobalSkip { features: Vec, - tests: Vec, + tests: BTreeMap>, } pub fn generate_tests_from_yaml( @@ -389,8 +394,8 @@ pub fn generate_tests_from_yaml( if &test_suite != suite { info!( - "skipping {:?}. compiling tests for {:?}", - relative_path, suite + "skipping {}. compiling tests for {:?}", + relative_path.to_slash_lossy(), suite ); continue; } @@ -401,8 +406,8 @@ pub fn generate_tests_from_yaml( let result = YamlLoader::load_from_str(&yaml); if result.is_err() { info!( - "skipping {:?}. cannot read as Yaml: {}", - relative_path, + "skipping {}. cannot read as Yaml struct: {}", + relative_path.to_slash_lossy(), result.err().unwrap().to_string() ); continue; @@ -449,7 +454,7 @@ pub fn generate_tests_from_yaml( //if there has been an Err in any step of the yaml test file, don't create a test for it match ok_or_accumulate(&results, 1) { Ok(_) => write_test_file(test, relative_path, generated_dir)?, - Err(e) => info!("skipping test file for {:?}\n{}", relative_path, e), + Err(e) => info!("skipping {} because\n{}", relative_path.to_slash_lossy(), e), } } } diff --git a/yaml_test_runner/src/step/comparison.rs b/yaml_test_runner/src/step/comparison.rs index 94d4cdc1..d0f10527 100644 --- a/yaml_test_runner/src/step/comparison.rs +++ b/yaml_test_runner/src/step/comparison.rs @@ -87,9 +87,9 @@ impl ToTokens for Comparison { let expr_ident = syn::Ident::from(expr.as_str()); let ident = syn::Ident::from(s); let op_ident = syn::Ident::from(op); - let message = "Expected value at {} to be numeric but is {}"; + let message = "Expected value at {} to be numeric but was {}"; let comparison_message = - "Expected value at {} to be {:?} {}, but is {}"; + "Expected value at {} to be {:?} {}, but was {}"; tokens.append(quote! { match &json#expr_ident { Value::Number(n) => { From c486a8e62e70cd66650139dee7e8bdb6eecb0b8d Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 19 May 2020 14:50:04 +1000 Subject: [PATCH 110/127] Pass expected values as serde_json::Value --- yaml_test_runner/src/step/match.rs | 38 ++++++++++++++----------- yaml_test_runner/tests/common/macros.rs | 6 ++-- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/yaml_test_runner/src/step/match.rs b/yaml_test_runner/src/step/match.rs index 4b36147a..cfafc518 100644 --- a/yaml_test_runner/src/step/match.rs +++ b/yaml_test_runner/src/step/match.rs @@ -49,22 +49,26 @@ impl ToTokens for Match { }); } } else { + let ident = syn::Ident::from(expr.as_str()); + // handle set values - let t = if s.starts_with('$') { - let t = s - .trim_start_matches('$') - .trim_start_matches('{') - .trim_end_matches('}'); - let ident = syn::Ident::from(t); - quote! { #ident.as_str().unwrap() } + if s.starts_with('$') { + let t = { + let s = s + .trim_start_matches('$') + .trim_start_matches('{') + .trim_end_matches('}'); + syn::Ident::from(s) + }; + + tokens.append(quote! { + assert_match!(json#ident, json!(#t)); + }); } else { - quote! { #s } + tokens.append(quote! { + assert_match!(json#ident, json!(#s)); + }) }; - - let ident = syn::Ident::from(expr.as_str()); - tokens.append(quote! { - assert_match!(json#ident, #t); - }) } } Yaml::Integer(i) => { @@ -84,7 +88,7 @@ impl ToTokens for Match { } else { let ident = syn::Ident::from(expr.as_str()); tokens.append(quote! { - assert_match!(json#ident, #f); + assert_match!(json#ident, json!(#f)); }); } } @@ -106,7 +110,7 @@ impl ToTokens for Match { } else { let ident = syn::Ident::from(expr.as_str()); tokens.append(quote! { - assert_match!(json#ident, #b); + assert_match!(json#ident, json!(#b)); }); } } @@ -130,12 +134,12 @@ impl ToTokens for Match { if self.expr.is_body() || self.expr.is_empty() { tokens.append(quote! { - assert_match!(json, #json); + assert_match!(json, json!(#json)); }); } else { let ident = syn::Ident::from(expr.as_str()); tokens.append(quote! { - assert_match!(json#ident, #json); + assert_match!(json#ident, json!(#json)); }); } } diff --git a/yaml_test_runner/tests/common/macros.rs b/yaml_test_runner/tests/common/macros.rs index 5d0df8a4..fe07a17a 100644 --- a/yaml_test_runner/tests/common/macros.rs +++ b/yaml_test_runner/tests/common/macros.rs @@ -50,9 +50,9 @@ macro_rules! assert_request_status_code { /// The second argument is converted to a [serde_json::Value] using the `json!` macro #[macro_export] macro_rules! assert_match { - ($expected:expr, $($actual:tt)+) => {{ - assert_eq!($expected, json!($($actual)+), - "expected value {} to match {:?} but was {:?}", stringify!($expected), json!($($actual)+), $expected + ($expected:expr, $actual:expr) => {{ + assert_eq!($expected, $actual, + "expected value {} to match {:?} but was {:?}", stringify!($expected), $actual, $expected ); }}; } From cf1529186de753b7bae61669b1f240b7fcbfb35b Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 19 May 2020 16:00:18 +1000 Subject: [PATCH 111/127] warnings assertion macros --- yaml_test_runner/src/step/do.rs | 8 ++------ yaml_test_runner/tests/common/macros.rs | 23 ++++++++++++++++++++++- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/yaml_test_runner/src/step/do.rs b/yaml_test_runner/src/step/do.rs index ef76dd6d..6e8dd517 100644 --- a/yaml_test_runner/src/step/do.rs +++ b/yaml_test_runner/src/step/do.rs @@ -82,17 +82,13 @@ impl Do { }); for warning in &self.warnings { tokens.append(quote! { - assert!( - warnings.iter().any(|w| w.contains(#warning)), - "expected warnings to contain '{}' but contained {:?}", - #warning, - &warnings); + assert_warnings_contain!(warnings, #warning); }); } } else if !self.allowed_warnings.is_empty() { tokens.append(quote! { let warnings: Vec<&str> = response.warning_headers().collect(); - assert!(warnings.is_empty(), "expected warnings to be empty but found {:?}", &warnings); + assert_warnings_is_empty!(warnings); }); } diff --git a/yaml_test_runner/tests/common/macros.rs b/yaml_test_runner/tests/common/macros.rs index fe07a17a..517374be 100644 --- a/yaml_test_runner/tests/common/macros.rs +++ b/yaml_test_runner/tests/common/macros.rs @@ -164,4 +164,25 @@ macro_rules! assert_is_true { v => {}, } }}; -} \ No newline at end of file +} + +/// Asserts that the deprecation warnings contain a given value +#[macro_export] +macro_rules! assert_warnings_contain { + ($warnings:expr, $expected:expr) => {{ + assert!( + $warnings.iter().any(|w| w.contains($expected)), + "expected warnings to contain '{}' but contained {:?}", + $expected, + &$warnings); + }}; +} + +/// Asserts that the deprecation warnings are empty +#[macro_export] +macro_rules! assert_warnings_is_empty { + ($warnings:expr) => {{ + assert!($warnings.is_empty(), "expected warnings to be empty but found {:?}", &$warnings); + }}; +} + From c77661585af2776fbf0465676527fb07fb633add Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 19 May 2020 17:29:12 +1000 Subject: [PATCH 112/127] emit yaml test name in log --- yaml_test_runner/src/generator.rs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index ba99aef2..e9f5b0cd 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -175,17 +175,18 @@ impl<'a> YamlTests<'a> { self.tests .iter() .map(|test_fn| { - let name = test_fn.unique_name(&mut seen_names); - if self.skip_test(test_fn.name()) { + let name = test_fn.name(); + let unique_name = test_fn.unique_name(&mut seen_names); + if self.skip_test(name) { info!( - "skipping '{}' in {} because it's included in skip.yml", - &name, + r#"skipping "{}" in {} because it's included in skip.yml"#, + name, self.path, ); return None; } - let fn_name = syn::Ident::from(name.as_str()); + let fn_name = syn::Ident::from(unique_name.as_str()); let mut body = Tokens::new(); let mut skip = Option::<&Skip>::None; let mut read_response = false; @@ -195,8 +196,8 @@ impl<'a> YamlTests<'a> { Step::Skip(s) => { skip = if s.skip_version(self.version) { info!( - "skipping '{}' in {} because version '{}' is met. {}", - &name, + r#"skipping "{}" in {} because version "{}" is met. {}"#, + name, &self.path, s.version(), s.reason() @@ -204,8 +205,8 @@ impl<'a> YamlTests<'a> { Some(s) } else if s.skip_features(&self.skip.features) { info!( - "skipping '{}' in {} because it needs features '{:?}' which are currently not implemented", - &name, + r#"skipping "{}" in {} because it needs features "{:?}" which are currently not implemented"#, + name, &self.path, s.features() ); From 401d2151a296ae58fc28f9e074e727aaf123409d Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 19 May 2020 17:29:30 +1000 Subject: [PATCH 113/127] skip tests that use numeric index into Object --- yaml_test_runner/skip.yml | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/yaml_test_runner/skip.yml b/yaml_test_runner/skip.yml index f671ae21..03ecf6eb 100644 --- a/yaml_test_runner/skip.yml +++ b/yaml_test_runner/skip.yml @@ -35,4 +35,21 @@ tests: oss/cat.indices/10_basic.yml: # this test fails as the regex needs a \n before the ending $ - - "Test cat indices using health status" \ No newline at end of file + - "Test cat indices using health status" + + oss/indices_shard_stores/10_basic.yml: + # uses number as a key into object. serde_json::Value expects a string key + - "basic index test" + - "multiple indices test" + + oss/indices_flush/10_basic.yml: + # uses number as a key into object. serde_json::Value expects a string key + - "Index synced flush rest test" + + oss/indices_segments/10_basic.yml: + # uses number as a key into object. serde_json::Value expects a string key + - "basic segments test" + + oss/indices_stats/12_level.yml: + # uses number as a key into object. serde_json::Value expects a string key + - "Level - shards" \ No newline at end of file From a1024b3547ec9478d8dc051ef3ab82793447578e Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 19 May 2020 17:46:17 +1000 Subject: [PATCH 114/127] clippy --- yaml_test_runner/src/bin/run.rs | 19 ++++++++----------- yaml_test_runner/src/regex.rs | 6 ++---- yaml_test_runner/src/step/comparison.rs | 2 +- .../src/step/transform_and_set.rs | 3 +-- 4 files changed, 12 insertions(+), 18 deletions(-) diff --git a/yaml_test_runner/src/bin/run.rs b/yaml_test_runner/src/bin/run.rs index 26b17109..9ef0da3c 100644 --- a/yaml_test_runner/src/bin/run.rs +++ b/yaml_test_runner/src/bin/run.rs @@ -108,18 +108,15 @@ fn main() -> Result<(), failure::Error> { let entries = fs::read_dir(&generated_dir)?; for entry in entries { if let Ok(e) = entry { - match e.file_type() { - Ok(f) => { - if e.file_name() != "common" { - if f.is_dir() { - fs::remove_dir_all(e.path())?; - } else if f.is_file() { - fs::remove_file(e.path())?; - } + if let Ok(f) = e.file_type() { + if e.file_name() != "common" { + if f.is_dir() { + fs::remove_dir_all(e.path())?; + } else if f.is_file() { + fs::remove_file(e.path())?; } - }, - Err(_) => () - }; + } + } } } } diff --git a/yaml_test_runner/src/regex.rs b/yaml_test_runner/src/regex.rs index 2a48c23f..fd4bebf3 100644 --- a/yaml_test_runner/src/regex.rs +++ b/yaml_test_runner/src/regex.rs @@ -1,4 +1,3 @@ -use lazy_static; use regex::{Captures, Regex}; lazy_static! { @@ -46,7 +45,6 @@ pub fn replace_i64>(s: S) -> String { INT_REGEX .replace_all(s.as_ref(), |c: &Captures| match &c[2].parse::() { Ok(i) if *i > i32::max_value() as i64 => format!("{}{}i64{}", &c[1], &c[2], &c[3]), - _ => format!("{}", &c[0]), - }) - .into_owned() + _ => c[0].to_string(), + }).into_owned() } diff --git a/yaml_test_runner/src/step/comparison.rs b/yaml_test_runner/src/step/comparison.rs index d0f10527..592e7012 100644 --- a/yaml_test_runner/src/step/comparison.rs +++ b/yaml_test_runner/src/step/comparison.rs @@ -4,7 +4,7 @@ use super::Step; use crate::step::Expr; use yaml_rust::Yaml; -pub const OPERATORS: [&'static str; 4] = ["lt", "lte", "gt", "gte"]; +pub const OPERATORS: [&str; 4] = ["lt", "lte", "gt", "gte"]; pub struct Comparison { pub(crate) expr: Expr, diff --git a/yaml_test_runner/src/step/transform_and_set.rs b/yaml_test_runner/src/step/transform_and_set.rs index 1749400b..ea59a81e 100644 --- a/yaml_test_runner/src/step/transform_and_set.rs +++ b/yaml_test_runner/src/step/transform_and_set.rs @@ -34,8 +34,7 @@ impl From<&str> for Transformation { let mut function = None; let mut exprs = Vec::new(); let mut value = String::new(); - let mut chars = t.chars(); - while let Some(ch) = chars.next() { + for ch in t.chars() { match ch { '#' => { continue; From b7a55a49ed77879343b76e5a64290dea0740c0d3 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 19 May 2020 17:48:52 +1000 Subject: [PATCH 115/127] correct test file path --- yaml_test_runner/skip.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yaml_test_runner/skip.yml b/yaml_test_runner/skip.yml index 03ecf6eb..58671167 100644 --- a/yaml_test_runner/skip.yml +++ b/yaml_test_runner/skip.yml @@ -37,19 +37,19 @@ tests: # this test fails as the regex needs a \n before the ending $ - "Test cat indices using health status" - oss/indices_shard_stores/10_basic.yml: + oss/indices.shard_stores/10_basic.yml: # uses number as a key into object. serde_json::Value expects a string key - "basic index test" - "multiple indices test" - oss/indices_flush/10_basic.yml: + oss/indices.flush/10_basic.yml: # uses number as a key into object. serde_json::Value expects a string key - "Index synced flush rest test" - oss/indices_segments/10_basic.yml: + oss/indices.segments/10_basic.yml: # uses number as a key into object. serde_json::Value expects a string key - "basic segments test" - oss/indices_stats/12_level.yml: + oss/indices.stats/12_level.yml: # uses number as a key into object. serde_json::Value expects a string key - "Level - shards" \ No newline at end of file From c0dcefefcedb98806f1fa14a471613c272b413b6 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 19 May 2020 17:49:49 +1000 Subject: [PATCH 116/127] cargo fmt --- yaml_test_runner/src/bin/run.rs | 7 +- yaml_test_runner/src/generator.rs | 18 ++--- yaml_test_runner/src/lib.rs | 4 +- yaml_test_runner/src/regex.rs | 3 +- yaml_test_runner/src/step/do.rs | 4 +- yaml_test_runner/src/step/match.rs | 2 +- yaml_test_runner/tests/common/macros.rs | 93 +++++++++++++++++++------ yaml_test_runner/tests/common/mod.rs | 2 +- yaml_test_runner/tests/mod.rs | 2 +- 9 files changed, 97 insertions(+), 38 deletions(-) diff --git a/yaml_test_runner/src/bin/run.rs b/yaml_test_runner/src/bin/run.rs index 9ef0da3c..258ade9e 100644 --- a/yaml_test_runner/src/bin/run.rs +++ b/yaml_test_runner/src/bin/run.rs @@ -1,7 +1,7 @@ #[macro_use] extern crate log; -extern crate simple_logger; extern crate api_generator; +extern crate simple_logger; use clap::{App, Arg}; use log::Level; @@ -9,7 +9,10 @@ use std::fs; use std::path::PathBuf; use std::process::exit; -use yaml_test_runner::{generator::{self, TestSuite}, github}; +use yaml_test_runner::{ + generator::{self, TestSuite}, + github, +}; fn main() -> Result<(), failure::Error> { simple_logger::init_with_level(Level::Info).unwrap(); diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index e9f5b0cd..b90c817e 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -3,17 +3,17 @@ use quote::{ToTokens, Tokens}; use crate::step::*; use api_generator::generator::Api; +use path_slash::PathExt; use regex::Regex; use semver::Version; use serde::Deserialize; -use std::collections::{HashSet, BTreeMap}; +use std::borrow::Borrow; +use std::collections::{BTreeMap, HashSet}; use std::fs; use std::fs::{File, OpenOptions}; use std::io::Write; use std::path::{Component, Path, PathBuf}; use yaml_rust::{Yaml, YamlLoader}; -use std::borrow::Borrow; -use path_slash::PathExt; /// The test suite to compile #[derive(Debug, PartialEq)] @@ -151,14 +151,13 @@ impl<'a> YamlTests<'a> { /// Whether the test should be skipped fn skip_test(&self, name: &str) -> bool { - if self.skip.tests.contains_key(self.path.as_str()) { let tests = self.skip.tests.get(self.path.as_str()); return match tests { Some(t) => t.contains(name.to_string().borrow()), - None => true - } + None => true, + }; } false @@ -396,7 +395,8 @@ pub fn generate_tests_from_yaml( if &test_suite != suite { info!( "skipping {}. compiling tests for {:?}", - relative_path.to_slash_lossy(), suite + relative_path.to_slash_lossy(), + suite ); continue; } @@ -455,7 +455,9 @@ pub fn generate_tests_from_yaml( //if there has been an Err in any step of the yaml test file, don't create a test for it match ok_or_accumulate(&results, 1) { Ok(_) => write_test_file(test, relative_path, generated_dir)?, - Err(e) => info!("skipping {} because\n{}", relative_path.to_slash_lossy(), e), + Err(e) => { + info!("skipping {} because\n{}", relative_path.to_slash_lossy(), e) + } } } } diff --git a/yaml_test_runner/src/lib.rs b/yaml_test_runner/src/lib.rs index 88aa3429..bf0f1f41 100644 --- a/yaml_test_runner/src/lib.rs +++ b/yaml_test_runner/src/lib.rs @@ -12,6 +12,6 @@ extern crate quote; extern crate api_generator; pub mod generator; -pub mod step; pub mod github; -pub mod regex; \ No newline at end of file +pub mod regex; +pub mod step; diff --git a/yaml_test_runner/src/regex.rs b/yaml_test_runner/src/regex.rs index fd4bebf3..c6f8663f 100644 --- a/yaml_test_runner/src/regex.rs +++ b/yaml_test_runner/src/regex.rs @@ -46,5 +46,6 @@ pub fn replace_i64>(s: S) -> String { .replace_all(s.as_ref(), |c: &Captures| match &c[2].parse::() { Ok(i) if *i > i32::max_value() as i64 => format!("{}{}i64{}", &c[1], &c[2], &c[3]), _ => c[0].to_string(), - }).into_owned() + }) + .into_owned() } diff --git a/yaml_test_runner/src/step/do.rs b/yaml_test_runner/src/step/do.rs index 6e8dd517..14e02a31 100644 --- a/yaml_test_runner/src/step/do.rs +++ b/yaml_test_runner/src/step/do.rs @@ -104,8 +104,8 @@ impl Do { match &self.api_call.ignore { Some(i) => tokens.append(quote! { - assert_response_success_or!(response, #i); - }), + assert_response_success_or!(response, #i); + }), None => (), } diff --git a/yaml_test_runner/src/step/match.rs b/yaml_test_runner/src/step/match.rs index cfafc518..e813470d 100644 --- a/yaml_test_runner/src/step/match.rs +++ b/yaml_test_runner/src/step/match.rs @@ -1,8 +1,8 @@ use super::Step; +use crate::regex::*; use crate::step::{clean_regex, Expr}; use quote::{ToTokens, Tokens}; use yaml_rust::{Yaml, YamlEmitter}; -use crate::regex::*; pub struct Match { pub expr: Expr, diff --git a/yaml_test_runner/tests/common/macros.rs b/yaml_test_runner/tests/common/macros.rs index 517374be..a76cc8bb 100644 --- a/yaml_test_runner/tests/common/macros.rs +++ b/yaml_test_runner/tests/common/macros.rs @@ -42,7 +42,9 @@ macro_rules! assert_request_status_code { let status_code = $status_code.as_u16(); assert!( status_code >= 400 && status_code < 600, - "expected status code in range 400-599 but was {}", status_code); + "expected status code in range 400-599 but was {}", + status_code + ); }}; } @@ -51,8 +53,13 @@ macro_rules! assert_request_status_code { #[macro_export] macro_rules! assert_match { ($expected:expr, $actual:expr) => {{ - assert_eq!($expected, $actual, - "expected value {} to match {:?} but was {:?}", stringify!($expected), $actual, $expected + assert_eq!( + $expected, + $actual, + "expected value {} to match {:?} but was {:?}", + stringify!($expected), + $actual, + $expected ); }}; } @@ -75,7 +82,12 @@ macro_rules! assert_numeric_match { #[macro_export] macro_rules! assert_null { ($expected:expr) => {{ - assert!($expected.is_null(), "expected value {} to be null but was {:?}", stringify!($expected), $expected); + assert!( + $expected.is_null(), + "expected value {} to be null but was {:?}", + stringify!($expected), + $expected + ); }}; } @@ -84,8 +96,7 @@ macro_rules! assert_null { #[macro_export] macro_rules! assert_regex_match { ($expected:expr, $regex:expr) => {{ - let regex = regex::RegexBuilder::new($regex) - .build()?; + let regex = regex::RegexBuilder::new($regex).build()?; assert!( regex.is_match($expected), "expected value {} to match regex\n\n{}\n\nbut was\n\n{}", @@ -122,7 +133,7 @@ macro_rules! assert_length { Value::String(s) => s.len(), Value::Array(a) => a.len(), Value::Object(o) => o.len(), - v => panic!("Cannot get length from {:?}", v) + v => panic!("Cannot get length from {:?}", v), }; assert_eq!( @@ -142,11 +153,30 @@ macro_rules! assert_is_false { ($expr:expr) => {{ let expr_string = stringify!($expr); match $expr { - Value::Null => {}, - Value::Bool(b) => assert_eq!(*b, false, "expected value at {} to be false but was {}", expr_string, b), - Value::Number(n) => assert_eq!(n.as_f64().unwrap(), 0.0, "expected value at {} to be false (0) but was {}", expr_string, n.as_f64().unwrap()), - Value::String(s) => assert!(s.is_empty(), "expected value at {} to be false (empty) but was {}", expr_string, &s), - v => assert!(false, "expected value at {} to be false but was {:?}", expr_string, &v), + Value::Null => {} + Value::Bool(b) => assert_eq!( + *b, false, + "expected value at {} to be false but was {}", + expr_string, b + ), + Value::Number(n) => assert_eq!( + n.as_f64().unwrap(), + 0.0, + "expected value at {} to be false (0) but was {}", + expr_string, + n.as_f64().unwrap() + ), + Value::String(s) => assert!( + s.is_empty(), + "expected value at {} to be false (empty) but was {}", + expr_string, + &s + ), + v => assert!( + false, + "expected value at {} to be false but was {:?}", + expr_string, &v + ), } }}; } @@ -157,11 +187,30 @@ macro_rules! assert_is_true { ($expr:expr) => {{ let expr_string = stringify!($expr); match $expr { - Value::Null => assert!(false, "expected value at {} to be true (not null) but was null", expr_string), - Value::Bool(b) => assert!(*b, "expected value at {} to be true but was false", expr_string), - Value::Number(n) => assert_ne!(n.as_f64().unwrap(), 0.0, "expected value at {} to be true (not 0) but was {}", expr_string, n.as_f64().unwrap()), - Value::String(s) => assert!(!s.is_empty(), "expected value at {} to be true (not empty) but was {}", expr_string, &s), - v => {}, + Value::Null => assert!( + false, + "expected value at {} to be true (not null) but was null", + expr_string + ), + Value::Bool(b) => assert!( + *b, + "expected value at {} to be true but was false", + expr_string + ), + Value::Number(n) => assert_ne!( + n.as_f64().unwrap(), + 0.0, + "expected value at {} to be true (not 0) but was {}", + expr_string, + n.as_f64().unwrap() + ), + Value::String(s) => assert!( + !s.is_empty(), + "expected value at {} to be true (not empty) but was {}", + expr_string, + &s + ), + v => {} } }}; } @@ -174,7 +223,8 @@ macro_rules! assert_warnings_contain { $warnings.iter().any(|w| w.contains($expected)), "expected warnings to contain '{}' but contained {:?}", $expected, - &$warnings); + &$warnings + ); }}; } @@ -182,7 +232,10 @@ macro_rules! assert_warnings_contain { #[macro_export] macro_rules! assert_warnings_is_empty { ($warnings:expr) => {{ - assert!($warnings.is_empty(), "expected warnings to be empty but found {:?}", &$warnings); + assert!( + $warnings.is_empty(), + "expected warnings to be empty but found {:?}", + &$warnings + ); }}; } - diff --git a/yaml_test_runner/tests/common/mod.rs b/yaml_test_runner/tests/common/mod.rs index 4bc096bb..94f8b2d5 100644 --- a/yaml_test_runner/tests/common/mod.rs +++ b/yaml_test_runner/tests/common/mod.rs @@ -2,4 +2,4 @@ pub mod macros; pub mod client; pub mod transform; -pub mod util; \ No newline at end of file +pub mod util; diff --git a/yaml_test_runner/tests/mod.rs b/yaml_test_runner/tests/mod.rs index 2c3317ff..c4e7158e 100644 --- a/yaml_test_runner/tests/mod.rs +++ b/yaml_test_runner/tests/mod.rs @@ -1,3 +1,3 @@ #[macro_use] pub mod common; -pub mod oss; \ No newline at end of file +pub mod oss; From a5bc0fd9a9eea29640a8124f6cac0fd910768c1a Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Wed, 20 May 2020 12:23:15 +1000 Subject: [PATCH 117/127] macros for comparison --- yaml_test_runner/src/step/comparison.rs | 37 ++------------------- yaml_test_runner/tests/common/macros.rs | 44 +++++++++++++++++++++++++ yaml_test_runner/tests/mod.rs | 2 +- 3 files changed, 47 insertions(+), 36 deletions(-) diff --git a/yaml_test_runner/src/step/comparison.rs b/yaml_test_runner/src/step/comparison.rs index 592e7012..dbde059c 100644 --- a/yaml_test_runner/src/step/comparison.rs +++ b/yaml_test_runner/src/step/comparison.rs @@ -39,24 +39,8 @@ impl Comparison { fn assert(&self, t: T, expr: &str, op: &str, tokens: &mut Tokens) { let ident = syn::Ident::from(expr); let op_ident = syn::Ident::from(op); - let message = "Expected value at {} to be numeric but is {}"; - let comparison_message = "Expected value at {} to be {:?} {}, but is {}"; tokens.append(quote! { - match &json#ident { - Value::Number(n) => { - match n.as_i64() { - Some(i) => assert!(i #op_ident #t as i64, #comparison_message, #expr, #op, #t, i), - None => match n.as_f64() { - Some(f) => assert!(f #op_ident #t as f64, #comparison_message, #expr, #op, #t, f), - None => match n.as_u64() { - Some(u) => assert!(u #op_ident #t as u64, #comparison_message, #expr, #op, #t, u), - None => assert!(false, #message, #expr, &n) - } - } - } - } - v => assert!(false, #message, #expr, &v), - } + assert_comparison!(&json#ident, #op_ident #t); }); } } @@ -87,25 +71,8 @@ impl ToTokens for Comparison { let expr_ident = syn::Ident::from(expr.as_str()); let ident = syn::Ident::from(s); let op_ident = syn::Ident::from(op); - let message = "Expected value at {} to be numeric but was {}"; - let comparison_message = - "Expected value at {} to be {:?} {}, but was {}"; tokens.append(quote! { - match &json#expr_ident { - Value::Number(n) => { - match n.as_i64() { - Some(i) => assert!(i #op_ident #ident.as_i64().unwrap(), #comparison_message, #expr, #op, #ident.as_i64().unwrap(), i), - None => match n.as_f64() { - Some(f) => assert!(f #op_ident #ident.as_f64().unwrap(), #comparison_message, #expr, #op, #ident.as_f64().unwrap(), f), - None => match n.as_u64() { - Some(u) => assert!(u #op_ident #ident.as_u64().unwrap(), #comparison_message, #expr, #op, #ident.as_u64().unwrap(), u), - None => assert!(false, #message, #expr, &n) - } - } - } - } - v => assert!(false, #message, #expr, &v), - } + assert_comparison_from_set_value!(&json#expr_ident, #op_ident #ident); }); } _ => panic!("Expected i64 or f64 but found {:?}", &self.value), diff --git a/yaml_test_runner/tests/common/macros.rs b/yaml_test_runner/tests/common/macros.rs index a76cc8bb..6cf62d7e 100644 --- a/yaml_test_runner/tests/common/macros.rs +++ b/yaml_test_runner/tests/common/macros.rs @@ -239,3 +239,47 @@ macro_rules! assert_warnings_is_empty { ); }}; } + +/// Asserts that the comparison is true +#[macro_export] +macro_rules! assert_comparison { + ($expr:expr, $($comparison:tt)+) => {{ + match $expr { + Value::Number(n) => { + match n.as_i64() { + Some(i) => assert!(i $($comparison)+ as i64, "Expected value {} to be {} but was {}", stringify!($expr), stringify!($($comparison)+ as i64), i), + None => match n.as_f64() { + Some(f) => assert!(f $($comparison)+ as f64, "Expected value {} to be {} but was {}", stringify!($expr), stringify!($($comparison)+ as f64), f), + None => match n.as_u64() { + Some(u) => assert!(u $($comparison)+ as u64, "Expected value {} to be {} but was {}", stringify!($expr), stringify!($($comparison)+ as u64), u), + None => assert!(false, "Expected value {} to be numeric but was {:?}", stringify!($expr), &n) + } + } + } + } + v => assert!(false, "Expected value {} to be numeric but was {:?}", stringify!($expr), &v), + } + }}; +} + +/// Asserts that the comparison is true when comparing against a "set" value +#[macro_export] +macro_rules! assert_comparison_from_set_value { + ($expr:expr, $($comparison:tt)+) => {{ + match $expr { + Value::Number(n) => { + match n.as_i64() { + Some(i) => assert!(i $($comparison)+.as_i64().unwrap(), "Expected value {} to be {} but was {}", stringify!($expr), stringify!($($comparison)+.as_i64().unwrap()), i), + None => match n.as_f64() { + Some(f) => assert!(f $($comparison)+.as_f64().unwrap(), "Expected value {} to be {} but was {}", stringify!($expr), stringify!($($comparison)+.as_f64().unwrap()), f), + None => match n.as_u64() { + Some(u) => assert!(u $($comparison)+.as_u64().unwrap(), "Expected value {} to be {} but was {}", stringify!($expr), stringify!($($comparison)+.as_u64().unwrap()), u), + None => assert!(false, "Expected value {} to be numeric but was {:?}", stringify!($expr), &n) + } + } + } + } + v => assert!(false, "Expected value {} to be numeric but was {:?}", stringify!($expr), &v), + } + }}; +} diff --git a/yaml_test_runner/tests/mod.rs b/yaml_test_runner/tests/mod.rs index c4e7158e..2c3317ff 100644 --- a/yaml_test_runner/tests/mod.rs +++ b/yaml_test_runner/tests/mod.rs @@ -1,3 +1,3 @@ #[macro_use] pub mod common; -pub mod oss; +pub mod oss; \ No newline at end of file From af12cb265de079cc4fa1f7f8848a2ab319b036d9 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 21 May 2020 10:08:24 +1000 Subject: [PATCH 118/127] stop tracking tests/mord.rs Generated file --- .gitignore | 1 + yaml_test_runner/tests/mod.rs | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 yaml_test_runner/tests/mod.rs diff --git a/.gitignore b/.gitignore index 410256a7..07d24d90 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ Cargo.lock yaml_test_runner/yaml/ yaml_test_runner/tests/oss yaml_test_runner/tests/xpack +yaml_test_runner/tests/mod.rs diff --git a/yaml_test_runner/tests/mod.rs b/yaml_test_runner/tests/mod.rs deleted file mode 100644 index 2c3317ff..00000000 --- a/yaml_test_runner/tests/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -#[macro_use] -pub mod common; -pub mod oss; \ No newline at end of file From 83bda3327ba3a5466e51e8777e3709a59b1037d5 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 21 May 2020 10:08:45 +1000 Subject: [PATCH 119/127] Implement contains --- yaml_test_runner/skip.yml | 1 - yaml_test_runner/src/generator.rs | 6 +- yaml_test_runner/src/step/contains.rs | 75 +++++++++++++++++++++++++ yaml_test_runner/src/step/match.rs | 21 ++----- yaml_test_runner/src/step/mod.rs | 34 +++++++++-- yaml_test_runner/tests/common/macros.rs | 34 +++++++++++ 6 files changed, 147 insertions(+), 24 deletions(-) create mode 100644 yaml_test_runner/src/step/contains.rs diff --git a/yaml_test_runner/skip.yml b/yaml_test_runner/skip.yml index 58671167..fe67b5c2 100644 --- a/yaml_test_runner/skip.yml +++ b/yaml_test_runner/skip.yml @@ -6,7 +6,6 @@ # features not yet implemented features: - node_selector - - contains - stash_path_replace - embedded_stash_key diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index b90c817e..2f011043 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -119,7 +119,7 @@ impl<'a> YamlTests<'a> { .collect(); quote! { - #![allow(unused_imports, unused_variables)] + #![allow(unused_imports, unused_variables, dead_code)] use crate::common::{client, macros, transform, util}; use elasticsearch::*; use elasticsearch::http::{ @@ -241,6 +241,10 @@ impl<'a> YamlTests<'a> { read_response = Self::read_response(read_response,&mut body); c.to_tokens(&mut body); }, + Step::Contains(c) => { + read_response = Self::read_response(read_response,&mut body); + c.to_tokens(&mut body); + }, Step::TransformAndSet(t) => { read_response = Self::read_response(read_response,&mut body); t.to_tokens(&mut body); diff --git a/yaml_test_runner/src/step/contains.rs b/yaml_test_runner/src/step/contains.rs new file mode 100644 index 00000000..01798f57 --- /dev/null +++ b/yaml_test_runner/src/step/contains.rs @@ -0,0 +1,75 @@ +use quote::{ToTokens, Tokens}; + +use super::Step; +use crate::step::{Expr, json_string_from_yaml}; +use yaml_rust::Yaml; + +pub struct Contains { + expr: Expr, + value: Yaml, +} + +impl From for Step { + fn from(contains: Contains) -> Self { + Step::Contains(contains) + } +} + +impl Contains { + pub fn try_parse(yaml: &Yaml) -> Result { + let hash = yaml + .as_hash() + .ok_or_else(|| failure::err_msg(format!("expected hash but found {:?}", yaml)))?; + + let (k, v) = hash.iter().next().unwrap(); + let expr = k.as_str().unwrap().trim(); + Ok(Contains { + expr: expr.into(), + value: v.clone(), + }) + } +} + +impl ToTokens for Contains { + fn to_tokens(&self, tokens: &mut Tokens) { + let expr = self.expr.expression(); + let ident = syn::Ident::from(expr.as_str()); + + match &self.value { + Yaml::Real(r) => { + let f = r.parse::().unwrap(); + tokens.append(quote! { + assert_contains!(json#ident, json!(#f)); + }); + }, + Yaml::Integer(i) => { + tokens.append(quote! { + assert_contains!(json#ident, json!(#i)); + }); + }, + Yaml::String(s) => { + tokens.append(quote! { + assert_contains!(json#ident, json!(#s)); + }); + }, + Yaml::Boolean(b) => { + tokens.append(quote! { + assert_contains!(json#ident, json!(#b)); + }); + }, + yaml if yaml.is_array() || yaml.as_hash().is_some() => { + let json = { + let s = json_string_from_yaml(yaml); + syn::Ident::from(s) + }; + + tokens.append(quote! { + assert_contains!(json#ident, json!(#json)); + }); + }, + yaml => { + panic!("Bad yaml value {:?}", &yaml); + } + } + } +} \ No newline at end of file diff --git a/yaml_test_runner/src/step/match.rs b/yaml_test_runner/src/step/match.rs index e813470d..0e48ba7d 100644 --- a/yaml_test_runner/src/step/match.rs +++ b/yaml_test_runner/src/step/match.rs @@ -1,8 +1,7 @@ use super::Step; -use crate::regex::*; -use crate::step::{clean_regex, Expr}; +use crate::step::{clean_regex, Expr, json_string_from_yaml}; use quote::{ToTokens, Tokens}; -use yaml_rust::{Yaml, YamlEmitter}; +use yaml_rust::Yaml; pub struct Match { pub expr: Expr, @@ -115,21 +114,9 @@ impl ToTokens for Match { } } yaml if yaml.is_array() || yaml.as_hash().is_some() => { - let mut s = String::new(); - { - let mut emitter = YamlEmitter::new(&mut s); - emitter.dump(yaml).unwrap(); - } - - let value: serde_json::Value = serde_yaml::from_str(&s).unwrap(); - let json = { - let mut json = value.to_string(); - json = replace_set_quoted_delimited(json); - json = replace_set_delimited(json); - json = replace_set(json); - json = replace_i64(json); - syn::Ident::from(json) + let s = json_string_from_yaml(yaml); + syn::Ident::from(s) }; if self.expr.is_body() || self.expr.is_empty() { diff --git a/yaml_test_runner/src/step/mod.rs b/yaml_test_runner/src/step/mod.rs index a2b8b1a0..f9743645 100644 --- a/yaml_test_runner/src/step/mod.rs +++ b/yaml_test_runner/src/step/mod.rs @@ -1,8 +1,10 @@ use api_generator::generator::Api; use std::fmt::Write; -use yaml_rust::Yaml; +use yaml_rust::{Yaml, YamlEmitter}; +use crate::regex::*; mod comparison; +mod contains; mod r#do; mod is_false; mod is_true; @@ -12,6 +14,7 @@ mod set; mod skip; mod transform_and_set; pub use comparison::{Comparison, OPERATORS}; +pub use contains::*; pub use is_false::*; pub use is_true::*; pub use length::*; @@ -26,12 +29,12 @@ pub fn parse_steps(api: &Api, steps: &[Yaml]) -> Result, failure::Erro for step in steps { let hash = step .as_hash() - .ok_or_else(|| failure::err_msg(format!("Expected hash but found {:?}", step)))?; + .ok_or_else(|| failure::err_msg(format!("expected hash but found {:?}", step)))?; let (key, value) = { let (k, yaml) = hash.iter().next().unwrap(); let key = k.as_str().ok_or_else(|| { - failure::err_msg(format!("Expected string key but found {:?}", k)) + failure::err_msg(format!("expected string key but found {:?}", k)) })?; (key, yaml) @@ -58,7 +61,10 @@ pub fn parse_steps(api: &Api, steps: &[Yaml]) -> Result, failure::Erro let m = Match::try_parse(value)?; parsed_steps.push(m.into()); } - "contains" => {} + "contains" => { + let c = Contains::try_parse(value)?; + parsed_steps.push(c.into()); + } "is_true" => { let e = IsTrue::try_parse(value)?; parsed_steps.push(e.into()) @@ -186,6 +192,7 @@ pub enum Step { IsTrue(IsTrue), IsFalse(IsFalse), Comparison(Comparison), + Contains(Contains), TransformAndSet(TransformAndSet), } @@ -222,7 +229,7 @@ pub fn ok_or_accumulate( } } -// cleans up a regex as specified in YAML to one that will work with the regex crate. +/// cleans up a regex as specified in YAML to one that will work with the regex crate. pub fn clean_regex>(s: S) -> String { s.as_ref() .trim() @@ -234,3 +241,20 @@ pub fn clean_regex>(s: S) -> String { .replace("\\'", "'") .replace("\\`", "`") } + +pub fn json_string_from_yaml(yaml: &Yaml) -> String { + let mut s = String::new(); + { + let mut emitter = YamlEmitter::new(&mut s); + emitter.dump(yaml).unwrap(); + } + + let value: serde_json::Value = serde_yaml::from_str(&s).unwrap(); + + let mut json = value.to_string(); + json = replace_set_quoted_delimited(json); + json = replace_set_delimited(json); + json = replace_set(json); + json = replace_i64(json); + json +} diff --git a/yaml_test_runner/tests/common/macros.rs b/yaml_test_runner/tests/common/macros.rs index 6cf62d7e..03997736 100644 --- a/yaml_test_runner/tests/common/macros.rs +++ b/yaml_test_runner/tests/common/macros.rs @@ -283,3 +283,37 @@ macro_rules! assert_comparison_from_set_value { } }}; } + +/// Asserts that the passed [serde_json::Value::Array] contains the second argument. +#[macro_export] +macro_rules! assert_contains { + ($expr:expr, $value:expr) => {{ + if !$expr.is_array() { + assert!(false, "expected {} to be an array but was {:?}", stringify!($expr), &$expr); + } + + let arr = $expr.as_array().unwrap(); + + // when dealing with a serde_json::Value::Object, the $value may only be a partial object + // such that equality can't be used. In this case, we need to assert that there is one + // object in the array that has all the keys and values of $value + if $value.is_object() { + let vv = $value.clone(); + let o = vv.as_object().unwrap(); + assert!(arr.iter() + .filter_map(serde_json::Value::as_object) + .any(|ao| o.iter().all(|(key, value)| ao.get(key).map_or(false, |v| *value == *v))), + "expected value {} to contain {:?} but contained {:?}", + stringify!($expr), + &vv, + &arr); + } else { + assert!( + arr.contains(&$value), + "expected value {} to contain {:?} but contained {:?}", + stringify!($expr), + &$value, + &arr); + } + }}; +} From 7c566cefe03e401b9352f40ed58e12c819f5cce3 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 21 May 2020 10:58:13 +1000 Subject: [PATCH 120/127] only change file names that start with numerics --- yaml_test_runner/src/generator.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 2f011043..0f0a6a12 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -521,16 +521,19 @@ fn test_file_path(relative_path: &Path) -> Result { // directories and files will form the module names so ensure they're valid module names let clean: String = relative .to_string_lossy() - .into_owned() .replace(".", "_") .replace("-", "_"); relative = PathBuf::from(clean); + + let file_name = relative.file_name().unwrap().to_string_lossy().into_owned(); // modules can't start with a number so prefix with underscore - relative.set_file_name(format!( - "_{}", - &relative.file_name().unwrap().to_string_lossy().into_owned() - )); + if file_name.starts_with(char::is_numeric) { + relative.set_file_name(format!( + "_{}", + file_name + )); + } Ok(relative) } From 68e9c2ec0b0dc3b04a5611797bdc8434edbf81a8 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 21 May 2020 11:12:37 +1000 Subject: [PATCH 121/127] make yaml_text_runner a binary --- yaml_test_runner/src/lib.rs | 17 ----------------- yaml_test_runner/src/{bin/run.rs => main.rs} | 17 ++++++++++++----- yaml_test_runner/src/regex.rs | 3 ++- 3 files changed, 14 insertions(+), 23 deletions(-) delete mode 100644 yaml_test_runner/src/lib.rs rename yaml_test_runner/src/{bin/run.rs => main.rs} (96%) diff --git a/yaml_test_runner/src/lib.rs b/yaml_test_runner/src/lib.rs deleted file mode 100644 index bf0f1f41..00000000 --- a/yaml_test_runner/src/lib.rs +++ /dev/null @@ -1,17 +0,0 @@ -#[macro_use] -extern crate log; -extern crate simple_logger; - -#[macro_use] -extern crate lazy_static; - -extern crate serde_json; - -#[macro_use] -extern crate quote; -extern crate api_generator; - -pub mod generator; -pub mod github; -pub mod regex; -pub mod step; diff --git a/yaml_test_runner/src/bin/run.rs b/yaml_test_runner/src/main.rs similarity index 96% rename from yaml_test_runner/src/bin/run.rs rename to yaml_test_runner/src/main.rs index 258ade9e..0cf1cd4b 100644 --- a/yaml_test_runner/src/bin/run.rs +++ b/yaml_test_runner/src/main.rs @@ -1,6 +1,11 @@ +extern crate api_generator; +#[macro_use] +extern crate lazy_static; #[macro_use] extern crate log; -extern crate api_generator; +#[macro_use] +extern crate quote; + extern crate simple_logger; use clap::{App, Arg}; @@ -9,10 +14,12 @@ use std::fs; use std::path::PathBuf; use std::process::exit; -use yaml_test_runner::{ - generator::{self, TestSuite}, - github, -}; +mod generator; +mod github; +mod regex; +mod step; + +use generator::TestSuite; fn main() -> Result<(), failure::Error> { simple_logger::init_with_level(Level::Info).unwrap(); diff --git a/yaml_test_runner/src/regex.rs b/yaml_test_runner/src/regex.rs index c6f8663f..88233d68 100644 --- a/yaml_test_runner/src/regex.rs +++ b/yaml_test_runner/src/regex.rs @@ -1,4 +1,5 @@ -use regex::{Captures, Regex}; +use ::regex::{Captures, Regex}; +use lazy_static; lazy_static! { // replace usages of "$.*" with the captured value From 317a397b768a95dd039c31e69eb8a1545cfcd264 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 21 May 2020 11:20:14 +1000 Subject: [PATCH 122/127] Add license headers --- yaml_test_runner/src/generator.rs | 51 ++++++++++++++----- yaml_test_runner/src/github.rs | 18 +++++++ yaml_test_runner/src/main.rs | 18 +++++++ yaml_test_runner/src/regex.rs | 18 +++++++ yaml_test_runner/src/step/comparison.rs | 18 +++++++ yaml_test_runner/src/step/contains.rs | 18 +++++++ yaml_test_runner/src/step/do.rs | 18 +++++++ yaml_test_runner/src/step/is_false.rs | 18 +++++++ yaml_test_runner/src/step/is_true.rs | 19 ++++++- yaml_test_runner/src/step/length.rs | 19 ++++++- yaml_test_runner/src/step/match.rs | 18 +++++++ yaml_test_runner/src/step/mod.rs | 18 +++++++ yaml_test_runner/src/step/set.rs | 18 +++++++ yaml_test_runner/src/step/skip.rs | 18 +++++++ .../src/step/transform_and_set.rs | 19 ++++++- yaml_test_runner/tests/common/client.rs | 18 +++++++ yaml_test_runner/tests/common/macros.rs | 18 +++++++ yaml_test_runner/tests/common/mod.rs | 18 +++++++ yaml_test_runner/tests/common/transform.rs | 18 +++++++ yaml_test_runner/tests/common/util.rs | 18 +++++++ 20 files changed, 380 insertions(+), 16 deletions(-) diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 0f0a6a12..aaac5d65 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -1,6 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ use inflector::Inflector; use quote::{ToTokens, Tokens}; - use crate::step::*; use api_generator::generator::Api; use path_slash::PathExt; @@ -550,23 +567,31 @@ fn write_test_file( fs::create_dir_all(&path.parent().unwrap())?; let mut file = File::create(&path)?; file.write_all( - "// ----------------------------------------------- -// ███╗ ██╗ ██████╗ ████████╗██╗ ██████╗███████╗ -// ████╗ ██║██╔═══██╗╚══██╔══╝██║██╔════╝██╔════╝ -// ██╔██╗ ██║██║ ██║ ██║ ██║██║ █████╗ -// ██║╚██╗██║██║ ██║ ██║ ██║██║ ██╔══╝ -// ██║ ╚████║╚██████╔╝ ██║ ██║╚██████╗███████╗ -// ╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝╚══════╝ + r#"/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ // ----------------------------------------------- -// -// This file is generated, -// Please do not edit it manually. +// This file is generated, please do not edit it manually. // Run the following in the root of the repo: // // cargo run -p yaml_test_runner -- --branch --token --path -// // ----------------------------------------------- -" +"# .as_bytes(), )?; diff --git a/yaml_test_runner/src/github.rs b/yaml_test_runner/src/github.rs index e0bcbb88..aee2f6c1 100644 --- a/yaml_test_runner/src/github.rs +++ b/yaml_test_runner/src/github.rs @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ use io::Write; use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION}; use serde::Deserialize; diff --git a/yaml_test_runner/src/main.rs b/yaml_test_runner/src/main.rs index 0cf1cd4b..837853a1 100644 --- a/yaml_test_runner/src/main.rs +++ b/yaml_test_runner/src/main.rs @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ extern crate api_generator; #[macro_use] extern crate lazy_static; diff --git a/yaml_test_runner/src/regex.rs b/yaml_test_runner/src/regex.rs index 88233d68..5201476c 100644 --- a/yaml_test_runner/src/regex.rs +++ b/yaml_test_runner/src/regex.rs @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ use ::regex::{Captures, Regex}; use lazy_static; diff --git a/yaml_test_runner/src/step/comparison.rs b/yaml_test_runner/src/step/comparison.rs index dbde059c..78dca574 100644 --- a/yaml_test_runner/src/step/comparison.rs +++ b/yaml_test_runner/src/step/comparison.rs @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ use quote::{ToTokens, Tokens}; use super::Step; diff --git a/yaml_test_runner/src/step/contains.rs b/yaml_test_runner/src/step/contains.rs index 01798f57..f4b5c016 100644 --- a/yaml_test_runner/src/step/contains.rs +++ b/yaml_test_runner/src/step/contains.rs @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ use quote::{ToTokens, Tokens}; use super::Step; diff --git a/yaml_test_runner/src/step/do.rs b/yaml_test_runner/src/step/do.rs index 14e02a31..35414bdf 100644 --- a/yaml_test_runner/src/step/do.rs +++ b/yaml_test_runner/src/step/do.rs @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ use inflector::Inflector; use quote::{ToTokens, Tokens}; diff --git a/yaml_test_runner/src/step/is_false.rs b/yaml_test_runner/src/step/is_false.rs index 3c2d21f2..a9c1121b 100644 --- a/yaml_test_runner/src/step/is_false.rs +++ b/yaml_test_runner/src/step/is_false.rs @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ use quote::{ToTokens, Tokens}; use super::Step; diff --git a/yaml_test_runner/src/step/is_true.rs b/yaml_test_runner/src/step/is_true.rs index 339f4425..54ac8685 100644 --- a/yaml_test_runner/src/step/is_true.rs +++ b/yaml_test_runner/src/step/is_true.rs @@ -1,5 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ use quote::{ToTokens, Tokens}; - use super::Step; use crate::step::Expr; use yaml_rust::Yaml; diff --git a/yaml_test_runner/src/step/length.rs b/yaml_test_runner/src/step/length.rs index 1af3ff56..91ef6452 100644 --- a/yaml_test_runner/src/step/length.rs +++ b/yaml_test_runner/src/step/length.rs @@ -1,5 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ use quote::{ToTokens, Tokens}; - use super::Step; use crate::step::Expr; use yaml_rust::Yaml; diff --git a/yaml_test_runner/src/step/match.rs b/yaml_test_runner/src/step/match.rs index 0e48ba7d..e3338ab3 100644 --- a/yaml_test_runner/src/step/match.rs +++ b/yaml_test_runner/src/step/match.rs @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ use super::Step; use crate::step::{clean_regex, Expr, json_string_from_yaml}; use quote::{ToTokens, Tokens}; diff --git a/yaml_test_runner/src/step/mod.rs b/yaml_test_runner/src/step/mod.rs index f9743645..d253e14f 100644 --- a/yaml_test_runner/src/step/mod.rs +++ b/yaml_test_runner/src/step/mod.rs @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ use api_generator::generator::Api; use std::fmt::Write; use yaml_rust::{Yaml, YamlEmitter}; diff --git a/yaml_test_runner/src/step/set.rs b/yaml_test_runner/src/step/set.rs index 8ddf9394..be1d5334 100644 --- a/yaml_test_runner/src/step/set.rs +++ b/yaml_test_runner/src/step/set.rs @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ use quote::{ToTokens, Tokens}; use super::Step; diff --git a/yaml_test_runner/src/step/skip.rs b/yaml_test_runner/src/step/skip.rs index 6a008eae..feeb8369 100644 --- a/yaml_test_runner/src/step/skip.rs +++ b/yaml_test_runner/src/step/skip.rs @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ use super::Step; use regex::Regex; use yaml_rust::Yaml; diff --git a/yaml_test_runner/src/step/transform_and_set.rs b/yaml_test_runner/src/step/transform_and_set.rs index ea59a81e..da50da0b 100644 --- a/yaml_test_runner/src/step/transform_and_set.rs +++ b/yaml_test_runner/src/step/transform_and_set.rs @@ -1,5 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ use quote::{ToTokens, Tokens}; - use super::Step; use crate::step::Expr; use inflector::Inflector; diff --git a/yaml_test_runner/tests/common/client.rs b/yaml_test_runner/tests/common/client.rs index 7047f839..8a941801 100644 --- a/yaml_test_runner/tests/common/client.rs +++ b/yaml_test_runner/tests/common/client.rs @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ use elasticsearch::cat::{CatSnapshotsParts, CatTemplatesParts}; use elasticsearch::cluster::ClusterHealthParts; use elasticsearch::ilm::IlmRemovePolicyParts; diff --git a/yaml_test_runner/tests/common/macros.rs b/yaml_test_runner/tests/common/macros.rs index 03997736..070b69ce 100644 --- a/yaml_test_runner/tests/common/macros.rs +++ b/yaml_test_runner/tests/common/macros.rs @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ /// Asserts that a [Response] has a status code >=200 and <300 #[macro_export] macro_rules! assert_response_success { diff --git a/yaml_test_runner/tests/common/mod.rs b/yaml_test_runner/tests/common/mod.rs index 94f8b2d5..12231a36 100644 --- a/yaml_test_runner/tests/common/mod.rs +++ b/yaml_test_runner/tests/common/mod.rs @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ #[macro_use] pub mod macros; pub mod client; diff --git a/yaml_test_runner/tests/common/transform.rs b/yaml_test_runner/tests/common/transform.rs index 9ebf3513..a33fb8d3 100644 --- a/yaml_test_runner/tests/common/transform.rs +++ b/yaml_test_runner/tests/common/transform.rs @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ use base64::write::EncoderWriter as Base64Encoder; use std::io::Write; diff --git a/yaml_test_runner/tests/common/util.rs b/yaml_test_runner/tests/common/util.rs index 62ecf3aa..b03aa2d9 100644 --- a/yaml_test_runner/tests/common/util.rs +++ b/yaml_test_runner/tests/common/util.rs @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ use elasticsearch::http::{response::Response, Method, StatusCode}; use serde_json::Value; From cfc21e3061584cd13d8fe500d18f9b17cdc79dc2 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 21 May 2020 11:23:01 +1000 Subject: [PATCH 123/127] fmt/clippy --- yaml_test_runner/src/generator.rs | 9 +++----- yaml_test_runner/src/regex.rs | 1 - yaml_test_runner/src/step/contains.rs | 14 ++++++------ yaml_test_runner/src/step/is_true.rs | 2 +- yaml_test_runner/src/step/length.rs | 2 +- yaml_test_runner/src/step/match.rs | 2 +- yaml_test_runner/src/step/mod.rs | 2 +- .../src/step/transform_and_set.rs | 2 +- yaml_test_runner/tests/common/macros.rs | 22 ++++++++++++++----- 9 files changed, 31 insertions(+), 25 deletions(-) diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index aaac5d65..55a13405 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -16,11 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -use inflector::Inflector; -use quote::{ToTokens, Tokens}; use crate::step::*; use api_generator::generator::Api; +use inflector::Inflector; use path_slash::PathExt; +use quote::{ToTokens, Tokens}; use regex::Regex; use semver::Version; use serde::Deserialize; @@ -546,10 +546,7 @@ fn test_file_path(relative_path: &Path) -> Result { let file_name = relative.file_name().unwrap().to_string_lossy().into_owned(); // modules can't start with a number so prefix with underscore if file_name.starts_with(char::is_numeric) { - relative.set_file_name(format!( - "_{}", - file_name - )); + relative.set_file_name(format!("_{}", file_name)); } Ok(relative) diff --git a/yaml_test_runner/src/regex.rs b/yaml_test_runner/src/regex.rs index 5201476c..d151c5e7 100644 --- a/yaml_test_runner/src/regex.rs +++ b/yaml_test_runner/src/regex.rs @@ -17,7 +17,6 @@ * under the License. */ use ::regex::{Captures, Regex}; -use lazy_static; lazy_static! { // replace usages of "$.*" with the captured value diff --git a/yaml_test_runner/src/step/contains.rs b/yaml_test_runner/src/step/contains.rs index f4b5c016..de07461a 100644 --- a/yaml_test_runner/src/step/contains.rs +++ b/yaml_test_runner/src/step/contains.rs @@ -19,7 +19,7 @@ use quote::{ToTokens, Tokens}; use super::Step; -use crate::step::{Expr, json_string_from_yaml}; +use crate::step::{json_string_from_yaml, Expr}; use yaml_rust::Yaml; pub struct Contains { @@ -59,22 +59,22 @@ impl ToTokens for Contains { tokens.append(quote! { assert_contains!(json#ident, json!(#f)); }); - }, + } Yaml::Integer(i) => { tokens.append(quote! { assert_contains!(json#ident, json!(#i)); }); - }, + } Yaml::String(s) => { tokens.append(quote! { assert_contains!(json#ident, json!(#s)); }); - }, + } Yaml::Boolean(b) => { tokens.append(quote! { assert_contains!(json#ident, json!(#b)); }); - }, + } yaml if yaml.is_array() || yaml.as_hash().is_some() => { let json = { let s = json_string_from_yaml(yaml); @@ -84,10 +84,10 @@ impl ToTokens for Contains { tokens.append(quote! { assert_contains!(json#ident, json!(#json)); }); - }, + } yaml => { panic!("Bad yaml value {:?}", &yaml); } } } -} \ No newline at end of file +} diff --git a/yaml_test_runner/src/step/is_true.rs b/yaml_test_runner/src/step/is_true.rs index 54ac8685..70a0c945 100644 --- a/yaml_test_runner/src/step/is_true.rs +++ b/yaml_test_runner/src/step/is_true.rs @@ -16,9 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -use quote::{ToTokens, Tokens}; use super::Step; use crate::step::Expr; +use quote::{ToTokens, Tokens}; use yaml_rust::Yaml; pub struct IsTrue { diff --git a/yaml_test_runner/src/step/length.rs b/yaml_test_runner/src/step/length.rs index 91ef6452..75449d8b 100644 --- a/yaml_test_runner/src/step/length.rs +++ b/yaml_test_runner/src/step/length.rs @@ -16,9 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -use quote::{ToTokens, Tokens}; use super::Step; use crate::step::Expr; +use quote::{ToTokens, Tokens}; use yaml_rust::Yaml; pub struct Length { diff --git a/yaml_test_runner/src/step/match.rs b/yaml_test_runner/src/step/match.rs index e3338ab3..c96e27dc 100644 --- a/yaml_test_runner/src/step/match.rs +++ b/yaml_test_runner/src/step/match.rs @@ -17,7 +17,7 @@ * under the License. */ use super::Step; -use crate::step::{clean_regex, Expr, json_string_from_yaml}; +use crate::step::{clean_regex, json_string_from_yaml, Expr}; use quote::{ToTokens, Tokens}; use yaml_rust::Yaml; diff --git a/yaml_test_runner/src/step/mod.rs b/yaml_test_runner/src/step/mod.rs index d253e14f..21224e53 100644 --- a/yaml_test_runner/src/step/mod.rs +++ b/yaml_test_runner/src/step/mod.rs @@ -16,10 +16,10 @@ * specific language governing permissions and limitations * under the License. */ +use crate::regex::*; use api_generator::generator::Api; use std::fmt::Write; use yaml_rust::{Yaml, YamlEmitter}; -use crate::regex::*; mod comparison; mod contains; diff --git a/yaml_test_runner/src/step/transform_and_set.rs b/yaml_test_runner/src/step/transform_and_set.rs index da50da0b..fc8ce03c 100644 --- a/yaml_test_runner/src/step/transform_and_set.rs +++ b/yaml_test_runner/src/step/transform_and_set.rs @@ -16,10 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -use quote::{ToTokens, Tokens}; use super::Step; use crate::step::Expr; use inflector::Inflector; +use quote::{ToTokens, Tokens}; use yaml_rust::Yaml; pub struct Transformation { diff --git a/yaml_test_runner/tests/common/macros.rs b/yaml_test_runner/tests/common/macros.rs index 070b69ce..592aa1ce 100644 --- a/yaml_test_runner/tests/common/macros.rs +++ b/yaml_test_runner/tests/common/macros.rs @@ -307,7 +307,12 @@ macro_rules! assert_comparison_from_set_value { macro_rules! assert_contains { ($expr:expr, $value:expr) => {{ if !$expr.is_array() { - assert!(false, "expected {} to be an array but was {:?}", stringify!($expr), &$expr); + assert!( + false, + "expected {} to be an array but was {:?}", + stringify!($expr), + &$expr + ); } let arr = $expr.as_array().unwrap(); @@ -318,20 +323,25 @@ macro_rules! assert_contains { if $value.is_object() { let vv = $value.clone(); let o = vv.as_object().unwrap(); - assert!(arr.iter() - .filter_map(serde_json::Value::as_object) - .any(|ao| o.iter().all(|(key, value)| ao.get(key).map_or(false, |v| *value == *v))), + assert!( + arr.iter() + .filter_map(serde_json::Value::as_object) + .any(|ao| o + .iter() + .all(|(key, value)| ao.get(key).map_or(false, |v| *value == *v))), "expected value {} to contain {:?} but contained {:?}", stringify!($expr), &vv, - &arr); + &arr + ); } else { assert!( arr.contains(&$value), "expected value {} to contain {:?} but contained {:?}", stringify!($expr), &$value, - &arr); + &arr + ); } }}; } From 79e604687634c775c0599026a965fa6f3e212d8a Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 21 May 2020 13:01:28 +1000 Subject: [PATCH 124/127] pass skip reason --- yaml_test_runner/src/generator.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 55a13405..98d0b529 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -204,29 +204,29 @@ impl<'a> YamlTests<'a> { let fn_name = syn::Ident::from(unique_name.as_str()); let mut body = Tokens::new(); - let mut skip = Option::<&Skip>::None; + let mut skip : Option = None; let mut read_response = false; for step in &test_fn.steps { match step { Step::Skip(s) => { skip = if s.skip_version(self.version) { - info!( + let m = format!( r#"skipping "{}" in {} because version "{}" is met. {}"#, name, &self.path, s.version(), s.reason() ); - Some(s) + Some(m) } else if s.skip_features(&self.skip.features) { - info!( + let m = format!( r#"skipping "{}" in {} because it needs features "{:?}" which are currently not implemented"#, name, &self.path, s.features() ); - Some(s) + Some(m) } else { None } @@ -270,7 +270,10 @@ impl<'a> YamlTests<'a> { } match skip { - Some(_) => None, + Some(s) => { + info!("{}", s); + None + }, None => Some(quote! { #[tokio::test] async fn #fn_name() -> Result<(), failure::Error> { From ec77542d732b4505bf2e8376d1cc7c53f887e0d4 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 21 May 2020 17:17:09 +1000 Subject: [PATCH 125/127] tidy up error messages --- yaml_test_runner/src/generator.rs | 6 +- yaml_test_runner/src/regex.rs | 29 ++++--- yaml_test_runner/src/step/comparison.rs | 3 +- yaml_test_runner/src/step/contains.rs | 3 +- yaml_test_runner/src/step/do.rs | 107 +++++++++++++----------- yaml_test_runner/src/step/is_false.rs | 5 +- yaml_test_runner/src/step/is_true.rs | 2 +- yaml_test_runner/src/step/match.rs | 5 +- yaml_test_runner/src/step/mod.rs | 42 +++------- yaml_test_runner/src/step/set.rs | 3 +- 10 files changed, 96 insertions(+), 109 deletions(-) diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 98d0b529..71760fe7 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -430,7 +430,7 @@ pub fn generate_tests_from_yaml( // a yaml test can contain multiple yaml docs, so use yaml_rust to parse let result = YamlLoader::load_from_str(&yaml); if result.is_err() { - info!( + error!( "skipping {}. cannot read as Yaml struct: {}", relative_path.to_slash_lossy(), result.err().unwrap().to_string() @@ -477,10 +477,10 @@ pub fn generate_tests_from_yaml( .collect(); //if there has been an Err in any step of the yaml test file, don't create a test for it - match ok_or_accumulate(&results, 1) { + match ok_or_accumulate(&results) { Ok(_) => write_test_file(test, relative_path, generated_dir)?, Err(e) => { - info!("skipping {} because\n{}", relative_path.to_slash_lossy(), e) + info!("skipping {} because {}", relative_path.to_slash_lossy(), e) } } } diff --git a/yaml_test_runner/src/regex.rs b/yaml_test_runner/src/regex.rs index d151c5e7..e0bc2421 100644 --- a/yaml_test_runner/src/regex.rs +++ b/yaml_test_runner/src/regex.rs @@ -38,22 +38,27 @@ lazy_static! { regex::Regex::new(r"([,:\[{]\s*)(\d{10,}?)(\s*[,}\]])").unwrap(); } -/// Replaces a "set" step value with a variable -pub fn replace_set_quoted_delimited>(s: S) -> String { - SET_QUOTED_DELIMITED_REGEX - .replace_all(s.as_ref(), "$1") - .into_owned() -} - -/// Replaces a "set" step value with a variable -pub fn replace_set_delimited>(s: S) -> String { - SET_DELIMITED_REGEX - .replace_all(s.as_ref(), "$1") - .into_owned() +/// cleans up a regex as specified in YAML to one that will work with the regex crate. +pub fn clean_regex>(s: S) -> String { + s.as_ref() + .trim() + .trim_matches('/') + .replace("\\/", "/") + .replace("\\:", ":") + .replace("\\#", "#") + .replace("\\%", "%") + .replace("\\'", "'") + .replace("\\`", "`") } /// Replaces a "set" step value with a variable pub fn replace_set>(s: S) -> String { + let mut s = SET_QUOTED_DELIMITED_REGEX + .replace_all(s.as_ref(), "$1").into_owned(); + + s = SET_DELIMITED_REGEX + .replace_all(s.as_ref(), "$1").into_owned(); + SET_REGEX.replace_all(s.as_ref(), "$1").into_owned() } diff --git a/yaml_test_runner/src/step/comparison.rs b/yaml_test_runner/src/step/comparison.rs index 78dca574..78ecc912 100644 --- a/yaml_test_runner/src/step/comparison.rs +++ b/yaml_test_runner/src/step/comparison.rs @@ -16,10 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -use quote::{ToTokens, Tokens}; - use super::Step; use crate::step::Expr; +use quote::{ToTokens, Tokens}; use yaml_rust::Yaml; pub const OPERATORS: [&str; 4] = ["lt", "lte", "gt", "gte"]; diff --git a/yaml_test_runner/src/step/contains.rs b/yaml_test_runner/src/step/contains.rs index de07461a..18c4b885 100644 --- a/yaml_test_runner/src/step/contains.rs +++ b/yaml_test_runner/src/step/contains.rs @@ -16,10 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -use quote::{ToTokens, Tokens}; - use super::Step; use crate::step::{json_string_from_yaml, Expr}; +use quote::{ToTokens, Tokens}; use yaml_rust::Yaml; pub struct Contains { diff --git a/yaml_test_runner/src/step/do.rs b/yaml_test_runner/src/step/do.rs index 35414bdf..75e13161 100644 --- a/yaml_test_runner/src/step/do.rs +++ b/yaml_test_runner/src/step/do.rs @@ -16,16 +16,15 @@ * specific language governing permissions and limitations * under the License. */ -use inflector::Inflector; -use quote::{ToTokens, Tokens}; - use super::{ok_or_accumulate, Step}; use crate::regex::*; -use crate::step::clean_regex; use api_generator::generator::{Api, ApiEndpoint, TypeKind}; +use inflector::Inflector; use itertools::Itertools; +use quote::{ToTokens, Tokens}; use std::collections::BTreeMap; use yaml_rust::{Yaml, YamlEmitter}; +use crate::regex::clean_regex; /// A catch expression on a do step pub struct Catch(String); @@ -151,7 +150,7 @@ impl Do { .iter() .map(|(k, v)| { let key = k.as_str().ok_or_else(|| { - failure::err_msg(format!("expected string key but found {:?}", k)) + failure::err_msg(format!("expected string but found {:?}", k)) })?; match key { @@ -161,10 +160,10 @@ impl Do { })?; for (hk, hv) in hash.iter() { let h = hk.as_str().ok_or_else(|| { - failure::err_msg(format!("expected str but found {:?}", hk)) + failure::err_msg(format!("expected string but found {:?}", hk)) })?; let v = hv.as_str().ok_or_else(|| { - failure::err_msg(format!("expected str but found {:?}", hv)) + failure::err_msg(format!("expected string but found {:?}", hv)) })?; headers.insert(h.into(), v.into()); } @@ -174,10 +173,7 @@ impl Do { catch = v.as_str().map(|s| Catch(s.to_string())); Ok(()) } - "node_selector" => { - // TODO: implement - Ok(()) - } + "node_selector" => Ok(()), "warnings" => { warnings = to_string_vec(v); Ok(()) @@ -194,12 +190,12 @@ impl Do { }) .collect(); - ok_or_accumulate(&results, 0)?; + ok_or_accumulate(&results)?; let (call, value) = call.ok_or_else(|| failure::err_msg("no API found in do"))?; let endpoint = api .endpoint_for_api_call(call) - .ok_or_else(|| failure::err_msg(format!("no API found for '{}'", call)))?; + .ok_or_else(|| failure::err_msg(format!(r#"no API found for "{}""#, call)))?; let api_call = ApiCall::try_from(api, endpoint, value, headers)?; Ok(Do { @@ -349,7 +345,7 @@ impl ApiCall { syn::Ident::from("Unspecified") } else { return Err(failure::err_msg(format!( - "Unhandled empty value for {}", + "unhandled empty value for {}", &e ))); } @@ -377,7 +373,7 @@ impl ApiCall { Some(t) => Ok(t), None => match api.common_params.get(*n) { Some(t) => Ok(t), - None => Err(failure::err_msg(format!("No param found for {}", n))), + None => Err(failure::err_msg(format!(r#"no param found for "{}""#, n))), }, }?; @@ -399,7 +395,7 @@ impl ApiCall { .map(|e| Self::generate_enum(n, e, &ty.options)) .collect(); - match ok_or_accumulate(&idents, 0) { + match ok_or_accumulate(&idents) { Ok(_) => { let idents: Vec = idents .into_iter() @@ -426,16 +422,25 @@ impl ApiCall { }) } TypeKind::Boolean => { - let b = s.parse::()?; - tokens.append(quote! { - .#param_ident(#b) - }); + match s.parse::() { + Ok(b) => tokens.append(quote! { + .#param_ident(#b) + }), + Err(e) => { + return Err(failure::err_msg(format!(r#"cannot parse bool from "{}" for param "{}", {}"#, s, n, e.to_string()))) + } + } + } TypeKind::Double => { - let f = s.parse::()?; - tokens.append(quote! { - .#param_ident(#f) - }); + match s.parse::() { + Ok(f) => tokens.append(quote! { + .#param_ident(#f) + }), + Err(e) => { + return Err(failure::err_msg(format!(r#"cannot parse f64 from "{}" for param "{}", {}"#, s, n, e.to_string()))) + } + } } TypeKind::Integer => { if is_set_value { @@ -444,10 +449,14 @@ impl ApiCall { .#param_ident(#set_value.as_i64().unwrap() as i32) }); } else { - let i = s.parse::()?; - tokens.append(quote! { - .#param_ident(#i) - }); + match s.parse::() { + Ok(i) => tokens.append(quote! { + .#param_ident(#i) + }), + Err(e) => { + return Err(failure::err_msg(format!(r#"cannot parse i32 from "{}" for param "{}", {}"#, s, n, e.to_string()))) + } + } } } TypeKind::Number | TypeKind::Long => { @@ -540,7 +549,7 @@ impl ApiCall { .map(|i| match i { Yaml::String(s) => Ok(s), y => Err(failure::err_msg(format!( - "Unsupported array value {:?}", + "unsupported array value {:?}", y ))), }) @@ -553,7 +562,7 @@ impl ApiCall { .map(|s| Self::generate_enum(n, s.as_str(), &ty.options)) .collect(); - match ok_or_accumulate(&result, 0) { + match ok_or_accumulate(&result) { Ok(_) => { let result: Vec = result.into_iter().filter_map(Result::ok).collect(); @@ -570,7 +579,13 @@ impl ApiCall { }); } } - _ => println!("Unsupported value {:?}", v), + Yaml::Real(r) => { + let f = r.parse::()?; + tokens.append(quote! { + .#param_ident(#f) + }); + } + _ => println!("unsupported value {:?} for param {}", v, n), } } @@ -599,7 +614,6 @@ impl ApiCall { .as_str(), ); let replacement = SET_DELIMITED_REGEX.replace_all(s, "{}"); - // wrap in Value::String so that generated .as_str().unwrap() logic works the same for both branches quote! { Value::String(format!(#replacement, #token.as_str().unwrap())) } } } @@ -631,7 +645,7 @@ impl ApiCall { // check there's actually a None value if !param_counts.contains(&0) { return Err(failure::err_msg(format!( - "No path for '{}' API with no URL parts", + r#"no path for "{}" API with no url parts"#, api_call ))); } @@ -679,7 +693,7 @@ impl ApiCall { } .ok_or_else(|| { failure::err_msg(format!( - "No path for '{}' API with URL parts {:?}", + r#"no path for "{}" API with url parts {:?}"#, &api_call, parts )) })?; @@ -696,7 +710,8 @@ impl ApiCall { let part_tokens: Vec> = parts .iter() - // don't rely on URL parts being ordered in the yaml test + // don't rely on URL parts being ordered in the yaml test in the same order as specified + // in the REST spec. .sorted_by(|(p, _), (p2, _)| { let f = path_parts.iter().position(|x| x == p).unwrap(); let s = path_parts.iter().position(|x| x == p2).unwrap(); @@ -704,7 +719,7 @@ impl ApiCall { }) .map(|(p, v)| { let ty = path.parts.get(*p).ok_or_else(|| { - failure::err_msg(format!("No URL part found for {} in {}", p, &path.path)) + failure::err_msg(format!(r#"no url part found for "{}" in {}"#, p, &path.path)) })?; match v { @@ -763,13 +778,13 @@ impl ApiCall { .map(|i| match i { Yaml::String(s) => Ok(s), y => Err(failure::err_msg(format!( - "Unsupported array value {:?}", + "unsupported array value {:?}", y ))), }) .collect(); - match ok_or_accumulate(&result, 0) { + match ok_or_accumulate(&result) { Ok(_) => { let result: Vec<_> = result.into_iter().filter_map(Result::ok).collect(); @@ -789,12 +804,12 @@ impl ApiCall { Err(e) => Err(failure::err_msg(e)), } } - _ => Err(failure::err_msg(format!("Unsupported value {:?}", v))), + _ => Err(failure::err_msg(format!("unsupported value {:?}", v))), } }) .collect(); - match ok_or_accumulate(&part_tokens, 0) { + match ok_or_accumulate(&part_tokens) { Ok(_) => { let part_tokens: Vec = part_tokens.into_iter().filter_map(Result::ok).collect(); @@ -816,9 +831,7 @@ impl ApiCall { Yaml::Null => None, Yaml::String(s) => { let json = { - let mut json = replace_set_quoted_delimited(s); - json = replace_set_delimited(json); - json = replace_set(json); + let json = replace_set(s); replace_i64(json) }; if endpoint.supports_nd_body() { @@ -828,7 +841,7 @@ impl ApiCall { // empty or a more-indented line (using >) // see https://yaml.org/spec/1.2/spec.html#id2760844 // - // need to trim the trailing newline to differentiate... + // need to trim the trailing newline to be able to differentiate... let contains_newlines = json.trim_end_matches('\n').contains('\n'); let split = if contains_newlines { json.split('\n').collect::>() @@ -864,14 +877,10 @@ impl ApiCall { .map(|value| { let mut json = serde_json::to_string(&value).unwrap(); if value.is_string() { - json = replace_set_quoted_delimited(json); - json = replace_set_delimited(json); json = replace_set(&json); let ident = syn::Ident::from(json); quote!(#ident) } else { - json = replace_set_quoted_delimited(json); - json = replace_set_delimited(json); json = replace_set(json); json = replace_i64(json); let ident = syn::Ident::from(json); @@ -883,8 +892,6 @@ impl ApiCall { } else { let value: serde_json::Value = serde_yaml::from_str(&s).unwrap(); let mut json = serde_json::to_string_pretty(&value).unwrap(); - json = replace_set_quoted_delimited(json); - json = replace_set_delimited(json); json = replace_set(json); json = replace_i64(json); let ident = syn::Ident::from(json); diff --git a/yaml_test_runner/src/step/is_false.rs b/yaml_test_runner/src/step/is_false.rs index a9c1121b..2b935cc5 100644 --- a/yaml_test_runner/src/step/is_false.rs +++ b/yaml_test_runner/src/step/is_false.rs @@ -16,10 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -use quote::{ToTokens, Tokens}; - use super::Step; use crate::step::Expr; +use quote::{ToTokens, Tokens}; use yaml_rust::Yaml; pub struct IsFalse { @@ -44,7 +43,7 @@ impl IsFalse { impl ToTokens for IsFalse { fn to_tokens(&self, tokens: &mut Tokens) { - if self.expr.is_empty() { + if self.expr.is_body() { tokens.append(quote! { assert!(text.is_empty(), "expected value to be empty but was {}", &text); }); diff --git a/yaml_test_runner/src/step/is_true.rs b/yaml_test_runner/src/step/is_true.rs index 70a0c945..132aacb4 100644 --- a/yaml_test_runner/src/step/is_true.rs +++ b/yaml_test_runner/src/step/is_true.rs @@ -43,7 +43,7 @@ impl IsTrue { impl ToTokens for IsTrue { fn to_tokens(&self, tokens: &mut Tokens) { - if self.expr.is_empty() { + if self.expr.is_body() { // for a HEAD request, the body is expected to be empty, so check the status code instead. tokens.append(quote! { match method { diff --git a/yaml_test_runner/src/step/match.rs b/yaml_test_runner/src/step/match.rs index c96e27dc..9f84d606 100644 --- a/yaml_test_runner/src/step/match.rs +++ b/yaml_test_runner/src/step/match.rs @@ -17,9 +17,10 @@ * under the License. */ use super::Step; -use crate::step::{clean_regex, json_string_from_yaml, Expr}; +use crate::step::{json_string_from_yaml, Expr}; use quote::{ToTokens, Tokens}; use yaml_rust::Yaml; +use crate::regex::clean_regex; pub struct Match { pub expr: Expr, @@ -137,7 +138,7 @@ impl ToTokens for Match { syn::Ident::from(s) }; - if self.expr.is_body() || self.expr.is_empty() { + if self.expr.is_body() { tokens.append(quote! { assert_match!(json, json!(#json)); }); diff --git a/yaml_test_runner/src/step/mod.rs b/yaml_test_runner/src/step/mod.rs index 21224e53..bcb8dafc 100644 --- a/yaml_test_runner/src/step/mod.rs +++ b/yaml_test_runner/src/step/mod.rs @@ -123,15 +123,9 @@ impl Expr { Self { expr: expr.into() } } - /// Whether the expression is empty. Used in is_true and is_false - /// to represent the body - pub fn is_empty(&self) -> bool { - self.expr.is_empty() - } - - /// Whether the expression is "$body" + /// Whether the expression is "$body" or "", which are both used to express the whole body pub fn is_body(&self) -> bool { - Self::is_string_body(&self.expr) + Self::is_string_body(&self.expr) || self.expr.is_empty() } fn is_string_body(s: &str) -> bool { @@ -177,7 +171,7 @@ impl Expr { } else if s.chars().all(char::is_numeric) { write!(expr, "[{}]", s).unwrap(); } else if s.starts_with('$') { - // handle set values + // handle "set" values let t = s .trim_start_matches('$') .trim_start_matches('{') @@ -227,8 +221,7 @@ impl Step { /// Checks whether there are any Errs in the collection, and accumulates them into one /// error message if there are. pub fn ok_or_accumulate( - results: &[Result], - indent: usize, + results: &[Result] ) -> Result<(), failure::Error> { let errs = results .iter() @@ -237,29 +230,16 @@ pub fn ok_or_accumulate( if errs.is_empty() { Ok(()) } else { - let msg = errs + let mut msgs = errs .iter() - .map(|e| format!("{}{}", " ".to_string().repeat(indent), e.to_string())) - .collect::>() - .join("\n"); - - Err(failure::err_msg(msg)) + .map(|e| e.to_string()) + .collect::>(); + msgs.sort(); + msgs.dedup_by(|a, b | a == b); + Err(failure::err_msg(msgs.join(", "))) } } -/// cleans up a regex as specified in YAML to one that will work with the regex crate. -pub fn clean_regex>(s: S) -> String { - s.as_ref() - .trim() - .trim_matches('/') - .replace("\\/", "/") - .replace("\\:", ":") - .replace("\\#", "#") - .replace("\\%", "%") - .replace("\\'", "'") - .replace("\\`", "`") -} - pub fn json_string_from_yaml(yaml: &Yaml) -> String { let mut s = String::new(); { @@ -270,8 +250,6 @@ pub fn json_string_from_yaml(yaml: &Yaml) -> String { let value: serde_json::Value = serde_yaml::from_str(&s).unwrap(); let mut json = value.to_string(); - json = replace_set_quoted_delimited(json); - json = replace_set_delimited(json); json = replace_set(json); json = replace_i64(json); json diff --git a/yaml_test_runner/src/step/set.rs b/yaml_test_runner/src/step/set.rs index be1d5334..a9dec7af 100644 --- a/yaml_test_runner/src/step/set.rs +++ b/yaml_test_runner/src/step/set.rs @@ -16,10 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -use quote::{ToTokens, Tokens}; - use super::Step; use crate::step::Expr; +use quote::{ToTokens, Tokens}; use yaml_rust::Yaml; pub struct Set { From 95eff3aae429e01637f7d324b50433b83eb063d8 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 28 May 2020 20:41:31 +1000 Subject: [PATCH 126/127] Create a singleton client for all tests --- yaml_test_runner/Cargo.toml | 1 + yaml_test_runner/src/generator.rs | 10 +++--- yaml_test_runner/src/step/do.rs | 2 +- yaml_test_runner/tests/common/client.rs | 39 ++++++++++++++++++++--- yaml_test_runner/tests/common/mod.rs | 1 - yaml_test_runner/tests/common/util.rs | 41 ------------------------- 6 files changed, 42 insertions(+), 52 deletions(-) delete mode 100644 yaml_test_runner/tests/common/util.rs diff --git a/yaml_test_runner/Cargo.toml b/yaml_test_runner/Cargo.toml index f8a67420..cccf9be1 100644 --- a/yaml_test_runner/Cargo.toml +++ b/yaml_test_runner/Cargo.toml @@ -17,6 +17,7 @@ itertools = "0.8.2" Inflector = "0.11.4" lazy_static = "1.4.0" log = "0.4.8" +once_cell = "1.4.0" path-slash = "0.1.1" quote = "~0.3" regex = "1.3.1" diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs index 71760fe7..8e0a0c4d 100644 --- a/yaml_test_runner/src/generator.rs +++ b/yaml_test_runner/src/generator.rs @@ -120,8 +120,8 @@ impl<'a> YamlTests<'a> { let (setup_fn, setup_call) = Self::generate_fixture(&self.setup); let (teardown_fn, teardown_call) = Self::generate_fixture(&self.teardown); let general_setup_call = match self.suite { - TestSuite::Oss => quote!(client::general_oss_setup(&client).await?;), - TestSuite::XPack => quote!(client::general_xpack_setup(&client).await?;), + TestSuite::Oss => quote!(client::general_oss_setup().await?;), + TestSuite::XPack => quote!(client::general_xpack_setup().await?;), }; let tests = self.fn_impls(general_setup_call, setup_call, teardown_call); @@ -137,7 +137,7 @@ impl<'a> YamlTests<'a> { quote! { #![allow(unused_imports, unused_variables, dead_code)] - use crate::common::{client, macros, transform, util}; + use crate::common::{client, macros, transform}; use elasticsearch::*; use elasticsearch::http::{ headers::{HeaderName, HeaderValue}, @@ -159,7 +159,7 @@ impl<'a> YamlTests<'a> { pub fn read_response(read_response: bool, tokens: &mut Tokens) -> bool { if !read_response { tokens.append(quote! { - let (method, status_code, text, json) = util::read_response(response).await?; + let (method, status_code, text, json) = client::read_response(response).await?; }); } @@ -277,7 +277,7 @@ impl<'a> YamlTests<'a> { None => Some(quote! { #[tokio::test] async fn #fn_name() -> Result<(), failure::Error> { - let client = client::create(); + let client = client::get(); #general_setup_call #setup_call #body diff --git a/yaml_test_runner/src/step/do.rs b/yaml_test_runner/src/step/do.rs index 75e13161..e42ca981 100644 --- a/yaml_test_runner/src/step/do.rs +++ b/yaml_test_runner/src/step/do.rs @@ -113,7 +113,7 @@ impl Do { if !read_response && c.needs_response_body() { read_response = true; tokens.append(quote! { - let (method, status_code, text, json) = util::read_response(response).await?; + let (method, status_code, text, json) = client::read_response(response).await?; }); } c.to_tokens(tokens); diff --git a/yaml_test_runner/tests/common/client.rs b/yaml_test_runner/tests/common/client.rs index 8a941801..6c1054cb 100644 --- a/yaml_test_runner/tests/common/client.rs +++ b/yaml_test_runner/tests/common/client.rs @@ -44,6 +44,10 @@ use elasticsearch::{ use serde_json::{json, Value}; use sysinfo::SystemExt; use url::Url; +use once_cell::sync::Lazy; +use std::ops::Deref; +use elasticsearch::http::response::Response; +use elasticsearch::http::{Method, StatusCode}; fn cluster_addr() -> String { match std::env::var("ES_TEST_SERVER") { @@ -58,8 +62,7 @@ fn running_proxy() -> bool { !system.get_process_by_name("Fiddler").is_empty() } -/// Creates a client to use in tests -pub fn create() -> Elasticsearch { +static GLOBAL_CLIENT: Lazy = Lazy::new(|| { let mut url = Url::parse(cluster_addr().as_ref()).unwrap(); // if the url is https and specifies a username and password, remove from the url and set credentials @@ -101,10 +104,37 @@ pub fn create() -> Elasticsearch { let transport = builder.build().unwrap(); Elasticsearch::new(transport) +}); + +/// Gets the client to use in tests +pub fn get() -> &'static Elasticsearch { + GLOBAL_CLIENT.deref() +} + +/// Reads the response from Elasticsearch, returning the method, status code, text response, +/// and the response parsed from json or yaml +pub async fn read_response( + response: Response, +) -> Result<(Method, StatusCode, String, Value), failure::Error> { + let is_json = response.content_type().starts_with("application/json"); + let is_yaml = response.content_type().starts_with("application/yaml"); + let method = response.method(); + let status_code = response.status_code(); + let text = response.text().await?; + let json = if is_json && !text.is_empty() { + serde_json::from_str::(text.as_ref())? + } else if is_yaml && !text.is_empty() { + serde_yaml::from_str::(text.as_ref())? + } else { + Value::Null + }; + + Ok((method, status_code, text, json)) } /// general setup step for an OSS yaml test -pub async fn general_oss_setup(client: &Elasticsearch) -> Result<(), Error> { +pub async fn general_oss_setup() -> Result<(), Error> { + let client= get(); delete_indices(client).await?; delete_templates(client).await?; @@ -145,7 +175,8 @@ pub async fn general_oss_setup(client: &Elasticsearch) -> Result<(), Error> { } /// general setup step for an xpack yaml test -pub async fn general_xpack_setup(client: &Elasticsearch) -> Result<(), Error> { +pub async fn general_xpack_setup() -> Result<(), Error> { + let client = get(); delete_templates(client).await?; let _delete_watch_response = client diff --git a/yaml_test_runner/tests/common/mod.rs b/yaml_test_runner/tests/common/mod.rs index 12231a36..6e28cfb2 100644 --- a/yaml_test_runner/tests/common/mod.rs +++ b/yaml_test_runner/tests/common/mod.rs @@ -20,4 +20,3 @@ pub mod macros; pub mod client; pub mod transform; -pub mod util; diff --git a/yaml_test_runner/tests/common/util.rs b/yaml_test_runner/tests/common/util.rs deleted file mode 100644 index b03aa2d9..00000000 --- a/yaml_test_runner/tests/common/util.rs +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -use elasticsearch::http::{response::Response, Method, StatusCode}; -use serde_json::Value; - -/// Reads the response from Elasticsearch, returning the method, status code, text response, -/// and the response parsed from json or yaml -pub async fn read_response( - response: Response, -) -> Result<(Method, StatusCode, String, Value), failure::Error> { - let is_json = response.content_type().starts_with("application/json"); - let is_yaml = response.content_type().starts_with("application/yaml"); - let method = response.method(); - let status_code = response.status_code(); - let text = response.text().await?; - let json = if is_json && !text.is_empty() { - serde_json::from_str::(text.as_ref())? - } else if is_yaml && !text.is_empty() { - serde_yaml::from_str::(text.as_ref())? - } else { - Value::Null - }; - - Ok((method, status_code, text, json)) -} From 96b2627fbb268c94daf1d16cb6ac2c0e578120f1 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Fri, 29 May 2020 09:13:48 +1000 Subject: [PATCH 127/127] formatting --- yaml_test_runner/src/regex.rs | 6 ++- yaml_test_runner/src/step/do.rs | 57 +++++++++++++++---------- yaml_test_runner/src/step/match.rs | 2 +- yaml_test_runner/src/step/mod.rs | 11 ++--- yaml_test_runner/tests/common/client.rs | 10 ++--- 5 files changed, 48 insertions(+), 38 deletions(-) diff --git a/yaml_test_runner/src/regex.rs b/yaml_test_runner/src/regex.rs index e0bc2421..cdb538b1 100644 --- a/yaml_test_runner/src/regex.rs +++ b/yaml_test_runner/src/regex.rs @@ -54,10 +54,12 @@ pub fn clean_regex>(s: S) -> String { /// Replaces a "set" step value with a variable pub fn replace_set>(s: S) -> String { let mut s = SET_QUOTED_DELIMITED_REGEX - .replace_all(s.as_ref(), "$1").into_owned(); + .replace_all(s.as_ref(), "$1") + .into_owned(); s = SET_DELIMITED_REGEX - .replace_all(s.as_ref(), "$1").into_owned(); + .replace_all(s.as_ref(), "$1") + .into_owned(); SET_REGEX.replace_all(s.as_ref(), "$1").into_owned() } diff --git a/yaml_test_runner/src/step/do.rs b/yaml_test_runner/src/step/do.rs index e42ca981..b9b5e92b 100644 --- a/yaml_test_runner/src/step/do.rs +++ b/yaml_test_runner/src/step/do.rs @@ -17,6 +17,7 @@ * under the License. */ use super::{ok_or_accumulate, Step}; +use crate::regex::clean_regex; use crate::regex::*; use api_generator::generator::{Api, ApiEndpoint, TypeKind}; use inflector::Inflector; @@ -24,7 +25,6 @@ use itertools::Itertools; use quote::{ToTokens, Tokens}; use std::collections::BTreeMap; use yaml_rust::{Yaml, YamlEmitter}; -use crate::regex::clean_regex; /// A catch expression on a do step pub struct Catch(String); @@ -421,27 +421,32 @@ impl ApiCall { .#param_ident(&[#(#values),*]) }) } - TypeKind::Boolean => { - match s.parse::() { - Ok(b) => tokens.append(quote! { - .#param_ident(#b) - }), - Err(e) => { - return Err(failure::err_msg(format!(r#"cannot parse bool from "{}" for param "{}", {}"#, s, n, e.to_string()))) - } + TypeKind::Boolean => match s.parse::() { + Ok(b) => tokens.append(quote! { + .#param_ident(#b) + }), + Err(e) => { + return Err(failure::err_msg(format!( + r#"cannot parse bool from "{}" for param "{}", {}"#, + s, + n, + e.to_string() + ))) } - - } - TypeKind::Double => { - match s.parse::() { - Ok(f) => tokens.append(quote! { - .#param_ident(#f) - }), - Err(e) => { - return Err(failure::err_msg(format!(r#"cannot parse f64 from "{}" for param "{}", {}"#, s, n, e.to_string()))) - } + }, + TypeKind::Double => match s.parse::() { + Ok(f) => tokens.append(quote! { + .#param_ident(#f) + }), + Err(e) => { + return Err(failure::err_msg(format!( + r#"cannot parse f64 from "{}" for param "{}", {}"#, + s, + n, + e.to_string() + ))) } - } + }, TypeKind::Integer => { if is_set_value { let set_value = Self::from_set_value(s); @@ -454,7 +459,12 @@ impl ApiCall { .#param_ident(#i) }), Err(e) => { - return Err(failure::err_msg(format!(r#"cannot parse i32 from "{}" for param "{}", {}"#, s, n, e.to_string()))) + return Err(failure::err_msg(format!( + r#"cannot parse i32 from "{}" for param "{}", {}"#, + s, + n, + e.to_string() + ))) } } } @@ -719,7 +729,10 @@ impl ApiCall { }) .map(|(p, v)| { let ty = path.parts.get(*p).ok_or_else(|| { - failure::err_msg(format!(r#"no url part found for "{}" in {}"#, p, &path.path)) + failure::err_msg(format!( + r#"no url part found for "{}" in {}"#, + p, &path.path + )) })?; match v { diff --git a/yaml_test_runner/src/step/match.rs b/yaml_test_runner/src/step/match.rs index 9f84d606..ac2ba568 100644 --- a/yaml_test_runner/src/step/match.rs +++ b/yaml_test_runner/src/step/match.rs @@ -17,10 +17,10 @@ * under the License. */ use super::Step; +use crate::regex::clean_regex; use crate::step::{json_string_from_yaml, Expr}; use quote::{ToTokens, Tokens}; use yaml_rust::Yaml; -use crate::regex::clean_regex; pub struct Match { pub expr: Expr, diff --git a/yaml_test_runner/src/step/mod.rs b/yaml_test_runner/src/step/mod.rs index bcb8dafc..1fa93bd3 100644 --- a/yaml_test_runner/src/step/mod.rs +++ b/yaml_test_runner/src/step/mod.rs @@ -220,9 +220,7 @@ impl Step { /// Checks whether there are any Errs in the collection, and accumulates them into one /// error message if there are. -pub fn ok_or_accumulate( - results: &[Result] -) -> Result<(), failure::Error> { +pub fn ok_or_accumulate(results: &[Result]) -> Result<(), failure::Error> { let errs = results .iter() .filter_map(|r| r.as_ref().err()) @@ -230,12 +228,9 @@ pub fn ok_or_accumulate( if errs.is_empty() { Ok(()) } else { - let mut msgs = errs - .iter() - .map(|e| e.to_string()) - .collect::>(); + let mut msgs = errs.iter().map(|e| e.to_string()).collect::>(); msgs.sort(); - msgs.dedup_by(|a, b | a == b); + msgs.dedup_by(|a, b| a == b); Err(failure::err_msg(msgs.join(", "))) } } diff --git a/yaml_test_runner/tests/common/client.rs b/yaml_test_runner/tests/common/client.rs index 6c1054cb..c524e11c 100644 --- a/yaml_test_runner/tests/common/client.rs +++ b/yaml_test_runner/tests/common/client.rs @@ -18,6 +18,8 @@ */ use elasticsearch::cat::{CatSnapshotsParts, CatTemplatesParts}; use elasticsearch::cluster::ClusterHealthParts; +use elasticsearch::http::response::Response; +use elasticsearch::http::{Method, StatusCode}; use elasticsearch::ilm::IlmRemovePolicyParts; use elasticsearch::indices::{IndicesDeleteParts, IndicesDeleteTemplateParts, IndicesRefreshParts}; use elasticsearch::ml::{ @@ -41,13 +43,11 @@ use elasticsearch::{ http::transport::{SingleNodeConnectionPool, TransportBuilder}, Elasticsearch, Error, DEFAULT_ADDRESS, }; +use once_cell::sync::Lazy; use serde_json::{json, Value}; +use std::ops::Deref; use sysinfo::SystemExt; use url::Url; -use once_cell::sync::Lazy; -use std::ops::Deref; -use elasticsearch::http::response::Response; -use elasticsearch::http::{Method, StatusCode}; fn cluster_addr() -> String { match std::env::var("ES_TEST_SERVER") { @@ -134,7 +134,7 @@ pub async fn read_response( /// general setup step for an OSS yaml test pub async fn general_oss_setup() -> Result<(), Error> { - let client= get(); + let client = get(); delete_indices(client).await?; delete_templates(client).await?;