From 43075992d4314b7913d4c60031fb5f8fc93e30ea Mon Sep 17 00:00:00 2001 From: AshokThangavel Date: Thu, 4 Dec 2025 18:51:59 +0530 Subject: [PATCH 1/4] feat: Add JSON, YAML, and Toon output options for zpm test results --- src/cls/IPM/Main.cls | 1 + src/cls/IPM/ResourceProcessor/Test.cls | 41 +++++++-- src/cls/IPM/Test/Abstract.cls | 68 ++++++++++++++ src/cls/IPM/Test/JsonOutput.cls | 89 +++++++++++++++++++ src/cls/IPM/Test/ToonOutput.cls | 75 ++++++++++++++++ src/cls/IPM/Test/YamlOutput.cls | 83 +++++++++++++++++ .../TestResultsOPFormatAndFileGenTest.cls | 60 +++++++++++++ 7 files changed, 408 insertions(+), 9 deletions(-) create mode 100644 src/cls/IPM/Test/Abstract.cls create mode 100644 src/cls/IPM/Test/JsonOutput.cls create mode 100644 src/cls/IPM/Test/ToonOutput.cls create mode 100644 src/cls/IPM/Test/YamlOutput.cls create mode 100644 tests/unit_tests/Test/PM/Unit/TestResultsOPFormatAndFileGenTest.cls diff --git a/src/cls/IPM/Main.cls b/src/cls/IPM/Main.cls index a28b2ad2d..f0bf7ee47 100644 --- a/src/cls/IPM/Main.cls +++ b/src/cls/IPM/Main.cls @@ -75,6 +75,7 @@ Can also specify desired version to update to. + diff --git a/src/cls/IPM/ResourceProcessor/Test.cls b/src/cls/IPM/ResourceProcessor/Test.cls index efb500c85..6366e826e 100644 --- a/src/cls/IPM/ResourceProcessor/Test.cls +++ b/src/cls/IPM/ResourceProcessor/Test.cls @@ -189,18 +189,41 @@ Method OnPhase( zkill ^UnitTestRoot $$$ThrowOnError(tSC) - if $data(pParams("UnitTest","JUnitOutput"),tJUnitFile) { - set tPostfix = "-"_$zconvert(pPhase,"L")_"-" - if (..Package '= "") { - set tPostfix = tPostfix_$replace(..Package,".","-")_"-PKG" - } elseif (..Class '= "") { - set tPostfix = tPostfix_$replace(..Class,".","-")_"-CLS" + if $data(pParams("UnitTest"))>1 { + set outputType="" + for { + set outputType = $order(pParams("UnitTest",outputType),1,fileName) + quit:outputType="" + set tPostfix = "-"_$$$lcase(pPhase)_"-" + if (..Package '= "") { + set tPostfix = tPostfix_$replace(..Package,".","-")_"-PKG" + } elseif (..Class '= "") { + set tPostfix = tPostfix_$replace(..Class,".","-")_"-CLS" + } + set outputClass = "%IPM.Test."_outputType + if '$$$defClassDefined(outputClass) { + $$$ThrowOnError($$$ERROR($$$GeneralError,"The requested "_outputType_" output format does not exist.")) + } + set extension = $select(outputType="JsonOutput":".json",outputType="ToonOutput":".toon",outputType="YamlOutput":".yaml",1:".xml") + set fileName = $piece(fileName,".",1,*-1)_tPostfix_extension + set tSC = $classmethod(outputClass,"ToFile",fileName) + $$$ThrowOnError(tSC) + } + } + write ! + if $data(pParams("outputformat"),outputFormat)||('tVerbose) { + write !,"Test result summary",! + // TODO: Move this default format to ^IPM.Config.Test("outputFormat") rather than keeping it hardcoded. + set:$get(outputFormat)="" outputFormat="Toon" + set outputClass = "%IPM.Test."_$zconvert(outputFormat,"w")_"Output" + if '$$$defClassDefined(outputClass) { + $$$ThrowOnError($$$ERROR($$$GeneralError,"The requested "_outputType_" output format does not exist.")) } - set tJUnitFile = $piece(tJUnitFile,".",1,*-1)_tPostfix_".xml" - set tSC = ##class(%IPM.Test.JUnitOutput).ToFile(tJUnitFile) + set defaultTestStatus = "failed" + set tSC = $classmethod(outputClass,"OutputToDevice",,defaultTestStatus) $$$ThrowOnError(tSC) + write ! } - // By default, detect and report unit test failures as an error from this phase if $get(pParams("UnitTest","FailuresAreFatal"),1) { do ##class(%IPM.Test.Manager).OutputFailures() diff --git a/src/cls/IPM/Test/Abstract.cls b/src/cls/IPM/Test/Abstract.cls new file mode 100644 index 000000000..e1a4b239e --- /dev/null +++ b/src/cls/IPM/Test/Abstract.cls @@ -0,0 +1,68 @@ +/// The class serves as the base class for all the unit test result formatting. +Class %IPM.Test.Abstract Extends %RegisteredObject +{ + +ClassMethod ToFile( + pFileName As %String, + pCaseStatus As %String = "", + pTestIndex As %Integer = {$order(^UnitTest.Result(""),-1)}) As %Status [ Abstract ] +{ +} + +ClassMethod OutputToDevice( + pTestIndex As %Integer = {$order(^UnitTest.Result(""),-1)}, + pTestStatus As %String = "") [ Abstract ] +{ +} + +Query FilteredTestResults( + pInstance As %Integer, + pTestStatus) As %SQLQuery(ROWSPEC = "TotalCounts:%Integer,namespace:%String,duration:%String,testDateTime:%String,suiteName:%String,testcaseName:%String,methodName:%String,testMethod:%String,assertAction:%String,assertCounter:%Integer,assertDescription:%String,assertLocation:%String", SELECTMODE = "DISPLAY") +{ +SELECT +count(*) as TotalCounts, +tinstance.Namespace AS namespace, +tinstance.Duration AS duration, +tinstance.DateTime AS testDateTime, +tsuite.Name AS suiteName, +tcase.Name AS testcaseName, +tmethod.Name AS methodName, +tassert.TestMethod AS testMethod, +tassert.Action AS assertAction, +tassert.Counter AS assertCounter, +tassert.Description AS assertDescription, +tassert.Location AS assertLocation +FROM +%UnitTest_Result.TestInstance tinstance +JOIN %UnitTest_Result.TestSuite tsuite ON tsuite.TestInstance=tinstance.ID +JOIN %UnitTest_Result.TestCase tcase ON tcase.TestSuite=tsuite.ID +JOIN %UnitTest_Result.TestMethod tmethod ON tmethod.TestCase=tcase.ID +JOIN %UnitTest_Result.TestAssert tassert ON tassert.TestMethod=tmethod.ID +WHERE tinstance.ID=:pInstance AND tassert.Status=:pTestStatus +} + +Query GetAllTestResults(pInstance As %Integer) As %SQLQuery(ROWSPEC = "TotalCounts:%String,namespace:%String,duration:%String,testDateTime:%String,suiteName:%String,testcaseName:%String,methodName:%String,testMethod:%String,assertAction:%String,assertCounter:%Integer,assertDescription:%String,assertLocation:%String", SELECTMODE = "DISPLAY") +{ +SELECT +count(*) as TotalCounts, +tinstance.Namespace AS namespace, +tinstance.Duration AS duration, +tinstance.DateTime AS testDateTime, +tsuite.Name AS suiteName, +tcase.Name AS testcaseName, +tmethod.Name AS methodName, +tassert.TestMethod AS testMethod, +tassert.Action AS assertAction, +tassert.Counter AS assertCounter, +tassert.Description AS assertDescription, +tassert.Location AS assertLocation +FROM +%UnitTest_Result.TestInstance tinstance +JOIN %UnitTest_Result.TestSuite tsuite ON tsuite.TestInstance=tinstance.ID +JOIN %UnitTest_Result.TestCase tcase ON tcase.TestSuite=tsuite.ID +JOIN %UnitTest_Result.TestMethod tmethod ON tmethod.TestCase=tcase.ID +JOIN %UnitTest_Result.TestAssert tassert ON tassert.TestMethod=tmethod.ID +WHERE tinstance.ID=:pInstance +} + +} diff --git a/src/cls/IPM/Test/JsonOutput.cls b/src/cls/IPM/Test/JsonOutput.cls new file mode 100644 index 000000000..6c6f0e38a --- /dev/null +++ b/src/cls/IPM/Test/JsonOutput.cls @@ -0,0 +1,89 @@ +Class %IPM.Test.JsonOutput Extends %IPM.Test.Abstract +{ + +ClassMethod ToFile( + pFileName As %String, + pTestStatus As %String = "", + pTestIndex As %Integer = {$order(^UnitTest.Result(""),-1)}) As %Status +{ + set tSC = $$$OK + try { + set fileStream = ##class(%Stream.FileCharacter).%New() + set fileStream.TranslateTable="UTF8" + do fileStream.LinkToFile(pFileName) + set responseJson = ..JSON(pTestIndex, pTestStatus) + do fileStream.Write(responseJson.%ToJSON()) + $$$ThrowOnError(fileStream.%Save()) + } catch e { + set tSC = e.AsStatus() + } + return tSC +} + +ClassMethod OutputToDevice( + pTestIndex As %Integer = {$order(^UnitTest.Result(""),-1)}, + pCaseStatus As %String = "") As %Status +{ + set tSC = $$$OK + try { + set responseJson= ..JSON(pTestIndex, pCaseStatus) + write ! + do responseJson.%ToJSON() + } catch e { + set tSC = e.AsStatus() + } + return tSC +} + +ClassMethod JSON( + pTestIndex, + pTestStatus) As %DynamicObject +{ + if pTestStatus'=""{ + set tResult = ..FilteredTestResultsFunc(pTestIndex,pTestStatus) + } else { + set tResult = ..GetAllTestResultsFunc(pTestIndex) + } + set unitTest = {} + set unitTest.results = [] + set (previousID,currentSuite,currentTestcase,suiteObj,testcaseObj) = "" + + while tResult.%Next() { + if previousID = "" { + set unitTest.id = pTestIndex + set unitTest.namespace = tResult.namespace + set unitTest.duration = tResult.duration + set unitTest.testDateTime = tResult.testDateTime + } + set previousID = pTestIndex + if tResult.suiteName '= currentSuite { + set currentSuite = tResult.suiteName + set suiteObj = { + "suiteName": (currentSuite), + "testcases": [] + } + do unitTest.results.%Push(suiteObj) + set currentTestcase = "" + } + if tResult.testcaseName '= currentTestcase { + set currentTestcase = tResult.testcaseName + set testcaseObj = { + "testcaseName": (currentTestcase), + "methods": [] + } + do suiteObj.testcases.%Push(testcaseObj) + } + set methodObj = { + "methodName": (tResult.methodName), + "testMethod": (tResult.testMethod), + "assertAction": (tResult.assertAction), + "assertCounter": (tResult.assertCounter), + "assertDescription": (tResult.assertDescription), + "assertLocation": (tResult.assertLocation) + } + do testcaseObj.methods.%Push(methodObj) + } + return unitTest +} + +} diff --git a/src/cls/IPM/Test/ToonOutput.cls b/src/cls/IPM/Test/ToonOutput.cls new file mode 100644 index 000000000..5a689862a --- /dev/null +++ b/src/cls/IPM/Test/ToonOutput.cls @@ -0,0 +1,75 @@ +Class %IPM.Test.ToonOutput Extends %IPM.Test.Abstract +{ + +ClassMethod ToFile( + pFileName As %String, + pTestStatus As %String = "", + pTestIndex As %Integer = {$order(^UnitTest.Result(""),-1)}) As %Status +{ + set tSC = $$$OK + try { + set fileStream = ##class(%Stream.FileCharacter).%New() + set fileStream.TranslateTable="UTF8" + do fileStream.LinkToFile(pFileName) + if pTestStatus'=""{ + set tResult = ..FilteredTestResultsFunc(pTestIndex, pTestStatus) + } else { + set tResult = ..GetAllTestResultsFunc(pTestIndex) + } + set currentID="" + while tResult.%Next() { + if currentID = "" { + set currentID = pTestIndex + do fileStream.WriteLine("unitTest:") + do fileStream.WriteLine(" id: "_pTestIndex) + do fileStream.WriteLine(" namespace: "_tResult.namespace) + do fileStream.WriteLine(" duration: "_tResult.duration) + do fileStream.WriteLine(" testDateTime: "_tResult.testDateTime) + do fileStream.WriteLine() + do fileStream.WriteLine("results["_tResult.TotalCounts_"]{suiteName,testcaseName,methodName,status,assertAction,assertCounter,assertDescription,assertLocation}:") + } + set data = " "_tResult.suiteName_","_tResult.testcaseName_","_tResult.methodName_","_pTestIndex_","_ + tResult.assertAction_","_tResult.assertCounter_","""_$translate(tResult.assertDescription,"""")_""","""_tResult.assertLocation_"""" + do fileStream.WriteLine(data) + } + $$$ThrowOnError(fileStream.%Save()) + } catch e { + set tSC = e.AsStatus() + } + return tSC +} + +ClassMethod OutputToDevice( + pTestIndex As %Integer = {$order(^UnitTest.Result(""),-1)}, + pTestStatus As %String = "") As %Status +{ + set tSC = $$$OK + try { + if pTestStatus'=""{ + set tResult = ..FilteredTestResultsFunc(pTestIndex, pTestStatus) + } else { + set tResult = ..GetAllTestResultsFunc(pTestIndex) + } + set currentID="" + while tResult.%Next() { + if currentID = "" { + set currentID = pTestIndex + write !,"unitTest:" + write !," id: "_pTestIndex + write !," namespace: "_tResult.namespace + write !," duration: "_tResult.duration + write !," testDateTime: "_tResult.testDateTime + write ! + write !,"results["_tResult.TotalCounts_"]{suiteName,testcaseName,methodName,status,assertAction,assertCounter,assertDescription,assertLocation}:" + } + set data = " "_tResult.suiteName_","_tResult.testcaseName_","_tResult.methodName_","_pTestIndex_","_ + tResult.assertAction_","_tResult.assertCounter_","""_$translate(tResult.assertDescription,"""")_""","""_tResult.assertLocation_"""" + write !,data + } + } catch e { + set tSC = e.AsStatus() + } + return tSC +} + +} diff --git a/src/cls/IPM/Test/YamlOutput.cls b/src/cls/IPM/Test/YamlOutput.cls new file mode 100644 index 000000000..019c59755 --- /dev/null +++ b/src/cls/IPM/Test/YamlOutput.cls @@ -0,0 +1,83 @@ +Class %IPM.Test.YamlOutput Extends %IPM.Test.Abstract +{ + +ClassMethod ToFile( + pFileName As %String, + pTestStatus As %String = "", + pTestIndex As %Integer = {$order(^UnitTest.Result(""),-1)}) As %Status +{ + set tSC = $$$OK + try { + set fileStream = ##class(%Stream.FileCharacter).%New() + set fileStream.TranslateTable="UTF8" + do fileStream.LinkToFile(pFileName) + do fileStream.CopyFrom(..YAML(pTestIndex, pTestStatus)) + $$$ThrowOnError(fileStream.%Save()) + } catch e { + set tSC = e.AsStatus() + } + return tSC +} + +ClassMethod OutputToDevice( + pTestIndex As %Integer = {$order(^UnitTest.Result(""),-1)}, + pTestStatus As %String = "") As %Status +{ + set tSC = $$$OK + try { + set yamlStream = ..YAML(pTestIndex, pTestStatus) + write ! + while 'yamlStream.AtEnd { + write yamlStream.Read() + } + } catch e { + set tSC = e.AsStatus() + } + return tSC +} + +ClassMethod YAML( + pTestIndex = {$order(^UnitTest.Result(""),-1)}, + pTestStatus As %String = "") As %Stream.TmpCharacter +{ + if pTestStatus'=""{ + set tResult= ..FilteredTestResultsFunc(pTestIndex, pTestStatus) + } else { + set tResult= ..GetAllTestResultsFunc(pTestIndex) + } + set yamlStream = ##class(%Stream.TmpCharacter).%New() + set (yaml,currentID,currentSuite,currentTestcase) = "" + while tResult.%Next() { + if currentID = "" { + set currentID = pTestIndex + do yamlStream.WriteLine("unitTest:") + do yamlStream.WriteLine(" id: "_pTestIndex) + do yamlStream.WriteLine(" namespace: """_tResult.namespace_"""") + do yamlStream.WriteLine(" duration: "_tResult.duration) + do yamlStream.WriteLine(" testDateTime: """_tResult.testDateTime_"""") + do yamlStream.WriteLine( "") + do yamlStream.WriteLine(" results:") + } + if tResult.suiteName '= currentSuite { + set currentSuite = tResult.suiteName + set currentTestcase = "" + do yamlStream.WriteLine(" - suiteName: """_tResult.suiteName_"""") + do yamlStream.WriteLine(" testcases:") + } + if tResult.testcaseName '= currentTestcase { + set currentTestcase = tResult.testcaseName + do yamlStream.WriteLine(" - testcaseName: """_tResult.testcaseName_"""") + do yamlStream.WriteLine(" methods:") + } + do yamlStream.WriteLine(" - methodName: """_tResult.methodName_"""") + //do yamlStream.WriteLine(" testMethod: """_tResult.testMethod_"""") + do yamlStream.WriteLine(" assertAction: """_tResult.assertAction_"""") + do yamlStream.WriteLine(" assertCounter: "_tResult.assertCounter) + //do yamlStream.WriteLine(" assertDescription: |") + //do yamlStream.WriteLine(" "_$replace(tResult.assertDescription, $c(10), $c(10)_" ")) + do yamlStream.WriteLine(" assertLocation: """_tResult.assertLocation_"""") + } + return yamlStream +} + +} diff --git a/tests/unit_tests/Test/PM/Unit/TestResultsOPFormatAndFileGenTest.cls b/tests/unit_tests/Test/PM/Unit/TestResultsOPFormatAndFileGenTest.cls new file mode 100644 index 000000000..abe8abc9b --- /dev/null +++ b/tests/unit_tests/Test/PM/Unit/TestResultsOPFormatAndFileGenTest.cls @@ -0,0 +1,60 @@ +/// Unit Test Class to validate the ZPM 'test' command output configuration. +/// This class ensures two primary functions work correctly: +/// 1. Console Formatting: Verifies the `-f` / `-output` flags correctly +/// format test results for terminal display (e.g., YAML, JSON). +/// 2. File Generation: Verifies the `-DUnitTest.Output` definitions +/// successfully create and populate the structured results files (e.g., .json, .yaml, .toon). +Class Test.PM.Unit.TestResultsOPFormatAndFileGenTest Extends %UnitTest.TestCase +{ + +/// generate .yaml,.json,.toon files +Method TestResultFileGeneration() +{ + #define NormalizeFilename(%file) ##class(%File).NormalizeFilename(%file) + + do $$$LogMessage("This file generation picks the last or current unit test id and generate the reports") + set fileDir = ##class(%File).NormalizeDirectory($get(^UnitTestRoot)_"/test-reports/") + if '##class(%File).DirectoryExists(fileDir){ + set status = ##class(%File).CreateDirectoryChain(fileDir) + do $$$AssertStatusOK(status,"Directory created: "_fileDir) + } + do $$$LogMessage("Start generating the reports") + set fileName = $$$NormalizeFilename(fileDir_"/test.yaml") + set status = ##class(%IPM.Test.YamlOutput).ToFile(fileName) + do $$$AssertStatusOK(status,"yaml file generated successfully in "_fileDir) + + set fileName = $$$NormalizeFilename(fileDir_"/test.Json") + set status = ##class(%IPM.Test.JsonOutput).ToFile(fileName) + do $$$AssertStatusOK(status,"Json file generated successfully in "_fileDir) + + set fileName = $$$NormalizeFilename(fileDir_"/test.toon") + set status = ##class(%IPM.Test.ToonOutput).ToFile(fileName) + do $$$AssertStatusOK(status,"Toon file generated successfully in "_fileDir) + + do ..ShowGeneratedFilesAndCleaup(fileDir) + + //set status = ##class(%File).RemoveDirectory(fileDir) + //do $$$AssertStatusOK(status,"Deleted the directory "_fileDir) +} + +Method ShowGeneratedFilesAndCleaup(fileDir As %String) +{ + do $$$LogMessage("Display the generated unit test report files") + set fileSet = ##class(%File).FileSetFunc(fileDir) + while fileSet.%Next(){ + set file = fileSet.Name + set fileNames(file) = fileSet.ItemName + do $$$LogMessage("Generated file "_file) + } + do $$$LogMessage("Started Cleanup the generated unit test report files") + set file = "" + for { + set file = $order(fileNames(file),1,fileName) + quit:file="" + set status = ##class(%File).Delete(file) + do $$$AssertStatusOK(status," File '"_fileName_"' has been deleted successfully") + } + do $$$LogMessage("File cleanup completed") +} + +} From dbb7bed038e2171f9c8f562f45d4fc2320ca26ed Mon Sep 17 00:00:00 2001 From: AshokThangavel Date: Thu, 4 Dec 2025 19:13:32 +0530 Subject: [PATCH 2/4] docs: Add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 483e56198..58e4fe46e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - #938 Added flag -export-python-deps to package command - #462: The `repo` command for repository configuration now supports secret input terminal mode for passwords with the `-password-stdin` flag - #935: Adding a generic JFrog Artifactory tarball resource processor for bundling artifact with a package and deploying it to a final location on install. +- #971: Adds support for JSON, YAML, and Toon formats via the -f flag and new -DUnitTest.*Output directives. ### Changed - #316: All parameters, except developer mode, included with a `load`, `install` or `update` command will be propagated to dependencies From aef4c32364b5d2f155f7b95f057ad62d1655f73f Mon Sep 17 00:00:00 2001 From: AshokThangavel Date: Mon, 16 Mar 2026 22:30:51 +0530 Subject: [PATCH 3/4] refactor: updated the code based on comments --- preload/cls/IPM/Installer.cls | 1 + src/cls/IPM/Repo/UniversalSettings.cls | 17 ++++- src/cls/IPM/ResourceProcessor/Test.cls | 20 ++++-- src/cls/IPM/Test/Abstract.cls | 68 +++++++++---------- src/cls/IPM/Test/JsonOutput.cls | 54 ++++++++------- src/cls/IPM/Test/ToonOutput.cls | 56 +++++++-------- src/cls/IPM/Test/YamlOutput.cls | 56 +++++++-------- tests/unit_tests/Test/PM/Unit/CLI.cls | 12 ++++ .../TestResultsOPFormatAndFileGenTest.cls | 11 ++- 9 files changed, 168 insertions(+), 127 deletions(-) diff --git a/preload/cls/IPM/Installer.cls b/preload/cls/IPM/Installer.cls index 26970544b..02916635b 100644 --- a/preload/cls/IPM/Installer.cls +++ b/preload/cls/IPM/Installer.cls @@ -113,6 +113,7 @@ ClassMethod ZPMInit( $$$QuitOnError(##class(%IPM.Repo.UniversalSettings).SetValue("UseStandalonePip", "", 0)) $$$QuitOnError(##class(%IPM.Repo.UniversalSettings).SetValue("SemVerPostRelease", 0, 0)) $$$QuitOnError(##class(%IPM.Repo.UniversalSettings).SetValue("DefaultLogEntryLimit",20, 0)) + $$$QuitOnError(##class(%IPM.Repo.UniversalSettings).SetValue("TestReportFormat","toon", 0)) quit $$$OK } diff --git a/src/cls/IPM/Repo/UniversalSettings.cls b/src/cls/IPM/Repo/UniversalSettings.cls index e1ff1f3ed..f644c3dc7 100644 --- a/src/cls/IPM/Repo/UniversalSettings.cls +++ b/src/cls/IPM/Repo/UniversalSettings.cls @@ -45,7 +45,10 @@ Parameter SemVerPostRelease = "SemVerPostRelease"; /// to retain IPM history records before they are eligible for cleanup. Parameter HistoryRetain = "history_retain"; -Parameter CONFIGURABLE = "trackingId,analytics,ColorScheme,TerminalPrompt,PublishTimeout,PipCaller,UseStandalonePip,SemVerPostRelease,DefaultLogEntryLimit,HistoryRetain"; +/// Specifies the serialization format (JSON, TOON, YAML) for unit and integration test results in the shell. +Parameter TestReportFormat = "TestReportFormat"; + +Parameter CONFIGURABLE = "trackingId,analytics,ColorScheme,TerminalPrompt,PublishTimeout,PipCaller,UseStandalonePip,SemVerPostRelease,DefaultLogEntryLimit,HistoryRetain,TestReportFormat"; /// Returns configArray, that includes all configurable settings ClassMethod GetAll(Output configArray) As %Status @@ -190,4 +193,16 @@ ClassMethod GetHistoryRetain() As %Integer return ..GetValue(..#HistoryRetain) } +ClassMethod SetTestReportFormat( + val As %String, + overwrite As %Boolean = 1) As %Boolean +{ + return ..SetValue(..#TestReportFormat, val, overwrite) +} + +ClassMethod GetTestReportFormat() As %String +{ + return ..GetValue(..#TestReportFormat) +} + } diff --git a/src/cls/IPM/ResourceProcessor/Test.cls b/src/cls/IPM/ResourceProcessor/Test.cls index 6366e826e..070aba8f4 100644 --- a/src/cls/IPM/ResourceProcessor/Test.cls +++ b/src/cls/IPM/ResourceProcessor/Test.cls @@ -213,22 +213,30 @@ Method OnPhase( write ! if $data(pParams("outputformat"),outputFormat)||('tVerbose) { write !,"Test result summary",! - // TODO: Move this default format to ^IPM.Config.Test("outputFormat") rather than keeping it hardcoded. - set:$get(outputFormat)="" outputFormat="Toon" + set defaultOutputFormat = ##class(%IPM.Repo.UniversalSettings).GetTestReportFormat() + if defaultOutputFormat'="" { + set outputFormat = defaultOutputFormat + } + else { + if $get(outputFormat)="" { + set outputFormat="Toon" + } + } + set outputClass = "%IPM.Test."_$zconvert(outputFormat,"w")_"Output" if '$$$defClassDefined(outputClass) { $$$ThrowOnError($$$ERROR($$$GeneralError,"The requested "_outputType_" output format does not exist.")) } set defaultTestStatus = "failed" - set tSC = $classmethod(outputClass,"OutputToDevice",,defaultTestStatus) - $$$ThrowOnError(tSC) + set sc = $classmethod(outputClass,"OutputToDevice",,defaultTestStatus) + $$$ThrowOnError(sc) write ! } // By default, detect and report unit test failures as an error from this phase if $get(pParams("UnitTest","FailuresAreFatal"),1) { do ##class(%IPM.Test.Manager).OutputFailures() - set tSC = ##class(%IPM.Test.Manager).GetLastStatus() - $$$ThrowOnError(tSC) + set sc = ##class(%IPM.Test.Manager).GetLastStatus() + $$$ThrowOnError(sc) } write ! } diff --git a/src/cls/IPM/Test/Abstract.cls b/src/cls/IPM/Test/Abstract.cls index e1a4b239e..c64cc3817 100644 --- a/src/cls/IPM/Test/Abstract.cls +++ b/src/cls/IPM/Test/Abstract.cls @@ -3,66 +3,66 @@ Class %IPM.Test.Abstract Extends %RegisteredObject { ClassMethod ToFile( - pFileName As %String, - pCaseStatus As %String = "", - pTestIndex As %Integer = {$order(^UnitTest.Result(""),-1)}) As %Status [ Abstract ] + FileName As %String, + CaseStatus As %String = "", + TestIndex As %Integer = {$order(^UnitTest.Result(""),-1)}) As %Status [ Abstract ] { } ClassMethod OutputToDevice( - pTestIndex As %Integer = {$order(^UnitTest.Result(""),-1)}, - pTestStatus As %String = "") [ Abstract ] + TestIndex As %Integer = {$order(^UnitTest.Result(""),-1)}, + TestStatus As %String = "") [ Abstract ] { } Query FilteredTestResults( - pInstance As %Integer, - pTestStatus) As %SQLQuery(ROWSPEC = "TotalCounts:%Integer,namespace:%String,duration:%String,testDateTime:%String,suiteName:%String,testcaseName:%String,methodName:%String,testMethod:%String,assertAction:%String,assertCounter:%Integer,assertDescription:%String,assertLocation:%String", SELECTMODE = "DISPLAY") + Instance As %Integer, + TestStatus) As %SQLQuery(ROWSPEC = "TotalCounts:%Integer,namespace:%String,duration:%String,testDateTime:%String,suiteName:%String,testcaseName:%String,methodName:%String,testMethod:%String,assertAction:%String,assertCounter:%Integer,assertDescription:%String,assertLocation:%String", SELECTMODE = "DISPLAY") { SELECT -count(*) as TotalCounts, -tinstance.Namespace AS namespace, -tinstance.Duration AS duration, -tinstance.DateTime AS testDateTime, -tsuite.Name AS suiteName, -tcase.Name AS testcaseName, -tmethod.Name AS methodName, -tassert.TestMethod AS testMethod, -tassert.Action AS assertAction, -tassert.Counter AS assertCounter, -tassert.Description AS assertDescription, -tassert.Location AS assertLocation + count(*) as TotalCounts, + tinstance.Namespace AS namespace, + tinstance.Duration AS duration, + tinstance.DateTime AS testDateTime, + tsuite.Name AS suiteName, + tcase.Name AS testcaseName, + tmethod.Name AS methodName, + tassert.TestMethod AS testMethod, + tassert.Action AS assertAction, + tassert.Counter AS assertCounter, + tassert.Description AS assertDescription, + tassert.Location AS assertLocation FROM %UnitTest_Result.TestInstance tinstance JOIN %UnitTest_Result.TestSuite tsuite ON tsuite.TestInstance=tinstance.ID JOIN %UnitTest_Result.TestCase tcase ON tcase.TestSuite=tsuite.ID JOIN %UnitTest_Result.TestMethod tmethod ON tmethod.TestCase=tcase.ID JOIN %UnitTest_Result.TestAssert tassert ON tassert.TestMethod=tmethod.ID -WHERE tinstance.ID=:pInstance AND tassert.Status=:pTestStatus +WHERE tinstance.ID=:Instance AND tassert.Status=:TestStatus } -Query GetAllTestResults(pInstance As %Integer) As %SQLQuery(ROWSPEC = "TotalCounts:%String,namespace:%String,duration:%String,testDateTime:%String,suiteName:%String,testcaseName:%String,methodName:%String,testMethod:%String,assertAction:%String,assertCounter:%Integer,assertDescription:%String,assertLocation:%String", SELECTMODE = "DISPLAY") +Query GetAllTestResults(Instance As %Integer) As %SQLQuery(ROWSPEC = "TotalCounts:%String,namespace:%String,duration:%String,testDateTime:%String,suiteName:%String,testcaseName:%String,methodName:%String,testMethod:%String,assertAction:%String,assertCounter:%Integer,assertDescription:%String,assertLocation:%String", SELECTMODE = "DISPLAY") { SELECT -count(*) as TotalCounts, -tinstance.Namespace AS namespace, -tinstance.Duration AS duration, -tinstance.DateTime AS testDateTime, -tsuite.Name AS suiteName, -tcase.Name AS testcaseName, -tmethod.Name AS methodName, -tassert.TestMethod AS testMethod, -tassert.Action AS assertAction, -tassert.Counter AS assertCounter, -tassert.Description AS assertDescription, -tassert.Location AS assertLocation + count(*) as TotalCounts, + tinstance.Namespace AS namespace, + tinstance.Duration AS duration, + tinstance.DateTime AS testDateTime, + tsuite.Name AS suiteName, + tcase.Name AS testcaseName, + tmethod.Name AS methodName, + tassert.TestMethod AS testMethod, + tassert.Action AS assertAction, + tassert.Counter AS assertCounter, + tassert.Description AS assertDescription, + tassert.Location AS assertLocation FROM %UnitTest_Result.TestInstance tinstance JOIN %UnitTest_Result.TestSuite tsuite ON tsuite.TestInstance=tinstance.ID JOIN %UnitTest_Result.TestCase tcase ON tcase.TestSuite=tsuite.ID JOIN %UnitTest_Result.TestMethod tmethod ON tmethod.TestCase=tcase.ID JOIN %UnitTest_Result.TestAssert tassert ON tassert.TestMethod=tmethod.ID -WHERE tinstance.ID=:pInstance +WHERE tinstance.ID=:Instance } } diff --git a/src/cls/IPM/Test/JsonOutput.cls b/src/cls/IPM/Test/JsonOutput.cls index 6c6f0e38a..dac91487d 100644 --- a/src/cls/IPM/Test/JsonOutput.cls +++ b/src/cls/IPM/Test/JsonOutput.cls @@ -2,47 +2,51 @@ Class %IPM.Test.JsonOutput Extends %IPM.Test.Abstract { ClassMethod ToFile( - pFileName As %String, - pTestStatus As %String = "", - pTestIndex As %Integer = {$order(^UnitTest.Result(""),-1)}) As %Status + FileName As %String, + TestStatus As %String = "", + TestIndex As %Integer = {$order(^UnitTest.Result(""),-1)}) As %Status { - set tSC = $$$OK + set sc = $$$OK try { set fileStream = ##class(%Stream.FileCharacter).%New() - set fileStream.TranslateTable="UTF8" - do fileStream.LinkToFile(pFileName) - set responseJson = ..JSON(pTestIndex, pTestStatus) - do fileStream.Write(responseJson.%ToJSON()) + set fileStream.TranslateTable = "UTF8" + $$$ThrowOnError(fileStream.LinkToFile(FileName)) + set responseJson = ..JSON(TestIndex, TestStatus) + if $isobject(responseJson) { + do fileStream.Write(responseJson.%ToJSON()) + } $$$ThrowOnError(fileStream.%Save()) - } catch e { - set tSC = e.AsStatus() + } catch ex { + set sc = ex.AsStatus() } - return tSC + return sc } ClassMethod OutputToDevice( - pTestIndex As %Integer = {$order(^UnitTest.Result(""),-1)}, + TestIndex As %Integer = {$order(^UnitTest.Result(""),-1)}, pCaseStatus As %String = "") As %Status { - set tSC = $$$OK + set sc = $$$OK try { - set responseJson= ..JSON(pTestIndex, pCaseStatus) + set responseJson = ..JSON(TestIndex, pCaseStatus) write ! - do responseJson.%ToJSON() - } catch e { - set tSC = e.AsStatus() + if $isobject(responseJson) { + do responseJson.%ToJSON() + } + } catch ex { + set sc = ex.AsStatus() } - return tSC + return sc } ClassMethod JSON( - pTestIndex, - pTestStatus) As %DynamicObject + TestIndex As %Integer, + TestStatus As %String) As %DynamicObject { - if pTestStatus'=""{ - set tResult = ..FilteredTestResultsFunc(pTestIndex,pTestStatus) + if TestStatus'="" { + set tResult = ..FilteredTestResultsFunc(TestIndex, TestStatus) } else { - set tResult = ..GetAllTestResultsFunc(pTestIndex) + set tResult = ..GetAllTestResultsFunc(TestIndex) } set unitTest = {} set unitTest.results = [] @@ -50,12 +54,12 @@ ClassMethod JSON( while tResult.%Next() { if previousID = "" { - set unitTest.id = pTestIndex + set unitTest.id = TestIndex set unitTest.namespace = tResult.namespace set unitTest.duration = tResult.duration set unitTest.testDateTime = tResult.testDateTime } - set previousID = pTestIndex + set previousID = TestIndex if tResult.suiteName '= currentSuite { set currentSuite = tResult.suiteName set suiteObj = { diff --git a/src/cls/IPM/Test/ToonOutput.cls b/src/cls/IPM/Test/ToonOutput.cls index 5a689862a..f0826a21f 100644 --- a/src/cls/IPM/Test/ToonOutput.cls +++ b/src/cls/IPM/Test/ToonOutput.cls @@ -2,74 +2,76 @@ Class %IPM.Test.ToonOutput Extends %IPM.Test.Abstract { ClassMethod ToFile( - pFileName As %String, - pTestStatus As %String = "", - pTestIndex As %Integer = {$order(^UnitTest.Result(""),-1)}) As %Status + FileName As %String, + TestStatus As %String = "", + TestIndex As %Integer = {$order(^UnitTest.Result(""),-1)}) As %Status { - set tSC = $$$OK + set sc = $$$OK try { set fileStream = ##class(%Stream.FileCharacter).%New() - set fileStream.TranslateTable="UTF8" - do fileStream.LinkToFile(pFileName) - if pTestStatus'=""{ - set tResult = ..FilteredTestResultsFunc(pTestIndex, pTestStatus) + set fileStream.TranslateTable = "UTF8" + $$$ThrowOnError(fileStream.LinkToFile(FileName)) + + if TestStatus'="" { + set tResult = ..FilteredTestResultsFunc(TestIndex, TestStatus) } else { - set tResult = ..GetAllTestResultsFunc(pTestIndex) + set tResult = ..GetAllTestResultsFunc(TestIndex) } + set currentID="" while tResult.%Next() { if currentID = "" { - set currentID = pTestIndex + set currentID = TestIndex do fileStream.WriteLine("unitTest:") - do fileStream.WriteLine(" id: "_pTestIndex) + do fileStream.WriteLine(" id: "_TestIndex) do fileStream.WriteLine(" namespace: "_tResult.namespace) do fileStream.WriteLine(" duration: "_tResult.duration) do fileStream.WriteLine(" testDateTime: "_tResult.testDateTime) do fileStream.WriteLine() do fileStream.WriteLine("results["_tResult.TotalCounts_"]{suiteName,testcaseName,methodName,status,assertAction,assertCounter,assertDescription,assertLocation}:") } - set data = " "_tResult.suiteName_","_tResult.testcaseName_","_tResult.methodName_","_pTestIndex_","_ + set data = " "_tResult.suiteName_","_tResult.testcaseName_","_tResult.methodName_","_TestIndex_","_ tResult.assertAction_","_tResult.assertCounter_","""_$translate(tResult.assertDescription,"""")_""","""_tResult.assertLocation_"""" do fileStream.WriteLine(data) } $$$ThrowOnError(fileStream.%Save()) - } catch e { - set tSC = e.AsStatus() + } catch ex { + set sc = ex.AsStatus() } - return tSC + return sc } ClassMethod OutputToDevice( - pTestIndex As %Integer = {$order(^UnitTest.Result(""),-1)}, - pTestStatus As %String = "") As %Status + TestIndex As %Integer = {$order(^UnitTest.Result(""),-1)}, + TestStatus As %String = "") As %Status { - set tSC = $$$OK + set sc = $$$OK try { - if pTestStatus'=""{ - set tResult = ..FilteredTestResultsFunc(pTestIndex, pTestStatus) + if TestStatus'="" { + set tResult = ..FilteredTestResultsFunc(TestIndex, TestStatus) } else { - set tResult = ..GetAllTestResultsFunc(pTestIndex) + set tResult = ..GetAllTestResultsFunc(TestIndex) } set currentID="" while tResult.%Next() { if currentID = "" { - set currentID = pTestIndex + set currentID = TestIndex write !,"unitTest:" - write !," id: "_pTestIndex + write !," id: "_TestIndex write !," namespace: "_tResult.namespace write !," duration: "_tResult.duration write !," testDateTime: "_tResult.testDateTime write ! write !,"results["_tResult.TotalCounts_"]{suiteName,testcaseName,methodName,status,assertAction,assertCounter,assertDescription,assertLocation}:" } - set data = " "_tResult.suiteName_","_tResult.testcaseName_","_tResult.methodName_","_pTestIndex_","_ + set data = " "_tResult.suiteName_","_tResult.testcaseName_","_tResult.methodName_","_TestIndex_","_ tResult.assertAction_","_tResult.assertCounter_","""_$translate(tResult.assertDescription,"""")_""","""_tResult.assertLocation_"""" write !,data } - } catch e { - set tSC = e.AsStatus() + } catch ex { + set sc = ex.AsStatus() } - return tSC + return sc } } diff --git a/src/cls/IPM/Test/YamlOutput.cls b/src/cls/IPM/Test/YamlOutput.cls index 019c59755..bc6c1d229 100644 --- a/src/cls/IPM/Test/YamlOutput.cls +++ b/src/cls/IPM/Test/YamlOutput.cls @@ -2,60 +2,60 @@ Class %IPM.Test.YamlOutput Extends %IPM.Test.Abstract { ClassMethod ToFile( - pFileName As %String, - pTestStatus As %String = "", - pTestIndex As %Integer = {$order(^UnitTest.Result(""),-1)}) As %Status + FileName As %String, + TestStatus As %String = "", + TestIndex As %Integer = {$order(^UnitTest.Result(""),-1)}) As %Status { - set tSC = $$$OK + set sc = $$$OK try { set fileStream = ##class(%Stream.FileCharacter).%New() - set fileStream.TranslateTable="UTF8" - do fileStream.LinkToFile(pFileName) - do fileStream.CopyFrom(..YAML(pTestIndex, pTestStatus)) + set fileStream.TranslateTable = "UTF8" + $$$ThrowOnError(fileStream.LinkToFile(FileName)) + do fileStream.CopyFrom(..YAML(TestIndex, TestStatus)) $$$ThrowOnError(fileStream.%Save()) - } catch e { - set tSC = e.AsStatus() + } catch ex { + set sc = ex.AsStatus() } - return tSC + return sc } ClassMethod OutputToDevice( - pTestIndex As %Integer = {$order(^UnitTest.Result(""),-1)}, - pTestStatus As %String = "") As %Status + TestIndex As %Integer = {$order(^UnitTest.Result(""),-1)}, + TestStatus As %String = "") As %Status { - set tSC = $$$OK + set sc = $$$OK try { - set yamlStream = ..YAML(pTestIndex, pTestStatus) + set yamlStream = ..YAML(TestIndex, TestStatus) write ! while 'yamlStream.AtEnd { write yamlStream.Read() } - } catch e { - set tSC = e.AsStatus() + } catch ex { + set sc = ex.AsStatus() } - return tSC + return sc } ClassMethod YAML( - pTestIndex = {$order(^UnitTest.Result(""),-1)}, - pTestStatus As %String = "") As %Stream.TmpCharacter + TestIndex = {$order(^UnitTest.Result(""),-1)}, + TestStatus As %String = "") As %Stream.TmpCharacter { - if pTestStatus'=""{ - set tResult= ..FilteredTestResultsFunc(pTestIndex, pTestStatus) + if TestStatus'="" { + set tResult = ..FilteredTestResultsFunc(TestIndex, TestStatus) } else { - set tResult= ..GetAllTestResultsFunc(pTestIndex) + set tResult = ..GetAllTestResultsFunc(TestIndex) } set yamlStream = ##class(%Stream.TmpCharacter).%New() set (yaml,currentID,currentSuite,currentTestcase) = "" while tResult.%Next() { if currentID = "" { - set currentID = pTestIndex + set currentID = TestIndex do yamlStream.WriteLine("unitTest:") - do yamlStream.WriteLine(" id: "_pTestIndex) + do yamlStream.WriteLine(" id: "_TestIndex) do yamlStream.WriteLine(" namespace: """_tResult.namespace_"""") do yamlStream.WriteLine(" duration: "_tResult.duration) do yamlStream.WriteLine(" testDateTime: """_tResult.testDateTime_"""") - do yamlStream.WriteLine( "") + do yamlStream.WriteLine() do yamlStream.WriteLine(" results:") } if tResult.suiteName '= currentSuite { @@ -70,11 +70,11 @@ ClassMethod YAML( do yamlStream.WriteLine(" methods:") } do yamlStream.WriteLine(" - methodName: """_tResult.methodName_"""") - //do yamlStream.WriteLine(" testMethod: """_tResult.testMethod_"""") + do yamlStream.WriteLine(" testMethod: """_tResult.testMethod_"""") do yamlStream.WriteLine(" assertAction: """_tResult.assertAction_"""") do yamlStream.WriteLine(" assertCounter: "_tResult.assertCounter) - //do yamlStream.WriteLine(" assertDescription: |") - //do yamlStream.WriteLine(" "_$replace(tResult.assertDescription, $c(10), $c(10)_" ")) + do yamlStream.WriteLine(" assertDescription: |") + do yamlStream.WriteLine(" "_$replace(tResult.assertDescription, $char(10), $char(10)_" ")) do yamlStream.WriteLine(" assertLocation: """_tResult.assertLocation_"""") } return yamlStream diff --git a/tests/unit_tests/Test/PM/Unit/CLI.cls b/tests/unit_tests/Test/PM/Unit/CLI.cls index 8e9f2329a..797ce4541 100644 --- a/tests/unit_tests/Test/PM/Unit/CLI.cls +++ b/tests/unit_tests/Test/PM/Unit/CLI.cls @@ -352,4 +352,16 @@ Method TestUninstallWithoutModuleName() do $$$AssertNotTrue(exists, "Module removed successfully.") } +/// Specifies the serialization format (json, toon, yaml) for unit and integration test results in the shell. +Method TestReportFormatConfiguration() +{ + do ..RunCommand("config set TestReportFormat json") + set format = ##class(%IPM.Repo.UniversalSettings).GetTestReportFormat() + do $$$AssertEquals(format, "json", "Verify TestReportFormat is set to JSON") + + do ..RunCommand("config set TestReportFormat yaml") + set format = ##class(%IPM.Repo.UniversalSettings).GetTestReportFormat() + do $$$AssertEquals(format, "yaml", "Verify TestReportFormat is set to YAML") +} + } diff --git a/tests/unit_tests/Test/PM/Unit/TestResultsOPFormatAndFileGenTest.cls b/tests/unit_tests/Test/PM/Unit/TestResultsOPFormatAndFileGenTest.cls index abe8abc9b..354eaa0d3 100644 --- a/tests/unit_tests/Test/PM/Unit/TestResultsOPFormatAndFileGenTest.cls +++ b/tests/unit_tests/Test/PM/Unit/TestResultsOPFormatAndFileGenTest.cls @@ -13,11 +13,13 @@ Method TestResultFileGeneration() #define NormalizeFilename(%file) ##class(%File).NormalizeFilename(%file) do $$$LogMessage("This file generation picks the last or current unit test id and generate the reports") + set fileDir = ##class(%File).NormalizeDirectory($get(^UnitTestRoot)_"/test-reports/") if '##class(%File).DirectoryExists(fileDir){ set status = ##class(%File).CreateDirectoryChain(fileDir) do $$$AssertStatusOK(status,"Directory created: "_fileDir) } + do $$$LogMessage("Start generating the reports") set fileName = $$$NormalizeFilename(fileDir_"/test.yaml") set status = ##class(%IPM.Test.YamlOutput).ToFile(fileName) @@ -32,16 +34,13 @@ Method TestResultFileGeneration() do $$$AssertStatusOK(status,"Toon file generated successfully in "_fileDir) do ..ShowGeneratedFilesAndCleaup(fileDir) - - //set status = ##class(%File).RemoveDirectory(fileDir) - //do $$$AssertStatusOK(status,"Deleted the directory "_fileDir) } -Method ShowGeneratedFilesAndCleaup(fileDir As %String) +Method ShowGeneratedFilesAndCleaup(FileDir As %String) { do $$$LogMessage("Display the generated unit test report files") - set fileSet = ##class(%File).FileSetFunc(fileDir) - while fileSet.%Next(){ + set fileSet = ##class(%File).FileSetFunc(FileDir) + while fileSet.%Next() { set file = fileSet.Name set fileNames(file) = fileSet.ItemName do $$$LogMessage("Generated file "_file) From d1c0dce28cb69556d3153fe919c3b56f99ec847a Mon Sep 17 00:00:00 2001 From: AshokThangavel Date: Mon, 16 Mar 2026 22:42:43 +0530 Subject: [PATCH 4/4] Refactor: add format alias and clarify output-format description --- src/cls/IPM/Main.cls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cls/IPM/Main.cls b/src/cls/IPM/Main.cls index d1c498297..d6b65e204 100644 --- a/src/cls/IPM/Main.cls +++ b/src/cls/IPM/Main.cls @@ -75,7 +75,7 @@ Can also specify desired version to update to. - +