ScriptReporter.cpp 57 KB

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