use std::{collections::HashSet, str::FromStr};
use anyhow::Result;
use serde::{Deserialize, Serialize};
use serde_yaml::{Error, Value};
const SHARDED_CLUSTER_FIXTURE_NAME: &str = "ShardedClusterFixture";
const REPLICA_SET_FIXTURE_NAME: &str = "ReplicaSetFixture";
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum SuiteFixtureType {
Shell,
Repl,
Shard,
Other,
}
#[derive(Serialize, Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum TestRoot {
Root { root: String },
Roots { roots: Vec<String> },
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ResmokeSelector {
#[serde(skip_serializing_if = "Option::is_none")]
pub exclude_tags: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exclude_files: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exclude_with_any_tags: Option<HashSet<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub group_size: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub group_count_multiplier: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_with_any_tags: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_files: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_tags: Option<String>,
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub test_root: Option<TestRoot>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tag_file: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub test: Option<String>,
}
#[derive(Serialize, Debug, Clone, Deserialize)]
pub struct ResmokeExecutor {
#[serde(skip_serializing_if = "Option::is_none")]
pub archive: Option<Box<Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hooks: Option<Vec<Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub config: Option<Box<Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fixture: Option<Box<Value>>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ResmokeSuiteConfig {
pub test_kind: String,
pub selector: ResmokeSelector,
pub executor: ResmokeExecutor,
}
impl FromStr for ResmokeSuiteConfig {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_yaml::from_str(s)
}
}
impl ToString for ResmokeSuiteConfig {
fn to_string(&self) -> String {
serde_yaml::to_string(self).unwrap()
}
}
impl ResmokeSuiteConfig {
pub fn get_fixture_type(&self) -> SuiteFixtureType {
let executor = &self.executor;
if let Some(fixture) = &executor.fixture {
Self::get_type_from_fixture_class(fixture)
} else {
SuiteFixtureType::Shell
}
}
fn get_type_from_fixture_class(fixture: &Value) -> SuiteFixtureType {
if let Value::Mapping(map) = fixture {
if let Some(Value::String(fixture_class)) = map.get(&Value::String("class".to_string()))
{
return match fixture_class.as_str() {
SHARDED_CLUSTER_FIXTURE_NAME => SuiteFixtureType::Shard,
REPLICA_SET_FIXTURE_NAME => SuiteFixtureType::Repl,
_ => SuiteFixtureType::Other,
};
}
}
SuiteFixtureType::Other
}
pub fn with_new_tests(
&self,
run_tests: Option<&[String]>,
exclude_tests: Option<&[String]>,
) -> Self {
let mut config = self.clone();
let mut updated_selector = self.selector.clone();
if let Some(exclude_tests) = exclude_tests {
let mut files_to_exclude = vec![];
if let Some(excluded_files) = &updated_selector.exclude_files {
files_to_exclude.extend(excluded_files);
}
files_to_exclude.extend(exclude_tests.iter());
updated_selector.exclude_files = Some(
files_to_exclude
.into_iter()
.map(|s| s.to_string())
.collect(),
);
} else if let Some(run_tests) = run_tests {
updated_selector.exclude_files = None;
updated_selector.test_root = Some(TestRoot::Roots {
roots: run_tests.iter().map(|s| s.to_string()).collect(),
});
}
config.selector = updated_selector;
config
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_no_fixture_defined_should_return_shell() {
let config_yaml = "
test_kind: js_test
selector:
roots:
- jstests/auth/*.js
exclude_files:
- jstests/auth/repl.js
executor:
config:
shell_options:
global_vars:
TestData:
roleGraphInvalidationIsFatal: true
nodb: ''
";
let config = ResmokeSuiteConfig::from_str(config_yaml).unwrap();
assert_eq!(config.get_fixture_type(), SuiteFixtureType::Shell);
}
#[test]
fn test_shared_cluster_fixture_should_return_sharded() {
let config_yaml = "
test_kind: js_test
selector:
roots:
- jstests/auth/*.js
exclude_files:
- jstests/auth/repl.js
executor:
config:
shell_options:
global_vars:
TestData:
roleGraphInvalidationIsFatal: true
nodb: ''
fixture:
class: ShardedClusterFixture
num_shards: 2
";
let config = ResmokeSuiteConfig::from_str(config_yaml).unwrap();
assert_eq!(config.get_fixture_type(), SuiteFixtureType::Shard);
}
#[test]
fn test_replica_set_fixture_should_return_repl() {
let config_yaml = "
test_kind: js_test
selector:
roots:
- jstests/auth/*.js
exclude_files:
- jstests/auth/repl.js
executor:
config:
shell_options:
global_vars:
TestData:
roleGraphInvalidationIsFatal: true
nodb: ''
fixture:
class: ReplicaSetFixture
num_nodes: 3
";
let config = ResmokeSuiteConfig::from_str(config_yaml).unwrap();
assert_eq!(config.get_fixture_type(), SuiteFixtureType::Repl);
}
#[test]
fn test_other_fixture_should_return_other() {
let config_yaml = "
test_kind: js_test
selector:
roots:
- jstests/auth/*.js
exclude_files:
- jstests/auth/repl.js
executor:
config:
shell_options:
global_vars:
TestData:
roleGraphInvalidationIsFatal: true
nodb: ''
fixture:
class: SomeOtherFixture
num_nodes: 3
";
let config = ResmokeSuiteConfig::from_str(config_yaml).unwrap();
assert_eq!(config.get_fixture_type(), SuiteFixtureType::Other);
}
#[test]
fn test_with_new_tests_can_add_tests_to_exclude_list() {
let config_yaml = "
test_kind: js_test
selector:
roots:
- jstests/auth/*.js
exclude_files:
- jstests/auth/repl.js
- jstests/core/add1.js
executor:
config:
value
fixture:
class: MyFixture
num_nodes: 3
";
let exclude_test_list = vec!["test0.js".to_string(), "test1.js".to_string()];
let resmoke_suite = ResmokeSuiteConfig::from_str(config_yaml).unwrap();
let new_config = resmoke_suite.with_new_tests(None, Some(&exclude_test_list));
assert!(new_config.selector.exclude_files.is_some());
if let Some(excluded_files) = new_config.selector.exclude_files {
for test in exclude_test_list {
assert!(excluded_files.contains(&test));
}
}
}
#[test]
fn test_with_new_tests_can_add_tests_to_test_root() {
let config_yaml = "
test_kind: js_test
selector:
roots:
- jstests/auth/*.js
exclude_files:
- jstests/auth/repl.js
- jstests/core/add1.js
executor:
config:
value
fixture:
class: MyFixture
num_nodes: 3
";
let new_test_list = vec!["test0.js".to_string(), "test1.js".to_string()];
let resmoke_suite = ResmokeSuiteConfig::from_str(config_yaml).unwrap();
let new_config = resmoke_suite.with_new_tests(Some(&new_test_list), None);
if let Some(TestRoot::Roots { roots: test_roots }) = new_config.selector.test_root {
for test in new_test_list {
assert!(test_roots.contains(&test));
}
} else {
panic!(
"New test root is not expected: {:?}",
new_config.selector.test_root
);
}
}
}