// choreo - A DSL for defining interactive test scenarios.
// This grammar defines the DSL structure and syntax.
// Ignore whitespace and comments.
WHITESPACE = _{ " " | "\t" | NEWLINE }
COMMENT = _{ "#" ~ (!NEWLINE ~ ANY)* }
// choreo DSL is a series of statements.
grammar = { SOI ~ statement* ~ EOI }
statement = { feature_def | settings_def | env_def | var_def | actors_def | background_def | task_def | scenario_def }
// --- Task Definitions (Reusable Drivers) ---
task_def = { "task" ~ identifier ~ "(" ~ task_param_list? ~ ")" ~ "{" ~ task_body_item+ ~ "}" }
task_param_list = { identifier ~ ("," ~ identifier)* }
task_body_item = { action | condition }
// --- Task Calls (in given/when/then blocks) ---
task_call = { identifier ~ "(" ~ task_arg_list? ~ ")" }
task_arg_list = { task_arg ~ ("," ~ task_arg)* }
task_arg = { wait_marker | string | number | variable_ref | identifier }
// Setting up the test suite.
settings_def = {
("setting:" ~ setting ~ ("," ~ setting)*) |
("settings" ~ "{" ~ setting+ ~ "}")
}
setting = { identifier ~ WHITESPACE* ~ "=" ~ WHITESPACE* ~ (value | binary_op) }
background_def = { "background" ~ "{" ~ (action | condition)+ ~ "}" }
env_def = {
("env:" ~ identifier ~ ("," ~ identifier)*) |
("env" ~ "{" ~ identifier+ ~ "}") |
("env" ~ identifier)
}
var_def = {
("var" ~ identifier ~ WHITESPACE* ~ "=" ~ WHITESPACE* ~ value) |
("var" ~ identifier ~ WHITESPACE* ~ "=" ~ WHITESPACE* ~ array) |
("var:" ~ identifier ~ ("," ~ identifier)*)
}
actors_def = {
("actors:" ~ identifier ~ ("," ~ identifier)*) |
("actors" ~ "{" ~ identifier+ ~ "}") |
("actor" ~ identifier)
}
feature_def = { "feature" ~ string }
parallel_keyword = { "parallel" }
scenario_body_item = { test | foreach_block }
scenario_body = { scenario_body_item* }
scenario_def = { parallel_keyword? ~ "scenario" ~ string ~ "{" ~ scenario_body ~ after_block? ~ "}" }
after_block = { "after" ~ "{" ~ (task_call | action)+ ~ "}" }
foreach_block = { "foreach" ~ identifier ~ "in" ~ (variable_ref | identifier) ~ "{" ~ test+ ~ "}" }
variable_ref = { "${" ~ identifier ~ ( ("[" ~ number ~ "]") | ("." ~ identifier) )* ~ "}" }
// Defining a test case.
test = { "test" ~ (identifier | string | variable_ref) ~ string ~ "{" ~ given_block ~ when_block ~ then_block ~ "}" }
given_block = { "given:" ~ (task_call | action | condition)* }
when_block = { "when:" ~ (task_call | action)+ }
then_block = { "then:" ~ (task_call | condition)+ }
// --- Generic Rule Block ---
rule = { "rule" ~ string ~ "{" ~ when_block_rule ~ then_block_rule ~ "}" }
when_block_rule = { "when:" ~ condition+ }
then_block_rule = { "then:" ~ action+ }
// --- Conditions & Actions ---
condition = {
wait_condition |
state_condition |
system_condition |
filesystem_condition |
terminal_condition |
web_condition
}
// --- System Conditions ---
system_condition = { "System" ~ (
service_is_running_condition |
service_is_stopped_condition |
service_is_installed_condition |
port_is_listening_condition |
port_is_closed_condition
) }
service_is_running_condition = { "service_is_running" ~ non_empty_string }
service_is_stopped_condition = { "service_is_stopped" ~ non_empty_string }
service_is_installed_condition = { "service_is_installed" ~ non_empty_string }
port_is_listening_condition = { "port_is_listening" ~ number }
port_is_closed_condition = { "port_is_closed" ~ number }
action = {
system_action |
filesystem_action |
set_cwd_action |
run_action |
web_action
}
// Condition Types
wait_condition = { "wait" ~ WHITESPACE* ~ comparison_op ~ WHITESPACE* ~ wait_marker }
state_condition = { "Test" ~ (state_has_succeeded | state_can_start) }
state_has_succeeded = { "has_succeeded" ~ identifier }
state_can_start = { "can_start" }
// --- Filesystem Conditions ---
file_is_not_empty_condition = { "file" ~ non_empty_string ~ "is_not_empty" }
file_is_empty_condition = { "file" ~ non_empty_string ~ "is_empty" }
filesystem_condition_keyword = @{ "file_exists" | "file_does_not_exist" | "dir_exists" | "dir_does_not_exist" | "file_contains" }
filesystem_condition = { "FileSystem" ~ (
(filesystem_condition_keyword ~ non_empty_string ~ "with_content" ~ string) |
(filesystem_condition_keyword ~ non_empty_string) |
file_is_not_empty_condition |
file_is_empty_condition
) }
// --- Terminal Conditions ---
terminal_condition = { "Terminal" ~ (
output_not_contains_condition |
output_contains_condition |
output_matches_condition |
last_command_succeeded_cond |
last_command_failed_cond |
last_command_exit_code_is_cond |
stdout_is_empty_condition |
stderr_is_empty_condition |
stderr_contains_condition |
output_starts_with_condition |
output_ends_with_condition |
output_equals_condition |
output_is_valid_json_condition |
json_output_has_path_condition |
json_output_at_equals_condition |
json_output_at_includes_condition |
json_output_at_has_item_count_condition
) }
// --- Web Conditions ---
web_condition = { "Web" ~ (
response_status_is_condition |
response_status_is_success_condition |
response_status_is_error_condition |
response_status_is_in_condition |
response_time_is_below_condition |
response_body_contains_condition |
response_body_matches_condition |
response_body_equals_json |
json_body_has_path_condition |
json_path_equals_condition |
json_path_capture_condition |
json_value_is_string_condition |
json_value_is_number_condition |
json_value_is_array_condition |
json_value_is_object_condition |
json_value_has_size_condition |
output_is_valid_json_condition |
json_output_has_path_condition |
json_output_at_equals_condition |
json_output_at_includes_condition |
json_output_at_has_item_count_condition
) }
// Json-specific condition parts
json_value_is_string_condition = { "json_response" ~ "at" ~ non_empty_string ~ "is_a_string" }
json_value_is_number_condition = { "json_response" ~ "at" ~ non_empty_string ~ "is_a_number" }
json_value_is_array_condition = { "json_response" ~ "at" ~ non_empty_string ~ "is_an_array" }
json_value_is_object_condition = { "json_response" ~ "at" ~ non_empty_string ~ "is_an_object" }
json_value_has_size_condition = { "json_response" ~ "at" ~ non_empty_string ~ "has_size" ~ number }
output_is_valid_json_condition = { "output_is_valid_json" }
json_output_has_path_condition = { "json_output" ~ "has_path" ~ non_empty_string }
json_output_at_equals_condition = { "json_output" ~ "at" ~ non_empty_string ~ "equals" ~ value }
json_output_at_includes_condition = { "json_output" ~ "at" ~ non_empty_string ~ "includes" ~ value }
json_output_at_has_item_count_condition = { "json_output" ~ "at" ~ non_empty_string ~ "has_item_count" ~ number }
json_body_has_path_condition = { "json_body" ~ "has_path" ~ non_empty_string }
json_path_equals_condition = { "json_path" ~ "at" ~ non_empty_string ~ "equals" ~ (value | binary_op) }
json_path_capture_condition = { "json_path" ~ "at" ~ non_empty_string ~ "as" ~ identifier }
// Terminal-specific condition parts
output_not_contains_condition = { "output_not_contains" ~ non_empty_string }
output_contains_condition = { "output_contains" ~ non_empty_string }
output_matches_condition = { "output_matches" ~ non_empty_string ~ ("as" ~ identifier)? }
last_command_succeeded_cond = { "last_command" ~ "succeeded" }
last_command_failed_cond = { "last_command" ~ "failed" }
last_command_exit_code_is_cond = { "last_command" ~ "exit_code_is" ~ number }
stdout_is_empty_condition = { "stdout_is_empty" }
stderr_is_empty_condition = { "stderr_is_empty" }
stderr_contains_condition = { "stderr_contains" ~ non_empty_string }
output_starts_with_condition = { "output_starts_with" ~ non_empty_string }
output_ends_with_condition = { "output_ends_with" ~ non_empty_string }
output_equals_condition = { "output_equals" ~ non_empty_string }
// Web-specific condition parts
response_status_is_condition = { "response_status_is" ~ ( number | string ) }
response_status_is_success_condition = { "response_status" ~ "is_success" }
response_status_is_error_condition = { "response_status" ~ "is_error" }
response_status_is_in_condition = { "response_status" ~ "is_in" ~ "[" ~ number ~ ("," ~ number)* ~ "]" }
response_time_is_below_condition = { "response_time" ~ "is_below" ~ wait_marker }
response_body_contains_condition = { "response_body_contains" ~ non_empty_string }
response_body_matches_condition = { "response_body_matches" ~ string ~ ("as" ~ identifier)? }
response_body_equals_json = { "response_body_equals_json" ~ non_empty_string ~ ("ignore_fields" ~ "[" ~ (string ~ ("," ~ string)*)? ~ "]")? }
// Action Types
set_cwd_action = { "Terminal" ~ "set_cwd" ~ non_empty_string }
run_action = { "Terminal" ~ "run" ~ non_empty_string }
web_action = { "Web" ~ web_action_type }
web_action_type = {
("set_header" ~ string ~ string) |
("clear_header" ~ string) |
("clear_headers") |
("set_cookie" ~ string ~ string) |
("clear_cookie" ~ string) |
("clear_cookies") |
("http_get" ~ non_empty_string) |
("http_post" ~ non_empty_string ~ "with_body" ~ string) |
("http_put" ~ non_empty_string ~ "with_body" ~ string) |
("http_patch" ~ non_empty_string ~ "with_body" ~ string) |
("http_delete" ~ non_empty_string)
}
filesystem_action = { "FileSystem" ~ filesystem_action_keyword ~ string ~ ("with_content" ~ string)? ~ ("as" ~ identifier)? }
filesystem_action_keyword = @{ "file" | "create_dir" | "delete_file" | "delete_dir" | "create_file" | "read_file" }
// --- System Actor ---
system_action = { "System" ~ system_action_type }
system_action_type = {
system_pause |
system_log |
system_timestamp |
system_uuid
}
system_pause = { "pause" ~ wait_marker }
system_log = { "log" ~ string }
system_timestamp = { "timestamp" ~ "as" ~ (string | identifier) }
system_uuid = { "uuid" ~ "as" ~ identifier }
// The basic building blocks of the choreo language.
identifier = @{ ( "_" | 'a'..'z' | 'A'..'Z' ) ~ ( "_" | 'a'..'z' | 'A'..'Z' | '0'..'9' )* }
string = ${ "\"" ~ within_double ~ "\"" | "'" ~ within_single ~ "'" }
within_double = @{ ( "\\" ~ ANY | !"\"" ~ ANY )* }
within_single = @{ ( "\\" ~ ANY | !"'" ~ ANY )* }
non_empty_string = ${ "\"" ~ within_double_non_empty ~ "\"" | "'" ~ within_single_non_empty ~ "'" }
within_double_non_empty = @{ ( "\\" ~ ANY | !"\"" ~ ANY )+ }
within_single_non_empty = @{ ( "\\" ~ ANY | !"'" ~ ANY )+ }
number = @{ ASCII_DIGIT+ }
float = @{ ASCII_DIGIT+ ~ ("." ~ ASCII_DIGIT+)? }
duration = @{ ASCII_DIGIT+ ~ ("ms" | "s") }
wait_unit = @{ "s" | "ms" }
wait_marker = @{ float ~ wait_unit }
value = { string | number | array | object | binary_op }
comparison_op = @{ ">=" | "<=" | "==" | ">" | "<" }
binary_op = { "true" | "false" }
array = { "[" ~ WHITESPACE* ~ (value ~ ("," ~ value)*)? ~ WHITESPACE* ~ "]" }
object = { "{" ~ WHITESPACE* ~ (pair ~ ("," ~ pair)*)? ~ WHITESPACE* ~ "}" }
pair = { (identifier | string) ~ WHITESPACE* ~ ":" ~ WHITESPACE* ~ value }