ScriptReporter.cpp 56 KB

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