Skip to content

Add test formatting options (toon, yaml, json)#1127

Open
isc-dchui wants to merge 19 commits intomainfrom
feat/llm-friendly-output-for-ipm-test
Open

Add test formatting options (toon, yaml, json)#1127
isc-dchui wants to merge 19 commits intomainfrom
feat/llm-friendly-output-for-ipm-test

Conversation

@isc-dchui
Copy link
Copy Markdown
Collaborator

@isc-dchui isc-dchui commented Apr 24, 2026

Description

  • Resolves LLM-friendly output mode for zpm "test" #971
  • Gets @AshokThangavel's work in PR feat: Add JSON, YAML, and Toon output options for zpm test results #979 across the line with a few changes:
    • Properly handle non-assert failures (thrown errors and issues in hooks, e.g. OnBeforeAllTests)
    • Redo the UX a bit, fleshing out the flags and default behaviors
    • Add extensive integration testing
  • New -f <format> flag (json, yaml, toon) on test/verify: one-shot terminal output format override. When omitted, falls back to the persistent default set via config set TestReportFormat <format>, and if neither is configured, shows the existing plain-text summary + legacy red FAILED lines.
  • New -output-file <path> flag: writes full results to a file; format inferred from extension (.json, .yaml, .toon, .xml for JUnit XML).
  • New -quiet behavior: suppresses build/install noise during test/verify while still showing the result summary.
  • config set/delete TestReportFormat: persistent default format; config delete now correctly resets to empty (legacy mode) rather than restoring the first-ever value.
  • Refactored formatter architecture: %IPM.Test.Abstract provides a shared OutputToDevice (plain-text summary) and ToFile template; each formatter (JsonOutput, YamlOutput, ToonOutput, JUnitOutput) only overrides WriteToFileStream and, where different from the base summary, OutputToDevice.
  • The most LLM friendly output will be from test <module-name> -quiet -format toon, where -quiet will suppress almost all other output and what's left will be a summary and the failed tests in the TOON format.

Examples

zpm:USER>test-output-format test -only

[USER|test-output-format]       Test START
Building dependency graph...Done.
Use the following URL to view the result:
http://172.19.0.3:52773/csp/sys/%25UnitTest.Portal.Indices.cls?Index=100&$NAMESPACE=USER
Some tests FAILED in suites:
  ,(root)

Test Results:

Test Run #100 (USER) .799303s 2026-04-24 17:20:12
Methods: 18 total, 6 passed, 12 failed
Assertions: 22 total, 20 passed, 2 failed

FAILED (root):Test.Output.Format.AfterAllReturn: OnAfterAllTests:  ERROR #5001: deliberate OnAfterAllTests return-error
FAILED (root):Test.Output.Format.AfterAllThrow: OnAfterAllTests:  ERROR #5001: deliberate OnAfterAllTests throw
FAILED (root):TestMethodBodyPasses: OnAfterOneTest:  ERROR #5001: deliberate OnAfterOneTest return-error for TestMethodBodyPasses
FAILED (root):TestMethodBodyPasses: OnAfterOneTest:  ERROR #5001: deliberate OnAfterOneTest throw for TestMethodBodyPasses
FAILED (root):Test.Output.Format.BeforeAllReturn: OnBeforeAllTests:  ERROR #5001: deliberate OnBeforeAllTests return-error
FAILED (root):Test.Output.Format.BeforeAllThrow: OnBeforeAllTests:  ERROR #5001: deliberate OnBeforeAllTests throw
FAILED (root):TestFirstMethod: OnBeforeOneTest:  ERROR #5001: deliberate OnBeforeOneTest return-error for TestFirstMethod
FAILED (root):TestSecondMethod: OnBeforeOneTest:  ERROR #5001: deliberate OnBeforeOneTest return-error for TestSecondMethod
FAILED (root):TestFirstMethod: OnBeforeOneTest:  ERROR #5001: deliberate OnBeforeOneTest throw for TestFirstMethod
FAILED (root):TestSecondMethod: OnBeforeOneTest:  ERROR #5001: deliberate OnBeforeOneTest throw for TestSecondMethod
FAILED (root):TestFailingAssertions: AssertTrue - deliberate failure: zero is not true
FAILED (root):TestFailingAssertions: AssertEquals - deliberate mismatch
FAILED (root):TestThrowsFromBody: TestThrowsFromBody:  ERROR #5001: deliberate throw from test body
[test-output-format]    Test FAILURE
ERROR! 13 failure(s).
zpm:USER>test-output-format test -only -f toon

[USER|test-output-format]       Test START
Building dependency graph...Done.
Use the following URL to view the result:
http://172.19.0.3:52773/csp/sys/%25UnitTest.Portal.Indices.cls?Index=101&$NAMESPACE=USER
Some tests FAILED in suites:
  ,(root)

Test Results:

summary:
  id: 101  namespace: USER  duration: .834048s  testDateTime: 2026-04-24 17:20:33
  methods[18]: 6 passed, 12 failed
  assertions[22]: 20 passed, 2 failed

