ScriptReporter.cpp 48 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109
  1. /*
  2. * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
  3. * its licensors.
  4. *
  5. * For complete copyright and license terms please see the LICENSE at the root of this
  6. * distribution (the "License"). All use of this software is governed by the License,
  7. * or, if provided, by the license below or the license accompanying this file. Do not
  8. * remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
  9. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. *
  11. */
  12. #include <Automation/ScriptReporter.h>
  13. #include <Utils/Utils.h>
  14. #include <Atom/Utils/PpmFile.h>
  15. #include <imgui/imgui.h>
  16. #include <Atom/RHI/Factory.h>
  17. #include <AzFramework/API/ApplicationAPI.h>
  18. #include <AzFramework/StringFunc/StringFunc.h>
  19. #include <AzFramework/IO/LocalFileIO.h>
  20. #include <AzCore/IO/SystemFile.h>
  21. #include <AzCore/Utils/Utils.h>
  22. namespace AtomSampleViewer
  23. {
  24. // Must match ScriptReporter::DisplayOption Enum
  25. static const char* DiplayOptions[] =
  26. {
  27. "All Results", "Warnings & Errors", "Errors Only",
  28. };
  29. namespace ScreenshotPaths
  30. {
  31. AZStd::string GetScreenshotsFolder(bool resolvePath)
  32. {
  33. AZStd::string path = "@user@/scripts/screenshots/";
  34. if (resolvePath)
  35. {
  36. path = Utils::ResolvePath(path);
  37. }
  38. return path;
  39. }
  40. AZStd::string GetLocalBaselineFolder(bool resolvePath)
  41. {
  42. AZStd::string path = AZStd::string::format("@user@/scripts/screenshotslocalbaseline/%s", AZ::RHI::Factory::Get().GetName().GetCStr());
  43. if (resolvePath)
  44. {
  45. path = Utils::ResolvePath(path);
  46. }
  47. return path;
  48. }
  49. AZStd::string GetOfficialBaselineFolder(bool resolvePath)
  50. {
  51. AZStd::string path = "scripts/expectedscreenshots/";
  52. if (resolvePath)
  53. {
  54. path = Utils::ResolvePath(path);
  55. }
  56. return path;
  57. }
  58. AZStd::string GetLocalBaseline(const AZStd::string& forScreenshotFile)
  59. {
  60. AZStd::string localBaselineFolder = GetLocalBaselineFolder(false);
  61. AzFramework::StringFunc::Replace(localBaselineFolder, "@user@/", "");
  62. AZStd::string newPath = forScreenshotFile;
  63. if (!AzFramework::StringFunc::Replace(newPath, "scripts/screenshots", localBaselineFolder.c_str()))
  64. {
  65. newPath = "";
  66. }
  67. return newPath;
  68. }
  69. AZStd::string GetOfficialBaseline(const AZStd::string& forScreenshotFile)
  70. {
  71. AZStd::string path = forScreenshotFile;
  72. const AZStd::string userPath = Utils::ResolvePath("@user@");
  73. // make the path relative to the user folder
  74. if (!AzFramework::StringFunc::Replace(path, userPath.c_str(), ""))
  75. {
  76. return "";
  77. }
  78. // After replacing "screenshots" with "expectedscreenshots", the path should be a valid asset path, relative to asset root.
  79. if (!AzFramework::StringFunc::Replace(path, "scripts/screenshots", "scripts/expectedscreenshots"))
  80. {
  81. return "";
  82. }
  83. // Turn it back into a full path
  84. path = Utils::ResolvePath("@assets@" + path);
  85. return path;
  86. }
  87. }
  88. AZStd::string ScriptReporter::ImageComparisonResult::GetSummaryString() const
  89. {
  90. AZStd::string resultString;
  91. if (m_resultCode == ResultCode::ThresholdExceeded || m_resultCode == ResultCode::Pass)
  92. {
  93. resultString = AZStd::string::format("Diff Score: %f", m_finalDiffScore);
  94. }
  95. else if (m_resultCode == ResultCode::WrongSize)
  96. {
  97. resultString = "Wrong size";
  98. }
  99. else if (m_resultCode == ResultCode::FileNotFound)
  100. {
  101. resultString = "File not found";
  102. }
  103. else if (m_resultCode == ResultCode::FileNotLoaded)
  104. {
  105. resultString = "File load failed";
  106. }
  107. else if (m_resultCode == ResultCode::WrongFormat)
  108. {
  109. resultString = "Format is not supported";
  110. }
  111. else if (m_resultCode == ResultCode::NullImageComparisonToleranceLevel)
  112. {
  113. resultString = "ImageComparisonToleranceLevel not provided";
  114. }
  115. else if (m_resultCode == ResultCode::None)
  116. {
  117. // "None" could be the case if the results dialog is open while the script is running
  118. resultString = "No results";
  119. }
  120. else
  121. {
  122. resultString = "Unhandled Image Comparison ResultCode";
  123. AZ_Assert(false, "Unhandled Image Comparison ResultCode");
  124. }
  125. return resultString;
  126. }
  127. void ScriptReporter::SetAvailableToleranceLevels(const AZStd::vector<ImageComparisonToleranceLevel>& toleranceLevels)
  128. {
  129. m_availableToleranceLevels = toleranceLevels;
  130. }
  131. void ScriptReporter::Reset()
  132. {
  133. m_scriptReports.clear();
  134. m_currentScriptIndexStack.clear();
  135. m_invalidationMessage.clear();
  136. }
  137. void ScriptReporter::SetInvalidationMessage(const AZStd::string& message)
  138. {
  139. m_invalidationMessage = message;
  140. // Reporting this message here instead of when running the script so it won't show up as an error in the ImGui report.
  141. AZ_Error("Automation", m_invalidationMessage.empty(), "Subsequent test results will be invalid because '%s'", m_invalidationMessage.c_str());
  142. }
  143. void ScriptReporter::PushScript(const AZStd::string& scriptAssetPath)
  144. {
  145. if (GetCurrentScriptReport())
  146. {
  147. // Only the current script should listen for Trace Errors
  148. GetCurrentScriptReport()->BusDisconnect();
  149. }
  150. m_currentScriptIndexStack.push_back(m_scriptReports.size());
  151. m_scriptReports.push_back();
  152. m_scriptReports.back().m_scriptAssetPath = scriptAssetPath;
  153. m_scriptReports.back().BusConnect();
  154. }
  155. void ScriptReporter::PopScript()
  156. {
  157. AZ_Assert(GetCurrentScriptReport(), "There is no active script");
  158. if (GetCurrentScriptReport())
  159. {
  160. GetCurrentScriptReport()->BusDisconnect();
  161. m_currentScriptIndexStack.pop_back();
  162. }
  163. if (GetCurrentScriptReport())
  164. {
  165. // Make sure the newly restored current script is listening for Trace Errors
  166. GetCurrentScriptReport()->BusConnect();
  167. }
  168. }
  169. bool ScriptReporter::HasActiveScript() const
  170. {
  171. return !m_currentScriptIndexStack.empty();
  172. }
  173. bool ScriptReporter::AddScreenshotTest(const AZStd::string& path)
  174. {
  175. AZ_Assert(GetCurrentScriptReport(), "There is no active script");
  176. ScreenshotTestInfo screenshotTestInfo;
  177. screenshotTestInfo.m_screenshotFilePath = path;
  178. GetCurrentScriptReport()->m_screenshotTests.push_back(AZStd::move(screenshotTestInfo));
  179. return true;
  180. }
  181. void ScriptReporter::TickImGui()
  182. {
  183. if (m_showReportDialog)
  184. {
  185. ShowReportDialog();
  186. }
  187. }
  188. bool ScriptReporter::HasErrorsAssertsInReport() const
  189. {
  190. for (const ScriptReport& scriptReport : m_scriptReports)
  191. {
  192. if (scriptReport.m_assertCount > 0 || scriptReport.m_generalErrorCount > 0 || scriptReport.m_screenshotErrorCount > 0)
  193. {
  194. return true;
  195. }
  196. }
  197. return false;
  198. }
  199. void ScriptReporter::ShowDiffButton(const char* buttonLabel, const AZStd::string& imagePathA, const AZStd::string& imagePathB)
  200. {
  201. if (ImGui::Button(buttonLabel))
  202. {
  203. if (!Utils::RunDiffTool(imagePathA, imagePathB))
  204. {
  205. m_messageBox.OpenPopupMessage("Can't Diff", "Image diff is not supported on this platform, or the required diff tool is not installed.");
  206. }
  207. }
  208. }
  209. const ImageComparisonToleranceLevel* ScriptReporter::FindBestToleranceLevel(float diffScore, bool filterImperceptibleDiffs) const
  210. {
  211. float thresholdChecked = 0.0f;
  212. bool ignoringMinorDiffs = false;
  213. for (const ImageComparisonToleranceLevel& level : m_availableToleranceLevels)
  214. {
  215. AZ_Assert(level.m_threshold > thresholdChecked || thresholdChecked == 0.0f, "Threshold values are not sequential");
  216. AZ_Assert(level.m_filterImperceptibleDiffs >= ignoringMinorDiffs, "filterImperceptibleDiffs values are not sequential");
  217. thresholdChecked = level.m_threshold;
  218. ignoringMinorDiffs = level.m_filterImperceptibleDiffs;
  219. if (filterImperceptibleDiffs <= level.m_filterImperceptibleDiffs && diffScore <= level.m_threshold)
  220. {
  221. return &level;
  222. }
  223. }
  224. return nullptr;
  225. }
  226. void ScriptReporter::ShowReportDialog()
  227. {
  228. if (ImGui::Begin("Script Results", &m_showReportDialog) && !m_scriptReports.empty())
  229. {
  230. const ImVec4& bgColor = ImGui::GetStyleColorVec4(ImGuiCol_WindowBg);
  231. const bool isDarkStyle = bgColor.x < 0.2 && bgColor.y < 0.2 && bgColor.z < 0.2;
  232. const ImVec4 HighlightPassed = isDarkStyle ? ImVec4{0.5, 1, 0.5, 1} : ImVec4{0, 0.75, 0, 1};
  233. const ImVec4 HighlightFailed = isDarkStyle ? ImVec4{1, 0.5, 0.5, 1} : ImVec4{0.75, 0, 0, 1};
  234. const ImVec4 HighlightWarning = isDarkStyle ? ImVec4{1, 1, 0.5, 1} : ImVec4{0.5, 0.5, 0, 1};
  235. // Local utilities for setting text color
  236. bool colorHasBeenSet = false;
  237. auto highlightTextIf = [&colorHasBeenSet](bool shouldSet, ImVec4 color)
  238. {
  239. if (colorHasBeenSet)
  240. {
  241. ImGui::PopStyleColor();
  242. colorHasBeenSet = false;
  243. }
  244. if (shouldSet)
  245. {
  246. ImGui::PushStyleColor(ImGuiCol_Text, color);
  247. colorHasBeenSet = true;
  248. }
  249. };
  250. auto highlightTextFailedOrWarning = [&](bool isFailed, bool isWarning)
  251. {
  252. if (colorHasBeenSet)
  253. {
  254. ImGui::PopStyleColor();
  255. colorHasBeenSet = false;
  256. }
  257. if (isFailed)
  258. {
  259. ImGui::PushStyleColor(ImGuiCol_Text, HighlightFailed);
  260. colorHasBeenSet = true;
  261. }
  262. else if (isWarning)
  263. {
  264. ImGui::PushStyleColor(ImGuiCol_Text, HighlightWarning);
  265. colorHasBeenSet = true;
  266. }
  267. };
  268. auto resetTextHighlight = [&colorHasBeenSet]()
  269. {
  270. if (colorHasBeenSet)
  271. {
  272. ImGui::PopStyleColor();
  273. colorHasBeenSet = false;
  274. }
  275. };
  276. auto seeConsole = [](uint32_t issueCount, const char* searchString)
  277. {
  278. if (issueCount == 0)
  279. {
  280. return AZStd::string{};
  281. }
  282. else
  283. {
  284. return AZStd::string::format("(See \"%s\" messages in console output)", searchString);
  285. }
  286. };
  287. auto seeBelow = [](uint32_t issueCount)
  288. {
  289. if (issueCount == 0)
  290. {
  291. return AZStd::string{};
  292. }
  293. else
  294. {
  295. return AZStd::string::format("(See below)");
  296. }
  297. };
  298. uint32_t totalAsserts = 0;
  299. uint32_t totalErrors = 0;
  300. uint32_t totalWarnings = 0;
  301. uint32_t totalScreenshotsCount = 0;
  302. uint32_t totalScreenshotsFailed = 0;
  303. uint32_t totalScreenshotWarnings = 0;
  304. for (ScriptReport& scriptReport : m_scriptReports)
  305. {
  306. totalAsserts += scriptReport.m_assertCount;
  307. // We don't include screenshot errors and warnings in these totals because those have their own line-items.
  308. totalErrors += scriptReport.m_generalErrorCount;
  309. totalWarnings += scriptReport.m_generalWarningCount;
  310. totalScreenshotWarnings += scriptReport.m_screenshotWarningCount;
  311. totalScreenshotsFailed += scriptReport.m_screenshotErrorCount;
  312. // This will catch any false-negatives that could occur if the screenshot failure error messages change without also updating ScriptReport::OnPreError()
  313. for (ScreenshotTestInfo& screenshotTest : scriptReport.m_screenshotTests)
  314. {
  315. if (screenshotTest.m_officialComparisonResult.m_resultCode != ImageComparisonResult::ResultCode::Pass &&
  316. screenshotTest.m_officialComparisonResult.m_resultCode != ImageComparisonResult::ResultCode::None)
  317. {
  318. AZ_Assert(scriptReport.m_screenshotErrorCount > 0, "If screenshot comparison failed in any way, m_screenshotErrorCount should be non-zero.");
  319. }
  320. }
  321. }
  322. ImGui::Separator();
  323. if (HasActiveScript())
  324. {
  325. ImGui::PushStyleColor(ImGuiCol_Text, HighlightWarning);
  326. ImGui::Text("Script is running... (_ _)zzz");
  327. ImGui::PopStyleColor();
  328. }
  329. else if (totalErrors > 0 || totalAsserts > 0 || totalScreenshotsFailed > 0)
  330. {
  331. ImGui::PushStyleColor(ImGuiCol_Text, HighlightFailed);
  332. ImGui::Text("(>_<) FAILED (>_<)");
  333. ImGui::PopStyleColor();
  334. }
  335. else
  336. {
  337. if (m_invalidationMessage.empty())
  338. {
  339. ImGui::PushStyleColor(ImGuiCol_Text, HighlightPassed);
  340. ImGui::Text("\\(^_^)/ PASSED \\(^_^)/");
  341. ImGui::PopStyleColor();
  342. }
  343. else
  344. {
  345. ImGui::Text("(-_-) INVALID ... but passed (-_-)");
  346. }
  347. }
  348. if (!m_invalidationMessage.empty())
  349. {
  350. ImGui::Separator();
  351. ImGui::PushStyleColor(ImGuiCol_Text, HighlightFailed);
  352. ImGui::Text("(%s)", m_invalidationMessage.c_str());
  353. ImGui::PopStyleColor();
  354. }
  355. ImGui::Separator();
  356. ImGui::Text("Test Script Count: %zu", m_scriptReports.size());
  357. highlightTextIf(totalAsserts > 0, HighlightFailed);
  358. ImGui::Text("Total Asserts: %u %s", totalAsserts, seeConsole(totalAsserts, "Trace::Assert").c_str());
  359. highlightTextIf(totalErrors > 0, HighlightFailed);
  360. ImGui::Text("Total Errors: %u %s", totalErrors, seeConsole(totalErrors, "Trace::Error").c_str());
  361. highlightTextIf(totalWarnings > 0, HighlightWarning);
  362. ImGui::Text("Total Warnings: %u %s", totalWarnings, seeConsole(totalWarnings, "Trace::Warning").c_str());
  363. resetTextHighlight();
  364. ImGui::Text("Total Screenshot Count: %u", totalScreenshotsCount);
  365. highlightTextIf(totalScreenshotsFailed > 0, HighlightFailed);
  366. ImGui::Text("Total Screenshot Failures: %u %s", totalScreenshotsFailed, seeBelow(totalScreenshotsFailed).c_str());
  367. highlightTextIf(totalScreenshotWarnings > 0, HighlightWarning);
  368. ImGui::Text("Total Screenshot Warnings: %u %s", totalScreenshotWarnings, seeBelow(totalScreenshotWarnings).c_str());
  369. resetTextHighlight();
  370. if (ImGui::Button("Update All Local Baseline Images"))
  371. {
  372. m_messageBox.OpenPopupConfirmation(
  373. "Update All Local Baseline Images",
  374. "This will replace all local baseline images \n"
  375. "with the images captured during this test run. \n"
  376. "Are you sure?",
  377. [this]() {
  378. UpdateAllLocalBaselineImages();
  379. });
  380. }
  381. int displayOption = m_displayOption;
  382. ImGui::Combo("Display", &displayOption, DiplayOptions, AZ_ARRAY_SIZE(DiplayOptions));
  383. m_displayOption = (DisplayOption)displayOption;
  384. bool showWarnings = (m_displayOption == DisplayOption::AllResults) || (m_displayOption == DisplayOption::WarningsAndErrors);
  385. bool showAll = (m_displayOption == DisplayOption::AllResults);
  386. ImGui::Separator();
  387. const ImGuiTreeNodeFlags FlagDefaultOpen = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick | ImGuiTreeNodeFlags_DefaultOpen;
  388. const ImGuiTreeNodeFlags FlagDefaultClosed = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick;
  389. for (ScriptReport& scriptReport : m_scriptReports)
  390. {
  391. const bool scriptPassed = scriptReport.m_assertCount == 0 && scriptReport.m_generalErrorCount == 0 && scriptReport.m_screenshotErrorCount == 0;
  392. const bool scriptHasWarnings = scriptReport.m_generalWarningCount > 0 || scriptReport.m_screenshotWarningCount > 0;
  393. // Skip if tests passed without warnings and we don't want to show successes
  394. bool skipReport = (scriptPassed && !scriptHasWarnings && !showAll);
  395. // Skip if we only have warnings only and we don't want to show warnings
  396. skipReport = skipReport || (scriptPassed && scriptHasWarnings && !showWarnings);
  397. if (skipReport)
  398. {
  399. continue;
  400. }
  401. ImGuiTreeNodeFlags scriptNodeFlag = scriptPassed ? FlagDefaultClosed : FlagDefaultOpen;
  402. AZStd::string header = AZStd::string::format("%s %s",
  403. scriptPassed ? "PASSED" : "FAILED",
  404. scriptReport.m_scriptAssetPath.c_str()
  405. );
  406. highlightTextFailedOrWarning(!scriptPassed, scriptHasWarnings);
  407. if (ImGui::TreeNodeEx(&scriptReport, scriptNodeFlag, "%s", header.c_str()))
  408. {
  409. resetTextHighlight();
  410. // Number of Asserts
  411. highlightTextIf(scriptReport.m_assertCount > 0, HighlightFailed);
  412. if (showAll || scriptReport.m_assertCount > 0)
  413. {
  414. ImGui::Text("Asserts: %u %s", scriptReport.m_assertCount, seeConsole(scriptReport.m_assertCount, "Trace::Assert").c_str());
  415. }
  416. // Number of Errors
  417. highlightTextIf(scriptReport.m_generalErrorCount > 0, HighlightFailed);
  418. if (showAll || scriptReport.m_generalErrorCount > 0)
  419. {
  420. ImGui::Text("Errors: %u %s", scriptReport.m_generalErrorCount, seeConsole(scriptReport.m_generalErrorCount, "Trace::Error").c_str());
  421. }
  422. // Number of Warnings
  423. highlightTextIf(scriptReport.m_generalWarningCount > 0, HighlightWarning);
  424. if (showAll || (showWarnings && scriptReport.m_generalWarningCount > 0))
  425. {
  426. ImGui::Text("Warnings: %u %s", scriptReport.m_generalWarningCount, seeConsole(scriptReport.m_generalWarningCount, "Trace::Warning").c_str());
  427. }
  428. resetTextHighlight();
  429. // Number of screenshots
  430. if (showAll || scriptReport.m_screenshotErrorCount > 0 || (showWarnings && scriptReport.m_screenshotWarningCount > 0))
  431. {
  432. ImGui::Text("Screenshot Test Count: %zu", scriptReport.m_screenshotTests.size());
  433. }
  434. // Number of screenshot failures
  435. highlightTextIf(scriptReport.m_screenshotErrorCount > 0, HighlightFailed);
  436. if (showAll || scriptReport.m_screenshotErrorCount > 0)
  437. {
  438. ImGui::Text("Screenshot Tests Failed: %u %s", scriptReport.m_screenshotErrorCount, seeBelow(scriptReport.m_screenshotErrorCount).c_str());
  439. }
  440. // Number of screenshot warnings
  441. highlightTextIf(scriptReport.m_screenshotWarningCount > 0, HighlightWarning);
  442. if (showAll || (showWarnings && scriptReport.m_screenshotWarningCount > 0))
  443. {
  444. ImGui::Text("Screenshot Warnings: %u %s", scriptReport.m_screenshotWarningCount, seeBelow(scriptReport.m_screenshotWarningCount).c_str());
  445. }
  446. resetTextHighlight();
  447. for (ScreenshotTestInfo& screenshotResult : scriptReport.m_screenshotTests)
  448. {
  449. const bool screenshotPassed = screenshotResult.m_officialComparisonResult.m_resultCode == ImageComparisonResult::ResultCode::Pass;
  450. const bool localBaselineWarning = screenshotResult.m_localComparisonResult.m_resultCode != ImageComparisonResult::ResultCode::Pass;
  451. // Skip if tests passed without warnings and we don't want to show successes
  452. bool skipScreenshot = (screenshotPassed && !localBaselineWarning && !showAll);
  453. // Skip if we only have warnings only and we don't want to show warnings
  454. skipScreenshot = skipScreenshot || (screenshotPassed && localBaselineWarning && !showWarnings);
  455. if (skipScreenshot)
  456. {
  457. continue;
  458. }
  459. AZStd::string fileName;
  460. AzFramework::StringFunc::Path::GetFullFileName(screenshotResult.m_screenshotFilePath.c_str(), fileName);
  461. AZStd::string headerSummary;
  462. if (!screenshotPassed)
  463. {
  464. headerSummary = "(" + screenshotResult.m_officialComparisonResult.GetSummaryString() + ") ";
  465. }
  466. if (localBaselineWarning)
  467. {
  468. headerSummary += "(Local Baseline Warning)";
  469. }
  470. ImGuiTreeNodeFlags screenshotNodeFlag = FlagDefaultClosed;
  471. AZStd::string screenshotHeader = AZStd::string::format("%s %s %s", screenshotPassed ? "PASSED" : "FAILED", fileName.c_str(), headerSummary.c_str());
  472. highlightTextFailedOrWarning(!screenshotPassed, localBaselineWarning);
  473. if (ImGui::TreeNodeEx(&screenshotResult, screenshotNodeFlag, "%s", screenshotHeader.c_str()))
  474. {
  475. resetTextHighlight();
  476. ImGui::Text(("Screenshot: " + screenshotResult.m_screenshotFilePath).c_str());
  477. ImGui::Spacing();
  478. highlightTextIf(!screenshotPassed, HighlightFailed);
  479. ImGui::Text(("Official Baseline: " + screenshotResult.m_officialBaselineScreenshotFilePath).c_str());
  480. // Official Baseline Result
  481. ImGui::Indent();
  482. {
  483. ImGui::Text(screenshotResult.m_officialComparisonResult.GetSummaryString().c_str());
  484. if (screenshotResult.m_officialComparisonResult.m_resultCode == ImageComparisonResult::ResultCode::ThresholdExceeded ||
  485. screenshotResult.m_officialComparisonResult.m_resultCode == ImageComparisonResult::ResultCode::Pass)
  486. {
  487. ImGui::Text("Used Tolerance: %s", screenshotResult.m_toleranceLevel.ToString().c_str());
  488. const ImageComparisonToleranceLevel* suggestedTolerance = ScriptReporter::FindBestToleranceLevel(
  489. screenshotResult.m_officialComparisonResult.m_finalDiffScore,
  490. screenshotResult.m_toleranceLevel.m_filterImperceptibleDiffs);
  491. if(suggestedTolerance)
  492. {
  493. ImGui::Text("Suggested Tolerance: %s", suggestedTolerance->ToString().c_str());
  494. }
  495. if (screenshotResult.m_toleranceLevel.m_filterImperceptibleDiffs)
  496. {
  497. // This gives an idea of what the tolerance level would be if the imperceptible diffs were not filtered out.
  498. const ImageComparisonToleranceLevel* unfilteredTolerance = ScriptReporter::FindBestToleranceLevel(
  499. screenshotResult.m_officialComparisonResult.m_standardDiffScore, false);
  500. ImGui::Text("(Unfiltered Diff Score: %f%s)",
  501. screenshotResult.m_officialComparisonResult.m_standardDiffScore,
  502. unfilteredTolerance ? AZStd::string::format(" ~ '%s'", unfilteredTolerance->m_name.c_str()).c_str() : "");
  503. }
  504. }
  505. resetTextHighlight();
  506. ImGui::PushID("Official");
  507. ShowDiffButton("View Diff", screenshotResult.m_officialBaselineScreenshotFilePath, screenshotResult.m_screenshotFilePath);
  508. ImGui::PopID();
  509. if (!screenshotPassed && ImGui::Button("Update"))
  510. {
  511. if (screenshotResult.m_localComparisonResult.m_resultCode == ImageComparisonResult::ResultCode::FileNotFound)
  512. {
  513. UpdateSourceBaselineImage(screenshotResult, true);
  514. }
  515. else
  516. {
  517. m_messageBox.OpenPopupConfirmation(
  518. "Update Official Baseline Image",
  519. "This will replace the official baseline image \n"
  520. "with the image captured during this test run. \n"
  521. "Are you sure?",
  522. // It's important to bind screenshotResult by reference because UpdateOfficialBaselineImage will update it
  523. [this, &screenshotResult]() {
  524. UpdateSourceBaselineImage(screenshotResult, true);
  525. });
  526. }
  527. }
  528. }
  529. ImGui::Unindent();
  530. ImGui::Spacing();
  531. highlightTextIf(localBaselineWarning, HighlightWarning);
  532. ImGui::Text(("Local Baseline: " + screenshotResult.m_localBaselineScreenshotFilePath).c_str());
  533. // Local Baseline Result
  534. ImGui::Indent();
  535. {
  536. ImGui::Text(screenshotResult.m_localComparisonResult.GetSummaryString().c_str());
  537. resetTextHighlight();
  538. ImGui::PushID("Local");
  539. ShowDiffButton("View Diff", screenshotResult.m_localBaselineScreenshotFilePath, screenshotResult.m_screenshotFilePath);
  540. ImGui::PopID();
  541. if (localBaselineWarning && ImGui::Button("Update"))
  542. {
  543. if (screenshotResult.m_localComparisonResult.m_resultCode == ImageComparisonResult::ResultCode::FileNotFound)
  544. {
  545. UpdateLocalBaselineImage(screenshotResult, true);
  546. }
  547. else
  548. {
  549. m_messageBox.OpenPopupConfirmation(
  550. "Update Local Baseline Image",
  551. "This will replace the local baseline image \n"
  552. "with the image captured during this test run. \n"
  553. "Are you sure?",
  554. // It's important to bind screenshotResult by reference because UpdateLocalBaselineImage will update it
  555. [this, &screenshotResult]() {
  556. UpdateLocalBaselineImage(screenshotResult, true);
  557. });
  558. }
  559. }
  560. }
  561. ImGui::Unindent();
  562. ImGui::Spacing();
  563. resetTextHighlight();
  564. ImGui::TreePop();
  565. }
  566. }
  567. ImGui::TreePop();
  568. }
  569. resetTextHighlight();
  570. }
  571. resetTextHighlight();
  572. // Repeat the m_invalidationMessage at the bottom as well, to make sure the user doesn't miss it.
  573. if (!m_invalidationMessage.empty())
  574. {
  575. ImGui::Separator();
  576. ImGui::PushStyleColor(ImGuiCol_Text, HighlightFailed);
  577. ImGui::Text("(%s)", m_invalidationMessage.c_str());
  578. ImGui::PopStyleColor();
  579. }
  580. }
  581. m_messageBox.TickPopup();
  582. ImGui::End();
  583. }
  584. void ScriptReporter::OpenReportDialog()
  585. {
  586. m_showReportDialog = true;
  587. }
  588. ScriptReporter::ScriptReport* ScriptReporter::GetCurrentScriptReport()
  589. {
  590. if (!m_currentScriptIndexStack.empty())
  591. {
  592. return &m_scriptReports[m_currentScriptIndexStack.back()];
  593. }
  594. else
  595. {
  596. return nullptr;
  597. }
  598. }
  599. void ScriptReporter::ReportScriptError([[maybe_unused]] const AZStd::string& message)
  600. {
  601. AZ_Error("Automation", false, "Script: %s", message.c_str());
  602. }
  603. void ScriptReporter::ReportScriptWarning([[maybe_unused]] const AZStd::string& message)
  604. {
  605. AZ_Warning("Automation", false, "Script: %s", message.c_str());
  606. }
  607. void ScriptReporter::ReportScriptIssue(const AZStd::string& message, TraceLevel traceLevel)
  608. {
  609. switch (traceLevel)
  610. {
  611. case TraceLevel::Error:
  612. ReportScriptError(message);
  613. break;
  614. case TraceLevel::Warning:
  615. ReportScriptWarning(message);
  616. break;
  617. default:
  618. AZ_Assert(false, "Unhandled TraceLevel");
  619. }
  620. }
  621. void ScriptReporter::ReportScreenshotComparisonIssue(const AZStd::string& message, const AZStd::string& expectedImageFilePath, const AZStd::string& actualImageFilePath, TraceLevel traceLevel)
  622. {
  623. AZStd::string fullMessage = AZStd::string::format("%s\n Expected: '%s'\n Actual: '%s'",
  624. message.c_str(),
  625. expectedImageFilePath.c_str(),
  626. actualImageFilePath.c_str());
  627. ReportScriptIssue(fullMessage, traceLevel);
  628. }
  629. bool ScriptReporter::LoadPpmData(ImageComparisonResult& imageComparisonResult, const AZStd::string& path, AZStd::vector<uint8_t>& buffer, AZ::RHI::Size& size, AZ::RHI::Format& format, TraceLevel traceLevel)
  630. {
  631. const size_t maxFileSize = 1024 * 1024 * 25;
  632. auto readScreenshotFileResult = AZ::Utils::ReadFile<AZStd::vector<uint8_t>>(path, maxFileSize);
  633. if (!readScreenshotFileResult.IsSuccess())
  634. {
  635. ReportScriptIssue(AZStd::string::format("Screenshot check failed. %s", readScreenshotFileResult.GetError().c_str()), traceLevel);
  636. imageComparisonResult.m_resultCode = ImageComparisonResult::ResultCode::FileNotFound;
  637. return false;
  638. }
  639. if (!AZ::Utils::PpmFile::CreateImageBufferFromPpm(readScreenshotFileResult.GetValue(), buffer, size, format))
  640. {
  641. ReportScriptIssue(AZStd::string::format("Screenshot check failed. Failed to read file '%s'", path.c_str()), traceLevel);
  642. imageComparisonResult.m_resultCode = ImageComparisonResult::ResultCode::FileNotLoaded;
  643. return false;
  644. }
  645. return true;
  646. }
  647. bool ScriptReporter::DiffImages(ImageComparisonResult& imageComparisonResult, const AZStd::string& expectedImageFilePath, const AZStd::string& actualImageFilePath, TraceLevel traceLevel)
  648. {
  649. using namespace AZ::Utils;
  650. AZStd::vector<uint8_t> actualImageBuffer;
  651. AZ::RHI::Size actualImageSize;
  652. AZ::RHI::Format actualImageFormat;
  653. if (!LoadPpmData(imageComparisonResult, actualImageFilePath, actualImageBuffer, actualImageSize, actualImageFormat, traceLevel))
  654. {
  655. return false;
  656. }
  657. AZStd::vector<uint8_t> expectedImageBuffer;
  658. AZ::RHI::Size expectedImageSize;
  659. AZ::RHI::Format expectedImageFormat;
  660. if (!LoadPpmData(imageComparisonResult, expectedImageFilePath, expectedImageBuffer, expectedImageSize, expectedImageFormat, traceLevel))
  661. {
  662. return false;
  663. }
  664. float diffScore = 0.0f;
  665. float filteredDiffScore = 0.0f;
  666. static constexpr float ImperceptibleDiffFilter = 0.01;
  667. ImageDiffResultCode rmsResult = AZ::Utils::CalcImageDiffRms(
  668. actualImageBuffer, actualImageSize, actualImageFormat,
  669. expectedImageBuffer, expectedImageSize, expectedImageFormat,
  670. &diffScore,
  671. &filteredDiffScore,
  672. ImperceptibleDiffFilter);
  673. if (rmsResult != ImageDiffResultCode::Success)
  674. {
  675. if(rmsResult == ImageDiffResultCode::SizeMismatch)
  676. {
  677. ReportScreenshotComparisonIssue(AZStd::string::format("Screenshot check failed. Sizes don't match. Expected %u x %u but was %u x %u.",
  678. expectedImageSize.m_width, expectedImageSize.m_height,
  679. actualImageSize.m_width, actualImageSize.m_height),
  680. expectedImageFilePath,
  681. actualImageFilePath,
  682. traceLevel);
  683. imageComparisonResult.m_resultCode = ImageComparisonResult::ResultCode::WrongSize;
  684. return false;
  685. }
  686. else if (rmsResult == ImageDiffResultCode::FormatMismatch || rmsResult == ImageDiffResultCode::UnsupportedFormat)
  687. {
  688. ReportScreenshotComparisonIssue(AZStd::string::format("Screenshot check failed. Could not compare screenshots due to a format issue."),
  689. expectedImageFilePath,
  690. actualImageFilePath,
  691. traceLevel);
  692. imageComparisonResult.m_resultCode = ImageComparisonResult::ResultCode::WrongFormat;
  693. return false;
  694. }
  695. }
  696. imageComparisonResult.m_standardDiffScore = diffScore;
  697. imageComparisonResult.m_filteredDiffScore = filteredDiffScore;
  698. imageComparisonResult.m_finalDiffScore = diffScore; // Set the final score to the standard score just in case the filtered one is ignored
  699. return true;
  700. }
  701. void ScriptReporter::UpdateAllLocalBaselineImages()
  702. {
  703. int failureCount = 0;
  704. int successCount = 0;
  705. for (ScriptReport& report : m_scriptReports)
  706. {
  707. for (ScreenshotTestInfo& screenshotTest : report.m_screenshotTests)
  708. {
  709. if (UpdateLocalBaselineImage(screenshotTest, false))
  710. {
  711. successCount++;
  712. }
  713. else
  714. {
  715. failureCount++;
  716. }
  717. }
  718. }
  719. ShowUpdateLocalBaselineResult(successCount, failureCount);
  720. }
  721. bool ScriptReporter::UpdateLocalBaselineImage(ScreenshotTestInfo& screenshotTest, bool showResultDialog)
  722. {
  723. const AZStd::string destinationFile = ScreenshotPaths::GetLocalBaseline(screenshotTest.m_screenshotFilePath);
  724. AZStd::string destinationFolder = destinationFile;
  725. AzFramework::StringFunc::Path::StripFullName(destinationFolder);
  726. m_fileIoErrorHandler.BusConnect();
  727. bool failed = false;
  728. if (!AZ::IO::LocalFileIO::GetInstance()->CreatePath(destinationFolder.c_str()))
  729. {
  730. failed = true;
  731. m_fileIoErrorHandler.ReportLatestIOError(AZStd::string::format("Failed to create folder '%s'.", destinationFolder.c_str()));
  732. }
  733. if (!AZ::IO::LocalFileIO::GetInstance()->Copy(screenshotTest.m_screenshotFilePath.c_str(), destinationFile.c_str()))
  734. {
  735. failed = true;
  736. m_fileIoErrorHandler.ReportLatestIOError(AZStd::string::format("Failed to copy '%s' to '%s'.", screenshotTest.m_screenshotFilePath.c_str(), destinationFile.c_str()));
  737. }
  738. m_fileIoErrorHandler.BusDisconnect();
  739. if (!failed)
  740. {
  741. // Since we just replaced the baseline image, we can update this screenshot test result as an exact match.
  742. // This will update the ImGui report dialog by the next frame.
  743. ClearImageComparisonResult(screenshotTest.m_localComparisonResult);
  744. }
  745. if (showResultDialog)
  746. {
  747. int successCount = !failed;
  748. int failureCount = failed;
  749. ShowUpdateLocalBaselineResult(successCount, failureCount);
  750. }
  751. return !failed;
  752. }
  753. bool ScriptReporter::UpdateSourceBaselineImage(ScreenshotTestInfo& screenshotTest, bool showResultDialog)
  754. {
  755. bool success = true;
  756. auto io = AZ::IO::LocalFileIO::GetInstance();
  757. // Get source folder
  758. if (m_officialBaselineSourceFolder.empty())
  759. {
  760. m_officialBaselineSourceFolder = (AZ::IO::FixedMaxPath(AZ::Utils::GetProjectPath()) / "Scripts" / "ExpectedScreenshots").String();
  761. if (!io->Exists(m_officialBaselineSourceFolder.c_str()))
  762. {
  763. AZ_Error("Automation", false, "Could not find source folder '%s'. Copying to source baseline can only be used on dev platforms.", m_officialBaselineSourceFolder.c_str());
  764. m_officialBaselineSourceFolder.clear();
  765. success = false;
  766. }
  767. }
  768. // Get official cache baseline file
  769. const AZStd::string cacheFilePath = ScreenshotPaths::GetOfficialBaseline(screenshotTest.m_screenshotFilePath);
  770. // Divide cache file path into components to we can access the file name and the parent folder
  771. AZStd::fixed_vector<AZ::IO::FixedMaxPathString, 16> reversePathComponents;
  772. auto GatherPathSegments = [&reversePathComponents](AZStd::string_view token)
  773. {
  774. reversePathComponents.emplace_back(token);
  775. };
  776. AzFramework::StringFunc::TokenizeVisitorReverse(cacheFilePath, GatherPathSegments, "/\\");
  777. // Source folder path
  778. // ".../AtomSampleViewer/Scripts/ExpectedScreenshots/" + "MyTestFolder/"
  779. AZStd::string sourceFolderPath = AZStd::string::format("%s\\%s", m_officialBaselineSourceFolder.c_str(), reversePathComponents[1].c_str());
  780. // Source file path
  781. // ".../AtomSampleViewer/Scripts/ExpectedScreenshots/MyTestFolder/" + "MyTest.ppm"
  782. AZStd::string sourceFilePath = AZStd::string::format("%s\\%s", sourceFolderPath.c_str(), reversePathComponents[0].c_str());
  783. m_fileIoErrorHandler.BusConnect();
  784. // Create parent folder if it doesn't exist
  785. if (success && !io->CreatePath(sourceFolderPath.c_str()))
  786. {
  787. success = false;
  788. m_fileIoErrorHandler.ReportLatestIOError(AZStd::string::format("Failed to create folder '%s'.", sourceFolderPath.c_str()));
  789. }
  790. // Replace source screenshot with new result
  791. if (success && !io->Copy(screenshotTest.m_screenshotFilePath.c_str(), sourceFilePath.c_str()))
  792. {
  793. success = false;
  794. m_fileIoErrorHandler.ReportLatestIOError(AZStd::string::format("Failed to copy '%s' to '%s'.", screenshotTest.m_screenshotFilePath.c_str(), sourceFilePath.c_str()));
  795. }
  796. m_fileIoErrorHandler.BusDisconnect();
  797. if (success)
  798. {
  799. // Since we just replaced the baseline image, we can update this screenshot test result as an exact match.
  800. // This will update the ImGui report dialog by the next frame.
  801. ClearImageComparisonResult(screenshotTest.m_officialComparisonResult);
  802. }
  803. if (showResultDialog)
  804. {
  805. AZStd::string message = "Destination: " + sourceFilePath + "\n";
  806. message += success
  807. ? AZStd::string::format("Copy successful!.\n")
  808. : AZStd::string::format("Copy failed!\n");
  809. m_messageBox.OpenPopupMessage("Update Baseline Image(s) Result", message);
  810. }
  811. return success;
  812. }
  813. void ScriptReporter::ClearImageComparisonResult(ImageComparisonResult& comparisonResult)
  814. {
  815. comparisonResult.m_resultCode = ImageComparisonResult::ResultCode::Pass;
  816. comparisonResult.m_standardDiffScore = 0.0f;
  817. comparisonResult.m_filteredDiffScore = 0.0f;
  818. comparisonResult.m_finalDiffScore = 0.0f;
  819. }
  820. void ScriptReporter::ShowUpdateLocalBaselineResult(int successCount, int failureCount)
  821. {
  822. AZStd::string message;
  823. if (failureCount == 0 && successCount == 0)
  824. {
  825. message = "No screenshots found.";
  826. }
  827. else
  828. {
  829. message = "Destination: " + ScreenshotPaths::GetLocalBaselineFolder(true) + "\n";
  830. if (successCount > 0)
  831. {
  832. message += AZStd::string::format("Successfully copied %d files.\n", successCount);
  833. }
  834. if (failureCount > 0)
  835. {
  836. message += AZStd::string::format("Failed to copy %d files.\n", failureCount);
  837. }
  838. }
  839. m_messageBox.OpenPopupMessage("Update Baseline Image(s) Result", message);
  840. }
  841. void ScriptReporter::CheckLatestScreenshot(const ImageComparisonToleranceLevel* toleranceLevel)
  842. {
  843. AZ_Assert(GetCurrentScriptReport(), "There is no active script");
  844. if (GetCurrentScriptReport() == nullptr || GetCurrentScriptReport()->m_screenshotTests.empty())
  845. {
  846. ReportScriptError("CheckLatestScreenshot() did not find any screenshots to check.");
  847. return;
  848. }
  849. ScreenshotTestInfo& screenshotTestInfo = GetCurrentScriptReport()->m_screenshotTests.back();
  850. if (toleranceLevel == nullptr)
  851. {
  852. screenshotTestInfo.m_officialComparisonResult.m_resultCode = ImageComparisonResult::ResultCode::NullImageComparisonToleranceLevel;
  853. ReportScriptError("Screenshot check failed. No ImageComparisonToleranceLevel provided.");
  854. return;
  855. }
  856. screenshotTestInfo.m_toleranceLevel = *toleranceLevel;
  857. screenshotTestInfo.m_officialBaselineScreenshotFilePath = ScreenshotPaths::GetOfficialBaseline(screenshotTestInfo.m_screenshotFilePath);
  858. if (screenshotTestInfo.m_officialBaselineScreenshotFilePath.empty())
  859. {
  860. ReportScriptError(AZStd::string::format("Screenshot check failed. Could not determine expected screenshot path for '%s'", screenshotTestInfo.m_screenshotFilePath.c_str()));
  861. screenshotTestInfo.m_officialComparisonResult.m_resultCode = ImageComparisonResult::ResultCode::FileNotFound;
  862. }
  863. else
  864. {
  865. bool imagesWereCompared = DiffImages(
  866. screenshotTestInfo.m_officialComparisonResult,
  867. screenshotTestInfo.m_officialBaselineScreenshotFilePath,
  868. screenshotTestInfo.m_screenshotFilePath,
  869. TraceLevel::Error);
  870. if (imagesWereCompared)
  871. {
  872. screenshotTestInfo.m_officialComparisonResult.m_finalDiffScore = toleranceLevel->m_filterImperceptibleDiffs ?
  873. screenshotTestInfo.m_officialComparisonResult.m_filteredDiffScore :
  874. screenshotTestInfo.m_officialComparisonResult.m_standardDiffScore;
  875. if (screenshotTestInfo.m_officialComparisonResult.m_finalDiffScore <= toleranceLevel->m_threshold)
  876. {
  877. screenshotTestInfo.m_officialComparisonResult.m_resultCode = ImageComparisonResult::ResultCode::Pass;
  878. }
  879. else
  880. {
  881. // Be aware there is an automation test script that looks for the "Screenshot check failed. Diff score" string text to report failures.
  882. // If you change this message, be sure to update the associated tests as well located here: "C:/path/to/Lumberyard/AtomSampleViewer/Standalone/PythonTests"
  883. ReportScreenshotComparisonIssue(
  884. AZStd::string::format("Screenshot check failed. Diff score %f exceeds threshold of %f ('%s').",
  885. screenshotTestInfo.m_officialComparisonResult.m_finalDiffScore, toleranceLevel->m_threshold, toleranceLevel->m_name.c_str()),
  886. screenshotTestInfo.m_officialBaselineScreenshotFilePath,
  887. screenshotTestInfo.m_screenshotFilePath,
  888. TraceLevel::Error);
  889. screenshotTestInfo.m_officialComparisonResult.m_resultCode = ImageComparisonResult::ResultCode::ThresholdExceeded;
  890. }
  891. }
  892. }
  893. screenshotTestInfo.m_localBaselineScreenshotFilePath = ScreenshotPaths::GetLocalBaseline(screenshotTestInfo.m_screenshotFilePath);
  894. if (screenshotTestInfo.m_localBaselineScreenshotFilePath.empty())
  895. {
  896. ReportScriptWarning(AZStd::string::format("Screenshot check failed. Could not determine local baseline screenshot path for '%s'", screenshotTestInfo.m_screenshotFilePath.c_str()));
  897. screenshotTestInfo.m_localComparisonResult.m_resultCode = ImageComparisonResult::ResultCode::FileNotFound;
  898. }
  899. else
  900. {
  901. // Local screenshots should be expected match 100% every time, otherwise warnings are reported. This will help developers track and investigate changes,
  902. // for example if they make local changes that impact some unrelated AtomSampleViewer sample in an unexpected way, they will see a warning about this.
  903. bool imagesWereCompared = DiffImages(
  904. screenshotTestInfo.m_localComparisonResult,
  905. screenshotTestInfo.m_localBaselineScreenshotFilePath,
  906. screenshotTestInfo.m_screenshotFilePath,
  907. TraceLevel::Warning);
  908. if (imagesWereCompared)
  909. {
  910. if(screenshotTestInfo.m_localComparisonResult.m_standardDiffScore == 0.0f)
  911. {
  912. screenshotTestInfo.m_localComparisonResult.m_resultCode = ImageComparisonResult::ResultCode::Pass;
  913. }
  914. else
  915. {
  916. ReportScreenshotComparisonIssue(
  917. AZStd::string::format("Screenshot check failed. Screenshot does not match the local baseline; something has changed. Diff score is %f.", screenshotTestInfo.m_localComparisonResult.m_standardDiffScore),
  918. screenshotTestInfo.m_localBaselineScreenshotFilePath,
  919. screenshotTestInfo.m_screenshotFilePath,
  920. TraceLevel::Warning);
  921. screenshotTestInfo.m_localComparisonResult.m_resultCode = ImageComparisonResult::ResultCode::ThresholdExceeded;
  922. }
  923. }
  924. }
  925. }
  926. } // namespace AtomSampleViewer