yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
screenshot_assertion.cc
Go to the documentation of this file.
2
3#include <cmath>
4#include <filesystem>
5#include <fstream>
6
7#include "absl/strings/str_cat.h"
8#include "util/log.h"
9
10namespace yaze {
11namespace test {
12
14
15absl::StatusOr<ComparisonResult> ScreenshotAssertion::AssertMatchesReference(
16 const std::string& reference_path) {
17 auto current = CaptureScreen();
18 if (!current.ok()) {
19 return current.status();
20 }
21
22 auto expected = LoadReference(reference_path);
23 if (!expected.ok()) {
24 return expected.status();
25 }
26
27 auto result = Compare(*current, *expected);
28 result.passed = result.similarity >= config_.tolerance;
29
30 return result;
31}
32
33absl::StatusOr<ComparisonResult> ScreenshotAssertion::AssertMatchesScreenshot(
34 const Screenshot& expected) {
35 auto current = CaptureScreen();
36 if (!current.ok()) {
37 return current.status();
38 }
39
40 auto result = Compare(*current, expected);
41 result.passed = result.similarity >= config_.tolerance;
42
43 return result;
44}
45
46absl::StatusOr<ComparisonResult> ScreenshotAssertion::AssertRegionMatches(
47 const std::string& reference_path, const ScreenRegion& region) {
48 auto current = CaptureScreen();
49 if (!current.ok()) {
50 return current.status();
51 }
52
53 auto expected = LoadReference(reference_path);
54 if (!expected.ok()) {
55 return expected.status();
56 }
57
58 auto result = CompareRegion(*current, *expected, region);
59 result.passed = result.similarity >= config_.tolerance;
60
61 return result;
62}
63
64absl::StatusOr<ComparisonResult> ScreenshotAssertion::AssertChanged(
65 const std::string& baseline_name) {
66 auto current = CaptureScreen();
67 if (!current.ok()) {
68 return current.status();
69 }
70
71 auto baseline = GetBaseline(baseline_name);
72 if (!baseline.ok()) {
73 return baseline.status();
74 }
75
76 auto result = Compare(*current, *baseline);
77 // For "changed" assertion, we want LOW similarity
78 result.passed = result.similarity < config_.tolerance;
79
80 return result;
81}
82
83absl::StatusOr<ComparisonResult> ScreenshotAssertion::AssertUnchanged(
84 const std::string& baseline_name) {
85 auto current = CaptureScreen();
86 if (!current.ok()) {
87 return current.status();
88 }
89
90 auto baseline = GetBaseline(baseline_name);
91 if (!baseline.ok()) {
92 return baseline.status();
93 }
94
95 auto result = Compare(*current, *baseline);
96 result.passed = result.similarity >= config_.tolerance;
97
98 return result;
99}
100
101absl::Status ScreenshotAssertion::CaptureBaseline(const std::string& name) {
102 auto screenshot = CaptureScreen();
103 if (!screenshot.ok()) {
104 return screenshot.status();
105 }
106
107 baselines_[name] = std::move(*screenshot);
108 LOG_DEBUG("ScreenshotAssertion", "Baseline '%s' captured (%dx%d)",
109 name.c_str(), baselines_[name].width, baselines_[name].height);
110
111 return absl::OkStatus();
112}
113
114absl::Status ScreenshotAssertion::SaveAsReference(const std::string& path) {
115 auto screenshot = CaptureScreen();
116 if (!screenshot.ok()) {
117 return screenshot.status();
118 }
119
120 return SaveScreenshot(*screenshot, path);
121}
122
123absl::StatusOr<Screenshot> ScreenshotAssertion::LoadReference(
124 const std::string& path) {
125 return LoadScreenshot(path);
126}
127
128absl::StatusOr<Screenshot> ScreenshotAssertion::GetBaseline(
129 const std::string& name) const {
130 auto it = baselines_.find(name);
131 if (it == baselines_.end()) {
132 return absl::NotFoundError(
133 absl::StrCat("Baseline not found: ", name));
134 }
135 return it->second;
136}
137
139 const Screenshot& expected) {
140 return CompareRegion(actual, expected, ScreenRegion::FullScreen());
141}
142
144 const Screenshot& actual, const Screenshot& expected,
145 const ScreenRegion& region) {
146 auto start = std::chrono::steady_clock::now();
147
148 ComparisonResult result;
149
150 // Validate screenshots
151 if (!actual.IsValid() || !expected.IsValid()) {
152 result.error_message = "Invalid screenshot data";
153 return result;
154 }
155
156 // Use appropriate algorithm
157 switch (config_.algorithm) {
159 result = ComparePixelExact(actual, expected, region);
160 break;
162 result = ComparePerceptualHash(actual, expected);
163 break;
165 result = CompareStructural(actual, expected);
166 break;
167 }
168
169 auto end = std::chrono::steady_clock::now();
170 result.comparison_time =
171 std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
172
173 // Generate diff image if requested and there are differences
174 if (config_.generate_diff_image && result.differing_pixels > 0) {
175 std::string diff_path = absl::StrCat(
176 config_.diff_output_dir, "/diff_",
177 std::chrono::system_clock::now().time_since_epoch().count(), ".png");
178 auto gen_result = GenerateDiffImage(actual, expected, diff_path);
179 if (gen_result.ok()) {
180 result.diff_image_path = *gen_result;
181 }
182 }
183
184 return result;
185}
186
188 const Screenshot& actual, const Screenshot& expected,
189 const ScreenRegion& region) {
190 ComparisonResult result;
191
192 // Determine comparison bounds
193 int start_x = region.x;
194 int start_y = region.y;
195 int end_x = region.width > 0 ? region.x + region.width
196 : std::min(actual.width, expected.width);
197 int end_y = region.height > 0 ? region.y + region.height
198 : std::min(actual.height, expected.height);
199
200 // Clamp to valid range
201 start_x = std::max(0, start_x);
202 start_y = std::max(0, start_y);
203 end_x = std::min(end_x, std::min(actual.width, expected.width));
204 end_y = std::min(end_y, std::min(actual.height, expected.height));
205
206 int total_pixels = 0;
207 int matching_pixels = 0;
208
209 for (int y = start_y; y < end_y; ++y) {
210 for (int x = start_x; x < end_x; ++x) {
211 // Check if in ignore region
212 bool ignored = false;
213 for (const auto& ignore : config_.ignore_regions) {
214 if (x >= ignore.x && x < ignore.x + ignore.width &&
215 y >= ignore.y && y < ignore.y + ignore.height) {
216 ignored = true;
217 break;
218 }
219 }
220
221 if (ignored) continue;
222
223 total_pixels++;
224
225 size_t actual_idx = actual.GetPixelIndex(x, y);
226 size_t expected_idx = expected.GetPixelIndex(x, y);
227
228 if (actual_idx + 3 < actual.data.size() &&
229 expected_idx + 3 < expected.data.size()) {
230 bool match = ColorsMatch(
231 actual.data[actual_idx], actual.data[actual_idx + 1],
232 actual.data[actual_idx + 2], expected.data[expected_idx],
233 expected.data[expected_idx + 1], expected.data[expected_idx + 2],
235
236 if (match) {
237 matching_pixels++;
238 }
239 }
240 }
241 }
242
243 result.total_pixels = total_pixels;
244 result.differing_pixels = total_pixels - matching_pixels;
245 result.similarity = total_pixels > 0
246 ? static_cast<float>(matching_pixels) / total_pixels
247 : 0.0f;
248 result.difference_percentage =
249 total_pixels > 0
250 ? (static_cast<float>(result.differing_pixels) / total_pixels) * 100
251 : 0.0f;
252
253 return result;
254}
255
257 const Screenshot& actual, const Screenshot& expected) {
258 // Simplified perceptual hash comparison
259 // TODO: Implement proper pHash algorithm
260 ComparisonResult result;
261 result.error_message = "Perceptual hash not yet implemented";
262 return result;
263}
264
266 const Screenshot& actual, const Screenshot& expected) {
267 // Simplified SSIM-like comparison
268 // TODO: Implement proper SSIM algorithm
269 ComparisonResult result;
270 result.error_message = "Structural similarity not yet implemented";
271 return result;
272}
273
274absl::StatusOr<std::string> ScreenshotAssertion::GenerateDiffImage(
275 const Screenshot& actual, const Screenshot& expected,
276 const std::string& output_path) {
277 // Create a diff image highlighting differences
278 Screenshot diff;
279 diff.width = std::min(actual.width, expected.width);
280 diff.height = std::min(actual.height, expected.height);
281 diff.data.resize(diff.width * diff.height * 4);
282
283 for (int y = 0; y < diff.height; ++y) {
284 for (int x = 0; x < diff.width; ++x) {
285 size_t actual_idx = actual.GetPixelIndex(x, y);
286 size_t expected_idx = expected.GetPixelIndex(x, y);
287 size_t diff_idx = diff.GetPixelIndex(x, y);
288
289 if (actual_idx + 3 < actual.data.size() &&
290 expected_idx + 3 < expected.data.size()) {
291 bool match = ColorsMatch(
292 actual.data[actual_idx], actual.data[actual_idx + 1],
293 actual.data[actual_idx + 2], expected.data[expected_idx],
294 expected.data[expected_idx + 1], expected.data[expected_idx + 2],
296
297 if (match) {
298 // Matching pixels: dimmed grayscale
299 uint8_t gray = static_cast<uint8_t>(
300 (actual.data[actual_idx] + actual.data[actual_idx + 1] +
301 actual.data[actual_idx + 2]) / 3 * 0.3);
302 diff.data[diff_idx] = gray;
303 diff.data[diff_idx + 1] = gray;
304 diff.data[diff_idx + 2] = gray;
305 diff.data[diff_idx + 3] = 255;
306 } else {
307 // Different pixels: bright red
308 diff.data[diff_idx] = 255;
309 diff.data[diff_idx + 1] = 0;
310 diff.data[diff_idx + 2] = 0;
311 diff.data[diff_idx + 3] = 255;
312 }
313 }
314 }
315 }
316
317 auto status = SaveScreenshot(diff, output_path);
318 if (!status.ok()) {
319 return status;
320 }
321
322 return output_path;
323}
324
326 int x, int y, uint8_t r, uint8_t g, uint8_t b, int tolerance) {
327 auto screenshot = CaptureScreen();
328 if (!screenshot.ok()) {
329 return screenshot.status();
330 }
331
332 if (x < 0 || x >= screenshot->width || y < 0 || y >= screenshot->height) {
333 return absl::OutOfRangeError("Pixel coordinates out of bounds");
334 }
335
336 size_t idx = screenshot->GetPixelIndex(x, y);
337 return ColorsMatch(screenshot->data[idx], screenshot->data[idx + 1],
338 screenshot->data[idx + 2], r, g, b, tolerance);
339}
340
342 const ScreenRegion& region, uint8_t r, uint8_t g, uint8_t b,
343 float min_coverage) {
344 auto screenshot = CaptureScreen();
345 if (!screenshot.ok()) {
346 return screenshot.status();
347 }
348
349 int matching = 0;
350 int total = 0;
351
352 int end_x = region.width > 0 ? region.x + region.width : screenshot->width;
353 int end_y = region.height > 0 ? region.y + region.height : screenshot->height;
354
355 for (int y = region.y; y < end_y && y < screenshot->height; ++y) {
356 for (int x = region.x; x < end_x && x < screenshot->width; ++x) {
357 total++;
358 size_t idx = screenshot->GetPixelIndex(x, y);
359 if (ColorsMatch(screenshot->data[idx], screenshot->data[idx + 1],
360 screenshot->data[idx + 2], r, g, b,
362 matching++;
363 }
364 }
365 }
366
367 float coverage = total > 0 ? static_cast<float>(matching) / total : 0.0f;
368 return coverage >= min_coverage;
369}
370
372 const ScreenRegion& region, uint8_t r, uint8_t g, uint8_t b,
373 int tolerance) {
374 auto result = AssertRegionContainsColor(region, r, g, b, 0.001f);
375 if (!result.ok()) {
376 return result.status();
377 }
378 return !*result; // Invert: true if color NOT found
379}
380
381absl::StatusOr<Screenshot> ScreenshotAssertion::CaptureScreen() {
382 if (!capture_callback_) {
383 return absl::FailedPreconditionError("Capture callback not set");
384 }
385 return capture_callback_();
386}
387
388absl::Status ScreenshotAssertion::SaveScreenshot(const Screenshot& screenshot,
389 const std::string& path) {
390 // Create directory if needed
391 std::filesystem::path filepath(path);
392 std::filesystem::create_directories(filepath.parent_path());
393
394 // TODO: Implement proper PNG encoding
395 // For now, save as raw RGBA
396 std::ofstream file(path, std::ios::binary);
397 if (!file) {
398 return absl::UnavailableError(
399 absl::StrCat("Cannot open file for writing: ", path));
400 }
401
402 // Write simple header (width, height, then RGBA data)
403 file.write(reinterpret_cast<const char*>(&screenshot.width), sizeof(int));
404 file.write(reinterpret_cast<const char*>(&screenshot.height), sizeof(int));
405 file.write(reinterpret_cast<const char*>(screenshot.data.data()),
406 screenshot.data.size());
407
408 LOG_DEBUG("ScreenshotAssertion", "Screenshot saved: %s (%dx%d)",
409 path.c_str(), screenshot.width, screenshot.height);
410
411 return absl::OkStatus();
412}
413
414absl::StatusOr<Screenshot> ScreenshotAssertion::LoadScreenshot(
415 const std::string& path) {
416 std::ifstream file(path, std::ios::binary);
417 if (!file) {
418 return absl::NotFoundError(absl::StrCat("Cannot open file: ", path));
419 }
420
421 Screenshot screenshot;
422 screenshot.source = path;
423
424 file.read(reinterpret_cast<char*>(&screenshot.width), sizeof(int));
425 file.read(reinterpret_cast<char*>(&screenshot.height), sizeof(int));
426
427 size_t data_size = screenshot.width * screenshot.height * 4;
428 screenshot.data.resize(data_size);
429 file.read(reinterpret_cast<char*>(screenshot.data.data()), data_size);
430
431 return screenshot;
432}
433
434bool ScreenshotAssertion::ColorsMatch(uint8_t r1, uint8_t g1, uint8_t b1,
435 uint8_t r2, uint8_t g2, uint8_t b2,
436 int threshold) const {
437 return std::abs(r1 - r2) <= threshold && std::abs(g1 - g2) <= threshold &&
438 std::abs(b1 - b2) <= threshold;
439}
440
442 const Screenshot& actual, const Screenshot& expected, int threshold) {
443 // TODO: Implement region clustering for difference visualization
444 return {};
445}
446
447} // namespace test
448} // namespace yaze
absl::StatusOr< ComparisonResult > AssertMatchesReference(const std::string &reference_path)
Assert current screen matches a reference image file.
absl::StatusOr< ComparisonResult > AssertUnchanged(const std::string &baseline_name)
Assert screen has NOT changed since baseline.
static absl::Status SaveScreenshot(const Screenshot &screenshot, const std::string &path)
Save screenshot to file (PNG format).
absl::StatusOr< Screenshot > CaptureScreen()
Capture current screen and return it.
ComparisonResult CompareStructural(const Screenshot &actual, const Screenshot &expected)
absl::StatusOr< bool > AssertRegionExcludesColor(const ScreenRegion &region, uint8_t r, uint8_t g, uint8_t b, int tolerance=10)
Assert region does NOT contain a specific color.
absl::StatusOr< Screenshot > LoadReference(const std::string &path)
Load a reference image from file.
absl::StatusOr< bool > AssertPixelColor(int x, int y, uint8_t r, uint8_t g, uint8_t b, int tolerance=10)
Assert pixel at (x, y) has expected color.
absl::StatusOr< std::string > GenerateDiffImage(const Screenshot &actual, const Screenshot &expected, const std::string &output_path)
Generate a visual diff image.
absl::StatusOr< ComparisonResult > AssertRegionMatches(const std::string &reference_path, const ScreenRegion &region)
Assert a specific region matches reference.
std::unordered_map< std::string, Screenshot > baselines_
ComparisonResult ComparePixelExact(const Screenshot &actual, const Screenshot &expected, const ScreenRegion &region)
absl::Status CaptureBaseline(const std::string &name)
Capture and store a baseline screenshot.
static absl::StatusOr< Screenshot > LoadScreenshot(const std::string &path)
Load screenshot from file.
bool ColorsMatch(uint8_t r1, uint8_t g1, uint8_t b1, uint8_t r2, uint8_t g2, uint8_t b2, int threshold) const
absl::Status SaveAsReference(const std::string &path)
Save current screen as a new reference image.
ComparisonResult Compare(const Screenshot &actual, const Screenshot &expected)
Compare two screenshots.
absl::StatusOr< Screenshot > GetBaseline(const std::string &name) const
Get a previously captured baseline.
absl::StatusOr< ComparisonResult > AssertChanged(const std::string &baseline_name)
Assert screen has changed since baseline.
ComparisonResult ComparePerceptualHash(const Screenshot &actual, const Screenshot &expected)
std::vector< ScreenRegion > FindDifferingRegions(const Screenshot &actual, const Screenshot &expected, int threshold)
absl::StatusOr< bool > AssertRegionContainsColor(const ScreenRegion &region, uint8_t r, uint8_t g, uint8_t b, float min_coverage=0.1f)
Assert region contains a specific color.
ComparisonResult CompareRegion(const Screenshot &actual, const Screenshot &expected, const ScreenRegion &region)
Compare specific regions of two screenshots.
absl::StatusOr< ComparisonResult > AssertMatchesScreenshot(const Screenshot &expected)
Assert current screen matches another Screenshot object.
#define LOG_DEBUG(category, format,...)
Definition log.h:103
std::vector< ScreenRegion > ignore_regions
Result of a screenshot comparison.
std::chrono::milliseconds comparison_time
Region of interest for screenshot comparison.
static ScreenRegion FullScreen()
Screenshot data container.
std::vector< uint8_t > data
size_t GetPixelIndex(int x, int y) const