yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
test_script_parser.cc
Go to the documentation of this file.
2
3#include <filesystem>
4#include <fstream>
5#include <sstream>
6
7#include "absl/strings/ascii.h"
8#include "absl/strings/str_format.h"
9#include "absl/time/clock.h"
10#include "nlohmann/json.hpp"
11#include "util/macro.h"
12
13namespace yaze {
14namespace test {
15namespace {
16
17constexpr int kSupportedSchemaVersion = 1;
18
19std::string FormatIsoTimestamp(absl::Time time) {
20 if (time == absl::InfinitePast()) {
21 return "";
22 }
23 return absl::FormatTime("%Y-%m-%dT%H:%M:%S%Ez", time, absl::UTCTimeZone());
24}
25
26absl::StatusOr<absl::Time> ParseIsoTimestamp(const nlohmann::json& node,
27 const char* field_name) {
28 if (!node.contains(field_name)) {
29 return absl::InfinitePast();
30 }
31 const std::string value = node.at(field_name).get<std::string>();
32 if (value.empty()) {
33 return absl::InfinitePast();
34 }
35 absl::Time parsed;
36 std::string err;
37 if (!absl::ParseTime("%Y-%m-%dT%H:%M:%S%Ez", value, &parsed, &err)) {
38 return absl::InvalidArgumentError(
39 absl::StrFormat("Failed to parse timestamp '%s': %s", value, err));
40 }
41 return parsed;
42}
43
44void WriteExpectSection(const TestScriptStep& step, nlohmann::json* node) {
45 nlohmann::json expect;
46 expect["success"] = step.expect_success;
47 if (!step.expect_status.empty()) {
48 expect["status"] = step.expect_status;
49 }
50 if (!step.expect_message.empty()) {
51 expect["message"] = step.expect_message;
52 }
53 if (!step.expect_assertion_failures.empty()) {
54 expect["assertion_failures"] = step.expect_assertion_failures;
55 }
56 if (!step.expect_metrics.empty()) {
57 nlohmann::json metrics(nlohmann::json::value_t::object);
58 for (const auto& [key, value] : step.expect_metrics) {
59 metrics[key] = value;
60 }
61 expect["metrics"] = std::move(metrics);
62 }
63 (*node)["expect"] = std::move(expect);
64}
65
66void PopulateStepNode(const TestScriptStep& step, nlohmann::json* node) {
67 (*node)["action"] = step.action;
68 if (!step.target.empty()) {
69 (*node)["target"] = step.target;
70 }
71 if (!step.widget_key.empty()) {
72 (*node)["widget_key"] = step.widget_key;
73 }
74 if (!step.click_type.empty()) {
75 (*node)["click_type"] = step.click_type;
76 }
77 if (!step.text.empty()) {
78 (*node)["text"] = step.text;
79 }
80 if (step.clear_first) {
81 (*node)["clear_first"] = step.clear_first;
82 }
83 if (!step.condition.empty()) {
84 (*node)["condition"] = step.condition;
85 }
86 if (step.timeout_ms > 0) {
87 (*node)["timeout_ms"] = step.timeout_ms;
88 }
89 if (!step.region.empty()) {
90 (*node)["region"] = step.region;
91 }
92 if (!step.format.empty()) {
93 (*node)["format"] = step.format;
94 }
95 WriteExpectSection(step, node);
96}
97
98absl::StatusOr<TestScriptStep> ParseStep(const nlohmann::json& node) {
99 if (!node.contains("action")) {
100 return absl::InvalidArgumentError(
101 "Test script step missing required field 'action'");
102 }
103
104 TestScriptStep step;
105 step.action = absl::AsciiStrToLower(node.at("action").get<std::string>());
106 if (node.contains("target")) {
107 step.target = node.at("target").get<std::string>();
108 }
109 if (node.contains("widget_key")) {
110 step.widget_key = node.at("widget_key").get<std::string>();
111 }
112 if (node.contains("click_type")) {
113 step.click_type =
114 absl::AsciiStrToLower(node.at("click_type").get<std::string>());
115 }
116 if (node.contains("text")) {
117 step.text = node.at("text").get<std::string>();
118 }
119 if (node.contains("clear_first")) {
120 step.clear_first = node.at("clear_first").get<bool>();
121 }
122 if (node.contains("condition")) {
123 step.condition = node.at("condition").get<std::string>();
124 }
125 if (node.contains("timeout_ms")) {
126 step.timeout_ms = node.at("timeout_ms").get<int>();
127 }
128 if (node.contains("region")) {
129 step.region = node.at("region").get<std::string>();
130 }
131 if (node.contains("format")) {
132 step.format = node.at("format").get<std::string>();
133 }
134
135 if (node.contains("expect")) {
136 const auto& expect = node.at("expect");
137 if (expect.contains("success")) {
138 step.expect_success = expect.at("success").get<bool>();
139 }
140 if (expect.contains("status")) {
141 step.expect_status =
142 absl::AsciiStrToLower(expect.at("status").get<std::string>());
143 }
144 if (expect.contains("message")) {
145 step.expect_message = expect.at("message").get<std::string>();
146 }
147 if (expect.contains("assertion_failures")) {
148 for (const auto& value : expect.at("assertion_failures")) {
149 step.expect_assertion_failures.push_back(value.get<std::string>());
150 }
151 }
152 if (expect.contains("metrics")) {
153 for (auto it = expect.at("metrics").begin();
154 it != expect.at("metrics").end(); ++it) {
155 step.expect_metrics[it.key()] = it.value().get<int32_t>();
156 }
157 }
158 }
159
160 return step;
161}
162
163} // namespace
164
165absl::Status TestScriptParser::WriteToFile(const TestScript& script,
166 const std::string& path) {
167 nlohmann::json root;
168 root["schema_version"] = script.schema_version;
169 root["recording_id"] = script.recording_id;
170 root["name"] = script.name;
171 root["description"] = script.description;
172 root["created_at"] = FormatIsoTimestamp(script.created_at);
173 root["duration_ms"] = absl::ToInt64Milliseconds(script.duration);
174
175 nlohmann::json steps_json = nlohmann::json::array();
176 for (const auto& step : script.steps) {
177 nlohmann::json step_node(nlohmann::json::value_t::object);
178 PopulateStepNode(step, &step_node);
179 steps_json.push_back(std::move(step_node));
180 }
181 root["steps"] = std::move(steps_json);
182
183 std::filesystem::path output_path(path);
184 std::error_code ec;
185 auto parent = output_path.parent_path();
186 if (!parent.empty() && !std::filesystem::exists(parent)) {
187 if (!std::filesystem::create_directories(parent, ec)) {
188 return absl::InternalError(
189 absl::StrFormat("Failed to create directory '%s': %s",
190 parent.string(), ec.message()));
191 }
192 }
193
194 std::ofstream ofs(output_path, std::ios::out | std::ios::trunc);
195 if (!ofs.good()) {
196 return absl::InternalError(
197 absl::StrFormat("Failed to open '%s' for writing", path));
198 }
199 ofs << root.dump(2) << '\n';
200 return absl::OkStatus();
201}
202
203absl::StatusOr<TestScript> TestScriptParser::ParseFromFile(
204 const std::string& path) {
205 std::ifstream ifs(path);
206 if (!ifs.good()) {
207 return absl::NotFoundError(
208 absl::StrFormat("Test script '%s' not found", path));
209 }
210
211 nlohmann::json root;
212 try {
213 ifs >> root;
214 } catch (const nlohmann::json::exception& e) {
215 return absl::InvalidArgumentError(
216 absl::StrFormat("Failed to parse JSON: %s", e.what()));
217 }
218
219 TestScript script;
220 script.schema_version =
221 root.contains("schema_version") ? root["schema_version"].get<int>() : 1;
222
223 if (script.schema_version != kSupportedSchemaVersion) {
224 return absl::InvalidArgumentError(absl::StrFormat(
225 "Unsupported test script schema version %d", script.schema_version));
226 }
227
228 if (root.contains("recording_id")) {
229 script.recording_id = root["recording_id"].get<std::string>();
230 }
231 if (root.contains("name")) {
232 script.name = root["name"].get<std::string>();
233 }
234 if (root.contains("description")) {
235 script.description = root["description"].get<std::string>();
236 }
237
238 ASSIGN_OR_RETURN(script.created_at, ParseIsoTimestamp(root, "created_at"));
239 if (root.contains("duration_ms")) {
240 script.duration = absl::Milliseconds(root["duration_ms"].get<int64_t>());
241 }
242
243 if (!root.contains("steps") || !root["steps"].is_array()) {
244 return absl::InvalidArgumentError(
245 "Test script missing required array field 'steps'");
246 }
247
248 for (const auto& step_node : root["steps"]) {
249 ASSIGN_OR_RETURN(auto step, ParseStep(step_node));
250 script.steps.push_back(std::move(step));
251 }
252
253 return script;
254}
255
256} // namespace test
257} // namespace yaze
static absl::Status WriteToFile(const TestScript &script, const std::string &path)
static absl::StatusOr< TestScript > ParseFromFile(const std::string &path)
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
absl::StatusOr< TestScriptStep > ParseStep(const nlohmann::json &node)
void WriteExpectSection(const TestScriptStep &step, nlohmann::json *node)
void PopulateStepNode(const TestScriptStep &step, nlohmann::json *node)
absl::StatusOr< absl::Time > ParseIsoTimestamp(const nlohmann::json &node, const char *field_name)
std::vector< std::string > expect_assertion_failures
std::map< std::string, int32_t > expect_metrics
std::vector< TestScriptStep > steps