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(absl::StrCat("Baseline not found: ", name));
133 }
134 return it->second;
135}
136
138 const Screenshot& expected) {
139 return CompareRegion(actual, expected, ScreenRegion::FullScreen());
140}
141
143 const Screenshot& actual, const Screenshot& expected,
144 const ScreenRegion& region) {
145 auto start = std::chrono::steady_clock::now();
146
147 ComparisonResult result;
148
149 // Validate screenshots
150 if (!actual.IsValid() || !expected.IsValid()) {
151 result.error_message = "Invalid screenshot data";
152 return result;
153 }
154
155 // Use appropriate algorithm
156 switch (config_.algorithm) {
158 result = ComparePixelExact(actual, expected, region);
159 break;
161 result = ComparePerceptualHash(actual, expected);
162 break;
164 result = CompareStructural(actual, expected);
165 break;
166 }
167
168 auto end = std::chrono::steady_clock::now();
169 result.comparison_time =
170 std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
171
172 // Generate diff image if requested and there are differences
173 if (config_.generate_diff_image && result.differing_pixels > 0) {
174 std::string diff_path = absl::StrCat(
175 config_.diff_output_dir, "/diff_",
176 std::chrono::system_clock::now().time_since_epoch().count(), ".png");
177 auto gen_result = GenerateDiffImage(actual, expected, diff_path);
178 if (gen_result.ok()) {
179 result.diff_image_path = *gen_result;
180 }
181 }
182
183 return result;
184}
185
187 const Screenshot& actual, const Screenshot& expected,
188 const ScreenRegion& region) {
189 ComparisonResult result;
190
191 // Determine comparison bounds
192 int start_x = region.x;
193 int start_y = region.y;
194 int end_x = region.width > 0 ? region.x + region.width
195 : std::min(actual.width, expected.width);
196 int end_y = region.height > 0 ? region.y + region.height
197 : std::min(actual.height, expected.height);
198
199 // Clamp to valid range
200 start_x = std::max(0, start_x);
201 start_y = std::max(0, start_y);
202 end_x = std::min(end_x, std::min(actual.width, expected.width));
203 end_y = std::min(end_y, std::min(actual.height, expected.height));
204
205 int total_pixels = 0;
206 int matching_pixels = 0;
207
208 for (int y = start_y; y < end_y; ++y) {
209 for (int x = start_x; x < end_x; ++x) {
210 // Check if in ignore region
211 bool ignored = false;
212 for (const auto& ignore : config_.ignore_regions) {
213 if (x >= ignore.x && x < ignore.x + ignore.width && y >= ignore.y &&
214 y < ignore.y + ignore.height) {
215 ignored = true;
216 break;
217 }
218 }
219
220 if (ignored)
221 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>((actual.data[actual_idx] +
300 actual.data[actual_idx + 1] +
301 actual.data[actual_idx + 2]) /
302 3 * 0.3);
303 diff.data[diff_idx] = gray;
304 diff.data[diff_idx + 1] = gray;
305 diff.data[diff_idx + 2] = gray;
306 diff.data[diff_idx + 3] = 255;
307 } else {
308 // Different pixels: bright red
309 diff.data[diff_idx] = 255;
310 diff.data[diff_idx + 1] = 0;
311 diff.data[diff_idx + 2] = 0;
312 diff.data[diff_idx + 3] = 255;
313 }
314 }
315 }
316 }
317
318 auto status = SaveScreenshot(diff, output_path);
319 if (!status.ok()) {
320 return status;
321 }
322
323 return output_path;
324}
325
326absl::StatusOr<bool> ScreenshotAssertion::AssertPixelColor(int x, int y,
327 uint8_t r, uint8_t g,
328 uint8_t b,
329 int tolerance) {
330 auto screenshot = CaptureScreen();
331 if (!screenshot.ok()) {
332 return screenshot.status();
333 }
334
335 if (x < 0 || x >= screenshot->width || y < 0 || y >= screenshot->height) {
336 return absl::OutOfRangeError("Pixel coordinates out of bounds");
337 }
338
339 size_t idx = screenshot->GetPixelIndex(x, y);
340 return ColorsMatch(screenshot->data[idx], screenshot->data[idx + 1],
341 screenshot->data[idx + 2], r, g, b, tolerance);
342}
343
345 const ScreenRegion& region, uint8_t r, uint8_t g, uint8_t b,
346 float min_coverage) {
347 auto screenshot = CaptureScreen();
348 if (!screenshot.ok()) {
349 return screenshot.status();
350 }
351
352 int matching = 0;
353 int total = 0;
354
355 int end_x = region.width > 0 ? region.x + region.width : screenshot->width;
356 int end_y = region.height > 0 ? region.y + region.height : screenshot->height;
357
358 for (int y = region.y; y < end_y && y < screenshot->height; ++y) {
359 for (int x = region.x; x < end_x && x < screenshot->width; ++x) {
360 total++;
361 size_t idx = screenshot->GetPixelIndex(x, y);
362 if (ColorsMatch(screenshot->data[idx], screenshot->data[idx + 1],
363 screenshot->data[idx + 2], r, g, b,
365 matching++;
366 }
367 }
368 }
369
370 float coverage = total > 0 ? static_cast<float>(matching) / total : 0.0f;
371 return coverage >= min_coverage;
372}
373
375 const ScreenRegion& region, uint8_t r, uint8_t g, uint8_t b,
376 int tolerance) {
377 auto result = AssertRegionContainsColor(region, r, g, b, 0.001f);
378 if (!result.ok()) {
379 return result.status();
380 }
381 return !*result; // Invert: true if color NOT found
382}
383
384absl::StatusOr<Screenshot> ScreenshotAssertion::CaptureScreen() {
385 if (!capture_callback_) {
386 return absl::FailedPreconditionError("Capture callback not set");
387 }
388 return capture_callback_();
389}
390
391absl::Status ScreenshotAssertion::SaveScreenshot(const Screenshot& screenshot,
392 const std::string& path) {
393 // Create directory if needed
394 std::filesystem::path filepath(path);
395 std::filesystem::create_directories(filepath.parent_path());
396
397 // TODO: Implement proper PNG encoding
398 // For now, save as raw RGBA
399 std::ofstream file(path, std::ios::binary);
400 if (!file) {
401 return absl::UnavailableError(
402 absl::StrCat("Cannot open file for writing: ", path));
403 }
404
405 // Write simple header (width, height, then RGBA data)
406 file.write(reinterpret_cast<const char*>(&screenshot.width), sizeof(int));
407 file.write(reinterpret_cast<const char*>(&screenshot.height), sizeof(int));
408 file.write(reinterpret_cast<const char*>(screenshot.data.data()),
409 screenshot.data.size());
410
411 LOG_DEBUG("ScreenshotAssertion", "Screenshot saved: %s (%dx%d)", path.c_str(),
412 screenshot.width, screenshot.height);
413
414 return absl::OkStatus();
415}
416
417absl::StatusOr<Screenshot> ScreenshotAssertion::LoadScreenshot(
418 const std::string& path) {
419 std::ifstream file(path, std::ios::binary);
420 if (!file) {
421 return absl::NotFoundError(absl::StrCat("Cannot open file: ", path));
422 }
423
424 Screenshot screenshot;
425 screenshot.source = path;
426
427 file.read(reinterpret_cast<char*>(&screenshot.width), sizeof(int));
428 file.read(reinterpret_cast<char*>(&screenshot.height), sizeof(int));
429
430 size_t data_size = screenshot.width * screenshot.height * 4;
431 screenshot.data.resize(data_size);
432 file.read(reinterpret_cast<char*>(screenshot.data.data()), data_size);
433
434 return screenshot;
435}
436
437bool ScreenshotAssertion::ColorsMatch(uint8_t r1, uint8_t g1, uint8_t b1,
438 uint8_t r2, uint8_t g2, uint8_t b2,
439 int threshold) const {
440 return std::abs(r1 - r2) <= threshold && std::abs(g1 - g2) <= threshold &&
441 std::abs(b1 - b2) <= threshold;
442}
443
445 const Screenshot& actual, const Screenshot& expected, int threshold) {
446 // TODO: Implement region clustering for difference visualization
447 return {};
448}
449
450} // namespace test
451} // 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