9#include "absl/strings/str_cat.h"
11#include "grpcpp/grpcpp.h"
12#include "protos/visual_service.grpc.pb.h"
13#include "protos/visual_service.pb.h"
20test::VisualDiffConfig ProtoToConfig(
const proto::ComparisonConfig& proto) {
21 test::VisualDiffConfig config;
22 config.tolerance = proto.tolerance() > 0 ? proto.tolerance() : 0.95f;
23 config.color_threshold =
24 proto.color_threshold() > 0 ? proto.color_threshold() : 10;
25 config.generate_diff_image = proto.generate_diff_image();
27 switch (proto.algorithm()) {
28 case proto::ComparisonConfig::SSIM:
31 case proto::ComparisonConfig::PERCEPTUAL_HASH:
39 switch (proto.diff_style()) {
40 case proto::ComparisonConfig::SIDE_BY_SIDE:
43 case proto::ComparisonConfig::HEATMAP:
52 for (
const auto& region : proto.ignore_regions()) {
53 config.ignore_regions.push_back(test::ScreenRegion{
54 region.x(), region.y(), region.width(), region.height()});
61void ResultToProto(
const test::VisualDiffResult& result,
62 proto::VisualDiffResult* proto) {
63 proto->set_identical(result.identical);
64 proto->set_passed(result.passed);
65 proto->set_similarity(result.similarity);
66 proto->set_difference_pct(result.difference_pct);
67 proto->set_width(result.width);
68 proto->set_height(result.height);
69 proto->set_total_pixels(result.total_pixels);
70 proto->set_differing_pixels(result.differing_pixels);
72 if (!result.diff_image_png.empty()) {
73 proto->set_diff_image_png(result.diff_image_png.data(),
74 result.diff_image_png.size());
77 proto->set_diff_description(result.diff_description);
79 for (
const auto& region : result.significant_regions) {
80 auto* proto_region = proto->add_significant_regions();
81 proto_region->set_x(region.x);
82 proto_region->set_y(region.y);
83 proto_region->set_width(region.width);
84 proto_region->set_height(region.height);
85 proto_region->set_local_diff_pct(region.local_diff_pct);
88 if (!result.error_message.empty()) {
89 proto->set_error(result.error_message);
99VisualServiceImpl::VisualServiceImpl()
100 : reference_dir_(
"/tmp/yaze_visual_references") {}
102VisualServiceImpl::~VisualServiceImpl() =
default;
104void VisualServiceImpl::SetReferenceImageDir(
const std::string& path) {
105 reference_dir_ = path;
106 std::filesystem::create_directories(path);
109void VisualServiceImpl::SetAIVisionVerifier(test::AIVisionVerifier* verifier) {
110 vision_verifier_ = verifier;
113test::VisualDiffEngine& VisualServiceImpl::GetDiffEngine() {
115 diff_engine_ = std::make_unique<test::VisualDiffEngine>();
117 return *diff_engine_;
120absl::Status VisualServiceImpl::ComparePngData(
121 const proto::ComparePngDataRequest* request,
122 proto::ComparePngDataResponse* response) {
123 if (request->png_a().empty() || request->png_b().empty()) {
124 return absl::InvalidArgumentError(
"Both PNG images must be provided");
127 auto& engine = GetDiffEngine();
128 if (request->has_config()) {
129 engine.SetConfig(ProtoToConfig(request->config()));
132 std::vector<uint8_t> png_a(request->png_a().begin(), request->png_a().end());
133 std::vector<uint8_t> png_b(request->png_b().begin(), request->png_b().end());
135 auto result = engine.ComparePngData(png_a, png_b);
137 return result.status();
140 ResultToProto(*result, response->mutable_result());
141 return absl::OkStatus();
144absl::Status VisualServiceImpl::ComparePngFiles(
145 const proto::ComparePngFilesRequest* request,
146 proto::ComparePngFilesResponse* response) {
147 if (request->path_a().empty() || request->path_b().empty()) {
148 return absl::InvalidArgumentError(
"Both file paths must be provided");
151 auto& engine = GetDiffEngine();
152 if (request->has_config()) {
153 engine.SetConfig(ProtoToConfig(request->config()));
156 auto result = engine.ComparePngFiles(request->path_a(), request->path_b());
158 return result.status();
161 ResultToProto(*result, response->mutable_result());
162 return absl::OkStatus();
165absl::Status VisualServiceImpl::CompareWithReference(
166 const proto::CompareWithReferenceRequest* request,
167 proto::CompareWithReferenceResponse* response) {
168 if (request->png_data().empty()) {
169 return absl::InvalidArgumentError(
"PNG data must be provided");
171 if (request->reference_path().empty()) {
172 return absl::InvalidArgumentError(
"Reference path must be provided");
175 auto& engine = GetDiffEngine();
176 if (request->has_config()) {
177 engine.SetConfig(ProtoToConfig(request->config()));
180 std::vector<uint8_t> png_data(request->png_data().begin(),
181 request->png_data().end());
184 engine.CompareWithReference(png_data, request->reference_path());
186 return result.status();
189 ResultToProto(*result, response->mutable_result());
190 return absl::OkStatus();
193absl::Status VisualServiceImpl::CompareRegion(
194 const proto::CompareRegionRequest* request,
195 proto::CompareRegionResponse* response) {
196 if (request->png_a().empty() || request->png_b().empty()) {
197 return absl::InvalidArgumentError(
"Both PNG images must be provided");
200 auto& engine = GetDiffEngine();
201 if (request->has_config()) {
202 engine.SetConfig(ProtoToConfig(request->config()));
205 std::vector<uint8_t> png_a(request->png_a().begin(), request->png_a().end());
206 std::vector<uint8_t> png_b(request->png_b().begin(), request->png_b().end());
208 test::ScreenRegion region{request->region().x(), request->region().y(),
209 request->region().width(),
210 request->region().height()};
212 auto result = engine.CompareRegion(png_a, png_b, region);
214 return result.status();
217 ResultToProto(*result, response->mutable_result());
218 return absl::OkStatus();
221absl::Status VisualServiceImpl::GenerateDiffImage(
222 const proto::GenerateDiffImageRequest* request,
223 proto::GenerateDiffImageResponse* response) {
224 if (request->png_a().empty() || request->png_b().empty()) {
225 return absl::InvalidArgumentError(
"Both PNG images must be provided");
228 test::VisualDiffConfig config;
229 config.generate_diff_image =
true;
231 switch (request->style()) {
232 case proto::ComparisonConfig::SIDE_BY_SIDE:
233 config.diff_style = test::VisualDiffConfig::DiffStyle::kSideBySide;
235 case proto::ComparisonConfig::HEATMAP:
236 config.diff_style = test::VisualDiffConfig::DiffStyle::kHeatmap;
239 config.diff_style = test::VisualDiffConfig::DiffStyle::kRedHighlight;
243 test::VisualDiffEngine engine(config);
246 auto screenshot_a = test::VisualDiffEngine::DecodePng(
247 std::vector<uint8_t>(request->png_a().begin(), request->png_a().end()));
248 if (!screenshot_a.ok()) {
249 response->set_success(
false);
250 response->set_error(std::string(screenshot_a.status().message()));
251 return absl::OkStatus();
254 auto screenshot_b = test::VisualDiffEngine::DecodePng(
255 std::vector<uint8_t>(request->png_b().begin(), request->png_b().end()));
256 if (!screenshot_b.ok()) {
257 response->set_success(
false);
258 response->set_error(std::string(screenshot_b.status().message()));
259 return absl::OkStatus();
263 auto diff_result = engine.GenerateDiffPng(*screenshot_a, *screenshot_b);
264 if (!diff_result.ok()) {
265 response->set_success(
false);
266 response->set_error(std::string(diff_result.status().message()));
267 return absl::OkStatus();
270 response->set_success(
true);
271 response->set_diff_image_png(diff_result->data(), diff_result->size());
272 return absl::OkStatus();
275absl::Status VisualServiceImpl::RunRegressionTest(
276 const proto::RunRegressionTestRequest* request,
277 proto::RunRegressionTestResponse* response) {
278 if (request->current_screenshot().empty()) {
279 return absl::InvalidArgumentError(
"Current screenshot must be provided");
281 if (request->reference_id().empty()) {
282 return absl::InvalidArgumentError(
"Reference ID must be provided");
286 std::string reference_path =
287 absl::StrCat(reference_dir_,
"/", request->reference_id(),
".png");
289 if (!std::filesystem::exists(reference_path)) {
290 return absl::NotFoundError(
291 absl::StrCat(
"Reference image not found: ", request->reference_id()));
294 auto& engine = GetDiffEngine();
295 if (request->has_config()) {
296 engine.SetConfig(ProtoToConfig(request->config()));
299 std::vector<uint8_t> current(request->current_screenshot().begin(),
300 request->current_screenshot().end());
302 auto result = engine.CompareWithReference(current, reference_path);
304 return result.status();
307 response->set_passed(result->passed);
308 ResultToProto(*result, response->mutable_result());
309 response->set_test_name(request->test_name());
310 response->set_reference_id(request->reference_id());
311 response->set_timestamp_ms(
312 std::chrono::duration_cast<std::chrono::milliseconds>(
313 std::chrono::system_clock::now().time_since_epoch())
316 return absl::OkStatus();
319absl::Status VisualServiceImpl::ListReferenceImages(
320 const proto::ListReferenceImagesRequest* request,
321 proto::ListReferenceImagesResponse* response) {
322 if (!std::filesystem::exists(reference_dir_)) {
323 return absl::OkStatus();
326 for (
const auto& entry :
327 std::filesystem::directory_iterator(reference_dir_)) {
328 if (entry.path().extension() ==
".png") {
329 auto* img = response->add_images();
330 img->set_id(entry.path().stem().string());
331 img->set_path(entry.path().string());
334 if (!request->category().empty()) {
338 if (!request->prefix().empty()) {
339 if (img->id().find(request->prefix()) != 0) {
340 response->mutable_images()->RemoveLast();
346 auto screenshot = test::VisualDiffEngine::LoadPng(entry.path().string());
347 if (screenshot.ok()) {
348 img->set_width(screenshot->width);
349 img->set_height(screenshot->height);
354 auto ftime = std::filesystem::last_write_time(entry);
355 auto file_duration = ftime.time_since_epoch();
357 std::chrono::duration_cast<std::chrono::milliseconds>(file_duration);
358 img->set_created_timestamp_ms(ms.count());
362 return absl::OkStatus();
365absl::Status VisualServiceImpl::SaveReferenceImage(
366 const proto::SaveReferenceImageRequest* request,
367 proto::SaveReferenceImageResponse* response) {
368 if (request->png_data().empty()) {
369 return absl::InvalidArgumentError(
"PNG data must be provided");
371 if (request->reference_id().empty()) {
372 return absl::InvalidArgumentError(
"Reference ID must be provided");
375 std::filesystem::create_directories(reference_dir_);
378 absl::StrCat(reference_dir_,
"/", request->reference_id(),
".png");
380 std::ofstream file(path, std::ios::binary);
382 response->set_success(
false);
384 absl::StrCat(
"Failed to open file for writing: ", path));
385 return absl::OkStatus();
388 file.write(request->png_data().data(), request->png_data().size());
390 response->set_success(
true);
391 response->set_path(path);
392 return absl::OkStatus();
395absl::Status VisualServiceImpl::AnalyzeScreenshot(
396 const proto::AnalyzeScreenshotRequest* request,
397 proto::AnalyzeScreenshotResponse* response) {
398 if (!vision_verifier_) {
399 response->set_success(
false);
400 response->set_error(
"AI vision verifier not configured");
401 return absl::OkStatus();
407 response->set_success(
false);
408 response->set_error(
"AI analysis not yet implemented");
409 return absl::OkStatus();
412absl::Status VisualServiceImpl::VerifyVisualCondition(
413 const proto::VerifyVisualConditionRequest* request,
414 proto::VerifyVisualConditionResponse* response) {
415 if (!vision_verifier_) {
416 response->set_success(
false);
417 response->set_error(
"AI vision verifier not configured");
418 return absl::OkStatus();
424 response->set_success(
false);
425 response->set_error(
"Visual condition verification not yet implemented");
426 return absl::OkStatus();
433class VisualServiceGrpc final :
public proto::VisualService::Service {
435 explicit VisualServiceGrpc(VisualServiceImpl* impl) : impl_(impl) {}
437#define IMPL_RPC(method) \
438 grpc::Status method(grpc::ServerContext* context, \
439 const proto::method##Request* request, \
440 proto::method##Response* response) override { \
442 auto status = impl_->method(request, response); \
443 if (!status.ok()) { \
444 return grpc::Status(grpc::StatusCode::INTERNAL, \
445 std::string(status.message())); \
447 return grpc::Status::OK; \
450 IMPL_RPC(ComparePngData)
451 IMPL_RPC(ComparePngFiles)
452 IMPL_RPC(CompareWithReference)
453 IMPL_RPC(CompareRegion)
454 IMPL_RPC(GenerateDiffImage)
455 IMPL_RPC(RunRegressionTest)
456 IMPL_RPC(ListReferenceImages)
457 IMPL_RPC(SaveReferenceImage)
458 IMPL_RPC(AnalyzeScreenshot)
459 IMPL_RPC(VerifyVisualCondition)
464 VisualServiceImpl* impl_;
467std::unique_ptr<grpc::Service> CreateVisualServiceGrpc(
468 VisualServiceImpl* impl) {
469 return std::make_unique<VisualServiceGrpc>(impl);