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/string_view.h"
14#include "absl/strings/strip.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(absl::StrCat(
178 "Invalid boolean for parallel_execution: '", value,
"'"));
181 return absl::OkStatus();
183 return absl::InvalidArgumentError(
184 absl::StrCat(
"Unknown config key: '", key,
"'"));
188 size_t* index,
int base_indent,
189 std::vector<std::string>* output) {
190 while (*index < lines.size()) {
192 std::string trimmed =
Trim(raw);
194 if (trimmed.empty()) {
198 if (indent < base_indent) {
201 if (indent != base_indent) {
202 return absl::InvalidArgumentError(
"Invalid indentation in list block");
204 if (trimmed.empty() || trimmed.front() !=
'-') {
205 return absl::InvalidArgumentError(
206 "Expected list entry starting with '-'");
208 std::string value =
Trim(trimmed.substr(1));
209 output->push_back(
Unquote(value));
212 return absl::OkStatus();
216 size_t* index,
int base_indent,
217 std::map<std::string, std::string>* params) {
218 while (*index < lines.size()) {
220 std::string trimmed =
Trim(raw);
222 if (trimmed.empty()) {
226 if (indent < base_indent) {
229 if (indent != base_indent) {
230 return absl::InvalidArgumentError(
231 "Invalid indentation in parameters block");
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(
253 "Invalid indentation for test case entry");
256 size_t dash_pos = stripped.find(
'-');
257 if (dash_pos == std::string::npos) {
258 return absl::InvalidArgumentError(
"Malformed list entry in tests block");
261 std::string content =
Trim(stripped.substr(dash_pos + 1));
265 auto commit_test = [&]() {
267 return absl::InvalidArgumentError(
"Test case missing script_path");
269 if (test.
name.empty()) {
272 if (test.
id.empty()) {
275 group->
tests.push_back(std::move(test));
276 return absl::OkStatus();
279 if (content.empty()) {
281 }
else if (content.find(
':') == std::string::npos) {
284 return commit_test();
289 return absl::InvalidArgumentError(
"Malformed key/value in test entry");
291 if (key ==
"path" || key ==
"script" || key ==
"script_path") {
293 }
else if (key ==
"name") {
295 }
else if (key ==
"description") {
297 }
else if (key ==
"id") {
299 }
else if (key ==
"tags") {
301 test.
tags.insert(test.
tags.end(), tags.begin(), tags.end());
308 while (*index < lines.size()) {
310 std::string trimmed =
Trim(raw);
312 if (trimmed.empty()) {
316 if (indent_next <= base_indent) {
319 if (indent_next == base_indent + 2) {
323 return absl::InvalidArgumentError(
324 "Expected key/value pair in test definition");
326 if (key ==
"path" || key ==
"script" || key ==
"script_path") {
328 }
else if (key ==
"name") {
330 }
else if (key ==
"description") {
332 }
else if (key ==
"id") {
334 }
else if (key ==
"tags") {
336 if (tags.empty() && value.empty()) {
342 test.
tags.insert(test.
tags.end(), tags.begin(), tags.end());
343 }
else if (key ==
"parameters") {
344 if (!value.empty()) {
345 return absl::InvalidArgumentError(
346 "parameters block must be indented on following lines");
357 return absl::InvalidArgumentError(
358 "Unexpected indentation inside test entry");
362 return commit_test();
366 size_t* index,
int base_indent,
368 while (*index < lines.size()) {
370 std::string trimmed =
Trim(raw);
372 if (trimmed.empty()) {
376 if (indent < base_indent) {
379 if (indent != base_indent) {
380 return absl::InvalidArgumentError(
381 "Invalid indentation inside tests block");
385 return absl::OkStatus();
390 const std::string& raw_line = lines[*index];
394 size_t dash_pos = stripped.find(
'-');
395 if (dash_pos == std::string::npos || dash_pos < base_indent) {
396 return absl::InvalidArgumentError(
"Expected '-' to start group entry");
399 std::string content =
Trim(stripped.substr(dash_pos + 1));
402 if (!content.empty()) {
406 return absl::InvalidArgumentError(
"Malformed group entry");
410 }
else if (key ==
"description") {
412 }
else if (key ==
"depends_on") {
416 return absl::InvalidArgumentError(
417 absl::StrCat(
"Unknown field in group entry: '", key,
"'"));
423 while (*index < lines.size()) {
425 std::string trimmed =
Trim(raw);
427 if (trimmed.empty()) {
431 if (indent <= base_indent) {
434 if (indent == base_indent + 2) {
438 return absl::InvalidArgumentError(
439 "Expected key/value pair inside group definition");
444 }
else if (key ==
"description") {
447 }
else if (key ==
"depends_on") {
448 if (!value.empty()) {
458 }
else if (key ==
"tests") {
459 if (!value.empty()) {
460 return absl::InvalidArgumentError(
461 "tests block must be defined as indented list");
466 return absl::InvalidArgumentError(
467 absl::StrCat(
"Unknown attribute in group definition: '", key,
"'"));
470 return absl::InvalidArgumentError(
471 "Unexpected indentation inside group definition");
475 if (group.
name.empty()) {
476 return absl::InvalidArgumentError(
"Each test group must define a name");
478 suite->
groups.push_back(std::move(group));
479 return absl::OkStatus();
484 while (*index < lines.size()) {
486 std::string trimmed =
Trim(raw);
488 if (trimmed.empty()) {
496 return absl::InvalidArgumentError(
497 "Invalid indentation inside test_groups block");
501 return absl::OkStatus();
506 while (*index < lines.size()) {
508 std::string trimmed =
Trim(raw);
510 if (trimmed.empty()) {
518 return absl::InvalidArgumentError(
519 "Invalid indentation inside config block");
524 return absl::InvalidArgumentError(
525 "Expected key/value pair inside config block");
530 return absl::OkStatus();
536 absl::string_view content) {
537 std::vector<std::string> lines = absl::StrSplit(content,
'\n');
541 while (index < lines.size()) {
542 std::string raw = StripComment(lines[index]);
543 std::string trimmed = Trim(raw);
544 if (trimmed.empty()) {
549 int indent = CountIndent(raw);
551 return absl::InvalidArgumentError(
552 "Top-level entries must not be indented in suite definition");
557 if (!ParseKeyValue(trimmed, &key, &value)) {
558 return absl::InvalidArgumentError(
559 absl::StrCat(
"Malformed top-level entry: '", trimmed,
"'"));
563 suite.
name = Unquote(value);
565 }
else if (key ==
"description") {
568 }
else if (key ==
"version") {
569 suite.
version = Unquote(value);
571 }
else if (key ==
"config") {
572 if (!value.empty()) {
573 return absl::InvalidArgumentError(
574 "config block must not specify inline value");
578 }
else if (key ==
"test_groups") {
579 if (!value.empty()) {
580 return absl::InvalidArgumentError(
581 "test_groups must be defined as an indented list");
586 return absl::InvalidArgumentError(
587 absl::StrCat(
"Unknown top-level key: '", key,
"'"));
591 if (suite.
name.empty()) {
592 suite.
name =
"Unnamed Suite";
601 const std::string& path) {
602 std::ifstream file(path);
603 if (!file.is_open()) {
604 return absl::NotFoundError(
605 absl::StrCat(
"Failed to open test suite file '", path,
"'"));
607 std::stringstream buffer;
608 buffer << file.rdbuf();
#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)
bool ParseKeyValue(string_view input, std::string *key, std::string *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)
#define RETURN_IF_ERROR(expr)
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