yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
test_suite_loader.cc
Go to the documentation of this file.
2
3#include <filesystem>
4#include <fstream>
5#include <sstream>
6
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"
15#include "util/macro.h"
16
17namespace yaze {
18namespace cli {
19namespace {
20
21using ::absl::string_view;
22
23std::string Trim(string_view value) {
24 return std::string(absl::StripAsciiWhitespace(value));
25}
26
27std::string StripComment(string_view line) {
28 bool in_single_quote = false;
29 bool in_double_quote = false;
30 for (size_t i = 0; i < line.size(); ++i) {
31 char c = line[i];
32 if (c == '\'') {
33 if (!in_double_quote) {
34 in_single_quote = !in_single_quote;
35 }
36 } else if (c == '"') {
37 if (!in_single_quote) {
38 in_double_quote = !in_double_quote;
39 }
40 } else if (c == '#' && !in_single_quote && !in_double_quote) {
41 return std::string(line.substr(0, i));
42 }
43 }
44 return std::string(line);
45}
46
47int CountIndent(string_view line) {
48 int count = 0;
49 for (char c : line) {
50 if (c == ' ') {
51 ++count;
52 } else if (c == '\t') {
53 count += 2; // Treat tab as two spaces for simplicity
54 } else {
55 break;
56 }
57 }
58 return count;
59}
60
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) {
64 return false;
65 }
66 *key = Trim(input.substr(0, colon_pos));
67 *value = Trim(input.substr(colon_pos + 1));
68 return true;
69}
70
71std::string Unquote(string_view value) {
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));
77 }
78 }
79 return trimmed;
80}
81
82std::vector<std::string> ParseInlineList(string_view value) {
83 std::vector<std::string> items;
84 std::string trimmed = Trim(value);
85 if (trimmed.empty()) {
86 return items;
87 }
88 if (trimmed.front() != '[' || trimmed.back() != ']') {
89 items.push_back(Unquote(trimmed));
90 return items;
91 }
92 string_view inner(trimmed.data() + 1, trimmed.size() - 2);
93 if (inner.empty()) {
94 return items;
95 }
96 for (string_view piece : absl::StrSplit(inner, ',', absl::SkipWhitespace())) {
97 items.push_back(Unquote(piece));
98 }
99 return items;
100}
101
102absl::StatusOr<int> ParseInt(string_view value) {
103 int result = 0;
104 if (!absl::SimpleAtoi(value, &result)) {
105 return absl::InvalidArgumentError(
106 absl::StrCat("Expected integer value, got '", value, "'"));
107 }
108 return result;
109}
110
111absl::StatusOr<int> ParseDurationSeconds(string_view value) {
112 std::string lower = absl::AsciiStrToLower(std::string(value));
113 if (lower.empty()) {
114 return 0;
115 }
116 int multiplier = 1;
117 if (absl::EndsWith(lower, "ms")) {
118 lower = lower.substr(0, lower.size() - 2);
119 multiplier = 0; // Use 0 to indicate sub-second resolution
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);
124 multiplier = 60;
125 }
126
127 int numeric = 0;
128 if (!absl::SimpleAtoi(lower, &numeric)) {
129 return absl::InvalidArgumentError(
130 absl::StrCat("Invalid duration value '", value, "'"));
131 }
132
133 if (multiplier == 0) {
134 // Round milliseconds up to nearest whole second
135 return (numeric + 999) / 1000;
136 }
137 return numeric * multiplier;
138}
139
140bool ParseBoolean(string_view value, bool* output) {
141 std::string lower = absl::AsciiStrToLower(std::string(value));
142 if (lower == "true" || lower == "yes" || lower == "on" || lower == "1") {
143 *output = true;
144 return true;
145 }
146 if (lower == "false" || lower == "no" || lower == "off" || lower == "0") {
147 *output = false;
148 return true;
149 }
150 return false;
151}
152
153std::string DeriveTestName(const std::string& path) {
154 std::filesystem::path fs_path(path);
155 std::string stem = fs_path.stem().string();
156 if (stem.empty()) {
157 return path;
158 }
159 return stem;
160}
161
162absl::Status ParseScalarConfig(const std::string& key, const std::string& value,
163 TestSuiteConfig* config) {
164 if (key == "timeout_per_test") {
165 ASSIGN_OR_RETURN(int seconds, ParseDurationSeconds(value));
166 config->timeout_seconds = seconds;
167 return absl::OkStatus();
168 }
169 if (key == "retry_on_failure") {
170 ASSIGN_OR_RETURN(int retries, ParseInt(value));
171 config->retry_on_failure = retries;
172 return absl::OkStatus();
173 }
174 if (key == "parallel_execution") {
175 bool enabled = false;
176 if (!ParseBoolean(value, &enabled)) {
177 return absl::InvalidArgumentError(
178 absl::StrCat("Invalid boolean for parallel_execution: '", value,
179 "'"));
180 }
181 config->parallel_execution = enabled;
182 return absl::OkStatus();
183 }
184 return absl::InvalidArgumentError(
185 absl::StrCat("Unknown config key: '", key, "'"));
186}
187
188absl::Status ParseStringListBlock(const std::vector<std::string>& lines,
189 size_t* index, int base_indent,
190 std::vector<std::string>* output) {
191 while (*index < lines.size()) {
192 std::string raw = StripComment(lines[*index]);
193 std::string trimmed = Trim(raw);
194 int indent = CountIndent(raw);
195 if (trimmed.empty()) {
196 ++(*index);
197 continue;
198 }
199 if (indent < base_indent) {
200 break;
201 }
202 if (indent != base_indent) {
203 return absl::InvalidArgumentError(
204 "Invalid indentation in list block");
205 }
206 if (trimmed.empty() || trimmed.front() != '-') {
207 return absl::InvalidArgumentError("Expected list entry starting with '-'");
208 }
209 std::string value = Trim(trimmed.substr(1));
210 output->push_back(Unquote(value));
211 ++(*index);
212 }
213 return absl::OkStatus();
214}
215
216absl::Status ParseParametersBlock(const std::vector<std::string>& lines,
217 size_t* index, int base_indent,
218 std::map<std::string, std::string>* params) {
219 while (*index < lines.size()) {
220 std::string raw = StripComment(lines[*index]);
221 std::string trimmed = Trim(raw);
222 int indent = CountIndent(raw);
223 if (trimmed.empty()) {
224 ++(*index);
225 continue;
226 }
227 if (indent < base_indent) {
228 break;
229 }
230 if (indent != base_indent) {
231 return absl::InvalidArgumentError("Invalid indentation in parameters block");
232 }
233 std::string key;
234 std::string value;
235 if (!ParseKeyValue(trimmed, &key, &value)) {
236 return absl::InvalidArgumentError(
237 "Expected key/value pair inside parameters block");
238 }
239 (*params)[key] = Unquote(value);
240 ++(*index);
241 }
242 return absl::OkStatus();
243}
244
245absl::Status ParseTestCaseEntry(const std::vector<std::string>& lines,
246 size_t* index, int base_indent,
247 TestGroupDefinition* group) {
248 const std::string& raw_line = lines[*index];
249 std::string stripped = StripComment(raw_line);
250 int indent = CountIndent(stripped);
251 if (indent != base_indent) {
252 return absl::InvalidArgumentError("Invalid indentation for test case entry");
253 }
254
255 size_t dash_pos = stripped.find('-');
256 if (dash_pos == std::string::npos) {
257 return absl::InvalidArgumentError("Malformed list entry in tests block");
258 }
259
260 std::string content = Trim(stripped.substr(dash_pos + 1));
262 test.group_name = group->name;
263
264 auto commit_test = [&]() {
265 if (test.script_path.empty()) {
266 return absl::InvalidArgumentError("Test case missing script_path");
267 }
268 if (test.name.empty()) {
269 test.name = DeriveTestName(test.script_path);
270 }
271 if (test.id.empty()) {
272 test.id = absl::StrCat(test.group_name, ":", test.name);
273 }
274 group->tests.push_back(std::move(test));
275 return absl::OkStatus();
276 };
277
278 if (content.empty()) {
279 ++(*index);
280 } else if (content.find(':') == std::string::npos) {
281 test.script_path = Unquote(content);
282 ++(*index);
283 return commit_test();
284 } else {
285 std::string key;
286 std::string value;
287 if (!ParseKeyValue(content, &key, &value)) {
288 return absl::InvalidArgumentError("Malformed key/value in test entry");
289 }
290 if (key == "path" || key == "script" || key == "script_path") {
291 test.script_path = Unquote(value);
292 } else if (key == "name") {
293 test.name = Unquote(value);
294 } else if (key == "description") {
295 test.description = Unquote(value);
296 } else if (key == "id") {
297 test.id = Unquote(value);
298 } else if (key == "tags") {
299 auto tags = ParseInlineList(value);
300 test.tags.insert(test.tags.end(), tags.begin(), tags.end());
301 } else {
302 test.parameters[key] = Unquote(value);
303 }
304 ++(*index);
305 }
306
307 while (*index < lines.size()) {
308 std::string raw = StripComment(lines[*index]);
309 std::string trimmed = Trim(raw);
310 int indent_next = CountIndent(raw);
311 if (trimmed.empty()) {
312 ++(*index);
313 continue;
314 }
315 if (indent_next <= base_indent) {
316 break;
317 }
318 if (indent_next == base_indent + 2) {
319 std::string key;
320 std::string value;
321 if (!ParseKeyValue(trimmed, &key, &value)) {
322 return absl::InvalidArgumentError(
323 "Expected key/value pair in test definition");
324 }
325 if (key == "path" || key == "script" || key == "script_path") {
326 test.script_path = Unquote(value);
327 } else if (key == "name") {
328 test.name = Unquote(value);
329 } else if (key == "description") {
330 test.description = Unquote(value);
331 } else if (key == "id") {
332 test.id = Unquote(value);
333 } else if (key == "tags") {
334 auto tags = ParseInlineList(value);
335 if (tags.empty() && value.empty()) {
336 ++(*index);
337 RETURN_IF_ERROR(ParseStringListBlock(lines, index, indent_next + 2,
338 &test.tags));
339 continue;
340 }
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");
346 }
347 ++(*index);
348 RETURN_IF_ERROR(ParseParametersBlock(lines, index, indent_next + 2,
349 &test.parameters));
350 continue;
351 } else {
352 test.parameters[key] = Unquote(value);
353 }
354 ++(*index);
355 } else {
356 return absl::InvalidArgumentError(
357 "Unexpected indentation inside test entry");
358 }
359 }
360
361 return commit_test();
362}
363
364absl::Status ParseTestsBlock(const std::vector<std::string>& lines,
365 size_t* index, int base_indent,
366 TestGroupDefinition* group) {
367 while (*index < lines.size()) {
368 std::string raw = StripComment(lines[*index]);
369 std::string trimmed = Trim(raw);
370 int indent = CountIndent(raw);
371 if (trimmed.empty()) {
372 ++(*index);
373 continue;
374 }
375 if (indent < base_indent) {
376 break;
377 }
378 if (indent != base_indent) {
379 return absl::InvalidArgumentError(
380 "Invalid indentation inside tests block");
381 }
382 RETURN_IF_ERROR(ParseTestCaseEntry(lines, index, base_indent, group));
383 }
384 return absl::OkStatus();
385}
386
387absl::Status ParseGroupEntry(const std::vector<std::string>& lines,
388 size_t* index, TestSuiteDefinition* suite) {
389 const std::string& raw_line = lines[*index];
390 std::string stripped = StripComment(raw_line);
391 int base_indent = CountIndent(stripped);
392
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");
396 }
397
398 std::string content = Trim(stripped.substr(dash_pos + 1));
400
401 if (!content.empty()) {
402 std::string key;
403 std::string value;
404 if (!ParseKeyValue(content, &key, &value)) {
405 return absl::InvalidArgumentError("Malformed group entry");
406 }
407 if (key == "name") {
408 group.name = Unquote(value);
409 } else if (key == "description") {
410 group.description = Unquote(value);
411 } else if (key == "depends_on") {
412 auto deps = ParseInlineList(value);
413 group.depends_on.insert(group.depends_on.end(), deps.begin(), deps.end());
414 } else {
415 return absl::InvalidArgumentError(
416 absl::StrCat("Unknown field in group entry: '", key, "'"));
417 }
418 }
419
420 ++(*index);
421
422 while (*index < lines.size()) {
423 std::string raw = StripComment(lines[*index]);
424 std::string trimmed = Trim(raw);
425 int indent = CountIndent(raw);
426 if (trimmed.empty()) {
427 ++(*index);
428 continue;
429 }
430 if (indent <= base_indent) {
431 break;
432 }
433 if (indent == base_indent + 2) {
434 std::string key;
435 std::string value;
436 if (!ParseKeyValue(trimmed, &key, &value)) {
437 return absl::InvalidArgumentError(
438 "Expected key/value pair inside group definition");
439 }
440 if (key == "name") {
441 group.name = Unquote(value);
442 ++(*index);
443 } else if (key == "description") {
444 group.description = Unquote(value);
445 ++(*index);
446 } else if (key == "depends_on") {
447 if (!value.empty()) {
448 auto deps = ParseInlineList(value);
449 group.depends_on.insert(group.depends_on.end(), deps.begin(),
450 deps.end());
451 ++(*index);
452 } else {
453 ++(*index);
454 RETURN_IF_ERROR(ParseStringListBlock(lines, index, base_indent + 4,
455 &group.depends_on));
456 }
457 } else if (key == "tests") {
458 if (!value.empty()) {
459 return absl::InvalidArgumentError(
460 "tests block must be defined as indented list");
461 }
462 ++(*index);
464 ParseTestsBlock(lines, index, base_indent + 4, &group));
465 } else {
466 return absl::InvalidArgumentError(
467 absl::StrCat("Unknown attribute in group definition: '", key,
468 "'"));
469 }
470 } else {
471 return absl::InvalidArgumentError(
472 "Unexpected indentation inside group definition");
473 }
474 }
475
476 if (group.name.empty()) {
477 return absl::InvalidArgumentError(
478 "Each test group must define a name");
479 }
480 suite->groups.push_back(std::move(group));
481 return absl::OkStatus();
482}
483
484absl::Status ParseGroupBlock(const std::vector<std::string>& lines,
485 size_t* index, TestSuiteDefinition* suite) {
486 while (*index < lines.size()) {
487 std::string raw = StripComment(lines[*index]);
488 std::string trimmed = Trim(raw);
489 int indent = CountIndent(raw);
490 if (trimmed.empty()) {
491 ++(*index);
492 continue;
493 }
494 if (indent < 2) {
495 break;
496 }
497 if (indent != 2) {
498 return absl::InvalidArgumentError(
499 "Invalid indentation inside test_groups block");
500 }
501 RETURN_IF_ERROR(ParseGroupEntry(lines, index, suite));
502 }
503 return absl::OkStatus();
504}
505
506absl::Status ParseConfigBlock(const std::vector<std::string>& lines,
507 size_t* index, TestSuiteConfig* config) {
508 while (*index < lines.size()) {
509 std::string raw = StripComment(lines[*index]);
510 std::string trimmed = Trim(raw);
511 int indent = CountIndent(raw);
512 if (trimmed.empty()) {
513 ++(*index);
514 continue;
515 }
516 if (indent < 2) {
517 break;
518 }
519 if (indent != 2) {
520 return absl::InvalidArgumentError(
521 "Invalid indentation inside config block");
522 }
523 std::string key;
524 std::string value;
525 if (!ParseKeyValue(trimmed, &key, &value)) {
526 return absl::InvalidArgumentError(
527 "Expected key/value pair inside config block");
528 }
529 RETURN_IF_ERROR(ParseScalarConfig(key, value, config));
530 ++(*index);
531 }
532 return absl::OkStatus();
533}
534
535} // namespace
536
537absl::StatusOr<TestSuiteDefinition> ParseTestSuiteDefinition(
538 absl::string_view content) {
539 std::vector<std::string> lines = absl::StrSplit(content, '\n');
541 size_t index = 0;
542
543 while (index < lines.size()) {
544 std::string raw = StripComment(lines[index]);
545 std::string trimmed = Trim(raw);
546 if (trimmed.empty()) {
547 ++index;
548 continue;
549 }
550
551 int indent = CountIndent(raw);
552 if (indent != 0) {
553 return absl::InvalidArgumentError(
554 "Top-level entries must not be indented in suite definition");
555 }
556
557 std::string key;
558 std::string value;
559 if (!ParseKeyValue(trimmed, &key, &value)) {
560 return absl::InvalidArgumentError(
561 absl::StrCat("Malformed top-level entry: '", trimmed, "'"));
562 }
563
564 if (key == "name") {
565 suite.name = Unquote(value);
566 ++index;
567 } else if (key == "description") {
568 suite.description = Unquote(value);
569 ++index;
570 } else if (key == "version") {
571 suite.version = Unquote(value);
572 ++index;
573 } else if (key == "config") {
574 if (!value.empty()) {
575 return absl::InvalidArgumentError(
576 "config block must not specify inline value");
577 }
578 ++index;
579 RETURN_IF_ERROR(ParseConfigBlock(lines, &index, &suite.config));
580 } else if (key == "test_groups") {
581 if (!value.empty()) {
582 return absl::InvalidArgumentError(
583 "test_groups must be defined as an indented list");
584 }
585 ++index;
586 RETURN_IF_ERROR(ParseGroupBlock(lines, &index, &suite));
587 } else {
588 return absl::InvalidArgumentError(
589 absl::StrCat("Unknown top-level key: '", key, "'"));
590 }
591 }
592
593 if (suite.name.empty()) {
594 suite.name = "Unnamed Suite";
595 }
596 if (suite.version.empty()) {
597 suite.version = "1.0";
598 }
599 return suite;
600}
601
602absl::StatusOr<TestSuiteDefinition> LoadTestSuiteFromFile(
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, "'"));
608 }
609 std::stringstream buffer;
610 buffer << file.rdbuf();
611 return ParseTestSuiteDefinition(buffer.str());
612}
613
614} // namespace cli
615} // namespace yaze
#define RETURN_IF_ERROR(expression)
Definition macro.h:53
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:61
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)
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)
absl::StatusOr< int > ParseDurationSeconds(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
Definition test_suite.h:28
std::vector< std::string > tags
Definition test_suite.h:27
std::vector< TestCaseDefinition > tests
Definition test_suite.h:35
std::vector< std::string > depends_on
Definition test_suite.h:34
std::vector< TestGroupDefinition > groups
Definition test_suite.h:43