failures[13]{suiteName,testcaseName,methodName,status,assertAction,assertCounter,assertDescription,assertLocation}:
  (root),Test.Output.Format.AfterAllReturn,Test.Output.Format.AfterAllReturn,failed,OnBeforeAllTests,0,"OnAfterAllTests:  ERROR #5001: deliberate OnAfterAllTests return-error",""
  (root),Test.Output.Format.AfterAllThrow,Test.Output.Format.AfterAllThrow,failed,OnBeforeAllTests,0,"OnAfterAllTests:  ERROR #5001: deliberate OnAfterAllTests throw",""
  (root),Test.Output.Format.AfterOneReturn,TestMethodBodyPasses,failed,error,0,"OnAfterOneTest:  ERROR #5001: deliberate OnAfterOneTest return-error for TestMethodBodyPasses",""
  (root),Test.Output.Format.AfterOneThrow,TestMethodBodyPasses,failed,error,0,"OnAfterOneTest:  ERROR #5001: deliberate OnAfterOneTest throw for TestMethodBodyPasses",""
  (root),Test.Output.Format.BeforeAllReturn,Test.Output.Format.BeforeAllReturn,failed,OnBeforeAllTests,0,"OnBeforeAllTests:  ERROR #5001: deliberate OnBeforeAllTests return-error",""
  (root),Test.Output.Format.BeforeAllThrow,Test.Output.Format.BeforeAllThrow,failed,OnBeforeAllTests,0,"OnBeforeAllTests:  ERROR #5001: deliberate OnBeforeAllTests throw",""
  (root),Test.Output.Format.BeforeOneReturn,TestFirstMethod,failed,error,0,"OnBeforeOneTest:  ERROR #5001: deliberate OnBeforeOneTest return-error for TestFirstMethod",""
  (root),Test.Output.Format.BeforeOneReturn,TestSecondMethod,failed,error,0,"OnBeforeOneTest:  ERROR #5001: deliberate OnBeforeOneTest return-error for TestSecondMethod",""
  (root),Test.Output.Format.BeforeOneThrow,TestFirstMethod,failed,error,0,"OnBeforeOneTest:  ERROR #5001: deliberate OnBeforeOneTest throw for TestFirstMethod",""
  (root),Test.Output.Format.BeforeOneThrow,TestSecondMethod,failed,error,0,"OnBeforeOneTest:  ERROR #5001: deliberate OnBeforeOneTest throw for TestSecondMethod",""
  (root),Test.Output.Format.MethodAssertFailure,TestFailingAssertions,failed,AssertTrue,2,"deliberate failure: zero is not true","TestFailingAssertions+2^Test.Output.Format.MethodAssertFailure.cls"
  (root),Test.Output.Format.MethodAssertFailure,TestFailingAssertions,failed,AssertEquals,3,"deliberate mismatch","TestFailingAssertions+3^Test.Output.Format.MethodAssertFailure.cls"
  (root),Test.Output.Format.MethodThrowFailure,TestThrowsFromBody,failed,error,0,"TestThrowsFromBody:  ERROR #5001: deliberate throw from test body",""

[test-output-format]    Test FAILURE
ERROR! 13 failure(s).

Testing

New unit test (Test.PM.Unit.CLI:TestReportFormatConfiguration) and extensive integration tests (Test.PM.Integration.TestOutputFormat) to cover all the different flag combos.

Checklist

  • This branch has the latest changes from the main branch rebased or merged.
  • Changelog entry added.
  • Unit (zpm test -only) and integration tests (zpm verify -only) pass.
  • Style matches the style guide in the contributing guide.
  • Documentation has been/will be updated
    • Will update wiki after this is merged.
  • Pull request correctly renders in the "Preview" tab.

Copy link
Copy Markdown
Contributor

@isc-tleavitt isc-tleavitt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My main feedback is on how the OutputSuppressor works. Using the null device is - I think! - better than I/O redirection. One catch with I/O redirection is behavior if there's output after changing to a namespace (e.g. %SYS) that doesn't have the routine used - null device gets around that.

Also should improve cleanup for OutputSuppressor to use %RegisteredObject patterns along the lines of https://community.intersystems.com/post/robust-error-handling-and-cleanup-objectscript

new $namespace
set tInitNS = $select($namespace="%SYS": "USER", 1: $namespace)
set tVerbose = $get(pParams("Verbose"))
set explicitQuiet = ($data(pParams("Verbose")) && (pParams("Verbose") = 0))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: $data(pParams("Verbose"))#2

But probably doesn't really matter.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe that's quite right as this is checking if pParams("Verbose") has been explicitly set to 0.

Comment thread src/cls/IPM/Utils/OutputSuppressor.cls Outdated
Class %IPM.Utils.OutputSuppressor
{

ClassMethod Begin(Output cookie As %String) As %Status [ ProcedureBlock = 0 ]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead return an instance of this class ByRef that manages cleanup through manual call or %OnClose.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched to using the null device

Comment thread src/cls/IPM/Utils/OutputSuppressor.cls Outdated
/// Used as the mnemonic device routine for BeginSuppressOutput / EndSuppressOutput.
/// Unlike BeginCaptureOutput, suppress does not use ^||%capture, so it is safe
/// to call while a capture is already active.
Class %IPM.Utils.OutputSuppressor
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should end capture in %OnClose.

Comment thread src/cls/IPM/Lifecycle/Base.cls Outdated
}
}
} catch e {
if capturing {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't need to do this if OutputSuppressor cancels in OnClose.

@isc-dchui
Copy link
Copy Markdown
Collaborator Author

@isc-tleavitt Addressed the comments by switching to using the null device and the registered object/%OnClose pattern

@isc-dchui isc-dchui requested a review from isc-tleavitt April 24, 2026 20:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LLM-friendly output mode for zpm "test"

3 participants