diff --git a/CHANGELOG.md b/CHANGELOG.md
index c0efbb5..7793fc0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [3.2.0] - Unreleased
+
+### Added
+- #42: Added a listener interface and manager with an associated user parameter, allowing the user to broadcast output on test method/case/suite completion.
## [3.1.1] - 2024-07-31
diff --git a/cls/TestCoverage/Listeners/ListenerInterface.cls b/cls/TestCoverage/Listeners/ListenerInterface.cls
new file mode 100644
index 0000000..44479a7
--- /dev/null
+++ b/cls/TestCoverage/Listeners/ListenerInterface.cls
@@ -0,0 +1,8 @@
+Class TestCoverage.Listeners.ListenerInterface Extends %RegisteredObject
+{
+
+Method Broadcast(pMessage As %DynamicObject) As %Status [ Abstract ]
+{
+}
+
+}
diff --git a/cls/TestCoverage/Listeners/ListenerManager.cls b/cls/TestCoverage/Listeners/ListenerManager.cls
new file mode 100644
index 0000000..a76f64b
--- /dev/null
+++ b/cls/TestCoverage/Listeners/ListenerManager.cls
@@ -0,0 +1,48 @@
+Class TestCoverage.Listeners.ListenerManager Extends %RegisteredObject
+{
+
+Property listeners As list Of TestCoverage.Listeners.ListenerInterface;
+
+Method BroadCastToAll(pMessage As %DynamicObject) As %Status
+{
+ set tSC = $$$OK
+ try {
+ for i = 1:1:..listeners.Count() {
+ set tListener = ..listeners.GetAt(i)
+ $$$ThrowOnError(tListener.Broadcast(pMessage))
+ }
+ }
+ catch e {
+ Set tSC = e.AsStatus()
+ }
+ quit tSC
+}
+
+Method AddListener(pListener As TestCoverage.Listeners.ListenerInterface) As %Status
+{
+ set tSC = $$$OK
+ try {
+ do ..listeners.Insert(pListener)
+ } catch e {
+ set tSC = e.AsStatus()
+ }
+ quit tSC
+}
+
+Method RemoveListener(pListener As TestCoverage.Listeners.ListenerInterface) As %Status
+{
+ set tSC = $$$OK
+ try {
+ set tIndex = ..listeners.FindOref(pListener)
+ if (tIndex = "") {
+ Set tMsg = "Listener not found"
+ $$$ThrowStatus($$$ERROR($$$GeneralError,tMsg))
+ }
+ do ..listeners.RemoveAt(tIndex)
+ } catch e {
+ set tSC = e.AsStatus()
+ }
+ quit tSC
+}
+
+}
diff --git a/cls/TestCoverage/Manager.cls b/cls/TestCoverage/Manager.cls
index 13aec20..53a7ea4 100644
--- a/cls/TestCoverage/Manager.cls
+++ b/cls/TestCoverage/Manager.cls
@@ -64,6 +64,9 @@ Property Hashes [ MultiDimensional ];
Property Monitor As TestCoverage.Utils.LineByLineMonitor [ InitialExpression = {##class(TestCoverage.Utils.LineByLineMonitor).%New()}, Private ];
+/// keeps track of all the listeners that may need information broadcasted about the unit test progress
+Property ListenerManager As TestCoverage.Listeners.ListenerManager;
+
/// Runs unit tests that have been loaded, with code coverage enabled.
/// Note that if coverage is to be tracked for lots of code, it may be necessary to increase the "gmheap" setting
/// (under Configuration - Additional Settings - Advanced Memory in the Management Portal).
@@ -563,6 +566,7 @@ ClassMethod OnBeforeAllTests(manager As TestCoverage.Manager, dir As %String, By
Set tCoverageRoutines = $Get(userparam("CoverageRoutines"))
Set tCoverageDetail = $Get(userparam("CoverageDetail"))
Set tSourceNamespace = $Get(userparam("SourceNamespace"),$Namespace)
+ Set tListenerManager = $Get(userparam("ListenerManager"))
Set tProcessIDs = $Get(userparam("ProcessIDs"),$ListBuild($Job))
If (tProcessIDs = "*") {
Set tProcessIDs = ""
@@ -584,6 +588,7 @@ ClassMethod OnBeforeAllTests(manager As TestCoverage.Manager, dir As %String, By
Set manager.ProcessIDs = tProcessIDs
Set manager.Timing = tTiming
Do manager.SetCoverageTargets(tCoverageClasses,tCoverageRoutines,1)
+ Set manager.ListenerManager = tListenerManager
If (tCoverageDetail '= "") {
If (tCoverageDetail '= +tCoverageDetail) {
// If we were passed a display value...
@@ -619,6 +624,10 @@ ClassMethod OnBeforeAllTests(manager As TestCoverage.Manager, dir As %String, By
}
If (manager.CoverageDetail = 0) {
+ if (manager.ListenerManager) {
+ set tObj = {"message": "Starting tests"}
+ Do manager.ListenerManager.BroadCastToAll(tObj)
+ }
Set tSC = manager.StartCoverageTracking()
$$$ThrowOnError(tSC)
}
@@ -637,6 +646,10 @@ ClassMethod OnAfterAllTests(manager As TestCoverage.Manager, dir As %String, ByR
Try {
If (manager.CoverageDetail = 0) {
Set tSC = manager.EndCoverageTracking()
+ if (manager.ListenerManager) {
+ set tObj = {"message": "All tests complete"}
+ Do manager.ListenerManager.BroadCastToAll(tObj)
+ }
}
Do manager.Monitor.Stop()
} Catch e {
@@ -673,9 +686,15 @@ Method OnBeforeTestSuite(dir As %String, suite As %String, testspec As %String,
Set ..CurrentTestSuite = $Case(suite,"":"(root)",:suite)
Set ..CurrentTestClass = ""
Set ..CurrentTestMethod = ""
+ if (..ListenerManager) {
+ set tObj = {"message": "Starting test suite: "}
+ do tObj.%Set("suite", suite)
+ Do ..ListenerManager.BroadCastToAll(tObj)
+ }
If (..CoverageDetail = 1) {
Set tSC = ..StartCoverageTracking()
}
+
} Catch e {
Set tSC = e.AsStatus()
}
@@ -688,9 +707,15 @@ Method OnAfterTestSuite(dir As %String, suite As %String, testspec As %String, B
{
Set tSC = $$$OK
Try {
+
If (..CoverageDetail = 1) {
Set tSC = ..EndCoverageTracking($Case(suite,"":"(root)",:suite))
}
+ if (..ListenerManager) {
+ set tObj = {"message": "Finished test suite: "}
+ do tObj.%Set("suite", suite)
+ Do ..ListenerManager.BroadCastToAll(tObj)
+ }
} Catch e {
Set tSC = e.AsStatus()
}
@@ -705,6 +730,12 @@ Method OnBeforeTestCase(suite As %String, class As %String) As %Status
Try {
Set ..CurrentTestClass = class
Set ..CurrentTestMethod = ""
+ if (..ListenerManager) {
+ set tObj = {"message": "Starting test case: "}
+ do tObj.%Set("suite", suite)
+ do tObj.%Set("class", class)
+ Do ..ListenerManager.BroadCastToAll(tObj)
+ }
If (..CoverageDetail = 2) {
Set tSC = ..StartCoverageTracking()
}
@@ -723,6 +754,12 @@ Method OnAfterTestCase(suite As %String, class As %String) As %Status
If (..CoverageDetail = 2) {
Set tSC = ..EndCoverageTracking(suite, class)
}
+ if (..ListenerManager) {
+ set tObj = {"message": "Finished test case: "}
+ do tObj.%Set("suite", suite)
+ do tObj.%Set("class", class)
+ Do ..ListenerManager.BroadCastToAll(tObj)
+ }
} Catch e {
Set tSC = e.AsStatus()
}
@@ -736,6 +773,13 @@ Method OnBeforeOneTest(suite As %String, class As %String, method As %String) As
Set tSC = $$$OK
Try {
Set ..CurrentTestMethod = method
+ if (..ListenerManager) {
+ set tObj = {"message": "Starting test method: "}
+ do tObj.%Set("suite", suite)
+ do tObj.%Set("class", class)
+ do tObj.%Set("method", method)
+ Do ..ListenerManager.BroadCastToAll(tObj)
+ }
If (..CoverageDetail = 3) {
Set tSC = ..StartCoverageTracking()
}
@@ -754,6 +798,13 @@ Method OnAfterOneTest(suite As %String, class As %String, method As %String) As
If (..CoverageDetail = 3) {
Set tSC = ..EndCoverageTracking(suite, class, method)
}
+ if (..ListenerManager) {
+ set tObj = {"message": "Finished test method: "}
+ do tObj.%Set("suite", suite)
+ do tObj.%Set("class", class)
+ do tObj.%Set("method", method)
+ Do ..ListenerManager.BroadCastToAll(tObj)
+ }
} Catch e {
Set tSC = e.AsStatus()
}