From 0ae51d030facfe49b06090c2e1cd4c1c0b64554c Mon Sep 17 00:00:00 2001 From: Sylvain Wallez Date: Thu, 25 Sep 2025 15:28:39 +0200 Subject: [PATCH] Cleanup: remove yaml tests and TEST_SUITE --- .buildkite/DockerFile | 1 - .buildkite/functions/imports.sh | 3 + .buildkite/pipeline.yml | 3 - .dockerignore | 3 - .gitignore | 4 - CONTRIBUTING.md | 21 - Cargo.lock | 66 -- Cargo.toml | 1 - Makefile.toml | 72 +- yaml_test_runner/Cargo.toml | 45 - yaml_test_runner/skip.yml | 147 --- yaml_test_runner/src/generator.rs | 606 ------------ yaml_test_runner/src/main.rs | 152 --- yaml_test_runner/src/regex.rs | 76 -- yaml_test_runner/src/step/comparison.rs | 102 -- yaml_test_runner/src/step/contains.rs | 93 -- yaml_test_runner/src/step/do.rs | 929 ------------------ yaml_test_runner/src/step/is_false.rs | 59 -- yaml_test_runner/src/step/is_true.rs | 63 -- yaml_test_runner/src/step/length.rs | 75 -- yaml_test_runner/src/step/match.rs | 160 --- yaml_test_runner/src/step/mod.rs | 286 ------ yaml_test_runner/src/step/set.rs | 66 -- yaml_test_runner/src/step/skip.rs | 134 --- .../src/step/transform_and_set.rs | 128 --- yaml_test_runner/tests/common/client.rs | 536 ---------- yaml_test_runner/tests/common/macros.rs | 379 ------- yaml_test_runner/tests/common/mod.rs | 22 - yaml_test_runner/tests/common/transform.rs | 31 - 29 files changed, 13 insertions(+), 4250 deletions(-) delete mode 100644 yaml_test_runner/Cargo.toml delete mode 100644 yaml_test_runner/skip.yml delete mode 100644 yaml_test_runner/src/generator.rs delete mode 100644 yaml_test_runner/src/main.rs delete mode 100644 yaml_test_runner/src/regex.rs delete mode 100644 yaml_test_runner/src/step/comparison.rs delete mode 100644 yaml_test_runner/src/step/contains.rs delete mode 100644 yaml_test_runner/src/step/do.rs delete mode 100644 yaml_test_runner/src/step/is_false.rs delete mode 100644 yaml_test_runner/src/step/is_true.rs delete mode 100644 yaml_test_runner/src/step/length.rs delete mode 100644 yaml_test_runner/src/step/match.rs delete mode 100644 yaml_test_runner/src/step/mod.rs delete mode 100644 yaml_test_runner/src/step/set.rs delete mode 100644 yaml_test_runner/src/step/skip.rs delete mode 100644 yaml_test_runner/src/step/transform_and_set.rs delete mode 100644 yaml_test_runner/tests/common/client.rs delete mode 100644 yaml_test_runner/tests/common/macros.rs delete mode 100644 yaml_test_runner/tests/common/mod.rs delete mode 100644 yaml_test_runner/tests/common/transform.rs diff --git a/.buildkite/DockerFile b/.buildkite/DockerFile index 71624833..b355f14a 100644 --- a/.buildkite/DockerFile +++ b/.buildkite/DockerFile @@ -25,7 +25,6 @@ COPY api_generator ./api_generator COPY elasticsearch/Cargo.toml ./elasticsearch/Cargo.toml COPY elasticsearch/src ./elasticsearch/src COPY elasticsearch/build.rs ./elasticsearch/build.rs -COPY yaml_test_runner ./yaml_test_runner COPY xtask ./xtask RUN cargo build --tests diff --git a/.buildkite/functions/imports.sh b/.buildkite/functions/imports.sh index 8e676fa5..8f297838 100644 --- a/.buildkite/functions/imports.sh +++ b/.buildkite/functions/imports.sh @@ -6,6 +6,9 @@ # - Initial version after refactor # - Validate STACK_VERSION asap +# Hardcoded, there's no more a distinction between "free" and "platinum" and security is on by default +export TEST_SUITE="platinum" + function require_stack_version() { if [[ -z $STACK_VERSION ]]; then echo -e "\033[31;1mERROR:\033[0m Required environment variable [STACK_VERSION] not set\033[0m" diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 138ab8f1..47b5dee3 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -4,14 +4,11 @@ steps: provider: "gcp" env: STACK_VERSION: "{{ matrix.stack_version }}" - TEST_SUITE: "{{ matrix.test_suite }}" RUST_TOOLCHAIN: "{{ matrix.toolchain }}" matrix: setup: stack_version: - "9.0.0" - test_suite: - - "platinum" toolchain: - "latest" command: ./.buildkite/run-tests diff --git a/.dockerignore b/.dockerignore index f3cb8460..2bbac683 100644 --- a/.dockerignore +++ b/.dockerignore @@ -9,6 +9,3 @@ docs *.iml api_generator/src/bin - -yaml_test_runner/tests -!yaml_test_runner/tests/common diff --git a/.gitignore b/.gitignore index 17ad73f1..09cc0019 100644 --- a/.gitignore +++ b/.gitignore @@ -6,9 +6,5 @@ pkg *.iml .vscode/ *.log -yaml_test_runner/tests/free -yaml_test_runner/tests/xpack -yaml_test_runner/tests/platinum -yaml_test_runner/tests/mod.rs test_results/ checkout/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4d4ec7fb..b99c8290 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -76,18 +76,6 @@ The `Elasticsearch` category of steps are specifically defined for this project cargo make test --env STACK_VERSION=7.9.0 ``` -- Run YAML tests - - Optionally pass - - - `STACK_VERSION`: Elasticsearch version like `7.9.0` or can be - a snapshot release like `7.x-SNAPSHOT` - - `TEST_SUITE`: Elasticsearch distribution of `free` or `platinum` - - ```sh - cargo make test-yaml --env STACK_VERSION=7.9.0 --env TEST_SUITE=free - ``` - ### Packages The workspace contains the following packages: @@ -109,15 +97,6 @@ The `quote!` macro is particularly useful as it accepts Rust code that can inclu that will be interpolated during expansion. Unlike procedural macros, the token stream returned by the `quote!` macro can be `to_string()`'ed and written to disk, and this is used to create much of the client scaffolding. -- #### `yaml_test_runner` - - A small executable that downloads YAML tests from GitHub and generates client tests from the YAML tests. The - version of YAML tests to download are determined from the commit hash of a running Elasticsearch instance. - - The `yaml_test_runner` package can be run with `cargo make test-yaml` to run the generated client tests, - passing environment variables `TEST_SUITE` and `STACK_VERSION` to control the distribution and version, - respectively. - ### Design principles 1. Generate as much of the client as feasible from the REST API specs diff --git a/Cargo.lock b/Cargo.lock index b4eee64e..1eafa2d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1315,12 +1315,6 @@ dependencies = [ "redox_syscall", ] -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - [[package]] name = "linux-raw-sys" version = "0.9.4" @@ -2056,19 +2050,6 @@ dependencies = [ "syn 2.0.100", ] -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap 2.9.0", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - [[package]] name = "sha1" version = "0.10.6" @@ -2542,12 +2523,6 @@ version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - [[package]] name = "untrusted" version = "0.9.0" @@ -3108,47 +3083,6 @@ dependencies = [ "lzma-sys", ] -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] - -[[package]] -name = "yaml_test_runner" -version = "9.1.0-alpha.1" -dependencies = [ - "Inflector", - "anyhow", - "api_generator", - "base64", - "clap", - "elasticsearch", - "flate2", - "globset", - "itertools", - "lazy_static", - "log", - "once_cell", - "path-slash", - "quote 0.3.15", - "regex", - "reqwest", - "semver", - "serde", - "serde_json", - "serde_yaml", - "simple_logger", - "syn 0.11.11", - "tar", - "tokio", - "url", - "yaml-rust", -] - [[package]] name = "yoke" version = "0.7.5" diff --git a/Cargo.toml b/Cargo.toml index 0eeec3b7..a5bc0777 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,6 @@ members = [ "api_generator", "elasticsearch", - "yaml_test_runner", "xtask" ] diff --git a/Makefile.toml b/Makefile.toml index f9dae7b2..6eab728f 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -1,29 +1,21 @@ [config] default_to_workspace = false +init_task = "init" [env] -# Determines the version of Elasticsearch docker container used -STACK_VERSION = { value = "9.0.0-SNAPSHOT", condition = { env_not_set = ["STACK_VERSION"] }} -# Determines the distribution of docker container used. Either platinum or free -TEST_SUITE = { value = "free", condition = { env_not_set = ["TEST_SUITE"] }} # Set publish flags to dry-run by default, to force user to explicitly define for publishing CARGO_MAKE_CARGO_PUBLISH_FLAGS = "--dry-run" # RUST_BACKTRACE is set to "full" in cargo make's builtin makefiles/stable.toml RUST_BACKTRACE = { value = "0", condition = { env_not_set = ["RUST_BACKTRACE"]}} +ELASTICSEARCH_URL = "https://elastic:changeme@localhost:9200" -[tasks.set-free-env] -category = "Elasticsearch" -description = "Sets ELASTICSEARCH_URL environment variable if not already set for later tasks when free test suite used" -private = true -condition = { env = { "TEST_SUITE" = "free" }, env_not_set = ["ELASTICSEARCH_URL"] } -env = { "ELASTICSEARCH_URL" = "http://elastic:changeme@localhost:9200" } - -[tasks.set-platinum-env] -category = "Elasticsearch" -description = "Sets ELASTICSEARCH_URL environment variable if not already set for later tasks when platinum test suite used" +[tasks.init] private = true -condition = { env = { "TEST_SUITE" = "platinum" }, env_not_set = ["ELASTICSEARCH_URL"] } -env = { "ELASTICSEARCH_URL" = "https://elastic:changeme@localhost:9200" } +condition = { env_not_set = ["STACK_VERSION"] } +script = [ + 'echo "Environment variable STACK_VERSION is required but not set."', + 'exit 1', +] [tasks.download-specs] category = "Elasticsearch" @@ -35,36 +27,9 @@ command = "cargo" args = ["run", "-q", "-p", "xtask", "--", "download-specs", "--url", "${ELASTICSEARCH_URL}"] dependencies = ["start-elasticsearch"] -[tasks.run-yaml-test-runner] -category = "Elasticsearch" -description = ''' -Runs yaml_test_runner package to generate tests from yaml files for a given Elasticsearch commit. -The commit to use is retrieved from the running Elasticsearch instance -''' -private = true -command = "cargo" -args = ["run", "-p", "yaml_test_runner", "--", "-u", "${ELASTICSEARCH_URL}"] -dependencies = ["download-specs"] - -[tasks.test-yaml-test-runner] -category = "Elasticsearch" -private = true -condition = { env_set = [ "ELASTICSEARCH_URL" ], env_false = ["CARGO_MAKE_CI"] } -command = "cargo" -args = ["test", "-p", "yaml_test_runner", "--", "--test-threads", "1"] -dependencies = ["generate-yaml-tests"] - -[tasks.test-yaml-test-runner-ci] -category = "Elasticsearch" -private = true -condition = { env_set = [ "ELASTICSEARCH_URL" ], env_true = ["CARGO_MAKE_CI"] } -script = ["cargo test -p yaml_test_runner -- --test-threads 1 | tee test_results/results.txt"] -dependencies = ["generate-yaml-tests"] - [tasks.test-elasticsearch] category = "Elasticsearch" private = true -condition = { env_set = [ "ELASTICSEARCH_URL" ], env = { "TEST_SUITE" = "platinum" } } command = "cargo" args = ["test", "-p", "elasticsearch"] dependencies = ["start-elasticsearch"] @@ -97,8 +62,7 @@ script = ["[ -d test_results ] || mkdir -p test_results"] [tasks.run-elasticsearch] category = "Elasticsearch" private = true -condition = { env_set = [ "STACK_VERSION", "TEST_SUITE" ], env_false = ["CARGO_MAKE_CI"] } -dependencies = ["set-free-env", "set-platinum-env"] +condition = { env_set = [ "STACK_VERSION" ], env_false = ["CARGO_MAKE_CI"] } [tasks.run-elasticsearch.linux] command = "./.buildkite/run-elasticsearch.sh" @@ -110,7 +74,7 @@ command = "./.buildkite/run-elasticsearch.sh" script_runner = "cmd" script = [ ''' -bash -c "STACK_VERSION=%STACK_VERSION% TEST_SUITE=%TEST_SUITE% DETACH=%DETACH% CLEANUP=%CLEANUP% bash .buildkite/run-elasticsearch.sh" +bash -c "STACK_VERSION=%STACK_VERSION% DETACH=%DETACH% CLEANUP=%CLEANUP% bash .buildkite/run-elasticsearch.sh" ''' ] @@ -147,13 +111,6 @@ private = false description = "Stops Elasticsearch docker container, if running" env = { "CLEANUP" = true, "DETACH" = false } -[tasks.test-yaml] -category = "Elasticsearch" -description = "Generates and runs yaml_test_runner package platinum/free tests against a given Elasticsearch version" -condition = { env_set = [ "STACK_VERSION", "TEST_SUITE" ] } -dependencies = ["generate-yaml-tests", "create-test-results-dir", "test-yaml-test-runner", "test-yaml-test-runner-ci"] -run_task = "stop-elasticsearch" - [tasks.test-generator] category = "Elasticsearch" clear = true @@ -165,16 +122,9 @@ args = ["test", "-p", "api_generator"] category = "Elasticsearch" clear = true description = "Runs elasticsearch package tests against a given Elasticsearch version" -env = { "TEST_SUITE" = { value = "platinum", condition = { env_set = ["TEST_SUITE"] } } } dependencies = ["test-elasticsearch"] run_task = "stop-elasticsearch" -[tasks.generate-yaml-tests] -category = "Elasticsearch" -description = "Generates Elasticsearch client tests from YAML tests" -dependencies = ["run-yaml-test-runner"] -run_task = "format" - [tasks.generate-api] category = "Elasticsearch" description = "Generates Elasticsearch client from REST API specs" @@ -343,7 +293,6 @@ script = [''' echo - start-elasticsearch: Starts Elasticsearch docker container with the given version and distribution echo - stop-elasticsearch: Stops Elasticsearch docker container, if running echo - echo - test-yaml: Generates and runs yaml_test_runner package platinum/free tests against a given Elasticsearch version echo - test-generator: Generates and runs api_generator package tests echo - test: Runs elasticsearch package tests against a given Elasticsearch version echo @@ -358,7 +307,6 @@ script = [''' echo echo Most tasks use these environment variables: echo - STACK_VERSION (default '${STACK_VERSION}'): the version of Elasticsearch - echo - TEST_SUITE ('free' or 'platinum', default '${TEST_SUITE}'): the distribution of Elasticsearch echo - CI (default not set): set when running on CI to determine whether to start Elasticsearch and format test output as JSON echo echo Run 'cargo make --list-all-steps' for a complete list of available tasks. diff --git a/yaml_test_runner/Cargo.toml b/yaml_test_runner/Cargo.toml deleted file mode 100644 index f64a28e0..00000000 --- a/yaml_test_runner/Cargo.toml +++ /dev/null @@ -1,45 +0,0 @@ -[package] -name = "yaml_test_runner" -version = "9.1.0-alpha.1" -publish = false -edition = "2018" -authors = ["Elastic and Contributors"] -description = "Generates and runs tests from Elasticsearch's YAML test specs" -repository = "https://github.com/elastic/elasticsearch-rs" -license = "Apache-2.0" - -[dependencies] -elasticsearch = { path = "./../elasticsearch", features = ["experimental-apis"]} -api_generator = { path = "./../api_generator" } - -anyhow = "1.0" -base64 = "0.22" -clap = "4" -itertools = "0.13" -Inflector = "0.11" -lazy_static = "1.5" -log = "0.4" -once_cell = "1" -path-slash = "0.2" -quote = "0.3" -regex = "1" -reqwest = "0.12" -semver = "1" -serde = "1" -serde_yaml = "0.9" -serde_json = { version = "1", features = ["arbitrary_precision"] } -simple_logger = "5" -syn = { version = "0.11", features = ["full"] } -url = "2" -yaml-rust = "0.4" -tar = "0.4" -flate2 = "1" -globset = "0.4" - -[dev-dependencies] -tokio = { version = "1", default-features = false, features = ["macros", "net", "time"] } - -[lints.clippy] -# yaml tests contain approximate values of PI -approx_constant = "allow" -uninlined_format_args = "allow" # too pedantic diff --git a/yaml_test_runner/skip.yml b/yaml_test_runner/skip.yml deleted file mode 100644 index 1aa708d4..00000000 --- a/yaml_test_runner/skip.yml +++ /dev/null @@ -1,147 +0,0 @@ -# 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 - - stash_path_replace - - embedded_stash_key - -# tests to skip generating and compiling a test for -tests: - - platinum/api_key/10_basic.yml: - # Regex contains variable substitutions that the test harness doesn't support yet - - "Test invalidate api keys" - - platinum/analytics/histogram.yml: - # Regex doesn't match. Could be either the leading space or escaping of '[' - - "Histogram requires values in increasing order" - - platinum/data_stream/40_supported_apis.yml: - # uses number as a key into object. serde_json::Value expects a string key - - "Verify shard stores api" - - platinum/data_stream/80_resolve_index_data_streams.yml: - # Regex not properly interpreted by the test harness - - "Resolve index with hidden and closed indices" - # expected value json["indices"][0]["name"] to match String("test_index1") but was String(".security-7") - - "Resolve index with indices, aliases, and data streams" - - platinum/license/30_enterprise_license.yml: - # Warning "Including [accept_enterprise] in get license requests is deprecated" is no more present - - "Installing enterprise license" - - platinum/ml/bucket_correlation_agg.yml: - # Expects true, got 0 (i.e. falsy) - - "Test correlation bucket agg simple" - - platinum/ml/filter_crud.yml: - # Warning "this request accesses system indices..." to match is wrong on all tests - - "*" - - platinum/ml/inference_crud.yml: - # expected response to be 404 but was 200 - # Missing cleanup as running on a fresh ES server succeeds - - "Test delete model alias with missing alias" - - "Test delete model alias where alias points to different model" - - "Test update model alias with model id referring to missing model" - - platinum/runtime_fields/20_long.yml: - # expected value ... to match Number(40.0) but was Number(40) - - "terms agg" - - platinum/searchable_snapshots/10_usage.yml: - # 2nd test fails and causes subsequent tests to timeout. Need to see if some extra cleanup is needed. - - "*" - - platinum/service_accounts/10_basic.yml: - # expected value json["count"] to match 2 but was Number(1) - - "Test service account tokens" - - platinum/snapshot/10_basic.yml: - # Expect 1 but got 2 - - "Create a source only snapshot and then restore it" - - platinum/snapshot/20_operator_privileges_disabled.yml: - # expected value json["persistent"]["xpack"]["security"]["http"]["filter"]["deny"] to match String("example.com") - # but was String("tutorial.com") - - "Operator only settings can be set and restored by non-operator user when operator privileges is disabled" - - platinum/ssl/10_basic.yml: - # this test returns the CA cert before the cert, so always fails - - "Test get SSL certificates" - - platinum/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" - - platinum/transform/transforms_update.yml: - # In setup: 409 Conflict - Cannot delete transform [airline-transform-stats-continuous] as the task is running. - - "Test alias scenarios" - - # Test generator currently doesn't handle unsigned long values. - # We skip all tests as even the setup code will fail to compile. - platinum/unsigned_long/10_basic.yml: - - "*" - platinum/unsigned_long/20_null_value.yml: - - "*" - platinum/unsigned_long/30_multi_fields.yml: - - "*" - platinum/unsigned_long/40_different_numeric.yml: - - "*" - platinum/unsigned_long/50_script_values.yml: - - "*" - platinum/unsigned_long/60_collapse.yml: - - "*" - - platinum/xpack/20_info.yml: - # Expects "trial" license but gets "enterprise" - - "XPack Info API" - - free/cat.aliases/10_basic.yml: - # this test fails as the regex needs a \n before the ending $ - - "Multiple alias names" - - free/cat.templates/10_basic.yml: - # Regex do not account for hidden templates returned by the request - - "Multiple template" - - "No templates" - - "Sort templates" - - free/indices.segments/10_basic.yml: - # uses number as a key into object. serde_json::Value expects a string key - - "basic segments test" - - free/indices.get_alias/10_basic.yml: - # expected value at &json["test_index_2"] to be false but was Object({"aliases": Object({"test_alias": Object({})})}) - - "Get alias against closed indices" - - free/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" - - free/indices.stats/12_level.yml: - # uses number as a key into object. serde_json::Value expects a string key - - "Level - shards" - - free/nodes.info/10_basic.yml: - # node has a lot more roles than those checked in the test (expects "ingest", finds "data_cold") - - "node_info role test" - - free/search.aggregation/40_range.yml: - # Setup uses 64 bits longs, and the code generator expects 32 bits - - "*" - - free/search.aggregation/250_moving_fn.yml: - # The use of an invalid window interval results in a 400 response which looks like - # it suppresses the sending of deprecation headers - - "Bad window deprecated interval" - - free/tsdb/80_index_resize.yml: - # Undeclared "node_id" variable in setup - - "*" diff --git a/yaml_test_runner/src/generator.rs b/yaml_test_runner/src/generator.rs deleted file mode 100644 index 3369c808..00000000 --- a/yaml_test_runner/src/generator.rs +++ /dev/null @@ -1,606 +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 crate::step::*; -use anyhow::anyhow; -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; -use std::{ - collections::{BTreeMap, HashSet}, - fs, - fs::{File, OpenOptions}, - io::Write, - path::{Component, Path, PathBuf}, -}; -use yaml_rust::{Yaml, YamlLoader}; - -/// The test suite to compile -#[derive(Debug, PartialEq)] -pub enum TestSuite { - Free, - XPack, -} - -/// The components of a test file, constructed from a yaml file -struct YamlTests<'a> { - path: String, - version: &'a Version, - skip: &'a GlobalSkip, - suite: TestSuite, - directives: HashSet, - setup: Option, - teardown: Option, - tests: Vec, -} - -impl<'a> YamlTests<'a> { - pub fn new( - path: &'a Path, - version: &'a semver::Version, - skip: &'a GlobalSkip, - suite: TestSuite, - len: usize, - ) -> Self { - let path = path.to_slash_lossy(); - Self { - path: path.to_string(), - version, - skip, - suite, - directives: HashSet::with_capacity(len), - setup: None, - teardown: None, - tests: Vec::with_capacity(len), - } - } - - /// Collects the use directives required for all steps and tests - fn use_directives_from_steps(steps: &[Step]) -> Vec { - steps - .iter() - .filter_map(Step::r#do) - .filter_map(|d| d.namespace()) - .map(|s| s.to_string()) - .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 { - self.directives.insert(directive); - } - - self.setup = Some(setup); - 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 { - self.directives.insert(directive); - } - - self.teardown = Some(teardown); - 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 { - self.directives.insert(directive); - } - - self.tests.push(test_fn); - 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_setup_call = match self.suite { - TestSuite::Free => 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); - - let directives: Vec = self - .directives - .iter() - .map(|n| { - let ident = syn::Ident::from(n.as_str()); - quote!(use elasticsearch::#ident::*;) - }) - .collect(); - - quote! { - #![allow(unused_imports, unused_variables, dead_code)] - use crate::common::{client, macros, transform}; - use elasticsearch::*; - use elasticsearch::http::{ - headers::{HeaderName, HeaderValue}, - request::{JsonBody, Body}, - Method, - }; - use elasticsearch::params::*; - #(#directives)* - use ::regex; - use serde_json::{json, Value}; - - #setup_fn - #teardown_fn - #(#tests)* - } - } - - /// 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 (method, status_code, text, json) = client::read_response(response).await?; - }); - } - - true - } - - /// Whether the test should be skipped - fn skip_test(&self, name: &str) -> bool { - if let Some(tests) = self.skip.tests.get(&self.path) { - tests.iter().any(|n| n == name || n == "*") - } else { - false - } - } - - fn fn_impls( - &self, - general_setup_call: Tokens, - setup_call: Option, - teardown_call: Option, - ) -> Vec> { - let mut seen_names = HashSet::new(); - - self.tests - .iter() - .map(|test_fn| { - let name = test_fn.name(); - let unique_name = test_fn.unique_name(&mut seen_names); - if self.skip_test(name) { - info!( - r#"skipping "{}" in {} because it's included in skip.yml"#, - name, - self.path, - ); - return None; - } - - let fn_name = syn::Ident::from(unique_name.as_str()); - let mut body = Tokens::new(); - 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) { - let m = format!( - r#"skipping "{}" in {} because version "{}" is met. {}"#, - name, - &self.path, - s.version(), - s.reason() - ); - Some(m) - } else if s.skip_features(&self.skip.features) { - let m = format!( - r#"skipping "{}" in {} because it needs features "{:?}" which are currently not implemented"#, - name, - &self.path, - s.features() - ); - Some(m) - } else { - None - } - } - Step::Do(d) => { - read_response = d.to_tokens(false, &mut body); - } - Step::Match(m) => { - read_response = Self::read_response(read_response,&mut body); - m.to_tokens(&mut body); - } - Step::Set(s) => { - read_response = Self::read_response(read_response, &mut body); - s.to_tokens(&mut body); - } - Step::Length(l) => { - read_response = Self::read_response(read_response,&mut body); - l.to_tokens(&mut body); - }, - Step::IsTrue(t) => { - read_response = Self::read_response(read_response,&mut body); - t.to_tokens(&mut body); - }, - Step::IsFalse(f) => { - read_response = Self::read_response(read_response, &mut body); - f.to_tokens(&mut body); - }, - Step::Comparison(c) => { - 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); - } - } - } - - match skip { - Some(s) => { - info!("{}", s); - None - }, - None => Some(quote! { - #[tokio::test] - async fn #fn_name() -> anyhow::Result<()> { - let client = client::get(); - #general_setup_call - #setup_call - #body - #teardown_call - Ok(()) - } - }), - } - }) - .collect() - } - - /// Generates the AST for the fixture fn and its invocation - 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 = t - .steps - .iter() - .filter_map(Step::r#do) - .map(|d| { - let mut tokens = Tokens::new(); - ToTokens::to_tokens(d, &mut tokens); - tokens - }) - .collect::>(); - - ( - Some(quote! { - async fn #ident(client: &Elasticsearch) -> anyhow::Result<()> { - #(#tokens)* - Ok(()) - } - }), - Some(quote! { #ident(client).await?; }), - ) - } else { - (None, None) - } - } -} - -/// A test function -struct TestFn { - name: String, - steps: Vec, -} - -impl TestFn { - pub fn new>(name: S, steps: Vec) -> Self { - Self { - name: name.into(), - steps, - } - } - - /// 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 { - 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 - } -} - -/// Items to globally skip -#[derive(Deserialize)] -struct GlobalSkip { - features: Vec, - tests: BTreeMap>, -} - -pub fn generate_tests_from_yaml( - api: &Api, - suite: &TestSuite, - version: &semver::Version, - base_download_dir: &PathBuf, - download_dir: &PathBuf, - generated_dir: &PathBuf, -) -> anyhow::Result<()> { - let skips = serde_yaml::from_str::(include_str!("./../skip.yml"))?; - let paths = fs::read_dir(download_dir)?; - for entry in paths.flatten() { - if let Ok(file_type) = entry.file_type() { - if file_type.is_dir() { - generate_tests_from_yaml( - api, - suite, - version, - base_download_dir, - &entry.path(), - generated_dir, - )?; - } else if file_type.is_file() { - let path = entry.path(); - // skip non-yaml files - let extension = path.extension().unwrap_or_else(|| "".as_ref()); - if extension != "yml" && extension != "yaml" { - continue; - } - - let relative_path = path.strip_prefix(base_download_dir)?; - let test_suite = { - let components = relative_path.components(); - let mut top_dir = "".to_string(); - for c in components { - if c != Component::RootDir { - top_dir = c.as_os_str().to_string_lossy().into_owned(); - break; - } - } - - match top_dir.as_str() { - "free" => TestSuite::Free, - "xpack" | "platinum" => TestSuite::XPack, - dir => panic!("Unknown test suite '{}' {:?}", dir, path), - } - }; - - if &test_suite != suite { - // Belongs to another test suite - continue; - } - - let yaml = fs::read_to_string(entry.path()).unwrap(); - - // 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() { - error!( - "skipping {}. cannot read as Yaml struct: {}", - relative_path.to_slash_lossy(), - result.err().unwrap() - ); - continue; - } - - let docs = result.unwrap(); - let mut test = - YamlTests::new(relative_path, version, &skips, test_suite, docs.len()); - - let results : Vec> = docs - .iter() - .map(|doc| { - let hash = doc - .as_hash() - .ok_or_else(|| anyhow!( - "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(anyhow!( - "expected string key and array value in {:?}, but found {:?} and {:?}", - relative_path, - &k, - &v, - )) - } - } - }) - .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, relative_path, generated_dir)?, - Err(e) => { - info!("skipping {} because {}", relative_path.to_slash_lossy(), e) - } - } - } - } - } - - write_mod_files(generated_dir, true)?; - - Ok(()) -} - -/// Writes a mod.rs file in each generated directory -fn write_mod_files(generated_dir: &PathBuf, toplevel: bool) -> anyhow::Result<()> { - if !generated_dir.exists() { - fs::create_dir(generated_dir)?; - } - - let paths = fs::read_dir(generated_dir)?; - let mut mods = vec![]; - for path in paths.flatten() { - let path = path.path(); - let name = path.file_stem().unwrap().to_string_lossy(); - - if name != "mod" { - mods.push(format!( - "pub mod {};", - path.file_stem().unwrap().to_string_lossy() - )); - } - - if path.is_dir() && !(toplevel && name == "common") { - write_mod_files(&path, false)?; - } - } - - // Make sure we have a stable output - mods.sort(); - - if toplevel { - // The "common" module must appear first so that its macros are parsed before the - // compiler visits other modules, otherwise we'll have "macro not found" errors. - mods.retain(|name| name != "pub mod common;"); - mods.insert(0, "#[macro_use]".into()); - mods.insert(1, "pub mod common;".into()); - mods.insert(2, "".into()); - } - - 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 test_file_path(relative_path: &Path) -> anyhow::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() - .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 - if file_name.starts_with(char::is_numeric) { - relative.set_file_name(format!("_{}", file_name)); - } - - Ok(relative) -} - -fn write_test_file( - test: YamlTests, - relative_path: &Path, - generated_dir: &Path, -) -> anyhow::Result<()> { - if test.skip_test("*") { - info!( - r#"skipping all tests in {} because it's included in skip.yml"#, - test.path, - ); - return Ok(()); - } - - 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)?; - 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. -// Run the following in the root of the repo: -// -// cargo run -p yaml_test_runner -- --branch --token --path -// ----------------------------------------------- -"# - .as_bytes(), - )?; - - let tokens = test.build(); - let generated = tokens.to_string(); - let mut file = OpenOptions::new().append(true).open(&path)?; - file.write_all(generated.as_bytes())?; - file.write_all(b"\n")?; - - Ok(()) -} diff --git a/yaml_test_runner/src/main.rs b/yaml_test_runner/src/main.rs deleted file mode 100644 index a67b070f..00000000 --- a/yaml_test_runner/src/main.rs +++ /dev/null @@ -1,152 +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. - */ -#![recursion_limit = "256"] - -extern crate api_generator; -#[macro_use] -extern crate lazy_static; -#[macro_use] -extern crate log; -#[macro_use] -extern crate quote; - -extern crate simple_logger; - -use anyhow::bail; -use clap::{Arg, Command}; -use log::LevelFilter; -use serde_json::Value; -use std::{fs, path::PathBuf, process::exit}; - -mod generator; -mod regex; -mod step; - -use generator::TestSuite; - -fn main() -> anyhow::Result<()> { - simple_logger::SimpleLogger::new() - .with_level(LevelFilter::Info) - .init() - .unwrap(); - - let matches = Command::new(env!("CARGO_PKG_NAME")) - .about(env!("CARGO_PKG_DESCRIPTION")) - .arg(Arg::new("url") - .short('u') - .long("url") - .value_name("ELASTICSEARCH_URL") - .help("The url of a running Elasticsearch cluster. Used to determine the version, test suite and branch to use to compile tests") - .required(true)) - .get_matches(); - - let url = matches - .get_one::("url") - .expect("missing 'url' argument"); - let (branch, suite, version, sem_version) = - match branch_suite_and_version_from_elasticsearch(url) { - Ok(v) => v, - Err(e) => { - error!( - "Problem getting values from Elasticsearch at {}. {:?}", - url, e - ); - exit(1); - } - }; - - info!("Using version {}", &version); - info!("Using branch {}", &branch); - info!("Using test_suite {:?}", &suite); - - let stack_version = std::env::var("STACK_VERSION").expect("Missing STACK_VERSION env var"); - - if version != stack_version { - bail!( - "ES server version {} is inconsistent with STACK_VERSION={}", - version, - stack_version - ); - } - - let rest_specs_dir = PathBuf::from(format!("./checkout/{}/rest-api-spec/api", stack_version)); - if !rest_specs_dir.is_dir() { - bail!("No specs found at {:?}", rest_specs_dir); - } - - let download_dir = PathBuf::from(format!("./checkout/{}/rest-api-spec/test", stack_version)); - let generated_dir = PathBuf::from(format!("./{}/tests", env!("CARGO_PKG_NAME"))); - - let api = api_generator::generator::read_api(&rest_specs_dir)?; - - // delete everything under the generated_dir except common dir - if generated_dir.exists() { - let entries = fs::read_dir(&generated_dir)?; - for entry in entries.flatten() { - if let Ok(f) = entry.file_type() { - if entry.file_name() != "common" { - if f.is_dir() { - fs::remove_dir_all(entry.path())?; - } else if f.is_file() { - fs::remove_file(entry.path())?; - } - } - } - } - } - - generator::generate_tests_from_yaml( - &api, - &suite, - &sem_version, - &download_dir, - &download_dir, - &generated_dir, - )?; - - Ok(()) -} - -fn branch_suite_and_version_from_elasticsearch( - url: &str, -) -> anyhow::Result<(String, TestSuite, String, semver::Version)> { - let client = reqwest::blocking::ClientBuilder::new() - .danger_accept_invalid_certs(true) - .build()?; - - let suite = match std::env::var("TEST_SUITE") { - Err(_) => panic!("Env var TEST_SUITE is not defined"), - Ok(ref s) if s == "free" => TestSuite::Free, - _ => TestSuite::XPack, - }; - let response = client.get(url).send()?; - let json: Value = response.json()?; - let branch = json["version"]["build_hash"].as_str().unwrap().to_string(); - - // 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 = json["version"]["number"].as_str().unwrap().to_string(); - - let sem_version = - semver::Version::parse(version.trim_end_matches(|c: char| c.is_alphabetic() || c == '-'))?; - - Ok((branch, suite, version, sem_version)) -} diff --git a/yaml_test_runner/src/regex.rs b/yaml_test_runner/src/regex.rs deleted file mode 100644 index 7028cf21..00000000 --- a/yaml_test_runner/src/regex.rs +++ /dev/null @@ -1,76 +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 ::regex::{Captures, 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 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{10,}?)(\s*[,}\]])").unwrap(); -} - -/// 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() -} - -/// 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(), |c: &Captures| match &c[2].parse::() { - Ok(i) if *i > i32::MAX as i64 => format!("{}{}i64{}", &c[1], &c[2], &c[3]), - _ => c[0].to_string(), - }) - .into_owned() -} diff --git a/yaml_test_runner/src/step/comparison.rs b/yaml_test_runner/src/step/comparison.rs deleted file mode 100644 index ecf2fb13..00000000 --- a/yaml_test_runner/src/step/comparison.rs +++ /dev/null @@ -1,102 +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 super::Step; -use crate::step::Expr; -use anyhow::anyhow; -use quote::{ToTokens, Tokens}; -use yaml_rust::Yaml; - -pub const OPERATORS: [&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) -> anyhow::Result { - let hash = yaml - .as_hash() - .ok_or_else(|| anyhow!("expected hash but found {:?}", yaml))?; - - let (k, v) = hash.iter().next().unwrap(); - let expr = k - .as_str() - .ok_or_else(|| anyhow!("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); - tokens.append(quote! { - assert_comparison!(&json#ident, #op_ident #t); - }); - } -} - -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 => { - match self.value.as_str() { - // handle "set" values - 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); - tokens.append(quote! { - 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/src/step/contains.rs b/yaml_test_runner/src/step/contains.rs deleted file mode 100644 index bfab1538..00000000 --- a/yaml_test_runner/src/step/contains.rs +++ /dev/null @@ -1,93 +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 super::Step; -use crate::step::{json_string_from_yaml, Expr}; -use anyhow::anyhow; -use quote::{ToTokens, Tokens}; -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) -> anyhow::Result { - let hash = yaml - .as_hash() - .ok_or_else(|| anyhow!("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); - } - } - } -} diff --git a/yaml_test_runner/src/step/do.rs b/yaml_test_runner/src/step/do.rs deleted file mode 100644 index 62bab53a..00000000 --- a/yaml_test_runner/src/step/do.rs +++ /dev/null @@ -1,929 +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 super::{ok_or_accumulate, Step}; -use crate::regex::{clean_regex, *}; -use anyhow::anyhow; -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}; - -/// 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_response_status_code!(response, #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! { - assert_request_status_code!(response.status_code()); - }); - } - "unavailable" => http_status_code(503, tokens), - "param" => { - // Not possible to pass a bad param to the client so ignore. - } - s => { - let t = clean_regex(s); - tokens.append(quote! { - assert_regex_match!(&text, #t); - }); - } - } - } -} - -pub struct Do { - api_call: ApiCall, - warnings: Vec, - allowed_warnings: Vec, - catch: Option, -} - -impl ToTokens for Do { - fn to_tokens(&self, tokens: &mut Tokens) { - let _ = self.to_tokens(false, tokens); - } -} - -impl From for Step { - fn from(d: Do) -> Self { - Step::Do(d) - } -} - -impl Do { - pub fn to_tokens(&self, mut read_response: bool, tokens: &mut Tokens) -> bool { - self.api_call.to_tokens(tokens); - - // Filter out [types removal] warnings in all cases, same as the java runner. This should - // really be in the yaml tests themselves - if !self.warnings.is_empty() { - tokens.append(quote! { - let warnings: Vec<&str> = response - .warning_headers() - .filter(|w| !w.starts_with("[types removal]")) - .collect(); - }); - for warning in &self.warnings { - tokens.append(quote! { - assert_warnings_contain!(warnings, #warning); - }); - } - } else if !self.allowed_warnings.is_empty() { - let allowed = &self.allowed_warnings; - tokens.append(quote! { - let allowed_warnings = [#(#allowed),*]; - let warnings: Vec<&str> = response.warning_headers() - .filter(|w| !w.starts_with("[types removal]") && !allowed_warnings.iter().any(|a| w.contains(a))) - .collect(); - assert_warnings_is_empty!(warnings); - }); - } - - if let Some(c) = &self.catch { - if !read_response && c.needs_response_body() { - read_response = true; - tokens.append(quote! { - let (method, status_code, text, json) = client::read_response(response).await?; - }); - } - c.to_tokens(tokens); - } - - if let Some(i) = &self.api_call.ignore { - tokens.append(quote! { - assert_response_success_or!(response, #i); - }); - } - - read_response - } - - pub fn try_parse(api: &Api, yaml: &Yaml) -> anyhow::Result { - let hash = yaml - .as_hash() - .ok_or_else(|| anyhow!("expected hash but found {:?}", yaml))?; - - 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)| { - let key = k - .as_str() - .ok_or_else(|| anyhow!("expected string but found {:?}", k))?; - - match key { - "headers" => { - let hash = v - .as_hash() - .ok_or_else(|| anyhow!("expected hash but found {:?}", v))?; - for (hk, hv) in hash.iter() { - let h = hk - .as_str() - .ok_or_else(|| anyhow!("expected string but found {:?}", hk))?; - let v = hv - .as_str() - .ok_or_else(|| anyhow!("expected string but found {:?}", hv))?; - headers.insert(h.into(), v.into()); - } - Ok(()) - } - "catch" => { - catch = v.as_str().map(|s| Catch(s.to_string())); - Ok(()) - } - "node_selector" => Ok(()), - "warnings" => { - warnings = to_string_vec(v); - Ok(()) - } - "allowed_warnings" => { - allowed_warnings = to_string_vec(v); - Ok(()) - } - api_call => { - call = Some((api_call, v)); - Ok(()) - } - } - }) - .collect(); - - ok_or_accumulate(&results)?; - - let (call, value) = call.ok_or_else(|| anyhow!("no API found in do"))?; - let endpoint = api - .endpoint_for_api_call(call) - .ok_or_else(|| anyhow!(r#"no API found for "{}""#, call))?; - let api_call = ApiCall::try_from(api, endpoint, value, headers)?; - - Ok(Do { - api_call, - catch, - warnings, - allowed_warnings, - }) - } - - pub fn namespace(&self) -> Option<&String> { - self.api_call.namespace.as_ref() - } -} - -/// The components of an API call -pub struct ApiCall { - pub namespace: Option, - function: syn::Ident, - parts: Option, - params: Option, - headers: BTreeMap, - 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; - - let headers: Vec = self - .headers - .iter() - .map(|(k, v)| { - // header names **must** be lowercase to satisfy Header lib - let k = k.to_lowercase(); - - // 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_str().unwrap()).as_ref())?) - } - } else { - quote! { .header( - HeaderName::from_static(#k), - HeaderValue::from_static(#v)) - } - } - }) - .collect(); - - tokens.append(quote! { - let response = client.#function(#parts) - #(#headers)* - #params - #body - .send() - .await?; - }); - } -} - -impl ApiCall { - /// Try to create an API call - pub fn try_from( - api: &Api, - endpoint: &ApiEndpoint, - yaml: &Yaml, - headers: BTreeMap, - ) -> anyhow::Result { - let hash = yaml - .as_hash() - .ok_or_else(|| anyhow!("expected hash but found {:?}", yaml))?; - - 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() { - 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), - } - } - key if endpoint.params.contains_key(key) || api.common_params.contains_key(key) => { - params.push((key, v)) - } - key => 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, - headers, - body, - ignore, - }) - } - - fn generate_enum( - enum_name: &str, - variant: &str, - options: &[serde_json::Value], - ) -> anyhow::Result { - if !variant.is_empty() && !options.contains(&serde_json::Value::String(variant.to_owned())) - { - return Err(anyhow!( - "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(anyhow!("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, - params: &[(&str, &Yaml)], - ) -> anyhow::Result> { - 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(anyhow!(r#"no param found for "{}""#, n)), - }, - }?; - - let kind = &ty.ty; - - match v { - Yaml::String(ref s) => { - let is_set_value = s.starts_with('$'); - - 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| Self::generate_enum(n, e, &ty.options)) - .collect(); - - match ok_or_accumulate(&idents) { - Ok(_) => { - let idents: Vec = idents - .into_iter() - .filter_map(Result::ok) - .collect(); - - tokens.append(quote! { - .#param_ident(&[#(#idents),*]) - }); - } - Err(e) => return Err(anyhow!(e)), - } - } else { - let e = Self::generate_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 => match s.parse::() { - Ok(b) => tokens.append(quote! { - .#param_ident(#b) - }), - Err(e) => { - return Err(anyhow!( - r#"cannot parse bool from "{}" for param "{}", {}"#, - s, - n, - e - )) - } - }, - TypeKind::Double => match s.parse::() { - Ok(f) => tokens.append(quote! { - .#param_ident(#f) - }), - Err(e) => { - return Err(anyhow!( - r#"cannot parse f64 from "{}" for param "{}", {}"#, - s, - n, - e - )) - } - }, - TypeKind::Integer => { - if is_set_value { - let set_value = Self::from_set_value(s); - tokens.append(quote! { - .#param_ident(#set_value.as_i64().unwrap() as i32) - }); - } else { - match s.parse::() { - Ok(i) => tokens.append(quote! { - .#param_ident(#i) - }), - Err(e) => { - return Err(anyhow!( - r#"cannot parse i32 from "{}" for param "{}", {}"#, - s, - n, - e - )) - } - } - } - } - TypeKind::Number | TypeKind::Long => { - if is_set_value { - let set_value = Self::from_set_value(s); - tokens.append(quote! { - .#param_ident(#set_value.as_i64().unwrap()) - }); - } else { - let i = s.parse::()?; - tokens.append(quote! { - .#param_ident(#i) - }); - } - } - _ => { - // handle set values - let t = if is_set_value { - let set_value = Self::from_set_value(s); - quote! { #set_value.as_str().unwrap() } - } else { - quote! { #s } - }; - - tokens.append(quote! { - .#param_ident(#t) - }) - } - } - } - 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(anyhow!("unsupported array value {:?}", y)), - }) - .filter_map(Result::ok) - .collect(); - - if n == &"expand_wildcards" { - let result: Vec> = result - .iter() - .map(|s| Self::generate_enum(n, s.as_str(), &ty.options)) - .collect(); - - match ok_or_accumulate(&result) { - Ok(_) => { - let result: Vec = - result.into_iter().filter_map(Result::ok).collect(); - - tokens.append(quote! { - .#param_ident(&[#(#result),*]) - }); - } - Err(e) => return Err(anyhow!(e)), - } - } else { - tokens.append(quote! { - .#param_ident(&[#(#result),*]) - }); - } - } - Yaml::Real(r) => match kind { - TypeKind::Long | TypeKind::Number => { - let f = r.parse::()?; - tokens.append(quote! { - .#param_ident(#f as i64) - }); - } - _ => { - let f = r.parse::()?; - tokens.append(quote! { - .#param_ident(#f) - }); - } - }, - _ => println!("unsupported value {:?} for param {}", v, n), - } - } - - Ok(Some(tokens)) - } - } - } - - 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('$') - .trim_start_matches('{') - .trim_end_matches('}'), - ); - quote! { #ident } - } else { - // only part of the string is a token, so substitute - let token = syn::Ident::from( - SET_DELIMITED_REGEX - .captures(s) - .unwrap() - .get(1) - .unwrap() - .as_str(), - ); - let replacement = SET_DELIMITED_REGEX.replace_all(s, "{}"); - quote! { Value::String(format!(#replacement, #token.as_str().unwrap())) } - } - } - - fn generate_parts( - api_call: &str, - endpoint: &ApiEndpoint, - parts: &[(&str, &Yaml)], - ) -> anyhow::Result> { - // 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. - // - // 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 - .iter() - .map(|p| p.path.params().len()) - .collect::>(); - - // check there's actually a None value - if !param_counts.contains(&0) { - return Err(anyhow!( - r#"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]), - } - } - } - .ok_or_else(|| { - anyhow!( - r#"no path for "{}" API with url parts {:?}"#, - &api_call, - parts - ) - })?; - - 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 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(); - f.cmp(&s) - }) - .map(|(p, v)| { - let ty = path - .parts - .get(*p) - .ok_or_else(|| anyhow!(r#"no url part found for "{}" in {}"#, p, &path.path))?; - - match v { - Yaml::String(s) => { - let is_set_value = s.starts_with('$') || s.contains("${"); - - match ty.ty { - TypeKind::List => { - let values: Vec = s - .split(',') - .map(|s| { - if is_set_value { - let set_value = Self::from_set_value(s); - quote! { #set_value.as_str().unwrap() } - } else { - quote! { #s } - } - }) - .collect(); - Ok(quote! { &[#(#values),*] }) - } - TypeKind::Long => { - if is_set_value { - let set_value = Self::from_set_value(s); - Ok(quote! { #set_value.as_i64().unwrap() }) - } else { - let l = s.parse::().unwrap(); - Ok(quote! { #l }) - } - } - _ => { - if is_set_value { - let set_value = Self::from_set_value(s); - Ok(quote! { #set_value.as_str().unwrap() }) - } else { - Ok(quote! { #s }) - } - } - } - } - Yaml::Boolean(b) => { - let s = b.to_string(); - Ok(quote! { #s }) - } - Yaml::Integer(l) => match ty.ty { - TypeKind::Long => Ok(quote! { #l }), - TypeKind::Integer => { - let i = *l as i32; - Ok(quote! { #i }) - } - _ => { - let s = l.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(anyhow!("unsupported array value {:?}", y)), - }) - .collect(); - - match ok_or_accumulate(&result) { - 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(anyhow!(e)), - } - } - _ => Err(anyhow!("unsupported value {:?}", v)), - } - }) - .collect(); - - match ok_or_accumulate(&part_tokens) { - 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(anyhow!(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) -> anyhow::Result> { - match v { - Yaml::Null => Ok(None), - Yaml::String(s) => { - let json = { - let json = replace_set(s); - replace_i64(json) - }; - 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 - // empty or a more-indented line (using >) - // see https://yaml.org/spec/1.2/spec.html#id2760844 - // - // 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::>() - } 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(); - Ok(Some(quote!(.body(vec![#(#values),*])))) - } else { - let ident = syn::Ident::from(json); - Ok(Some(quote!(.body(json!{#ident})))) - } - } - _ => { - let mut s = String::new(); - { - let mut emitter = YamlEmitter::new(&mut s); - emitter.dump(v).unwrap(); - } - - if endpoint.supports_nd_body() { - let values: Vec = serde_yaml::from_str(&s)?; - let json: Vec = values - .iter() - .map(|value| { - let mut json = serde_json::to_string(&value).unwrap(); - if value.is_string() { - json = replace_set(&json); - let ident = syn::Ident::from(json); - quote!(Box::new(String::from(#ident))) - } else { - json = replace_set(json); - json = replace_i64(json); - let ident = syn::Ident::from(json); - quote!(Box::new(JsonBody::from(json!(#ident)))) - } - }) - .collect(); - Ok(Some( - quote!(.body({ let mut v: Vec> = Vec::new(); v.append(&mut vec![ #(#json),* ]); v })), - )) - } else { - let value: serde_json::Value = serde_yaml::from_str(&s)?; - let mut json = serde_json::to_string_pretty(&value)?; - json = replace_set(json); - json = replace_i64(json); - let ident = syn::Ident::from(json); - - Ok(Some(quote!(.body(json!{#ident})))) - } - } - } - } -} diff --git a/yaml_test_runner/src/step/is_false.rs b/yaml_test_runner/src/step/is_false.rs deleted file mode 100644 index cc4ede2d..00000000 --- a/yaml_test_runner/src/step/is_false.rs +++ /dev/null @@ -1,59 +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 super::Step; -use crate::step::Expr; -use anyhow::anyhow; -use quote::{ToTokens, Tokens}; -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) -> anyhow::Result { - let expr = yaml - .as_str() - .ok_or_else(|| anyhow!("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_body() { - tokens.append(quote! { - 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! { - 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 deleted file mode 100644 index 837834b8..00000000 --- a/yaml_test_runner/src/step/is_true.rs +++ /dev/null @@ -1,63 +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 super::Step; -use crate::step::Expr; -use anyhow::anyhow; -use quote::{ToTokens, Tokens}; -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) -> anyhow::Result { - let expr = yaml - .as_str() - .ok_or_else(|| anyhow!("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_body() { - // for a HEAD request, the body is expected to be empty, so check the status code instead. - tokens.append(quote! { - 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! { - assert_is_true!(&json#ident); - }); - } - } -} diff --git a/yaml_test_runner/src/step/length.rs b/yaml_test_runner/src/step/length.rs deleted file mode 100644 index 9ab3349f..00000000 --- a/yaml_test_runner/src/step/length.rs +++ /dev/null @@ -1,75 +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 super::Step; -use crate::step::Expr; -use anyhow::anyhow; -use quote::{ToTokens, Tokens}; -use yaml_rust::Yaml; - -pub struct Length { - pub(crate) expr: Expr, - len: usize, -} - -impl From for Step { - fn from(length: Length) -> Self { - Step::Length(length) - } -} - -impl Length { - pub fn try_parse(yaml: &Yaml) -> anyhow::Result { - let hash = yaml - .as_hash() - .ok_or_else(|| anyhow!("expected hash but found {:?}", yaml))?; - - let (k, v) = hash.iter().next().unwrap(); - - let expr = k - .as_str() - .ok_or_else(|| anyhow!("expected string key but found {:?}", k))?; - - let len = v - .as_i64() - .ok_or_else(|| anyhow!("expected i64 but found {:?}", v))?; - - Ok(Length { - len: len as usize, - expr: expr.into(), - }) - } -} - -impl ToTokens for Length { - fn to_tokens(&self, tokens: &mut Tokens) { - let len = self.len; - - if self.expr.is_body() { - tokens.append(quote! { - assert_length!(&json, #len); - }); - } else { - let expr = self.expr.expression(); - let ident = syn::Ident::from(expr); - tokens.append(quote! { - assert_length!(&json#ident, #len); - }); - } - } -} diff --git a/yaml_test_runner/src/step/match.rs b/yaml_test_runner/src/step/match.rs deleted file mode 100644 index 29bcfab7..00000000 --- a/yaml_test_runner/src/step/match.rs +++ /dev/null @@ -1,160 +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 super::Step; -use crate::{ - regex::clean_regex, - step::{json_string_from_yaml, Expr}, -}; -use anyhow::anyhow; -use quote::{ToTokens, Tokens}; -use yaml_rust::Yaml; - -pub struct Match { - pub expr: Expr, - value: Yaml, -} - -impl From for Step { - fn from(m: Match) -> Self { - Step::Match(m) - } -} - -impl Match { - pub fn try_parse(yaml: &Yaml) -> anyhow::Result { - let hash = yaml - .as_hash() - .ok_or_else(|| anyhow!("expected hash but found {:?}", yaml))?; - - 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.expr.expression(); - - match &self.value { - Yaml::String(s) => { - if s.starts_with('/') { - let s = clean_regex(s); - if self.expr.is_body() { - tokens.append(quote! { - 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, true); - }); - } - } else { - let ident = syn::Ident::from(expr.as_str()); - - // handle set values - 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 { - tokens.append(quote! { - assert_match!(json#ident, json!(#s)); - }) - }; - } - } - Yaml::Integer(i) => { - if self.expr.is_body() { - panic!("match on $body with i64"); - } else { - let ident = syn::Ident::from(expr.as_str()); - tokens.append(quote! { - assert_numeric_match!(json#ident, #i); - }); - } - } - 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_match!(json#ident, json!(#f)); - }); - } - } - 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_null!(json#ident); - }); - } - } - 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_match!(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) - }; - - if self.expr.is_body() { - tokens.append(quote! { - assert_match!(json, json!(#json)); - }); - } else { - let ident = syn::Ident::from(expr.as_str()); - tokens.append(quote! { - assert_match!(json#ident, json!(#json)); - }); - } - } - yaml => { - panic!("Bad yaml value {:?}", &yaml); - } - } - } -} diff --git a/yaml_test_runner/src/step/mod.rs b/yaml_test_runner/src/step/mod.rs deleted file mode 100644 index d681ba3e..00000000 --- a/yaml_test_runner/src/step/mod.rs +++ /dev/null @@ -1,286 +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 crate::regex::*; -use anyhow::anyhow; -use api_generator::generator::Api; -use std::fmt::Write; -use yaml_rust::{Yaml, YamlEmitter}; - -mod comparison; -mod contains; -mod r#do; -mod is_false; -mod is_true; -mod length; -mod r#match; -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::*; -pub use r#do::*; -pub use r#match::*; -pub use set::*; -pub use skip::*; -pub use transform_and_set::*; - -pub fn parse_steps(api: &Api, steps: &[Yaml]) -> anyhow::Result> { - let mut parsed_steps: Vec = Vec::new(); - for step in steps { - let hash = step - .as_hash() - .ok_or_else(|| anyhow!("expected hash but found {:?}", step))?; - - let (key, value) = { - let (k, yaml) = hash.iter().next().unwrap(); - let key = k - .as_str() - .ok_or_else(|| anyhow!("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" => { - let s = Set::try_parse(value)?; - parsed_steps.push(s.into()); - } - "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()); - } - "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()) - } - "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()) - } - op if OPERATORS.contains(&op) => { - let comp = Comparison::try_parse(value, op)?; - parsed_steps.push(comp.into()) - } - op => return Err(anyhow!("unknown step operation: {}", op)), - } - } - - Ok(parsed_steps) -} - -/// 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, -} - -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 "$body" or "", which are both used to express the whole body - pub fn is_body(&self) -> bool { - Self::is_string_body(&self.expr) || self.expr.is_empty() - } - - fn is_string_body(s: &str) -> bool { - s == "$body" - } - - 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 = self.expr.chars(); - while let Some(ch) = chars.next() { - match ch { - '\\' => { - // consume the next character too - let mut maybe_next = chars.next(); - if let Some('\\') = maybe_next { - // Some paths use a double backslash to escape dots - maybe_next = chars.next(); - } - if let Some(next) = maybe_next { - value.push(next); - } - } - '.' => { - values.push(value); - value = String::new(); - } - _ => { - value.push(ch); - } - } - } - values.push(value); - - // some APIs specify the response body as the first part of the path - // which should be removed. - if Self::is_string_body(values[0].as_ref()) { - values.remove(0); - } else if values[0].is_empty() { - // some tests start the json path with a dot, leading to an empty first element - values.remove(0); - } - - let mut expr = String::new(); - for s in values { - 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 - let t = s - .trim_start_matches('$') - .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(); - } - } - expr - } - } -} - -/// 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), - Contains(Contains), - TransformAndSet(TransformAndSet), -} - -impl Step { - /// Gets a Do 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: &[anyhow::Result]) -> anyhow::Result<()> { - let errs = results - .iter() - .filter_map(|r| r.as_ref().err()) - .collect::>(); - if errs.is_empty() { - Ok(()) - } else { - let mut msgs = errs.iter().map(|e| e.to_string()).collect::>(); - msgs.sort(); - msgs.dedup_by(|a, b| a == b); - Err(anyhow!(msgs.join(", "))) - } -} - -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(json); - json = replace_i64(json); - json -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_expr() { - let expr = Expr::new("$body"); - assert!(expr.is_body()); - - let expr = Expr::new("a.b.c"); - assert_eq!(r#"["a"]["b"]["c"]"#, expr.expression()); - - let expr = Expr::new(r#"a.b\.c"#); - assert_eq!(r#"["a"]["b.c"]"#, expr.expression()); - - let expr = Expr::new(r#"a.b\\.c"#); - assert_eq!(r#"["a"]["b.c"]"#, expr.expression()); - - let expr = Expr::new("a.0"); - assert_eq!(r#"["a"][0]"#, expr.expression()); - - let expr = Expr::new("a.${b}.c"); - assert_eq!(r#"["a"][b.as_str().unwrap()]["c"]"#, expr.expression()); - } -} diff --git a/yaml_test_runner/src/step/set.rs b/yaml_test_runner/src/step/set.rs deleted file mode 100644 index 1ae99c61..00000000 --- a/yaml_test_runner/src/step/set.rs +++ /dev/null @@ -1,66 +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 super::Step; -use crate::step::Expr; -use anyhow::anyhow; -use quote::{ToTokens, Tokens}; -use yaml_rust::Yaml; - -pub struct Set { - ident: syn::Ident, - expr: Expr, -} - -impl From for Step { - fn from(set: Set) -> Self { - Step::Set(set) - } -} - -impl Set { - pub fn try_parse(yaml: &Yaml) -> anyhow::Result { - let hash = yaml - .as_hash() - .ok_or_else(|| anyhow!("expected hash but found {:?}", yaml))?; - - let (k, v) = hash.iter().next().unwrap(); - let expr = k - .as_str() - .ok_or_else(|| anyhow!("expected string key but found {:?}", k))?; - - let id = v - .as_str() - .ok_or_else(|| anyhow!("expected string value but found {:?}", v))?; - - 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.expr.expression().as_str()); - tokens.append(quote! { - let #ident = json#expr.clone(); - }); - } -} diff --git a/yaml_test_runner/src/step/skip.rs b/yaml_test_runner/src/step/skip.rs deleted file mode 100644 index a0708b68..00000000 --- a/yaml_test_runner/src/step/skip.rs +++ /dev/null @@ -1,134 +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 super::Step; -use regex::Regex; -use yaml_rust::Yaml; - -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 { - /// 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, - 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::STAR) - } 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 - } - } - - pub fn try_parse(yaml: &Yaml) -> anyhow::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, - reason, - features, - }) - } - - /// Determines if this instance matches the version - pub fn skip_version(&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: &[String]) -> bool { - match &self.features { - Some(test_features) => test_features.iter().any(|f| features.contains(f)), - None => false, - } - } -} diff --git a/yaml_test_runner/src/step/transform_and_set.rs b/yaml_test_runner/src/step/transform_and_set.rs deleted file mode 100644 index e6068fb2..00000000 --- a/yaml_test_runner/src/step/transform_and_set.rs +++ /dev/null @@ -1,128 +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 super::Step; -use crate::step::Expr; -use anyhow::anyhow; -use inflector::Inflector; -use quote::{ToTokens, Tokens}; -use yaml_rust::Yaml; - -pub struct Transformation { - #[allow(dead_code)] - 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(); - for ch in t.chars() { - 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) -> anyhow::Result { - let hash = yaml - .as_hash() - .ok_or_else(|| anyhow!("expected hash but found {:?}", yaml))?; - - let (k, v) = hash.iter().next().unwrap(); - let ident = k - .as_str() - .ok_or_else(|| anyhow!("expected string key but found {:?}", k))?; - - let transformation = v - .as_str() - .ok_or_else(|| anyhow!("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/tests/common/client.rs b/yaml_test_runner/tests/common/client.rs deleted file mode 100644 index 313e0854..00000000 --- a/yaml_test_runner/tests/common/client.rs +++ /dev/null @@ -1,536 +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::{ - auth::Credentials, - cat::CatTemplatesParts, - cert::CertificateValidation, - cluster::ClusterHealthParts, - http::{ - response::Response, - transport::{SingleNodeConnectionPool, TransportBuilder}, - Method, StatusCode, - }, - ilm::IlmRemovePolicyParts, - indices::{ - IndicesDeleteIndexTemplateParts, IndicesDeleteParts, IndicesDeleteTemplateParts, - IndicesRefreshParts, - }, - ml::{ - MlCloseJobParts, MlDeleteDatafeedParts, MlDeleteJobParts, MlGetDatafeedsParts, - MlGetJobsParts, MlStopDatafeedParts, - }, - params::{ExpandWildcards, WaitForStatus}, - security::{ - SecurityDeletePrivilegesParts, SecurityDeleteRoleParts, SecurityDeleteUserParts, - SecurityGetPrivilegesParts, SecurityGetRoleParts, SecurityGetUserParts, - SecurityPutUserParts, - }, - snapshot::{SnapshotDeleteParts, SnapshotDeleteRepositoryParts}, - tasks::TasksCancelParts, - transform::{ - TransformDeleteTransformParts, TransformGetTransformParts, TransformStopTransformParts, - }, - watcher::WatcherDeleteWatchParts, - Elasticsearch, Error, DEFAULT_ADDRESS, -}; -use once_cell::sync::Lazy; -use serde_json::{json, Value}; -use std::ops::Deref; -use url::Url; - -fn cluster_addr() -> String { - match std::env::var("ELASTICSEARCH_URL") { - Ok(server) => server, - Err(_) => DEFAULT_ADDRESS.into(), - } -} - -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 - 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); - let mut builder = TransportBuilder::new(conn_pool); - - builder = match credentials { - Some(c) => builder.auth(c).cert_validation(CertificateValidation::None), - None => builder, - }; - - 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, -) -> anyhow::Result<(Method, StatusCode, String, Value)> { - 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() -> Result<(), Error> { - let client = get(); - delete_data_streams(client).await?; - delete_indices(client).await?; - delete_templates(client).await?; - delete_snapshots(client).await?; - - Ok(()) -} - -pub async fn delete_snapshots(client: &Elasticsearch) -> Result<(), Error> { - let cat_repo_response = client - .cat() - .repositories() - .h(&["id"]) - .send() - .await? - .error_for_status_code()? - .text() - .await?; - - if !cat_repo_response.is_empty() { - let repositories: Vec<&str> = cat_repo_response.split_terminator('\n').collect(); - - // Delete snapshots in each repository - for repo in repositories { - let delete_snapshots_response = client - .snapshot() - .delete(SnapshotDeleteParts::RepositorySnapshot(repo, &["*"])) - .send() - .await?; - - assert_response_success!(delete_snapshots_response); - } - - // Delete all snapshot repositories - let delete_repo_response = client - .snapshot() - .delete_repository(SnapshotDeleteRepositoryParts::Repository(&["*"])) - .send() - .await?; - - assert_response_success!(delete_repo_response); - } - - Ok(()) -} - -/// general setup step for an xpack yaml test -pub async fn general_xpack_setup() -> Result<(), Error> { - let client = get(); - - let _delete_watch_response = client - .watcher() - .delete_watch(WatcherDeleteWatchParts::Id("my_watch")) - .send() - .await?; - - delete_roles(client).await?; - delete_users(client).await?; - delete_privileges(client).await?; - stop_and_delete_datafeeds(client).await?; - - let response = client - .ilm() - .remove_policy(IlmRemovePolicyParts::Index("_all")) - .send() - .await?; - - assert_response_success!(response); - - 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_data_streams(client).await?; - delete_indices(client).await?; - delete_templates(client).await?; - - let response = client - .security() - .put_user(SecurityPutUserParts::Username("x_pack_rest_user")) - .body(json!({ - "password": "x-pack-test-password", - "roles": ["superuser"] - })) - .send() - .await?; - - assert_response_success!(response); - - let response = client - .indices() - .refresh(IndicesRefreshParts::Index(&["_all"])) - .expand_wildcards(&[ - ExpandWildcards::Open, - ExpandWildcards::Closed, - ExpandWildcards::Hidden, - ]) - .send() - .await?; - - assert_response_success!(response); - - wait_for_yellow_status(client).await?; - - Ok(()) -} - -async fn wait_for_yellow_status(client: &Elasticsearch) -> Result<(), Error> { - let cluster_health = client - .cluster() - .health(ClusterHealthParts::None) - .wait_for_status(WaitForStatus::Yellow) - .send() - .await?; - - assert_response_success!(cluster_health); - Ok(()) -} - -async fn delete_data_streams(client: &Elasticsearch) -> Result<(), Error> { - // Hand-crafted request as the indices.delete_data_stream spec doesn't yet have the - // "expand_wildcards" parameter that is needed to delete ILM data streams - // - // Not deleting data streams yields errors like this when trying to delete hidden indices: - // { - // "type":"illegal_argument_exception" - // "reason":"index [.ds-ilm-history-5-2021.02.14-000001] is the write index for data - // stream [ilm-history-5] and cannot be deleted" - // } - // - // Quoting the docs: - // You cannot delete the current write index of a data stream. To delete the index, - // you must roll over the data stream so a new write index is created. You can then use - // the delete index API to delete the previous write index. - // - let delete_response = client - .transport() - .send( - Method::Delete, - "/_data_stream/*", - elasticsearch::http::headers::HeaderMap::new(), - Some(&[("expand_wildcards", "hidden")]), - None::<()>, // body - None, // timeout - ) - .await?; - - assert_response_success!(delete_response); - - 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?; - - assert_response_success!(delete_response); - 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 transform in transforms_response["transforms"].as_array().unwrap() { - let id = transform["id"].as_str().unwrap(); - let response = client - .transform() - .stop_transform(TransformStopTransformParts::TransformId(id)) - .send() - .await?; - - assert_response_success!(response); - - let response = client - .transform() - .delete_transform(TransformDeleteTransformParts::TransformId(id)) - .send() - .await?; - - assert_response_success!(response); - } - - 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 response = client - .tasks() - .cancel(TasksCancelParts::TaskId(task_id)) - .send() - .await?; - - assert_response_success!(response); - } - } - } - } - - Ok(()) -} - -async fn delete_templates(client: &Elasticsearch) -> Result<(), Error> { - // There are "legacy templates and "new templates" - - let cat_template_response = client - .cat() - .templates(CatTemplatesParts::Name("*")) - .h(&["name"]) - .send() - .await? - .text() - .await?; - - let all_templates: Vec<&str> = cat_template_response - .split_terminator('\n') - .filter(|s| !s.starts_with('.') && s != &"security-audit-log") - .collect(); - - for template in all_templates { - if template == "ilm-history" { - // We may need to extend this to mimic ESRestTestCase.isXPackTemplate() from the ES - // test harness - continue; - } - - let mut delete_template_response = client - .indices() - .delete_index_template(IndicesDeleteIndexTemplateParts::Name(template)) - .send() - .await?; - - if delete_template_response.status_code().as_u16() == 404 { - // Certainly an old-style template - delete_template_response = client - .indices() - .delete_template(IndicesDeleteTemplateParts::Name(template)) - .send() - .await?; - } - assert_response_success!(delete_template_response); - } - - Ok(()) -} - -async fn delete_users(client: &Elasticsearch) -> Result<(), Error> { - 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 response = client - .security() - .delete_user(SecurityDeleteUserParts::Username(k)) - .send() - .await?; - - assert_response_success!(response); - } - } - } - - Ok(()) -} - -async fn delete_roles(client: &Elasticsearch) -> Result<(), Error> { - let roles_response = client - .security() - .get_role(SecurityGetRoleParts::None) - .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 response = client - .security() - .delete_role(SecurityDeleteRoleParts::Name(k)) - .send() - .await?; - - assert_response_success!(response); - } - } - } - - 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 response = client - .security() - .delete_privileges(SecurityDeletePrivilegesParts::ApplicationName(k, "_all")) - .send() - .await?; - - assert_response_success!(response); - } - } - } - - 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?; - - assert_response_success!(stop_data_feed_response); - - 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?; - } - - Ok(()) -} - -async fn close_and_delete_jobs(client: &Elasticsearch) -> Result<(), Error> { - let response = client - .ml() - .close_job(MlCloseJobParts::JobId("_all")) - .send() - .await?; - - assert_response_success!(response); - - 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 response = client - .ml() - .delete_job(MlDeleteJobParts::JobId(id)) - .send() - .await?; - - assert_response_success!(response); - } - - Ok(()) -} diff --git a/yaml_test_runner/tests/common/macros.rs b/yaml_test_runner/tests/common/macros.rs deleted file mode 100644 index bb4ef760..00000000 --- a/yaml_test_runner/tests/common/macros.rs +++ /dev/null @@ -1,379 +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. - */ -/// Asserts that a [Response] has a status code >=200 and <300 - -#[macro_export] -macro_rules! assert_response_success { - ($response:ident) => {{ - let status_code = $response.status_code(); - if !status_code.is_success() { - let text = $response - .text() - .await - .unwrap_or("[no response]".to_string()); - panic!( - "expected response to be successful but was {}. Response: {}", - status_code, text - ); - } - }}; -} - -/// 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) => {{ - let status_code = $response.status_code(); - if !status_code.is_success() && status_code.as_u16() != $status { - let text = $response - .text() - .await - .unwrap_or("[no response]".to_string()); - panic!( - "expected response to be successful or {} but was {}", - $status, - status_code.as_u16() - ); - } - }}; -} - -/// Asserts that a [Response] has a status that matches the passed status -#[macro_export] -macro_rules! assert_response_status_code { - ($response:ident, $status:expr) => {{ - let status_code = $response.status_code(); - if status_code.as_u16() != $status { - let text = $response - .text() - .await - .unwrap_or("[no response]".to_string()); - panic!( - "expected response to be {} but was {}", - $status, - status_code.as_u16() - ); - } - }}; -} - -#[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!( - (400..600).contains(&status_code), - "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:expr) => {{ - assert_eq!( - $expected, - $actual, - "expected value {} to match {:?} but was {:?}", - stringify!($expected), - $actual, - $expected - ); - }}; -} - -/// 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 { - ($expected:expr, $actual:expr) => {{ - if $expected.is_i64() { - assert_match!($expected, $actual); - } else { - assert_match!($expected, $actual as f64); - } - }}; -} - -/// Asserts that a [serde_json::Value] is null. -#[macro_export] -macro_rules! assert_null { - ($expected:expr) => {{ - assert!( - $expected.is_null(), - "expected value {} to be null but was {:?}", - stringify!($expected), - $expected - ); - }}; -} - -/// 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).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), - "expected value {} to match regex\n\n{}\n\nbut was\n\n{}", - stringify!($expected), - $regex, - $expected - ); - }}; -} - -/// 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 -/// - 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 - ); - }}; -} - -/// 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 => {} - } - }}; -} - -/// 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 - ); - }}; -} - -/// 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), - } - }}; -} - -/// 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 - ); - } - }}; -} diff --git a/yaml_test_runner/tests/common/mod.rs b/yaml_test_runner/tests/common/mod.rs deleted file mode 100644 index 6e28cfb2..00000000 --- a/yaml_test_runner/tests/common/mod.rs +++ /dev/null @@ -1,22 +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. - */ -#[macro_use] -pub mod macros; -pub mod client; -pub mod transform; diff --git a/yaml_test_runner/tests/common/transform.rs b/yaml_test_runner/tests/common/transform.rs deleted file mode 100644 index 1921456a..00000000 --- a/yaml_test_runner/tests/common/transform.rs +++ /dev/null @@ -1,31 +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 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::engine::general_purpose::STANDARD); - write!(encoder, "{}:", user).unwrap(); - write!(encoder, "{}", password).unwrap(); - }; - String::from_utf8(value).unwrap() -}