use std::collections::HashMap;
use std::fmt;
#[derive(Debug, Clone)]
pub struct TestSuite {
pub statements: Vec<Statement>,
}
#[derive(Debug, PartialEq, Clone)]
pub enum TestState {
Pending,
Running,
Passed,
Failed(String),
Skipped,
}
impl TestState {
pub fn is_done(&self) -> bool {
matches!(self, TestState::Passed | TestState::Failed(_))
}
pub fn is_failed(&self) -> bool {
matches!(self, TestState::Failed(_))
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ReportFormat {
Json,
Junit,
None,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Span {
pub start: usize,
pub end: usize,
pub line: usize,
pub column: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ScenarioSpan {
pub name_span: Option<Span>,
pub tests_span: Option<Span>,
pub after_span: Option<Span>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SettingSpan {
pub timeout_seconds_span: Option<Span>,
pub report_path_span: Option<Span>,
pub report_format_span: Option<Span>,
pub shell_path_span: Option<Span>,
pub stop_on_failure_span: Option<Span>,
pub expected_failures_span: Option<Span>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct TestSuiteSettings {
pub timeout_seconds: u64,
pub report_format: ReportFormat,
pub report_path: String,
pub stop_on_failure: bool,
pub shell_path: Option<String>,
pub expected_failures: usize,
pub span: Option<Span>,
pub setting_spans: Option<SettingSpan>,
}
impl Default for TestSuiteSettings {
fn default() -> Self {
Self {
timeout_seconds: 60,
report_format: ReportFormat::Json,
report_path: "reports/".to_string(),
stop_on_failure: false,
shell_path: Option::from("/bin/sh".to_string()),
expected_failures: 0,
span: None,
setting_spans: None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TestCase {
pub name: String,
pub description: String,
pub given: Vec<GivenStep>,
pub when: Vec<WhenStep>,
pub then: Vec<ThenStep>,
pub span: Option<Span>,
pub testcase_spans: Option<TestCaseSpan>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct TestCaseSpan {
pub name_span: Option<Span>,
pub description_span: Option<Span>,
pub given_span: Option<Span>,
pub when_span: Option<Span>,
pub then_span: Option<Span>,
}
impl Default for TestCase {
fn default() -> Self {
Self {
name: String::new(),
description: String::new(),
given: Vec::new(),
when: Vec::new(),
then: Vec::new(),
span: None,
testcase_spans: None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum WhenStep {
Action(Action),
TaskCall(TaskCall),
}
#[derive(Debug, Clone, PartialEq)]
pub enum ThenStep {
Condition(Condition),
TaskCall(TaskCall),
}
#[derive(Debug, Clone)]
pub enum Statement {
SettingsDef(TestSuiteSettings),
BackgroundDef(Vec<GivenStep>),
EnvDef(Vec<String>),
VarDef(String, Value),
ActorDef(Vec<String>),
FeatureDef(String),
Scenario(Scenario),
TestCase(TestCase),
TaskDef(TaskDef),
}
#[derive(Debug, Clone, PartialEq)]
pub struct TaskDef {
pub name: String,
pub parameters: Vec<String>,
pub body: Vec<TaskBodyItem>,
pub span: Option<Span>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum TaskBodyItem {
Action(Action),
Condition(Condition),
}
#[derive(Debug, Clone, PartialEq)]
pub struct TaskCall {
pub name: String,
pub arguments: Vec<TaskArg>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum TaskArg {
String(String),
Number(i32),
Duration(f32),
VariableRef(String),
}
#[derive(Debug, Clone, PartialEq)]
pub struct ForeachBlock {
pub loop_variable: String,
pub array_variable: String,
pub tests: Vec<TestCase>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ScenarioBodyItem {
Test(TestCase),
Foreach(ForeachBlock),
}
#[derive(Debug, Clone, PartialEq)]
pub struct Scenario {
pub name: String,
pub tests: Vec<TestCase>,
pub body: Vec<ScenarioBodyItem>,
pub after: Vec<WhenStep>,
pub parallel: bool,
pub span: Option<Span>,
pub scenario_span: Option<ScenarioSpan>,
}
impl Default for Scenario {
fn default() -> Self {
Self {
name: String::new(),
tests: Vec::new(),
body: Vec::new(),
after: Vec::new(),
parallel: false,
span: None,
scenario_span: None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum GivenStep {
Action(Action),
Condition(Condition),
TaskCall(TaskCall),
}
#[derive(Debug, Clone, PartialEq)]
pub enum StateCondition {
HasSucceeded(String),
CanStart,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Condition {
Wait {
op: String,
wait: f32,
},
StateSucceeded {
outcome: String,
},
State(StateCondition),
OutputContains {
actor: String,
text: String,
},
OutputNotContains {
actor: String,
text: String,
},
OutputMatches {
actor: String,
regex: String,
capture_as: Option<String>,
},
LastCommandSucceeded,
LastCommandFailed,
LastCommandExitCodeIs(i32),
StdoutIsEmpty,
StderrIsEmpty,
StderrContains(String),
OutputStartsWith(String),
OutputEndsWith(String),
OutputEquals(String),
OutputIsValidJson,
JsonValueIsString {
path: String,
},
JsonValueIsNumber {
path: String,
},
JsonValueIsArray {
path: String,
},
JsonValueIsObject {
path: String,
},
JsonValueHasSize {
path: String,
size: usize,
},
JsonOutputHasPath {
path: String,
},
JsonOutputAtEquals {
path: String,
value: Value,
},
JsonOutputAtIncludes {
path: String,
value: Value,
},
JsonOutputAtHasItemCount {
path: String,
count: i32,
},
FileExists {
path: String,
},
DirExists {
path: String,
},
DirDoesNotExist {
path: String,
},
FileContains {
path: String,
content: String,
},
FileDoesNotExist {
path: String,
},
FileIsEmpty {
path: String,
},
FileIsNotEmpty {
path: String,
},
ResponseStatusIs(u16),
ResponseStatusIsSuccess,
ResponseStatusIsError,
ResponseStatusIsIn(Vec<u16>),
ResponseTimeIsBelow {
duration: f32,
},
ResponseBodyContains {
value: String,
},
ResponseBodyMatches {
regex: String,
capture_as: Option<String>,
},
ResponseBodyEqualsJson {
expected: String,
ignored: Vec<String>,
},
JsonBodyHasPath {
path: String,
},
JsonPathEquals {
path: String,
expected_value: Value,
},
JsonPathCapture {
path: String,
capture_as: String,
},
ServiceIsRunning {
name: String,
},
ServiceIsStopped {
name: String,
},
ServiceIsInstalled {
name: String,
},
PortIsListening {
port: u16,
},
PortIsClosed {
port: u16,
},
}
#[derive(Debug, Clone, PartialEq)]
pub enum Action {
Pause {
duration: f32,
},
Log {
message: String,
},
Timestamp {
variable: String,
},
Uuid {
variable: String,
},
Run {
actor: String,
command: String,
},
SetCwd {
path: String,
},
CreateFile {
path: String,
content: String,
},
CreateDir {
path: String,
},
DeleteFile {
path: String,
},
DeleteDir {
path: String,
},
ReadFile {
path: String,
variable: Option<String>,
},
HttpSetHeader {
key: String,
value: String,
},
HttpClearHeader {
key: String,
},
HttpClearHeaders,
HttpSetCookie {
key: String,
value: String,
},
HttpClearCookie {
key: String,
},
HttpClearCookies,
HttpGet {
url: String,
},
HttpPost {
url: String,
body: String,
},
HttpPut {
url: String,
body: String,
},
HttpPatch {
url: String,
body: String,
},
HttpDelete {
url: String,
},
}
impl Action {
pub fn is_filesystem_creation(&self) -> bool {
matches!(self, Self::CreateFile { .. } | Self::CreateDir { .. })
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
String(String),
Number(i32),
Bool(bool),
Array(Vec<Value>),
Object(HashMap<String, Value>),
}
impl Value {
pub fn as_string(&self) -> String {
match self {
Value::String(s) => s.clone(),
Value::Number(n) => n.to_string(),
Value::Bool(b) => b.to_string(),
Value::Array(a) => a
.iter()
.map(Value::as_string)
.collect::<Vec<_>>()
.join(", "),
Value::Object(_) => {
match self.to_json_value() {
serde_json::Value::String(s) => s,
other => other.to_string(),
}
}
}
}
fn to_json_value(&self) -> serde_json::Value {
match self {
Value::String(s) => serde_json::Value::String(s.clone()),
Value::Number(n) => serde_json::Value::Number(serde_json::Number::from(*n)),
Value::Bool(b) => serde_json::Value::Bool(*b),
Value::Array(arr) => {
let vec = arr.iter().map(|v| v.to_json_value()).collect();
serde_json::Value::Array(vec)
}
Value::Object(map) => {
let mut obj = serde_json::Map::new();
for (k, v) in map {
obj.insert(k.clone(), v.to_json_value());
}
serde_json::Value::Object(obj)
}
}
}
}
impl fmt::Display for Value {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Value::String(s) => write!(f, "{}", s),
Value::Number(n) => write!(f, "{}", n),
Value::Bool(b) => write!(f, "{}", b),
Value::Array(arr) => {
let items: Vec<String> = arr.iter().map(|v| v.to_string()).collect();
write!(f, "[{}]", items.join(", "))
}
Value::Object(_) => {
let json = self.to_json_value();
write!(f, "{}", json)
}
}
}
}