From 8e9c27165916ffb04108bbc8a64d6e4682ff28e6 Mon Sep 17 00:00:00 2001 From: Chris Ge Date: Mon, 24 Jun 2024 15:31:39 -0400 Subject: [PATCH 01/36] Added in a python method that calls sys.settrace and stores the results in a python list, started and stopped along with the LineByLine Monitor --- cls/TestCoverage/Manager.cls | 5 +-- cls/TestCoverage/Utils/LineByLineMonitor.cls | 36 ++++++++++++++++++-- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/cls/TestCoverage/Manager.cls b/cls/TestCoverage/Manager.cls index 4694a4e..33b1175 100644 --- a/cls/TestCoverage/Manager.cls +++ b/cls/TestCoverage/Manager.cls @@ -243,7 +243,8 @@ Method StartCoverageTracking() As %Status [ Private ] } } Set tMetrics = $ListBuild("RtnLine") _ $Select(..Timing:$ListBuild("Time","TotalTime"),1:"") - $$$ThrowOnError(..Monitor.StartWithScope(tRelevantTargets,tMetrics,tProcessIDs)) + set ^IRIS.TEMPCG($i(^IRIS.TEMPCG)) = ..CoverageClasses + $$$ThrowOnError(..Monitor.StartWithScope(tRelevantTargets,tMetrics,tProcessIDs, ..CoverageClasses)) } } Catch e { Set tSC = e.AsStatus() @@ -441,6 +442,7 @@ Method EndCoverageTracking(pTestSuite As %String = "", pTestClass As %String = " If (..CoverageTargets '= "") { // Pause the monitor. Set tSC = ..Monitor.Pause() + Do ##class(TestCoverage.Utils.LineByLineMonitor).PyStop() If $$$ISERR(tSC) { If $System.Status.GetErrorCodes(tSC) = $$$MonitorNotRunning { // Not really an error, and nothing to do in this case. @@ -762,4 +764,3 @@ ClassMethod GetURL(pRunID As %String, Output pHost As %String, Output pPath As % } } - diff --git a/cls/TestCoverage/Utils/LineByLineMonitor.cls b/cls/TestCoverage/Utils/LineByLineMonitor.cls index e219f71..fe094a5 100644 --- a/cls/TestCoverage/Utils/LineByLineMonitor.cls +++ b/cls/TestCoverage/Utils/LineByLineMonitor.cls @@ -51,8 +51,39 @@ ClassMethod CheckAvailableMemory(pProcessCount As %Integer, pRoutineCount As %In Quit tSC } +ClassMethod PyStartWithScope(pCoverageClasses As %List, CoveredLines As %List) [ Language = python ] +{ + # pass CoveredLines by reference + + from sys import settrace + import iris + tCoverageClasses = set(iris.cls('%SYS.Python').ToList(pCoverageClasses)) + CoveredLines = [] + def my_tracer(frame, event, arg = None): + # extracts frame code + code = frame.f_code + # extracts calling function name and the class that the function is in + func_name = code.co_name + class_name = frame.f_globals['__name__'] + # extracts the line number + line_no = frame.f_lineno + + #; if class_name in tCoverageClasses: # if this is in a covered class + #; print(f"A {event} encountered in {func_name}() in class {class_name} at line number {line_no}") + CoveredLines.append([line_no, func_name, class_name]) + return my_tracer + settrace(my_tracer) +} + +ClassMethod PyStop() [ Language = python ] +{ + from sys import settrace + + settrace(None) +} + /// Tracks current monitoring context and stops/starts or resets counters depending on whether it has changed -Method StartWithScope(pRoutineList As %List, pMetricList As %List, pProcessList As %List) As %Status +Method StartWithScope(pRoutineList As %List, pMetricList As %List, pProcessList As %List, pCoverageClasses As %List) As %Status { Set tSC = $$$OK Try { @@ -60,6 +91,7 @@ Method StartWithScope(pRoutineList As %List, pMetricList As %List, pProcessList If tDifferentScope && ..Started { // If we need to track different routines/metrics/processes, need to stop the monitor before restarting with the new context. Do ..Stop() + Do ..PyStop() Set ..LastRoutineList = pRoutineList Set ..LastMetricList = pMetricList Set ..LastProcessList = pProcessList @@ -67,6 +99,7 @@ Method StartWithScope(pRoutineList As %List, pMetricList As %List, pProcessList If '..Started { Set tSC = ..Start(pRoutineList, pMetricList, pProcessList) + Do ..PyStartWithScope(pCoverageClasses) If $System.Status.Equals(tSC,$$$ERRORCODE($$$MonitorMemoryAlloc)) { // Construct a more helpful error message. Set tSC = $$$EMBEDSC(..CheckAvailableMemory($ListLength(pProcessList),$ListLength(pRoutineList),1),tSC) @@ -158,4 +191,3 @@ ClassMethod GetError(key As %String, args...) } } - From b9845f86df44b44c3360e9ed48a79b3e289c6fd6 Mon Sep 17 00:00:00 2001 From: Chris Ge Date: Thu, 27 Jun 2024 11:57:59 -0400 Subject: [PATCH 02/36] Got rid of the extra argument in PyStartWithCoverage --- cls/TestCoverage/Utils/LineByLineMonitor.cls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cls/TestCoverage/Utils/LineByLineMonitor.cls b/cls/TestCoverage/Utils/LineByLineMonitor.cls index fe094a5..816f1e1 100644 --- a/cls/TestCoverage/Utils/LineByLineMonitor.cls +++ b/cls/TestCoverage/Utils/LineByLineMonitor.cls @@ -51,7 +51,7 @@ ClassMethod CheckAvailableMemory(pProcessCount As %Integer, pRoutineCount As %In Quit tSC } -ClassMethod PyStartWithScope(pCoverageClasses As %List, CoveredLines As %List) [ Language = python ] +ClassMethod PyStartWithScope(pCoverageClasses As %List) [ Language = python ] { # pass CoveredLines by reference From d591894d17fe305bfb262905896a6452609ba49a Mon Sep 17 00:00:00 2001 From: Chris Ge Date: Thu, 27 Jun 2024 15:13:51 -0400 Subject: [PATCH 03/36] Added a way to hash .PY files in CodeUnit.GetCurrentHash() --- cls/TestCoverage/Data/CodeUnit.cls | 7 +++++-- cls/TestCoverage/DataType/RoutineType.cls | 3 +-- cls/TestCoverage/Manager.cls | 1 - 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/cls/TestCoverage/Data/CodeUnit.cls b/cls/TestCoverage/Data/CodeUnit.cls index 7266342..e03e143 100644 --- a/cls/TestCoverage/Data/CodeUnit.cls +++ b/cls/TestCoverage/Data/CodeUnit.cls @@ -14,7 +14,7 @@ Index NameTypeHash On (Name, Type, Hash) [ Data = ExecutableLines, Unique ]; /// Name of the code unit Property Name As %String(MAXLEN = 255) [ Required ]; -/// Type (3-letter extension) of the code unit +/// Type (2 or 3-letter extension) of the code unit Property Type As TestCoverage.DataType.RoutineType [ Required ]; /// Hash of the code unit; for methods for determining this, see GetCurrentHash @@ -396,6 +396,10 @@ ClassMethod GetCurrentHash(pName As %String, pType As %String, Output pHash As % // Skip header (lines 1-4) which, for .INT routines generated from classes, // includes the class compilation signature. Set pHash = ..HashArrayRange(.pCodeArray,5,pName_"."_pType,.tSizeHint) + } ElseIf (pType = "PY") { + Merge pCodeArray = ^ROUTINE(pName_".py",0) + set tSizeHint = ^ROUTINE(pName_".py",0,0) + set pHash = ..HashArrayRange(.pCodeArray, ,pName_".py", .pSizeHint) } Else { // Give standard descriptive error about the type being invalid. $$$ThrowStatus(..TypeIsValid(pType)) @@ -492,4 +496,3 @@ Storage Default } } - diff --git a/cls/TestCoverage/DataType/RoutineType.cls b/cls/TestCoverage/DataType/RoutineType.cls index 4dd999a..11b4cc9 100644 --- a/cls/TestCoverage/DataType/RoutineType.cls +++ b/cls/TestCoverage/DataType/RoutineType.cls @@ -3,7 +3,6 @@ Class TestCoverage.DataType.RoutineType Extends %String [ ClassType = datatype ] Parameter MAXLEN = 3; -Parameter VALUELIST = ",CLS,MAC,INT"; +Parameter VALUELIST = ",CLS,MAC,INT,PY"; } - diff --git a/cls/TestCoverage/Manager.cls b/cls/TestCoverage/Manager.cls index 33b1175..146d28a 100644 --- a/cls/TestCoverage/Manager.cls +++ b/cls/TestCoverage/Manager.cls @@ -243,7 +243,6 @@ Method StartCoverageTracking() As %Status [ Private ] } } Set tMetrics = $ListBuild("RtnLine") _ $Select(..Timing:$ListBuild("Time","TotalTime"),1:"") - set ^IRIS.TEMPCG($i(^IRIS.TEMPCG)) = ..CoverageClasses $$$ThrowOnError(..Monitor.StartWithScope(tRelevantTargets,tMetrics,tProcessIDs, ..CoverageClasses)) } } Catch e { From d0ce282485a492e0c41868ec16c3099975c89f10 Mon Sep 17 00:00:00 2001 From: Chris Ge Date: Fri, 28 Jun 2024 15:52:14 -0400 Subject: [PATCH 04/36] Added code to find what lines in a python file are executable, and set it in the CodeUnit --- cls/TestCoverage/Data/CodeUnit.cls | 14 +++++- cls/TestCoverage/Utils.cls | 73 +++++++++++++++++++++++++++++- 2 files changed, 83 insertions(+), 4 deletions(-) diff --git a/cls/TestCoverage/Data/CodeUnit.cls b/cls/TestCoverage/Data/CodeUnit.cls index e03e143..a90bea9 100644 --- a/cls/TestCoverage/Data/CodeUnit.cls +++ b/cls/TestCoverage/Data/CodeUnit.cls @@ -98,12 +98,22 @@ ClassMethod GetCurrentByName(pInternalName As %String, pSourceNamespace As %Stri } Set $Namespace = pSourceNamespace + If (tType = "CLS") { Do ##class(TestCoverage.Utils).GetClassLineExecutableFlags(tName,.tCodeArray,.tExecutableFlags) - } Else { + } ElseIf ((tType = "INT") || (tType = "MAC")) { Do ##class(TestCoverage.Utils).GetRoutineLineExecutableFlags(.tCodeArray,.tExecutableFlags) + } ElseIf (tType="PY") { + Do ##class(TestCoverage.Utils).CodeArrayToList(.tCodeArray, .pDocumentText) + Set tExecutableFlagsPyList = ##class(TestCoverage.Utils).GetPythonLineExecutableFlags(pDocumentText) + Kill tExecutableFlags + for i=1:1:tExecutableFlagsPyList."__len__()"-1 { + set tExecutableFlags(i) = tExecutableFlagsPyList."__getitem__"(i) + } + } + Else { + return $$$ERROR($$$GeneralError,"File type not supported") } - Set $Namespace = tOriginalNamespace Set pCodeUnit = ..%New() Set pCodeUnit.Name = tName diff --git a/cls/TestCoverage/Utils.cls b/cls/TestCoverage/Utils.cls index 2cd8662..cd6d5b0 100644 --- a/cls/TestCoverage/Utils.cls +++ b/cls/TestCoverage/Utils.cls @@ -391,6 +391,77 @@ ClassMethod GetClassLineExecutableFlags(pClassName As %String, ByRef pDocumentTe } } +ClassMethod CodeArrayToList(ByRef pCodeArray, Output pDocumentText As %List) +{ + set pDocumentText = $lb() + for i=1:1:pCodeArray(0) { + set $list(pDocumentText, i) = pCodeArray(i) + } + quit +} + +/// returns a python list with a 1 or 0 for subscript i indicating if line i is executable or not +ClassMethod GetPythonLineExecutableFlags(pDocumentText) [ Language = python ] +{ + import iris + import ast + source_lines = iris.cls('%SYS.Python').ToList(pDocumentText) + source_lines = [line + "\n" for line in source_lines] # contains a list of each line of the source code + print(source_lines) + glob = iris.gref('IRIS.TEMPCG') + + # create the abstract syntax tree for the code, and walk through it, getting each line of code in its context + source = ''.join(source_lines) + tree = ast.parse(source) + executable_lines = set() # stores the 1-indexed line numbers of the executable lines + + class ExecutableLineVisitor(ast.NodeVisitor): + def __init__(self): + self.function_depth = 0 + + def visit(self, node): + if hasattr(node, 'lineno'): + + # decorators for functions and class definitions are executable + if isinstance(node, (ast.FunctionDef, ast.ClassDef, ast.AsyncFunctionDef)): + decorators = [element.id for element in node.decorator_list] + num_decorators = len(decorators) + # print(f"{node.lineno=}") + for i, element in enumerate(decorators): + # print(f"{element=}") + conjectured_line = (node.lineno-1)-num_decorators+i # change this back if the line numbers aren't 0 indexed + # print(f"{source_lines[conjectured_line]=}") + if "@" + element in source_lines[conjectured_line]: + #print(f"added {element=}") + executable_lines.add(conjectured_line+1) # because back to 1-indexing + executable_lines.add(node.lineno) + elif isinstance(node, (ast.Call, + ast.Return, ast.Assign, ast.AugAssign, ast.AnnAssign, + ast.For, ast.AsyncFor, ast.While, ast.If, ast.With, + ast.AsyncWith, ast.Raise, ast.Try, ast.Assert, + ast.Import, ast.ImportFrom, ast.Pass, + ast.Break, ast.Continue, ast.Delete, ast.Yield, + ast.YieldFrom, ast.Await, ast.Nonlocal)): # all executable + executable_lines.add(node.lineno) + elif isinstance(node, ast.ExceptHandler): # except (but not finally) is executable + executable_lines.add(node.lineno) + elif isinstance(node, ast.Expr) and not isinstance(node.value, ast.Constant): # expressions that aren't docstrings are executable + executable_lines.add(node.lineno) + self.generic_visit(node) + ExecutableLineVisitor().visit(tree) + + output = [0] * (len(source_lines)+1) + for line in executable_lines: + output[line] = 1 + output[1] = 0 # manually set the class definition to be not executable + def print_executable_lines(): + for i, line in enumerate(source_lines, start=1): + is_exec = output[i] + print(f"{i:2d} {'*' if is_exec else ' '} {line.rstrip()}") + print_executable_lines() + return output +} + /// For a routine (.MAC/.INT) with code in pDocumentText as an integer-subscripted array of lines, /// returns an array (pExecutableFlags) subscripted by line with boolean flags indicating whether the corresponding line is executable. ClassMethod GetRoutineLineExecutableFlags(ByRef pDocumentText, Output pExecutableFlags) @@ -400,7 +471,6 @@ ClassMethod GetRoutineLineExecutableFlags(ByRef pDocumentText, Output pExecutabl For tDocLine=1:1:$Get(pDocumentText) { Do tSourceStream.WriteLine(pDocumentText(tDocLine)) } - Set tSC = ##class(%Library.SyntaxColorReader).FromCode(tSourceStream,"COS","A",.tSCReader) $$$ThrowOnError(tSC) @@ -540,4 +610,3 @@ ClassMethod LineByLineMonitorResultClose(ByRef qHandle As %Binary) As %Status [ } } - From e9860ced791592f0090675c431154882a5a85495 Mon Sep 17 00:00:00 2001 From: Chris Ge Date: Mon, 1 Jul 2024 14:04:26 -0400 Subject: [PATCH 05/36] Finished GetCurrentByName and UpdateSourceMap: GetCurrentByName now stores the MethodMap (line numbers for each method), and executable lines got a bug fix. UpdateSourceMap now saves mapped line numbers from .py to .cls --- cls/TestCoverage/Data/CodeUnit.cls | 143 +++++++++++++++++++++-------- cls/TestCoverage/Utils.cls | 56 +++++++++-- 2 files changed, 154 insertions(+), 45 deletions(-) diff --git a/cls/TestCoverage/Data/CodeUnit.cls b/cls/TestCoverage/Data/CodeUnit.cls index a90bea9..4516614 100644 --- a/cls/TestCoverage/Data/CodeUnit.cls +++ b/cls/TestCoverage/Data/CodeUnit.cls @@ -28,6 +28,7 @@ Property ExecutableLines As TestCoverage.DataType.Bitstring; /// For classes, map of method names in the code to their associated line numbers /// For routines, map of labels to associated line numbers +/// For python, map of method names to a $lb(starting line number, ending line number) Property MethodMap As array Of %Integer; /// For classes, map of line numbers in code to associated method names @@ -98,7 +99,7 @@ ClassMethod GetCurrentByName(pInternalName As %String, pSourceNamespace As %Stri } Set $Namespace = pSourceNamespace - + If (tType = "CLS") { Do ##class(TestCoverage.Utils).GetClassLineExecutableFlags(tName,.tCodeArray,.tExecutableFlags) } ElseIf ((tType = "INT") || (tType = "MAC")) { @@ -107,13 +108,14 @@ ClassMethod GetCurrentByName(pInternalName As %String, pSourceNamespace As %Stri Do ##class(TestCoverage.Utils).CodeArrayToList(.tCodeArray, .pDocumentText) Set tExecutableFlagsPyList = ##class(TestCoverage.Utils).GetPythonLineExecutableFlags(pDocumentText) Kill tExecutableFlags - for i=1:1:tExecutableFlagsPyList."__len__()"-1 { + for i=1:1:tExecutableFlagsPyList."__len__"()-1 { set tExecutableFlags(i) = tExecutableFlagsPyList."__getitem__"(i) } } Else { return $$$ERROR($$$GeneralError,"File type not supported") } + Set $Namespace = tOriginalNamespace Set pCodeUnit = ..%New() Set pCodeUnit.Name = tName @@ -130,46 +132,80 @@ ClassMethod GetCurrentByName(pInternalName As %String, pSourceNamespace As %Stri Set pCodeUnit.Generated = ($$$comClassKeyGet(tName,$$$cCLASSgeneratedby) '= "") } - Set tMethod = "" - Set tMethodSignature = "" - Set tMethodMask = "" - For tLineNumber=1:1:$Get(tCodeArray,0) { - Set tLine = tCodeArray(tLineNumber) - Do pCodeUnit.Lines.Insert(tLine) - - If (tType = "CLS") { - // Extract line offset of methods in classes - Set tStart = $Piece(tLine," ") - If (tStart = "ClassMethod") || (tStart = "Method") { - Set tMethod = $Piece($Piece(tLine,"(")," ",2) - Set tMethodSignature = tLine - Do pCodeUnit.MethodMap.SetAt(tLineNumber,tMethod) - Do pCodeUnit.LineToMethodMap.SetAt(tMethod,tLineNumber) - } ElseIf ($Extract(tStart) = "{") { - // Ignore the opening bracket for a method. - } ElseIf ($Extract(tStart) = "}") && (tMethod '= "") { - // End of method. Add method subunit to class. - Set tSubUnit = ##class(TestCoverage.Data.CodeSubUnit.Method).%New() - Set tSubUnit.Name = tMethod - Set tSubUnit.DisplaySignature = tMethodSignature - Set tSubUnit.Mask = tMethodMask - Do pCodeUnit.SubUnits.Insert(tSubUnit) - Set tMethod = "" - Set tMethodSignature = "" - Set tMethodMask = "" - } ElseIf (tMethod '= "") { - Set $Bit(tMethodMask,tLineNumber) = 1 + + If (tType = "PY") { + Set ClassName = $Piece(tName,".", *) + Set tMethodInfo = ##class(TestCoverage.Utils).GetPythonMethodMapping(pDocumentText, ClassName) + Set tLineToMethodInfo = tMethodInfo."__getitem__"(0) + Set tMethodMapInfo = tMethodInfo."__getitem__"(1) + for i=1:1:$listlength(pDocumentText) { + Set tMethod = tLineToMethodInfo."__getitem__"(i) + Do pCodeUnit.LineToMethodMap.SetAt(tMethod,i) + } + Set iterator = tMethodMapInfo."__iter__"() + for i=1:1:tMethodMapInfo."__len__"() { + Set tMethod = iterator."__next__"() + Set tStartEnd = tMethodMapInfo."__getitem__"(tMethod) + Set tStartLine = tStartEnd."__getitem__"(0) + Set tEndLine = tStartEnd."__getitem__"(1) + Do pCodeUnit.MethodMap.SetAt($lb(tStartLine, tEndLine),tMethod) + + Set tMethodMask = "" + for j = tStartLine:1:tEndLine { + Set $Bit(tMethodMask,j) = 1 } - } Else { - // Extract line offset of labels in routines - If ($ZStrip($Extract(tLine),"*PWC") '= "") { - Set tLabel = $Piece($Piece(tLine," "),"(") - Do pCodeUnit.MethodMap.SetAt(tLineNumber,tLabel) - Do pCodeUnit.LineToMethodMap.SetAt(tLabel,tLineNumber) + Set tMethodSignature = $list(pDocumentText, tStartLine) + Set tSubUnit = ##class(TestCoverage.Data.CodeSubUnit.Method).%New() + Set tSubUnit.Name = tMethod + Set tSubUnit.DisplaySignature = tMethodSignature + Set tSubUnit.Mask = tMethodMask + Do pCodeUnit.SubUnits.Insert(tSubUnit) + } + } + Else { + Set tMethod = "" + Set tMethodSignature = "" + Set tMethodMask = "" + For tLineNumber=1:1:$Get(tCodeArray,0) { + Set tLine = tCodeArray(tLineNumber) + Do pCodeUnit.Lines.Insert(tLine) + + If (tType = "CLS") { + // Extract line offset of methods in classes + Set tStart = $Piece(tLine," ") + If (tStart = "ClassMethod") || (tStart = "Method") { + Set tMethod = $Piece($Piece(tLine,"(")," ",2) + Set tMethodSignature = tLine + Do pCodeUnit.MethodMap.SetAt(tLineNumber,tMethod) + Do pCodeUnit.LineToMethodMap.SetAt(tMethod,tLineNumber) + } ElseIf ($Extract(tStart) = "{") { + // Ignore the opening bracket for a method. + } ElseIf ($Extract(tStart) = "}") && (tMethod '= "") { + // End of method. Add method subunit to class. + Set tSubUnit = ##class(TestCoverage.Data.CodeSubUnit.Method).%New() + Set tSubUnit.Name = tMethod + Set tSubUnit.DisplaySignature = tMethodSignature + Set tSubUnit.Mask = tMethodMask + Do pCodeUnit.SubUnits.Insert(tSubUnit) + Set tMethod = "" + Set tMethodSignature = "" + Set tMethodMask = "" + } ElseIf (tMethod '= "") { + Set $Bit(tMethodMask,tLineNumber) = 1 + } + } + Else { + // Extract line offset of labels in routines + If ($ZStrip($Extract(tLine),"*PWC") '= "") { + Set tLabel = $Piece($Piece(tLine," "),"(") + Do pCodeUnit.MethodMap.SetAt(tLineNumber,tLabel) + Do pCodeUnit.LineToMethodMap.SetAt(tLabel,tLineNumber) + } } } } + Set tSC = pCodeUnit.%Save() If $$$ISERR(tSC) && $System.Status.Equals(tSC,$$$ERRORCODE($$$IDKeyNotUnique)) { // Some other process beat us to it. @@ -197,6 +233,7 @@ Method UpdateSourceMap(pSourceNamespace As %String, ByRef pCache) As %Status { Set tSC = $$$OK Try { + // First, build local array (tMap) of all maps from the .INT file to other files. If (..Type = "INT") { For tLineNumber=1:1:..Lines.Count() { @@ -235,6 +272,36 @@ Method UpdateSourceMap(pSourceNamespace As %String, ByRef pCache) As %Status } } } + If (..Type = "PY") { + + set tClass = ..Name + Set tSourceUnits(tClass_".CLS") = "" + $$$ThrowOnError(..GetCurrentByName(tClass _ ".CLS", pSourceNamespace, .pCLSCodeUnit, .pCLSCache)) + // we'll do the mappings from the .py to the .cls direction, so that we don't iterate over objectscript lines + Set tMethod = "" + Do ..MethodMap.GetNext(.tMethod) + while (tMethod '= "") + { + Set tCLSMethodNum = pCLSCodeUnit.MethodMap.GetAt(tMethod) + Set $lb(tMethodStart, tMethodEnd) = ..MethodMap.GetAt(tMethod) + Set tMethodName = tMethod + Set tFullMap(tMethodStart) = $lb("CLS", tClass,tMethodName, -1, -1) ; -1 because the class + ; definition doesn't have the +1 offset from the { + + Do ..MethodMap.GetNext(.tMethod) + For i = tMethodStart+1:1:tMethodEnd { + Set tClassLineNum = i-tMethodStart + Set tFullMap(i) = $lb("CLS", tClass,tMethodName, tClassLineNum, tClassLineNum) + + // extra check to make sure that the lines we're mapping between are the same as expected + Set tClassLineCode = $zstrip(pCLSCodeUnit.Lines.GetAt(tCLSMethodNum + tClassLineNum + 1), "<>W") + Set tPyLineCode = $zstrip(..Lines.GetAt(i), "<>W") + if (tPyLineCode '= tClassLineCode) { + Set tSC = $$$ERROR($$$GeneralError,"Compiled .py code doesn't match .CLS python code ") + } + } + } + } // If we are a generator .INT file, ensure that we have source for the original class populated. // In such files, the second line looks like (for example): @@ -280,7 +347,7 @@ Method UpdateSourceMap(pSourceNamespace As %String, ByRef pCache) As %Status Set tCodeUnits(tCodeUnit.Type,tCodeUnit.Name) = tCodeUnit } - // Create CodeUnitMap data based on .INT->.CLS mapping. + // Create CodeUnitMap data based on .INT / .py ->.CLS mapping. Set tFromHash = ..Hash Set tLineNumber = "" For { diff --git a/cls/TestCoverage/Utils.cls b/cls/TestCoverage/Utils.cls index cd6d5b0..a4a1360 100644 --- a/cls/TestCoverage/Utils.cls +++ b/cls/TestCoverage/Utils.cls @@ -400,6 +400,54 @@ ClassMethod CodeArrayToList(ByRef pCodeArray, Output pDocumentText As %List) quit } +ClassMethod GetPythonMethodMapping(pDocumentText, ClassName) [ Language = python ] +{ + import iris + import ast + source_lines = iris.cls('%SYS.Python').ToList(pDocumentText) + source_lines = [line + "\n" for line in source_lines] # contains a list of each line of the source code + + class_name = ClassName + source = ''.join(source_lines) + tree = ast.parse(source) + line_function_map = [None] * (len(source_lines)+2) + method_map = {} + + class FunctionMapper(ast.NodeVisitor): + def __init__(self): + self.current_class = None + self.current_function = None + self.outermost_function = None + + def visit_ClassDef(self, node): + prev_class = self.current_class + self.current_class = node.name + self.generic_visit(node) + self.current_class = prev_class + + def visit_FunctionDef(self, node): + if self.outermost_function is None: + self.outermost_function = node.name + method_map[node.name] = (node.lineno-1, node.end_lineno-1) + + self.current_function = node.name + for lineno in range(node.lineno, node.end_lineno + 1): + line_function_map[lineno-1] = self.outermost_function + + self.generic_visit(node) + self.current_function = None + if self.outermost_function == node.name: + self.outermost_function = None + + tree_with_line_numbers = ast.increment_lineno(tree, n=1) + for node in ast.walk(tree_with_line_numbers): + if isinstance(node, ast.FunctionDef): + node.end_lineno = node.body[-1].end_lineno + + FunctionMapper().visit(tree) + return (line_function_map, method_map) +} + /// returns a python list with a 1 or 0 for subscript i indicating if line i is executable or not ClassMethod GetPythonLineExecutableFlags(pDocumentText) [ Language = python ] { @@ -407,8 +455,6 @@ ClassMethod GetPythonLineExecutableFlags(pDocumentText) [ Language = python ] import ast source_lines = iris.cls('%SYS.Python').ToList(pDocumentText) source_lines = [line + "\n" for line in source_lines] # contains a list of each line of the source code - print(source_lines) - glob = iris.gref('IRIS.TEMPCG') # create the abstract syntax tree for the code, and walk through it, getting each line of code in its context source = ''.join(source_lines) @@ -426,13 +472,9 @@ ClassMethod GetPythonLineExecutableFlags(pDocumentText) [ Language = python ] if isinstance(node, (ast.FunctionDef, ast.ClassDef, ast.AsyncFunctionDef)): decorators = [element.id for element in node.decorator_list] num_decorators = len(decorators) - # print(f"{node.lineno=}") for i, element in enumerate(decorators): - # print(f"{element=}") conjectured_line = (node.lineno-1)-num_decorators+i # change this back if the line numbers aren't 0 indexed - # print(f"{source_lines[conjectured_line]=}") if "@" + element in source_lines[conjectured_line]: - #print(f"added {element=}") executable_lines.add(conjectured_line+1) # because back to 1-indexing executable_lines.add(node.lineno) elif isinstance(node, (ast.Call, @@ -458,7 +500,7 @@ ClassMethod GetPythonLineExecutableFlags(pDocumentText) [ Language = python ] for i, line in enumerate(source_lines, start=1): is_exec = output[i] print(f"{i:2d} {'*' if is_exec else ' '} {line.rstrip()}") - print_executable_lines() + # print_executable_lines() return output } From baf4f708704e6248f9b6decf4cb3a8f2311c8312 Mon Sep 17 00:00:00 2001 From: Chris Ge Date: Tue, 2 Jul 2024 10:14:46 -0400 Subject: [PATCH 06/36] Wrote code that successfully saves the line monitoring into ^IRIS.TEMPCJG (as well as code that unsuccessfully tries to save it into SQL) --- cls/TestCoverage/Data/PyMonitorResults.cls | 62 ++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 cls/TestCoverage/Data/PyMonitorResults.cls diff --git a/cls/TestCoverage/Data/PyMonitorResults.cls b/cls/TestCoverage/Data/PyMonitorResults.cls new file mode 100644 index 0000000..2756830 --- /dev/null +++ b/cls/TestCoverage/Data/PyMonitorResults.cls @@ -0,0 +1,62 @@ +Class TestCoverage.Data.PyMonitorResults Extends %Persistent [ DdlAllowed ] +{ + +Property ClassName As %String(MAXLEN = 255) [ Required ]; + +Property FunctionName As %String(MAXLEN = 255) [ Required ]; + +Property LineNumber As %Integer [ Required ]; + +Storage Default +{ + + +%%CLASSNAME + + +ClassName + + +FunctionName + + +LineNumber + + +^TestCovera3FF5.PyMonitorReC80BD +PyMonitorResultsDefaultData +1 +^TestCovera3FF5.PyMonitorReC80BD +^TestCovera3FF5.PyMonitorReC80BI + +2 +.999999: +0.0001% + + +3 +1 + + +16 +.999999:"EP.sysSettrace" +0.0001% + + +11 +.999999:"TestAsync" +0.0001% + + +3 +.999999:28 +0.0001% + + +-4 + +^TestCovera3FF5.PyMonitorReC80BS +%Storage.Persistent +} + +} From 91af8be5a616c21f4f2d545306390023c8b3f8c2 Mon Sep 17 00:00:00 2001 From: Chris Ge Date: Tue, 2 Jul 2024 10:15:50 -0400 Subject: [PATCH 07/36] Meant to be included with the previous commit --- cls/TestCoverage/Utils/LineByLineMonitor.cls | 73 ++++++++++++++++++-- 1 file changed, 67 insertions(+), 6 deletions(-) diff --git a/cls/TestCoverage/Utils/LineByLineMonitor.cls b/cls/TestCoverage/Utils/LineByLineMonitor.cls index 816f1e1..b7e34c1 100644 --- a/cls/TestCoverage/Utils/LineByLineMonitor.cls +++ b/cls/TestCoverage/Utils/LineByLineMonitor.cls @@ -53,12 +53,12 @@ ClassMethod CheckAvailableMemory(pProcessCount As %Integer, pRoutineCount As %In ClassMethod PyStartWithScope(pCoverageClasses As %List) [ Language = python ] { - # pass CoveredLines by reference from sys import settrace import iris + tCoverageClasses = set(iris.cls('%SYS.Python').ToList(pCoverageClasses)) - CoveredLines = [] + #; glob = iris.gref('^IRIS.TEMPCG') def my_tracer(frame, event, arg = None): # extracts frame code code = frame.f_code @@ -68,17 +68,77 @@ ClassMethod PyStartWithScope(pCoverageClasses As %List) [ Language = python ] # extracts the line number line_no = frame.f_lineno - #; if class_name in tCoverageClasses: # if this is in a covered class - #; print(f"A {event} encountered in {func_name}() in class {class_name} at line number {line_no}") - CoveredLines.append([line_no, func_name, class_name]) + if class_name in tCoverageClasses: # if this is in a covered class + print(f"A {event} encountered in {func_name}() in class {class_name} at line number {line_no}") + tGlob = iris.gref('^IRIS.TEMPCJG') + curId = tGlob.get([None]) + if not curId: + tGlob[None] = 1 + curId = 1 + tGlob[curId, 1] = class_name + tGlob[curId, 2] = func_name + tGlob[curId, 3] = line_no + tGlob[None] = curId+1 + #; iris.execute('set ^TestCovera3FF5.PyMonitorReC80BD($i(^TestCovera3FF5.PyMonitorReC80BD)) = $listbuild("", ^IRIS.TEMPCJG(1), ^IRIS.TEMPCJG(2),^IRIS.TEMPCJG(3) )') + + #; glob = iris.gref('^TestCovera3FF5.PyMonitorReC80BD') + #; id = glob.get([None])+1 + + #; glob([None]) = id + + #; new_line = iris.cls("TestCoverage.Data.PyMonitorResults")._New() + #; new_line.ClassName = class_name + #; new_line.FunctionName = func_name + #; new_line.LineNumber = line_no + #; new_line._Save() + + #; stmt = iris.sql.prepare("INSERT INTO TestCoverage_Data.PyMonitorResults (ClassName, FunctionName, LineNumber) VALUES (?, ?, ?)") + #; try: + #; rs = stmt.execute(class_name, func_name, line_no) + #; except Exception as ex: + #; print(ex.sqlcode, ex.message) + + # iris.cls("TestCoverage.Utils.LineByLineMonitor").DoNothing() + # iris.cls("TestCoverage.Utils.LineByLineMonitor").SavePyLine(line_no, func_name, class_name) + #; glob[0] = "we make it back" return my_tracer settrace(my_tracer) } +ClassMethod PyClearCounters() +{ + Kill ^IRIS.TEMPCJG + #; &sql( + #; delete from TestCoverage_Data.PyMonitorResults + #; ) + #; If (SQLCODE < 0) { + #; Throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) + #; } +} + +ClassMethod DoNothing() +{ + set x = 1 +} + +ClassMethod SavePyLine(LineNumber As %Integer, FunctionName As %String, ClassName As %String) +{ + // set ^IRIS.TEMPCG($i(^IRIS.TEMPCG)) = $lb(LineNumber, FunctionName, ClassName) + #; &sql( + #; INSERT INTO TestCoverage_Data.PyMonitorResults + #; (ClassName, FunctionName, LineNumber) + #; VALUES (:ClassName, :FunctionName, :LineNumber) + #; ) + #; set ^IRIS.TEMPCG($i(^IRIS.TEMPCG)) = $lb("after", SQLCODE) + + #; If (SQLCODE < 0) { + #; Throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) + #; } +} + ClassMethod PyStop() [ Language = python ] { from sys import settrace - settrace(None) } @@ -98,6 +158,7 @@ Method StartWithScope(pRoutineList As %List, pMetricList As %List, pProcessList } If '..Started { + Do ..PyClearCounters() Set tSC = ..Start(pRoutineList, pMetricList, pProcessList) Do ..PyStartWithScope(pCoverageClasses) If $System.Status.Equals(tSC,$$$ERRORCODE($$$MonitorMemoryAlloc)) { From 757093887661f1749bcc5093d04aac224a4c05a0 Mon Sep 17 00:00:00 2001 From: Chris Ge Date: Tue, 2 Jul 2024 15:39:11 -0400 Subject: [PATCH 08/36] Storing the sys.settrace data as an array indexed by class name, and then wrote StorePyCoverage to store that data in a Coverage --- cls/TestCoverage/Data/Coverage.cls | 61 ++++++++++++-------- cls/TestCoverage/Utils/LineByLineMonitor.cls | 23 ++------ 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/cls/TestCoverage/Data/Coverage.cls b/cls/TestCoverage/Data/Coverage.cls index 9c8e2be..868613a 100644 --- a/cls/TestCoverage/Data/Coverage.cls +++ b/cls/TestCoverage/Data/Coverage.cls @@ -50,12 +50,13 @@ Property Time As array Of TestCoverage.DataType.Timing [ SqlFieldName = _TIME ]; /// List of "TotalTime" measurements from line-by-line monitor, subscripted by line number Property TotalTime As array Of TestCoverage.DataType.Timing; -ClassMethod StoreIntCoverage(pRun As %Integer, pTestPath As %String, pRoutineName As %String, ByRef pCache) As %Status +ClassMethod StoreIntCoverage(pRun As %Integer, pTestPath As %String, pName As %String, pType As %String, ByRef pCache) As %Status { + // pType must be either INT or PY Set tSC = $$$OK Try { #dim tResult As %SQL.StatementResult - Set tSC = ##class(TestCoverage.Data.CodeUnit).GetCurrentByName(pRoutineName_".INT",,.tCodeUnit,.pCache) + Set tSC = ##class(TestCoverage.Data.CodeUnit).GetCurrentByName(pName_"."_pType,,.tCodeUnit,.pCache) $$$ThrowOnError(tSC) If ..UniqueCoverageDataExists(pRun,tCodeUnit.Hash,pTestPath,.tID) { @@ -70,33 +71,44 @@ ClassMethod StoreIntCoverage(pRun As %Integer, pTestPath As %String, pRoutineNam } Set tCoveredLines = tInstance.CoveredLines - - Set tAvailableMetrics = ..GetAvailableMetrics() - Set tPointer = 0 - While $ListNext(tAvailableMetrics,tPointer,tMetricKey) { - If tInstance.Run.Metrics.Find(tMetricKey) { - Set tMetrics(tMetricKey) = $Property(tInstance,tMetricKey) + if (pType = "INT") + { + Set tAvailableMetrics = ..GetAvailableMetrics() + Set tPointer = 0 + While $ListNext(tAvailableMetrics,tPointer,tMetricKey) { + If tInstance.Run.Metrics.Find(tMetricKey) { + Set tMetrics(tMetricKey) = $Property(tInstance,tMetricKey) + } } - } - - // ROWSPEC = "LineNumber:%Integer,LineCovered:%Boolean,RtnLine:%Integer,Time:%Numeric,TotalTime:%Numeric" - Set tResult = ##class(TestCoverage.Utils).LineByLineMonitorResultFunc(pRoutineName) - While tResult.%Next(.tSC) { - $$$ThrowOnError(tSC) - Set tLineNumber = tResult.%Get("LineNumber") - If tResult.%Get("LineCovered") { - Set $Bit(tCoveredLines,tLineNumber) = 1 + // ROWSPEC = "LineNumber:%Integer,LineCovered:%Boolean,RtnLine:%Integer,Time:%Numeric,TotalTime:%Numeric" + Set tResult = ##class(TestCoverage.Utils).LineByLineMonitorResultFunc(pName) + While tResult.%Next(.tSC) { + $$$ThrowOnError(tSC) + Set tLineNumber = tResult.%Get("LineNumber") + If tResult.%Get("LineCovered") { + Set $Bit(tCoveredLines,tLineNumber) = 1 + } + Set tMetricKey = "" + For { + Set tMetricKey = $Order(tMetrics(tMetricKey),1,tMetric) + If (tMetricKey = "") { + Quit + } + Do tMetric.SetAt(tResult.%Get(tMetricKey) + tMetric.GetAt(tLineNumber),tLineNumber) + } } - Set tMetricKey = "" - For { - Set tMetricKey = $Order(tMetrics(tMetricKey),1,tMetric) - If (tMetricKey = "") { - Quit + $$$ThrowOnError(tSC) + } + Else { // If pType = "PY" + //^IRIS.TEMPCJG(148,1)="EP.sysSettrace", ^IRIS.TEMPCJG(148,2)="fun_generator", ^IRIS.TEMPCJG(148,3)=120 + if $Data(^IRIS.TEMPCJG(pName)) { + for i = 1:1:(^IRIS.TEMPCJG(pName)-1) { + Set tLineNumber = ^IRIS.TEMPCJG(pName, i) + Set $Bit(tCoveredLines, tLineNumber) = 1 } - Do tMetric.SetAt(tResult.%Get(tMetricKey) + tMetric.GetAt(tLineNumber),tLineNumber) } + } - $$$ThrowOnError(tSC) Set tInstance.CoveredLines = $BitLogic(tInstance.CoveredLines|tCoveredLines) @@ -167,4 +179,3 @@ Storage Default } } - diff --git a/cls/TestCoverage/Utils/LineByLineMonitor.cls b/cls/TestCoverage/Utils/LineByLineMonitor.cls index b7e34c1..af0e802 100644 --- a/cls/TestCoverage/Utils/LineByLineMonitor.cls +++ b/cls/TestCoverage/Utils/LineByLineMonitor.cls @@ -58,7 +58,6 @@ ClassMethod PyStartWithScope(pCoverageClasses As %List) [ Language = python ] import iris tCoverageClasses = set(iris.cls('%SYS.Python').ToList(pCoverageClasses)) - #; glob = iris.gref('^IRIS.TEMPCG') def my_tracer(frame, event, arg = None): # extracts frame code code = frame.f_code @@ -67,25 +66,17 @@ ClassMethod PyStartWithScope(pCoverageClasses As %List) [ Language = python ] class_name = frame.f_globals['__name__'] # extracts the line number line_no = frame.f_lineno - - if class_name in tCoverageClasses: # if this is in a covered class - print(f"A {event} encountered in {func_name}() in class {class_name} at line number {line_no}") + if class_name in tCoverageClasses and line_no != 1: # if this is in a covered class + # print(f"A {event} encountered in {func_name}() in class {class_name} at line number {line_no}") tGlob = iris.gref('^IRIS.TEMPCJG') - curId = tGlob.get([None]) + curId = tGlob.get([class_name]) if not curId: - tGlob[None] = 1 + tGlob[class_name] = 1 curId = 1 - tGlob[curId, 1] = class_name - tGlob[curId, 2] = func_name - tGlob[curId, 3] = line_no - tGlob[None] = curId+1 + tGlob[class_name, curId] = line_no + tGlob[class_name] = curId+1 #; iris.execute('set ^TestCovera3FF5.PyMonitorReC80BD($i(^TestCovera3FF5.PyMonitorReC80BD)) = $listbuild("", ^IRIS.TEMPCJG(1), ^IRIS.TEMPCJG(2),^IRIS.TEMPCJG(3) )') - #; glob = iris.gref('^TestCovera3FF5.PyMonitorReC80BD') - #; id = glob.get([None])+1 - - #; glob([None]) = id - #; new_line = iris.cls("TestCoverage.Data.PyMonitorResults")._New() #; new_line.ClassName = class_name #; new_line.FunctionName = func_name @@ -123,13 +114,11 @@ ClassMethod DoNothing() ClassMethod SavePyLine(LineNumber As %Integer, FunctionName As %String, ClassName As %String) { - // set ^IRIS.TEMPCG($i(^IRIS.TEMPCG)) = $lb(LineNumber, FunctionName, ClassName) #; &sql( #; INSERT INTO TestCoverage_Data.PyMonitorResults #; (ClassName, FunctionName, LineNumber) #; VALUES (:ClassName, :FunctionName, :LineNumber) #; ) - #; set ^IRIS.TEMPCG($i(^IRIS.TEMPCG)) = $lb("after", SQLCODE) #; If (SQLCODE < 0) { #; Throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) From 642b8135cf3c674ac0c69164b660e5ea2dd47dea Mon Sep 17 00:00:00 2001 From: Chris Ge Date: Tue, 2 Jul 2024 17:18:53 -0400 Subject: [PATCH 09/36] Wrote the HasPython Method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added ..CoveragePythons Changed AddCoverageClass to add to ..CoveragePythons Added ..PyCoverageTargets Changed SetCoverageTargets (which is called when the user actually passes in coverage classes and routines, instead of it being in coverage.list) to fill in PyCoverageTargets with just the non .py part of the class name Got UpdateCoverageTargetsForTestDirectory to update ..PyCoverageTargets in the case where there’s a coverage.list Handle StartCoverageTracking seems redundant to include the python in the new / relevant stuff: it’s new or relevant if its class is new / relevant Added the python components of the old relevant classes into tPyRelevantTargets With that being said, I think we need Snapshot to return the python targets for the relevant classes In Snapshot: checked if the class has a python part (if it does, it’s relevant), and added those to the list of python new relevant targets. Took a snapshot of each of those Combined tPyNewRelevantTargets into tPyRelevantTargets --- cls/TestCoverage/Data/CodeUnit.cls | 16 ++++- cls/TestCoverage/Data/Coverage.cls | 1 + cls/TestCoverage/Manager.cls | 62 ++++++++++++++++++-- cls/TestCoverage/Utils.cls | 21 ++++++- cls/TestCoverage/Utils/LineByLineMonitor.cls | 8 ++- 5 files changed, 96 insertions(+), 12 deletions(-) diff --git a/cls/TestCoverage/Data/CodeUnit.cls b/cls/TestCoverage/Data/CodeUnit.cls index 4516614..eff9bc0 100644 --- a/cls/TestCoverage/Data/CodeUnit.cls +++ b/cls/TestCoverage/Data/CodeUnit.cls @@ -28,9 +28,12 @@ Property ExecutableLines As TestCoverage.DataType.Bitstring; /// For classes, map of method names in the code to their associated line numbers /// For routines, map of labels to associated line numbers -/// For python, map of method names to a $lb(starting line number, ending line number) +/// For python, map of method names to associated starting line number Property MethodMap As array Of %Integer; +/// Only for python: map of method names to associated ending line number of the method +Property MethodEndMap 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 %Dictionary.CacheIdentifier [ Private ]; @@ -148,7 +151,8 @@ ClassMethod GetCurrentByName(pInternalName As %String, pSourceNamespace As %Stri Set tStartEnd = tMethodMapInfo."__getitem__"(tMethod) Set tStartLine = tStartEnd."__getitem__"(0) Set tEndLine = tStartEnd."__getitem__"(1) - Do pCodeUnit.MethodMap.SetAt($lb(tStartLine, tEndLine),tMethod) + Do pCodeUnit.MethodMap.SetAt(tStartLine,tMethod) + Do pCodeUnit.MethodEndMap.SetAt(tEndLine, tMethod) Set tMethodMask = "" for j = tStartLine:1:tEndLine { @@ -283,7 +287,8 @@ Method UpdateSourceMap(pSourceNamespace As %String, ByRef pCache) As %Status while (tMethod '= "") { Set tCLSMethodNum = pCLSCodeUnit.MethodMap.GetAt(tMethod) - Set $lb(tMethodStart, tMethodEnd) = ..MethodMap.GetAt(tMethod) + Set tMethodStart = ..MethodMap.GetAt(tMethod) + Set tMethodEnd = ..MethodEndMap.GetAt(tMethod) Set tMethodName = tMethod Set tFullMap(tMethodStart) = $lb("CLS", tClass,tMethodName, -1, -1) ; -1 because the class ; definition doesn't have the +1 offset from the { @@ -559,6 +564,11 @@ Storage Default subnode "Lines" + +MethodEndMap +subnode +"MethodEndMap" + MethodMap subnode diff --git a/cls/TestCoverage/Data/Coverage.cls b/cls/TestCoverage/Data/Coverage.cls index 868613a..46a7f7c 100644 --- a/cls/TestCoverage/Data/Coverage.cls +++ b/cls/TestCoverage/Data/Coverage.cls @@ -100,6 +100,7 @@ ClassMethod StoreIntCoverage(pRun As %Integer, pTestPath As %String, pName As %S $$$ThrowOnError(tSC) } Else { // If pType = "PY" + Set ^IRIS.TEMPCG($i(^IRIS.TEMPCG)) = "storing something pythonic" //^IRIS.TEMPCJG(148,1)="EP.sysSettrace", ^IRIS.TEMPCJG(148,2)="fun_generator", ^IRIS.TEMPCJG(148,3)=120 if $Data(^IRIS.TEMPCJG(pName)) { for i = 1:1:(^IRIS.TEMPCJG(pName)-1) { diff --git a/cls/TestCoverage/Manager.cls b/cls/TestCoverage/Manager.cls index 146d28a..39bf399 100644 --- a/cls/TestCoverage/Manager.cls +++ b/cls/TestCoverage/Manager.cls @@ -26,9 +26,12 @@ Property Timing As %Boolean [ InitialExpression = 0, Internal, Private ]; /// Set to true (1) if coverage targets should be loaded dynamically from the unit test root. Property DynamicTargets As %Boolean [ InitialExpression = 1, Internal, Private ]; -/// Current list of targets (routines, classes, .int code, etc.) for line-by-line monitoring +/// Current list of objectscript targets (routines, classes, .int code, etc.) for line-by-line monitoring Property CoverageTargets As %List [ Internal, Private ]; +/// Current list of compiled Python targets for line-by-line monitoring +Property PyCoverageTargets As %List [ Internal, Private ]; + /// All classes considered at any point during this unit test run /// Top-level node has $ListBuild list; also has subscripts with individual class names for a quicker lookup Property CoverageClasses As %List [ Internal, MultiDimensional, Private ]; @@ -37,6 +40,10 @@ Property CoverageClasses As %List [ Internal, MultiDimensional, Private ]; /// Top-level node has $ListBuild list; also has subscripts with individual routine names for a quicker lookup Property CoverageRoutines As %List [ Internal, MultiDimensional, Private ]; +/// All compiled python files considered at any point during this unit test run +/// Top-level node has $ListBuild list; also has subscripts with individual routine names for a quicker lookup +Property CoveragePythons As %List [ Internal, MultiDimensional, Private ]; + /// Last known coverage.list file Property LastCoverageListFile As %String(MAXLEN = "") [ Internal, Private ]; @@ -139,19 +146,32 @@ ClassMethod ArrayToList(pDynamicArray As %DynamicArray) As %List Quit tList } +/// write ##class(TestCoverage.Manager).HasPython("EP.sysSettrace") +/// 1 +ClassMethod HasPython(pClassName As %String) As %Boolean +{ + return $data(^ROUTINE(pClassName _ ".py", 0, 0)) > 0 +} + +/// >write ##class(TestCoverage.Manager).HasPython("EP.sysSettrace") Method SetCoverageTargets(pClasses As %List = "", pRoutines As %List = "", pInit As %Boolean = 0) [ Private ] { Set tList = "", tPtr = 0 + Set tPyList = "" While $ListNext(pClasses,tPtr,tClass) { // Use a wildcard to include all .int files associated with the class. Set tList = tList_$ListBuild(tClass_".CLS") Do ..AddCoverageClass(tClass) + If (..HasPython(tClass)) { + set tPyList = tPyList _ $ListBuild(tClass) + } } While $ListNext(pRoutines,tPtr,tRoutine) { Set tList = tList_$ListBuild(tRoutine_".MAC") Do ..AddCoverageRoutine(tRoutine) } Set ..CoverageTargets = ..GetObjectCodeForSourceNames(tList) + Set ..PyCoverageTargets = tPyList If pInit { // Set flag to determine code coverage dynamically. Set ..DynamicTargets = (tList = "") @@ -185,6 +205,7 @@ Method StartCoverageTracking() As %Status [ Private ] Set $Namespace = ..SourceNamespace Set tRelevantTargets = "" + Set tPyRelevantTargets = "" Set tNewTargets = "" Set tPointer = 0 While $ListNext(..CoverageTargets,tPointer,tCoverageTarget) { @@ -192,12 +213,20 @@ Method StartCoverageTracking() As %Status [ Private ] Set tNewTargets = tNewTargets_$ListBuild(tCoverageTarget) } ElseIf tIsRelevant { Set tRelevantTargets = tRelevantTargets_$ListBuild(tCoverageTarget) + // check if the relevant old targets have python components + Set tOther = ##class(%Library.RoutineMgr).GetOther(tCoverageTarget,"INT",-1) + If (tOther '= "") && ($Piece(tOther,".",*) = "CLS") { + set tName = $piece(tOther, ".", 1, *-1) + If (..HasPython(tName)) { + set tPyRelevantTargets = tPyRelevantTargets _ $ListBuild(tName) + } + } } } If (tNewTargets '= "") { $$$StartTimer("Taking snapshot of code and CLS/MAC/INT mappings") - Set tSC = ##class(TestCoverage.Utils).Snapshot(tNewTargets, .tNewRelevantTargets) + Set tSC = ##class(TestCoverage.Utils).Snapshot(tNewTargets, .tNewRelevantTargets, .tNewPyRelevantTargets) $$$StopTimer $$$ThrowOnError(tSC) @@ -211,6 +240,11 @@ Method StartCoverageTracking() As %Status [ Private ] Set ..KnownCoverageTargets(tRelevantTarget) = 1 Set tRelevantTargets = tRelevantTargets_$ListBuild(tRelevantTarget) } + + Set tPointer = 0 + While $ListNext(tNewPyRelevantTargets,tPointer,tPyRelevantTarget) { + Set tPyRelevantTargets = tPyRelevantTargets_$ListBuild(tPyRelevantTarget) + } } If (tRelevantTargets = "") { @@ -243,7 +277,8 @@ Method StartCoverageTracking() As %Status [ Private ] } } Set tMetrics = $ListBuild("RtnLine") _ $Select(..Timing:$ListBuild("Time","TotalTime"),1:"") - $$$ThrowOnError(..Monitor.StartWithScope(tRelevantTargets,tMetrics,tProcessIDs, ..CoverageClasses)) + // Set ^IRIS.TEMPCG($i(^IRIS.TEMPCG)) = tPyRelevantTargets + $$$ThrowOnError(..Monitor.StartWithScope(tRelevantTargets, tPyRelevantTargets, tMetrics,tProcessIDs)) } } Catch e { Set tSC = e.AsStatus() @@ -284,6 +319,7 @@ Method UpdateCoverageTargetsForTestDirectory(pDirectory As %String) As %Status [ // If we found it, read in coverage list from there. Set tCoverageTargetList = "" + Set tPyCoverageTargetList = "" If (tListFile '= "") { Do ..PrintLine("Tracking code coverage on resources listed in "_tListFile) Do ..GetCoverageTargetsForFile(tListFile, .tCoverageTargets) @@ -299,6 +335,9 @@ Method UpdateCoverageTargetsForTestDirectory(pDirectory As %String) As %Status [ If (tType = "CLS") { Do ..AddCoverageClass(tCoverageTargetKey) + If (..HasPython(tCoverageTargetKey)) { + Set tPyCoverageTargetList = $get(tPyCoverageTargetList) _ $ListBuild(tCoverageTargetKey) + } } Else { Do ..AddCoverageRoutine(tCoverageTargetKey) } @@ -308,6 +347,7 @@ Method UpdateCoverageTargetsForTestDirectory(pDirectory As %String) As %Status [ Set tObjectCodeList = ..GetObjectCodeForSourceNames(tCoverageTargetList) Set ..CoverageTargets = tObjectCodeList // Also restarts the monitor if it is running and updates data on covered routines/classes + Set ..PyCoverageTargets = tPyCoverageTargetList // no need to get the compiled names, it's already correct } Catch e { Set tSC = e.AsStatus() } @@ -319,6 +359,10 @@ Method AddCoverageClass(pClassName As %Dictionary.CacheClassname) [ Private ] If '$Data(..CoverageClasses(pClassName)) { Set ..CoverageClasses = $Get(..CoverageClasses) _ $ListBuild(pClassName) Set ..CoverageClasses(pClassName) = "" + If (..HasPython(pClassName)) { + Set ..CoveragePythons = $Get(..CoveragePythons) _ $ListBuild(pClassName) + Set ..CoveragePythons(pClassName) = "" + } } } @@ -430,7 +474,7 @@ Method GetObjectCodeForSourceNames(pSourceNameList As %List) As %List [ Private While $ListNext(tOthers,tOtherPointer,tOtherName) { Set tOutputNameList = tOutputNameList_$ListBuild($Piece(tOtherName,".",1,*-1)) } - } +} Quit tOutputNameList } @@ -472,10 +516,18 @@ Method EndCoverageTracking(pTestSuite As %String = "", pTestClass As %String = " Set tRtnCount = ..Monitor.GetRoutineCount() For i=1:1:tRtnCount { Set tRtnName = ..Monitor.GetRoutineName(i) - Set tSC = ##class(TestCoverage.Data.Coverage).StoreIntCoverage(tTestIndex,tTarget,tRtnName,.tCache) + Set tSC = ##class(TestCoverage.Data.Coverage).StoreIntCoverage(tTestIndex,tTarget,tRtnName,"INT",.tCache) + $$$ThrowOnError(tSC) + } + + Set tPyClasses = ..Monitor.PythonClassList + Set tPointer = 0 + While $ListNext(tPyClasses, tPointer, tClass) { + Set tSC = ##class(TestCoverage.Data.Coverage).StoreIntCoverage(tTestIndex,tTarget,tClass,"PY",.tCache) $$$ThrowOnError(tSC) } Merge ..Hashes = tCache + $$$StopTimer } } Catch e { diff --git a/cls/TestCoverage/Utils.cls b/cls/TestCoverage/Utils.cls index a4a1360..04bce27 100644 --- a/cls/TestCoverage/Utils.cls +++ b/cls/TestCoverage/Utils.cls @@ -78,7 +78,7 @@ ClassMethod GetTestCoverageTableList() As %List /// 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 +ClassMethod Snapshot(pIntRoutines As %List, Output pRelevantRoutines As %List = "", Output pPyRelevantRoutines As %List = "") As %Status { Set tSC = $$$OK Try { @@ -107,13 +107,30 @@ ClassMethod Snapshot(pIntRoutines As %List, Output pRelevantRoutines As %List = If $$$ISERR(tSC) { Continue // Non-fatal. Just skip it. } - If '$BitCount(tCodeUnit.ExecutableLines,1) { + Set tName = tCodeUnit.Name // should be the same as tOther without the .cls, but if I have it already why not + If ##class(TestCoverage.Manager).HasPython(tName) { + set pPyRelevantRoutines = pPyRelevantRoutines _ $ListBuild(tName) + } ElseIf '$BitCount(tCodeUnit.ExecutableLines,1) { // Skip it - no executable lines. Continue } } + Set pRelevantRoutines = pRelevantRoutines _ $ListBuild(tIntRoutine) } + + // snapshot all the compiled python CodeUnits + Set tSnapshotQueue = $System.WorkMgr.Initialize(,.tSC) + $$$ThrowOnError(tSC) + Set ^IRIS.TEMPCG($i(^IRIS.TEMPCG)) = pPyRelevantRoutines + Set tPointer = 0 + While $ListNext(pPyRelevantRoutines,tPointer,tPyRoutine) { + Set tSC = tSnapshotQueue.Queue("##class(TestCoverage.Data.CodeUnit).GetCurrentByName",tPyRoutine_".PY") + $$$ThrowOnError(tSC) + } + + Set tSC = tSnapshotQueue.WaitForComplete() + $$$ThrowOnError(tSC) Write ! } Catch e { Set tSC = e.AsStatus() diff --git a/cls/TestCoverage/Utils/LineByLineMonitor.cls b/cls/TestCoverage/Utils/LineByLineMonitor.cls index af0e802..ee0833f 100644 --- a/cls/TestCoverage/Utils/LineByLineMonitor.cls +++ b/cls/TestCoverage/Utils/LineByLineMonitor.cls @@ -21,6 +21,9 @@ Method PausedGet() As %Boolean [ CodeMode = expression ] ..Started && '$zu(84,1) } +/// The current python classes being tracked, so that we know what to store the coverage for +Property PythonClassList As %List; + Property LastRoutineList As %List [ Private ]; Property LastMetricList As %List [ Private ]; @@ -132,10 +135,11 @@ ClassMethod PyStop() [ Language = python ] } /// Tracks current monitoring context and stops/starts or resets counters depending on whether it has changed -Method StartWithScope(pRoutineList As %List, pMetricList As %List, pProcessList As %List, pCoverageClasses As %List) As %Status +Method StartWithScope(pRoutineList As %List, pPyClasses As %List, pMetricList As %List, pProcessList As %List) As %Status { Set tSC = $$$OK Try { + Set ..PythonClassList = pPyClasses Set tDifferentScope = (..LastRoutineList '= pRoutineList) || (..LastMetricList '= pMetricList) || (..LastProcessList '= pProcessList) If tDifferentScope && ..Started { // If we need to track different routines/metrics/processes, need to stop the monitor before restarting with the new context. @@ -149,7 +153,7 @@ Method StartWithScope(pRoutineList As %List, pMetricList As %List, pProcessList If '..Started { Do ..PyClearCounters() Set tSC = ..Start(pRoutineList, pMetricList, pProcessList) - Do ..PyStartWithScope(pCoverageClasses) + Do ..PyStartWithScope(pPyClasses) If $System.Status.Equals(tSC,$$$ERRORCODE($$$MonitorMemoryAlloc)) { // Construct a more helpful error message. Set tSC = $$$EMBEDSC(..CheckAvailableMemory($ListLength(pProcessList),$ListLength(pRoutineList),1),tSC) From 830083962bbd701b5071e3c1b9a1c6f9bd324280 Mon Sep 17 00:00:00 2001 From: Chris Ge Date: Wed, 3 Jul 2024 08:52:01 -0400 Subject: [PATCH 10/36] Addendum to previous commit: previously, also made the call in EndCoverageTracking to StoreIntRoutine to store the coverage results from the .py CodeUnits, resulting in python lines being marked as covered in .cls files --- cls/TestCoverage/Data/Coverage.cls | 1 - cls/TestCoverage/Manager.cls | 2 -- cls/TestCoverage/Utils.cls | 1 - 3 files changed, 4 deletions(-) diff --git a/cls/TestCoverage/Data/Coverage.cls b/cls/TestCoverage/Data/Coverage.cls index 46a7f7c..868613a 100644 --- a/cls/TestCoverage/Data/Coverage.cls +++ b/cls/TestCoverage/Data/Coverage.cls @@ -100,7 +100,6 @@ ClassMethod StoreIntCoverage(pRun As %Integer, pTestPath As %String, pName As %S $$$ThrowOnError(tSC) } Else { // If pType = "PY" - Set ^IRIS.TEMPCG($i(^IRIS.TEMPCG)) = "storing something pythonic" //^IRIS.TEMPCJG(148,1)="EP.sysSettrace", ^IRIS.TEMPCJG(148,2)="fun_generator", ^IRIS.TEMPCJG(148,3)=120 if $Data(^IRIS.TEMPCJG(pName)) { for i = 1:1:(^IRIS.TEMPCJG(pName)-1) { diff --git a/cls/TestCoverage/Manager.cls b/cls/TestCoverage/Manager.cls index 39bf399..f36a37b 100644 --- a/cls/TestCoverage/Manager.cls +++ b/cls/TestCoverage/Manager.cls @@ -153,7 +153,6 @@ ClassMethod HasPython(pClassName As %String) As %Boolean return $data(^ROUTINE(pClassName _ ".py", 0, 0)) > 0 } -/// >write ##class(TestCoverage.Manager).HasPython("EP.sysSettrace") Method SetCoverageTargets(pClasses As %List = "", pRoutines As %List = "", pInit As %Boolean = 0) [ Private ] { Set tList = "", tPtr = 0 @@ -277,7 +276,6 @@ Method StartCoverageTracking() As %Status [ Private ] } } Set tMetrics = $ListBuild("RtnLine") _ $Select(..Timing:$ListBuild("Time","TotalTime"),1:"") - // Set ^IRIS.TEMPCG($i(^IRIS.TEMPCG)) = tPyRelevantTargets $$$ThrowOnError(..Monitor.StartWithScope(tRelevantTargets, tPyRelevantTargets, tMetrics,tProcessIDs)) } } Catch e { diff --git a/cls/TestCoverage/Utils.cls b/cls/TestCoverage/Utils.cls index 04bce27..0a38fe6 100644 --- a/cls/TestCoverage/Utils.cls +++ b/cls/TestCoverage/Utils.cls @@ -122,7 +122,6 @@ ClassMethod Snapshot(pIntRoutines As %List, Output pRelevantRoutines As %List = // snapshot all the compiled python CodeUnits Set tSnapshotQueue = $System.WorkMgr.Initialize(,.tSC) $$$ThrowOnError(tSC) - Set ^IRIS.TEMPCG($i(^IRIS.TEMPCG)) = pPyRelevantRoutines Set tPointer = 0 While $ListNext(pPyRelevantRoutines,tPointer,tPyRoutine) { Set tSC = tSnapshotQueue.Queue("##class(TestCoverage.Data.CodeUnit).GetCurrentByName",tPyRoutine_".PY") From d6b81ca4021ab445213882501786c9ca6cb90712 Mon Sep 17 00:00:00 2001 From: Chris Ge Date: Wed, 3 Jul 2024 12:01:51 -0400 Subject: [PATCH 11/36] Fetch the executable lines for the python methods in .cls files; whole python now working end to end --- cls/TestCoverage/Data/CodeUnit.cls | 71 ++++++++++++++++++++++++++++++ cls/TestCoverage/Utils.cls | 50 ++++++++++++++++----- 2 files changed, 110 insertions(+), 11 deletions(-) diff --git a/cls/TestCoverage/Data/CodeUnit.cls b/cls/TestCoverage/Data/CodeUnit.cls index eff9bc0..c7a6e20 100644 --- a/cls/TestCoverage/Data/CodeUnit.cls +++ b/cls/TestCoverage/Data/CodeUnit.cls @@ -233,6 +233,77 @@ ClassMethod GetCurrentByName(pInternalName As %String, pSourceNamespace As %Stri Quit tSC } +/// Get the executable lines of code in python over to the .cls CodeUnit +Method UpdatePyExecutableLines(pName As %String, ByRef pPyCodeUnit) As %Status +{ + Set tSC = $$$OK + Set tOriginalNamespace = $Namespace + Set tInitTLevel = $TLevel + Try { + TSTART + + Set tBitString = "" + If (##class(TestCoverage.Manager).HasPython(pName)) { + + // snapshot the Python CodeUnit for it + Set tFromHash = pPyCodeUnit.Hash + Set tToHash = ..Hash + // now bring over the executable lines + + &sql( + DECLARE C1 CURSOR FOR + SELECT map.ToLine + INTO :hToLine + FROM TestCoverage_Data.CodeUnitMap map + JOIN TestCoverage_Data.CodeUnit fromCodeUnit + ON map.FromHash = fromCodeUnit.Hash + JOIN TestCoverage_Data.CodeUnit toCodeUnit + ON map.ToHash = toCodeUnit.Hash + AND fromCodeUnit.Hash = :tFromHash + AND toCodeUnit.Hash = :tToHash + WHERE TestCoverage.BIT_VALUE(fromCodeUnit.ExecutableLines,map.FromLine) <> 0 + ) + &sql(OPEN C1) + If (SQLCODE < 0) { + Throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) + } + For { + &SQL(FETCH C1) + If (SQLCODE < 0) { + Throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) + } ElseIf (SQLCODE) { + Quit + } + // Process the fetched rows + // hToLine contains the line number in the .cls file corresponding to executable lines in the .py file + Set $Bit(tBitString, hToLine) = 1 + } + &sql(CLOSE C1) + If (SQLCODE < 0) { + Throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) + } + } + if (pName = "TestCoverage.Utils") { + set ^IRIS.TEMPCG($i(^IRIS.TEMPCG)) = tBitString + set ^IRIS.TEMPCG($i(^IRIS.TEMPCG)) = ..ExecutableLines + } + Set ..ExecutableLines = $BITLOGIC(..ExecutableLines | tBitString) + Set tSC = ..%Save() + $$$ThrowOnError(tSC) + if (pName = "TestCoverage.Utils") { + set ^IRIS.TEMPCG($i(^IRIS.TEMPCG)) = ..ExecutableLines + } + TCOMMIT + } Catch e { + Set pCodeUnit = $$$NULLOREF + Set tSC = e.AsStatus() + } + While ($TLevel > tInitTLevel) { + TROLLBACK 1 + } + Quit tSC +} + Method UpdateSourceMap(pSourceNamespace As %String, ByRef pCache) As %Status { Set tSC = $$$OK diff --git a/cls/TestCoverage/Utils.cls b/cls/TestCoverage/Utils.cls index 0a38fe6..cbaba0c 100644 --- a/cls/TestCoverage/Utils.cls +++ b/cls/TestCoverage/Utils.cls @@ -82,6 +82,12 @@ ClassMethod Snapshot(pIntRoutines As %List, Output pRelevantRoutines As %List = { Set tSC = $$$OK Try { + // For debugging use only: no parallelization + #; Set tPointer = 0 + #; While $ListNext(pIntRoutines, tPointer, tIntRoutine) { + #; do ##class(TestCoverage.Data.CodeUnit).GetCurrentByName(tIntRoutine_".INT") + #; } + #dim tSnapshotQueue As %SYSTEM.WorkMgr Set tSnapshotQueue = $System.WorkMgr.Initialize(,.tSC) $$$ThrowOnError(tSC) @@ -94,7 +100,8 @@ ClassMethod Snapshot(pIntRoutines As %List, Output pRelevantRoutines As %List = 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 @@ -102,6 +109,7 @@ ClassMethod Snapshot(pIntRoutines As %List, Output pRelevantRoutines As %List = Set tOther = ##class(%Library.RoutineMgr).GetOther(tIntRoutine,"INT",-1) If (tOther '= "") && ($Piece(tOther,".",*) = "CLS") { // With the code already cached, this will be faster. + // This also snapshots the compiled python routine with it if there is one #dim tCodeUnit As TestCoverage.Data.CodeUnit Set tSC = ##class(TestCoverage.Data.CodeUnit).GetCurrentByName(tOther,,.tCodeUnit) If $$$ISERR(tSC) { @@ -110,6 +118,13 @@ ClassMethod Snapshot(pIntRoutines As %List, Output pRelevantRoutines As %List = Set tName = tCodeUnit.Name // should be the same as tOther without the .cls, but if I have it already why not If ##class(TestCoverage.Manager).HasPython(tName) { set pPyRelevantRoutines = pPyRelevantRoutines _ $ListBuild(tName) + // take a snapshot of the compiled python file + $$$ThrowOnError(##class(TestCoverage.Data.CodeUnit).GetCurrentByName(tName_".PY",,.tPyCodeUnit)) + + // update the executable lines for the .cls file's python + set ^IRIS.TEMPCG($i(^IRIS.TEMPCG)) = "Calling update executable lines on " _ tName + $$$ThrowOnError(tCodeUnit.UpdatePyExecutableLines(tName, .tPyCodeUnit)) + } ElseIf '$BitCount(tCodeUnit.ExecutableLines,1) { // Skip it - no executable lines. Continue @@ -119,17 +134,30 @@ ClassMethod Snapshot(pIntRoutines As %List, Output pRelevantRoutines As %List = Set pRelevantRoutines = pRelevantRoutines _ $ListBuild(tIntRoutine) } - // snapshot all the compiled python CodeUnits - Set tSnapshotQueue = $System.WorkMgr.Initialize(,.tSC) - $$$ThrowOnError(tSC) - Set tPointer = 0 - While $ListNext(pPyRelevantRoutines,tPointer,tPyRoutine) { - Set tSC = tSnapshotQueue.Queue("##class(TestCoverage.Data.CodeUnit).GetCurrentByName",tPyRoutine_".PY") - $$$ThrowOnError(tSC) - } + // snapshot all the compiled python CodeUnits + #; Set tSnapshotQueue = $System.WorkMgr.Initialize(,.tSC) + #; $$$ThrowOnError(tSC) + #; Set tPointer = 0 + #; While $ListNext(pPyRelevantRoutines,tPointer,tPyRoutine) { + #; Set tSC = tSnapshotQueue.Queue("##class(TestCoverage.Data.CodeUnit).GetCurrentByName",tPyRoutine_".PY") + #; $$$ThrowOnError(tSC) + #; } - Set tSC = tSnapshotQueue.WaitForComplete() - $$$ThrowOnError(tSC) + #; Set tSC = tSnapshotQueue.WaitForComplete() + #; $$$ThrowOnError(tSC) + + // update the executable lines for the .cls files that have python -- TODO: make the UpdatePyExecutableLines method not rely on knowing the .cls and .py CodeUnits beforehand + #; Set tSnapshotQueue = $System.WorkMgr.Initialize(,.tSC) + #; $$$ThrowOnError(tSC) + #; Set tPointer = 0 + #; While $ListNext(pPyRelevantRoutines,tPointer,tClass) { + #; Set tSC = tSnapshotQueue.Queue("##class(TestCoverage.Data.CodeUnit).UpdatePyExecutableLines",tClass) + #; $$$ThrowOnError(tSC) + #; } + + #; Set tSC = tSnapshotQueue.WaitForComplete() + #; $$$ThrowOnError(tSC) + Write ! } Catch e { Set tSC = e.AsStatus() From 96098b821cea1c2d13b0798a6e50c015ff4dd8b0 Mon Sep 17 00:00:00 2001 From: Chris Ge Date: Fri, 5 Jul 2024 11:41:54 -0400 Subject: [PATCH 12/36] Changed global name from IRIS.TEMPCJG to IRIS.TEMP.TestCoveragePY --- cls/TestCoverage/Data/CodeUnit.cls | 8 +------- cls/TestCoverage/Data/Coverage.cls | 7 +++---- cls/TestCoverage/Utils/LineByLineMonitor.cls | 6 +++--- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/cls/TestCoverage/Data/CodeUnit.cls b/cls/TestCoverage/Data/CodeUnit.cls index c7a6e20..cfeb62f 100644 --- a/cls/TestCoverage/Data/CodeUnit.cls +++ b/cls/TestCoverage/Data/CodeUnit.cls @@ -283,16 +283,10 @@ Method UpdatePyExecutableLines(pName As %String, ByRef pPyCodeUnit) As %Status Throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) } } - if (pName = "TestCoverage.Utils") { - set ^IRIS.TEMPCG($i(^IRIS.TEMPCG)) = tBitString - set ^IRIS.TEMPCG($i(^IRIS.TEMPCG)) = ..ExecutableLines - } Set ..ExecutableLines = $BITLOGIC(..ExecutableLines | tBitString) Set tSC = ..%Save() $$$ThrowOnError(tSC) - if (pName = "TestCoverage.Utils") { - set ^IRIS.TEMPCG($i(^IRIS.TEMPCG)) = ..ExecutableLines - } + TCOMMIT } Catch e { Set pCodeUnit = $$$NULLOREF diff --git a/cls/TestCoverage/Data/Coverage.cls b/cls/TestCoverage/Data/Coverage.cls index 868613a..ab77e8b 100644 --- a/cls/TestCoverage/Data/Coverage.cls +++ b/cls/TestCoverage/Data/Coverage.cls @@ -100,10 +100,9 @@ ClassMethod StoreIntCoverage(pRun As %Integer, pTestPath As %String, pName As %S $$$ThrowOnError(tSC) } Else { // If pType = "PY" - //^IRIS.TEMPCJG(148,1)="EP.sysSettrace", ^IRIS.TEMPCJG(148,2)="fun_generator", ^IRIS.TEMPCJG(148,3)=120 - if $Data(^IRIS.TEMPCJG(pName)) { - for i = 1:1:(^IRIS.TEMPCJG(pName)-1) { - Set tLineNumber = ^IRIS.TEMPCJG(pName, i) + if $Data(^IRIS.TEMP.TestCoveragePY(pName)) { + for i = 1:1:(^IRIS.TEMP.TestCoveragePY(pName)-1) { + Set tLineNumber = ^IRIS.TEMP.TestCoveragePY(pName, i) Set $Bit(tCoveredLines, tLineNumber) = 1 } } diff --git a/cls/TestCoverage/Utils/LineByLineMonitor.cls b/cls/TestCoverage/Utils/LineByLineMonitor.cls index ee0833f..1cca8f5 100644 --- a/cls/TestCoverage/Utils/LineByLineMonitor.cls +++ b/cls/TestCoverage/Utils/LineByLineMonitor.cls @@ -71,14 +71,14 @@ ClassMethod PyStartWithScope(pCoverageClasses As %List) [ Language = python ] line_no = frame.f_lineno if class_name in tCoverageClasses and line_no != 1: # if this is in a covered class # print(f"A {event} encountered in {func_name}() in class {class_name} at line number {line_no}") - tGlob = iris.gref('^IRIS.TEMPCJG') + tGlob = iris.gref('^IRIS.TEMP.TestCoveragePY') curId = tGlob.get([class_name]) if not curId: tGlob[class_name] = 1 curId = 1 tGlob[class_name, curId] = line_no tGlob[class_name] = curId+1 - #; iris.execute('set ^TestCovera3FF5.PyMonitorReC80BD($i(^TestCovera3FF5.PyMonitorReC80BD)) = $listbuild("", ^IRIS.TEMPCJG(1), ^IRIS.TEMPCJG(2),^IRIS.TEMPCJG(3) )') + #; iris.execute('set ^TestCovera3FF5.PyMonitorReC80BD($i(^TestCovera3FF5.PyMonitorReC80BD)) = $listbuild("", ^IRIS.TEMP.TestCoveragePY(1), ^IRIS.TEMP.TestCoveragePY(2),^IRIS.TEMP.TestCoveragePY(3) )') #; new_line = iris.cls("TestCoverage.Data.PyMonitorResults")._New() #; new_line.ClassName = class_name @@ -101,7 +101,7 @@ ClassMethod PyStartWithScope(pCoverageClasses As %List) [ Language = python ] ClassMethod PyClearCounters() { - Kill ^IRIS.TEMPCJG + Kill ^IRIS.TEMP.TestCoveragePY #; &sql( #; delete from TestCoverage_Data.PyMonitorResults #; ) From f2b21bd9b68a4599dcef76a57aefa3a180cce9b2 Mon Sep 17 00:00:00 2001 From: Chris Ge Date: Fri, 5 Jul 2024 14:37:57 -0400 Subject: [PATCH 13/36] Manually set the python class method definition lines to not executable --- cls/TestCoverage/Data/CodeUnit.cls | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/cls/TestCoverage/Data/CodeUnit.cls b/cls/TestCoverage/Data/CodeUnit.cls index cfeb62f..d288f93 100644 --- a/cls/TestCoverage/Data/CodeUnit.cls +++ b/cls/TestCoverage/Data/CodeUnit.cls @@ -125,11 +125,7 @@ ClassMethod GetCurrentByName(pInternalName As %String, pSourceNamespace As %Stri Set pCodeUnit.Type = tType Set pCodeUnit.Hash = tHash - Set tBitString = "" - For tLine=1:1:$Get(tCodeArray,0) { - Set $Bit(tBitString,tLine) = $Get(tExecutableFlags(tLine),0) - } - Set pCodeUnit.ExecutableLines = tBitString + If (tType = "CLS") { Set pCodeUnit.Generated = ($$$comClassKeyGet(tName,$$$cCLASSgeneratedby) '= "") @@ -137,6 +133,11 @@ ClassMethod GetCurrentByName(pInternalName As %String, pSourceNamespace As %Stri If (tType = "PY") { + #; set tPointer = 0 + #; While $ListNext(pDocumentText, tPointer, tCurLine) { + #; do pCodeUnit.Lines.Insert(tCurLine) + #; } + Set ClassName = $Piece(tName,".", *) Set tMethodInfo = ##class(TestCoverage.Utils).GetPythonMethodMapping(pDocumentText, ClassName) Set tLineToMethodInfo = tMethodInfo."__getitem__"(0) @@ -152,6 +153,7 @@ ClassMethod GetCurrentByName(pInternalName As %String, pSourceNamespace As %Stri Set tStartLine = tStartEnd."__getitem__"(0) Set tEndLine = tStartEnd."__getitem__"(1) Do pCodeUnit.MethodMap.SetAt(tStartLine,tMethod) + Set tExecutableFlags(tStartLine) = 0 Do pCodeUnit.MethodEndMap.SetAt(tEndLine, tMethod) Set tMethodMask = "" @@ -209,7 +211,13 @@ ClassMethod GetCurrentByName(pInternalName As %String, pSourceNamespace As %Stri } } - + Set tBitString = "" + For tLine=1:1:$Get(tCodeArray,0) { + Set $Bit(tBitString,tLine) = $Get(tExecutableFlags(tLine),0) + } + Set pCodeUnit.ExecutableLines = tBitString + + Set tSC = pCodeUnit.%Save() If $$$ISERR(tSC) && $System.Status.Equals(tSC,$$$ERRORCODE($$$IDKeyNotUnique)) { // Some other process beat us to it. From 6837c62212e7b8a9459055787ba52c3110424be5 Mon Sep 17 00:00:00 2001 From: Chris Ge Date: Fri, 5 Jul 2024 14:58:21 -0400 Subject: [PATCH 14/36] Added the source code lines in to the .py CodeUnits --- cls/TestCoverage/Data/CodeUnit.cls | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cls/TestCoverage/Data/CodeUnit.cls b/cls/TestCoverage/Data/CodeUnit.cls index d288f93..33d26ff 100644 --- a/cls/TestCoverage/Data/CodeUnit.cls +++ b/cls/TestCoverage/Data/CodeUnit.cls @@ -133,10 +133,12 @@ ClassMethod GetCurrentByName(pInternalName As %String, pSourceNamespace As %Stri If (tType = "PY") { - #; set tPointer = 0 - #; While $ListNext(pDocumentText, tPointer, tCurLine) { - #; do pCodeUnit.Lines.Insert(tCurLine) - #; } + set tPointer = 0 + While $ListNext(pDocumentText, tPointer, tCurLine) { + do pCodeUnit.Lines.Insert(tCurLine) + } + + do pCodeUnit.Lines.Insert("") Set ClassName = $Piece(tName,".", *) Set tMethodInfo = ##class(TestCoverage.Utils).GetPythonMethodMapping(pDocumentText, ClassName) From bcd58fa3397d08c05080e7891ae7922728a4e953 Mon Sep 17 00:00:00 2001 From: Chris Ge Date: Fri, 5 Jul 2024 17:20:26 -0400 Subject: [PATCH 15/36] Added cyclomatic complexities for python subunits --- cls/TestCoverage/Data/CodeSubUnit.cls | 7 ++++- cls/TestCoverage/Data/CodeUnit.cls | 38 ++++++++++++++++++++++----- cls/TestCoverage/Utils.cls | 1 - requirements.txt | 1 + 4 files changed, 38 insertions(+), 9 deletions(-) create mode 100644 requirements.txt diff --git a/cls/TestCoverage/Data/CodeSubUnit.cls b/cls/TestCoverage/Data/CodeSubUnit.cls index 073c3c1..f8eb8ca 100644 --- a/cls/TestCoverage/Data/CodeSubUnit.cls +++ b/cls/TestCoverage/Data/CodeSubUnit.cls @@ -9,6 +9,9 @@ Property Mask As TestCoverage.DataType.Bitstring; /// Cyclomatic complexity of the code subunit Property Complexity As %Integer [ InitialExpression = 1 ]; +/// 1 if it's a python class method, 0 if not +Property IsPythonMethod As %Boolean; + Method UpdateComplexity() As %Status { Quit $$$OK @@ -26,6 +29,9 @@ Storage Default Complexity + +IsPythonMethod +
{%%PARENT}("SubUnits") CodeSubUnitDefaultData @@ -36,4 +42,3 @@ Storage Default } } - diff --git a/cls/TestCoverage/Data/CodeUnit.cls b/cls/TestCoverage/Data/CodeUnit.cls index 33d26ff..ad9353e 100644 --- a/cls/TestCoverage/Data/CodeUnit.cls +++ b/cls/TestCoverage/Data/CodeUnit.cls @@ -163,11 +163,6 @@ ClassMethod GetCurrentByName(pInternalName As %String, pSourceNamespace As %Stri Set $Bit(tMethodMask,j) = 1 } Set tMethodSignature = $list(pDocumentText, tStartLine) - Set tSubUnit = ##class(TestCoverage.Data.CodeSubUnit.Method).%New() - Set tSubUnit.Name = tMethod - Set tSubUnit.DisplaySignature = tMethodSignature - Set tSubUnit.Mask = tMethodMask - Do pCodeUnit.SubUnits.Insert(tSubUnit) } } Else { @@ -194,6 +189,8 @@ ClassMethod GetCurrentByName(pInternalName As %String, pSourceNamespace As %Stri Set tSubUnit.Name = tMethod Set tSubUnit.DisplaySignature = tMethodSignature Set tSubUnit.Mask = tMethodMask + set NormalizedSignature = $zconvert($zstrip(tMethodSignature, "*W"), "l") + set tSubUnit.IsPythonMethod = (NormalizedSignature [ "[language=python]") Do pCodeUnit.SubUnits.Insert(tSubUnit) Set tMethod = "" Set tMethodSignature = "" @@ -506,15 +503,28 @@ Method UpdateComplexity() As %Status If (..Type '= "CLS") { Quit } - + + // python methods + If (##class(TestCoverage.Manager).HasPython(..Name)) { + do ##class(TestCoverage.Data.CodeUnit).GetCurrentByName(..Name _ ".PY", , .pPyCodeUnit, ) + set tDocumentText = pPyCodeUnit.Lines.Serialize() + set tMethodComplexities = ..GetPythonComplexities(tDocumentText) + } + Set tKey = "" For { Set tSubUnit = ..SubUnits.GetNext(.tKey) If (tKey = "") { Quit } - $$$ThrowOnError(tSubUnit.UpdateComplexity()) + If (tSubUnit.IsPythonMethod) { + set tSubUnit.Complexity = tMethodComplexities."__getitem__"(tSubUnit.Name) + $$$ThrowOnError(tSubUnit.%Save(0)) + } Else { + $$$ThrowOnError(tSubUnit.UpdateComplexity()) + } } + $$$ThrowOnError(..%Save()) } Catch e { @@ -523,6 +533,20 @@ Method UpdateComplexity() As %Status Quit tSC } +ClassMethod GetPythonComplexities(pDocumentText) [ Language = python ] +{ + from radon.complexity import cc_visit + import iris + source_lines = iris.cls('%SYS.Python').ToList(pDocumentText) + source_code = "\n".join(source_lines) + visitor = cc_visit(source_code) + class_info = visitor[0] + method_complexities = {} + for method in class_info.methods: + method_complexities[method.name] = method.complexity + return method_complexities +} + Method GetMethodOffset(pAbsoluteLine As %Integer, Output pMethod As %String, Output pOffset As %Integer) { } diff --git a/cls/TestCoverage/Utils.cls b/cls/TestCoverage/Utils.cls index cbaba0c..0897e3d 100644 --- a/cls/TestCoverage/Utils.cls +++ b/cls/TestCoverage/Utils.cls @@ -122,7 +122,6 @@ ClassMethod Snapshot(pIntRoutines As %List, Output pRelevantRoutines As %List = $$$ThrowOnError(##class(TestCoverage.Data.CodeUnit).GetCurrentByName(tName_".PY",,.tPyCodeUnit)) // update the executable lines for the .cls file's python - set ^IRIS.TEMPCG($i(^IRIS.TEMPCG)) = "Calling update executable lines on " _ tName $$$ThrowOnError(tCodeUnit.UpdatePyExecutableLines(tName, .tPyCodeUnit)) } ElseIf '$BitCount(tCodeUnit.ExecutableLines,1) { diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..703cbdb --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +radon==6.* \ No newline at end of file From 040e56a4dd404c3f615e03901711f0090effd042 Mon Sep 17 00:00:00 2001 From: Chris Ge Date: Mon, 8 Jul 2024 09:24:57 -0400 Subject: [PATCH 16/36] Cleaned up code with the old method of storing monitor results in PyMonitorResults --- cls/TestCoverage/Data/PyMonitorResults.cls | 62 -------------------- cls/TestCoverage/Utils/LineByLineMonitor.cls | 41 ------------- 2 files changed, 103 deletions(-) delete mode 100644 cls/TestCoverage/Data/PyMonitorResults.cls diff --git a/cls/TestCoverage/Data/PyMonitorResults.cls b/cls/TestCoverage/Data/PyMonitorResults.cls deleted file mode 100644 index 2756830..0000000 --- a/cls/TestCoverage/Data/PyMonitorResults.cls +++ /dev/null @@ -1,62 +0,0 @@ -Class TestCoverage.Data.PyMonitorResults Extends %Persistent [ DdlAllowed ] -{ - -Property ClassName As %String(MAXLEN = 255) [ Required ]; - -Property FunctionName As %String(MAXLEN = 255) [ Required ]; - -Property LineNumber As %Integer [ Required ]; - -Storage Default -{ - - -%%CLASSNAME - - -ClassName - - -FunctionName - - -LineNumber - - -^TestCovera3FF5.PyMonitorReC80BD -PyMonitorResultsDefaultData -1 -^TestCovera3FF5.PyMonitorReC80BD -^TestCovera3FF5.PyMonitorReC80BI - -2 -.999999: -0.0001% - - -3 -1 - - -16 -.999999:"EP.sysSettrace" -0.0001% - - -11 -.999999:"TestAsync" -0.0001% - - -3 -.999999:28 -0.0001% - - --4 - -^TestCovera3FF5.PyMonitorReC80BS -%Storage.Persistent -} - -} diff --git a/cls/TestCoverage/Utils/LineByLineMonitor.cls b/cls/TestCoverage/Utils/LineByLineMonitor.cls index 1cca8f5..aecf3b9 100644 --- a/cls/TestCoverage/Utils/LineByLineMonitor.cls +++ b/cls/TestCoverage/Utils/LineByLineMonitor.cls @@ -78,23 +78,6 @@ ClassMethod PyStartWithScope(pCoverageClasses As %List) [ Language = python ] curId = 1 tGlob[class_name, curId] = line_no tGlob[class_name] = curId+1 - #; iris.execute('set ^TestCovera3FF5.PyMonitorReC80BD($i(^TestCovera3FF5.PyMonitorReC80BD)) = $listbuild("", ^IRIS.TEMP.TestCoveragePY(1), ^IRIS.TEMP.TestCoveragePY(2),^IRIS.TEMP.TestCoveragePY(3) )') - - #; new_line = iris.cls("TestCoverage.Data.PyMonitorResults")._New() - #; new_line.ClassName = class_name - #; new_line.FunctionName = func_name - #; new_line.LineNumber = line_no - #; new_line._Save() - - #; stmt = iris.sql.prepare("INSERT INTO TestCoverage_Data.PyMonitorResults (ClassName, FunctionName, LineNumber) VALUES (?, ?, ?)") - #; try: - #; rs = stmt.execute(class_name, func_name, line_no) - #; except Exception as ex: - #; print(ex.sqlcode, ex.message) - - # iris.cls("TestCoverage.Utils.LineByLineMonitor").DoNothing() - # iris.cls("TestCoverage.Utils.LineByLineMonitor").SavePyLine(line_no, func_name, class_name) - #; glob[0] = "we make it back" return my_tracer settrace(my_tracer) } @@ -102,30 +85,6 @@ ClassMethod PyStartWithScope(pCoverageClasses As %List) [ Language = python ] ClassMethod PyClearCounters() { Kill ^IRIS.TEMP.TestCoveragePY - #; &sql( - #; delete from TestCoverage_Data.PyMonitorResults - #; ) - #; If (SQLCODE < 0) { - #; Throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) - #; } -} - -ClassMethod DoNothing() -{ - set x = 1 -} - -ClassMethod SavePyLine(LineNumber As %Integer, FunctionName As %String, ClassName As %String) -{ - #; &sql( - #; INSERT INTO TestCoverage_Data.PyMonitorResults - #; (ClassName, FunctionName, LineNumber) - #; VALUES (:ClassName, :FunctionName, :LineNumber) - #; ) - - #; If (SQLCODE < 0) { - #; Throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) - #; } } ClassMethod PyStop() [ Language = python ] From cc3700d729b60befab8423639e93a7d7ee386853 Mon Sep 17 00:00:00 2001 From: Chris Ge Date: Mon, 8 Jul 2024 10:04:00 -0400 Subject: [PATCH 17/36] Added unit tests for Python CodeUnit and embedded python in .CLS files --- .../UnitTest/TestCoverage/Unit/CodeUnit.cls | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/internal/testing/unit_tests/UnitTest/TestCoverage/Unit/CodeUnit.cls b/internal/testing/unit_tests/UnitTest/TestCoverage/Unit/CodeUnit.cls index 42f50a3..f1e30c6 100644 --- a/internal/testing/unit_tests/UnitTest/TestCoverage/Unit/CodeUnit.cls +++ b/internal/testing/unit_tests/UnitTest/TestCoverage/Unit/CodeUnit.cls @@ -16,6 +16,7 @@ Method TestCodeUnitCreation() #dim tCodeUnit As TestCoverage.Data.CodeUnit Set tClsName = $classname()_".CLS" Set tIntName = $classname()_".1.INT" + Set tPyName = $classname()_".PY" Set tSC = ##class(TestCoverage.Data.CodeUnit).GetCurrentByName(tIntName,$Namespace,.tIntCodeUnit) Do $$$AssertStatusOK(tSC,"Found test coverage data for "_tIntName) Do $$$AssertEquals(tIntCodeUnit.Name,$classname()_".1") @@ -27,28 +28,40 @@ Method TestCodeUnitCreation() Do $$$AssertEquals(tGenCodeUnit.Name,$classname()_".G1") Do $$$AssertEquals(tGenCodeUnit.Type,"INT") + Set tSC = ##class(TestCoverage.Data.CodeUnit).GetCurrentByName(tClsName,$Namespace,.tClsCodeUnit) Do $$$AssertStatusOK(tSC,"Found test coverage data for "_tClsName) Do $$$AssertEquals(tClsCodeUnit.Name,$classname()) Do $$$AssertEquals(tClsCodeUnit.Type,"CLS") + Set tSC = ##class(TestCoverage.Data.CodeUnit).GetCurrentByName(tPyName,$Namespace,.tPyCodeUnit) + Do $$$AssertStatusOK(tSC,"Found test coverage data for "_tPyName) + Do $$$AssertEquals(tPyCodeUnit.Name,$classname()) + Do $$$AssertEquals(tPyCodeUnit.Type,"PY") + + Set tSC = tClsCodeUnit.UpdatePyExecutableLines($classname(),.tPyCodeUnit) + Do $$$AssertStatusOK(tSC,"Found updated executable line data for "_tClsName) + Set tConstantReturnValueLine = tClsCodeUnit.MethodMap.GetAt("SampleConstantReturnValue") Set tCodeGeneratorLine = tClsCodeUnit.MethodMap.GetAt("SampleCodeGenerator") Set tNormalMethodLine = tClsCodeUnit.MethodMap.GetAt("SampleNormalMethod") + Set tPythonMethodLine = tClsCodeUnit.MethodMap.GetAt("SamplePythonMethod") Do $$$AssertNotEquals(tConstantReturnValueLine,"") Do $$$AssertNotEquals(tCodeGeneratorLine,"") Do $$$AssertNotEquals(tNormalMethodLine,"") + Do $$$AssertNotEquals(tPythonMethodLine,"") // tTestLines(line number) = $ListBuild(description, executable (default 1), mapped (default 1), mapped from hash (if relevant), mapped from line (if relevant)) Set tTestLines(tConstantReturnValueLine+2) = $ListBuild("SampleConstantReturnValue+1",0,0) - Set tTestLines(tCodeGeneratorLine+2) = $ListBuild("SampleCodeGenerator+1",,,tGenCodeUnit.Hash,tGenCodeUnit.MethodMap.GetAt("SampleCodeGenerator")+1) - Set tTestLines(tCodeGeneratorLine+3) = $ListBuild("SampleCodeGenerator+2",,,tGenCodeUnit.Hash,tGenCodeUnit.MethodMap.GetAt("SampleCodeGenerator")+2) - Set tTestLines(tCodeGeneratorLine+4) = $ListBuild("SampleCodeGenerator+3",,,tGenCodeUnit.Hash,tGenCodeUnit.MethodMap.GetAt("SampleCodeGenerator")+3) + Set tTestLines(tCodeGeneratorLine+2) = $ListBuild("SampleCodeGenerator+1",,,tGenCodeUnit.Hash,tGenCodeUnit.MethodMap.GetAt("SampleCodeGenerator")+1, "INT") + Set tTestLines(tCodeGeneratorLine+3) = $ListBuild("SampleCodeGenerator+2",,,tGenCodeUnit.Hash,tGenCodeUnit.MethodMap.GetAt("SampleCodeGenerator")+2, "INT") + Set tTestLines(tCodeGeneratorLine+4) = $ListBuild("SampleCodeGenerator+3",,,tGenCodeUnit.Hash,tGenCodeUnit.MethodMap.GetAt("SampleCodeGenerator")+3, "INT") Set methodLabel = $Select($System.Version.GetMajor()<2023:"z",1:"")_"SampleNormalMethod" - Set tTestLines(tNormalMethodLine+2) = $ListBuild("SampleNormalMethod+1",,,tIntCodeUnit.Hash,tIntCodeUnit.MethodMap.GetAt(methodLabel)+1) - Set tTestLines(tNormalMethodLine+3) = $ListBuild("SampleNormalMethod+2",,,tIntCodeUnit.Hash,tIntCodeUnit.MethodMap.GetAt(methodLabel)+2) - + Set tTestLines(tNormalMethodLine+2) = $ListBuild("SampleNormalMethod+1",,,tIntCodeUnit.Hash,tIntCodeUnit.MethodMap.GetAt(methodLabel)+1, "INT") + Set tTestLines(tNormalMethodLine+3) = $ListBuild("SampleNormalMethod+2",,,tIntCodeUnit.Hash,tIntCodeUnit.MethodMap.GetAt(methodLabel)+2, "INT") + Set tTestLines(tPythonMethodLine+2) = $ListBuild("SamplePythonMethod+1",,,tPyCodeUnit.Hash,tPyCodeUnit.MethodMap.GetAt("SamplePythonMethod")+1, "PY") + Set tTestLines(tPythonMethodLine+3) = $ListBuild("SamplePythonMethod+2",,,tPyCodeUnit.Hash,tPyCodeUnit.MethodMap.GetAt("SamplePythonMethod")+2, "PY") Set tLine = "" For { Set tLine = $Order(tTestLines(tLine),1,tInfo) @@ -57,9 +70,9 @@ Method TestCodeUnitCreation() } // Overwrite with defined values, leave defaults in AssertLineHandledCorrectly for undefined values (passing byref) - Kill tDescription, tExecutable, tMapped, tExpectedFromHash, tExpectedFromLine - Set $ListBuild(tDescription, tExecutable, tMapped, tExpectedFromHash, tExpectedFromLine) = tInfo - Do ..AssertLineHandledCorrectly(tClsCodeUnit, tLine, .tDescription, .tExecutable, .tMapped, .tExpectedFromHash, .tExpectedFromLine) + Kill tDescription, tExecutable, tMapped, tExpectedFromHash, tExpectedFromLine, tType + Set $ListBuild(tDescription, tExecutable, tMapped, tExpectedFromHash, tExpectedFromLine, tType) = tInfo + Do ..AssertLineHandledCorrectly(tClsCodeUnit, tLine, .tDescription, .tExecutable, .tMapped, .tExpectedFromHash, .tExpectedFromLine, .tType) } } Catch e { Set tSC = e.AsStatus() @@ -67,7 +80,7 @@ Method TestCodeUnitCreation() Do $$$AssertStatusOK(tSC,"No unexpected errors occurred.") } -Method AssertLineHandledCorrectly(pClassCodeUnit As TestCoverage.Data.CodeUnit, pLine As %Integer, pDescription As %String = {"Line "_pLine}, pExecutable As %Boolean = 1, pMapped As %Boolean = 1, pExpectedFromHash As %String = "", pExpectedFromLine As %Integer = "") As %Boolean +Method AssertLineHandledCorrectly(pClassCodeUnit As TestCoverage.Data.CodeUnit, pLine As %Integer, pDescription As %String = {"Line "_pLine}, pExecutable As %Boolean = 1, pMapped As %Boolean = 1, pExpectedFromHash As %String = "", pExpectedFromLine As %Integer = "", pType As %String = "INT") As %Boolean { Set tAllGood = 1 If pExecutable { @@ -78,7 +91,7 @@ Method AssertLineHandledCorrectly(pClassCodeUnit As TestCoverage.Data.CodeUnit, &sql(select count(*),FromHash,FromLine into :tCount,:tFromHash,:tFromLine from TestCoverage_Data.CodeUnitMap - where ToHash = :pClassCodeUnit.Hash and ToLine = :pLine and FromHash->Type = 'INT') + where ToHash = :pClassCodeUnit.Hash and ToLine = :pLine and FromHash->Type = :pType) If (SQLCODE < 0) { Throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) } @@ -123,5 +136,10 @@ ClassMethod SampleNormalMethod() Quit y } +ClassMethod SamplePythonMethod() [ Language = python ] +{ + import iris + return 50 } +} From 5e5ca7efd5285a18930d097e25cb2985298fc6bb Mon Sep 17 00:00:00 2001 From: Chris Ge Date: Mon, 8 Jul 2024 10:46:53 -0400 Subject: [PATCH 18/36] Added a test to make sure the embedded python methods' complexities are correct (per radon) --- .../UnitTest/TestCoverage/Unit/TestComplexity.cls | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/internal/testing/unit_tests/UnitTest/TestCoverage/Unit/TestComplexity.cls b/internal/testing/unit_tests/UnitTest/TestCoverage/Unit/TestComplexity.cls index e8942eb..1801d6c 100644 --- a/internal/testing/unit_tests/UnitTest/TestCoverage/Unit/TestComplexity.cls +++ b/internal/testing/unit_tests/UnitTest/TestCoverage/Unit/TestComplexity.cls @@ -10,6 +10,8 @@ Method TestMethodsInThisClass() Do $$$AssertStatusOK($System.OBJ.Compile($classname(),"ck-d")) Do $$$AssertStatusOK(##class(TestCoverage.Data.CodeUnit).GetCurrentByName(tClass_".1.INT")) If $$$AssertStatusOK(##class(TestCoverage.Data.CodeUnit).GetCurrentByName(tClass_".CLS",,.tCodeUnit)) { + Do $$$AssertStatusOK(##class(TestCoverage.Data.CodeUnit).GetCurrentByName(tClass_".PY",,.tPyCodeUnit)) + Do tCodeUnit.UpdatePyExecutableLines(tClass, .tPyCodeUnit) Set tKey = "" For { #dim tMethod As TestCoverage.Data.CodeSubUnit.Method @@ -243,5 +245,13 @@ Method MethodWithComplexMacros(pStatus As %Status) $$$ThrowOnError(pStatus) } +/// Complexity: 3 (1 + for + if) +ClassMethod ForLoopPython() [ Language = python ] +{ + for i in range(5): + if i > 4: + continue + return 1 } +} From d2e3ac5d8e1cba4f92f5689f9507c8cb3e044ca9 Mon Sep 17 00:00:00 2001 From: Chris Ge Date: Mon, 8 Jul 2024 11:10:00 -0400 Subject: [PATCH 19/36] Cleaned up a bit of dead code in CodeUnit and commented a lot of code --- cls/TestCoverage/Data/CodeUnit.cls | 25 +++++++++--------- cls/TestCoverage/Data/Coverage.cls | 2 ++ cls/TestCoverage/Utils.cls | 41 +++++------------------------- 3 files changed, 21 insertions(+), 47 deletions(-) diff --git a/cls/TestCoverage/Data/CodeUnit.cls b/cls/TestCoverage/Data/CodeUnit.cls index ad9353e..5e5351e 100644 --- a/cls/TestCoverage/Data/CodeUnit.cls +++ b/cls/TestCoverage/Data/CodeUnit.cls @@ -133,6 +133,7 @@ ClassMethod GetCurrentByName(pInternalName As %String, pSourceNamespace As %Stri If (tType = "PY") { + // fill in the Lines property of this CodeUnit set tPointer = 0 While $ListNext(pDocumentText, tPointer, tCurLine) { do pCodeUnit.Lines.Insert(tCurLine) @@ -140,10 +141,12 @@ ClassMethod GetCurrentByName(pInternalName As %String, pSourceNamespace As %Stri do pCodeUnit.Lines.Insert("") + // Filling in the MethodMap and LineToMethodMap properties Set ClassName = $Piece(tName,".", *) Set tMethodInfo = ##class(TestCoverage.Utils).GetPythonMethodMapping(pDocumentText, ClassName) - Set tLineToMethodInfo = tMethodInfo."__getitem__"(0) - Set tMethodMapInfo = tMethodInfo."__getitem__"(1) + // tMethodInfo is a python tuple of (line to method info, method map info) + Set tLineToMethodInfo = tMethodInfo."__getitem__"(0) // a python builtins list where the item at index i is the name of the method that line i is a part of + Set tMethodMapInfo = tMethodInfo."__getitem__"(1) // a python builtins dict with key = method name, value = the line number of its definition for i=1:1:$listlength(pDocumentText) { Set tMethod = tLineToMethodInfo."__getitem__"(i) Do pCodeUnit.LineToMethodMap.SetAt(tMethod,i) @@ -157,12 +160,6 @@ ClassMethod GetCurrentByName(pInternalName As %String, pSourceNamespace As %Stri Do pCodeUnit.MethodMap.SetAt(tStartLine,tMethod) Set tExecutableFlags(tStartLine) = 0 Do pCodeUnit.MethodEndMap.SetAt(tEndLine, tMethod) - - Set tMethodMask = "" - for j = tStartLine:1:tEndLine { - Set $Bit(tMethodMask,j) = 1 - } - Set tMethodSignature = $list(pDocumentText, tStartLine) } } Else { @@ -252,10 +249,8 @@ Method UpdatePyExecutableLines(pName As %String, ByRef pPyCodeUnit) As %Status Set tBitString = "" If (##class(TestCoverage.Manager).HasPython(pName)) { - // snapshot the Python CodeUnit for it Set tFromHash = pPyCodeUnit.Hash Set tToHash = ..Hash - // now bring over the executable lines &sql( DECLARE C1 CURSOR FOR @@ -352,12 +347,15 @@ Method UpdateSourceMap(pSourceNamespace As %String, ByRef pCache) As %Status set tClass = ..Name Set tSourceUnits(tClass_".CLS") = "" + // we'll need the MethodMap from the .CLS CodeUnit to figure out the line mappings $$$ThrowOnError(..GetCurrentByName(tClass _ ".CLS", pSourceNamespace, .pCLSCodeUnit, .pCLSCache)) // we'll do the mappings from the .py to the .cls direction, so that we don't iterate over objectscript lines Set tMethod = "" Do ..MethodMap.GetNext(.tMethod) while (tMethod '= "") { + // for each method in the .py file, we'll find the line number of the corresponding method (guaranteed to be unique) in the .cls file + // and then map each line in the .py file to each line in the .cls file by just going 1 by 1 down the lines Set tCLSMethodNum = pCLSCodeUnit.MethodMap.GetAt(tMethod) Set tMethodStart = ..MethodMap.GetAt(tMethod) Set tMethodEnd = ..MethodEndMap.GetAt(tMethod) @@ -506,7 +504,7 @@ Method UpdateComplexity() As %Status // python methods If (##class(TestCoverage.Manager).HasPython(..Name)) { - do ##class(TestCoverage.Data.CodeUnit).GetCurrentByName(..Name _ ".PY", , .pPyCodeUnit, ) + do ##class(TestCoverage.Data.CodeUnit).GetCurrentByName(..Name _ ".PY", , .pPyCodeUnit, ) // need the source code for the python set tDocumentText = pPyCodeUnit.Lines.Serialize() set tMethodComplexities = ..GetPythonComplexities(tDocumentText) } @@ -533,6 +531,7 @@ Method UpdateComplexity() As %Status Quit tSC } +/// returns a python dict with (key, value) = (method name, complexity) for each python method ClassMethod GetPythonComplexities(pDocumentText) [ Language = python ] { from radon.complexity import cc_visit @@ -578,8 +577,8 @@ ClassMethod GetCurrentHash(pName As %String, pType As %String, Output pHash As % // includes the class compilation signature. Set pHash = ..HashArrayRange(.pCodeArray,5,pName_"."_pType,.tSizeHint) } ElseIf (pType = "PY") { - Merge pCodeArray = ^ROUTINE(pName_".py",0) - set tSizeHint = ^ROUTINE(pName_".py",0,0) + Merge pCodeArray = ^ROUTINE(pName_".py",0) // the python source code + set tSizeHint = ^ROUTINE(pName_".py",0,0) // the number of lines in the python code set pHash = ..HashArrayRange(.pCodeArray, ,pName_".py", .pSizeHint) } Else { // Give standard descriptive error about the type being invalid. diff --git a/cls/TestCoverage/Data/Coverage.cls b/cls/TestCoverage/Data/Coverage.cls index ab77e8b..b7df41a 100644 --- a/cls/TestCoverage/Data/Coverage.cls +++ b/cls/TestCoverage/Data/Coverage.cls @@ -100,6 +100,8 @@ ClassMethod StoreIntCoverage(pRun As %Integer, pTestPath As %String, pName As %S $$$ThrowOnError(tSC) } Else { // If pType = "PY" + //^IRIS.TEMP.TestCoveragePy(ClassName) contains the number of covered lines in this class + //^IRIS.TEMP.TestCoveragePy(ClassName, i) in increasing order contain the line numbers for the covered lines if $Data(^IRIS.TEMP.TestCoveragePY(pName)) { for i = 1:1:(^IRIS.TEMP.TestCoveragePY(pName)-1) { Set tLineNumber = ^IRIS.TEMP.TestCoveragePY(pName, i) diff --git a/cls/TestCoverage/Utils.cls b/cls/TestCoverage/Utils.cls index 0897e3d..150f216 100644 --- a/cls/TestCoverage/Utils.cls +++ b/cls/TestCoverage/Utils.cls @@ -82,12 +82,6 @@ ClassMethod Snapshot(pIntRoutines As %List, Output pRelevantRoutines As %List = { Set tSC = $$$OK Try { - // For debugging use only: no parallelization - #; Set tPointer = 0 - #; While $ListNext(pIntRoutines, tPointer, tIntRoutine) { - #; do ##class(TestCoverage.Data.CodeUnit).GetCurrentByName(tIntRoutine_".INT") - #; } - #dim tSnapshotQueue As %SYSTEM.WorkMgr Set tSnapshotQueue = $System.WorkMgr.Initialize(,.tSC) $$$ThrowOnError(tSC) @@ -133,30 +127,6 @@ ClassMethod Snapshot(pIntRoutines As %List, Output pRelevantRoutines As %List = Set pRelevantRoutines = pRelevantRoutines _ $ListBuild(tIntRoutine) } - // snapshot all the compiled python CodeUnits - #; Set tSnapshotQueue = $System.WorkMgr.Initialize(,.tSC) - #; $$$ThrowOnError(tSC) - #; Set tPointer = 0 - #; While $ListNext(pPyRelevantRoutines,tPointer,tPyRoutine) { - #; Set tSC = tSnapshotQueue.Queue("##class(TestCoverage.Data.CodeUnit).GetCurrentByName",tPyRoutine_".PY") - #; $$$ThrowOnError(tSC) - #; } - - #; Set tSC = tSnapshotQueue.WaitForComplete() - #; $$$ThrowOnError(tSC) - - // update the executable lines for the .cls files that have python -- TODO: make the UpdatePyExecutableLines method not rely on knowing the .cls and .py CodeUnits beforehand - #; Set tSnapshotQueue = $System.WorkMgr.Initialize(,.tSC) - #; $$$ThrowOnError(tSC) - #; Set tPointer = 0 - #; While $ListNext(pPyRelevantRoutines,tPointer,tClass) { - #; Set tSC = tSnapshotQueue.Queue("##class(TestCoverage.Data.CodeUnit).UpdatePyExecutableLines",tClass) - #; $$$ThrowOnError(tSC) - #; } - - #; Set tSC = tSnapshotQueue.WaitForComplete() - #; $$$ThrowOnError(tSC) - Write ! } Catch e { Set tSC = e.AsStatus() @@ -443,6 +413,9 @@ ClassMethod CodeArrayToList(ByRef pCodeArray, Output pDocumentText As %List) quit } +/// returns a python tuple of (line to method info, method map info) +/// linetomethodinfo: a python builtins list where the item at index i is the name of the method that line i is a part of +/// methodmapinfo: a python builtins dict with key = method name, value = the line number of its definition ClassMethod GetPythonMethodMapping(pDocumentText, ClassName) [ Language = python ] { import iris @@ -454,13 +427,13 @@ ClassMethod GetPythonMethodMapping(pDocumentText, ClassName) [ Language = python source = ''.join(source_lines) tree = ast.parse(source) line_function_map = [None] * (len(source_lines)+2) - method_map = {} + method_map = {} # dictionary from the method name to its start and ending line number class FunctionMapper(ast.NodeVisitor): def __init__(self): self.current_class = None self.current_function = None - self.outermost_function = None + self.outermost_function = None # for objectscript purposes, we only care about the outer level functions/methods def visit_ClassDef(self, node): prev_class = self.current_class @@ -471,7 +444,7 @@ ClassMethod GetPythonMethodMapping(pDocumentText, ClassName) [ Language = python def visit_FunctionDef(self, node): if self.outermost_function is None: self.outermost_function = node.name - method_map[node.name] = (node.lineno-1, node.end_lineno-1) + method_map[node.name] = (node.lineno-1, node.end_lineno-1) self.current_function = node.name for lineno in range(node.lineno, node.end_lineno + 1): @@ -526,7 +499,7 @@ ClassMethod GetPythonLineExecutableFlags(pDocumentText) [ Language = python ] ast.AsyncWith, ast.Raise, ast.Try, ast.Assert, ast.Import, ast.ImportFrom, ast.Pass, ast.Break, ast.Continue, ast.Delete, ast.Yield, - ast.YieldFrom, ast.Await, ast.Nonlocal)): # all executable + ast.YieldFrom, ast.Await, ast.Nonlocal)): # all executable (determined manually) executable_lines.add(node.lineno) elif isinstance(node, ast.ExceptHandler): # except (but not finally) is executable executable_lines.add(node.lineno) From 793046ff231ea264e9361ded9cfc797194bf7947 Mon Sep 17 00:00:00 2001 From: Chris Ge Date: Mon, 8 Jul 2024 13:50:58 -0400 Subject: [PATCH 20/36] Edited changelog and module.xml --- CHANGELOG.md | 5 +++++ module.xml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f2b161..763dbcf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ 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] - 2024-07-08 + +### Added +- #29: Track code coverage for embedded python methods in .cls files + ## [3.1.0] - 2024-07-05 ### Added diff --git a/module.xml b/module.xml index 9ed8a0f..55a559a 100644 --- a/module.xml +++ b/module.xml @@ -2,7 +2,7 @@ TestCoverage - 3.1.0 + 3.2.0 Run your typical ObjectScript %UnitTest tests and see which lines of your code are executed. Includes Cobertura-style reporting for use in continuous integration tools. module From 35c119266e4006681db6296cad18351aba687cef Mon Sep 17 00:00:00 2001 From: Chris Ge Date: Fri, 12 Jul 2024 11:10:39 -0400 Subject: [PATCH 21/36] fix: classes with only embedded python files are now covered Turns out there's no .INT routine generated for .CLS files with only embedded python, which means that we can no longer piggyback finding out which python files to track by using the .CLS files. I now keep track of relevant and new python coverage targets separately --- cls/TestCoverage/Manager.cls | 42 ++++++++++++++++++++++-------------- cls/TestCoverage/Utils.cls | 29 +++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 18 deletions(-) diff --git a/cls/TestCoverage/Manager.cls b/cls/TestCoverage/Manager.cls index e6d178d..b7d4992 100644 --- a/cls/TestCoverage/Manager.cls +++ b/cls/TestCoverage/Manager.cls @@ -66,6 +66,10 @@ Property Run As TestCoverage.Data.Run; /// Value at subscript is set to 1 if there are executable lines of code in the target, 0 if not. Property KnownCoverageTargets [ MultiDimensional, Private ]; +/// Known python 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 KnownPyCoverageTargets [ MultiDimensional, Private ]; + /// Cache of (name, type) -> hash Property Hashes [ MultiDimensional ]; @@ -229,10 +233,9 @@ Method StartCoverageTracking() As %Status [ Private ] Set tSC = $$$OK New $Namespace Try { - If (..CoverageTargets '= "") { + If ((..CoverageTargets '= "" ) || (..PyCoverageTargets '= "")) { Set $Namespace = ..SourceNamespace Set tRelevantTargets = "" - Set tPyRelevantTargets = "" Set tNewTargets = "" Set tPointer = 0 While $ListNext(..CoverageTargets,tPointer,tCoverageTarget) { @@ -240,23 +243,25 @@ Method StartCoverageTracking() As %Status [ Private ] Set tNewTargets = tNewTargets_$ListBuild(tCoverageTarget) } ElseIf tIsRelevant { Set tRelevantTargets = tRelevantTargets_$ListBuild(tCoverageTarget) - // check if the relevant old targets have python components - Set tOther = ##class(%Library.RoutineMgr).GetOther(tCoverageTarget,"INT",-1) - If (tOther '= "") && ($Piece(tOther,".",*) = "CLS") { - set tName = $piece(tOther, ".", 1, *-1) - If (..HasPython(tName)) { - set tPyRelevantTargets = tPyRelevantTargets _ $ListBuild(tName) - } - } + } + } + + Set tPyRelevantTargets = "" + Set tNewPyTargets = "" + Set tPointer = 0 + While $ListNext(..PyCoverageTargets,tPointer,tPyCoverageTarget) { + If '$Data(..KnownPyCoverageTargets(tPyCoverageTarget),tIsRelevant)#2 { + Set tNewPyTargets = tNewPyTargets_$ListBuild(tPyCoverageTarget) + } ElseIf tIsRelevant { + Set tPyRelevantTargets = tPyRelevantTargets_$ListBuild(tPyCoverageTarget) } } - If (tNewTargets '= "") { + If ((tNewTargets '= "") || (tNewPyTargets '= "")) { $$$StartTimer("Taking snapshot of code and CLS/MAC/INT mappings") - Set tSC = ##class(TestCoverage.Utils).Snapshot(tNewTargets, .tNewRelevantTargets, .tNewPyRelevantTargets) + Set tSC = ##class(TestCoverage.Utils).Snapshot(tNewTargets, tNewPyTargets, .tNewRelevantTargets, .tNewPyRelevantTargets) $$$StopTimer $$$ThrowOnError(tSC) - Set tPointer = 0 While $ListNext(tNewTargets,tPointer,tNewTarget) { Set ..KnownCoverageTargets(tNewTarget) = 0 @@ -268,13 +273,19 @@ Method StartCoverageTracking() As %Status [ Private ] Set tRelevantTargets = tRelevantTargets_$ListBuild(tRelevantTarget) } + Set tPointer = 0 + While $ListNext(tNewPyTargets,tPointer,tNewPyTarget) { + Set ..KnownPyCoverageTargets(tNewPyTarget) = 0 + } + Set tPointer = 0 While $ListNext(tNewPyRelevantTargets,tPointer,tPyRelevantTarget) { + Set ..KnownPyCoverageTargets(tPyRelevantTarget) = 1 Set tPyRelevantTargets = tPyRelevantTargets_$ListBuild(tPyRelevantTarget) } } - If (tRelevantTargets = "") { + If ((tRelevantTargets = "") && (tPyRelevantTargets = "")) { Write !,"WARNING: Nothing found to monitor for routine(s): "_$ListToString(tNewTargets) } @@ -374,7 +385,6 @@ Method UpdateCoverageTargetsForTestDirectory(pDirectory As %String) As %Status [ } } } - Set tObjectCodeList = ..GetObjectCodeForSourceNames(tCoverageTargetList) Set ..CoverageTargets = tObjectCodeList // Also restarts the monitor if it is running and updates data on covered routines/classes Set ..PyCoverageTargets = tPyCoverageTargetList // no need to get the compiled names, it's already correct @@ -512,7 +522,7 @@ Method EndCoverageTracking(pTestSuite As %String = "", pTestClass As %String = " { Set tSC = $$$OK Try { - If (..CoverageTargets '= "") { + If ((..CoverageTargets '= "") || (..PyCoverageTargets '= "")) { // Pause the monitor. Set tSC = ..Monitor.Pause() Do ##class(TestCoverage.Utils.LineByLineMonitor).PyStop() diff --git a/cls/TestCoverage/Utils.cls b/cls/TestCoverage/Utils.cls index 150f216..c4a5b81 100644 --- a/cls/TestCoverage/Utils.cls +++ b/cls/TestCoverage/Utils.cls @@ -78,7 +78,7 @@ ClassMethod GetTestCoverageTableList() As %List /// 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 = "", Output pPyRelevantRoutines As %List = "") As %Status +ClassMethod Snapshot(pIntRoutines As %List, pPyRoutines As %List, Output pRelevantRoutines As %List = "", Output pPyRelevantRoutines As %List = "") As %Status { Set tSC = $$$OK Try { @@ -96,6 +96,7 @@ ClassMethod Snapshot(pIntRoutines As %List, Output pRelevantRoutines As %List = $$$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 @@ -106,17 +107,26 @@ ClassMethod Snapshot(pIntRoutines As %List, Output pRelevantRoutines As %List = // This also snapshots the compiled python routine with it if there is one #dim tCodeUnit As TestCoverage.Data.CodeUnit Set tSC = ##class(TestCoverage.Data.CodeUnit).GetCurrentByName(tOther,,.tCodeUnit) + If $$$ISERR(tSC) { Continue // Non-fatal. Just skip it. } Set tName = tCodeUnit.Name // should be the same as tOther without the .cls, but if I have it already why not + Set SnapshottedClasses(tName) = 1 If ##class(TestCoverage.Manager).HasPython(tName) { - set pPyRelevantRoutines = pPyRelevantRoutines _ $ListBuild(tName) // take a snapshot of the compiled python file $$$ThrowOnError(##class(TestCoverage.Data.CodeUnit).GetCurrentByName(tName_".PY",,.tPyCodeUnit)) // update the executable lines for the .cls file's python $$$ThrowOnError(tCodeUnit.UpdatePyExecutableLines(tName, .tPyCodeUnit)) + + // update the relevant python routines + If ($BitCount(tPyCodeUnit.ExecutableLines, 1)) { + set pPyRelevantRoutines = pPyRelevantRoutines _ $ListBuild(tName) + } ElseIf '$BitCount(tCodeUnit.ExecutableLines,1) { + // if there's no executable python and no executable objectscript, skip it + Continue + } } ElseIf '$BitCount(tCodeUnit.ExecutableLines,1) { // Skip it - no executable lines. @@ -127,6 +137,21 @@ ClassMethod Snapshot(pIntRoutines As %List, Output pRelevantRoutines As %List = Set pRelevantRoutines = pRelevantRoutines _ $ListBuild(tIntRoutine) } + // Snapshot all the python routines and their corresponding classes that haven't already been snapshotted + Set tPointer = 0 + While $ListNext(pPyRoutines, tPointer, tPyRoutine) { + If ('$Data(SnapshottedClasses(tPyRoutine))) { + $$$ThrowOnError(##class(TestCoverage.Data.CodeUnit).GetCurrentByName(tPyRoutine_".CLS",,.tCodeUnit)) + $$$ThrowOnError(##class(TestCoverage.Data.CodeUnit).GetCurrentByName(tPyRoutine_".PY",,.tPyCodeUnit)) + $$$ThrowOnError(tCodeUnit.UpdatePyExecutableLines(tPyRoutine, .tPyCodeUnit)) + + If ($BitCount(tPyCodeUnit.ExecutableLines, 1)) { + set pPyRelevantRoutines = pPyRelevantRoutines _ $ListBuild(tPyRoutine) + } + Set SnapshottedClasses(tPyRoutine) = 1 + } + } + Write ! } Catch e { Set tSC = e.AsStatus() From 1e0f4e49e6875feb3074fc25adc63a9689459fa4 Mon Sep 17 00:00:00 2001 From: Chris Ge Date: Mon, 15 Jul 2024 13:12:59 -0400 Subject: [PATCH 22/36] style: addressed Tim's comments on pr 37 (and simplified the sql statement) --- cls/TestCoverage/Data/CodeUnit.cls | 47 +++++++------------- cls/TestCoverage/Data/Coverage.cls | 8 ++-- cls/TestCoverage/Utils.cls | 6 +-- cls/TestCoverage/Utils/LineByLineMonitor.cls | 7 ++- inc/TestCoverage.inc | 1 + 5 files changed, 28 insertions(+), 41 deletions(-) diff --git a/cls/TestCoverage/Data/CodeUnit.cls b/cls/TestCoverage/Data/CodeUnit.cls index a01b2a4..6436f5b 100644 --- a/cls/TestCoverage/Data/CodeUnit.cls +++ b/cls/TestCoverage/Data/CodeUnit.cls @@ -251,38 +251,24 @@ Method UpdatePyExecutableLines(pName As %String, ByRef pPyCodeUnit) As %Status Set tFromHash = pPyCodeUnit.Hash Set tToHash = ..Hash - - &sql( - DECLARE C1 CURSOR FOR - SELECT map.ToLine - INTO :hToLine - FROM TestCoverage_Data.CodeUnitMap map - JOIN TestCoverage_Data.CodeUnit fromCodeUnit - ON map.FromHash = fromCodeUnit.Hash - JOIN TestCoverage_Data.CodeUnit toCodeUnit - ON map.ToHash = toCodeUnit.Hash - AND fromCodeUnit.Hash = :tFromHash - AND toCodeUnit.Hash = :tToHash - WHERE TestCoverage.BIT_VALUE(fromCodeUnit.ExecutableLines,map.FromLine) <> 0 - ) - &sql(OPEN C1) - If (SQLCODE < 0) { - Throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) + set sql = "SELECT map.ToLine FROM TestCoverage_Data.CodeUnitMap map " _ + "JOIN TestCoverage_Data.CodeUnit fromCodeUnit " _ + "ON fromCodeUnit.Hash = map.FromHash " _ + "WHERE map.FromHash = ? " _ + "AND map.ToHash = ? " _ + "AND TestCoverage.BIT_VALUE(fromCodeUnit.ExecutableLines,map.FromLine) <> 0" + + set resultSet = ##class(%SQL.Statement).%ExecDirect(, sql, tFromHash, tToHash) + If (resultSet.%SQLCODE < 0) { + Throw ##class(%Exception.SQL).CreateFromSQLCODE(resultSet.%SQLCODE, resultSet.%Message) } - For { - &SQL(FETCH C1) - If (SQLCODE < 0) { - Throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) - } ElseIf (SQLCODE) { - Quit - } - // Process the fetched rows - // hToLine contains the line number in the .cls file corresponding to executable lines in the .py file + while resultSet.%Next(.tSC) { + $$$ThrowOnError(tSC) + Set hToLine = resultSet.%GetData(1) Set $Bit(tBitString, hToLine) = 1 } - &sql(CLOSE C1) - If (SQLCODE < 0) { - Throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) + If (resultSet.%SQLCODE < 0) { + Throw ##class(%Exception.SQL).CreateFromSQLCODE(resultSet.%SQLCODE, resultSet.%Message) } } Set ..ExecutableLines = $BITLOGIC(..ExecutableLines | tBitString) @@ -362,8 +348,6 @@ Method UpdateSourceMap(pSourceNamespace As %String, ByRef pCache) As %Status Set tMethodName = tMethod Set tFullMap(tMethodStart) = $lb("CLS", tClass,tMethodName, -1, -1) ; -1 because the class ; definition doesn't have the +1 offset from the { - - Do ..MethodMap.GetNext(.tMethod) For i = tMethodStart+1:1:tMethodEnd { Set tClassLineNum = i-tMethodStart Set tFullMap(i) = $lb("CLS", tClass,tMethodName, tClassLineNum, tClassLineNum) @@ -375,6 +359,7 @@ Method UpdateSourceMap(pSourceNamespace As %String, ByRef pCache) As %Status Set tSC = $$$ERROR($$$GeneralError,"Compiled .py code doesn't match .CLS python code ") } } + Do ..MethodMap.GetNext(.tMethod) } } diff --git a/cls/TestCoverage/Data/Coverage.cls b/cls/TestCoverage/Data/Coverage.cls index b7df41a..7d900b8 100644 --- a/cls/TestCoverage/Data/Coverage.cls +++ b/cls/TestCoverage/Data/Coverage.cls @@ -1,3 +1,5 @@ +Include TestCoverage + IncludeGenerator TestCoverage Class TestCoverage.Data.Coverage Extends %Persistent @@ -102,9 +104,9 @@ ClassMethod StoreIntCoverage(pRun As %Integer, pTestPath As %String, pName As %S Else { // If pType = "PY" //^IRIS.TEMP.TestCoveragePy(ClassName) contains the number of covered lines in this class //^IRIS.TEMP.TestCoveragePy(ClassName, i) in increasing order contain the line numbers for the covered lines - if $Data(^IRIS.TEMP.TestCoveragePY(pName)) { - for i = 1:1:(^IRIS.TEMP.TestCoveragePY(pName)-1) { - Set tLineNumber = ^IRIS.TEMP.TestCoveragePY(pName, i) + if $Data($$$PyMonitorResults(pName)) { + for i = 1:1:($$$PyMonitorResults(pName)-1) { + Set tLineNumber = $$$PyMonitorResults(pName, i) Set $Bit(tCoveredLines, tLineNumber) = 1 } } diff --git a/cls/TestCoverage/Utils.cls b/cls/TestCoverage/Utils.cls index c4a5b81..d46835b 100644 --- a/cls/TestCoverage/Utils.cls +++ b/cls/TestCoverage/Utils.cls @@ -431,9 +431,9 @@ ClassMethod GetClassLineExecutableFlags(pClassName As %String, ByRef pDocumentTe ClassMethod CodeArrayToList(ByRef pCodeArray, Output pDocumentText As %List) { - set pDocumentText = $lb() - for i=1:1:pCodeArray(0) { - set $list(pDocumentText, i) = pCodeArray(i) + set pDocumentText = "" + for i=1:1:$get(pCodeArray(0)) { + set pDocumentText = pDocumentText _ $ListBuild(pCodeArray(i)) } quit } diff --git a/cls/TestCoverage/Utils/LineByLineMonitor.cls b/cls/TestCoverage/Utils/LineByLineMonitor.cls index aecf3b9..bbe1947 100644 --- a/cls/TestCoverage/Utils/LineByLineMonitor.cls +++ b/cls/TestCoverage/Utils/LineByLineMonitor.cls @@ -1,4 +1,4 @@ -Include %occErrors +Include (%occErrors, TestCoverage) /// Wrapper around %Monitor.System.LineByLine to ensure that the monitor is stopped when it should be, and also /// to wrap the decision about whether to stop/start the monitor or to just clear counters. @@ -70,8 +70,7 @@ ClassMethod PyStartWithScope(pCoverageClasses As %List) [ Language = python ] # extracts the line number line_no = frame.f_lineno if class_name in tCoverageClasses and line_no != 1: # if this is in a covered class - # print(f"A {event} encountered in {func_name}() in class {class_name} at line number {line_no}") - tGlob = iris.gref('^IRIS.TEMP.TestCoveragePY') + tGlob = iris.gref('^IRIS.TEMP.TestCoveragePY') # python doesn't have macros -- this is $$$PyMonitorResults curId = tGlob.get([class_name]) if not curId: tGlob[class_name] = 1 @@ -84,7 +83,7 @@ ClassMethod PyStartWithScope(pCoverageClasses As %List) [ Language = python ] ClassMethod PyClearCounters() { - Kill ^IRIS.TEMP.TestCoveragePY + Kill $$$PyMonitorResults } ClassMethod PyStop() [ Language = python ] diff --git a/inc/TestCoverage.inc b/inc/TestCoverage.inc index ba88615..ba71e4e 100644 --- a/inc/TestCoverage.inc +++ b/inc/TestCoverage.inc @@ -3,3 +3,4 @@ ROUTINE TestCoverage [Type=INC] #define StopTimer If (..Display["log") { Write ($zh-tStartTime)," seconds" } #define METRICS "RtnLine","Time","TotalTime" #define TestPathAllTests "all tests" +#define PyMonitorResults ^IRIS.TEMP.TestCoveragePY \ No newline at end of file From 08e99028dffb2b7c9e2f42317d365c4c0731256d Mon Sep 17 00:00:00 2001 From: Chris Ge Date: Mon, 15 Jul 2024 15:50:20 -0400 Subject: [PATCH 23/36] fix: testcoveragelist doesn't fail if FindCoverageList doesn't find samplecovlist.list in the first directory now --- .../UnitTest/TestCoverage/Unit/TestCoverageList.cls | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/internal/testing/unit_tests/UnitTest/TestCoverage/Unit/TestCoverageList.cls b/internal/testing/unit_tests/UnitTest/TestCoverage/Unit/TestCoverageList.cls index f43a0e6..038e0bb 100644 --- a/internal/testing/unit_tests/UnitTest/TestCoverage/Unit/TestCoverageList.cls +++ b/internal/testing/unit_tests/UnitTest/TestCoverage/Unit/TestCoverageList.cls @@ -4,6 +4,7 @@ Class UnitTest.TestCoverage.Unit.TestCoverageList Extends %UnitTest.TestCase /// helper function to find the samplecovlist.list's path ClassMethod FindCoverageList(directory As %String = "") As %String { + set stmt = ##class(%SQL.Statement).%New() set status = stmt.%PrepareClassQuery("%File", "FileSet") if $$$ISERR(status) {write "%Prepare failed:" do $SYSTEM.Status.DisplayError(status) quit} @@ -22,15 +23,20 @@ ClassMethod FindCoverageList(directory As %String = "") As %String return name } } elseif (type = "D"){ - do ..FindCoverageList(name) + set retVal = ..FindCoverageList(name) + if (retVal '= "") { + return retVal + } } } if (rset.%SQLCODE < 0) {write "%Next failed:", !, "SQLCODE ", rset.%SQLCODE, ": ", rset.%Message quit} + return "" // didn't find it in this directory } Method TestGettingCoverageList() { set tFile = ..FindCoverageList(^UnitTestRoot) // finds the samplecovlist.list + set ^IRIS.TEMPCJG($i(^IRIS.TEMPCJG)) = tFile do ##class(TestCoverage.Manager).GetCoverageTargetsForFile(tFile, .tTargetArray) Set CorrectCoverageTargets("CLS", "TestCoverage.Data.CodeSubUnit") = "" From eb6077f2b86fc5a14ca952181cb2dd492f5eadc0 Mon Sep 17 00:00:00 2001 From: Chris Ge Date: Tue, 16 Jul 2024 16:58:48 -0400 Subject: [PATCH 24/36] feat: the python monitor results now store Rtnline metrics, and in a more efficient manner than before --- cls/TestCoverage/Data/Coverage.cls | 20 ++++++++++++++------ cls/TestCoverage/Utils/LineByLineMonitor.cls | 13 +++++++------ 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/cls/TestCoverage/Data/Coverage.cls b/cls/TestCoverage/Data/Coverage.cls index 7d900b8..237ca90 100644 --- a/cls/TestCoverage/Data/Coverage.cls +++ b/cls/TestCoverage/Data/Coverage.cls @@ -60,7 +60,6 @@ ClassMethod StoreIntCoverage(pRun As %Integer, pTestPath As %String, pName As %S #dim tResult As %SQL.StatementResult Set tSC = ##class(TestCoverage.Data.CodeUnit).GetCurrentByName(pName_"."_pType,,.tCodeUnit,.pCache) $$$ThrowOnError(tSC) - If ..UniqueCoverageDataExists(pRun,tCodeUnit.Hash,pTestPath,.tID) { Set tInstance = ..%OpenId(tID,,.tSC) $$$ThrowOnError(tSC) @@ -70,6 +69,10 @@ ClassMethod StoreIntCoverage(pRun As %Integer, pTestPath As %String, pName As %S $$$ThrowOnError(tSC) Set tInstance.TestPath = pTestPath Set tInstance.Hash = tCodeUnit + For tLineNumber=1:1:tCodeUnit.Lines.Count() { + do tInstance.RtnLine.SetAt(0, tLineNumber) // initialized to 0 hits of each line + // necessary for the python coverages because they don't track lines that aren't covered, only lines that are covered + } } Set tCoveredLines = tInstance.CoveredLines @@ -102,12 +105,17 @@ ClassMethod StoreIntCoverage(pRun As %Integer, pTestPath As %String, pName As %S $$$ThrowOnError(tSC) } Else { // If pType = "PY" - //^IRIS.TEMP.TestCoveragePy(ClassName) contains the number of covered lines in this class - //^IRIS.TEMP.TestCoveragePy(ClassName, i) in increasing order contain the line numbers for the covered lines + // $$$PyMonitorResults(classname, linenumber) = the number of times that linenumber in that class was covered + if $Data($$$PyMonitorResults(pName)) { - for i = 1:1:($$$PyMonitorResults(pName)-1) { - Set tLineNumber = $$$PyMonitorResults(pName, i) - Set $Bit(tCoveredLines, tLineNumber) = 1 + Set tLine = "" + for { + Set tLine = $Order($$$PyMonitorResults(pName, tLine), 1, tLineCount) + if (tLine = "") { + quit + } + Set $Bit(tCoveredLines, tLine) = 1 + Do tInstance.RtnLine.SetAt(tInstance.RtnLine.GetAt(tLine) + tLineCount, tLine) } } diff --git a/cls/TestCoverage/Utils/LineByLineMonitor.cls b/cls/TestCoverage/Utils/LineByLineMonitor.cls index bbe1947..65aff95 100644 --- a/cls/TestCoverage/Utils/LineByLineMonitor.cls +++ b/cls/TestCoverage/Utils/LineByLineMonitor.cls @@ -71,12 +71,13 @@ ClassMethod PyStartWithScope(pCoverageClasses As %List) [ Language = python ] line_no = frame.f_lineno if class_name in tCoverageClasses and line_no != 1: # if this is in a covered class tGlob = iris.gref('^IRIS.TEMP.TestCoveragePY') # python doesn't have macros -- this is $$$PyMonitorResults - curId = tGlob.get([class_name]) - if not curId: - tGlob[class_name] = 1 - curId = 1 - tGlob[class_name, curId] = line_no - tGlob[class_name] = curId+1 + # $$$PyMonitorResults(classname, linenumber) = the number of times that linenumber in that class was covered + + curCount = tGlob.get([class_name, line_no]) + if not curCount: + curCount = 0 + tGlob[class_name, line_no] = curCount + 1 + return my_tracer settrace(my_tracer) } From 4ef22615c9fcbc43b40ac90bd94322756cb2b541 Mon Sep 17 00:00:00 2001 From: Chris Ge Date: Wed, 17 Jul 2024 14:30:29 -0400 Subject: [PATCH 25/36] fix: changed pSizeHint to tSizeHint --- cls/TestCoverage/Data/CodeUnit.cls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cls/TestCoverage/Data/CodeUnit.cls b/cls/TestCoverage/Data/CodeUnit.cls index 6436f5b..2858c0c 100644 --- a/cls/TestCoverage/Data/CodeUnit.cls +++ b/cls/TestCoverage/Data/CodeUnit.cls @@ -564,7 +564,7 @@ ClassMethod GetCurrentHash(pName As %String, pType As %String, Output pHash As % } ElseIf (pType = "PY") { Merge pCodeArray = ^ROUTINE(pName_".py",0) // the python source code set tSizeHint = ^ROUTINE(pName_".py",0,0) // the number of lines in the python code - set pHash = ..HashArrayRange(.pCodeArray, ,pName_".py", .pSizeHint) + set pHash = ..HashArrayRange(.pCodeArray, ,pName_".py", .tSizeHint) } Else { // Give standard descriptive error about the type being invalid. $$$ThrowStatus(..TypeIsValid(pType)) From d07947e8351e22685dba65783546a3aa1562afbe Mon Sep 17 00:00:00 2001 From: Chris Ge Date: Wed, 17 Jul 2024 16:08:17 -0400 Subject: [PATCH 26/36] style: cleaned up some dead code --- cls/TestCoverage/Data/CodeUnit.cls | 5 ++--- cls/TestCoverage/Utils.cls | 4 ++-- cls/TestCoverage/Utils/LineByLineMonitor.cls | 1 - 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/cls/TestCoverage/Data/CodeUnit.cls b/cls/TestCoverage/Data/CodeUnit.cls index 2858c0c..b00ac73 100644 --- a/cls/TestCoverage/Data/CodeUnit.cls +++ b/cls/TestCoverage/Data/CodeUnit.cls @@ -142,8 +142,7 @@ ClassMethod GetCurrentByName(pInternalName As %String, pSourceNamespace As %Stri do pCodeUnit.Lines.Insert("") // Filling in the MethodMap and LineToMethodMap properties - Set ClassName = $Piece(tName,".", *) - Set tMethodInfo = ##class(TestCoverage.Utils).GetPythonMethodMapping(pDocumentText, ClassName) + Set tMethodInfo = ##class(TestCoverage.Utils).GetPythonMethodMapping(pDocumentText) // tMethodInfo is a python tuple of (line to method info, method map info) Set tLineToMethodInfo = tMethodInfo."__getitem__"(0) // a python builtins list where the item at index i is the name of the method that line i is a part of Set tMethodMapInfo = tMethodInfo."__getitem__"(1) // a python builtins dict with key = method name, value = the line number of its definition @@ -356,7 +355,7 @@ Method UpdateSourceMap(pSourceNamespace As %String, ByRef pCache) As %Status Set tClassLineCode = $zstrip(pCLSCodeUnit.Lines.GetAt(tCLSMethodNum + tClassLineNum + 1), "<>W") Set tPyLineCode = $zstrip(..Lines.GetAt(i), "<>W") if (tPyLineCode '= tClassLineCode) { - Set tSC = $$$ERROR($$$GeneralError,"Compiled .py code doesn't match .CLS python code ") + Set tSC = $$$ERROR($$$GeneralError,"Compiled .py code doesn't match .CLS python code at line " _ $char(10,13) _ tPyLineCode) } } Do ..MethodMap.GetNext(.tMethod) diff --git a/cls/TestCoverage/Utils.cls b/cls/TestCoverage/Utils.cls index d46835b..9e48539 100644 --- a/cls/TestCoverage/Utils.cls +++ b/cls/TestCoverage/Utils.cls @@ -441,14 +441,13 @@ ClassMethod CodeArrayToList(ByRef pCodeArray, Output pDocumentText As %List) /// returns a python tuple of (line to method info, method map info) /// linetomethodinfo: a python builtins list where the item at index i is the name of the method that line i is a part of /// methodmapinfo: a python builtins dict with key = method name, value = the line number of its definition -ClassMethod GetPythonMethodMapping(pDocumentText, ClassName) [ Language = python ] +ClassMethod GetPythonMethodMapping(pDocumentText) [ Language = python ] { import iris import ast source_lines = iris.cls('%SYS.Python').ToList(pDocumentText) source_lines = [line + "\n" for line in source_lines] # contains a list of each line of the source code - class_name = ClassName source = ''.join(source_lines) tree = ast.parse(source) line_function_map = [None] * (len(source_lines)+2) @@ -480,6 +479,7 @@ ClassMethod GetPythonMethodMapping(pDocumentText, ClassName) [ Language = python if self.outermost_function == node.name: self.outermost_function = None + # preprocessing the ending line number for each function tree_with_line_numbers = ast.increment_lineno(tree, n=1) for node in ast.walk(tree_with_line_numbers): if isinstance(node, ast.FunctionDef): diff --git a/cls/TestCoverage/Utils/LineByLineMonitor.cls b/cls/TestCoverage/Utils/LineByLineMonitor.cls index 65aff95..d4912c8 100644 --- a/cls/TestCoverage/Utils/LineByLineMonitor.cls +++ b/cls/TestCoverage/Utils/LineByLineMonitor.cls @@ -65,7 +65,6 @@ ClassMethod PyStartWithScope(pCoverageClasses As %List) [ Language = python ] # extracts frame code code = frame.f_code # extracts calling function name and the class that the function is in - func_name = code.co_name class_name = frame.f_globals['__name__'] # extracts the line number line_no = frame.f_lineno From db53aa955f2600c2c51004e09cfe0270cb08ecfc Mon Sep 17 00:00:00 2001 From: Chris Ge Date: Wed, 17 Jul 2024 16:08:17 -0400 Subject: [PATCH 27/36] style: cleaned up some dead code --- cls/TestCoverage/Data/CodeUnit.cls | 5 ++--- cls/TestCoverage/Utils.cls | 4 ++-- cls/TestCoverage/Utils/LineByLineMonitor.cls | 1 - .../UnitTest/TestCoverage/Unit/TestCoverageList.cls | 1 - 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/cls/TestCoverage/Data/CodeUnit.cls b/cls/TestCoverage/Data/CodeUnit.cls index 2858c0c..b00ac73 100644 --- a/cls/TestCoverage/Data/CodeUnit.cls +++ b/cls/TestCoverage/Data/CodeUnit.cls @@ -142,8 +142,7 @@ ClassMethod GetCurrentByName(pInternalName As %String, pSourceNamespace As %Stri do pCodeUnit.Lines.Insert("") // Filling in the MethodMap and LineToMethodMap properties - Set ClassName = $Piece(tName,".", *) - Set tMethodInfo = ##class(TestCoverage.Utils).GetPythonMethodMapping(pDocumentText, ClassName) + Set tMethodInfo = ##class(TestCoverage.Utils).GetPythonMethodMapping(pDocumentText) // tMethodInfo is a python tuple of (line to method info, method map info) Set tLineToMethodInfo = tMethodInfo."__getitem__"(0) // a python builtins list where the item at index i is the name of the method that line i is a part of Set tMethodMapInfo = tMethodInfo."__getitem__"(1) // a python builtins dict with key = method name, value = the line number of its definition @@ -356,7 +355,7 @@ Method UpdateSourceMap(pSourceNamespace As %String, ByRef pCache) As %Status Set tClassLineCode = $zstrip(pCLSCodeUnit.Lines.GetAt(tCLSMethodNum + tClassLineNum + 1), "<>W") Set tPyLineCode = $zstrip(..Lines.GetAt(i), "<>W") if (tPyLineCode '= tClassLineCode) { - Set tSC = $$$ERROR($$$GeneralError,"Compiled .py code doesn't match .CLS python code ") + Set tSC = $$$ERROR($$$GeneralError,"Compiled .py code doesn't match .CLS python code at line " _ $char(10,13) _ tPyLineCode) } } Do ..MethodMap.GetNext(.tMethod) diff --git a/cls/TestCoverage/Utils.cls b/cls/TestCoverage/Utils.cls index d46835b..9e48539 100644 --- a/cls/TestCoverage/Utils.cls +++ b/cls/TestCoverage/Utils.cls @@ -441,14 +441,13 @@ ClassMethod CodeArrayToList(ByRef pCodeArray, Output pDocumentText As %List) /// returns a python tuple of (line to method info, method map info) /// linetomethodinfo: a python builtins list where the item at index i is the name of the method that line i is a part of /// methodmapinfo: a python builtins dict with key = method name, value = the line number of its definition -ClassMethod GetPythonMethodMapping(pDocumentText, ClassName) [ Language = python ] +ClassMethod GetPythonMethodMapping(pDocumentText) [ Language = python ] { import iris import ast source_lines = iris.cls('%SYS.Python').ToList(pDocumentText) source_lines = [line + "\n" for line in source_lines] # contains a list of each line of the source code - class_name = ClassName source = ''.join(source_lines) tree = ast.parse(source) line_function_map = [None] * (len(source_lines)+2) @@ -480,6 +479,7 @@ ClassMethod GetPythonMethodMapping(pDocumentText, ClassName) [ Language = python if self.outermost_function == node.name: self.outermost_function = None + # preprocessing the ending line number for each function tree_with_line_numbers = ast.increment_lineno(tree, n=1) for node in ast.walk(tree_with_line_numbers): if isinstance(node, ast.FunctionDef): diff --git a/cls/TestCoverage/Utils/LineByLineMonitor.cls b/cls/TestCoverage/Utils/LineByLineMonitor.cls index 65aff95..d4912c8 100644 --- a/cls/TestCoverage/Utils/LineByLineMonitor.cls +++ b/cls/TestCoverage/Utils/LineByLineMonitor.cls @@ -65,7 +65,6 @@ ClassMethod PyStartWithScope(pCoverageClasses As %List) [ Language = python ] # extracts frame code code = frame.f_code # extracts calling function name and the class that the function is in - func_name = code.co_name class_name = frame.f_globals['__name__'] # extracts the line number line_no = frame.f_lineno diff --git a/internal/testing/unit_tests/UnitTest/TestCoverage/Unit/TestCoverageList.cls b/internal/testing/unit_tests/UnitTest/TestCoverage/Unit/TestCoverageList.cls index 038e0bb..2658e6b 100644 --- a/internal/testing/unit_tests/UnitTest/TestCoverage/Unit/TestCoverageList.cls +++ b/internal/testing/unit_tests/UnitTest/TestCoverage/Unit/TestCoverageList.cls @@ -36,7 +36,6 @@ ClassMethod FindCoverageList(directory As %String = "") As %String Method TestGettingCoverageList() { set tFile = ..FindCoverageList(^UnitTestRoot) // finds the samplecovlist.list - set ^IRIS.TEMPCJG($i(^IRIS.TEMPCJG)) = tFile do ##class(TestCoverage.Manager).GetCoverageTargetsForFile(tFile, .tTargetArray) Set CorrectCoverageTargets("CLS", "TestCoverage.Data.CodeSubUnit") = "" From c4c0164bbc91210602d1813253dbc7555d8562d8 Mon Sep 17 00:00:00 2001 From: Chris Ge Date: Fri, 19 Jul 2024 10:26:44 -0400 Subject: [PATCH 28/36] feat: initialized all metrics to 0 so that none are empty at the end --- cls/TestCoverage/Data/Run.cls | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/cls/TestCoverage/Data/Run.cls b/cls/TestCoverage/Data/Run.cls index 09dadc9..1afd79d 100644 --- a/cls/TestCoverage/Data/Run.cls +++ b/cls/TestCoverage/Data/Run.cls @@ -101,6 +101,17 @@ ClassMethod MapRunCoverage(pRunIndex As %Integer) As %Status Do tCoverage.RunSetObjectId(pRunIndex) Do tCoverage.HashSetObjectId(hToHash) Set tCoverage.TestPath = hTestPath + // also set all of its metrics to 0 to start with + Set tCodeUnit = ##class(TestCoverage.Data.CodeUnit).%OpenId(hToHash) + For i=1:1:tRun.Metrics.Count() { + Set tMetricKey = tRun.Metrics.GetAt(i) + Set tMetric = $PROPERTY(tCoverage, tMetricKey) + for tLineNumber = 1:1:tCodeUnit.Lines.Count() { + Do tMetric.SetAt(0, tLineNumber) + } + } + + } Set tCoverage.Ignore = hIgnore Set tCoverage.CoveredLines = $BitLogic(tCoverage.CoveredLines|hCoveredLines) @@ -191,4 +202,3 @@ Storage Default } } - From 4963be8e9b990230da4763e2e8aa9ca7a462564a Mon Sep 17 00:00:00 2001 From: Chris Ge Date: Mon, 22 Jul 2024 10:11:08 -0400 Subject: [PATCH 29/36] feat: added a property LineIsPython that stores if each line of code is a python line or not --- cls/TestCoverage/Data/CodeUnit.cls | 61 +++++++++++++++++++ cls/TestCoverage/Utils.cls | 4 ++ .../UnitTest/TestCoverage/Unit/CodeUnit.cls | 7 +++ 3 files changed, 72 insertions(+) diff --git a/cls/TestCoverage/Data/CodeUnit.cls b/cls/TestCoverage/Data/CodeUnit.cls index b00ac73..c63b838 100644 --- a/cls/TestCoverage/Data/CodeUnit.cls +++ b/cls/TestCoverage/Data/CodeUnit.cls @@ -38,9 +38,13 @@ Property MethodEndMap As array Of %Integer; /// For routines, map of labels to associated line numbers Property LineToMethodMap As array Of %Dictionary.CacheIdentifier [ Private ]; +/// For each line, whether or not it belongs to a python method, only populated for .cls CodeUnits +Property LineIsPython As array Of %Boolean; + /// Set to true if this class/routine is generated Property Generated As %Boolean [ InitialExpression = 0 ]; +/// /// Methods, branches, etc. within this unit of code. Relationship SubUnits As TestCoverage.Data.CodeSubUnit [ Cardinality = children, Inverse = Parent ]; @@ -129,6 +133,7 @@ ClassMethod GetCurrentByName(pInternalName As %String, pSourceNamespace As %Stri If (tType = "CLS") { Set pCodeUnit.Generated = ($$$comClassKeyGet(tName,$$$cCLASSgeneratedby) '= "") + } @@ -170,6 +175,9 @@ ClassMethod GetCurrentByName(pInternalName As %String, pSourceNamespace As %Stri Do pCodeUnit.Lines.Insert(tLine) If (tType = "CLS") { + // initialize each line to not python (we'll update this later) + Do pCodeUnit.LineIsPython.SetAt(0, tLineNumber) + // Extract line offset of methods in classes Set tStart = $Piece(tLine," ") If (tStart = "ClassMethod") || (tStart = "Method") { @@ -236,6 +244,54 @@ ClassMethod GetCurrentByName(pInternalName As %String, pSourceNamespace As %Stri Quit tSC } +/// Fill in the LineIsPython property of .cls files +Method UpdatePythonLines(pName As %String, ByRef pPyCodeUnit) As %Status +{ + + Set tSC = $$$OK + Set tOriginalNamespace = $Namespace + Set tInitTLevel = $TLevel + + Try { + TSTART + + If (##class(TestCoverage.Manager).HasPython(pName)) { + + Set tFromHash = pPyCodeUnit.Hash + Set tToHash = ..Hash + set sql = "SELECT map.ToLine FROM TestCoverage_Data.CodeUnitMap map " _ + "JOIN TestCoverage_Data.CodeUnit fromCodeUnit " _ + "ON fromCodeUnit.Hash = map.FromHash " _ + "WHERE map.FromHash = ? " _ + "AND map.ToHash = ? " + set resultSet = ##class(%SQL.Statement).%ExecDirect(, sql, tFromHash, tToHash) + If (resultSet.%SQLCODE < 0) { + Throw ##class(%Exception.SQL).CreateFromSQLCODE(resultSet.%SQLCODE, resultSet.%Message) + } + while resultSet.%Next(.tSC) { + $$$ThrowOnError(tSC) + Set hToLine = resultSet.%GetData(1) + set ^IRIS.TempCG($i(^IRIS.TempCG)) = hToLine + do ..LineIsPython.SetAt(1, hToLine) + } + If (resultSet.%SQLCODE < 0) { + Throw ##class(%Exception.SQL).CreateFromSQLCODE(resultSet.%SQLCODE, resultSet.%Message) + } + } + Set tSC = ..%Save() + $$$ThrowOnError(tSC) + + TCOMMIT + } Catch e { + Set pCodeUnit = $$$NULLOREF + Set tSC = e.AsStatus() + } + While ($TLevel > tInitTLevel) { + TROLLBACK 1 + } + Quit tSC +} + /// Get the executable lines of code in python over to the .cls CodeUnit Method UpdatePyExecutableLines(pName As %String, ByRef pPyCodeUnit) As %Status { @@ -636,6 +692,11 @@ Storage Default Generated + +LineIsPython +subnode +"LineIsPython" + LineToMethodMap subnode diff --git a/cls/TestCoverage/Utils.cls b/cls/TestCoverage/Utils.cls index 9e48539..16ddf19 100644 --- a/cls/TestCoverage/Utils.cls +++ b/cls/TestCoverage/Utils.cls @@ -120,6 +120,9 @@ ClassMethod Snapshot(pIntRoutines As %List, pPyRoutines As %List, Output pReleva // update the executable lines for the .cls file's python $$$ThrowOnError(tCodeUnit.UpdatePyExecutableLines(tName, .tPyCodeUnit)) + // update the pythonicity of the lines for the .cls file + $$$ThrowOnError(tCodeUnit.UpdatePythonLines(tName, .tPyCodeUnit)) + // update the relevant python routines If ($BitCount(tPyCodeUnit.ExecutableLines, 1)) { set pPyRelevantRoutines = pPyRelevantRoutines _ $ListBuild(tName) @@ -144,6 +147,7 @@ ClassMethod Snapshot(pIntRoutines As %List, pPyRoutines As %List, Output pReleva $$$ThrowOnError(##class(TestCoverage.Data.CodeUnit).GetCurrentByName(tPyRoutine_".CLS",,.tCodeUnit)) $$$ThrowOnError(##class(TestCoverage.Data.CodeUnit).GetCurrentByName(tPyRoutine_".PY",,.tPyCodeUnit)) $$$ThrowOnError(tCodeUnit.UpdatePyExecutableLines(tPyRoutine, .tPyCodeUnit)) + $$$ThrowOnError(tCodeUnit.UpdatePythonLines(tPyRoutine, .tPyCodeUnit)) If ($BitCount(tPyCodeUnit.ExecutableLines, 1)) { set pPyRelevantRoutines = pPyRelevantRoutines _ $ListBuild(tPyRoutine) diff --git a/internal/testing/unit_tests/UnitTest/TestCoverage/Unit/CodeUnit.cls b/internal/testing/unit_tests/UnitTest/TestCoverage/Unit/CodeUnit.cls index f1e30c6..1174a48 100644 --- a/internal/testing/unit_tests/UnitTest/TestCoverage/Unit/CodeUnit.cls +++ b/internal/testing/unit_tests/UnitTest/TestCoverage/Unit/CodeUnit.cls @@ -42,6 +42,9 @@ Method TestCodeUnitCreation() Set tSC = tClsCodeUnit.UpdatePyExecutableLines($classname(),.tPyCodeUnit) Do $$$AssertStatusOK(tSC,"Found updated executable line data for "_tClsName) + Set tSC = tClsCodeUnit.UpdatePythonLines($classname(),.tPyCodeUnit) + Do $$$AssertStatusOK(tSC,"Found updated pythonicity line data for "_tClsName) + Set tConstantReturnValueLine = tClsCodeUnit.MethodMap.GetAt("SampleConstantReturnValue") Set tCodeGeneratorLine = tClsCodeUnit.MethodMap.GetAt("SampleCodeGenerator") Set tNormalMethodLine = tClsCodeUnit.MethodMap.GetAt("SampleNormalMethod") @@ -51,6 +54,10 @@ Method TestCodeUnitCreation() Do $$$AssertNotEquals(tCodeGeneratorLine,"") Do $$$AssertNotEquals(tNormalMethodLine,"") Do $$$AssertNotEquals(tPythonMethodLine,"") + + // test if LineIsPython is working properly + Do $$$AssertEquals(tClsCodeUnit.LineIsPython.GetAt(tPythonMethodLine+2), 1) + Do $$$AssertEquals(tClsCodeUnit.LineIsPython.GetAt(tNormalMethodLine+2), 0) // tTestLines(line number) = $ListBuild(description, executable (default 1), mapped (default 1), mapped from hash (if relevant), mapped from line (if relevant)) Set tTestLines(tConstantReturnValueLine+2) = $ListBuild("SampleConstantReturnValue+1",0,0) From b1cd1b96e6b71981c6c741aafee869c9a32eca80 Mon Sep 17 00:00:00 2001 From: Chris Ge Date: Mon, 22 Jul 2024 10:59:20 -0400 Subject: [PATCH 30/36] style: removed debugging global --- cls/TestCoverage/Data/CodeUnit.cls | 1 - 1 file changed, 1 deletion(-) diff --git a/cls/TestCoverage/Data/CodeUnit.cls b/cls/TestCoverage/Data/CodeUnit.cls index c63b838..0ac96c3 100644 --- a/cls/TestCoverage/Data/CodeUnit.cls +++ b/cls/TestCoverage/Data/CodeUnit.cls @@ -271,7 +271,6 @@ Method UpdatePythonLines(pName As %String, ByRef pPyCodeUnit) As %Status while resultSet.%Next(.tSC) { $$$ThrowOnError(tSC) Set hToLine = resultSet.%GetData(1) - set ^IRIS.TempCG($i(^IRIS.TempCG)) = hToLine do ..LineIsPython.SetAt(1, hToLine) } If (resultSet.%SQLCODE < 0) { From 2854c1840d7cc1037251fc4a5a61387989e27c4e Mon Sep 17 00:00:00 2001 From: Chris Ge Date: Mon, 22 Jul 2024 16:10:06 -0400 Subject: [PATCH 31/36] style: changed IRIS.TEMP to IRIS.Temp --- cls/TestCoverage/Utils/LineByLineMonitor.cls | 2 +- inc/TestCoverage.inc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cls/TestCoverage/Utils/LineByLineMonitor.cls b/cls/TestCoverage/Utils/LineByLineMonitor.cls index d4912c8..fe929d9 100644 --- a/cls/TestCoverage/Utils/LineByLineMonitor.cls +++ b/cls/TestCoverage/Utils/LineByLineMonitor.cls @@ -69,7 +69,7 @@ ClassMethod PyStartWithScope(pCoverageClasses As %List) [ Language = python ] # extracts the line number line_no = frame.f_lineno if class_name in tCoverageClasses and line_no != 1: # if this is in a covered class - tGlob = iris.gref('^IRIS.TEMP.TestCoveragePY') # python doesn't have macros -- this is $$$PyMonitorResults + tGlob = iris.gref('^IRIS.Temp.TestCoveragePY') # python doesn't have macros -- this is $$$PyMonitorResults # $$$PyMonitorResults(classname, linenumber) = the number of times that linenumber in that class was covered curCount = tGlob.get([class_name, line_no]) diff --git a/inc/TestCoverage.inc b/inc/TestCoverage.inc index ba71e4e..47611ca 100644 --- a/inc/TestCoverage.inc +++ b/inc/TestCoverage.inc @@ -3,4 +3,4 @@ ROUTINE TestCoverage [Type=INC] #define StopTimer If (..Display["log") { Write ($zh-tStartTime)," seconds" } #define METRICS "RtnLine","Time","TotalTime" #define TestPathAllTests "all tests" -#define PyMonitorResults ^IRIS.TEMP.TestCoveragePY \ No newline at end of file +#define PyMonitorResults ^IRIS.Temp.TestCoveragePY \ No newline at end of file From 9a2da9be3a6cc4b655d192eef21dc4b40e85834f Mon Sep 17 00:00:00 2001 From: Chris Ge Date: Tue, 30 Jul 2024 17:10:59 -0400 Subject: [PATCH 32/36] feat: added listener interface and manager with broadcasting on test starts and finishes --- CHANGELOG.md | 3 ++ .../Listeners/ListenerInterface.cls | 8 ++++ .../Listeners/ListenerManager.cls | 48 +++++++++++++++++++ cls/TestCoverage/Manager.cls | 28 +++++++++++ 4 files changed, 87 insertions(+) create mode 100644 cls/TestCoverage/Listeners/ListenerInterface.cls create mode 100644 cls/TestCoverage/Listeners/ListenerManager.cls diff --git a/CHANGELOG.md b/CHANGELOG.md index 8efb43b..816f5bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - #39: Fixed bug where results viewer gave divide by zero error when there were 0 executed methods in the covered code +### 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.0] - 2024-07-05 ### Added diff --git a/cls/TestCoverage/Listeners/ListenerInterface.cls b/cls/TestCoverage/Listeners/ListenerInterface.cls new file mode 100644 index 0000000..ce60843 --- /dev/null +++ b/cls/TestCoverage/Listeners/ListenerInterface.cls @@ -0,0 +1,8 @@ +Class TestCoverage.Listeners.ListenerInterface Extends %RegisteredObject +{ + +Method Broadcast(pMessage As %String) As %Status [ Abstract ] +{ +} + +} diff --git a/cls/TestCoverage/Listeners/ListenerManager.cls b/cls/TestCoverage/Listeners/ListenerManager.cls new file mode 100644 index 0000000..ec91970 --- /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 %String) 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 9ca08a2..8ff6a8b 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... @@ -637,6 +642,9 @@ ClassMethod OnAfterAllTests(manager As TestCoverage.Manager, dir As %String, ByR Try { If (manager.CoverageDetail = 0) { Set tSC = manager.EndCoverageTracking() + if (manager.ListenerManager) { + Do manager.ListenerManager.BroadCastToAll("All tests complete") + } } Do manager.Monitor.Stop() } Catch e { @@ -673,9 +681,13 @@ Method OnBeforeTestSuite(dir As %String, suite As %String, testspec As %String, Set ..CurrentTestSuite = $Case(suite,"":"(root)",:suite) Set ..CurrentTestClass = "" Set ..CurrentTestMethod = "" + if (..ListenerManager) { + Do ..ListenerManager.BroadCastToAll("Starting test suite: " _ suite ) + } If (..CoverageDetail = 1) { Set tSC = ..StartCoverageTracking() } + } Catch e { Set tSC = e.AsStatus() } @@ -688,9 +700,13 @@ 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) { + Do ..ListenerManager.BroadCastToAll("Finished test suite: " _ suite ) + } } Catch e { Set tSC = e.AsStatus() } @@ -705,6 +721,9 @@ Method OnBeforeTestCase(suite As %String, class As %String) As %Status Try { Set ..CurrentTestClass = class Set ..CurrentTestMethod = "" + if (..ListenerManager) { + Do ..ListenerManager.BroadCastToAll("Starting test case: " _ suite _ "/" _ class ) + } If (..CoverageDetail = 2) { Set tSC = ..StartCoverageTracking() } @@ -723,6 +742,9 @@ Method OnAfterTestCase(suite As %String, class As %String) As %Status If (..CoverageDetail = 2) { Set tSC = ..EndCoverageTracking(suite, class) } + if (..ListenerManager) { + Do ..ListenerManager.BroadCastToAll("Starting test case: " _ suite _ "/" _ class ) + } } Catch e { Set tSC = e.AsStatus() } @@ -736,6 +758,9 @@ Method OnBeforeOneTest(suite As %String, class As %String, method As %String) As Set tSC = $$$OK Try { Set ..CurrentTestMethod = method + if (..ListenerManager) { + Do ..ListenerManager.BroadCastToAll("Starting test method: " _ suite _ "/" _ class _ "/" _ method ) + } If (..CoverageDetail = 3) { Set tSC = ..StartCoverageTracking() } @@ -754,6 +779,9 @@ Method OnAfterOneTest(suite As %String, class As %String, method As %String) As If (..CoverageDetail = 3) { Set tSC = ..EndCoverageTracking(suite, class, method) } + if (..ListenerManager) { + Do ..ListenerManager.BroadCastToAll("Finished test method: " _ suite _ "/" _ class _ "/" _ method ) + } } Catch e { Set tSC = e.AsStatus() } From 9f7b9cbecf27eeed095293d35d8a897a0e220b55 Mon Sep 17 00:00:00 2001 From: Chris Ge Date: Wed, 31 Jul 2024 10:38:39 -0400 Subject: [PATCH 33/36] feat: changed the broadcast message type from strig to json --- .../Listeners/ListenerInterface.cls | 2 +- .../Listeners/ListenerManager.cls | 2 +- cls/TestCoverage/Manager.cls | 33 +++++++++++++++---- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/cls/TestCoverage/Listeners/ListenerInterface.cls b/cls/TestCoverage/Listeners/ListenerInterface.cls index ce60843..44479a7 100644 --- a/cls/TestCoverage/Listeners/ListenerInterface.cls +++ b/cls/TestCoverage/Listeners/ListenerInterface.cls @@ -1,7 +1,7 @@ Class TestCoverage.Listeners.ListenerInterface Extends %RegisteredObject { -Method Broadcast(pMessage As %String) As %Status [ Abstract ] +Method Broadcast(pMessage As %DynamicObject) As %Status [ Abstract ] { } diff --git a/cls/TestCoverage/Listeners/ListenerManager.cls b/cls/TestCoverage/Listeners/ListenerManager.cls index ec91970..a76f64b 100644 --- a/cls/TestCoverage/Listeners/ListenerManager.cls +++ b/cls/TestCoverage/Listeners/ListenerManager.cls @@ -3,7 +3,7 @@ Class TestCoverage.Listeners.ListenerManager Extends %RegisteredObject Property listeners As list Of TestCoverage.Listeners.ListenerInterface; -Method BroadCastToAll(pMessage As %String) As %Status +Method BroadCastToAll(pMessage As %DynamicObject) As %Status { set tSC = $$$OK try { diff --git a/cls/TestCoverage/Manager.cls b/cls/TestCoverage/Manager.cls index 8ff6a8b..1678849 100644 --- a/cls/TestCoverage/Manager.cls +++ b/cls/TestCoverage/Manager.cls @@ -643,7 +643,8 @@ ClassMethod OnAfterAllTests(manager As TestCoverage.Manager, dir As %String, ByR If (manager.CoverageDetail = 0) { Set tSC = manager.EndCoverageTracking() if (manager.ListenerManager) { - Do manager.ListenerManager.BroadCastToAll("All tests complete") + set tObj = {"message": "All tests complete"} + Do manager.ListenerManager.BroadCastToAll(tObj) } } Do manager.Monitor.Stop() @@ -682,7 +683,9 @@ Method OnBeforeTestSuite(dir As %String, suite As %String, testspec As %String, Set ..CurrentTestClass = "" Set ..CurrentTestMethod = "" if (..ListenerManager) { - Do ..ListenerManager.BroadCastToAll("Starting test suite: " _ suite ) + set tObj = {"message": "Starting test suite: "} + do tObj.%Set("suite", suite) + Do ..ListenerManager.BroadCastToAll(tObj) } If (..CoverageDetail = 1) { Set tSC = ..StartCoverageTracking() @@ -705,7 +708,9 @@ Method OnAfterTestSuite(dir As %String, suite As %String, testspec As %String, B Set tSC = ..EndCoverageTracking($Case(suite,"":"(root)",:suite)) } if (..ListenerManager) { - Do ..ListenerManager.BroadCastToAll("Finished test suite: " _ suite ) + set tObj = {"message": "Finished test suite: "} + do tObj.%Set("suite", suite) + Do ..ListenerManager.BroadCastToAll(tObj) } } Catch e { Set tSC = e.AsStatus() @@ -722,7 +727,10 @@ Method OnBeforeTestCase(suite As %String, class As %String) As %Status Set ..CurrentTestClass = class Set ..CurrentTestMethod = "" if (..ListenerManager) { - Do ..ListenerManager.BroadCastToAll("Starting test case: " _ suite _ "/" _ class ) + 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() @@ -743,7 +751,10 @@ Method OnAfterTestCase(suite As %String, class As %String) As %Status Set tSC = ..EndCoverageTracking(suite, class) } if (..ListenerManager) { - Do ..ListenerManager.BroadCastToAll("Starting test case: " _ suite _ "/" _ class ) + 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() @@ -759,7 +770,11 @@ Method OnBeforeOneTest(suite As %String, class As %String, method As %String) As Try { Set ..CurrentTestMethod = method if (..ListenerManager) { - Do ..ListenerManager.BroadCastToAll("Starting test method: " _ suite _ "/" _ class _ "/" _ method ) + 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() @@ -780,7 +795,11 @@ Method OnAfterOneTest(suite As %String, class As %String, method As %String) As Set tSC = ..EndCoverageTracking(suite, class, method) } if (..ListenerManager) { - Do ..ListenerManager.BroadCastToAll("Finished test method: " _ suite _ "/" _ class _ "/" _ method ) + 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() From d6b3079bd9610a197000b60009e249575b529a08 Mon Sep 17 00:00:00 2001 From: Chris Ge Date: Wed, 31 Jul 2024 10:41:25 -0400 Subject: [PATCH 34/36] style: added in a broadcast for starting tests too --- cls/TestCoverage/Manager.cls | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cls/TestCoverage/Manager.cls b/cls/TestCoverage/Manager.cls index 1678849..65d8fa7 100644 --- a/cls/TestCoverage/Manager.cls +++ b/cls/TestCoverage/Manager.cls @@ -624,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) } From 25ae6b143af151b4b8b36a8258662637ddfdde95 Mon Sep 17 00:00:00 2001 From: Chris Ge Date: Wed, 31 Jul 2024 16:01:47 -0400 Subject: [PATCH 35/36] docs: edited changelog --- CHANGELOG.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 816f5bc..9cb15a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,14 +5,16 @@ 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] - Unreleased ### Fixed - #39: Fixed bug where results viewer gave divide by zero error when there were 0 executed methods in the covered code -### 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.0] - 2024-07-05 ### Added From 05300c6a832a2156bfd86ce710072efea0eabe55 Mon Sep 17 00:00:00 2001 From: Chris Ge Date: Wed, 31 Jul 2024 16:14:04 -0400 Subject: [PATCH 36/36] docs: fixed changelog.md again --- CHANGELOG.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2ca722..3021ec7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,9 +17,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - #39: Fixed bug where results viewer gave divide by zero error when there were 0 executed methods in the covered code - #41: Now the code strips leading and trailing whitespace from coverage.list, so "PackageName.PKG " will still be loaded properly -### 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.0] - 2024-07-05 ### Added