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.click_type.empty()) {
72 (*node)["click_type"] = step.click_type;
73 }
74 if (!step.text.empty()) {
75 (*node)["text"] = step.text;
76 }
77 if (step.clear_first) {
78 (*node)["clear_first"] = step.clear_first;
79 }
80 if (!step.condition.empty()) {
81 (*node)["condition"] = step.condition;
82 }
83 if (step.timeout_ms > 0) {
84 (*node)["timeout_ms"] = step.timeout_ms;
85 }
86 if (!step.region.empty()) {
87 (*node)["region"] = step.region;
88 }
89 if (!step.format.empty()) {
90 (*node)["format"] = step.format;
91 }
92 WriteExpectSection(step, node);
93}
94
95absl::StatusOr<TestScriptStep> ParseStep(const nlohmann::json& node) {
96 if (!node.contains("action")) {
97 return absl::InvalidArgumentError(
98 "Test script step missing required field 'action'");
99 }
100
101 TestScriptStep step;
102 step.action = absl::AsciiStrToLower(node.at("action").get<std::string>());
103 if (node.contains("target")) {
104 step.target = node.at("target").get<std::string>();
105 }
106 if (node.contains("click_type")) {
107 step.click_type =
108 absl::AsciiStrToLower(node.at("click_type").get<std::string>());
109 }
110 if (node.contains("text")) {
111 step.text = node.at("text").get<std::string>();
112 }
113 if (node.contains("clear_first")) {
114 step.clear_first = node.at("clear_first").get<bool>();
115 }
116 if (node.contains("condition")) {
117 step.condition = node.at("condition").get<std::string>();
118 }
119 if (node.contains("timeout_ms")) {
120 step.timeout_ms = node.at("timeout_ms").get<int>();
121 }
122 if (node.contains("region")) {
123 step.region = node.at("region").get<std::string>();
124 }
125 if (node.contains("format")) {
126 step.format = node.at("format").get<std::string>();
127 }
128
129 if (node.contains("expect")) {
130 const auto& expect = node.at("expect");
131 if (expect.contains("success")) {
132 step.expect_success = expect.at("success").get<bool>();
133 }
134 if (expect.contains("status")) {
135 step.expect_status =
136 absl::AsciiStrToLower(expect.at("status").get<std::string>());
137 }
138 if (expect.contains("message")) {
139 step.expect_message = expect.at("message").get<std::string>();
140 }
141 if (expect.contains("assertion_failures")) {
142 for (const auto& value : expect.at("assertion_failures")) {
143 step.expect_assertion_failures.push_back(value.get<std::string>());
144 }
145 }
146 if (expect.contains("metrics")) {
147 for (auto it = expect.at("metrics").begin();
148 it != expect.at("metrics").end(); ++it) {
149 step.expect_metrics[it.key()] = it.value().get<int32_t>();
150 }
151 }
152 }
153
154 return step;
155}
156
157} // namespace
158
159absl::Status TestScriptParser::WriteToFile(const TestScript& script,
160 const std::string& path) {
161 nlohmann::json root;
162 root["schema_version"] = script.schema_version;
163 root["recording_id"] = script.recording_id;
164 root["name"] = script.name;
165 root["description"] = script.description;
166 root["created_at"] = FormatIsoTimestamp(script.created_at);
167 root["duration_ms"] = absl::ToInt64Milliseconds(script.duration);
168
169 nlohmann::json steps_json = nlohmann::json::array();
170 for (const auto& step : script.steps) {
171 nlohmann::json step_node(nlohmann::json::value_t::object);
172 PopulateStepNode(step, &step_node);
173 steps_json.push_back(std::move(step_node));
174 }
175 root["steps"] = std::move(steps_json);
176
177 std::filesystem::path output_path(path);
178 std::error_code ec;
179 auto parent = output_path.parent_path();
180 if (!parent.empty() && !std::filesystem::exists(parent)) {
181 if (!std::filesystem::create_directories(parent, ec)) {
182 return absl::InternalError(absl::StrFormat(
183 "Failed to create directory '%s': %s", parent.string(), ec.message()));
184 }
185 }
186
187 std::ofstream ofs(output_path, std::ios::out | std::ios::trunc);
188 if (!ofs.good()) {
189 return absl::InternalError(
190 absl::StrFormat("Failed to open '%s' for writing", path));
191 }
192 ofs << root.dump(2) << '\n';
193 return absl::OkStatus();
194}
195
196absl::StatusOr<TestScript> TestScriptParser::ParseFromFile(
197 const std::string& path) {
198 std::ifstream ifs(path);
199 if (!ifs.good()) {
200 return absl::NotFoundError(
201 absl::StrFormat("Test script '%s' not found", path));
202 }
203
204 nlohmann::json root;
205 try {
206 ifs >> root;
207 } catch (const nlohmann::json::exception& e) {
208 return absl::InvalidArgumentError(
209 absl::StrFormat("Failed to parse JSON: %s", e.what()));
210 }
211
212 TestScript script;
213 script.schema_version =
214 root.contains("schema_version") ? root["schema_version"].get<int>() : 1;
215
216 if (script.schema_version != kSupportedSchemaVersion) {
217 return absl::InvalidArgumentError(absl::StrFormat(
218 "Unsupported test script schema version %d", script.schema_version));
219 }
220
221 if (root.contains("recording_id")) {
222 script.recording_id = root["recording_id"].get<std::string>();
223 }
224 if (root.contains("name")) {
225 script.name = root["name"].get<std::string>();
226 }
227 if (root.contains("description")) {
228 script.description = root["description"].get<std::string>();
229 }
230
232 ParseIsoTimestamp(root, "created_at"));
233 if (root.contains("duration_ms")) {
234 script.duration = absl::Milliseconds(root["duration_ms"].get<int64_t>());
235 }
236
237 if (!root.contains("steps") || !root["steps"].is_array()) {
238 return absl::InvalidArgumentError(
239 "Test script missing required array field 'steps'");
240 }
241
242 for (const auto& step_node : root["steps"]) {
243 ASSIGN_OR_RETURN(auto step, ParseStep(step_node));
244 script.steps.push_back(std::move(step));
245 }
246
247 return script;
248}
249
250} // namespace test
251} // 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:61
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)
Main namespace for the application.
Definition controller.cc:20
std::vector< std::string > expect_assertion_failures
std::map< std::string, int32_t > expect_metrics
std::vector< TestScriptStep > steps