7#include "absl/status/status.h"
8#include "absl/strings/ascii.h"
9#include "absl/strings/match.h"
10#include "absl/strings/numbers.h"
11#include "absl/strings/str_cat.h"
12#include "absl/strings/str_split.h"
13#include "absl/strings/strip.h"
14#include "absl/strings/string_view.h"
21using ::absl::string_view;
23std::string
Trim(string_view value) {
24 return std::string(absl::StripAsciiWhitespace(value));
28 bool in_single_quote =
false;
29 bool in_double_quote =
false;
30 for (
size_t i = 0; i < line.size(); ++i) {
33 if (!in_double_quote) {
34 in_single_quote = !in_single_quote;
36 }
else if (c ==
'"') {
37 if (!in_single_quote) {
38 in_double_quote = !in_double_quote;
40 }
else if (c ==
'#' && !in_single_quote && !in_double_quote) {
41 return std::string(line.substr(0, i));
44 return std::string(line);
52 }
else if (c ==
'\t') {
61bool ParseKeyValue(string_view input, std::string* key, std::string* value) {
62 size_t colon_pos = input.find(
':');
63 if (colon_pos == string_view::npos) {
66 *key =
Trim(input.substr(0, colon_pos));
67 *value =
Trim(input.substr(colon_pos + 1));
72 std::string trimmed =
Trim(value);
73 if (trimmed.size() >= 2) {
74 if ((trimmed.front() ==
'"' && trimmed.back() ==
'"') ||
75 (trimmed.front() ==
'\'' && trimmed.back() ==
'\'')) {
76 return std::string(trimmed.substr(1, trimmed.size() - 2));
83 std::vector<std::string> items;
84 std::string trimmed =
Trim(value);
85 if (trimmed.empty()) {
88 if (trimmed.front() !=
'[' || trimmed.back() !=
']') {
89 items.push_back(
Unquote(trimmed));
92 string_view inner(trimmed.data() + 1, trimmed.size() - 2);
96 for (string_view piece : absl::StrSplit(inner,
',', absl::SkipWhitespace())) {
97 items.push_back(
Unquote(piece));
104 if (!absl::SimpleAtoi(value, &result)) {
105 return absl::InvalidArgumentError(
106 absl::StrCat(
"Expected integer value, got '", value,
"'"));
112 std::string lower = absl::AsciiStrToLower(std::string(value));
117 if (absl::EndsWith(lower,
"ms")) {
118 lower = lower.substr(0, lower.size() - 2);
120 }
else if (absl::EndsWith(lower,
"s")) {
121 lower = lower.substr(0, lower.size() - 1);
122 }
else if (absl::EndsWith(lower,
"m")) {
123 lower = lower.substr(0, lower.size() - 1);
128 if (!absl::SimpleAtoi(lower, &numeric)) {
129 return absl::InvalidArgumentError(
130 absl::StrCat(
"Invalid duration value '", value,
"'"));
133 if (multiplier == 0) {
135 return (numeric + 999) / 1000;
137 return numeric * multiplier;
141 std::string lower = absl::AsciiStrToLower(std::string(value));
142 if (lower ==
"true" || lower ==
"yes" || lower ==
"on" || lower ==
"1") {
146 if (lower ==
"false" || lower ==
"no" || lower ==
"off" || lower ==
"0") {
154 std::filesystem::path fs_path(path);
155 std::string stem = fs_path.stem().string();
164 if (key ==
"timeout_per_test") {
167 return absl::OkStatus();
169 if (key ==
"retry_on_failure") {
172 return absl::OkStatus();
174 if (key ==
"parallel_execution") {
175 bool enabled =
false;
177 return absl::InvalidArgumentError(
178 absl::StrCat(
"Invalid boolean for parallel_execution: '", value,
182 return absl::OkStatus();
184 return absl::InvalidArgumentError(
185 absl::StrCat(
"Unknown config key: '", key,
"'"));
189 size_t* index,
int base_indent,
190 std::vector<std::string>* output) {
191 while (*index < lines.size()) {
193 std::string trimmed =
Trim(raw);
195 if (trimmed.empty()) {
199 if (indent < base_indent) {
202 if (indent != base_indent) {
203 return absl::InvalidArgumentError(
204 "Invalid indentation in list block");
206 if (trimmed.empty() || trimmed.front() !=
'-') {
207 return absl::InvalidArgumentError(
"Expected list entry starting with '-'");
209 std::string value =
Trim(trimmed.substr(1));
210 output->push_back(
Unquote(value));
213 return absl::OkStatus();
217 size_t* index,
int base_indent,
218 std::map<std::string, std::string>* params) {
219 while (*index < lines.size()) {
221 std::string trimmed =
Trim(raw);
223 if (trimmed.empty()) {
227 if (indent < base_indent) {
230 if (indent != base_indent) {
231 return absl::InvalidArgumentError(
"Invalid indentation in parameters block");
235 if (!ParseKeyValue(trimmed, &key, &value)) {
236 return absl::InvalidArgumentError(
237 "Expected key/value pair inside parameters block");
239 (*params)[key] =
Unquote(value);
242 return absl::OkStatus();
246 size_t* index,
int base_indent,
248 const std::string& raw_line = lines[*index];
251 if (indent != base_indent) {
252 return absl::InvalidArgumentError(
"Invalid indentation for test case entry");
255 size_t dash_pos = stripped.find(
'-');
256 if (dash_pos == std::string::npos) {
257 return absl::InvalidArgumentError(
"Malformed list entry in tests block");
260 std::string content =
Trim(stripped.substr(dash_pos + 1));
264 auto commit_test = [&]() {
266 return absl::InvalidArgumentError(
"Test case missing script_path");
268 if (test.
name.empty()) {
271 if (test.
id.empty()) {
274 group->
tests.push_back(std::move(test));
275 return absl::OkStatus();
278 if (content.empty()) {
280 }
else if (content.find(
':') == std::string::npos) {
283 return commit_test();
287 if (!ParseKeyValue(content, &key, &value)) {
288 return absl::InvalidArgumentError(
"Malformed key/value in test entry");
290 if (key ==
"path" || key ==
"script" || key ==
"script_path") {
292 }
else if (key ==
"name") {
294 }
else if (key ==
"description") {
296 }
else if (key ==
"id") {
298 }
else if (key ==
"tags") {
300 test.
tags.insert(test.
tags.end(), tags.begin(), tags.end());
307 while (*index < lines.size()) {
309 std::string trimmed =
Trim(raw);
311 if (trimmed.empty()) {
315 if (indent_next <= base_indent) {
318 if (indent_next == base_indent + 2) {
321 if (!ParseKeyValue(trimmed, &key, &value)) {
322 return absl::InvalidArgumentError(
323 "Expected key/value pair in test definition");
325 if (key ==
"path" || key ==
"script" || key ==
"script_path") {
327 }
else if (key ==
"name") {
329 }
else if (key ==
"description") {
331 }
else if (key ==
"id") {
333 }
else if (key ==
"tags") {
335 if (tags.empty() && value.empty()) {
341 test.
tags.insert(test.
tags.end(), tags.begin(), tags.end());
342 }
else if (key ==
"parameters") {
343 if (!value.empty()) {
344 return absl::InvalidArgumentError(
345 "parameters block must be indented on following lines");
356 return absl::InvalidArgumentError(
357 "Unexpected indentation inside test entry");
361 return commit_test();
365 size_t* index,
int base_indent,
367 while (*index < lines.size()) {
369 std::string trimmed =
Trim(raw);
371 if (trimmed.empty()) {
375 if (indent < base_indent) {
378 if (indent != base_indent) {
379 return absl::InvalidArgumentError(
380 "Invalid indentation inside tests block");
384 return absl::OkStatus();
389 const std::string& raw_line = lines[*index];
393 size_t dash_pos = stripped.find(
'-');
394 if (dash_pos == std::string::npos || dash_pos < base_indent) {
395 return absl::InvalidArgumentError(
"Expected '-' to start group entry");
398 std::string content =
Trim(stripped.substr(dash_pos + 1));
401 if (!content.empty()) {
404 if (!ParseKeyValue(content, &key, &value)) {
405 return absl::InvalidArgumentError(
"Malformed group entry");
409 }
else if (key ==
"description") {
411 }
else if (key ==
"depends_on") {
415 return absl::InvalidArgumentError(
416 absl::StrCat(
"Unknown field in group entry: '", key,
"'"));
422 while (*index < lines.size()) {
424 std::string trimmed =
Trim(raw);
426 if (trimmed.empty()) {
430 if (indent <= base_indent) {
433 if (indent == base_indent + 2) {
436 if (!ParseKeyValue(trimmed, &key, &value)) {
437 return absl::InvalidArgumentError(
438 "Expected key/value pair inside group definition");
443 }
else if (key ==
"description") {
446 }
else if (key ==
"depends_on") {
447 if (!value.empty()) {
457 }
else if (key ==
"tests") {
458 if (!value.empty()) {
459 return absl::InvalidArgumentError(
460 "tests block must be defined as indented list");
466 return absl::InvalidArgumentError(
467 absl::StrCat(
"Unknown attribute in group definition: '", key,
471 return absl::InvalidArgumentError(
472 "Unexpected indentation inside group definition");
476 if (group.
name.empty()) {
477 return absl::InvalidArgumentError(
478 "Each test group must define a name");
480 suite->
groups.push_back(std::move(group));
481 return absl::OkStatus();
486 while (*index < lines.size()) {
488 std::string trimmed =
Trim(raw);
490 if (trimmed.empty()) {
498 return absl::InvalidArgumentError(
499 "Invalid indentation inside test_groups block");
503 return absl::OkStatus();
508 while (*index < lines.size()) {
510 std::string trimmed =
Trim(raw);
512 if (trimmed.empty()) {
520 return absl::InvalidArgumentError(
521 "Invalid indentation inside config block");
525 if (!ParseKeyValue(trimmed, &key, &value)) {
526 return absl::InvalidArgumentError(
527 "Expected key/value pair inside config block");
532 return absl::OkStatus();
538 absl::string_view content) {
539 std::vector<std::string> lines = absl::StrSplit(content,
'\n');
543 while (index < lines.size()) {
544 std::string raw = StripComment(lines[index]);
545 std::string trimmed = Trim(raw);
546 if (trimmed.empty()) {
551 int indent = CountIndent(raw);
553 return absl::InvalidArgumentError(
554 "Top-level entries must not be indented in suite definition");
559 if (!ParseKeyValue(trimmed, &key, &value)) {
560 return absl::InvalidArgumentError(
561 absl::StrCat(
"Malformed top-level entry: '", trimmed,
"'"));
565 suite.
name = Unquote(value);
567 }
else if (key ==
"description") {
570 }
else if (key ==
"version") {
571 suite.
version = Unquote(value);
573 }
else if (key ==
"config") {
574 if (!value.empty()) {
575 return absl::InvalidArgumentError(
576 "config block must not specify inline value");
580 }
else if (key ==
"test_groups") {
581 if (!value.empty()) {
582 return absl::InvalidArgumentError(
583 "test_groups must be defined as an indented list");
588 return absl::InvalidArgumentError(
589 absl::StrCat(
"Unknown top-level key: '", key,
"'"));
593 if (suite.
name.empty()) {
594 suite.
name =
"Unnamed Suite";
603 const std::string& path) {
604 std::ifstream file(path);
605 if (!file.is_open()) {
606 return absl::NotFoundError(
607 absl::StrCat(
"Failed to open test suite file '", path,
"'"));
609 std::stringstream buffer;
610 buffer << file.rdbuf();
#define RETURN_IF_ERROR(expression)
#define ASSIGN_OR_RETURN(type_variable_name, expression)
absl::Status ParseGroupEntry(const std::vector< std::string > &lines, size_t *index, TestSuiteDefinition *suite)
absl::Status ParseParametersBlock(const std::vector< std::string > &lines, size_t *index, int base_indent, std::map< std::string, std::string > *params)
std::vector< std::string > ParseInlineList(string_view value)
std::string DeriveTestName(const std::string &path)
absl::Status ParseConfigBlock(const std::vector< std::string > &lines, size_t *index, TestSuiteConfig *config)
std::string Trim(string_view value)
absl::Status ParseScalarConfig(const std::string &key, const std::string &value, TestSuiteConfig *config)
absl::Status ParseStringListBlock(const std::vector< std::string > &lines, size_t *index, int base_indent, std::vector< std::string > *output)
absl::Status ParseTestsBlock(const std::vector< std::string > &lines, size_t *index, int base_indent, TestGroupDefinition *group)
absl::StatusOr< int > ParseInt(string_view value)
absl::Status ParseGroupBlock(const std::vector< std::string > &lines, size_t *index, TestSuiteDefinition *suite)
std::string StripComment(string_view line)
int CountIndent(string_view line)
absl::StatusOr< int > ParseDurationSeconds(string_view value)
std::string Unquote(string_view value)
absl::Status ParseTestCaseEntry(const std::vector< std::string > &lines, size_t *index, int base_indent, TestGroupDefinition *group)
bool ParseBoolean(string_view value, bool *output)
absl::StatusOr< TestSuiteDefinition > LoadTestSuiteFromFile(const std::string &path)
absl::StatusOr< TestSuiteDefinition > ParseTestSuiteDefinition(absl::string_view content)
Main namespace for the application.
std::map< std::string, std::string > parameters
std::vector< std::string > tags
std::vector< TestCaseDefinition > tests
std::vector< std::string > depends_on
std::vector< TestGroupDefinition > groups