diff --git a/cls/TestCoverage/Data/CodeUnit.cls b/cls/TestCoverage/Data/CodeUnit.cls index 5401b78..496b85c 100644 --- a/cls/TestCoverage/Data/CodeUnit.cls +++ b/cls/TestCoverage/Data/CodeUnit.cls @@ -32,7 +32,7 @@ Property MethodMap As array Of %Integer; /// For classes, map of line numbers in code to associated method names /// For routines, map of labels to associated line numbers -Property LineToMethodMap As array Of %String [ Private ]; +Property LineToMethodMap As array Of %Dictionary.CacheIdentifier [ Private ]; /// Set to true if this class/routine is generated Property Generated As %Boolean [ InitialExpression = 0 ]; @@ -160,8 +160,13 @@ ClassMethod GetCurrentByName(pInternalName As %String, pSourceNamespace As %Stri } } - $$$ThrowOnError(pCodeUnit.%Save()) - + Set tSC = pCodeUnit.%Save() + If $$$ISERR(tSC) && $System.Status.Equals(tSC,$$$ERRORCODE($$$IDKeyNotUnique)) { + // Some other process beat us to it. + Set tSC = $$$OK + Set pCodeUnit = ..%OpenId(pCodeUnit.Hash,,.tSC) + Quit + } // For non-class (e.g., .MAC/.INT) code, it's possible that something else generated it, // so update the mappings between generated and the thing that generated it. If (tType '= "CLS") { diff --git a/cls/TestCoverage/Data/CodeUnitMap.cls b/cls/TestCoverage/Data/CodeUnitMap.cls index c2d4e88..b2a2d0c 100644 --- a/cls/TestCoverage/Data/CodeUnitMap.cls +++ b/cls/TestCoverage/Data/CodeUnitMap.cls @@ -1,3 +1,5 @@ +/// This class maintains the mapping between .INT/.MAC/.CLS and therefore is critical for +/// interpreting the .INT-level coverage data that the line-by-line monitor collects. Class TestCoverage.Data.CodeUnitMap Extends %Persistent { @@ -23,26 +25,36 @@ ForeignKey ToCodeUnitFK(ToHash) References TestCoverage.Data.CodeUnit(Hash) [ On ClassMethod Create(pFromHash As %String, pFromLine As %Integer, pToHash As %String, pToLineStart As %Integer, pToLineEnd As %Integer) As %Status { + #def1arg DefaultStorageNode(%node) ##expression($$$comMemberKeyGet("TestCoverage.Data.CodeUnitMap", $$$cCLASSstorage, "Default", %node)) + #def1arg CodeUnitMasterMap(%arg) $$$DefaultStorageNode($$$cSDEFdatalocation)(%arg) + #def1arg CodeUnitReverseMap(%arg) $$$DefaultStorageNode($$$cSDEFindexlocation)("Reverse",%arg) + Set tSC = $$$OK Try { - &sql(insert or update %NOLOCK %NOCHECK into TestCoverage_Data.CodeUnitMap - (FromHash, FromLine, ToHash, ToLine) - select :pFromHash, :pFromLine, :pToHash, Counter - from TestCoverage.Sequence(:pToLineStart,:pToLineEnd)) - If (SQLCODE < 0) { - Throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) + For counter=pToLineStart:1:pToLineEnd { + // Uses direct global references for performance boost; this is one of the most performance-critical sections. + If '$Data($$$CodeUnitMasterMap(pFromHash,pFromLine,pToHash,counter)) { + &sql(insert %NOLOCK %NOCHECK into TestCoverage_Data.CodeUnitMap + (FromHash, FromLine, ToHash, ToLine) + select :pFromHash, :pFromLine, :pToHash, :counter) + If (SQLCODE < 0) { + Throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) + } + } } // Insert/update transitive data (e.g., .INT -> .MAC (generator) -> .CLS) + // Original implementation: + /* + // Leg 1: Lines that map to the "from" line also map to the "to" line + // Leg 2: The "from" line also maps to lines that the "to" line maps to &sql( - /* Lines that map to the "from" line also map to the "to" line */ insert or update %NOLOCK %NOCHECK into TestCoverage_Data.CodeUnitMap (FromHash, FromLine, ToHash, ToLine) select FromHash, FromLine, :pToHash, Counter from TestCoverage.Sequence(:pToLineStart,:pToLineEnd),TestCoverage_Data.CodeUnitMap where ToHash = :pFromHash and ToLine = :pFromLine union - /* The "from" line also maps to lines that the "to" line maps to */ select :pFromHash, :pFromLine, ToHash, ToLine from TestCoverage.Sequence(:pToLineStart,:pToLineEnd) join TestCoverage_Data.CodeUnitMap @@ -50,6 +62,62 @@ ClassMethod Create(pFromHash As %String, pFromLine As %Integer, pToHash As %Stri If (SQLCODE < 0) { Throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) } + */ + + // This introduced some unacceptable performance overhead, and has been rewritten with direct global references. + // This reduces overall overhead of code capture for test coverage measurement by roughly 40%. + + // Leg 1: Lines that map to the "from" line also map to the "to" line + Set fromHash = "" + For { + Set fromHash = $Order($$$CodeUnitReverseMap(pFromHash,pFromLine,fromHash)) + If (fromHash = "") { + Quit + } + Set fromLine = "" + For { + Set fromLine = $Order($$$CodeUnitReverseMap(pFromHash,pFromLine,fromHash,fromLine)) + If (fromLine = "") { + Quit + } + For counter=pToLineStart:1:pToLineEnd { + If '$Data($$$CodeUnitMasterMap(fromHash,fromLine,pToHash,counter)) { + &sql(insert %NOLOCK %NOCHECK into TestCoverage_Data.CodeUnitMap + (FromHash, FromLine, ToHash, ToLine) + select :fromHash, :fromLine, :pToHash, :counter) + If (SQLCODE < 0) { + Throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) + } + } + } + } + } + + For counter=pToLineStart:1:pToLineEnd { + // Leg 2: The "from" line also maps to lines that the "to" line maps to + Set toHash = "" + For { + Set toHash = $Order($$$CodeUnitMasterMap(pToHash,counter,toHash)) + If (toHash = "") { + Quit + } + Set toLine = "" + For { + Set toLine = $Order($$$CodeUnitMasterMap(pToHash,counter,toHash,toLine)) + If (toLine = "") { + Quit + } + If '$Data($$$CodeUnitMasterMap(pFromHash,pFromLine,toHash,toLine)) { + &sql(insert %NOLOCK %NOCHECK into TestCoverage_Data.CodeUnitMap + (FromHash, FromLine, ToHash, ToLine) + select :pFromHash, :pFromLine, :toHash, :toLine) + If (SQLCODE < 0) { + Throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) + } + } + } + } + } } Catch e { Set tSC = e.AsStatus() } diff --git a/cls/TestCoverage/Data/Run.cls b/cls/TestCoverage/Data/Run.cls index 8ba8855..8fb77a0 100644 --- a/cls/TestCoverage/Data/Run.cls +++ b/cls/TestCoverage/Data/Run.cls @@ -85,7 +85,7 @@ ClassMethod MapRunCoverage(pRunIndex As %Integer) As %Status Set tMetric = tRun.Metrics.GetAt(i) Set tSQLStatement = "INSERT OR UPDATE %NOLOCK %NOCHECK INTO TestCoverage_Data.""Coverage_"_tMetric_""" "_ "(Coverage,element_key,"""_tMetric_""") "_ - "SELECT target.ID,map.ToLine,NVL(oldMetric."""_tMetric_""",0) + metric."""_tMetric_""" "_ + "SELECT target.ID,map.ToLine,NVL(oldMetric."""_tMetric_""",0) + SUM(metric."""_tMetric_""") "_ "FROM TestCoverage_Data.Coverage source "_ "JOIN TestCoverage_Data.CodeUnitMap map "_ " ON source.Hash = map.FromHash "_ @@ -101,7 +101,8 @@ ClassMethod MapRunCoverage(pRunIndex As %Integer) As %Status " AND oldMetric.element_key = map.ToLine "_ "WHERE source.Run = ? "_ " AND source.Ignore = 0"_ - " AND source.Calculated = 0" + " AND source.Calculated = 0"_ + "GROUP BY target.ID,map.ToLine" #dim tResult As %SQL.StatementResult Set tResult = ##class(%SQL.Statement).%ExecDirect(,tSQLStatement,pRunIndex) diff --git a/cls/TestCoverage/Manager.cls b/cls/TestCoverage/Manager.cls index 214ecba..f8c2a0e 100644 --- a/cls/TestCoverage/Manager.cls +++ b/cls/TestCoverage/Manager.cls @@ -52,7 +52,8 @@ Property ProcessIDs As %List [ Internal, Private ]; Property Run As TestCoverage.Data.Run; -/// Known coverage targets (already snapshotted) +/// Known coverage targets (already snapshotted).
+/// Value at subscript is set to 1 if there are executable lines of code in the target, 0 if not. Property KnownCoverageTargets [ MultiDimensional, Private ]; /// Cache of (name, type) -> hash @@ -172,23 +173,33 @@ Method StartCoverageTracking() As %Status [ Private ] Try { If (..CoverageTargets '= "") { Set $Namespace = ..SourceNamespace - + + Set tRelevantTargets = "" Set tNewTargets = "" Set tPointer = 0 While $ListNext(..CoverageTargets,tPointer,tCoverageTarget) { - If '$Data(..KnownCoverageTargets(tCoverageTarget)) { + If '$Data(..KnownCoverageTargets(tCoverageTarget),tIsRelevant)#2 { Set tNewTargets = tNewTargets_$ListBuild(tCoverageTarget) + } ElseIf tIsRelevant { + Set tRelevantTargets = tRelevantTargets_$ListBuild(tCoverageTarget) } } If (tNewTargets '= "") { $$$StartTimer("Taking snapshot of code and CLS/MAC/INT mappings") - Do ##class(TestCoverage.Utils).Snapshot(tNewTargets) + Set tSC = ##class(TestCoverage.Utils).Snapshot(tNewTargets, .tNewRelevantTargets) $$$StopTimer + $$$ThrowOnError(tSC) Set tPointer = 0 While $ListNext(tNewTargets,tPointer,tNewTarget) { - Set ..KnownCoverageTargets(tNewTarget) = "" + Set ..KnownCoverageTargets(tNewTarget) = 0 + } + + Set tPointer = 0 + While $ListNext(tNewRelevantTargets,tPointer,tRelevantTarget) { + Set ..KnownCoverageTargets(tRelevantTarget) = 1 + Set tRelevantTargets = tRelevantTargets_$ListBuild(tRelevantTarget) } } @@ -218,7 +229,7 @@ Method StartCoverageTracking() As %Status [ Private ] } } Set tMetrics = $ListBuild("RtnLine") _ $Select(..Timing:$ListBuild("Time","TotalTime"),1:"") - $$$ThrowOnError(..Monitor.StartWithScope(..CoverageTargets,tMetrics,tProcessIDs)) + $$$ThrowOnError(..Monitor.StartWithScope(tRelevantTargets,tMetrics,tProcessIDs)) } } Catch e { Set tSC = e.AsStatus() @@ -282,8 +293,6 @@ Method UpdateCoverageTargetsForTestDirectory(pDirectory As %String) As %Status [ } Set tObjectCodeList = ..GetObjectCodeForSourceNames(tCoverageTargetList) - // Check the available memory before trying to capture coverage, so the user can remediate without waiting a really long time - $$$ThrowOnError(##class(TestCoverage.Utils.LineByLineMonitor).CheckAvailableMemory($ListLength(..ProcessIDs),$ListLength(tObjectCodeList))) Set ..CoverageTargets = tObjectCodeList // Also restarts the monitor if it is running and updates data on covered routines/classes } Catch e { Set tSC = e.AsStatus() @@ -697,19 +706,35 @@ Method PrintURL() { Do ##super() - Do ..PrintLine("Use the following URL to view test coverage data:") - Do ..PrintLine(..GetURL(..Run.%Id())) + Set tURL = ..GetURL(..Run.%Id()) + If (tURL '= "") { + Do ..PrintLine("Use the following URL to view test coverage data:") + Do ..PrintLine(tURL) + } Else { + Do ..PrintLine("WARNING: No default web application found for namespace '"_$Namespace_"' - test coverage results cannot be viewed.") + } Quit } +/// Returns the URL to the aggregate result viewer.
+/// pRunID is the test coverage run index. +/// pHost contains the host/protocol to use. +/// pPath contains the rest of the URL after that. ClassMethod GetURL(pRunID As %String, Output pHost As %String, Output pPath As %String) As %String { - Set tSC = ##class(%RoutineMgr).GetWebServerPort(.tPort,.tServer,.tURLPrefix) + Set tSC = ##class(%Library.RoutineMgr).GetWebServerPort(.tPort,.tServer,.tURLPrefix) $$$ThrowOnError(tSC) - Set pHost = $Case(tPort,443:"https",:"http")_"://"_$Get(^%SYS("HealthShare","NetworkHostName"),tServer) - Set pHost = pHost _ $Case(tPort,80:"",:":"_tPort) + Set pHost = $ZConvert($Get(^%SYS("WebServer","Protocol"),$Select(tPort=443:"https",1:"http")),"l") + Set pHost = pHost_"://"_$Get(^%SYS("HealthShare","NetworkHostName"),tServer) + // Ports 80 and 443 are defaults for their respective protocols; in other cases, port needs to be explicit. + Set pHost = pHost _ $Case(tPort,80:"",443:"",:":"_tPort) + Set tDefaultApp = $System.CSP.GetDefaultApp($Namespace) + If (tDefaultApp = "") || (((tDefaultApp = "/csp/sys") || (tDefaultApp [ "/csp/sys/")) && ($Namespace '= "%SYS")) { + // The URL won't be valid, so just return an empty string. + Quit "" + } Set pPath = $Case(tURLPrefix,"":"",:"/"_tURLPrefix) - Set pPath = pPath _ $$getDefaultApp^%SYS.cspServer2($Namespace) + Set pPath = pPath _ tDefaultApp Set pPath = pPath _ "/TestCoverage.UI.AggregateResultViewer.cls?Index="_$ZConvert(pRunID,"O","URL") Quit pHost_pPath } diff --git a/cls/TestCoverage/Procedures.cls b/cls/TestCoverage/Procedures.cls index d08e5d0..85542f3 100644 --- a/cls/TestCoverage/Procedures.cls +++ b/cls/TestCoverage/Procedures.cls @@ -1,3 +1,4 @@ +/// Contains several helpful stored procedures for use in SQL. Class TestCoverage.Procedures { @@ -86,32 +87,5 @@ ClassMethod ListToBit(pSource As %List) As %Binary [ SqlName = LIST_TO_BIT, SqlP Quit tResult } -/// Table-valued function returning a sequence of integers (column name "Counter") going from pStart to pEnd by pIncrement. -Query Sequence(pStart As %Integer, pEnd As %Integer, pIncrement As %Integer = 1) As %Query(ROWSPEC = "Counter:%Integer") [ SqlName = SEQUENCE, SqlProc ] -{ -} - -ClassMethod SequenceExecute(ByRef qHandle As %Binary, pStart As %Integer, pEnd As %Integer, pIncrement As %Integer = 1) As %Status -{ - Set qHandle = pStart - Set qHandle("inc") = pIncrement - Set qHandle("end") = pEnd - Quit $$$OK -} - -ClassMethod SequenceClose(ByRef qHandle As %Binary) As %Status [ PlaceAfter = SequenceExecute ] -{ - Quit $$$OK -} - -ClassMethod SequenceFetch(ByRef qHandle As %Binary, ByRef Row As %List, ByRef AtEnd As %Integer = 0) As %Status [ PlaceAfter = SequenceExecute ] -{ - Set Row = $ListBuild(qHandle) - If ($Increment(qHandle,qHandle("inc")) > qHandle("end")) { - Set AtEnd = 1 - } - Quit $$$OK -} - } diff --git a/cls/TestCoverage/UI/Application.cls b/cls/TestCoverage/UI/Application.cls index df994ae..765b393 100644 --- a/cls/TestCoverage/UI/Application.cls +++ b/cls/TestCoverage/UI/Application.cls @@ -70,14 +70,14 @@ pre.coverage { max-height: 75vh; overflow-x: auto; } -pre.coverage span { +pre.coverage > span { white-space: pre; tab-size: 4; display: block; line-height: 1.5em; width: 100%; } -pre.coverage span:before { +pre.coverage > span:before { counter-increment: line; content: counter(line); display: inline-block; @@ -89,15 +89,15 @@ pre.coverage span:before { } /* Classes for display of code coverage */ -pre.coverage span.executable:before { +pre.coverage > span.executable:before { background-color: #f66; } -pre.coverage span.covered:before { +pre.coverage > span.covered:before { background-color: #6f6; } -pre.coverage span.hide:before { +pre.coverage > span.hide:before { background-color: #fff; } diff --git a/cls/TestCoverage/UI/Component/altJSONSQLProvider.cls b/cls/TestCoverage/UI/Component/altJSONSQLProvider.cls new file mode 100644 index 0000000..82b473c --- /dev/null +++ b/cls/TestCoverage/UI/Component/altJSONSQLProvider.cls @@ -0,0 +1,102 @@ +Class TestCoverage.UI.Component.altJSONSQLProvider Extends %ZEN.Auxiliary.altJSONSQLProvider [ System = 3 ] +{ + +/// This is the XML namespace for this component. +Parameter NAMESPACE = "http://www.intersystems.com/zen/healthshare/test-coverage"; + +/// Overridden to deal with a few issues on older platform versions +/// (minimal modifications, all commented as such) +Method %DrawJSON() As %Status [ Internal ] +{ + Set ..contentType = "array" + // override base method to get information from SQL statement + Set tSC = $$$OK + Try { + #; convert parameters to local array + Set key = ..parameters.Next("") + While (key'="") { + Set value = ..parameters.GetAt(key).value + Set tParms(key) = $$$ZENVAL(value) + Set key = ..parameters.Next(key) + } + Set tOrigSQL = ..sql + Set tSQL = ..sql + + If (..OnGetSQL '= "") { + Set tSC = ..%OnGetSQL(.tParms,.tSQL) + If $$$ISERR(tSC) { + Write "null" + Quit + } + Set ..sql = tSQL + } + + Set tInfo = ##class(%ZEN.Auxiliary.QueryInfo).%New() + Merge tInfo.parms=tParms + Set tRS = ..%CreateResultSet(.tSC,tInfo) + If $$$ISERR(tSC)||'$IsObject(tRS) { + Write "null" + Quit + } + + // find number and name of columns + Kill tColInfo + If tRS.%IsA("%Library.ResultSet") { + Set tCols = tRS.GetColumnCount() + For c = 1:1:tCols { + Set tColInfo(c,"name") = tRS.GetColumnHeader(c) + } + } + Else { + Set tCols = tRS.%ResultColumnCount + For c = 1:1:tCols { + Set tColInfo(c,"name") = tRS.%Metadata.columns.GetAt(c).label + } + } + Set ..sql = tOrigSQL + + Set aet = ##class(%DynamicAbstractObject).%FromJSON("{"""_..arrayName_""":[]}") + Set arrayNode = aet.%Get(..arrayName) + + // fetch and emit JSON + // n.b. this should be pushed into the result set itself + Set tRow = 0 + While (tRS.%Next(.tSC) && ((..maxRows = 0) || (tRow < ..maxRows))) { + Quit:$$$ISERR(tSC) + Set tRow = tRow + 1 + Set node = ##class(%DynamicObject).%New() + For c = 1:1:tCols { + Set tVal = tRS.%GetData(c) + + // MODIFICATION IN OVERRIDE: + // To avoid JS errors on clients, insert an "invisible space" into any "" tags specifically. + Set tVal = $Replace(tVal,"","") + Set tVal = $Replace(tVal,"","") + // END MODIFICATION. + + If ($IsValidNum(tVal)) { + Do node.%Set($Get(tColInfo(c,"name")),$Num(tVal),"number") + } + Else { + Do node.%Set($Get(tColInfo(c,"name")),tVal) + } + } + Do arrayNode.%Push(node) + } + + // MODIFICATION IN OVERRIDE: + // Support larger text and avoid old I/O redirection issues by outputting to stream. + Set tStream = ##class(%Stream.TmpCharacter).%New() + Do aet.%ToJSON(.tStream) + Set tSC = tStream.OutputToDevice() + // END MODIFICATION. + } + Catch(ex) { + Write "null" + Set tSC = ex.AsStatus() + } + Quit tSC +} + +} + diff --git a/cls/TestCoverage/UI/Component/codeCSS.cls b/cls/TestCoverage/UI/Component/codeCSS.cls new file mode 100644 index 0000000..7da1b00 --- /dev/null +++ b/cls/TestCoverage/UI/Component/codeCSS.cls @@ -0,0 +1,33 @@ +/// Component to render CSS styles for all languages supported by %Library.SyntaxColor +/// (Supports using CSS-enabled output mode rather than generating tags.) +Class TestCoverage.UI.Component.codeCSS Extends %ZEN.Component.component [ System = 3 ] +{ + +/// This is the XML namespace for this component. +Parameter NAMESPACE = "http://www.intersystems.com/zen/healthshare/test-coverage"; + +/// Generated to provide styles for all supported languages. +Method %DrawHTML() [ CodeMode = objectgenerator ] +{ + Do %code.WriteLine($c(9)_"&html<>") + Quit sc +} + +} + diff --git a/cls/TestCoverage/UI/ResultDetailViewer.cls b/cls/TestCoverage/UI/ResultDetailViewer.cls index b268d59..9842e5c 100644 --- a/cls/TestCoverage/UI/ResultDetailViewer.cls +++ b/cls/TestCoverage/UI/ResultDetailViewer.cls @@ -12,6 +12,7 @@ Property testPath As %ZEN.Datatype.string; XData Contents [ XMLNamespace = "http://www.intersystems.com/zen" ] { +
@@ -26,11 +27,11 @@ XData Contents [ XMLNamespace = "http://www.intersystems.com/zen" ]
- + - + @@ -53,15 +54,14 @@ ClientMethod testFilterChanged(pSrcComponent As %ZEN.Component.select) [ Languag ClientMethod showCodeCoverage() [ Language = javascript ] { zen('coverageDataProvider').reloadContentsAsynch(function() { - var code = zen('coverageDataProvider').getContentObject().children; - document.getElementById('coverageResults').innerHTML = zenPage.buildCodeHTML(code); + zenPage.onloadHandler(); }); } /// This client event, if present, is fired when the page is loaded. ClientMethod onloadHandler() [ Language = javascript ] { - zenPage.showCodeCoverage(); + zenPage.renderCodeCoverage('coverageDataProvider','coverageResults'); } } diff --git a/cls/TestCoverage/UI/SimpleResultViewer.cls b/cls/TestCoverage/UI/SimpleResultViewer.cls index 7f38652..b19fed3 100644 --- a/cls/TestCoverage/UI/SimpleResultViewer.cls +++ b/cls/TestCoverage/UI/SimpleResultViewer.cls @@ -12,6 +12,7 @@ Property testPath As %ZEN.Datatype.string; XData Contents [ XMLNamespace = "http://www.intersystems.com/zen" ] { +
@@ -39,11 +40,11 @@ XData Contents [ XMLNamespace = "http://www.intersystems.com/zen" ]
- + - + @@ -74,8 +75,7 @@ ClientMethod showCodeCoverage() [ Language = javascript ] { zenPage.codeUnit = zen('codeCovered').getValue(); zen('coverageDataProvider').reloadContentsAsynch(function() { - var code = zen('coverageDataProvider').getContentObject().children; - document.getElementById('coverageResults').innerHTML = zenPage.buildCodeHTML(code); + zenPage.renderCodeCoverage('coverageDataProvider','coverageResults'); }); } diff --git a/cls/TestCoverage/UI/Template.cls b/cls/TestCoverage/UI/Template.cls index 93eea24..3e194a1 100644 --- a/cls/TestCoverage/UI/Template.cls +++ b/cls/TestCoverage/UI/Template.cls @@ -3,6 +3,13 @@ Class TestCoverage.UI.Template Extends %ZEN.Component.page [ Abstract ] Parameter APPLICATION As CLASSNAME = "TestCoverage.UI.Application"; +XData CSSPane [ XMLNamespace = "http://www.intersystems.com/zen" ] +{ + + + +} + XData TogglePane [ XMLNamespace = "http://www.intersystems.com/zen" ] { @@ -15,8 +22,12 @@ ClientMethod toggleCovered(pVisible) [ Language = javascript ] $("pre.coverage span.covered").toggleClass("hide",!pVisible); } -ClientMethod buildCodeHTML(codeLines) [ Language = javascript ] +ClientMethod buildCodeHTML(targetElement, codeLines) [ Language = javascript ] { + // Remove all children from the target to make the subsequent set of innerHTML faster. + while (targetElement.firstChild) { + targetElement.removeChild(targetElement.firstChild); + } var html = new Array(); var showCovered = zen('markCovered').getValue() html.push('
\r\n');
@@ -31,7 +42,13 @@ ClientMethod buildCodeHTML(codeLines) [ Language = javascript ]
 		html.push(line);
 	}
 	html.push('
'); - return html.join(''); + targetElement.innerHTML = html.join(''); +} + +ClientMethod renderCodeCoverage(providerID, htmlID) [ Language = javascript ] +{ + var code = zen(providerID).getContentObject().children; + zenPage.buildCodeHTML(document.getElementById(htmlID),code); } } diff --git a/cls/TestCoverage/UI/Utils.cls b/cls/TestCoverage/UI/Utils.cls index 9238700..f8af79a 100644 --- a/cls/TestCoverage/UI/Utils.cls +++ b/cls/TestCoverage/UI/Utils.cls @@ -1,16 +1,23 @@ Class TestCoverage.UI.Utils { -Query ColoredText(pTestIndex As %String, pCodeUnit As %String, pTestPath As %String = "") As %Query(ROWSPEC = "PlainText:%String,ColoredHTML:%String,Covered:%Boolean,Executable:%Boolean,RawLine:%String") [ SqlProc ] +Query ColoredText(pTestIndex As %String, pCodeUnit As %String, pTestPath As %String = "") As %Query(ROWSPEC = "PlainText:%String,ColoredHTML:%String,Covered:%Boolean,Executable:%Boolean") [ SqlProc ] { } ClassMethod ColoredTextExecute(ByRef qHandle As %Binary, pTestIndex As %String, pCodeUnit As %String, pTestPath As %String = "") As %Status { + // The initial implementation of this class query used a process-private global. + // It is faster to use local variables, and memory constraints should always be well out-of-reach for these. + // Passing everything in qHandle also has a significant performance hit on method dispatch. + #def1arg TempStorage %TempColoredText + + // Clean up TempStorage in case another query in the same process failed to. + Kill $$$TempStorage + Set qHandle = "" Set tSC = $$$OK Try { If '##class(TestCoverage.Data.CodeUnit).%ExistsId(pCodeUnit) { - Set qHandle = "" Quit } @@ -46,91 +53,35 @@ ClassMethod ColoredTextExecute(ByRef qHandle As %Binary, pTestIndex As %String, For tLineNumber=1:1:tCodeUnit.Lines.Count() { Set tText = tCodeUnit.Lines.GetAt(tLineNumber) Do tCodeStream.WriteLine(tText) - Set qHandle($i(qHandle)) = $ListBuild(tText,tText,$Bit(tCoveredLines,tLineNumber),$Bit(tCodeUnit.ExecutableLines,tLineNumber)) + Set $$$TempStorage($Increment($$$TempStorage)) = $ListBuild(tText,tText,$Bit(tCoveredLines,tLineNumber),$Bit(tCodeUnit.ExecutableLines,tLineNumber)) } // Color the code stream. Set tColoredStream = ##class(%GlobalCharacterStream).%New() Set tColorer = ##class(%Library.SyntaxColor).%New() Set tLanguage = $Case($ZConvert(tCodeUnit.Type,"L"),"cls":"CLS","int":"MAC","inc":"INC",:"COS") - Set tFlags = "PFE"_$Case(tLanguage,"CLS":"X",:"") + Set tFlags = "PFES"_$Case(tLanguage,"CLS":"X",:"") Set tGood = tColorer.Color(tCodeStream,tColoredStream,tLanguage,tFlags,,,,.tColoringErrors,.tErrorEnv,.tColoringWarnings) If tGood { - // TODO: Something with coloring/environment errors/warnings? - - // Extract colored lines (HTML) into qHandle subscripts - - // This code ensures that each line contains HTML that is valid in itself; for example, the output may have: - // foo
bar
ba
z
- - // This is normalized to three lines with: - // foo - // bar - // baz - - // Care is taken to avoid leaving certain common tags (e.g., to denote errors) unmatched on a given line. - - For tColoredLineIndex=1:1 { + For tLineNumber=1:1 { Set tColoredLine = tColoredStream.ReadLine(,.tSC) - Set tOriginalColoredLine = tColoredLine $$$ThrowOnError(tSC) If (tColoredStream.AtEnd) { Quit } - If $Extract(tColoredLine,1,7) = "
" { - Set $Extract(tColoredLine,1,7) = "" - } Else { - // Extract last font color from previous line and prepend it (if found). - Merge tPrevLine = qHandle(tColoredLineIndex - 1) - If $Data(tPrevLine) { - Set tPrevLine = $ListGet(tPrevLine,2) - Set tIndex = 0 - Set tSearchIndex = 0 - Set tSearchString = "",tIndex+1)-1) - Set tColoredLine = tStartFont_tColoredLine - } - } - } - - // Strip all and if unmatched - If $Length(tColoredLine,"") '= $Length(tColoredLine,"") { - Set tColoredLine = $Replace(tColoredLine,"","") - Set tColoredLine = $Replace(tColoredLine,"","") - } - // Ensure all tags on line are closed - Set tOpenedTags = $Length(tColoredLine,"") - If (tOpenedTags > tClosedTags) { - // Add as many times as it is missing. - Set tColoredLine = tColoredLine_$Replace($Justify("",tOpenedTags-tClosedTags)," ","") + // Sometimes there are random extra lines inserted. Detect these by looking for a colored line length shorter + // than the non-colored line. + Set tRawLine = $ListGet($$$TempStorage(tLineNumber)) + If ($Length(tColoredLine) < $Length(tRawLine)) && $Increment(tLineNumber,-1) { + Continue } // Remove line breaks Set tColoredLine = $Replace(tColoredLine,"
","") - Set $List(qHandle(tColoredLineIndex),2) = tColoredLine - - // For debugging purposes, also report the original HTML for each line. - Set $List(qHandle(tColoredLineIndex),5) = tOriginalColoredLine + Set $List($$$TempStorage(tLineNumber),2) = tColoredLine } } - - Set qHandle = "" } Catch e { Set tSC = e.AsStatus() } @@ -139,7 +90,8 @@ ClassMethod ColoredTextExecute(ByRef qHandle As %Binary, pTestIndex As %String, ClassMethod ColoredTextFetch(ByRef qHandle As %Binary, ByRef Row As %List, ByRef AtEnd As %Integer = 0) As %Status [ PlaceAfter = ColoredTextExecute ] { - Set qHandle = $Order(qHandle(qHandle),1,Row) + #def1arg TempStorage %TempColoredText + Set qHandle = $Order($$$TempStorage(qHandle),1,Row) If (qHandle = "") { Set AtEnd = 1 } @@ -148,6 +100,9 @@ ClassMethod ColoredTextFetch(ByRef qHandle As %Binary, ByRef Row As %List, ByRef ClassMethod ColoredTextClose(ByRef qHandle As %Binary) As %Status [ PlaceAfter = ColoredTextExecute ] { + #def1arg TempStorage %TempColoredText + Kill $$$TempStorage + Kill qHandle Quit $$$OK } diff --git a/cls/TestCoverage/Utils.cls b/cls/TestCoverage/Utils.cls index 51ecf08..207faca 100644 --- a/cls/TestCoverage/Utils.cls +++ b/cls/TestCoverage/Utils.cls @@ -74,9 +74,11 @@ ClassMethod GetTestCoverageTableList() As %List Quit tList } -/// Given pIntRoutines, a $ListBuild list of .INT routine names, creates snapshots of the current state of the code of each. -/// This is parallelized using %SYSTEM.WorkMgr for better performance. -ClassMethod Snapshot(pIntRoutines As %List) As %Status +/// Given pIntRoutines, a $ListBuild list of .INT routine names, creates snapshots of the current state of the code of each.
+/// This is parallelized using %SYSTEM.WorkMgr for better performance.
+/// pRelevantRoutines is a $ListBuild list of .INT routines that map back to a .CLS or .MAC +/// routine with at least one executable line. +ClassMethod Snapshot(pIntRoutines As %List, Output pRelevantRoutines As %List = "") As %Status { Set tSC = $$$OK Try { @@ -92,6 +94,27 @@ ClassMethod Snapshot(pIntRoutines As %List) As %Status Set tSC = tSnapshotQueue.WaitForComplete() $$$ThrowOnError(tSC) + + // See which routines are actually relevant (one or more lines mapping back to a class with 1 or more executable lines) + // There's no point in optimizing out .MAC routines; they'll always have code + Set tPointer = 0 + While $ListNext(pIntRoutines,tPointer,tIntRoutine) { + Set tOther = ##class(%Library.RoutineMgr).GetOther(tIntRoutine,"INT",-1) + If (tOther '= "") && ($Piece(tOther,".",*) = "CLS") { + // With the code already cached, this will be faster. + #dim tCodeUnit As TestCoverage.Data.CodeUnit + Set tSC = ##class(TestCoverage.Data.CodeUnit).GetCurrentByName(tOther,,.tCodeUnit) + If $$$ISERR(tSC) { + Continue // Non-fatal. Just skip it. + } + If '$BitCount(tCodeUnit.ExecutableLines,1) { + // Skip it - no executable lines. + Continue + } + } + Set pRelevantRoutines = pRelevantRoutines _ $ListBuild(tIntRoutine) + } + Write ! } Catch e { Set tSC = e.AsStatus() }