ScriptReporter.cpp 56 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include <sstream>
  9. #include <Automation/ScriptReporter.h>
  10. #include <Utils/Utils.h>
  11. #include <Atom/RHI/Factory.h>
  12. #include <AzFramework/API/ApplicationAPI.h>
  13. #include <AzFramework/StringFunc/StringFunc.h>
  14. #include <AzFramework/IO/LocalFileIO.h>
  15. #include <AzCore/IO/SystemFile.h>
  16. #include <AzCore/Utils/Utils.h>
  17. namespace AtomSampleViewer
  18. {
  19. // Must match ScriptReporter::DisplayOption Enum
  20. static const char* DiplayOptions[] =
  21. {
  22. "All Results", "Warnings & Errors", "Errors Only",
  23. };
  24. static const char* SortOptions[] =
  25. {
  26. "Sort by Script", "Sort by Official Baseline Diff Score", "Sort by Local Baseline Diff Score",
  27. };
  28. AZStd::string ScriptReporter::ImageComparisonResult::GetSummaryString() const
  29. {
  30. AZStd::string resultString;
  31. if (m_resultCode == ResultCode::ThresholdExceeded || m_resultCode == ResultCode::Pass)
  32. {
  33. resultString = AZStd::string::format("Diff Score: %f", m_diffScore);
  34. }
  35. else if (m_resultCode == ResultCode::WrongSize)
  36. {
  37. resultString = "Wrong size";
  38. }
  39. else if (m_resultCode == ResultCode::FileNotFound)
  40. {
  41. resultString = "File not found";
  42. }
  43. else if (m_resultCode == ResultCode::FileNotLoaded)
  44. {
  45. resultString = "File load failed";
  46. }
  47. else if (m_resultCode == ResultCode::WrongFormat)
  48. {
  49. resultString = "Format is not supported";
  50. }
  51. else if (m_resultCode == ResultCode::NullImageComparisonToleranceLevel)
  52. {
  53. resultString = "ImageComparisonToleranceLevel not provided";
  54. }
  55. else if (m_resultCode == ResultCode::None)
  56. {
  57. // "None" could be the case if the results dialog is open while the script is running
  58. resultString = "No results";
  59. }
  60. else
  61. {
  62. resultString = "Unhandled Image Comparison ResultCode";
  63. AZ_Assert(false, "Unhandled Image Comparison ResultCode");
  64. }
  65. return resultString;
  66. }
  67. void ScriptReporter::SetAvailableToleranceLevels(const AZStd::vector<ImageComparisonToleranceLevel>& toleranceLevels)
  68. {
  69. m_availableToleranceLevels = toleranceLevels;
  70. }
  71. void ScriptReporter::Reset()
  72. {
  73. m_scriptReports.clear();
  74. m_reportsSortedByOfficialBaslineScore.clear();
  75. m_reportsSortedByLocaBaslineScore.clear();
  76. m_currentScriptIndexStack.clear();
  77. m_invalidationMessage.clear();
  78. m_uniqueTimestamp = GenerateTimestamp();
  79. }
  80. void ScriptReporter::SetInvalidationMessage(const AZStd::string& message)
  81. {
  82. m_invalidationMessage = message;
  83. // Reporting this message here instead of when running the script so it won't show up as an error in the ImGui report.
  84. AZ_Error("ScriptReporter", m_invalidationMessage.empty(), "Subsequent test results will be invalid because '%s'", m_invalidationMessage.c_str());
  85. }
  86. void ScriptReporter::PushScript(const AZStd::string& scriptAssetPath)
  87. {
  88. if (GetCurrentScriptReport())
  89. {
  90. // Only the current script should listen for Trace Errors
  91. GetCurrentScriptReport()->BusDisconnect();
  92. }
  93. m_currentScriptIndexStack.push_back(m_scriptReports.size());
  94. m_scriptReports.emplace_back().m_scriptAssetPath = scriptAssetPath;
  95. m_scriptReports.back().BusConnect();
  96. }
  97. void ScriptReporter::PopScript()
  98. {
  99. AZ_Assert(GetCurrentScriptReport(), "There is no active script");
  100. if (GetCurrentScriptReport())
  101. {
  102. GetCurrentScriptReport()->BusDisconnect();
  103. m_currentScriptIndexStack.pop_back();
  104. }
  105. if (GetCurrentScriptReport())
  106. {
  107. // Make sure the newly restored current script is listening for Trace Errors
  108. GetCurrentScriptReport()->BusConnect();
  109. }
  110. }
  111. bool ScriptReporter::HasActiveScript() const
  112. {
  113. return !m_currentScriptIndexStack.empty();
  114. }
  115. ScriptReporter::ScreenshotTestInfo::ScreenshotTestInfo(const AZStd::string& screenshotName)
  116. {
  117. AZ_Assert(!screenshotName.empty(), "The screenshot file name shouldn't be empty.");
  118. AZ::Render::FrameCapturePathOutcome pathOutcome;
  119. AZ::Render::FrameCaptureTestRequestBus::BroadcastResult(
  120. pathOutcome,
  121. &AZ::Render::FrameCaptureTestRequestBus::Events::BuildScreenshotFilePath,
  122. screenshotName, true);
  123. if (pathOutcome.IsSuccess())
  124. {
  125. m_screenshotFilePath = pathOutcome.GetValue();
  126. }
  127. else
  128. {
  129. AZ_Error("ScriptReporter", false, "%s", pathOutcome.GetError().m_errorMessage.c_str());
  130. }
  131. AZ::Render::FrameCaptureTestRequestBus::BroadcastResult(
  132. pathOutcome,
  133. &AZ::Render::FrameCaptureTestRequestBus::Events::BuildOfficialBaselineFilePath,
  134. screenshotName, false);
  135. if (pathOutcome.IsSuccess())
  136. {
  137. m_officialBaselineScreenshotFilePath = pathOutcome.GetValue();
  138. }
  139. else
  140. {
  141. AZ_Error("ScriptReporter", false, "%s", pathOutcome.GetError().m_errorMessage.c_str());
  142. }
  143. AZ::Render::FrameCaptureTestRequestBus::BroadcastResult(
  144. pathOutcome,
  145. &AZ::Render::FrameCaptureTestRequestBus::Events::BuildLocalBaselineFilePath,
  146. screenshotName, true);
  147. if (pathOutcome.IsSuccess())
  148. {
  149. m_localBaselineScreenshotFilePath = pathOutcome.GetValue();
  150. }
  151. else
  152. {
  153. AZ_Error("ScriptReporter", false, "%s", pathOutcome.GetError().m_errorMessage.c_str());
  154. }
  155. }
  156. bool ScriptReporter::AddScreenshotTest(const AZStd::string& imageName)
  157. {
  158. AZ_Assert(GetCurrentScriptReport(), "There is no active script");
  159. ScreenshotTestInfo screenshotTestInfo(imageName);
  160. GetCurrentScriptReport()->m_screenshotTests.push_back(AZStd::move(screenshotTestInfo));
  161. return true;
  162. }
  163. void ScriptReporter::TickImGui()
  164. {
  165. if (m_showReportDialog)
  166. {
  167. ShowReportDialog();
  168. }
  169. }
  170. bool ScriptReporter::HasErrorsAssertsInReport() const
  171. {
  172. for (const ScriptReport& scriptReport : m_scriptReports)
  173. {
  174. if (scriptReport.m_assertCount > 0 || scriptReport.m_generalErrorCount > 0 || scriptReport.m_screenshotErrorCount > 0)
  175. {
  176. return true;
  177. }
  178. }
  179. return false;
  180. }
  181. void ScriptReporter::DisplayScriptResultsSummary()
  182. {
  183. ImGui::Separator();
  184. if (HasActiveScript())
  185. {
  186. ImGui::PushStyleColor(ImGuiCol_Text, m_highlightSettings.m_highlightWarning);
  187. ImGui::Text("Script is running... (_ _)zzz");
  188. ImGui::PopStyleColor();
  189. }
  190. else if (m_resultsSummary.m_totalErrors > 0 || m_resultsSummary.m_totalAsserts > 0 || m_resultsSummary.m_totalScreenshotsFailed > 0)
  191. {
  192. ImGui::PushStyleColor(ImGuiCol_Text, m_highlightSettings.m_highlightFailed);
  193. ImGui::Text("(>_<) FAILED (>_<)");
  194. ImGui::PopStyleColor();
  195. }
  196. else
  197. {
  198. if (m_invalidationMessage.empty())
  199. {
  200. ImGui::PushStyleColor(ImGuiCol_Text, m_highlightSettings.m_highlightPassed);
  201. ImGui::Text("\\(^_^)/ PASSED \\(^_^)/");
  202. ImGui::PopStyleColor();
  203. }
  204. else
  205. {
  206. ImGui::Text("(-_-) INVALID ... but passed (-_-)");
  207. }
  208. }
  209. if (!m_invalidationMessage.empty())
  210. {
  211. ImGui::Separator();
  212. ImGui::PushStyleColor(ImGuiCol_Text, m_highlightSettings.m_highlightFailed);
  213. ImGui::Text("(%s)", m_invalidationMessage.c_str());
  214. ImGui::PopStyleColor();
  215. }
  216. ImGui::Separator();
  217. ImGui::Text("Test Script Count: %zu", m_scriptReports.size());
  218. HighlightTextIf(m_resultsSummary.m_totalAsserts > 0, m_highlightSettings.m_highlightFailed);
  219. ImGui::Text("Total Asserts: %u %s", m_resultsSummary.m_totalAsserts, SeeConsole(m_resultsSummary.m_totalAsserts, "Trace::Assert").c_str());
  220. HighlightTextIf(m_resultsSummary.m_totalErrors > 0, m_highlightSettings.m_highlightFailed);
  221. ImGui::Text("Total Errors: %u %s", m_resultsSummary.m_totalErrors, SeeConsole(m_resultsSummary.m_totalErrors, "Trace::Error").c_str());
  222. HighlightTextIf(m_resultsSummary.m_totalWarnings > 0, m_highlightSettings.m_highlightWarning);
  223. ImGui::Text("Total Warnings: %u %s", m_resultsSummary.m_totalWarnings, SeeConsole(m_resultsSummary.m_totalWarnings, "Trace::Warning").c_str());
  224. ResetTextHighlight();
  225. ImGui::Text("Total Screenshot Count: %u", m_resultsSummary.m_totalScreenshotsCount);
  226. HighlightTextIf(m_resultsSummary.m_totalScreenshotsFailed > 0, m_highlightSettings.m_highlightFailed);
  227. ImGui::Text("Total Screenshot Failures: %u %s", m_resultsSummary.m_totalScreenshotsFailed, SeeBelow(m_resultsSummary.m_totalScreenshotsFailed).c_str());
  228. HighlightTextIf(m_resultsSummary.m_totalScreenshotWarnings > 0, m_highlightSettings.m_highlightWarning);
  229. ImGui::Text("Total Screenshot Warnings: %u %s", m_resultsSummary.m_totalScreenshotWarnings, SeeBelow(m_resultsSummary.m_totalScreenshotWarnings).c_str());
  230. ResetTextHighlight();
  231. }
  232. const AtomSampleViewer::ScriptReporter::ScriptResultsSummary& ScriptReporter::GetScriptResultSummary() const
  233. {
  234. return m_resultsSummary;
  235. }
  236. void ScriptReporter::ShowDiffButton(const char* buttonLabel, const AZStd::string& imagePathA, const AZStd::string& imagePathB)
  237. {
  238. if (ImGui::Button(buttonLabel))
  239. {
  240. if (!Utils::RunDiffTool(imagePathA, imagePathB))
  241. {
  242. m_messageBox.OpenPopupMessage("Can't Diff", "Image diff is not supported on this platform, or the required diff tool is not installed.");
  243. }
  244. }
  245. }
  246. AZStd::string ScriptReporter::GenerateTimestamp() const
  247. {
  248. const AZStd::chrono::system_clock::time_point now = AZStd::chrono::system_clock::now();
  249. const float timeFloat = AZStd::chrono::duration<float>(now.time_since_epoch()).count();
  250. return AZStd::string::format("%.4f", timeFloat);
  251. }
  252. AZStd::string ScriptReporter::GenerateAndCreateExportedImageDiffPath(const ScriptReport& scriptReport, const ScreenshotTestInfo& screenshotTest) const
  253. {
  254. const auto projectPath = AZ::Utils::GetProjectPath();
  255. AZStd::string imageDiffPath;
  256. AZStd::string scriptFilenameWithouExtension;
  257. AzFramework::StringFunc::Path::GetFileName(scriptReport.m_scriptAssetPath.c_str(), scriptFilenameWithouExtension);
  258. AzFramework::StringFunc::Path::StripExtension(scriptFilenameWithouExtension);
  259. AZStd::string screenshotFilenameWithouExtension;
  260. AzFramework::StringFunc::Path::GetFileName(screenshotTest.m_screenshotFilePath.c_str(), screenshotFilenameWithouExtension);
  261. AzFramework::StringFunc::Path::StripExtension(screenshotFilenameWithouExtension);
  262. AZStd::string imageDiffFilename = "imageDiff_" + scriptFilenameWithouExtension + "_" + screenshotFilenameWithouExtension + "_" + m_uniqueTimestamp + ".png";
  263. AzFramework::StringFunc::Path::Join(projectPath.c_str(), UserFolder, imageDiffPath);
  264. AzFramework::StringFunc::Path::Join(imageDiffPath.c_str(), TestResultsFolder, imageDiffPath);
  265. AzFramework::StringFunc::Path::Join(imageDiffPath.c_str(), imageDiffFilename.c_str(), imageDiffPath);
  266. AZStd::string imageDiffFolderPath;
  267. AzFramework::StringFunc::Path::GetFolderPath(imageDiffPath.c_str(), imageDiffFolderPath);
  268. auto io = AZ::IO::LocalFileIO::GetInstance();
  269. io->CreatePath(imageDiffFolderPath.c_str());
  270. return imageDiffPath;
  271. }
  272. const ImageComparisonToleranceLevel* ScriptReporter::FindBestToleranceLevel(float diffScore, bool filterImperceptibleDiffs) const
  273. {
  274. [[maybe_unused]] float thresholdChecked = 0.0f;
  275. [[maybe_unused]] bool ignoringMinorDiffs = false;
  276. for (const ImageComparisonToleranceLevel& level : m_availableToleranceLevels)
  277. {
  278. AZ_Assert(level.m_threshold > thresholdChecked || thresholdChecked == 0.0f, "Threshold values are not sequential");
  279. AZ_Assert(level.m_filterImperceptibleDiffs >= ignoringMinorDiffs, "filterImperceptibleDiffs values are not sequential");
  280. thresholdChecked = level.m_threshold;
  281. ignoringMinorDiffs = level.m_filterImperceptibleDiffs;
  282. if (filterImperceptibleDiffs <= level.m_filterImperceptibleDiffs && diffScore <= level.m_threshold)
  283. {
  284. return &level;
  285. }
  286. }
  287. return nullptr;
  288. }
  289. void ScriptReporter::ShowReportDialog()
  290. {
  291. if (ImGui::Begin("Script Results", &m_showReportDialog) && !m_scriptReports.empty())
  292. {
  293. m_highlightSettings.UpdateColorSettings();
  294. m_colorHasBeenSet = false;
  295. m_resultsSummary = ScriptResultsSummary();
  296. for (ScriptReport& scriptReport : m_scriptReports)
  297. {
  298. m_resultsSummary.m_totalAsserts += scriptReport.m_assertCount;
  299. // We don't include screenshot errors and warnings in these totals because those have their own line-items.
  300. m_resultsSummary.m_totalErrors += scriptReport.m_generalErrorCount;
  301. m_resultsSummary.m_totalWarnings += scriptReport.m_generalWarningCount;
  302. m_resultsSummary.m_totalScreenshotWarnings += scriptReport.m_screenshotWarningCount;
  303. m_resultsSummary.m_totalScreenshotsFailed += scriptReport.m_screenshotErrorCount;
  304. // This will catch any false-negatives that could occur if the screenshot failure error messages change without also updating ScriptReport::OnPreError()
  305. m_resultsSummary.m_totalScreenshotsCount += aznumeric_cast<uint32_t>(scriptReport.m_screenshotTests.size());
  306. for (ScreenshotTestInfo& screenshotTest : scriptReport.m_screenshotTests)
  307. {
  308. if (screenshotTest.m_officialComparisonResult.m_resultCode != ImageComparisonResult::ResultCode::Pass &&
  309. screenshotTest.m_officialComparisonResult.m_resultCode != ImageComparisonResult::ResultCode::None)
  310. {
  311. AZ_Assert(scriptReport.m_screenshotErrorCount > 0, "If screenshot comparison failed in any way, m_screenshotErrorCount should be non-zero.");
  312. }
  313. }
  314. }
  315. DisplayScriptResultsSummary();
  316. ImGui::Text("Exported test results: %s", m_exportedTestResultsPath.c_str());
  317. if (ImGui::Button("Update All Local Baseline Images"))
  318. {
  319. m_messageBox.OpenPopupConfirmation(
  320. "Update All Local Baseline Images",
  321. "This will replace all local baseline images \n"
  322. "with the images captured during this test run. \n"
  323. "Are you sure?",
  324. [this]() {
  325. UpdateAllLocalBaselineImages();
  326. });
  327. }
  328. if (ImGui::Button("Export Test Results"))
  329. {
  330. m_messageBox.OpenPopupConfirmation(
  331. "Export Test Results",
  332. "All test results will be exported \n"
  333. "Proceed?",
  334. [this]() {
  335. ExportTestResults();
  336. });
  337. }
  338. int displayOption = m_displayOption;
  339. ImGui::Combo("Display", &displayOption, DiplayOptions, AZ_ARRAY_SIZE(DiplayOptions));
  340. m_displayOption = (DisplayOption)displayOption;
  341. int sortOption = m_currentSortOption;
  342. ImGui::Combo("Sort Results", &sortOption, SortOptions, AZ_ARRAY_SIZE(SortOptions));
  343. m_currentSortOption = (SortOption)sortOption;
  344. ImGui::Checkbox("Force Show 'Update' Buttons", &m_forceShowUpdateButtons);
  345. ImGui::Checkbox("Force Show 'Export Png Diff' Buttons", &m_forceShowExportPngDiffButtons);
  346. m_showWarnings = (m_displayOption == DisplayOption::AllResults) || (m_displayOption == DisplayOption::WarningsAndErrors);
  347. m_showAll = (m_displayOption == DisplayOption::AllResults);
  348. ImGui::Separator();
  349. if (m_currentSortOption == SortOption::Unsorted)
  350. {
  351. for (ScriptReport& scriptReport : m_scriptReports)
  352. {
  353. const bool scriptPassed = scriptReport.m_assertCount == 0 && scriptReport.m_generalErrorCount == 0 && scriptReport.m_screenshotErrorCount == 0;
  354. const bool scriptHasWarnings = scriptReport.m_generalWarningCount > 0 || scriptReport.m_screenshotWarningCount > 0;
  355. // Skip if tests passed and
  356. // 1). have no warnings and we don't want to show successes OR
  357. // 2). only have warnings and we don't want to show warnings
  358. bool skipReport = scriptPassed && ((!scriptHasWarnings && !m_showAll) || (scriptHasWarnings && !m_showWarnings));
  359. if (skipReport)
  360. {
  361. continue;
  362. }
  363. ImGuiTreeNodeFlags scriptNodeFlag = scriptPassed ? FlagDefaultClosed : FlagDefaultOpen;
  364. AZStd::string header = AZStd::string::format("%s %s",
  365. scriptPassed ? "PASSED" : "FAILED",
  366. scriptReport.m_scriptAssetPath.c_str()
  367. );
  368. HighlightTextFailedOrWarning(!scriptPassed, scriptHasWarnings);
  369. if (ImGui::TreeNodeEx(&scriptReport, scriptNodeFlag, "%s", header.c_str()))
  370. {
  371. ResetTextHighlight();
  372. // Number of Asserts
  373. HighlightTextIf(scriptReport.m_assertCount > 0, m_highlightSettings.m_highlightFailed);
  374. if (m_showAll || scriptReport.m_assertCount > 0)
  375. {
  376. ImGui::Text("Asserts: %u %s", scriptReport.m_assertCount, SeeConsole(scriptReport.m_assertCount, "Trace::Assert").c_str());
  377. }
  378. // Number of Errors
  379. HighlightTextIf(scriptReport.m_generalErrorCount > 0, m_highlightSettings.m_highlightFailed);
  380. if (m_showAll || scriptReport.m_generalErrorCount > 0)
  381. {
  382. ImGui::Text("Errors: %u %s", scriptReport.m_generalErrorCount, SeeConsole(scriptReport.m_generalErrorCount, "Trace::Error").c_str());
  383. }
  384. // Number of Warnings
  385. HighlightTextIf(scriptReport.m_generalWarningCount > 0, m_highlightSettings.m_highlightWarning);
  386. if (m_showAll || (m_showWarnings && scriptReport.m_generalWarningCount > 0))
  387. {
  388. ImGui::Text("Warnings: %u %s", scriptReport.m_generalWarningCount, SeeConsole(scriptReport.m_generalWarningCount, "Trace::Warning").c_str());
  389. }
  390. ResetTextHighlight();
  391. // Number of screenshots
  392. if (m_showAll || scriptReport.m_screenshotErrorCount > 0 || (m_showWarnings && scriptReport.m_screenshotWarningCount > 0))
  393. {
  394. ImGui::Text("Screenshot Test Count: %zu", scriptReport.m_screenshotTests.size());
  395. }
  396. // Number of screenshot failures
  397. HighlightTextIf(scriptReport.m_screenshotErrorCount > 0, m_highlightSettings.m_highlightFailed);
  398. if (m_showAll || scriptReport.m_screenshotErrorCount > 0)
  399. {
  400. ImGui::Text("Screenshot Tests Failed: %u %s", scriptReport.m_screenshotErrorCount, SeeBelow(scriptReport.m_screenshotErrorCount).c_str());
  401. }
  402. // Number of screenshot warnings
  403. HighlightTextIf(scriptReport.m_screenshotWarningCount > 0, m_highlightSettings.m_highlightWarning);
  404. if (m_showAll || (m_showWarnings && scriptReport.m_screenshotWarningCount > 0))
  405. {
  406. ImGui::Text("Screenshot Warnings: %u %s", scriptReport.m_screenshotWarningCount, SeeBelow(scriptReport.m_screenshotWarningCount).c_str());
  407. }
  408. ResetTextHighlight();
  409. for (ScreenshotTestInfo& screenshotResult : scriptReport.m_screenshotTests)
  410. {
  411. const bool screenshotPassed = screenshotResult.m_officialComparisonResult.m_resultCode == ImageComparisonResult::ResultCode::Pass;
  412. const bool localBaselineWarning = screenshotResult.m_localComparisonResult.m_resultCode != ImageComparisonResult::ResultCode::Pass;
  413. AZStd::string fileName;
  414. AzFramework::StringFunc::Path::GetFullFileName(screenshotResult.m_screenshotFilePath.c_str(), fileName);
  415. std::stringstream headerSummary;
  416. if (!screenshotPassed)
  417. {
  418. headerSummary << "(" << screenshotResult.m_officialComparisonResult.GetSummaryString().c_str() << ") ";
  419. }
  420. if (localBaselineWarning)
  421. {
  422. headerSummary << "(Local Baseline Warning)";
  423. }
  424. AZStd::string screenshotHeader = AZStd::string::format("%s %s %s",
  425. screenshotPassed ? "PASSED" : "FAILED",
  426. fileName.c_str(),
  427. headerSummary.str().c_str());
  428. ShowScreenshotTestInfoTreeNode(screenshotHeader, scriptReport, screenshotResult);
  429. }
  430. ImGui::TreePop();
  431. }
  432. ResetTextHighlight();
  433. }
  434. }
  435. else
  436. {
  437. const SortedReportIndexMap* sortedReportMap = nullptr;
  438. if (m_currentSortOption == SortOption::OfficialBaselineDiffScore)
  439. {
  440. sortedReportMap = &m_reportsSortedByOfficialBaslineScore;
  441. }
  442. else if (m_currentSortOption == SortOption::LocalBaselineDiffScore)
  443. {
  444. sortedReportMap = &m_reportsSortedByLocaBaslineScore;
  445. }
  446. AZ_Assert(sortedReportMap, "Unhandled m_currentSortOption");
  447. if (sortedReportMap)
  448. {
  449. for (const auto& [threshold, reportIndex] : *sortedReportMap)
  450. {
  451. ScriptReport& scriptReport = m_scriptReports[reportIndex.first];
  452. ScreenshotTestInfo& screenshotResult = scriptReport.m_screenshotTests[reportIndex.second];
  453. float diffScore = 0.0f;
  454. if (m_currentSortOption == SortOption::OfficialBaselineDiffScore)
  455. {
  456. diffScore = screenshotResult.m_officialComparisonResult.m_diffScore;
  457. }
  458. else if (m_currentSortOption == SortOption::LocalBaselineDiffScore)
  459. {
  460. diffScore = screenshotResult.m_localComparisonResult.m_diffScore;
  461. }
  462. const bool screenshotPassed = screenshotResult.m_officialComparisonResult.m_resultCode == ImageComparisonResult::ResultCode::Pass;
  463. AZStd::string fileName;
  464. AzFramework::StringFunc::Path::GetFullFileName(screenshotResult.m_screenshotFilePath.c_str(), fileName);
  465. AZStd::string header = AZStd::string::format("%f %s %s %s '%s'",
  466. diffScore,
  467. screenshotPassed ? "PASSED" : "FAILED",
  468. scriptReport.m_scriptAssetPath.c_str(),
  469. fileName.c_str(),
  470. screenshotResult.m_toleranceLevel.m_name.c_str());
  471. ShowScreenshotTestInfoTreeNode(header, scriptReport, screenshotResult);
  472. }
  473. }
  474. }
  475. ResetTextHighlight();
  476. // Repeat the m_invalidationMessage at the bottom as well, to make sure the user doesn't miss it.
  477. if (!m_invalidationMessage.empty())
  478. {
  479. ImGui::Separator();
  480. ImGui::PushStyleColor(ImGuiCol_Text, m_highlightSettings.m_highlightFailed);
  481. ImGui::Text("(%s)", m_invalidationMessage.c_str());
  482. ImGui::PopStyleColor();
  483. }
  484. }
  485. m_messageBox.TickPopup();
  486. ImGui::End();
  487. }
  488. void ScriptReporter::ShowScreenshotTestInfoTreeNode(const AZStd::string& header, ScriptReport& scriptReport, ScreenshotTestInfo& screenshotResult)
  489. {
  490. const bool screenshotPassed = screenshotResult.m_officialComparisonResult.m_resultCode == ImageComparisonResult::ResultCode::Pass;
  491. const bool localBaselineWarning = screenshotResult.m_localComparisonResult.m_resultCode != ImageComparisonResult::ResultCode::Pass;
  492. // Skip if tests passed without warnings and we don't want to show successes
  493. bool skipScreenshot = (screenshotPassed && !localBaselineWarning && !m_showAll);
  494. // Skip if we only have warnings only and we don't want to show warnings
  495. skipScreenshot = skipScreenshot || (screenshotPassed && localBaselineWarning && !m_showWarnings);
  496. if (skipScreenshot)
  497. {
  498. return;
  499. }
  500. ImGuiTreeNodeFlags screenshotNodeFlag = FlagDefaultClosed;
  501. HighlightTextFailedOrWarning(!screenshotPassed, localBaselineWarning);
  502. if (ImGui::TreeNodeEx(&screenshotResult, screenshotNodeFlag, "%s", header.c_str()))
  503. {
  504. ResetTextHighlight();
  505. ImGui::Text("Screenshot: %s", screenshotResult.m_screenshotFilePath.c_str());
  506. ImGui::Spacing();
  507. HighlightTextIf(!screenshotPassed, m_highlightSettings.m_highlightFailed);
  508. ImGui::Text("Official Baseline: %s", screenshotResult.m_officialBaselineScreenshotFilePath.c_str());
  509. // Official Baseline Result
  510. ImGui::Indent();
  511. {
  512. ImGui::Text("%s", screenshotResult.m_officialComparisonResult.GetSummaryString().c_str());
  513. if (screenshotResult.m_officialComparisonResult.m_resultCode == ImageComparisonResult::ResultCode::ThresholdExceeded ||
  514. screenshotResult.m_officialComparisonResult.m_resultCode == ImageComparisonResult::ResultCode::Pass)
  515. {
  516. ImGui::Text("Used Tolerance: %s", screenshotResult.m_toleranceLevel.ToString().c_str());
  517. const ImageComparisonToleranceLevel* suggestedTolerance = ScriptReporter::FindBestToleranceLevel(
  518. screenshotResult.m_officialComparisonResult.m_diffScore,
  519. screenshotResult.m_toleranceLevel.m_filterImperceptibleDiffs);
  520. if (suggestedTolerance)
  521. {
  522. ImGui::Text("Suggested Tolerance: %s", suggestedTolerance->ToString().c_str());
  523. }
  524. if (screenshotResult.m_toleranceLevel.m_filterImperceptibleDiffs)
  525. {
  526. // This gives an idea of what the tolerance level would be if the imperceptible diffs were not filtered out.
  527. const ImageComparisonToleranceLevel* unfilteredTolerance =
  528. ScriptReporter::FindBestToleranceLevel(screenshotResult.m_officialComparisonResult.m_diffScore, false);
  529. ImGui::Text(
  530. "(Unfiltered Diff Score: %f%s)", screenshotResult.m_officialComparisonResult.m_diffScore,
  531. unfilteredTolerance ? AZStd::string::format(" ~ '%s'", unfilteredTolerance->m_name.c_str()).c_str() : "");
  532. }
  533. }
  534. ResetTextHighlight();
  535. ImGui::PushID("Official");
  536. ShowDiffButton("View Diff", screenshotResult.m_officialBaselineScreenshotFilePath, screenshotResult.m_screenshotFilePath);
  537. ImGui::PopID();
  538. if ((m_forceShowExportPngDiffButtons ||
  539. screenshotResult.m_officialComparisonResult.m_resultCode == ImageComparisonResult::ResultCode::ThresholdExceeded) &&
  540. ImGui::Button("Export Png Diff"))
  541. {
  542. const AZStd::string imageDiffPath = GenerateAndCreateExportedImageDiffPath(scriptReport, screenshotResult);
  543. ExportImageDiff(imageDiffPath.c_str(), screenshotResult);
  544. m_messageBox.OpenPopupMessage(
  545. "Image Diff Exported Successfully",
  546. AZStd::string::format("The image diff file was saved in %s", imageDiffPath.c_str()).c_str());
  547. }
  548. if ((!screenshotPassed || m_forceShowUpdateButtons) && ImGui::Button("Update##Official"))
  549. {
  550. if (screenshotResult.m_localComparisonResult.m_resultCode == ImageComparisonResult::ResultCode::FileNotFound)
  551. {
  552. UpdateSourceBaselineImage(screenshotResult, true);
  553. }
  554. else
  555. {
  556. m_messageBox.OpenPopupConfirmation(
  557. "Update Official Baseline Image",
  558. "This will replace the official baseline image \n"
  559. "with the image captured during this test run. \n"
  560. "Are you sure?",
  561. // It's important to bind screenshotResult by reference because UpdateOfficialBaselineImage will update it
  562. [this, &screenshotResult]()
  563. {
  564. UpdateSourceBaselineImage(screenshotResult, true);
  565. });
  566. }
  567. }
  568. }
  569. ImGui::Unindent();
  570. ImGui::Spacing();
  571. HighlightTextIf(localBaselineWarning, m_highlightSettings.m_highlightWarning);
  572. ImGui::Text("Local Baseline: %s", screenshotResult.m_localBaselineScreenshotFilePath.c_str());
  573. // Local Baseline Result
  574. ImGui::Indent();
  575. {
  576. ImGui::Text("%s", screenshotResult.m_localComparisonResult.GetSummaryString().c_str());
  577. ResetTextHighlight();
  578. ImGui::PushID("Local");
  579. ShowDiffButton("View Diff", screenshotResult.m_localBaselineScreenshotFilePath, screenshotResult.m_screenshotFilePath);
  580. ImGui::PopID();
  581. if ((localBaselineWarning || m_forceShowUpdateButtons) && ImGui::Button("Update##Local"))
  582. {
  583. if (screenshotResult.m_localComparisonResult.m_resultCode == ImageComparisonResult::ResultCode::FileNotFound)
  584. {
  585. UpdateLocalBaselineImage(screenshotResult, true);
  586. }
  587. else
  588. {
  589. m_messageBox.OpenPopupConfirmation(
  590. "Update Local Baseline Image",
  591. "This will replace the local baseline image \n"
  592. "with the image captured during this test run. \n"
  593. "Are you sure?",
  594. // It's important to bind screenshotResult by reference because UpdateLocalBaselineImage will update it
  595. [this, &screenshotResult]()
  596. {
  597. UpdateLocalBaselineImage(screenshotResult, true);
  598. });
  599. }
  600. }
  601. }
  602. ImGui::Unindent();
  603. ImGui::Spacing();
  604. ResetTextHighlight();
  605. ImGui::TreePop();
  606. }
  607. }
  608. void ScriptReporter::OpenReportDialog()
  609. {
  610. m_showReportDialog = true;
  611. }
  612. void ScriptReporter::HideReportDialog()
  613. {
  614. m_showReportDialog = false;
  615. }
  616. ScriptReporter::ScriptReport* ScriptReporter::GetCurrentScriptReport()
  617. {
  618. if (!m_currentScriptIndexStack.empty())
  619. {
  620. return &m_scriptReports[m_currentScriptIndexStack.back()];
  621. }
  622. else
  623. {
  624. return nullptr;
  625. }
  626. }
  627. AZStd::string ScriptReporter::SeeConsole(uint32_t issueCount, const char* searchString)
  628. {
  629. if (issueCount == 0)
  630. {
  631. return AZStd::string{};
  632. }
  633. else
  634. {
  635. return AZStd::string::format("(See \"%s\" messages in console output)", searchString);
  636. }
  637. }
  638. AZStd::string ScriptReporter::SeeBelow(uint32_t issueCount)
  639. {
  640. if (issueCount == 0)
  641. {
  642. return AZStd::string{};
  643. }
  644. else
  645. {
  646. return AZStd::string::format("(See below)");
  647. }
  648. }
  649. void ScriptReporter::HighlightTextIf(bool shouldSet, ImVec4 color)
  650. {
  651. if (m_colorHasBeenSet)
  652. {
  653. ImGui::PopStyleColor();
  654. m_colorHasBeenSet = false;
  655. }
  656. if (shouldSet)
  657. {
  658. ImGui::PushStyleColor(ImGuiCol_Text, color);
  659. m_colorHasBeenSet = true;
  660. }
  661. }
  662. void ScriptReporter::ResetTextHighlight()
  663. {
  664. if (m_colorHasBeenSet)
  665. {
  666. ImGui::PopStyleColor();
  667. m_colorHasBeenSet = false;
  668. }
  669. }
  670. void ScriptReporter::HighlightTextFailedOrWarning(bool isFailed, bool isWarning)
  671. {
  672. if (m_colorHasBeenSet)
  673. {
  674. ImGui::PopStyleColor();
  675. m_colorHasBeenSet = false;
  676. }
  677. if (isFailed)
  678. {
  679. ImGui::PushStyleColor(ImGuiCol_Text, m_highlightSettings.m_highlightFailed);
  680. m_colorHasBeenSet = true;
  681. }
  682. else if (isWarning)
  683. {
  684. ImGui::PushStyleColor(ImGuiCol_Text, m_highlightSettings.m_highlightWarning);
  685. m_colorHasBeenSet = true;
  686. }
  687. }
  688. void ScriptReporter::SortScriptReports()
  689. {
  690. for (size_t i = 0; i < m_scriptReports.size(); ++i)
  691. {
  692. const AZStd::vector<ScriptReporter::ScreenshotTestInfo>& screenshotTestInfos = m_scriptReports[i].m_screenshotTests;
  693. for (size_t j = 0; j < screenshotTestInfos.size(); ++j)
  694. {
  695. m_reportsSortedByOfficialBaslineScore.insert(AZStd::pair<float, ReportIndex>(
  696. screenshotTestInfos[j].m_officialComparisonResult.m_diffScore,
  697. ReportIndex{ i, j }));
  698. m_reportsSortedByLocaBaslineScore.insert(AZStd::pair<float, ReportIndex>(
  699. screenshotTestInfos[j].m_localComparisonResult.m_diffScore,
  700. ReportIndex{ i, j }));
  701. }
  702. }
  703. }
  704. void ScriptReporter::ReportScriptError([[maybe_unused]] const AZStd::string& message)
  705. {
  706. AZ_Error("ScriptReporter", false, "Script: %s", message.c_str());
  707. }
  708. void ScriptReporter::ReportScriptWarning([[maybe_unused]] const AZStd::string& message)
  709. {
  710. AZ_Warning("ScriptReporter", false, "Script: %s", message.c_str());
  711. }
  712. void ScriptReporter::ReportScriptIssue(const AZStd::string& message, TraceLevel traceLevel)
  713. {
  714. switch (traceLevel)
  715. {
  716. case TraceLevel::Error:
  717. ReportScriptError(message);
  718. break;
  719. case TraceLevel::Warning:
  720. ReportScriptWarning(message);
  721. break;
  722. default:
  723. AZ_Assert(false, "Unhandled TraceLevel");
  724. }
  725. }
  726. void ScriptReporter::ReportScreenshotComparisonIssue(const AZStd::string& message, const AZStd::string& expectedImageFilePath, const AZStd::string& actualImageFilePath, TraceLevel traceLevel)
  727. {
  728. AZStd::string fullMessage = AZStd::string::format("%s\n Expected: '%s'\n Actual: '%s'",
  729. message.c_str(),
  730. expectedImageFilePath.c_str(),
  731. actualImageFilePath.c_str());
  732. ReportScriptIssue(fullMessage, traceLevel);
  733. }
  734. void ScriptReporter::UpdateAllLocalBaselineImages()
  735. {
  736. int failureCount = 0;
  737. int successCount = 0;
  738. for (ScriptReport& report : m_scriptReports)
  739. {
  740. for (ScreenshotTestInfo& screenshotTest : report.m_screenshotTests)
  741. {
  742. if (UpdateLocalBaselineImage(screenshotTest, false))
  743. {
  744. successCount++;
  745. }
  746. else
  747. {
  748. failureCount++;
  749. }
  750. }
  751. }
  752. ShowUpdateLocalBaselineResult(successCount, failureCount);
  753. }
  754. bool ScriptReporter::UpdateLocalBaselineImage(ScreenshotTestInfo& screenshotTest, bool showResultDialog)
  755. {
  756. const AZStd::string destinationFile = screenshotTest.m_localBaselineScreenshotFilePath;
  757. AZStd::string destinationFolder = destinationFile;
  758. AzFramework::StringFunc::Path::StripFullName(destinationFolder);
  759. bool failed = false;
  760. if (!AZ::IO::LocalFileIO::GetInstance()->CreatePath(destinationFolder.c_str()))
  761. {
  762. failed = true;
  763. AZ_Error("ScriptReporter", false, "Failed to create folder '%s'.", destinationFolder.c_str());
  764. }
  765. if (!AZ::IO::LocalFileIO::GetInstance()->Copy(screenshotTest.m_screenshotFilePath.c_str(), destinationFile.c_str()))
  766. {
  767. failed = true;
  768. AZ_Error("ScriptReporter", false, "Failed to copy '%s' to '%s'.", screenshotTest.m_screenshotFilePath.c_str(), destinationFile.c_str());
  769. }
  770. if (!failed)
  771. {
  772. // Since we just replaced the baseline image, we can update this screenshot test result as an exact match.
  773. // This will update the ImGui report dialog by the next frame.
  774. ClearImageComparisonResult(screenshotTest.m_localComparisonResult);
  775. }
  776. if (showResultDialog)
  777. {
  778. int successCount = !failed;
  779. int failureCount = failed;
  780. ShowUpdateLocalBaselineResult(successCount, failureCount);
  781. }
  782. return !failed;
  783. }
  784. bool ScriptReporter::UpdateSourceBaselineImage(ScreenshotTestInfo& screenshotTest, bool showResultDialog)
  785. {
  786. bool success = true;
  787. auto io = AZ::IO::LocalFileIO::GetInstance();
  788. // Get source folder
  789. if (m_officialBaselineSourceFolder.empty())
  790. {
  791. m_officialBaselineSourceFolder = (AZ::IO::FixedMaxPath(AZ::Utils::GetProjectPath()) / "Scripts" / "ExpectedScreenshots").String();
  792. if (!io->Exists(m_officialBaselineSourceFolder.c_str()))
  793. {
  794. AZ_Error("ScriptReporter", false, "Could not find source folder '%s'. Copying to source baseline can only be used on dev platforms.", m_officialBaselineSourceFolder.c_str());
  795. m_officialBaselineSourceFolder.clear();
  796. success = false;
  797. }
  798. }
  799. // Get official cache baseline file
  800. const AZStd::string cacheFilePath = screenshotTest.m_officialBaselineScreenshotFilePath;
  801. // Divide cache file path into components to we can access the file name and the parent folder
  802. AZStd::fixed_vector<AZ::IO::FixedMaxPathString, 16> reversePathComponents;
  803. auto GatherPathSegments = [&reversePathComponents](AZStd::string_view token)
  804. {
  805. reversePathComponents.emplace_back(token);
  806. };
  807. AzFramework::StringFunc::TokenizeVisitorReverse(cacheFilePath, GatherPathSegments, "/\\");
  808. // Source folder path
  809. // ".../AtomSampleViewer/Scripts/ExpectedScreenshots/" + "MyTestFolder/"
  810. AZStd::string sourceFolderPath = AZStd::string::format("%s\\%s", m_officialBaselineSourceFolder.c_str(), reversePathComponents[1].c_str());
  811. // Source file path
  812. // ".../AtomSampleViewer/Scripts/ExpectedScreenshots/MyTestFolder/" + "MyTest.png"
  813. AZStd::string sourceFilePath = AZStd::string::format("%s\\%s", sourceFolderPath.c_str(), reversePathComponents[0].c_str());
  814. // Create parent folder if it doesn't exist
  815. if (success && !io->CreatePath(sourceFolderPath.c_str()))
  816. {
  817. success = false;
  818. AZ_Error("ScriptReporter", false, "Failed to create folder '%s'.", sourceFolderPath.c_str());
  819. }
  820. // Replace source screenshot with new result
  821. if (success && !io->Copy(screenshotTest.m_screenshotFilePath.c_str(), sourceFilePath.c_str()))
  822. {
  823. success = false;
  824. AZ_Error("ScriptReporter", false, "Failed to copy '%s' to '%s'.", screenshotTest.m_screenshotFilePath.c_str(), sourceFilePath.c_str());
  825. }
  826. if (success)
  827. {
  828. // Since we just replaced the baseline image, we can update this screenshot test result as an exact match.
  829. // This will update the ImGui report dialog by the next frame.
  830. ClearImageComparisonResult(screenshotTest.m_officialComparisonResult);
  831. }
  832. if (showResultDialog)
  833. {
  834. AZStd::string message = "Destination: " + sourceFilePath + "\n";
  835. message += success
  836. ? AZStd::string::format("Copy successful!.\n")
  837. : AZStd::string::format("Copy failed!\n");
  838. m_messageBox.OpenPopupMessage("Update Baseline Image(s) Result", message);
  839. }
  840. return success;
  841. }
  842. void ScriptReporter::ClearImageComparisonResult(ImageComparisonResult& comparisonResult)
  843. {
  844. comparisonResult.m_resultCode = ImageComparisonResult::ResultCode::Pass;
  845. comparisonResult.m_diffScore = 0.0f;
  846. }
  847. void ScriptReporter::ShowUpdateLocalBaselineResult(int successCount, int failureCount)
  848. {
  849. AZStd::string message;
  850. if (failureCount == 0 && successCount == 0)
  851. {
  852. message = "No screenshots found.";
  853. }
  854. else
  855. {
  856. AZ::Render::FrameCapturePathOutcome pathOutcome;
  857. AZ::Render::FrameCaptureTestRequestBus::BroadcastResult(
  858. pathOutcome,
  859. &AZ::Render::FrameCaptureTestRequestBus::Events::BuildScreenshotFilePath,
  860. "", true);
  861. AZ_Error("ScriptReporter", pathOutcome.IsSuccess(), "%s", pathOutcome.GetError().m_errorMessage.c_str());
  862. AZStd::string localBaselineFolder = pathOutcome.IsSuccess() ? pathOutcome.GetValue() : "";
  863. message = "Destination: " + localBaselineFolder + "\n";
  864. if (successCount > 0)
  865. {
  866. message += AZStd::string::format("Successfully copied %d files.\n", successCount);
  867. }
  868. if (failureCount > 0)
  869. {
  870. message += AZStd::string::format("Failed to copy %d files.\n", failureCount);
  871. }
  872. }
  873. m_messageBox.OpenPopupMessage("Update Baseline Image(s) Result", message);
  874. }
  875. void ScriptReporter::CheckLatestScreenshot(const ImageComparisonToleranceLevel* toleranceLevel)
  876. {
  877. AZ_Assert(GetCurrentScriptReport(), "There is no active script");
  878. if (GetCurrentScriptReport() == nullptr || GetCurrentScriptReport()->m_screenshotTests.empty())
  879. {
  880. ReportScriptError("CheckLatestScreenshot() did not find any screenshots to check.");
  881. return;
  882. }
  883. ScreenshotTestInfo& screenshotTestInfo = GetCurrentScriptReport()->m_screenshotTests.back();
  884. if (toleranceLevel == nullptr)
  885. {
  886. screenshotTestInfo.m_officialComparisonResult.m_resultCode = ImageComparisonResult::ResultCode::NullImageComparisonToleranceLevel;
  887. ReportScriptError("Screenshot check failed. No ImageComparisonToleranceLevel provided.");
  888. return;
  889. }
  890. auto io = AZ::IO::LocalFileIO::GetInstance();
  891. screenshotTestInfo.m_toleranceLevel = *toleranceLevel;
  892. static constexpr float ImperceptibleDiffFilter = 0.01;
  893. if (screenshotTestInfo.m_officialBaselineScreenshotFilePath.empty()
  894. || !io->Exists(screenshotTestInfo.m_officialBaselineScreenshotFilePath.c_str()))
  895. {
  896. ReportScriptError(AZStd::string::format("Screenshot check failed. Could not determine expected screenshot path for '%s'", screenshotTestInfo.m_screenshotFilePath.c_str()));
  897. screenshotTestInfo.m_officialComparisonResult.m_resultCode = ImageComparisonResult::ResultCode::FileNotFound;
  898. }
  899. else
  900. {
  901. AZ::Render::FrameCaptureComparisonOutcome compOutcome;
  902. AZ::Render::FrameCaptureTestRequestBus::BroadcastResult(
  903. compOutcome,
  904. &AZ::Render::FrameCaptureTestRequestBus::Events::CompareScreenshots,
  905. screenshotTestInfo.m_screenshotFilePath,
  906. screenshotTestInfo.m_officialBaselineScreenshotFilePath,
  907. ImperceptibleDiffFilter
  908. );
  909. screenshotTestInfo.m_officialComparisonResult.m_diffScore = 0.0;
  910. if (compOutcome.IsSuccess())
  911. {
  912. screenshotTestInfo.m_officialComparisonResult.m_diffScore = toleranceLevel->m_filterImperceptibleDiffs
  913. ? compOutcome.GetValue().m_diffScore
  914. : compOutcome.GetValue().m_filteredDiffScore;
  915. if (screenshotTestInfo.m_officialComparisonResult.m_diffScore <= toleranceLevel->m_threshold)
  916. {
  917. screenshotTestInfo.m_officialComparisonResult.m_resultCode = ImageComparisonResult::ResultCode::Pass;
  918. }
  919. else
  920. {
  921. // Be aware there is an automation test script that looks for the "Screenshot check failed. Diff score" string text to report failures.
  922. // If you change this message, be sure to update the associated tests as well located here: "C:/path/to/Lumberyard/AtomSampleViewer/Standalone/PythonTests"
  923. ReportScreenshotComparisonIssue(
  924. AZStd::string::format("Screenshot check failed. Diff score %f exceeds threshold of %f ('%s').",
  925. screenshotTestInfo.m_officialComparisonResult.m_diffScore, toleranceLevel->m_threshold, toleranceLevel->m_name.c_str()),
  926. screenshotTestInfo.m_officialBaselineScreenshotFilePath,
  927. screenshotTestInfo.m_screenshotFilePath,
  928. TraceLevel::Error);
  929. screenshotTestInfo.m_officialComparisonResult.m_resultCode = ImageComparisonResult::ResultCode::ThresholdExceeded;
  930. }
  931. }
  932. }
  933. if (screenshotTestInfo.m_localBaselineScreenshotFilePath.empty()
  934. || !io->Exists(screenshotTestInfo.m_localBaselineScreenshotFilePath.c_str()))
  935. {
  936. ReportScriptWarning(AZStd::string::format("Screenshot check failed. Could not determine local baseline screenshot path for '%s'", screenshotTestInfo.m_screenshotFilePath.c_str()));
  937. screenshotTestInfo.m_localComparisonResult.m_resultCode = ImageComparisonResult::ResultCode::FileNotFound;
  938. }
  939. else
  940. {
  941. // Local screenshots should be expected match 100% every time, otherwise warnings are reported. This will help developers track and investigate changes,
  942. // for example if they make local changes that impact some unrelated AtomSampleViewer sample in an unexpected way, they will see a warning about this.
  943. AZ::Render::FrameCaptureComparisonOutcome compOutcome;
  944. AZ::Render::FrameCaptureTestRequestBus::BroadcastResult(
  945. compOutcome,
  946. &AZ::Render::FrameCaptureTestRequestBus::Events::CompareScreenshots,
  947. screenshotTestInfo.m_screenshotFilePath,
  948. screenshotTestInfo.m_localBaselineScreenshotFilePath,
  949. ImperceptibleDiffFilter
  950. );
  951. screenshotTestInfo.m_localComparisonResult.m_diffScore = 0.0f;
  952. if (compOutcome.IsSuccess())
  953. {
  954. screenshotTestInfo.m_localComparisonResult.m_diffScore = compOutcome.GetValue().m_diffScore;
  955. if (screenshotTestInfo.m_localComparisonResult.m_diffScore == 0.0f)
  956. {
  957. screenshotTestInfo.m_localComparisonResult.m_resultCode = ImageComparisonResult::ResultCode::Pass;
  958. }
  959. else
  960. {
  961. ReportScreenshotComparisonIssue(
  962. AZStd::string::format("Screenshot check failed. Screenshot does not match the local baseline; something has changed. Diff score is %f.", screenshotTestInfo.m_localComparisonResult.m_diffScore),
  963. screenshotTestInfo.m_localBaselineScreenshotFilePath,
  964. screenshotTestInfo.m_screenshotFilePath,
  965. TraceLevel::Warning);
  966. screenshotTestInfo.m_localComparisonResult.m_resultCode = ImageComparisonResult::ResultCode::ThresholdExceeded;
  967. }
  968. }
  969. }
  970. }
  971. void ScriptReporter::ExportTestResults()
  972. {
  973. m_exportedTestResultsPath = GenerateAndCreateExportedTestResultsPath();
  974. for (const ScriptReport& scriptReport : m_scriptReports)
  975. {
  976. const AZStd::string assertLogLine = AZStd::string::format("Asserts: %u \n", scriptReport.m_assertCount);
  977. const AZStd::string errorsLogLine = AZStd::string::format("Errors: %u \n", scriptReport.m_generalErrorCount);
  978. const AZStd::string warningsLogLine = AZStd::string::format("Warnings: %u \n", scriptReport.m_generalWarningCount);
  979. const AZStd::string screenshotErrorsLogLine = AZStd::string::format("Screenshot errors: %u \n", scriptReport.m_screenshotErrorCount);
  980. const AZStd::string screenshotWarningsLogLine = AZStd::string::format("Screenshot warnings: %u \n", scriptReport.m_screenshotWarningCount);
  981. const AZStd::string failedScreenshotsLogLine = "\nScreenshot test info below.\n";
  982. AZ::IO::HandleType logHandle;
  983. auto io = AZ::IO::LocalFileIO::GetInstance();
  984. if (io->Open(m_exportedTestResultsPath.c_str(), AZ::IO::OpenMode::ModeWrite, logHandle))
  985. {
  986. io->Write(logHandle, assertLogLine.c_str(), assertLogLine.size());
  987. io->Write(logHandle, errorsLogLine.c_str(), errorsLogLine.size());
  988. io->Write(logHandle, warningsLogLine.c_str(), warningsLogLine.size());
  989. io->Write(logHandle, screenshotErrorsLogLine.c_str(), screenshotErrorsLogLine.size());
  990. io->Write(logHandle, screenshotWarningsLogLine.c_str(), screenshotWarningsLogLine.size());
  991. io->Write(logHandle, failedScreenshotsLogLine.c_str(), failedScreenshotsLogLine.size());
  992. for (const ScreenshotTestInfo& screenshotTest : scriptReport.m_screenshotTests)
  993. {
  994. const AZStd::string screenshotPath = AZStd::string::format("Test screenshot path: %s \n", screenshotTest.m_screenshotFilePath.c_str());
  995. const AZStd::string officialBaselineScreenshotPath = AZStd::string::format("Official baseline screenshot path: %s \n", screenshotTest.m_officialBaselineScreenshotFilePath.c_str());
  996. const AZStd::string toleranceLevelLogLine = AZStd::string::format("Tolerance level: %s \n", screenshotTest.m_toleranceLevel.ToString().c_str());
  997. const AZStd::string officialComparisonLogLine = AZStd::string::format("Image comparison result: %s \n", screenshotTest.m_officialComparisonResult.GetSummaryString().c_str());
  998. io->Write(logHandle, toleranceLevelLogLine.c_str(), toleranceLevelLogLine.size());
  999. io->Write(logHandle, officialComparisonLogLine.c_str(), officialComparisonLogLine.size());
  1000. }
  1001. io->Close(logHandle);
  1002. }
  1003. m_messageBox.OpenPopupMessage("Exported test results", AZStd::string::format("Results exported to %s", m_exportedTestResultsPath.c_str()));
  1004. AZ_Printf("Test results exported to %s \n", m_exportedTestResultsPath.c_str());
  1005. }
  1006. }
  1007. void ScriptReporter::ExportImageDiff(const char* filePath, const ScreenshotTestInfo& screenshotTestInfo)
  1008. {
  1009. using namespace AZ::Utils;
  1010. PngFile officialBaseline = PngFile::Load(screenshotTestInfo.m_officialBaselineScreenshotFilePath.c_str());
  1011. PngFile actualScreenshot = PngFile::Load(screenshotTestInfo.m_screenshotFilePath.c_str());
  1012. const size_t bufferSize = officialBaseline.GetBuffer().size();
  1013. AZStd::vector<uint8_t> diffBuffer = AZStd::vector<uint8_t>(bufferSize);
  1014. GenerateImageDiff(officialBaseline.GetBuffer(), actualScreenshot.GetBuffer(), diffBuffer);
  1015. AZStd::vector<uint8_t> buffer = AZStd::vector<uint8_t>(bufferSize * 3);
  1016. memcpy(buffer.data(), officialBaseline.GetBuffer().data(), bufferSize);
  1017. memcpy(buffer.data() + bufferSize, actualScreenshot.GetBuffer().data(), bufferSize);
  1018. memcpy(buffer.data() + bufferSize * 2, diffBuffer.data(), bufferSize);
  1019. PngFile imageDiff = PngFile::Create(AZ::RHI::Size(officialBaseline.GetWidth(), officialBaseline.GetHeight() * 3, 1), AZ::RHI::Format::R8G8B8A8_UNORM, buffer);
  1020. imageDiff.Save(filePath);
  1021. }
  1022. AZStd::string ScriptReporter::ExportImageDiff(const ScriptReport& scriptReport, const ScreenshotTestInfo& screenshotTest)
  1023. {
  1024. const AZStd::string imageDiffPath = GenerateAndCreateExportedImageDiffPath(scriptReport, screenshotTest);
  1025. ExportImageDiff(imageDiffPath.c_str(), screenshotTest);
  1026. return imageDiffPath;
  1027. }
  1028. AZStd::string ScriptReporter::GenerateAndCreateExportedTestResultsPath() const
  1029. {
  1030. // Setup our variables for the exported test results path and .txt file.
  1031. const auto projectPath = AZ::Utils::GetProjectPath();
  1032. const AZStd::string exportFileName = AZStd::string::format("exportedTestResults_%s.txt", m_uniqueTimestamp.c_str());
  1033. AZStd::string exportTestResultsFolder;
  1034. AzFramework::StringFunc::Path::Join(projectPath.c_str(), TestResultsFolder, exportTestResultsFolder);
  1035. // Create the exported test results path & return .txt file path.
  1036. auto io = AZ::IO::LocalFileIO::GetInstance();
  1037. io->CreatePath(exportTestResultsFolder.c_str());
  1038. AZStd::string exportFile;
  1039. AzFramework::StringFunc::Path::Join(exportTestResultsFolder.c_str(), exportFileName.c_str(), exportFile);
  1040. return exportFile;
  1041. }
  1042. void ScriptReporter::GenerateImageDiff(AZStd::span<const uint8_t> img1, AZStd::span<const uint8_t> img2, AZStd::vector<uint8_t>& buffer)
  1043. {
  1044. static constexpr size_t BytesPerPixel = 4;
  1045. static constexpr float MinDiffFilter = 0.01;
  1046. static constexpr uint8_t DefaultPixelValue = 122;
  1047. memset(buffer.data(), DefaultPixelValue, buffer.size() * sizeof(uint8_t));
  1048. for (size_t i = 0; i < img1.size(); i += BytesPerPixel)
  1049. {
  1050. const int16_t maxDiff = AZ::Utils::CalcMaxChannelDifference(img1, img2, i);
  1051. if (maxDiff / 255.0f > MinDiffFilter)
  1052. {
  1053. buffer[i] = aznumeric_cast<uint8_t>(maxDiff);
  1054. buffer[i + 1] = 0;
  1055. buffer[i + 2] = 0;
  1056. }
  1057. buffer[i + 3] = 255;
  1058. }
  1059. }
  1060. void ScriptReporter::HighlightColorSettings::UpdateColorSettings()
  1061. {
  1062. const ImVec4& bgColor = ImGui::GetStyleColorVec4(ImGuiCol_WindowBg);
  1063. const bool isDarkStyle = bgColor.x < 0.2 && bgColor.y < 0.2 && bgColor.z < 0.2;
  1064. m_highlightPassed = isDarkStyle ? ImVec4{ 0.5, 1, 0.5, 1 } : ImVec4{ 0, 0.75, 0, 1 };
  1065. m_highlightFailed = isDarkStyle ? ImVec4{ 1, 0.5, 0.5, 1 } : ImVec4{ 0.75, 0, 0, 1 };
  1066. m_highlightWarning = isDarkStyle ? ImVec4{ 1, 1, 0.5, 1 } : ImVec4{ 0.5, 0.5, 0, 1 };
  1067. }
  1068. } // namespace AtomSampleViewer