├── .github └── workflows │ ├── main.yml │ ├── objectscript-quality.yml │ └── release.yml ├── .gitignore ├── .project ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── cls └── TestCoverage │ ├── Data │ ├── Aggregate │ │ ├── Base.cls │ │ ├── ByCodeUnit.cls │ │ └── ByRun.cls │ ├── CodeSubUnit.cls │ ├── CodeSubUnit │ │ └── Method.cls │ ├── CodeUnit.cls │ ├── CodeUnitMap.cls │ ├── Coverage.cls │ └── Run.cls │ ├── DataType │ ├── Bitstring.cls │ ├── Detail.cls │ ├── Metric.cls │ ├── RoutineType.cls │ └── Timing.cls │ ├── Listeners │ ├── ListenerInterface.cls │ └── ListenerManager.cls │ ├── Manager.cls │ ├── Procedures.cls │ ├── Report │ ├── AbstractReportGenerator.cls │ └── Cobertura │ │ ├── ReportGenerator.cls │ │ └── Schema.cls │ ├── UI │ ├── AggregateResultViewer.cls │ ├── Application.cls │ ├── CodeMapExplorer.cls │ ├── Component │ │ ├── altJSONSQLProvider.cls │ │ ├── codeCSS.cls │ │ ├── dataGrid.cls │ │ ├── select.cls │ │ └── testResultsLink.cls │ ├── ResultDetailViewer.cls │ ├── SimpleResultViewer.cls │ ├── Template.cls │ └── Utils.cls │ ├── Utils.cls │ └── Utils │ ├── ComplexityParser.cls │ ├── File.cls │ ├── LineByLineMonitor.cls │ └── Projection │ └── SchemaGenerator.cls ├── inc └── TestCoverage.inc ├── internal └── testing │ └── unit_tests │ └── UnitTest │ ├── TestCoverage │ └── Unit │ │ ├── CodeUnit.cls │ │ ├── Procedures.cls │ │ ├── TestComplexity.cls │ │ ├── TestCoverageList.cls │ │ ├── TestDelimitedIdentifiers.cls │ │ ├── TestIsExecutable.cls │ │ ├── coverage.list │ │ ├── sampleRoutine.mac │ │ └── samplecovlist.list │ └── coverage.list ├── isc.json ├── module.xml └── requirements.txt /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # Continuous integration workflow 2 | name: CI 3 | 4 | # Controls when the action will run. Triggers the workflow on push or pull request 5 | # events in all branches 6 | on: [push, pull_request] 7 | 8 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 9 | jobs: 10 | # This workflow contains a single job called "build" 11 | build: 12 | # The type of runner that the job will run on 13 | runs-on: ubuntu-latest 14 | 15 | env: 16 | # ** FOR GENERAL USE, LIKELY NEED TO CHANGE: ** 17 | package: TestCoverage 18 | container_image: intersystemsdc/iris-community:latest 19 | 20 | # ** FOR GENERAL USE, MAY NEED TO CHANGE: ** 21 | build_flags: -dev -verbose # Load in -dev mode to get unit test code preloaded 22 | test_package: UnitTest 23 | 24 | # ** FOR GENERAL USE, SHOULD NOT NEED TO CHANGE: ** 25 | instance: iris 26 | # Note: test_reports value is duplicated in test_flags environment variable 27 | test_reports: test-reports 28 | test_flags: >- 29 | -verbose -DUnitTest.ManagerClass=TestCoverage.Manager -DUnitTest.JUnitOutput=/test-reports/junit.xml 30 | -DUnitTest.FailuresAreFatal=1 -DUnitTest.Manager=TestCoverage.Manager 31 | -DUnitTest.UserParam.CoverageReportClass=TestCoverage.Report.Cobertura.ReportGenerator 32 | -DUnitTest.UserParam.CoverageReportFile=/source/coverage.xml 33 | 34 | # Steps represent a sequence of tasks that will be executed as part of the job 35 | steps: 36 | 37 | # Checks out this repository under $GITHUB_WORKSPACE, so your job can access it 38 | - uses: actions/checkout@v2 39 | 40 | - name: Run Container 41 | run: | 42 | # Create test_reports directory to share test results before running container 43 | mkdir $test_reports 44 | chmod 777 $test_reports 45 | # Run InterSystems IRIS instance 46 | docker pull $container_image 47 | docker run -d -h $instance --name $instance -v $GITHUB_WORKSPACE:/source -v $GITHUB_WORKSPACE/$test_reports:/$test_reports --init $container_image 48 | echo halt > wait 49 | # Wait for instance to be ready 50 | until docker exec --interactive $instance iris session $instance < wait; do sleep 1; done 51 | 52 | - name: Install TestCoverage 53 | run: | 54 | echo "zpm \"install testcoverage\":1:1" > install-testcoverage 55 | docker exec --interactive $instance iris session $instance -B < install-testcoverage 56 | # Workaround for permissions issues in TestCoverage (creating directory for source export) 57 | chmod 777 $GITHUB_WORKSPACE 58 | 59 | # Runs a set of commands using the runners shell 60 | - name: Build and Test 61 | run: | 62 | # Run build 63 | echo "zpm \"load /source $build_flags\":1:1" > build 64 | # Test package is compiled first as a workaround for some dependency issues. 65 | echo "do \$System.OBJ.CompilePackage(\"$test_package\",\"ckd\") " > test 66 | # Run tests 67 | echo "zpm \"$package test -only $test_flags\":1:1" >> test 68 | docker exec --interactive $instance iris session $instance -B < build && docker exec --interactive $instance iris session $instance -B < test && bash <(curl -s https://codecov.io/bash) 69 | # Generate and Upload HTML xUnit report 70 | - name: XUnit Viewer 71 | id: xunit-viewer 72 | uses: AutoModality/action-xunit-viewer@v1 73 | if: always() 74 | with: 75 | # With -DUnitTest.FailuresAreFatal=1 a failed unit test will fail the build before this point. 76 | # This action would otherwise misinterpret our xUnit style output and fail the build even if 77 | # all tests passed. 78 | fail: false 79 | - name: Attach the report 80 | uses: actions/upload-artifact@v4 81 | if: always() 82 | with: 83 | name: ${{ steps.xunit-viewer.outputs.report-name }} 84 | path: ${{ steps.xunit-viewer.outputs.report-dir }} 85 | -------------------------------------------------------------------------------- /.github/workflows/objectscript-quality.yml: -------------------------------------------------------------------------------- 1 | name: objectscriptquality 2 | on: push 3 | 4 | jobs: 5 | linux: 6 | name: Linux build 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Execute ObjectScript Quality Analysis 11 | run: wget https://raw.githubusercontent.com/litesolutions/objectscriptquality-jenkins-integration/master/iris-community-hook.sh && sh ./iris-community-hook.sh 12 | 13 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v[0-9]+.[0-9]+.[0-9]+' # force semantic versioning 7 | 8 | jobs: 9 | build-and-release: 10 | runs-on: ubuntu-latest 11 | 12 | env: 13 | container_image: intersystemsdc/iris-community:latest 14 | instance: iris 15 | test_reports: test-reports 16 | 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v4 20 | 21 | - name: Run Container 22 | run: | 23 | # Create test_reports directory to share test results before running container 24 | mkdir $test_reports 25 | chmod 777 $test_reports 26 | # Run InterSystems IRIS instance 27 | docker pull $container_image 28 | docker run -d -h $instance --name $instance -v $GITHUB_WORKSPACE:/source -v $GITHUB_WORKSPACE/$test_reports:/$test_reports --init $container_image 29 | echo halt > wait 30 | # Wait for instance to be ready 31 | until docker exec --interactive $instance iris session $instance < wait; do sleep 1; done 32 | 33 | - name: Install TestCoverage 34 | run: | 35 | echo "zpm \"install testcoverage\":1:1" > install-testcoverage 36 | docker exec --interactive $instance iris session $instance -B < install-testcoverage 37 | # Workaround for permissions issues in TestCoverage (creating directory for source export) 38 | chmod 777 $GITHUB_WORKSPACE 39 | 40 | - name: Export XML 41 | run: | 42 | # Pick the targets to export as XML 43 | echo 'set list("TestCoverage.*.cls") = ""' >> export 44 | echo 'set list("TestCoverage.inc") = ""' >> export 45 | echo 'do $System.OBJ.Export(.list,"/source/TestCoverage-${{ github.ref_name }}.xml","/exportversion=2017.2")' >> export 46 | docker exec --interactive $instance iris session $instance -B < export 47 | 48 | - name: Create Release 49 | id: create_release 50 | uses: softprops/action-gh-release@v1 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | with: 54 | files: TestCoverage-${{ github.ref_name }}.xml 55 | tag_name: ${{ github.ref_name }} 56 | name: ${{ github.ref_name }} 57 | body: | 58 | Automated release created by [action-gh-release](https://github.com/softprops/action-gh-release). 59 | draft: false 60 | prerelease: false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.bak 2 | readme.txt 3 | .buildpath 4 | .vscode -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | TestCoverage 4 | 5 | 6 | 7 | 8 | 9 | com.intersystems.atelier.core.validator 10 | 11 | 12 | 13 | 14 | 15 | com.intersystems.atelier.core.nature 16 | 17 | 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # TestCoverage 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [4.0.5] - 2024-11-04 9 | 10 | ### Fixed 11 | - #57: Improve SQL performance when mapping run coverage 12 | 13 | ## [4.0.4] - 2024-10-15 14 | 15 | ### Fixed 16 | - #54: Defend against possible configuration-dependent SQL exceptions in mapping INT to MAC/CLS coverage 17 | 18 | ## [4.0.3] - 2024-08-19 19 | 20 | ### Fixed 21 | - #52: Method mapping code now doesn't use AST's endline_no property to support older python versions 22 | - #53: Ignore traced commands from code without a class name 23 | 24 | ## [4.0.2] - 2024-08-16 25 | 26 | ### Fixed 27 | - #51: Don't start (and stop) the ObjectScript and Python monitors if there are no ObjectScript/Python routines being tracked respectively, fixes error from trying to start/stop the %Monitor.System.LineByLine with no routines 28 | 29 | 30 | ## [4.0.1] - 2024-08-16 31 | 32 | ### Fixed 33 | - #45: Fixed Python line 0 tracking for 2024.2 34 | - #46: Fix for bug caused by UpdateComplexity calling GetCurrentByName unnecessarily and causing dependency issues 35 | - #47: Fixed mapping issue caused by empty lines at top of Python method not showing up in compiled Python 36 | - #48: When the Line-By-Line Monitor resumes after pausing, resume the Python tracer too 37 | - #49: Added user parameter for preloading python modules (fixes problem of pandas breaking sys.settrace on first import) 38 | 39 | ## [4.0.0] - 2024-08-01 40 | 41 | ### Changed 42 | - #29: As a consequence of this change, the minimum supported platform version is 2022.1 43 | 44 | ### Added 45 | - #29: Track code coverage for embedded python methods in .cls files 46 | - #42: Added a listener interface and manager with an associated user parameter, allowing the user to broadcast output on test method/case/suite completion. 47 | 48 | ## [3.1.1] - 2024-07-31 49 | 50 | ### Fixed 51 | - #39: Fixed bug where results viewer gave divide by zero error when there were 0 executed methods in the covered code 52 | - #41: Now the code strips leading and trailing whitespace from coverage.list, so "PackageName.PKG " will still be loaded properly 53 | 54 | ## [3.1.0] - 2024-07-05 55 | 56 | ### Added 57 | - #23: Allow CoverageClasses and CoverageRoutines to be specified as %DynamicArray in addition to $ListBuild() lists. 58 | - #14: Added a straightforward way to find and track coverage on all interoperability processes in the current namespace 59 | 60 | ### Fixed 61 | - #24: Whenever a new tag is created, a new release will be published using the tag string as its version. The release also comes with an export of the TestCoverage package in XML. 62 | 63 | ## [3.0.0] - 2023-12-01 64 | 65 | ### Changed 66 | - #25: As a consequence of this change, the minimum supported platform version is 2019.1. 67 | 68 | ### Fixed 69 | - #18: EOL normalization in coverage.list 70 | - #19: Update CI to latest IRIS community (and corresponding test updates) 71 | - #25: Fix so the tool works on 2023.1 72 | 73 | ## [2.1.3] - 2022-03-30 74 | - Last released version before CHANGELOG existed. 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | Thank you for your interest in contributing! While this project originated at InterSystems, it is our hope that the community will continue to extend and enhance it. Our priority is to add and enhance features to meet our internal and general use cases. We do not intend to provide adapters to other code coverage reporting tools and formats, but if you make one, please do share it with the community! 4 | 5 | ## Submitting changes 6 | 7 | If you have made a change that you would like to contribute back to the community, please send a [GitHub Pull Request](/pull/new/master) explaining it. If your change fixes an issue that you or another user reported, please mention it in the pull request. You can find out more about pull requests [here](http://help.github.com/pull-requests/). 8 | 9 | ## Coding conventions 10 | 11 | Generally speaking, just try to match the conventions you see in the code you are reading. For this project, these include: 12 | 13 | * Do not use shortened command and function names - e.g., `s` instead of `set`, `$p` instead of `$piece` 14 | * One command per line 15 | * Do not use dot syntax 16 | * Indentation with tabs 17 | * Pascal case class and method names 18 | * Avoid using postconditionals 19 | * Local variables start with `t`; formal parameter names start with `p` 20 | * Always check %Status return values 21 | 22 | Thanks, 23 | Tim Leavitt, InterSystems Corporation -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 InterSystems Corporation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![codecov](https://codecov.io/gh/intersystems/TestCoverage/branch/master/graph/badge.svg)](https://codecov.io/gh/intersystems/TestCoverage) 2 | [![Quality Gate Status](https://community.objectscriptquality.com/api/project_badges/measure?project=intersystems_iris_community%2FTestCoverage&metric=alert_status)](https://community.objectscriptquality.com/dashboard?id=intersystems_iris_community%2FTestCoverage) 3 | 4 | # Unit Test Coverage for InterSystems ObjectScript 5 | 6 | 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. 7 | 8 | ## Getting Started 9 | 10 | A minimum platform version of InterSystems IRIS® data platform 2022.1 is required to run the latest version of TestCoverage. 11 | 12 | | InterSystems Platform Version | Compatible TestCoverage Version | 13 | |-------------------------------|----------------------------------------------------------------------------------------------------| 14 | | IRIS >=2022.1 | 4.x | 15 | | IRIS <2022.1 | 3.x | 16 | | Caché / Ensemble | 2.x (via artifacts available in [Releases](https://github.com/intersystems/TestCoverage/releases))| 17 | 18 | ### Installation: IPM 19 | 20 | If you already have the [InterSystems Package Manager](https://openexchange.intersystems.com/package/InterSystems-Package-Manager-1), installation is as easy as: 21 | ``` 22 | zpm "install testcoverage" 23 | ``` 24 | 25 | ### Installation: of Release 26 | 27 | Download an XML file from [Releases](https://github.com/intersystems/TestCoverage/releases), then run: 28 | ``` 29 | Set releaseFile = "" 30 | Do $System.OBJ.Load(releaseFile,"ck") 31 | ``` 32 | 33 | ### Installation: from Terminal 34 | 35 | First, clone or download the repository. Then run the following commands: 36 | 37 | ``` 38 | Set root = "" 39 | Do $System.OBJ.ImportDir(root,"*.inc;*.cls","ck",,1) 40 | ``` 41 | 42 | ### Security 43 | Note that, depending on your security settings, SQL privileges may be required for access to test coverage data. The relevant permissions may be granted by running: 44 | 45 | ``` 46 | zw ##class(TestCoverage.Utils).GrantSQLReadPermissions("") 47 | ``` 48 | 49 | For example: 50 | 51 | ``` 52 | zw ##class(TestCoverage.Utils).GrantSQLReadPermissions("_PUBLIC") 53 | ``` 54 | 55 | ## User Guide 56 | 57 | ### Running Tests with Coverage 58 | Generally speaking, set `^UnitTestRoot`, and then call `##class(TestCoverage.Manager).RunTest()` the same you would call `##class(%UnitTest.Manager).RunTest()`. For more information on InterSystems' %UnitTest framework, see the [tutorial](https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=TUNT) and/or the [class reference for %UnitTest.Manager](https://docs.intersystems.com/irislatest/csp/documatic/%25CSP.Documatic.cls?PAGE=CLASS&LIBRARY=%25SYS&CLASSNAME=%25UnitTest.Manager). 59 | 60 | The "userparam" argument can be used to pass optional information about code coverage data collection. For example: 61 | 62 | ``` 63 | Set tCoverageParams("CoverageClasses") = <$ListBuild list or %DynamicArray of class names for which code coverage data should be collected> 64 | Set tCoverageParams("CoverageRoutines") = <$ListBuild list or %DynamicArray of routine names for which code coverage data should be collected> 65 | Set tCoverageParams("CoverageDetail") = <0 to track code coverage overall; 1 to track it per test suite (the default); 2 to track it per test class; 3 to track it per test method.> 66 | Set tCoverageParams("ProcessIDs") = <$ListBuild list of process IDs to monitor, or "Interoperability"> 67 | Set tCoverageParams("Timing") = <1 to capture timing data, 0 to not> 68 | Set tCoverageParams("PyModules") = <$ListBuild list of Python module names to preload> 69 | Set tCoverageParams("ListenerManager") = ) 70 | Do ##class(TestCoverage.Manager).RunTest(,,.tCoverageParams) 71 | ``` 72 | 73 | The first two arguments to `TestCoverage.Manager:RunTest` are the same as `%UnitTest.Manager`. 74 | 75 | At the selected level of granularity (before all tests or a test suite/case/method is run), there will be a search for a file named "coverage.list" within the directory for the test suite and parent directories, stopping at the first such file found. This file may contain a list of classes, packages, and routines for which code coverage will be measured. For .MAC routines only (not classes/packages), the coverage list also supports the * wildcard. It is also possible to exclude classes/packages by prefixing the line with "-". For example, to track coverage for all classes in the `MyApplication` package (except those in the `MyApplication.UI` subpackage), and all routines with names starting with "MyApplication": 76 | 77 | ``` 78 | // Include all application code 79 | MyApplication.PKG 80 | MyApplication*.MAC 81 | 82 | // Exclude Zen Pages 83 | -MyApplication.UI.PKG 84 | ``` 85 | 86 | As an alternative approach, with unit test classes that have already been loaded and compiled (and which will not be deleted after running tests) and a known list of classes and routines for which code coverage should be collected, use: 87 | 88 | ``` 89 | Do ##class(TestCoverage.Manager).RunAllTests(tPackage,tLogFile,tCoverageClasses,tCoverageRoutines,tCoverageLevel,.tLogIndex,tSourceNamespace,tProcessIDs,tTiming) 90 | ``` 91 | 92 | Where: 93 | 94 | * `tPackage` has the top-level package containing all the unit test classes to run. These must already be loaded. 95 | * `tLogFile` (optional) may specify a file to log all output to. 96 | * `tCoverageClasses` (optional) has a $ListBuild list of class names within which to track code coverage. By default, none are tracked. 97 | * `tCoverageRoutines` (optional) has a $ListBuild list of routine names within which to track code coverage. By default, none are tracked. 98 | * `tCoverageLevel` (optional) is 0 to track code coverage overall; 1 to track it per test suite (the default); 2 to track it per test class; 3 to track it per test method. 99 | * `tLogIndex` (optional) allows for aggregation of code coverage results across unit test runs. To use this, get it back as output from the first test run, then pass it to the next. 100 | * `tSourceNamespace` (optional) specifies the namespace in which classes were compiled, defaulting to the current namespace. This may be required to retrieve some metadata. 101 | * `tPIDList` (optional) has a $ListBuild list of process IDs to monitor. If this is empty, all processes are monitored. If this is $ListBuild("Interop") or "Interoperability", all interoperability processes and the current process are monitored. By default, only the current process is monitored. 102 | * `tTiming` (optional) is 1 to capture execution time data for monitored classes/routines as well, or 0 (the default) to not capture this data. 103 | * `tListenerManager` (optional) is an instance of TestCoverage.Listeners.ListenerManager that allows downstream applications to listen to the completion of unit test suites/cases/methods. It should use the AddListener method to populate with listeners that extend TestCoverage.Listeners.ListenerInterface. See [isc.perf.ui](https://github.com/intersystems/isc-perf-ui) for an example usage 104 | * `tPyModules` a $ListBuild list of Python module names the covered code uses that should be imported before the unit tests are run. This is for modules like `pandas` and `sklearn`, whose import sometimes breaks sys.settrace 105 | 106 | 107 | ### Running Tests with Coverage via IPM 108 | 109 | Running unit tests with test coverage measurement via IPM is much simpler. Given a package `mycompany.foo`, a coverage.list file within its [unit test resource(s)](https://github.com/intersystems/ipm/wiki/03.-IPM-Manifest-(Module.xml)#unittest-or-test), and TestCoverage installed, tests can be run with coverage with: 110 | 111 | ``` 112 | zpm "mycompany.foo test -only -DUnitTest.ManagerClass=TestCoverage.Manager" 113 | ``` 114 | 115 | Additional "userparam" keys can be passed in the zpm command prefixed with `-DUnitTest.UserParam.` - for example: 116 | 117 | ``` 118 | zpm "mycompany.foo test -only "_ 119 | "-verbose -DUnitTest.ManagerClass=TestCoverage.Manager -DUnitTest.JUnitOutput=/test-reports/junit.xml "_ 120 | "-DUnitTest.FailuresAreFatal=1 -DUnitTest.Manager=TestCoverage.Manager "_ 121 | "-DUnitTest.UserParam.CoverageReportClass=TestCoverage.Report.Cobertura.ReportGenerator "_ 122 | "-DUnitTest.UserParam.CoverageReportFile=/source/coverage.xml" 123 | ``` 124 | 125 | Note that it is best practice to put your unit tests in a separate directory from your source code, most commonly `/tests`. 126 | 127 | For more details and examples, see [this InterSystems Developer Community article series](https://community.intersystems.com/post/unit-tests-and-test-coverage-intersystems-package-manager). 128 | 129 | ### Viewing Results 130 | After running the tests, a URL is shown in the output at which you can view test coverage results. If the hostname/IP address in this URL is incorrect, you can fix it by changing the "WebServerName" setting in the management portal, at System Administration > Configuration > Additional Settings > Startup. 131 | 132 | ### Reporting on results in Cobertura format 133 | The `RunTest()` method reports back a log index in the "userparam" argument. This can be used to generate a report in the same format as Cobertura, a popular Java code coverage tool. For example: 134 | 135 | ``` 136 | Set userParams("CoverageDetail") = 0 137 | Do ##class(TestCoverage.Manager).RunTest(,"/nodelete",.userParams) 138 | Set reportFile = "C:\Temp\Reports\"_tUserParams("LogIndex")_"\coverage.xml" 139 | Do ##class(TestCoverage.Report.Cobertura.ReportGenerator).GenerateReport(userParams("LogIndex"),reportFile) 140 | ``` 141 | 142 | This exports both the coverage results themselves and the associated source code (in UDL format) for correlation/display, and has been verified with [the Cobertura plugin for Jenkins](https://wiki.jenkins.io/display/JENKINS/Cobertura+Plugin). 143 | 144 | ## Support 145 | 146 | If you find a bug or would like to request an enhancement, [report an issue](https://github.com/intersystems/TestCoverage/issues/new). If you have a question, post it on the [InterSystems Developer Community](https://community.intersystems.com/) - consider using the "Testing" or "Continuous Integration" tags as appropriate. 147 | 148 | ## Contributing 149 | 150 | Please read [contributing](CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us. 151 | 152 | ## Versioning 153 | 154 | We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/intersystems/TestCoverage/tags). 155 | 156 | ## Authors 157 | 158 | * **Tim Leavitt** - *Initial implementation* - [timleavitt](http://github.com/timleavitt) / [isc-tleavitt](http://github.com/isc-tleavitt) 159 | * **Chris Ge** - Embedded Python support and other improvements - [isc-cge](http://github.com/isc-cge) 160 | 161 | See also the list of [contributors](https://github.com/intersystems/TestCoverage/contributors) who participated in this project. 162 | 163 | ## License 164 | 165 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 166 | -------------------------------------------------------------------------------- /cls/TestCoverage/Data/Aggregate/Base.cls: -------------------------------------------------------------------------------- 1 | Class TestCoverage.Data.Aggregate.Base Extends %Persistent [ Abstract, NoExtent ] 2 | { 3 | 4 | Property ExecutableLines As %Integer [ Required ]; 5 | 6 | Property CoveredLines As %Integer [ Required ]; 7 | 8 | Property ExecutableMethods As %Integer [ Required ]; 9 | 10 | Property CoveredMethods As %Integer [ Required ]; 11 | 12 | Property RtnLine As %Integer [ InitialExpression = 0 ]; 13 | 14 | Property Time As TestCoverage.DataType.Timing [ InitialExpression = 0, SqlFieldName = _TIME ]; 15 | 16 | Property TotalTime As TestCoverage.DataType.Timing [ InitialExpression = 0 ]; 17 | 18 | } 19 | 20 | -------------------------------------------------------------------------------- /cls/TestCoverage/Data/Aggregate/ByCodeUnit.cls: -------------------------------------------------------------------------------- 1 | Class TestCoverage.Data.Aggregate.ByCodeUnit Extends TestCoverage.Data.Aggregate.Base 2 | { 3 | 4 | Index RunCodeUnit On (Run, CodeUnit) [ Unique ]; 5 | 6 | Property Run As TestCoverage.Data.Run [ Required ]; 7 | 8 | ForeignKey RunFK(Run) References TestCoverage.Data.Run() [ OnDelete = cascade ]; 9 | 10 | Property CodeUnit As TestCoverage.Data.CodeUnit [ Required ]; 11 | 12 | ForeignKey CodeUnitFK(CodeUnit) References TestCoverage.Data.CodeUnit(Hash) [ OnDelete = cascade ]; 13 | 14 | Storage Default 15 | { 16 | 17 | 18 | %%CLASSNAME 19 | 20 | 21 | ExecutableLines 22 | 23 | 24 | CoveredLines 25 | 26 | 27 | ExecutableMethods 28 | 29 | 30 | CoveredMethods 31 | 32 | 33 | RtnLine 34 | 35 | 36 | Time 37 | 38 | 39 | TotalTime 40 | 41 | 42 | Run 43 | 44 | 45 | CodeUnit 46 | 47 | 48 | ^TestCoverage.Data.Agg.ByCUD 49 | ByCodeUnitDefaultData 50 | ^TestCoverage.Data.Agg.ByCUD 51 | ^TestCoverage.Data.Agg.ByCUI 52 | ^TestCoverage.Data.Agg.ByCUS 53 | %Storage.Persistent 54 | } 55 | 56 | } 57 | 58 | -------------------------------------------------------------------------------- /cls/TestCoverage/Data/Aggregate/ByRun.cls: -------------------------------------------------------------------------------- 1 | Class TestCoverage.Data.Aggregate.ByRun Extends TestCoverage.Data.Aggregate.Base 2 | { 3 | 4 | Index Run On Run [ Unique ]; 5 | 6 | Property Run As TestCoverage.Data.Run [ Required ]; 7 | 8 | ForeignKey RunFK(Run) References TestCoverage.Data.Run() [ OnDelete = cascade ]; 9 | 10 | Storage Default 11 | { 12 | 13 | 14 | %%CLASSNAME 15 | 16 | 17 | ExecutableLines 18 | 19 | 20 | CoveredLines 21 | 22 | 23 | ExecutableMethods 24 | 25 | 26 | CoveredMethods 27 | 28 | 29 | RtnLine 30 | 31 | 32 | Time 33 | 34 | 35 | TotalTime 36 | 37 | 38 | Run 39 | 40 | 41 | ^TestCoverage.Data.Agg.ByRunD 42 | ByRunDefaultData 43 | ^TestCoverage.Data.Agg.ByRunD 44 | ^TestCoverage.Data.Agg.ByRunI 45 | ^TestCoverage.Data.Agg.ByRunS 46 | %Storage.Persistent 47 | } 48 | 49 | } 50 | 51 | -------------------------------------------------------------------------------- /cls/TestCoverage/Data/CodeSubUnit.cls: -------------------------------------------------------------------------------- 1 | Class TestCoverage.Data.CodeSubUnit Extends %Persistent [ Abstract ] 2 | { 3 | 4 | Relationship Parent As TestCoverage.Data.CodeUnit [ Cardinality = parent, Inverse = SubUnits ]; 5 | 6 | /// Bitstring representing which lines are part of this section (method, branch, etc.) of the code 7 | Property Mask As TestCoverage.DataType.Bitstring; 8 | 9 | /// Cyclomatic complexity of the code subunit 10 | Property Complexity As %Integer [ InitialExpression = 1 ]; 11 | 12 | /// 1 if it's a python class method, 0 if not 13 | Property IsPythonMethod As %Boolean; 14 | 15 | Method UpdateComplexity() As %Status 16 | { 17 | Quit $$$OK 18 | } 19 | 20 | Storage Default 21 | { 22 | 23 | 24 | %%CLASSNAME 25 | 26 | 27 | Mask 28 | 29 | 30 | Complexity 31 | 32 | 33 | IsPythonMethod 34 | 35 | 36 | {%%PARENT}("SubUnits") 37 | CodeSubUnitDefaultData 38 | ^TestCoverage.Data.CodeUnitC("SubUnits") 39 | ^TestCoverage.Data.CodeSubUnitI 40 | ^TestCoverage.Data.CodeSubUnitS 41 | %Storage.Persistent 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /cls/TestCoverage/Data/CodeSubUnit/Method.cls: -------------------------------------------------------------------------------- 1 | Class TestCoverage.Data.CodeSubUnit.Method Extends TestCoverage.Data.CodeSubUnit 2 | { 3 | 4 | Property Name As %Dictionary.CacheIdentifier [ Required ]; 5 | 6 | Property DisplaySignature As %String(MAXLEN = ""); 7 | 8 | Method UpdateComplexity() As %Status 9 | { 10 | Set tSC = $$$OK 11 | Try { 12 | // Get int lines mapped to this method's mask. 13 | // As an optimization, find start/end of mask 14 | Set tMaskStart = $BitFind(..Mask,1,0,1) 15 | Set tMaskEnd = $BitFind(..Mask,1,0,-1) 16 | 17 | // Get lines mapped to this method's mask 18 | // Get unique by map.ToLine to avoid issues with mapping of embedded SQL 19 | // (all lines of the generated query map back to the class line defining it) 20 | Set tResult = ##class(%SQL.Statement).%ExecDirect(, 21 | "select distinct by (map.ToLine) element_key as Line, Lines as Code from TestCoverage_Data.CodeUnit_Lines intcode "_ 22 | "join TestCoverage_Data.CodeUnitMap map "_ 23 | " on map.FromHash = intcode.CodeUnit "_ 24 | " and map.FromLine = intcode.element_key "_ 25 | "where intcode.CodeUnit->Type = 'INT' "_ 26 | " and map.ToHash = ? "_ 27 | " and map.ToLine >= ? and map.ToLine <= ? "_ 28 | "order by map.FromLine",..Parent.Hash,tMaskStart,tMaskEnd) 29 | If (tResult.%SQLCODE < 0) { 30 | Throw ##class(%Exception.SQL).CreateFromSQLCODE(tResult.%SQLCODE,tResult.%Message) 31 | } 32 | Set tCodeStream = ##class(%Stream.TmpCharacter).%New() 33 | While tResult.%Next(.tSC) { 34 | $$$ThrowOnError(tSC) 35 | $$$ThrowOnError(tCodeStream.WriteLine(tResult.%Get("Code"))) 36 | } 37 | $$$ThrowOnError(tSC) 38 | 39 | Set ..Complexity = ##class(TestCoverage.Utils.ComplexityParser).%New(tCodeStream).GetComplexity() 40 | $$$ThrowOnError(..%Save(0)) 41 | } Catch e { 42 | Set tSC = e.AsStatus() 43 | } 44 | Quit tSC 45 | } 46 | 47 | Storage Default 48 | { 49 | 50 | "Method" 51 | 52 | Name 53 | 54 | 55 | DisplaySignature 56 | 57 | 58 | MethodDefaultData 59 | %Storage.Persistent 60 | } 61 | 62 | } 63 | 64 | -------------------------------------------------------------------------------- /cls/TestCoverage/Data/CodeUnit.cls: -------------------------------------------------------------------------------- 1 | Include %syGluedef 2 | 3 | /// Represents a single unit of code (class, routine), which may be generated or user-written. 4 | /// TODO: Create subclasses representing classes, routines, and intermediate code. 5 | Class TestCoverage.Data.CodeUnit Extends %Persistent 6 | { 7 | 8 | /// Hash had better be unique... 9 | Index Hash On Hash [ IdKey ]; 10 | 11 | /// Uniquely identifies a unit of code by name, type, and hash. 12 | Index NameTypeHash On (Name, Type, Hash) [ Data = ExecutableLines, Unique ]; 13 | 14 | /// Name of the code unit 15 | Property Name As %String(MAXLEN = 255) [ Required ]; 16 | 17 | /// Type (2 or 3-letter extension) of the code unit 18 | Property Type As TestCoverage.DataType.RoutineType [ Required ]; 19 | 20 | /// Hash of the code unit; for methods for determining this, see GetCurrentHash 21 | Property Hash As %String [ Required ]; 22 | 23 | /// Lines (with position in list corresponding to line number) 24 | Property Lines As list Of %String(MAXLEN = "", STORAGEDEFAULT = "array"); 25 | 26 | /// Bitstring of (line # is executable) 27 | Property ExecutableLines As TestCoverage.DataType.Bitstring; 28 | 29 | /// For classes, map of method names in the code to their associated line numbers 30 | /// For routines, map of labels to associated line numbers 31 | /// For python, map of method names to associated starting line number 32 | Property MethodMap As array Of %Integer; 33 | 34 | /// Only for python: map of method names to associated ending line number of the method 35 | Property MethodEndMap As array Of %Integer; 36 | 37 | /// For classes, map of line numbers in code to associated method names 38 | /// For routines, map of labels to associated line numbers 39 | Property LineToMethodMap As array Of %Dictionary.CacheIdentifier [ Private ]; 40 | 41 | /// For each line, whether or not it belongs to a python method, only populated for .cls CodeUnits 42 | Property LineIsPython As array Of %Boolean; 43 | 44 | /// Set to true if this class/routine is generated 45 | Property Generated As %Boolean [ InitialExpression = 0 ]; 46 | 47 | /// If the CodeUnit has changed since we last updated it, used to see if we need to call UpdateComplexity 48 | Property OutdatedComplexity As %Boolean [ InitialExpression = 1 ]; 49 | 50 | /// Methods, branches, etc. within this unit of code. 51 | Relationship SubUnits As TestCoverage.Data.CodeSubUnit [ Cardinality = children, Inverse = Parent ]; 52 | 53 | /// Gets the current instance of a unit of code by its internal name (e.g., SomePackage.ClassName.CLS or SomePackage.ClassName.1.INT) 54 | ClassMethod GetCurrentByName(pInternalName As %String, pSourceNamespace As %String = {$Namespace}, Output pCodeUnit As TestCoverage.Data.CodeUnit, ByRef pCache) As %Status 55 | { 56 | Set tSC = $$$OK 57 | Set tOriginalNamespace = $Namespace 58 | Set tInitTLevel = $TLevel 59 | Try { 60 | Set pCodeUnit = $$$NULLOREF 61 | 62 | New $Namespace 63 | Set $Namespace = pSourceNamespace 64 | 65 | // Figure out the hash. 66 | Set pInternalName = ##class(%Studio.SourceControl.Interface).normalizeName(pInternalName) 67 | Set tName = $Piece(pInternalName,".",1,*-1) 68 | Set tType = $Piece(pInternalName,".",*) 69 | 70 | TSTART 71 | // GetCurrentHash may store the current version of the routine, 72 | // so start the transaction before calling it. 73 | $$$ThrowOnError(..GetCurrentHash(tName,tType,.tHash,.tCodeArray,.pCache)) 74 | 75 | // Ensure mappings from the specified name/type/hash are up to date. 76 | #dim tMapToResult As %SQL.StatementResult 77 | Set tMapToResult = ##class(%SQL.Statement).%ExecDirect(, 78 | "select distinct ToHash from TestCoverage_Data.CodeUnitMap where FromHash = ?",tHash) 79 | If (tMapToResult.%SQLCODE < 0) { 80 | Throw ##class(%Exception.SQL).CreateFromSQLCODE(tMapToResult.%SQLCODE,tMapToResult.%Message) 81 | } 82 | Set tNeedsUpdate = 0 83 | While tMapToResult.%Next(.tSC) { 84 | $$$ThrowOnError(tSC) 85 | Set tKnownHash = tMapToResult.%GetData(1) 86 | Set tMapToUnit = ..HashOpen(tKnownHash,,.tSC) 87 | $$$ThrowOnError(tSC) 88 | do ..GetCurrentHash(tMapToUnit.Name, tMapToUnit.Type, .tUpdatedHash, , ) 89 | If (tUpdatedHash '= tKnownHash) { 90 | //Clear out old data and flag the need for an update. 91 | Set tNeedsUpdate = 1 92 | If $IsObject($Get(tMapToUnit)) { 93 | set tMapToUnit.OutdatedComplexity = 1 94 | } 95 | &sql(delete from TestCoverage_Data.CodeUnitMap where ToHash = :tKnownHash) 96 | If (SQLCODE < 0) { 97 | Throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) 98 | } 99 | } 100 | } 101 | 102 | Set $Namespace = tOriginalNamespace 103 | If ..NameTypeHashExists(tName,tType,tHash,.tID) { 104 | Set pCodeUnit = ..%OpenId(tID,,.tSC) 105 | $$$ThrowOnError(tSC) 106 | If tNeedsUpdate { 107 | $$$ThrowOnError(pCodeUnit.UpdateSourceMap(pSourceNamespace,.pCache)) 108 | } 109 | TCOMMIT 110 | Quit 111 | } 112 | 113 | Set $Namespace = pSourceNamespace 114 | 115 | If (tType = "CLS") { 116 | Do ##class(TestCoverage.Utils).GetClassLineExecutableFlags(tName,.tCodeArray,.tExecutableFlags) 117 | } ElseIf ((tType = "INT") || (tType = "MAC")) { 118 | Do ##class(TestCoverage.Utils).GetRoutineLineExecutableFlags(.tCodeArray,.tExecutableFlags) 119 | } ElseIf (tType="PY") { 120 | Do ##class(TestCoverage.Utils).CodeArrayToList(.tCodeArray, .pDocumentText) 121 | Set tExecutableFlagsPyList = ##class(TestCoverage.Utils).GetPythonLineExecutableFlags(pDocumentText) 122 | Kill tExecutableFlags 123 | for i=1:1:tExecutableFlagsPyList."__len__"()-1 { 124 | set tExecutableFlags(i) = tExecutableFlagsPyList."__getitem__"(i) 125 | } 126 | } 127 | Else { 128 | return $$$ERROR($$$GeneralError,"File type not supported") 129 | } 130 | 131 | Set $Namespace = tOriginalNamespace 132 | Set pCodeUnit = ..%New() 133 | Set pCodeUnit.Name = tName 134 | Set pCodeUnit.Type = tType 135 | Set pCodeUnit.Hash = tHash 136 | 137 | 138 | 139 | If (tType = "CLS") { 140 | Set pCodeUnit.Generated = ($$$comClassKeyGet(tName,$$$cCLASSgeneratedby) '= "") 141 | 142 | } 143 | 144 | 145 | If (tType = "PY") { 146 | // fill in the Lines property of this CodeUnit 147 | set tPointer = 0 148 | While $ListNext(pDocumentText, tPointer, tCurLine) { 149 | do pCodeUnit.Lines.Insert(tCurLine) 150 | } 151 | 152 | do pCodeUnit.Lines.Insert("") 153 | 154 | // Filling in the MethodMap and LineToMethodMap properties 155 | Set tMethodInfo = ##class(TestCoverage.Utils).GetPythonMethodMapping(pDocumentText) 156 | // tMethodInfo is a python tuple of (line to method info, method map info) 157 | 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 158 | Set tMethodMapInfo = tMethodInfo."__getitem__"(1) // a python builtins dict with key = method name, value = the line number of its definition 159 | for i=1:1:$listlength(pDocumentText) { 160 | Set tMethod = tLineToMethodInfo."__getitem__"(i) 161 | Do pCodeUnit.LineToMethodMap.SetAt(tMethod,i) 162 | } 163 | Set iterator = tMethodMapInfo."__iter__"() 164 | for i=1:1:tMethodMapInfo."__len__"() { // when iterator passes the last element, it throws a Python exception: StopIteration error, so I think the current pattern is preferable over that 165 | Set tMethod = iterator."__next__"() 166 | Set tStartEnd = tMethodMapInfo."__getitem__"(tMethod) 167 | Set tStartLine = tStartEnd."__getitem__"(0) 168 | Set tEndLine = tStartEnd."__getitem__"(1) 169 | Do pCodeUnit.MethodMap.SetAt(tStartLine,tMethod) 170 | Set tExecutableFlags(tStartLine) = 0 171 | Do pCodeUnit.MethodEndMap.SetAt(tEndLine, tMethod) 172 | } 173 | } 174 | Else { 175 | Set tMethod = "" 176 | Set tMethodSignature = "" 177 | Set tMethodMask = "" 178 | For tLineNumber=1:1:$Get(tCodeArray,0) { 179 | Set tLine = tCodeArray(tLineNumber) 180 | Do pCodeUnit.Lines.Insert(tLine) 181 | 182 | If (tType = "CLS") { 183 | // initialize each line to not python (we'll update this later) 184 | Do pCodeUnit.LineIsPython.SetAt(0, tLineNumber) 185 | 186 | // Extract line offset of methods in classes 187 | Set tStart = $Piece(tLine," ") 188 | If (tStart = "ClassMethod") || (tStart = "Method") { 189 | Set tMethod = $Piece($Piece(tLine,"(")," ",2) 190 | Set tMethodSignature = tLine 191 | Do pCodeUnit.MethodMap.SetAt(tLineNumber,tMethod) 192 | Do pCodeUnit.LineToMethodMap.SetAt(tMethod,tLineNumber) 193 | } ElseIf ($Extract(tStart) = "{") { 194 | // Ignore the opening bracket for a method. 195 | } ElseIf ($Extract(tStart) = "}") && (tMethod '= "") { 196 | // End of method. Add method subunit to class. 197 | Set tSubUnit = ##class(TestCoverage.Data.CodeSubUnit.Method).%New() 198 | Set tSubUnit.Name = tMethod 199 | Set tSubUnit.DisplaySignature = tMethodSignature 200 | Set tSubUnit.Mask = tMethodMask 201 | set NormalizedSignature = $zconvert($zstrip(tMethodSignature, "*W"), "l") 202 | set tSubUnit.IsPythonMethod = (NormalizedSignature [ "[language=python]") 203 | Do pCodeUnit.SubUnits.Insert(tSubUnit) 204 | Set tMethod = "" 205 | Set tMethodSignature = "" 206 | Set tMethodMask = "" 207 | } ElseIf (tMethod '= "") { 208 | Set $Bit(tMethodMask,tLineNumber) = 1 209 | } 210 | } 211 | Else { 212 | // Extract line offset of labels in routines 213 | If ($ZStrip($Extract(tLine),"*PWC") '= "") { 214 | Set tLabel = $Piece($Piece(tLine," "),"(") 215 | Do pCodeUnit.MethodMap.SetAt(tLineNumber,tLabel) 216 | Do pCodeUnit.LineToMethodMap.SetAt(tLabel,tLineNumber) 217 | } 218 | } 219 | } 220 | } 221 | 222 | Set tBitString = "" 223 | For tLine=1:1:$Get(tCodeArray,0) { 224 | Set $Bit(tBitString,tLine) = $Get(tExecutableFlags(tLine),0) 225 | } 226 | Set pCodeUnit.ExecutableLines = tBitString 227 | 228 | 229 | Set tSC = pCodeUnit.%Save() 230 | If $$$ISERR(tSC) && $System.Status.Equals(tSC,$$$ERRORCODE($$$IDKeyNotUnique)) { 231 | // Some other process beat us to it. 232 | Set tSC = $$$OK 233 | Set pCodeUnit = ..%OpenId(pCodeUnit.Hash,,.tSC) 234 | Quit 235 | } 236 | // For non-class (e.g., .MAC/.INT) code, it's possible that something else generated it, 237 | // so update the mappings between generated and the thing that generated it. 238 | If (tType '= "CLS") { 239 | $$$ThrowOnError(pCodeUnit.UpdateSourceMap(pSourceNamespace,.pCache)) 240 | } 241 | TCOMMIT 242 | } Catch e { 243 | Set pCodeUnit = $$$NULLOREF 244 | Set tSC = e.AsStatus() 245 | } 246 | While ($TLevel > tInitTLevel) { 247 | TROLLBACK 1 248 | } 249 | Quit tSC 250 | } 251 | 252 | /// Fill in the LineIsPython property of .cls files 253 | Method UpdatePythonLines(pName As %String, ByRef pPyCodeUnit) As %Status 254 | { 255 | Set tSC = $$$OK 256 | Set tOriginalNamespace = $Namespace 257 | Set tInitTLevel = $TLevel 258 | 259 | Try { 260 | TSTART 261 | 262 | If (##class(TestCoverage.Manager).HasPython(pName)) { 263 | 264 | Set tFromHash = pPyCodeUnit.Hash 265 | Set tToHash = ..Hash 266 | set sql = "SELECT map.ToLine FROM TestCoverage_Data.CodeUnitMap map " _ 267 | "JOIN TestCoverage_Data.CodeUnit fromCodeUnit " _ 268 | "ON fromCodeUnit.Hash = map.FromHash " _ 269 | "WHERE map.FromHash = ? " _ 270 | "AND map.ToHash = ? " 271 | set resultSet = ##class(%SQL.Statement).%ExecDirect(, sql, tFromHash, tToHash) 272 | If (resultSet.%SQLCODE < 0) { 273 | Throw ##class(%Exception.SQL).CreateFromSQLCODE(resultSet.%SQLCODE, resultSet.%Message) 274 | } 275 | while resultSet.%Next(.tSC) { 276 | $$$ThrowOnError(tSC) 277 | Set hToLine = resultSet.%GetData(1) 278 | do ..LineIsPython.SetAt(1, hToLine) 279 | } 280 | If (resultSet.%SQLCODE < 0) { 281 | Throw ##class(%Exception.SQL).CreateFromSQLCODE(resultSet.%SQLCODE, resultSet.%Message) 282 | } 283 | } 284 | Set tSC = ..%Save() 285 | $$$ThrowOnError(tSC) 286 | 287 | TCOMMIT 288 | } Catch e { 289 | Set pCodeUnit = $$$NULLOREF 290 | Set tSC = e.AsStatus() 291 | } 292 | While ($TLevel > tInitTLevel) { 293 | TROLLBACK 1 294 | } 295 | Quit tSC 296 | } 297 | 298 | /// Get the executable lines of code in python over to the .cls CodeUnit 299 | Method UpdatePyExecutableLines(pName As %String, ByRef pPyCodeUnit) As %Status 300 | { 301 | Set tSC = $$$OK 302 | Set tOriginalNamespace = $Namespace 303 | Set tInitTLevel = $TLevel 304 | Try { 305 | TSTART 306 | 307 | Set tBitString = "" 308 | If (##class(TestCoverage.Manager).HasPython(pName)) { 309 | 310 | Set tFromHash = pPyCodeUnit.Hash 311 | Set tToHash = ..Hash 312 | set sql = "SELECT map.ToLine FROM TestCoverage_Data.CodeUnitMap map " _ 313 | "JOIN TestCoverage_Data.CodeUnit fromCodeUnit " _ 314 | "ON fromCodeUnit.Hash = map.FromHash " _ 315 | "WHERE map.FromHash = ? " _ 316 | "AND map.ToHash = ? " _ 317 | "AND TestCoverage.BIT_VALUE(fromCodeUnit.ExecutableLines,map.FromLine) <> 0" 318 | 319 | set resultSet = ##class(%SQL.Statement).%ExecDirect(, sql, tFromHash, tToHash) 320 | If (resultSet.%SQLCODE < 0) { 321 | Throw ##class(%Exception.SQL).CreateFromSQLCODE(resultSet.%SQLCODE, resultSet.%Message) 322 | } 323 | while resultSet.%Next(.tSC) { 324 | $$$ThrowOnError(tSC) 325 | Set hToLine = resultSet.%GetData(1) 326 | Set $Bit(tBitString, hToLine) = 1 327 | } 328 | If (resultSet.%SQLCODE < 0) { 329 | Throw ##class(%Exception.SQL).CreateFromSQLCODE(resultSet.%SQLCODE, resultSet.%Message) 330 | } 331 | } 332 | Set ..ExecutableLines = $BITLOGIC(..ExecutableLines | tBitString) 333 | Set tSC = ..%Save() 334 | $$$ThrowOnError(tSC) 335 | 336 | TCOMMIT 337 | } Catch e { 338 | Set pCodeUnit = $$$NULLOREF 339 | Set tSC = e.AsStatus() 340 | } 341 | While ($TLevel > tInitTLevel) { 342 | TROLLBACK 1 343 | } 344 | Quit tSC 345 | } 346 | 347 | Method UpdateSourceMap(pSourceNamespace As %String, ByRef pCache) As %Status 348 | { 349 | Set tSC = $$$OK 350 | Try { 351 | 352 | // First, build local array (tMap) of all maps from the .INT file to other files. 353 | If (..Type = "INT") { 354 | For tLineNumber=1:1:..Lines.Count() { 355 | Set tLine = ..Lines.GetAt(tLineNumber) 356 | Set tSC = ##class(%Studio.Debugger).SourceLine(..Name, tLineNumber, 1, tLineNumber, $Length(tLine), pSourceNamespace, .tMap) 357 | $$$ThrowOnError(tSC) 358 | 359 | If $Data(tMap("CLS",1),tData1) && $Data(tMap("CLS",2),tData2) { 360 | Set $ListBuild(tClass,tMethod,tLine1) = tData1 361 | Set tLine2 = $List(tData2,3) 362 | 363 | // Skip stub classes 364 | If $$$defClassKeyGet(tClass,$$$cCLASShidden) && ($$$defClassKeyGet(tClass,$$$cCLASSdeployed) = 2) { 365 | Continue 366 | } 367 | 368 | // Generated method lines are not correctly mapped to the generator method's lines. 369 | // Therefore, skip mapping generator methods directly from INT to CLS. 370 | // Instead, these are mapped from INT to MAC and MAC to CLS (transitively). 371 | If '$Data(tCodeModeCache(tClass,tMethod),tCodeMode) { 372 | Set tCodeMode = $$$comMemberKeyGet(tClass,$$$cCLASSmethod,tMethod,$$$cMETHcodemode) 373 | Set tCodeModeCache(tClass,tMethod) = tCodeMode 374 | } 375 | If (tCodeMode = $$$cMETHCODEMODEGENERATOR) || (tCodeMode = $$$cMETHCODEMODEOBJECTGENERATOR) { 376 | Continue 377 | } 378 | 379 | Set tFullMap(tLineNumber) = $ListBuild("CLS",tClass,tMethod,tLine1,tLine2) 380 | Set tSourceUnits(tClass_".CLS") = "" 381 | } ElseIf $Data(tMap("MAC",1),tData1) && $Data(tMap("MAC",2),tData2) { 382 | Set tRoutine = $ListGet(tData1) 383 | Set tLine1 = $ListGet(tData1,3) 384 | Set tLine2 = $ListGet(tData2,3) 385 | Set tFullMap(tLineNumber) = $ListBuild("MAC",tRoutine,"",tLine1,tLine2) 386 | Set tSourceUnits(tRoutine_".MAC") = "" 387 | } 388 | } 389 | } 390 | If (..Type = "PY") { 391 | 392 | set tClass = ..Name 393 | Set tSourceUnits(tClass_".CLS") = "" 394 | // we'll need the MethodMap from the .CLS CodeUnit to figure out the line mappings 395 | $$$ThrowOnError(..GetCurrentByName(tClass _ ".CLS", pSourceNamespace, .pCLSCodeUnit, .pCLSCache)) 396 | // we'll do the mappings from the .py to the .cls direction, so that we don't iterate over objectscript lines 397 | Set tMethod = "" 398 | Do ..MethodMap.GetNext(.tMethod) 399 | while (tMethod '= "") 400 | { 401 | // 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 402 | // 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 403 | Set tCLSMethodNum = pCLSCodeUnit.MethodMap.GetAt(tMethod) 404 | Set tMethodStart = ..MethodMap.GetAt(tMethod) 405 | Set tMethodEnd = ..MethodEndMap.GetAt(tMethod) 406 | Set tMethodName = tMethod 407 | 408 | // tFullMap(py/int Line Number, absolute) = $lb("CLS", class name, method name, CLS/mac start line (relative to method), CLS/mac end line (relative to method)) 409 | Set tFullMap(tMethodStart) = $lb("CLS", tClass,tMethodName, -1, -1) ; -1 because the class 410 | ; definition doesn't have the +1 offset from the { 411 | 412 | // there's a strange edge case where if the python method in the .CLS file starts with spaces, that's not captured in the Python compiled code 413 | // so we have to find how many empty lines there are at the beginning 414 | Set tEmptyLines = 0 415 | while ($zstrip( pCLSCodeUnit.Lines.GetAt(tCLSMethodNum + 1 + tEmptyLines + 1), "<>W") = "") { 416 | Set tEmptyLines = tEmptyLines + 1 417 | } 418 | 419 | For i = tMethodStart+1:1:tMethodEnd { 420 | Set tClassLineNum = i-tMethodStart 421 | Set tFullMap(i) = $lb("CLS", tClass,tMethodName, tClassLineNum+tEmptyLines, tClassLineNum+tEmptyLines) 422 | 423 | // extra check to make sure that the lines we're mapping between are the same as expected 424 | Set tClassLineCode = $zstrip(pCLSCodeUnit.Lines.GetAt(tCLSMethodNum + 1 + tEmptyLines + tClassLineNum), "<>W") 425 | Set tPyLineCode = $zstrip(..Lines.GetAt(i), "<>W") 426 | if (tPyLineCode '= tClassLineCode) { 427 | $$$ThrowStatus($$$ERROR($$$GeneralError,"Compiled .py code doesn't match .CLS python code at line " _ $char(10,13) _ tPyLineCode)) 428 | } 429 | } 430 | Do ..MethodMap.GetNext(.tMethod) 431 | } 432 | } 433 | 434 | // If we are a generator .INT file, ensure that we have source for the original class populated. 435 | // In such files, the second line looks like (for example): 436 | // ;(C)InterSystems, method generator for class %ZHSLIB.PackageManager.Developer.AbstractSettings. Do NOT edit. 437 | Set tIsGenerator = 0 438 | Set tCommentLine = ..Lines.GetAt(2) 439 | If (tCommentLine [ "method generator for class ") { 440 | Set tClass = $Piece($Piece(tCommentLine,"method generator for class ",2),". ") 441 | Set tIsGenerator = 1 442 | Set tSourceUnits(tClass_".CLS") = "" 443 | 444 | If (..Type = "MAC") { 445 | Set ..Generated = 1 446 | $$$ThrowOnError(..%Save()) 447 | Set tMethod = "" 448 | Set tLastOffset = 0 449 | For tLineNumber=1:1:..Lines.Count() { 450 | Set tLine = ..Lines.GetAt(tLineNumber) 451 | If ($Piece(tLine," ") = "#classmethod") { 452 | Set tMethod = $Piece(tLine," ",2) 453 | Set tMethodGenerators(tLineNumber) = tMethod 454 | Set tLastOffset = tLineNumber 455 | } ElseIf (tLine = "#generator") { 456 | Set tMethod = "" 457 | } ElseIf (tLineNumber - tLastOffset > 1) && (tMethod '= "") { 458 | Set tMethodOffset = tLineNumber-tLastOffset-1 459 | Set tFullMap(tLineNumber) = $ListBuild("CLS",tClass,tMethod,tMethodOffset,tMethodOffset) 460 | } 461 | } 462 | } 463 | } 464 | 465 | // Ensure we have up-to-date code stashed for originating code (MAC/CLS) 466 | Set tSourceKey = "" 467 | For { 468 | Set tSourceKey = $Order(tSourceUnits(tSourceKey)) 469 | If (tSourceKey = "") { 470 | Quit 471 | } 472 | 473 | Set tSC = ..GetCurrentByName(tSourceKey,pSourceNamespace,.tCodeUnit,.pCache) 474 | $$$ThrowOnError(tSC) 475 | Set tCodeUnits(tCodeUnit.Type,tCodeUnit.Name) = tCodeUnit 476 | } 477 | 478 | // Create CodeUnitMap data based on .INT / .py ->.CLS mapping. 479 | Set tFromHash = ..Hash 480 | Set tLineNumber = "" 481 | For { 482 | Set tLineNumber = $Order(tFullMap(tLineNumber),1,tData) 483 | If (tLineNumber = "") { 484 | Quit 485 | } 486 | 487 | Set tType = $ListGet(tData,1) 488 | Set tName = $ListGet(tData,2) 489 | Set tMethod = $ListGet(tData,3) 490 | Set tLine1 = $ListGet(tData,4) 491 | Set tLine2 = $ListGet(tData,5) 492 | 493 | Set tToHash = tCodeUnits(tType,tName).Hash 494 | If (tType = "CLS") { 495 | Set tOffset = 1 + tCodeUnits(tType,tName).MethodMap.GetAt(tMethod) 496 | } ElseIf (tType = "MAC") { 497 | Set tOffset = 0 498 | } 499 | 500 | $$$ThrowOnError(##class(TestCoverage.Data.CodeUnitMap).Create(tFromHash,tLineNumber,tToHash,tLine1 + tOffset,tLine2 + tOffset)) 501 | } 502 | 503 | // Fill in missing details from .CLS->.INT debug mapping. 504 | // In some cases .CLS->.INT is more accurate; it is possible to have a many-to-many relationship in line mappings. 505 | // Embedded SQL, in particular, seems to throw a wrench in the works. 506 | If (..Type = "INT") { 507 | #dim tClassCodeUnit As TestCoverage.Data.CodeUnit 508 | Set tClass = $Order(tCodeUnits("CLS",""),1,tClassCodeUnit) 509 | If $IsObject($Get(tClassCodeUnit)) { 510 | Set tLine = 0 511 | For { 512 | Set tLine = $BitFind(tClassCodeUnit.ExecutableLines,1,tLine+1) 513 | If (tLine = 0) { 514 | Quit 515 | } 516 | 517 | // Find method offset of line 518 | Set tMethodOffset = tLine 519 | Set tMethodName = tClassCodeUnit.LineToMethodMap.GetPrevious(.tMethodOffset) 520 | If (tMethodName '= "") { 521 | If '$Data(tCodeModeCache(tClass,tMethodName),tCodeMode) { 522 | Set tCodeMode = $$$comMemberKeyGet(tClass,$$$cCLASSmethod,tMethodName,$$$cMETHcodemode) 523 | Set tCodeModeCache(tClass,tMethodName) = tCodeMode 524 | } 525 | If (tCodeMode = $$$cMETHCODEMODEGENERATOR) || (tCodeMode = $$$cMETHCODEMODEOBJECTGENERATOR) { 526 | Continue 527 | } 528 | Set tOffset = tLine - tMethodOffset - 1 529 | Set tSC = ##class(%Studio.Debugger).INTLine(tClass_".CLS",tMethodName,tOffset,.tIntName,.tIntLine,.tMissing,pSourceNamespace) 530 | $$$ThrowOnError(tSC) 531 | If 'tMissing && (tIntName = ..Name) { 532 | $$$ThrowOnError(##class(TestCoverage.Data.CodeUnitMap).Create(..Hash,tIntLine,tClassCodeUnit.Hash,tLine,tLine)) 533 | } 534 | } 535 | } 536 | } 537 | } 538 | 539 | // Update cyclomatic complexity for methods in the linked class if we don't already have the newest version 540 | Set tClass = $Order(tCodeUnits("CLS",""),1,tClassCodeUnit) 541 | If ($IsObject($Get(tClassCodeUnit)) && (tClassCodeUnit.OutdatedComplexity)){ 542 | set tClassCodeUnit.OutdatedComplexity = 0 543 | $$$ThrowOnError(tClassCodeUnit.UpdateComplexity()) 544 | } 545 | } Catch e { 546 | Set tSC = e.AsStatus() 547 | } 548 | Quit tSC 549 | } 550 | 551 | Method UpdateComplexity() As %Status 552 | { 553 | Set tSC = $$$OK 554 | Try { 555 | If (..Type '= "CLS") { 556 | Quit 557 | } 558 | 559 | // python methods 560 | If (##class(TestCoverage.Manager).HasPython(..Name)) { 561 | do ..GetCurrentHash(..Name, "PY", ,.tPyCodeArray, ) // need the source code for the python to pass into the method complexity calculator 562 | do ##class(TestCoverage.Utils).CodeArrayToList(.tPyCodeArray, .tDocumentText) 563 | set tDocumentText = tDocumentText _ $listbuild("") 564 | set tMethodComplexities = ..GetPythonComplexities(tDocumentText) 565 | } 566 | 567 | Set tKey = "" 568 | For { 569 | Set tSubUnit = ..SubUnits.GetNext(.tKey) 570 | If (tKey = "") { 571 | Quit 572 | } 573 | If (tSubUnit.IsPythonMethod) { 574 | set tSubUnit.Complexity = tMethodComplexities."__getitem__"(tSubUnit.Name) 575 | $$$ThrowOnError(tSubUnit.%Save(0)) 576 | } Else { 577 | $$$ThrowOnError(tSubUnit.UpdateComplexity()) 578 | } 579 | } 580 | 581 | 582 | $$$ThrowOnError(..%Save()) 583 | } Catch e { 584 | Set tSC = e.AsStatus() 585 | } 586 | Quit tSC 587 | } 588 | 589 | /// returns a python dict with (key, value) = (method name, complexity) for each python method 590 | ClassMethod GetPythonComplexities(pDocumentText) [ Language = python ] 591 | { 592 | from radon.complexity import cc_visit 593 | import iris 594 | source_lines = iris.cls('%SYS.Python').ToList(pDocumentText) 595 | source_code = "\n".join(source_lines) 596 | visitor = cc_visit(source_code) 597 | class_info = visitor[0] 598 | method_complexities = {} 599 | for method in class_info.methods: 600 | method_complexities[method.name] = method.complexity 601 | return method_complexities 602 | } 603 | 604 | Method GetMethodOffset(pAbsoluteLine As %Integer, Output pMethod As %String, Output pOffset As %Integer) 605 | { 606 | } 607 | 608 | ClassMethod GetCurrentHash(pName As %String, pType As %String, Output pHash As %String, Output pCodeArray As %String, Output pCache) As %Status 609 | { 610 | Set tSC = $$$OK 611 | Try { 612 | If '$Data(pCache(pName,pType),pHash) { 613 | If (pType = "CLS") { 614 | Set pHash = $$$comClassKeyGet(pName,$$$cCLASShash) 615 | If (pHash '= "") { 616 | // Get the code too. 617 | Set pHash = pHash_"|"_$zcrc(pName,7) // In case multiple class definitions are the same! 618 | $$$ThrowOnError(##class(%Compiler.UDL.TextServices).GetTextAsArray($Namespace,pName,.pCodeArray)) 619 | Set pCodeArray = $Order(pCodeArray(""),-1) //Set top level node to # of lines. 620 | } 621 | } ElseIf (pType = "MAC") { 622 | Merge pCodeArray = ^rMAC(pName,0) 623 | Merge tSizeHint = ^rMAC(pName,0,"SIZE") 624 | Set pCodeArray = $Get(pCodeArray(0),0) 625 | Set pHash = ..HashArrayRange(.pCodeArray,,pName_"."_pType,.tSizeHint) 626 | } ElseIf (pType = "INT") { 627 | Merge pCodeArray = ^ROUTINE(pName,0) 628 | Merge tSizeHint = ^ROUTINE(pName,0,"SIZE") 629 | Set pCodeArray = $Get(pCodeArray(0),0) 630 | 631 | // Skip header (lines 1-4) which, for .INT routines generated from classes, 632 | // includes the class compilation signature. 633 | Set pHash = ..HashArrayRange(.pCodeArray,5,pName_"."_pType,.tSizeHint) 634 | } ElseIf (pType = "PY") { 635 | Merge pCodeArray = ^ROUTINE(pName_".py",0) // the python source code 636 | set tSizeHint = ^ROUTINE(pName_".py",0,0) // the number of lines in the python code 637 | set pHash = ..HashArrayRange(.pCodeArray, ,pName_".py", .tSizeHint) 638 | } Else { 639 | // Give standard descriptive error about the type being invalid. 640 | $$$ThrowStatus(..TypeIsValid(pType)) 641 | } 642 | Set pCache(pName,pType) = pHash 643 | } 644 | If (pHash = "") { 645 | Set tSC = $$$ERROR($$$GeneralError,"Source code not available for "_pName_"."_pType) 646 | } 647 | } Catch e { 648 | Set pHash = "" 649 | Set tSC = e.AsStatus() 650 | } 651 | Quit tSC 652 | } 653 | 654 | ClassMethod HashArrayRange(ByRef pArray, pStart As %Integer = 1, pInitValue As %String, pSizeHint As %Integer = 0) [ Private ] 655 | { 656 | Set tHash = "" 657 | Set tSC = $$$OK 658 | If (pSizeHint > $$$MaxLocalLength) { 659 | // If we would exceed the max string length, use a stream instead. 660 | Set tTmpStream = ##class(%Stream.TmpCharacter).%New() 661 | Set tString = pInitValue 662 | For tIndex = pStart:1:$Get(pArray) { 663 | Do tTmpStream.Write(pArray(tIndex)) 664 | } 665 | Set tHash = $Case(tTmpStream.Size,0:"",:$System.Encryption.Base64Encode($System.Encryption.SHA1HashStream(tTmpStream,.tSC))) 666 | $$$ThrowOnError(tSC) 667 | } Else { 668 | Set tString = pInitValue 669 | For tIndex = pStart:1:$Get(pArray) { 670 | Set tString = tString_$Get(pArray(tIndex)) 671 | } 672 | // This is fast enough; overhead is only ~3x that of $zcrc(tString,7) 673 | Set tHash = $Case(tString,"":"",:$System.Encryption.Base64Encode($System.Encryption.SHA1Hash(tString))) 674 | } 675 | Quit tHash 676 | } 677 | 678 | Method ExportToStream(pStream As %Stream.Object) As %Status 679 | { 680 | Set tSC = $$$OK 681 | Try { 682 | For tLineNumber = 1:1:..Lines.Count() { 683 | Do pStream.WriteLine(..Lines.GetAt(tLineNumber)) 684 | } 685 | } Catch e { 686 | Set tSC = e.AsStatus() 687 | } 688 | Quit tSC 689 | } 690 | 691 | Storage Default 692 | { 693 | 694 | 695 | %%CLASSNAME 696 | 697 | 698 | Name 699 | 700 | 701 | Type 702 | 703 | 704 | ExecutableLines 705 | 706 | 707 | Generated 708 | 709 | 710 | OutdatedComplexity 711 | 712 | 713 | 714 | LineIsPython 715 | subnode 716 | "LineIsPython" 717 | 718 | 719 | LineToMethodMap 720 | subnode 721 | "LineToMethodMap" 722 | 723 | 724 | Lines 725 | subnode 726 | "Lines" 727 | 728 | 729 | MethodEndMap 730 | subnode 731 | "MethodEndMap" 732 | 733 | 734 | MethodMap 735 | subnode 736 | "MethodMap" 737 | 738 | ^TestCoverage.Data.CodeUnitD 739 | CodeUnitDefaultData 740 | ^TestCoverage.Data.CodeUnitD 741 | ^TestCoverage.Data.CodeUnitI 742 | ^TestCoverage.Data.CodeUnitS 743 | %Storage.Persistent 744 | } 745 | 746 | } 747 | -------------------------------------------------------------------------------- /cls/TestCoverage/Data/CodeUnitMap.cls: -------------------------------------------------------------------------------- 1 | /// This class maintains the mapping between .INT/.MAC/.CLS and therefore is critical for 2 | /// interpreting the .INT-level coverage data that the line-by-line monitor collects. 3 | Class TestCoverage.Data.CodeUnitMap Extends %Persistent 4 | { 5 | 6 | Index Key On (FromHash, FromLine, ToHash, ToLine) [ IdKey ]; 7 | 8 | Property FromHash As TestCoverage.Data.CodeUnit [ Required ]; 9 | 10 | Property FromLine As %Integer [ Required ]; 11 | 12 | Property ToHash As TestCoverage.Data.CodeUnit [ Required ]; 13 | 14 | Property ToLine As %Integer [ Required ]; 15 | 16 | Index Reverse On (ToHash, ToLine, FromHash, FromLine) [ Unique ]; 17 | 18 | Index HashForward On (FromHash, ToHash); 19 | 20 | Index HashReverse On (ToHash, FromHash); 21 | 22 | ForeignKey FromCodeUnitFK(FromHash) References TestCoverage.Data.CodeUnit(Hash) [ OnDelete = cascade ]; 23 | 24 | ForeignKey ToCodeUnitFK(ToHash) References TestCoverage.Data.CodeUnit(Hash) [ OnDelete = cascade ]; 25 | 26 | ClassMethod Create(pFromHash As %String, pFromLine As %Integer, pToHash As %String, pToLineStart As %Integer, pToLineEnd As %Integer) As %Status 27 | { 28 | #def1arg DefaultStorageNode(%node) ##expression($$$comMemberKeyGet("TestCoverage.Data.CodeUnitMap", $$$cCLASSstorage, "Default", %node)) 29 | #def1arg CodeUnitMasterMap(%arg) $$$DefaultStorageNode($$$cSDEFdatalocation)(%arg) 30 | #def1arg CodeUnitReverseMap(%arg) $$$DefaultStorageNode($$$cSDEFindexlocation)("Reverse",%arg) 31 | 32 | Set tSC = $$$OK 33 | Try { 34 | For counter=pToLineStart:1:pToLineEnd { 35 | // Uses direct global references for performance boost; this is one of the most performance-critical sections. 36 | If '$Data($$$CodeUnitMasterMap(pFromHash,pFromLine,pToHash,counter)) { 37 | &sql(insert %NOLOCK %NOCHECK into TestCoverage_Data.CodeUnitMap 38 | (FromHash, FromLine, ToHash, ToLine) 39 | select :pFromHash, :pFromLine, :pToHash, :counter) 40 | If (SQLCODE < 0) { 41 | Throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) 42 | } 43 | } 44 | } 45 | 46 | // Insert/update transitive data (e.g., .INT -> .MAC (generator) -> .CLS) 47 | // Original implementation: 48 | /* 49 | // Leg 1: Lines that map to the "from" line also map to the "to" line 50 | // Leg 2: The "from" line also maps to lines that the "to" line maps to 51 | &sql( 52 | insert or update %NOLOCK %NOCHECK into TestCoverage_Data.CodeUnitMap 53 | (FromHash, FromLine, ToHash, ToLine) 54 | select FromHash, FromLine, :pToHash, Counter 55 | from TestCoverage.Sequence(:pToLineStart,:pToLineEnd),TestCoverage_Data.CodeUnitMap 56 | where ToHash = :pFromHash and ToLine = :pFromLine 57 | union 58 | select :pFromHash, :pFromLine, ToHash, ToLine 59 | from TestCoverage.Sequence(:pToLineStart,:pToLineEnd) 60 | join TestCoverage_Data.CodeUnitMap 61 | on FromHash = :pToHash and FromLine = Counter) 62 | If (SQLCODE < 0) { 63 | Throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) 64 | } 65 | */ 66 | 67 | // This introduced some unacceptable performance overhead, and has been rewritten with direct global references. 68 | // This reduces overall overhead of code capture for test coverage measurement by roughly 40%. 69 | 70 | // Leg 1: Lines that map to the "from" line also map to the "to" line 71 | Set fromHash = "" 72 | For { 73 | Set fromHash = $Order($$$CodeUnitReverseMap(pFromHash,pFromLine,fromHash)) 74 | If (fromHash = "") { 75 | Quit 76 | } 77 | Set fromLine = "" 78 | For { 79 | Set fromLine = $Order($$$CodeUnitReverseMap(pFromHash,pFromLine,fromHash,fromLine)) 80 | If (fromLine = "") { 81 | Quit 82 | } 83 | For counter=pToLineStart:1:pToLineEnd { 84 | If '$Data($$$CodeUnitMasterMap(fromHash,fromLine,pToHash,counter)) { 85 | &sql(insert %NOLOCK %NOCHECK into TestCoverage_Data.CodeUnitMap 86 | (FromHash, FromLine, ToHash, ToLine) 87 | select :fromHash, :fromLine, :pToHash, :counter) 88 | If (SQLCODE < 0) { 89 | Throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) 90 | } 91 | } 92 | } 93 | } 94 | } 95 | 96 | For counter=pToLineStart:1:pToLineEnd { 97 | // Leg 2: The "from" line also maps to lines that the "to" line maps to 98 | Set toHash = "" 99 | For { 100 | Set toHash = $Order($$$CodeUnitMasterMap(pToHash,counter,toHash)) 101 | If (toHash = "") { 102 | Quit 103 | } 104 | Set toLine = "" 105 | For { 106 | Set toLine = $Order($$$CodeUnitMasterMap(pToHash,counter,toHash,toLine)) 107 | If (toLine = "") { 108 | Quit 109 | } 110 | If '$Data($$$CodeUnitMasterMap(pFromHash,pFromLine,toHash,toLine)) { 111 | &sql(insert %NOLOCK %NOCHECK into TestCoverage_Data.CodeUnitMap 112 | (FromHash, FromLine, ToHash, ToLine) 113 | select :pFromHash, :pFromLine, :toHash, :toLine) 114 | If (SQLCODE < 0) { 115 | Throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) 116 | } 117 | } 118 | } 119 | } 120 | } 121 | } Catch e { 122 | Set tSC = e.AsStatus() 123 | } 124 | Quit tSC 125 | } 126 | 127 | ClassMethod IsLineMappedTo(pToHash As %String, pToLine As %Integer) As %Boolean 128 | { 129 | If (pToHash = "") || (pToLine = "") { 130 | Quit 0 131 | } 132 | 133 | // In theory, the query would be really really fast and just have a single global reference. 134 | // In practice, the generated code loops over subscripts in the "Reverse" index. 135 | /* 136 | &sql(select top 1 1 from TestCoverage_Data.CodeUnitMap where ToHash = :pToHash and ToLine = :pToLine) 137 | Quit (SQLCODE = 0) 138 | */ 139 | 140 | // Therefore, as an optimization, just check the global of interest. 141 | Quit ($Data(^TestCoverage.Data.CodeUnitMapI("Reverse",pToHash,pToLine)) > 0) 142 | } 143 | 144 | Storage Default 145 | { 146 | 147 | 148 | %%CLASSNAME 149 | 150 | 151 | ^TestCoverage.Data.CodeUnitMapD 152 | CodeUnitMapDefaultData 153 | ^TestCoverage.Data.CodeUnitMapD 154 | ^TestCoverage.Data.CodeUnitMapI 155 | ^TestCoverage.Data.CodeUnitMapS 156 | %Storage.Persistent 157 | } 158 | 159 | } 160 | 161 | -------------------------------------------------------------------------------- /cls/TestCoverage/Data/Coverage.cls: -------------------------------------------------------------------------------- 1 | Include TestCoverage 2 | 3 | IncludeGenerator TestCoverage 4 | 5 | Class TestCoverage.Data.Coverage Extends %Persistent 6 | { 7 | 8 | Index UniqueCoverageData On (Run, Hash, TestPath) [ Unique ]; 9 | 10 | Index MeaningfulCoverageData On (Run, Calculated, Ignore, Hash, TestPath) [ Data = CoveredLines, Unique ]; 11 | 12 | /// Reference to the test coverage tracking run for which this coverage data was collected. 13 | Property Run As TestCoverage.Data.Run [ Required ]; 14 | 15 | /// Path through test cases/suites 16 | Property TestPath As %String(COLLATION = "EXACT", MAXLEN = 300, TRUNCATE = 1) [ Required ]; 17 | 18 | /// Target code unit, uniquely identified by hash. 19 | Property Hash As TestCoverage.Data.CodeUnit [ Required ]; 20 | 21 | /// If set to 1, this coverage data should be ignored in reports/aggregates. 22 | Property Ignore As %Boolean [ InitialExpression = 0 ]; 23 | 24 | /// If set to 1, this coverage data was calculated as a rollup based on underlying data. 25 | Property Calculated As %Boolean [ InitialExpression = 0 ]; 26 | 27 | Index Run On Run [ Type = bitmap ]; 28 | 29 | Index TestPath On TestPath [ Type = bitmap ]; 30 | 31 | Index Hash On Hash [ Type = bitmap ]; 32 | 33 | Index Ignore On Ignore [ Type = bitmap ]; 34 | 35 | Index Calculated On Calculated [ Type = bitmap ]; 36 | 37 | ForeignKey RunFK(Run) References TestCoverage.Data.Run() [ OnDelete = cascade ]; 38 | 39 | ForeignKey HashFK(Hash) References TestCoverage.Data.CodeUnit(Hash); 40 | 41 | // METRICS 42 | 43 | /// Bitstring of "Line Covered" flags 44 | Property CoveredLines As TestCoverage.DataType.Bitstring; 45 | 46 | /// List of "RtnLine" counts subscripted by line number 47 | Property RtnLine As array Of %Integer; 48 | 49 | /// List of "Time" measurements from line-by-line monitor, subscripted by line number 50 | Property Time As array Of TestCoverage.DataType.Timing [ SqlFieldName = _TIME ]; 51 | 52 | /// List of "TotalTime" measurements from line-by-line monitor, subscripted by line number 53 | Property TotalTime As array Of TestCoverage.DataType.Timing; 54 | 55 | ClassMethod StoreIntCoverage(pRun As %Integer, pTestPath As %String, pName As %String, pType As %String, ByRef pCache) As %Status 56 | { 57 | // pType must be either INT or PY 58 | Set tSC = $$$OK 59 | Try { 60 | #dim tResult As %SQL.StatementResult 61 | Set tSC = ##class(TestCoverage.Data.CodeUnit).GetCurrentByName(pName_"."_pType,,.tCodeUnit,.pCache) 62 | $$$ThrowOnError(tSC) 63 | If ..UniqueCoverageDataExists(pRun,tCodeUnit.Hash,pTestPath,.tID) { 64 | Set tInstance = ..%OpenId(tID,,.tSC) 65 | $$$ThrowOnError(tSC) 66 | } Else { 67 | Set tInstance = ..%New() 68 | Set tSC = tInstance.RunSetObjectId(pRun) 69 | $$$ThrowOnError(tSC) 70 | Set tInstance.TestPath = pTestPath 71 | Set tInstance.Hash = tCodeUnit 72 | For tLineNumber=1:1:tCodeUnit.Lines.Count() { 73 | do tInstance.RtnLine.SetAt(0, tLineNumber) // initialized to 0 hits of each line 74 | // necessary for the python coverages because they don't track lines that aren't covered, only lines that are covered 75 | } 76 | } 77 | 78 | Set tCoveredLines = tInstance.CoveredLines 79 | if (pType = "INT") 80 | { 81 | Set tAvailableMetrics = ..GetAvailableMetrics() 82 | Set tPointer = 0 83 | While $ListNext(tAvailableMetrics,tPointer,tMetricKey) { 84 | If tInstance.Run.Metrics.Find(tMetricKey) { 85 | Set tMetrics(tMetricKey) = $Property(tInstance,tMetricKey) 86 | } 87 | } 88 | // ROWSPEC = "LineNumber:%Integer,LineCovered:%Boolean,RtnLine:%Integer,Time:%Numeric,TotalTime:%Numeric" 89 | Set tResult = ##class(TestCoverage.Utils).LineByLineMonitorResultFunc(pName) 90 | While tResult.%Next(.tSC) { 91 | $$$ThrowOnError(tSC) 92 | Set tLineNumber = tResult.%Get("LineNumber") 93 | If tResult.%Get("LineCovered") { 94 | Set $Bit(tCoveredLines,tLineNumber) = 1 95 | } 96 | Set tMetricKey = "" 97 | For { 98 | Set tMetricKey = $Order(tMetrics(tMetricKey),1,tMetric) 99 | If (tMetricKey = "") { 100 | Quit 101 | } 102 | Do tMetric.SetAt(tResult.%Get(tMetricKey) + tMetric.GetAt(tLineNumber),tLineNumber) 103 | } 104 | } 105 | $$$ThrowOnError(tSC) 106 | } 107 | Else { // If pType = "PY" 108 | // $$$PyMonitorResults(classname, linenumber) = the number of times that linenumber in that class was covered 109 | 110 | if $Data($$$PyMonitorResults(pName)) { 111 | Set tLine = "" 112 | for { 113 | Set tLine = $Order($$$PyMonitorResults(pName, tLine), 1, tLineCount) 114 | if (tLine = "") { 115 | quit 116 | } 117 | Set $Bit(tCoveredLines, tLine) = 1 118 | Do tInstance.RtnLine.SetAt(tInstance.RtnLine.GetAt(tLine) + tLineCount, tLine) 119 | } 120 | } 121 | 122 | } 123 | 124 | Set tInstance.CoveredLines = $BitLogic(tInstance.CoveredLines|tCoveredLines) 125 | 126 | Set tSC = tInstance.%Save() 127 | $$$ThrowOnError(tSC) 128 | } Catch e { 129 | Set tSC = e.AsStatus() 130 | } 131 | Quit tSC 132 | } 133 | 134 | ClassMethod GetAvailableMetrics() As %List [ CodeMode = objectgenerator ] 135 | { 136 | // Note: this is implemented as a generator method to avoid referencing an include file in a persistent class. 137 | // Doing so makes shipping this tool as a deployed Studio project difficult, because include files cannot be deployed, 138 | // and dynamic queries against this class will end up referencing the include file. 139 | #define QuotedMetrics ##quote($$$METRICS) 140 | Do %code.WriteLine(" Quit $ListBuild("_$$$QuotedMetrics_")") 141 | Quit $$$OK 142 | } 143 | 144 | Storage Default 145 | { 146 | 147 | 148 | %%CLASSNAME 149 | 150 | 151 | Run 152 | 153 | 154 | TestPath 155 | 156 | 157 | Hash 158 | 159 | 160 | Ignore 161 | 162 | 163 | Calculated 164 | 165 | 166 | CoveredLines 167 | 168 | 169 | 170 | RtnLine 171 | subnode 172 | "RtnLine" 173 | 174 | 175 | Time 176 | subnode 177 | "Time" 178 | 179 | 180 | TotalTime 181 | subnode 182 | "TotalTime" 183 | 184 | ^TestCoverage.Data.CoverageD 185 | CoverageDefaultData 186 | ^TestCoverage.Data.CoverageD 187 | ^TestCoverage.Data.CoverageI 188 | ^TestCoverage.Data.CoverageS 189 | %Storage.Persistent 190 | } 191 | 192 | } 193 | -------------------------------------------------------------------------------- /cls/TestCoverage/Data/Run.cls: -------------------------------------------------------------------------------- 1 | Class TestCoverage.Data.Run Extends %Persistent 2 | { 3 | 4 | /// Test paths included in this run 5 | Property TestPaths As list Of %String(MAXLEN = "", STORAGEDEFAULT = "array"); 6 | 7 | /// Unit test results associated with this coverage report 8 | Property TestResults As %UnitTest.Result.TestInstance; 9 | 10 | /// List of metrics measured during test coverage (see datatype class for options) 11 | Property Metrics As list Of TestCoverage.DataType.Metric(STORAGEDEFAULT = "array"); 12 | 13 | /// Subject of test coverage. 14 | /// For example, an application or module name. 15 | Property Subject As %String(MAXLEN = 255); 16 | 17 | /// Changelist, timestamp, or other identifier at which these coverage results were obtained. 18 | /// Any metric used for this should order changes in ascending order by point in time. 19 | Property Ordering As %String; 20 | 21 | /// Set to true if the test coverage data is for committed code (rather than pending/in-review changes) 22 | Property IsCommitted As %Boolean [ InitialExpression = 0 ]; 23 | 24 | /// Index to easily find the first coverage run before/after a given point in time. 25 | Index ComparisonIndex On (Subject, IsCommitted, Ordering); 26 | 27 | /// Level of detail of the test coverage run 28 | Property Detail As TestCoverage.DataType.Detail; 29 | 30 | /// Given .INT code coverage for a test run, maps it to .CLS/.MAC. 31 | ClassMethod MapRunCoverage(pRunIndex As %Integer) As %Status 32 | { 33 | Set tCursorOpen = 0 34 | Set tSC = $$$OK 35 | Try { 36 | Set tRun = ##class(TestCoverage.Data.Run).%OpenId(pRunIndex,,.tSC) 37 | $$$ThrowOnError(tSC) 38 | 39 | // It would be wonderful if there was support for something along the lines of (with a few made up non-functions): 40 | /* 41 | INSERT OR UPDATE INTO TestCoverage_Data.Coverage 42 | (Run,Hash,TestPath,CoveredLines,Ignore) 43 | SELECT :pRunIndex,map.ToHash,TestPath,$BITLOGIC(%BITLIST(CASE $BIT(source.CoveredLines,map.FromLine) 44 | WHEN 1 THEN map.ToLine ELSE NULL END)|oldCoverage.CoveredLines), 45 | source.Hash->Generated 46 | FROM TestCoverage_Data.Coverage source 47 | JOIN TestCoverage_Data.CodeUnitMap map 48 | ON source.Hash = map.FromHash 49 | LEFT JOIN TestCoverage_Data.Coverage oldCoverage 50 | ON oldCoverage.Run = source.Run 51 | AND oldCoverage.Hash = map.ToHash 52 | AND oldCoverage.TestPath = source.TestPath 53 | WHERE source.Run = :pRunIndex 54 | AND source.Ignore = 0 55 | AND source.Calculated = 0 56 | GROUP BY map.ToHash,source.TestPath 57 | */ 58 | 59 | // Here's a worse-performing approach with some extrinsic calls that ideally wouldn't be necessary: 60 | &SQL( 61 | /* INSERT OR UPDATE %NOLOCK %NOCHECK INTO TestCoverage_Data.Coverage 62 | (Run,Hash,TestPath,CoveredLines,Ignore) */ 63 | DECLARE C0 CURSOR FOR 64 | SELECT map.ToHash,source.TestPath,TestCoverage.BITWISE_OR( 65 | TestCoverage.LIST_TO_BIT(%DLIST( 66 | CASE TestCoverage.BIT_VALUE(source.CoveredLines,map.FromLine) 67 | WHEN 1 THEN map.ToLine 68 | ELSE NULL END)),oldCoverage.CoveredLines), 69 | map.ToHash->Generated 70 | INTO :hToHash, :hTestPath, :hCoveredLines, :hIgnore 71 | FROM %NOPARALLEL TestCoverage_Data.Coverage source 72 | JOIN TestCoverage_Data.CodeUnitMap map 73 | ON source.Hash = map.FromHash 74 | LEFT JOIN TestCoverage_Data.Coverage oldCoverage 75 | ON oldCoverage.Run = source.Run 76 | AND oldCoverage.Hash = map.ToHash 77 | AND oldCoverage.TestPath = source.TestPath 78 | WHERE source.Run = :pRunIndex 79 | AND source.Ignore = 0 80 | AND source.Calculated = 0 81 | GROUP BY map.ToHash,source.TestPath) 82 | 83 | &sql(OPEN C0) 84 | If (SQLCODE < 0) { 85 | Throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) 86 | } 87 | Set tCursorOpen = 1 88 | 89 | For { 90 | &SQL(FETCH C0) 91 | If (SQLCODE < 0) { 92 | Throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) 93 | } ElseIf (SQLCODE) { 94 | Quit 95 | } 96 | If ##class(TestCoverage.Data.Coverage).UniqueCoverageDataExists(pRunIndex,hToHash,hTestPath,.tID) { 97 | Set tCoverage = ##class(TestCoverage.Data.Coverage).%OpenId(tID,,.tSC) 98 | $$$ThrowOnError(tSC) 99 | } Else { 100 | Set tCoverage = ##class(TestCoverage.Data.Coverage).%New() 101 | Do tCoverage.RunSetObjectId(pRunIndex) 102 | Do tCoverage.HashSetObjectId(hToHash) 103 | Set tCoverage.TestPath = hTestPath 104 | // also set all of its metrics to 0 to start with 105 | Set tCodeUnit = ##class(TestCoverage.Data.CodeUnit).%OpenId(hToHash) 106 | For i=1:1:tRun.Metrics.Count() { 107 | Set tMetricKey = tRun.Metrics.GetAt(i) 108 | Set tMetric = $PROPERTY(tCoverage, tMetricKey) 109 | for tLineNumber = 1:1:tCodeUnit.Lines.Count() { 110 | Do tMetric.SetAt(0, tLineNumber) 111 | } 112 | } 113 | 114 | 115 | } 116 | Set tCoverage.Ignore = hIgnore 117 | Set tCoverage.CoveredLines = $BitLogic(tCoverage.CoveredLines|hCoveredLines) 118 | $$$ThrowOnError(tCoverage.%Save()) 119 | } 120 | 121 | // Copy any other metrics captured/requested as well. 122 | For i=1:1:tRun.Metrics.Count() { 123 | Set tMetric = tRun.Metrics.GetAt(i) 124 | If $System.SQL.IsReservedWord(tMetric) { 125 | // e.g., "Time" -> "_Time" 126 | Set tMetric = "_"_tMetric 127 | } 128 | Set tSQLStatement = "INSERT OR UPDATE %NOLOCK %NOCHECK INTO TestCoverage_Data.Coverage_"_tMetric_" "_ 129 | "(Coverage,element_key,"_tMetric_") "_ 130 | "SELECT target.ID,map.ToLine,NVL(oldMetric."_tMetric_",0) + SUM(metric."_tMetric_") "_ 131 | "FROM %INORDER TestCoverage_Data.Coverage source "_ 132 | "JOIN TestCoverage_Data.Coverage_"_tMetric_" metric "_ 133 | " ON metric.Coverage = source.ID "_ 134 | "JOIN TestCoverage_Data.CodeUnitMap map "_ 135 | " ON source.Hash = map.FromHash "_ 136 | " AND metric.element_key = map.FromLine "_ 137 | "JOIN TestCoverage_Data.Coverage target "_ 138 | " ON target.Run = source.Run "_ 139 | " AND target.Hash = map.ToHash "_ 140 | " AND target.TestPath = source.TestPath "_ 141 | "LEFT JOIN TestCoverage_Data.Coverage_"_tMetric_" oldMetric "_ 142 | " ON oldMetric.ID = target.ID "_ 143 | " AND oldMetric.element_key = map.ToLine "_ 144 | "WHERE source.Run = ? "_ 145 | " AND source.Ignore = 0"_ 146 | " AND source.Calculated = 0 "_ 147 | "GROUP BY target.ID,map.ToLine" 148 | 149 | #dim tResult As %SQL.StatementResult 150 | Set tResult = ##class(%SQL.Statement).%ExecDirect(,tSQLStatement,pRunIndex) 151 | If (tResult.%SQLCODE < 0) { 152 | Throw ##class(%Exception.SQL).CreateFromSQLCODE(tResult.%SQLCODE,tResult.%Message) 153 | } 154 | } 155 | } Catch e { 156 | Set tSC = e.AsStatus() 157 | } 158 | If tCursorOpen { 159 | &sql(CLOSE C0) 160 | } 161 | Quit tSC 162 | } 163 | 164 | Storage Default 165 | { 166 | 167 | Metrics 168 | subnode 169 | "Metrics" 170 | 171 | 172 | 173 | %%CLASSNAME 174 | 175 | 176 | TestResults 177 | 178 | 179 | Subject 180 | 181 | 182 | Ordering 183 | 184 | 185 | IsCommitted 186 | 187 | 188 | Detail 189 | 190 | 191 | 192 | TestPaths 193 | subnode 194 | "TestPaths" 195 | 196 | ^TestCoverage.Data.RunD 197 | RunDefaultData 198 | ^TestCoverage.Data.RunD 199 | ^TestCoverage.Data.RunI 200 | ^TestCoverage.Data.RunS 201 | %Storage.Persistent 202 | } 203 | 204 | } 205 | -------------------------------------------------------------------------------- /cls/TestCoverage/DataType/Bitstring.cls: -------------------------------------------------------------------------------- 1 | /// Overrides aggregates for bitstrings:
2 | /// MAX is the bitwise OR of all values considered in aggregation
3 | /// MIN is the bitwise AND of all values considered in aggregation
4 | /// Note that this only works for aggregates operating on properties of this type - not for arbitrary expressions 5 | Class TestCoverage.DataType.Bitstring Extends %Binary [ ClassType = datatype ] 6 | { 7 | 8 | Parameter MAXLEN As INTEGER; 9 | 10 | ClassMethod SQLmax(pAccumulated As TestCoverage.DataType.Bitstring, pValue As TestCoverage.DataType.Bitstring) As TestCoverage.DataType.Bitstring 11 | { 12 | Quit $BitLogic(pAccumulated|pValue) 13 | } 14 | 15 | ClassMethod SQLmin(pAccumulated As TestCoverage.DataType.Bitstring, pValue As TestCoverage.DataType.Bitstring) As TestCoverage.DataType.Bitstring 16 | { 17 | Quit $BitLogic(pAccumulated&pValue) 18 | } 19 | 20 | } 21 | 22 | -------------------------------------------------------------------------------- /cls/TestCoverage/DataType/Detail.cls: -------------------------------------------------------------------------------- 1 | Class TestCoverage.DataType.Detail Extends %String [ ClassType = datatype ] 2 | { 3 | 4 | Parameter DISPLAYLIST = ",Overall,Suite,Class,Method"; 5 | 6 | Parameter VALUELIST = ",0,1,2,3"; 7 | 8 | } 9 | 10 | -------------------------------------------------------------------------------- /cls/TestCoverage/DataType/Metric.cls: -------------------------------------------------------------------------------- 1 | /// Valid metric names in %SYS.MONLBL 2 | Class TestCoverage.DataType.Metric Extends %String [ ClassType = datatype ] 3 | { 4 | 5 | Parameter VALUELIST = ",RtnLine,Time,TotalTime"; 6 | 7 | } 8 | 9 | -------------------------------------------------------------------------------- /cls/TestCoverage/DataType/RoutineType.cls: -------------------------------------------------------------------------------- 1 | Class TestCoverage.DataType.RoutineType Extends %String [ ClassType = datatype ] 2 | { 3 | 4 | Parameter MAXLEN = 3; 5 | 6 | Parameter VALUELIST = ",CLS,MAC,INT,PY"; 7 | 8 | } 9 | -------------------------------------------------------------------------------- /cls/TestCoverage/DataType/Timing.cls: -------------------------------------------------------------------------------- 1 | Class TestCoverage.DataType.Timing Extends %Numeric 2 | { 3 | 4 | /// The scale value (number of digits following the decimal point) for this data type. The logical value will be rounded to the specified number of decimal places. 5 | Parameter SCALE As INTEGER = 6; 6 | 7 | } 8 | 9 | -------------------------------------------------------------------------------- /cls/TestCoverage/Listeners/ListenerInterface.cls: -------------------------------------------------------------------------------- 1 | Class TestCoverage.Listeners.ListenerInterface Extends %RegisteredObject 2 | { 3 | 4 | Method Broadcast(pMessage As %DynamicObject) As %Status [ Abstract ] 5 | { 6 | } 7 | 8 | } 9 | -------------------------------------------------------------------------------- /cls/TestCoverage/Listeners/ListenerManager.cls: -------------------------------------------------------------------------------- 1 | Class TestCoverage.Listeners.ListenerManager Extends %RegisteredObject 2 | { 3 | 4 | Property listeners As list Of TestCoverage.Listeners.ListenerInterface; 5 | 6 | Method BroadCastToAll(pMessage As %DynamicObject) As %Status 7 | { 8 | set tSC = $$$OK 9 | try { 10 | for i = 1:1:..listeners.Count() { 11 | set tListener = ..listeners.GetAt(i) 12 | $$$ThrowOnError(tListener.Broadcast(pMessage)) 13 | } 14 | } 15 | catch e { 16 | Set tSC = e.AsStatus() 17 | } 18 | quit tSC 19 | } 20 | 21 | Method AddListener(pListener As TestCoverage.Listeners.ListenerInterface) As %Status 22 | { 23 | set tSC = $$$OK 24 | try { 25 | do ..listeners.Insert(pListener) 26 | } catch e { 27 | set tSC = e.AsStatus() 28 | } 29 | quit tSC 30 | } 31 | 32 | Method RemoveListener(pListener As TestCoverage.Listeners.ListenerInterface) As %Status 33 | { 34 | set tSC = $$$OK 35 | try { 36 | set tIndex = ..listeners.FindOref(pListener) 37 | if (tIndex = "") { 38 | Set tMsg = "Listener not found" 39 | $$$ThrowStatus($$$ERROR($$$GeneralError,tMsg)) 40 | } 41 | do ..listeners.RemoveAt(tIndex) 42 | } catch e { 43 | set tSC = e.AsStatus() 44 | } 45 | quit tSC 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /cls/TestCoverage/Procedures.cls: -------------------------------------------------------------------------------- 1 | /// Contains several helpful stored procedures for use in SQL. 2 | Class TestCoverage.Procedures 3 | { 4 | 5 | /// Wraps $Bit for exposure as an SQL stored procedure 6 | ClassMethod BitValue(pSource As %Binary, pIndex As %Integer) As %Boolean [ SqlName = BIT_VALUE, SqlProc ] 7 | { 8 | Quit $Bit(pSource,pIndex) 9 | } 10 | 11 | /// Wraps $BitCount for exposure as an SQL stored procedure 12 | ClassMethod BitCount(pSource As %Binary, pValue As %Boolean) As %Integer [ SqlName = BIT_COUNT, SqlProc ] 13 | { 14 | If $Data(pValue) { 15 | Quit $BitCount(pSource, pValue) 16 | } 17 | Quit $BitCount(pSource) 18 | } 19 | 20 | /// Wrapper for $BitLogic(pArg1&pArg2) for exposure as an SQL stored procedure 21 | ClassMethod BitwiseAnd(pArgs...) As %Binary [ SqlName = BITWISE_AND, SqlProc ] 22 | { 23 | Set tResult = $Get(pArgs(1)) 24 | For i=2:1:$Get(pArgs) { 25 | Set tResult = $BitLogic(tResult&pArgs(i)) 26 | } 27 | Quit tResult 28 | } 29 | 30 | /// Wrapper for $BitLogic(pArg1|pArg2) for exposure as an SQL stored procedure 31 | ClassMethod BitwiseOr(pArgs...) As %Binary [ SqlName = BITWISE_OR, SqlProc ] 32 | { 33 | Set tResult = $Get(pArgs(1)) 34 | For i=2:1:$Get(pArgs) { 35 | Set tResult = $BitLogic(tResult|pArgs(i)) 36 | } 37 | Quit tResult 38 | } 39 | 40 | /// Wrapper for $BitLogic(pArg1^pArg2) for exposure as an SQL stored procedure 41 | ClassMethod BitwiseXor(pArg1 As %Binary, pArg2 As %Binary) As %Binary [ SqlName = BITWISE_XOR, SqlProc ] 42 | { 43 | Quit $BitLogic(pArg1^pArg2) 44 | } 45 | 46 | /// Wrapper for $BitLogic(~pArg) for exposure as an SQL stored procedure 47 | ClassMethod BitwiseNot(pArg As %Binary) As %Binary [ SqlName = BITWISE_NOT, SqlProc ] 48 | { 49 | Quit $BitLogic(~pArg) 50 | } 51 | 52 | /// Applies a bitwise OR to a $ListBuild list of bitstrings; input may be from the SQL %DLIST aggregate 53 | ClassMethod BitwiseOrList(pSource As %List) As %Binary [ SqlName = BITWISE_OR_LIST, SqlProc ] 54 | { 55 | Set tResult = "" 56 | Set tPointer = 0 57 | While $ListNext(pSource,tPointer,tItem) { 58 | Set tResult = $BitLogic(tResult|tItem) 59 | } 60 | Quit tResult 61 | } 62 | 63 | /// Applies a bitwise AND to a $ListBuild list of bitstrings; input may be from the SQL %DLIST aggregate 64 | ClassMethod BitwiseAndList(pSource As %List) As %Binary [ SqlName = BITWISE_AND_LIST, SqlProc ] 65 | { 66 | Set tResult = "" 67 | Set tPointer = 0 68 | While $ListNext(pSource,tPointer,tItem) { 69 | If (tResult = "") { 70 | Set tResult = tItem 71 | } Else { 72 | Set tResult = $BitLogic(tResult&tItem) 73 | } 74 | } 75 | Quit tResult 76 | } 77 | 78 | /// Convert a $ListBuild list of integers into a $Bit with 1s in positions present in the list 79 | /// Use in SQL with the %DLIST aggregate 80 | ClassMethod ListToBit(pSource As %List) As %Binary [ SqlName = LIST_TO_BIT, SqlProc ] 81 | { 82 | Set tResult = "" 83 | Set tPointer = 0 84 | While $ListNext(pSource,tPointer,tBitPosition) { 85 | If $Data(tBitPosition)#2 && (+tBitPosition > 0) { 86 | Set $Bit(tResult,+tBitPosition) = 1 87 | } 88 | } 89 | Quit tResult 90 | } 91 | 92 | } 93 | 94 | -------------------------------------------------------------------------------- /cls/TestCoverage/Report/AbstractReportGenerator.cls: -------------------------------------------------------------------------------- 1 | /// @API.Extensible 2 | Class TestCoverage.Report.AbstractReportGenerator [ Abstract ] 3 | { 4 | 5 | /// @API.Method 6 | /// @API.Overrideable 7 | ClassMethod GenerateReport(pRunIndex As %Integer, pOutputFileName As %String) As %Status [ Abstract ] 8 | { 9 | } 10 | 11 | } 12 | 13 | -------------------------------------------------------------------------------- /cls/TestCoverage/Report/Cobertura/ReportGenerator.cls: -------------------------------------------------------------------------------- 1 | Include TestCoverage 2 | 3 | Class TestCoverage.Report.Cobertura.ReportGenerator Extends TestCoverage.Report.AbstractReportGenerator 4 | { 5 | 6 | ClassMethod GenerateReport(pRunIndex As %Integer, pOutputFileName As %String) As %Status 7 | { 8 | Set tSC = $$$OK 9 | Try { 10 | Set tCoverage = ##class(TestCoverage.Report.Cobertura.type.coverage).%New() 11 | Set tCoverage.version = ##class(TestCoverage.Report.Cobertura.Schema).#VERSION 12 | Set tCoverage.timestamp = $zdatetime($ztimestamp,-2) * 1000 13 | 14 | // Summary statistics 15 | &sql(select 16 | ROUND(CoveredLines/ExecutableLines,4), 17 | ExecutableLines, 18 | CoveredLines 19 | into :tLineRate,:tLinesValid,:tLinesCovered 20 | from TestCoverage_Data_Aggregate.ByRun where ExecutableLines > 0 and Run = :pRunIndex) 21 | If (SQLCODE < 0) { 22 | Throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) 23 | } 24 | 25 | If (SQLCODE = 100) { 26 | Set tLineRate = 0 27 | Set tLinesCovered = 0 28 | Set tLinesValid = 0 29 | } 30 | 31 | Set tCoverage.linerate = tLineRate 32 | Set tCoverage.linescovered = tLinesCovered 33 | Set tCoverage.linesvalid = tLinesValid 34 | 35 | // For now: 36 | #define EMPTYBRANCHRATE "" 37 | #define EMPTYCOMPLEXITY "" 38 | Set tCoverage.branchescovered = 0 39 | Set tCoverage.branchesvalid = 0 40 | Set tCoverage.branchrate = $$$EMPTYBRANCHRATE 41 | Set tCoverage.complexity = $$$EMPTYCOMPLEXITY 42 | 43 | // Create directory for source code export 44 | Set tSourceDirectory = ##class(%Library.File).GetDirectory(pOutputFileName,1)_"source" 45 | $$$ThrowOnError(##class(TestCoverage.Utils.File).CreateDirectoryChain(tSourceDirectory)) 46 | 47 | Do tCoverage.sources.Insert(tSourceDirectory) 48 | 49 | // Package-level results 50 | Set tTestPath = $$$TestPathAllTests 51 | Set tResults = ##class(%SQL.Statement).%ExecDirect(, 52 | "SELECT ID from TestCoverage_Data.Coverage "_ 53 | "WHERE Run = ? and TestPath = ? and Ignore = 0 And Hash->Type in ('CLS','MAC')",pRunIndex,tTestPath) 54 | If (tResults.%SQLCODE < 0) { 55 | Throw ##class(%Exception.SQL).CreateFromSQLCODE(tResults.%SQLCODE,tResults.%Message) 56 | } 57 | While tResults.%Next(.tSC) { 58 | $$$ThrowOnError(tSC) 59 | Set tCoverageData = ##class(TestCoverage.Data.Coverage).%OpenId(tResults.%Get("ID"),,.tSC) 60 | $$$ThrowOnError(tSC) 61 | Set tCodeUnit = tCoverageData.Hash 62 | Set tFileName = $Replace(tCodeUnit.Name,".","/")_"."_$ZConvert(tCodeUnit.Type,"L") 63 | Set tFullName = ##class(%Library.File).NormalizeFilename(tSourceDirectory_"/"_tFileName) 64 | Set tDirectory = ##class(%Library.File).GetDirectory(tFullName) 65 | $$$ThrowOnError(##class(TestCoverage.Utils.File).CreateDirectoryChain(tDirectory)) 66 | Set tStream = ##class(%Stream.FileCharacter).%New() 67 | $$$ThrowOnError(tStream.LinkToFile(tFullName)) 68 | $$$ThrowOnError(tCoverageData.Hash.ExportToStream(tStream)) 69 | $$$ThrowOnError(tStream.%Save()) 70 | 71 | Set tCoveredLineCount = $BitCount($BitLogic(tCodeUnit.ExecutableLines&tCoverageData.CoveredLines),1) 72 | Set tValidLineCount = $BitCount(tCodeUnit.ExecutableLines,1) 73 | 74 | // Class 75 | Set tClass = ##class(TestCoverage.Report.Cobertura.type.class).%New() 76 | Set tClass.name = tCodeUnit.Name 77 | Set tClass.filename = tFileName 78 | Set tClass.linerate = $Case(tValidLineCount,0:1,:tCoveredLineCount/tValidLineCount) 79 | Set tClass.branchrate = $$$EMPTYBRANCHRATE 80 | Set tClass.complexity = $$$EMPTYCOMPLEXITY 81 | Set tTotalMethodComplexity = 0 82 | 83 | // Lines 84 | Set tLineNumber = 0 85 | For { 86 | Set tLineNumber = $BitFind(tCodeUnit.ExecutableLines,1,tLineNumber+1) 87 | If (tLineNumber = 0) || (tLineNumber = "") { 88 | Quit 89 | } 90 | Set tLine = ##class(TestCoverage.Report.Cobertura.type.line).%New() 91 | Set tLine.number = tLineNumber 92 | Set tLine.branch = "false" 93 | Set tLine.hits = +tCoverageData.RtnLine.GetAt(tLineNumber) 94 | Do tClass.lines.Insert(tLine) 95 | } 96 | 97 | // Methods 98 | Set tMethodKey = "" 99 | For { 100 | Set tSubUnit = tCodeUnit.SubUnits.GetNext(.tMethodKey) 101 | If (tMethodKey = "") { 102 | Quit 103 | } 104 | If tSubUnit.%IsA("TestCoverage.Data.CodeSubUnit.Method") { 105 | Set tMethod = ##class(TestCoverage.Report.Cobertura.type.method).%New() 106 | Set tMethod.name = $Piece(tSubUnit.DisplaySignature," ")_" "_tSubUnit.Name //ClassMethod/Method and method name only 107 | Set tMethod.signature = $c(0) // Interpretation is tied to Java, so we just use the method name (above). 108 | Set tExecutableMethodLines = $BitLogic(tCodeUnit.ExecutableLines&tSubUnit.Mask) 109 | Set tExecutableCount = $BitCount(tExecutableMethodLines,1) 110 | Set tMethod.linerate = $Case(tExecutableCount, 0:1, 111 | :$BitCount($BitLogic(tExecutableMethodLines&tCoverageData.CoveredLines),1) / tExecutableCount) 112 | Set tMethod.branchrate = $$$EMPTYBRANCHRATE 113 | Set tMethod.complexity = tSubUnit.Complexity 114 | Set tTotalMethodComplexity = tTotalMethodComplexity + tMethod.complexity 115 | Set tLineNumber = 0 116 | For { 117 | Set tLineNumber = $BitFind(tExecutableMethodLines,1,tLineNumber+1) 118 | If (tLineNumber = 0) || (tLineNumber = "") { 119 | Quit 120 | } 121 | Set tLine = ##class(TestCoverage.Report.Cobertura.type.line).%New() 122 | Set tLine.number = tLineNumber 123 | Set tLine.branch = "false" 124 | Set tLine.hits = +tCoverageData.RtnLine.GetAt(tLineNumber) 125 | Do tMethod.lines.Insert(tLine) 126 | } 127 | Do tClass.methods.Insert(tMethod) 128 | } 129 | } 130 | 131 | Set tMethodCount = tClass.methods.Count() 132 | If (tMethodCount > 0) { 133 | Set tClass.complexity = tTotalMethodComplexity/tMethodCount 134 | } 135 | 136 | If (tCodeUnit.Type = "CLS") { 137 | Set tPackageSub = $Piece(tCodeUnit.Name,".",1,*-1) 138 | Set tMemberSub = $Piece(tCodeUnit.Name,".",*) 139 | } Else { 140 | Set tPackageSub = $c(0) 141 | Set tMemberSub = tCodeUnit.Name_"."_tCodeUnit.Type 142 | } 143 | 144 | Set tOldCoveredCount = 0 145 | Set tOldValidCount = 0 146 | If $Data(tPackages(tPackageSub),tOldCounts) { 147 | Set $ListBuild(tOldCoveredCount,tOldValidCount) = tOldCounts 148 | } 149 | Set tPackages(tPackageSub) = $ListBuild(tOldCoveredCount + tCoveredLineCount, tOldValidCount + tValidLineCount) 150 | Set tPackages(tPackageSub,tMemberSub) = tClass 151 | } 152 | $$$ThrowOnError(tSC) 153 | 154 | // Package-level aggregation 155 | Set tTotalComplexity = 0 156 | Set tPackageSub = "" 157 | For { 158 | Set tPackageSub = $Order(tPackages(tPackageSub),1,tCounts) 159 | If (tPackageSub = "") { 160 | Quit 161 | } 162 | Set tPackage = ##class(TestCoverage.Report.Cobertura.type.package).%New() 163 | Set tPackage.name = tPackageSub 164 | Set tPackage.linerate = $Case($ListGet(tCounts,2), 0:1, :$ListGet(tCounts)/$ListGet(tCounts,2)) 165 | Set tPackage.branchrate = $$$EMPTYBRANCHRATE 166 | Set tPackage.complexity = $$$EMPTYCOMPLEXITY 167 | Set tTotalClassComplexity = 0 168 | 169 | Set tMemberSub = "" 170 | For { 171 | Set tMemberSub = $Order(tPackages(tPackageSub,tMemberSub),1,tMember) 172 | If (tMemberSub = "") { 173 | Quit 174 | } 175 | Set tTotalClassComplexity = tTotalClassComplexity + tMember.complexity 176 | Do tPackage.classes.Insert(tMember) 177 | } 178 | 179 | Set tClassCount = tPackage.classes.Count() 180 | If (tClassCount > 0) { 181 | Set tPackage.complexity = tTotalClassComplexity/tClassCount 182 | } 183 | 184 | Set tTotalComplexity = tTotalComplexity + tPackage.complexity 185 | Do tCoverage.packages.Insert(tPackage) 186 | } 187 | 188 | // Coverage-level complexity aggregation 189 | Set tPackageCount = tCoverage.packages.Count() 190 | If (tPackageCount > 0) { 191 | Set tCoverage.complexity = tTotalComplexity/tPackageCount 192 | } 193 | 194 | // Actual XML export 195 | Set tStream = ##class(%Stream.FileCharacter).%New() 196 | Set tSC = tStream.LinkToFile(pOutputFileName) 197 | Do tStream.WriteLine("") 198 | Do tStream.WriteLine("") 199 | Do tStream.WriteLine() 200 | $$$ThrowOnError(tSC) 201 | Set tSC = tCoverage.XMLExportToStream(tStream,,",literal,indent") 202 | $$$ThrowOnError(tSC) 203 | 204 | $$$ThrowOnError(tStream.%Save()) 205 | } Catch e { 206 | Set tSC = e.AsStatus() 207 | } 208 | Quit tSC 209 | } 210 | 211 | } 212 | 213 | -------------------------------------------------------------------------------- /cls/TestCoverage/Report/Cobertura/Schema.cls: -------------------------------------------------------------------------------- 1 | Class TestCoverage.Report.Cobertura.Schema 2 | { 3 | 4 | Projection ClassGenerator As TestCoverage.Utils.Projection.SchemaGenerator(PACKAGE = "TestCoverage.Report.Cobertura.type"); 5 | 6 | Parameter VERSION = "2.1.1"; 7 | 8 | /// Based on https://github.com/cobertura/cobertura/blob/master/cobertura/src/site/htdocs/xml/coverage-loose.dtd 9 | /// Converted from DTD to XSD using Visual Studio's tool for such conversions, then edited to produce the correct 10 | /// projection of list properties (most significantly, using "type" rather than "ref" in sequences representing collections) 11 | XData XSD 12 | { 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | } 118 | 119 | } 120 | 121 | -------------------------------------------------------------------------------- /cls/TestCoverage/UI/AggregateResultViewer.cls: -------------------------------------------------------------------------------- 1 | Class TestCoverage.UI.AggregateResultViewer Extends TestCoverage.UI.Template 2 | { 3 | 4 | /// Displayed name of this page. 5 | Parameter PAGENAME = "Unit Test Coverage - Aggregate Results"; 6 | 7 | /// Domain used for localization. 8 | Parameter DOMAIN; 9 | 10 | Property testIndex As %ZEN.Datatype.string(ZENURL = "Index"); 11 | 12 | /// This XML block defines the contents of this page. 13 | XData Contents [ XMLNamespace = "http://www.intersystems.com/zen" ] 14 | { 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | } 67 | 68 | ClientMethod testFilterChanged() [ Language = javascript ] 69 | { 70 | zenPage.testIndex = zen('testIndex').getValue(); 71 | zen('aggregateDataProvider').reloadContentsAsynch(function() { 72 | zen('aggregateDataProvider').raiseDataChange(); 73 | }); 74 | zen('testResultsLink').refreshContents(true); 75 | } 76 | 77 | ClientMethod drawCell(value, row, col) [ Language = javascript ] 78 | { 79 | if ((col == 2) || (col == 5)) { 80 | // Fix value to 2 decimal places 81 | var text = (value === '') ? '' : parseFloat(value).toFixed(2); 82 | return { 83 | content:''+text+'', 84 | align:'right' 85 | }; 86 | } else if (col == 8) { //Info 87 | var html = []; 88 | if (value != '') { 89 | var testIndex = encodeURIComponent(zenPage.testIndex); 90 | var codeUnit = encodeURIComponent(value); 91 | var url = 'TestCoverage.UI.ResultDetailViewer.cls?testIndex='+testIndex+'&codeUnit='+codeUnit; 92 | html.push(''); 93 | html.push('[detail]'); 94 | html.push(''); 95 | } 96 | return { 97 | content:html.join('') 98 | }; 99 | } 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /cls/TestCoverage/UI/Application.cls: -------------------------------------------------------------------------------- 1 | /// TestCoverage.UI.Application 2 | Class TestCoverage.UI.Application Extends %ZEN.application 3 | { 4 | 5 | /// This is the name of this application. 6 | Parameter APPLICATIONNAME; 7 | 8 | /// This is the URL of the main starting page of this application. 9 | Parameter HOMEPAGE; 10 | 11 | /// Comma-separated list of additional JS include files that should be 12 | /// included for every page within the application. 13 | Parameter JSINCLUDES As STRING = "jquery-2.0.3.min.js"; 14 | 15 | /// This Style block contains application-wide CSS style definitions. 16 | XData Style 17 | { 18 | 105 | } 106 | 107 | } 108 | 109 | -------------------------------------------------------------------------------- /cls/TestCoverage/UI/CodeMapExplorer.cls: -------------------------------------------------------------------------------- 1 | Class TestCoverage.UI.CodeMapExplorer Extends %ZEN.Component.page 2 | { 3 | 4 | Property generatedHash As %ZEN.Datatype.string; 5 | 6 | Property sourceHash As %ZEN.Datatype.string; 7 | 8 | XData Contents [ XMLNamespace = "http://www.intersystems.com/zen" ] 9 | { 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 |
19 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 | } 39 | 40 | } 41 | 42 | -------------------------------------------------------------------------------- /cls/TestCoverage/UI/Component/altJSONSQLProvider.cls: -------------------------------------------------------------------------------- 1 | Class TestCoverage.UI.Component.altJSONSQLProvider Extends %ZEN.Auxiliary.altJSONSQLProvider [ System = 3 ] 2 | { 3 | 4 | /// This is the XML namespace for this component. 5 | Parameter NAMESPACE = "http://www.intersystems.com/zen/healthshare/test-coverage"; 6 | 7 | /// Overridden to deal with a few issues on older platform versions 8 | /// (minimal modifications, all commented as such) 9 | Method %DrawJSON() As %Status [ Internal ] 10 | { 11 | Set ..contentType = "array" 12 | // override base method to get information from SQL statement 13 | Set tSC = $$$OK 14 | Try { 15 | #; convert parameters to local array 16 | Set key = ..parameters.Next("") 17 | While (key'="") { 18 | Set value = ..parameters.GetAt(key).value 19 | Set tParms(key) = $$$ZENVAL(value) 20 | Set key = ..parameters.Next(key) 21 | } 22 | Set tOrigSQL = ..sql 23 | Set tSQL = ..sql 24 | 25 | If (..OnGetSQL '= "") { 26 | Set tSC = ..%OnGetSQL(.tParms,.tSQL) 27 | If $$$ISERR(tSC) { 28 | Write "null" 29 | Quit 30 | } 31 | Set ..sql = tSQL 32 | } 33 | 34 | Set tInfo = ##class(%ZEN.Auxiliary.QueryInfo).%New() 35 | Merge tInfo.parms=tParms 36 | Set tRS = ..%CreateResultSet(.tSC,tInfo) 37 | If $$$ISERR(tSC)||'$IsObject(tRS) { 38 | Write "null" 39 | Quit 40 | } 41 | 42 | // find number and name of columns 43 | Kill tColInfo 44 | If tRS.%IsA("%Library.ResultSet") { 45 | Set tCols = tRS.GetColumnCount() 46 | For c = 1:1:tCols { 47 | Set tColInfo(c,"name") = tRS.GetColumnHeader(c) 48 | } 49 | } 50 | Else { 51 | Set tCols = tRS.%ResultColumnCount 52 | For c = 1:1:tCols { 53 | Set tColInfo(c,"name") = tRS.%Metadata.columns.GetAt(c).label 54 | } 55 | } 56 | Set ..sql = tOrigSQL 57 | 58 | Set aet = ##class(%DynamicAbstractObject).%FromJSON("{"""_..arrayName_""":[]}") 59 | Set arrayNode = aet.%Get(..arrayName) 60 | 61 | // fetch and emit JSON 62 | // n.b. this should be pushed into the result set itself 63 | Set tRow = 0 64 | While (tRS.%Next(.tSC) && ((..maxRows = 0) || (tRow < ..maxRows))) { 65 | Quit:$$$ISERR(tSC) 66 | Set tRow = tRow + 1 67 | Set node = ##class(%DynamicObject).%New() 68 | For c = 1:1:tCols { 69 | Set tVal = tRS.%GetData(c) 70 | 71 | // MODIFICATION IN OVERRIDE: 72 | // To avoid JS errors on clients, insert an "invisible space" into any "" tags specifically. 73 | Set tVal = $Replace(tVal,"","") 74 | Set tVal = $Replace(tVal,"","") 75 | // END MODIFICATION. 76 | 77 | If ($IsValidNum(tVal)) { 78 | Do node.%Set($Get(tColInfo(c,"name")),$Num(tVal),"number") 79 | } 80 | Else { 81 | Do node.%Set($Get(tColInfo(c,"name")),tVal) 82 | } 83 | } 84 | Do arrayNode.%Push(node) 85 | } 86 | 87 | // MODIFICATION IN OVERRIDE: 88 | // Support larger text and avoid old I/O redirection issues by outputting to stream. 89 | Set tStream = ##class(%Stream.TmpCharacter).%New() 90 | Do aet.%ToJSON(.tStream) 91 | Set tSC = tStream.OutputToDevice() 92 | // END MODIFICATION. 93 | } 94 | Catch(ex) { 95 | Write "null" 96 | Set tSC = ex.AsStatus() 97 | } 98 | Quit tSC 99 | } 100 | 101 | } 102 | 103 | -------------------------------------------------------------------------------- /cls/TestCoverage/UI/Component/codeCSS.cls: -------------------------------------------------------------------------------- 1 | /// Component to render CSS styles for all languages supported by %Library.SyntaxColor 2 | /// (Supports using CSS-enabled output mode rather than generating tags.) 3 | Class TestCoverage.UI.Component.codeCSS Extends %ZEN.Component.component [ System = 3 ] 4 | { 5 | 6 | /// This is the XML namespace for this component. 7 | Parameter NAMESPACE = "http://www.intersystems.com/zen/healthshare/test-coverage"; 8 | 9 | /// Generated to provide styles for all supported languages. 10 | Method %DrawHTML() [ CodeMode = objectgenerator ] 11 | { 12 | Do %code.WriteLine($c(9)_"&html<>") 29 | Quit sc 30 | } 31 | 32 | } 33 | 34 | -------------------------------------------------------------------------------- /cls/TestCoverage/UI/Component/dataGrid.cls: -------------------------------------------------------------------------------- 1 | Class TestCoverage.UI.Component.dataGrid Extends %ZEN.Component.dataGrid [ System = 3 ] 2 | { 3 | 4 | /// This is the XML namespace for this component. 5 | Parameter NAMESPACE = "http://www.intersystems.com/zen/healthshare/test-coverage"; 6 | 7 | /// Focus taken from the grid's invisible edit control. 8 | /// Overridden to avoid switching cell focus and consequently scrolling the grid (a major usability annoyance) 9 | ClientMethod gridKeyBlur() [ Language = javascript ] 10 | { 11 | this.hasFocus = false; 12 | } 13 | 14 | } 15 | 16 | -------------------------------------------------------------------------------- /cls/TestCoverage/UI/Component/select.cls: -------------------------------------------------------------------------------- 1 | /// %ZEN.Component.select customized to behave in more expected ways with an SQL data source and parameters 2 | Class TestCoverage.UI.Component.select Extends %ZEN.Component.select [ System = 3 ] 3 | { 4 | 5 | /// This is the XML namespace for this component. 6 | Parameter NAMESPACE = "http://www.intersystems.com/zen/healthshare/test-coverage"; 7 | 8 | /// Set to true (default) to automatically reexecute the query when a parameter changes 9 | Property autoExecute As %ZEN.Datatype.boolean [ InitialExpression = 0 ]; 10 | 11 | /// Overridden to allow zen expressions in initial value. 12 | Method %DrawHTML() 13 | { 14 | Set tSC = ..%BuildValueLists(.tValueList,.tDisplayList) 15 | 16 | If $$$ISERR(tSC) { 17 | Do ##class(%ZEN.Utils).%DisplayErrorHTML($this,tSC) 18 | Quit 19 | } 20 | 21 | #; > 27 | 28 | If (..showEmpty) { 29 | #; empty item for value of "" 30 | &html<> 31 | } 32 | 33 | // Also replaced a bunch of $ListGets with $ListNext, which is faster. 34 | Set tDisplayPointer = 0 35 | Set tValuePointer = 0 36 | While $ListNext(tDisplayList,tDisplayPointer,tDisplayItem) && $ListNext(tValueList,tValuePointer,tValueItem) { 37 | Set tDisplayItem = $$$ZENVAL($Get(tDisplayItem)) 38 | Set tValueItem = $$$ZENVAL($Get(tValueItem)) 39 | &html<> 40 | } 41 | 42 | &html<> 43 | } 44 | 45 | /// Set the value of a named property. 46 | /// Overridden to allow parameters to be set. 47 | ClientMethod setProperty(property, value, value2) [ Language = javascript ] 48 | { 49 | var el = this.findElement('control'); 50 | 51 | switch(property) { 52 | case 'parameters': 53 | if ('' != value) { 54 | value = value - 1; 55 | if (this.parameters[value]) { 56 | if (this.parameters[value].value != value2) { 57 | this.parameters[value].value = value2; 58 | if (this.autoExecute) { this.triggerRefresh(); } 59 | } 60 | } 61 | } 62 | break; 63 | default: 64 | // dispatch 65 | return this.invokeSuper('setProperty',arguments); 66 | } 67 | return true; 68 | } 69 | 70 | ClientMethod triggerRefresh() [ Language = javascript ] 71 | { 72 | this._refreshing = true; 73 | this.setDisabled(true); 74 | this.refreshContents(); 75 | } 76 | 77 | /// This client callback is called just from refreshContents 78 | /// just after the new HTML is delivered from the server. 79 | ClientMethod onRefreshContents() [ Language = javascript ] 80 | { 81 | if (this._refreshing == true) { 82 | this.setDisabled(false); 83 | } 84 | } 85 | 86 | } 87 | 88 | -------------------------------------------------------------------------------- /cls/TestCoverage/UI/Component/testResultsLink.cls: -------------------------------------------------------------------------------- 1 | Class TestCoverage.UI.Component.testResultsLink Extends %ZEN.Component.link [ System = 3 ] 2 | { 3 | 4 | /// This is the XML namespace for this component. 5 | Parameter NAMESPACE = "http://www.intersystems.com/zen/healthshare/test-coverage"; 6 | 7 | /// ID of the instance of TestCoverage.Data.Run for which the associated test results should be shown. 8 | Property coverageRunId As %ZEN.Datatype.string(ZENEXPRESSION = 1); 9 | 10 | /// Text to display for the link.
11 | /// This value is interpreted as text, not HTML.
12 | /// Overridden to add a default. 13 | Property caption As %ZEN.Datatype.caption(ZENEXPRESSION = 1) [ InitialExpression = "View Test Results" ]; 14 | 15 | /// Overridden to default to "_blank" 16 | Property target As %ZEN.Datatype.string [ InitialExpression = "_blank" ]; 17 | 18 | /// Overridden to add ZENSETTING=0, which avoids invoking setProperty after this is 19 | /// changed during a server-side operation; 20 | /// this property is maintained automatically based on the presence and validity of 21 | /// coverageRunId 22 | Property disabled As %ZEN.Datatype.boolean(ZENSETTING = 0) [ InitialExpression = 0 ]; 23 | 24 | Method %DrawHTML() 25 | { 26 | Try { 27 | Set tCoverageRunId = $$$ZENVAL(..coverageRunId) 28 | 29 | Set tResultsExist = 0 30 | If (tCoverageRunId '= "") { 31 | Set tCoverageRun = ##class(TestCoverage.Data.Run).%OpenId(tCoverageRunId,,.tSC) 32 | $$$ThrowOnError(tSC) 33 | If $IsObject(tCoverageRun.TestResults) { 34 | Set tResultsExist = 1 35 | Set tQuery("$NAMESPACE") = $namespace 36 | Set tQuery("Index") = tCoverageRun.TestResults.%Id() 37 | Set ..href = ##class(%CSP.Page).Link("/csp/sys/%25UnitTest.Portal.Indices.zen",.tQuery) 38 | } 39 | } 40 | 41 | Set ..disabled = 'tResultsExist 42 | If 'tResultsExist { 43 | Set ..href = "#" 44 | } 45 | 46 | Do ##super() 47 | } Catch e { 48 | Do ##class(%ZEN.Utils).%DisplayErrorHTML($This,e.AsStatus()) 49 | } 50 | } 51 | 52 | } 53 | 54 | -------------------------------------------------------------------------------- /cls/TestCoverage/UI/ResultDetailViewer.cls: -------------------------------------------------------------------------------- 1 | Class TestCoverage.UI.ResultDetailViewer Extends TestCoverage.UI.Template 2 | { 3 | 4 | Parameter PAGENAME = "Unit Test Coverage - Detail"; 5 | 6 | Property testIndex As %ZEN.Datatype.string(ZENURL = "testIndex"); 7 | 8 | Property codeUnit As %ZEN.Datatype.string(ZENURL = "codeUnit"); 9 | 10 | Property testPath As %ZEN.Datatype.string; 11 | 12 | XData Contents [ XMLNamespace = "http://www.intersystems.com/zen" ] 13 | { 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | 41 |
42 | } 43 | 44 | ClientMethod testFilterChanged(pSrcComponent As %ZEN.Component.select) [ Language = javascript ] 45 | { 46 | var value = pSrcComponent.getValue(); 47 | if (pSrcComponent.id == 'testPath') { 48 | zenPage.testPath = value; 49 | } 50 | 51 | this.showCodeCoverage(); 52 | } 53 | 54 | ClientMethod showCodeCoverage() [ Language = javascript ] 55 | { 56 | zen('coverageDataProvider').reloadContentsAsynch(function() { 57 | zenPage.onloadHandler(); 58 | }); 59 | } 60 | 61 | /// This client event, if present, is fired when the page is loaded. 62 | ClientMethod onloadHandler() [ Language = javascript ] 63 | { 64 | zenPage.renderCodeCoverage('coverageDataProvider','coverageResults'); 65 | } 66 | 67 | } 68 | 69 | -------------------------------------------------------------------------------- /cls/TestCoverage/UI/SimpleResultViewer.cls: -------------------------------------------------------------------------------- 1 | Class TestCoverage.UI.SimpleResultViewer Extends TestCoverage.UI.Template 2 | { 3 | 4 | Parameter PAGENAME = "Unit Test Coverage - Class Viewer"; 5 | 6 | Property testIndex As %ZEN.Datatype.string(ZENURL = "testIndex"); 7 | 8 | Property codeUnit As %ZEN.Datatype.string(ZENURL = "codeUnit"); 9 | 10 | Property testPath As %ZEN.Datatype.string; 11 | 12 | XData Contents [ XMLNamespace = "http://www.intersystems.com/zen" ] 13 | { 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
53 | 54 |
55 | } 56 | 57 | ClientMethod testFilterChanged(pSrcComponent As %ZEN.Component.select) [ Language = javascript ] 58 | { 59 | zenPage[pSrcComponent.id] = pSrcComponent.getValue(); 60 | 61 | if (pSrcComponent.id == 'testIndex') { 62 | zen('testPath').setValue(''); 63 | zenPage.testPath = ''; 64 | } 65 | 66 | zen('testPath').triggerRefresh(); 67 | zen('codeCovered').triggerRefresh(); 68 | 69 | if (zen('codeCovered').getValue() != '') { 70 | this.showCodeCoverage(); 71 | } 72 | } 73 | 74 | ClientMethod showCodeCoverage() [ Language = javascript ] 75 | { 76 | zenPage.codeUnit = zen('codeCovered').getValue(); 77 | zen('coverageDataProvider').reloadContentsAsynch(function() { 78 | zenPage.renderCodeCoverage('coverageDataProvider','coverageResults'); 79 | }); 80 | } 81 | 82 | } 83 | 84 | -------------------------------------------------------------------------------- /cls/TestCoverage/UI/Template.cls: -------------------------------------------------------------------------------- 1 | Class TestCoverage.UI.Template Extends %ZEN.Component.page [ Abstract ] 2 | { 3 | 4 | Parameter APPLICATION As CLASSNAME = "TestCoverage.UI.Application"; 5 | 6 | XData CSSPane [ XMLNamespace = "http://www.intersystems.com/zen" ] 7 | { 8 | 9 | 10 | 11 | } 12 | 13 | XData TogglePane [ XMLNamespace = "http://www.intersystems.com/zen" ] 14 | { 15 | 16 | 17 | 18 | } 19 | 20 | ClientMethod toggleCovered(pVisible) [ Language = javascript ] 21 | { 22 | $("pre.coverage span.covered").toggleClass("hide",!pVisible); 23 | } 24 | 25 | ClientMethod buildCodeHTML(targetElement, codeLines) [ Language = javascript ] 26 | { 27 | // Remove all children from the target to make the subsequent set of innerHTML faster. 28 | while (targetElement.firstChild) { 29 | targetElement.removeChild(targetElement.firstChild); 30 | } 31 | var html = new Array(); 32 | var showCovered = zen('markCovered').getValue() 33 | html.push('
\r\n');
34 | 	for (var i = 0; i < codeLines.length; i++) {
35 | 		var classes = new Array();
36 | 		if (codeLines[i].Executable) classes.push("executable");
37 | 		if (codeLines[i].Covered) {
38 | 			classes.push("covered");
39 | 			if (!showCovered) classes.push("hide");
40 | 		}
41 | 		var line = ''+codeLines[i].ColoredHTML+'\r\n';
42 | 		html.push(line);
43 | 	}
44 | 	html.push('
'); 45 | targetElement.innerHTML = html.join(''); 46 | } 47 | 48 | ClientMethod renderCodeCoverage(providerID, htmlID) [ Language = javascript ] 49 | { 50 | var code = zen(providerID).getContentObject().children; 51 | zenPage.buildCodeHTML(document.getElementById(htmlID),code); 52 | } 53 | 54 | } 55 | 56 | -------------------------------------------------------------------------------- /cls/TestCoverage/UI/Utils.cls: -------------------------------------------------------------------------------- 1 | Class TestCoverage.UI.Utils 2 | { 3 | 4 | Query ColoredText(pTestIndex As %String, pCodeUnit As %String, pTestPath As %String = "") As %Query(ROWSPEC = "PlainText:%String,ColoredHTML:%String,Covered:%Boolean,Executable:%Boolean,LineNumber:%Integer") [ SqlProc ] 5 | { 6 | } 7 | 8 | ClassMethod ColoredTextExecute(ByRef qHandle As %Binary, pTestIndex As %String, pCodeUnit As %String, pTestPath As %String = "") As %Status 9 | { 10 | // The initial implementation of this class query used a process-private global. 11 | // It is faster to use local variables, and memory constraints should always be well out-of-reach for these. 12 | // Passing everything in qHandle also has a significant performance hit on method dispatch. 13 | #def1arg TempStorage %TempColoredText 14 | 15 | // Clean up TempStorage in case another query in the same process failed to. 16 | Kill $$$TempStorage 17 | Set qHandle = "" 18 | Set tSC = $$$OK 19 | Try { 20 | If '##class(TestCoverage.Data.CodeUnit).%ExistsId(pCodeUnit) { 21 | Quit 22 | } 23 | 24 | Set tCodeUnit = ##class(TestCoverage.Data.CodeUnit).%OpenId(pCodeUnit,,.tSC) 25 | $$$ThrowOnError(tSC) 26 | 27 | Set tSQL = "select CoveredLines from TestCoverage_Data.Coverage where Run = ? and Hash = ?" 28 | Set tArgs($i(tArgs)) = pTestIndex 29 | Set tArgs($i(tArgs)) = pCodeUnit 30 | If (pTestPath '= "") { 31 | Set tSQL = tSQL_" and coverage.TestPath = ?" 32 | Set tArgs($i(tArgs)) = pTestPath 33 | } 34 | Set tResult = ##class(%SQL.Statement).%ExecDirect(,tSQL,tArgs...) 35 | If (tResult.%SQLCODE < 0) { 36 | Throw ##class(%Exception.SQL).CreateFromSQLCODE(tResult.%SQLCODE, tResult.%Message) 37 | } 38 | 39 | // Aggregate CoveredLines (bitwise OR) 40 | Set tCoveredLines = "" 41 | While tResult.%Next(.tSC) { 42 | $$$ThrowOnError(tSC) 43 | Set tNextCoveredLines = tResult.%Get("CoveredLines") 44 | Set tCoveredLines = $BitLogic(tCoveredLines|tNextCoveredLines) 45 | } 46 | $$$ThrowOnError(tSC) 47 | 48 | // Mask by only treating "executable" lines as covered 49 | Set tCoveredLines = $BitLogic(tCoveredLines&tCodeUnit.ExecutableLines) 50 | 51 | // Create code stream and stash line data 52 | Set tCodeStream = ##class(%GlobalCharacterStream).%New() 53 | For tLineNumber=1:1:tCodeUnit.Lines.Count() { 54 | Set tText = tCodeUnit.Lines.GetAt(tLineNumber) 55 | Do tCodeStream.WriteLine(tText) 56 | Set $$$TempStorage($Increment($$$TempStorage)) = $ListBuild(tText,tText,$Bit(tCoveredLines,tLineNumber),$Bit(tCodeUnit.ExecutableLines,tLineNumber),tLineNumber) 57 | } 58 | 59 | // Color the code stream. 60 | Set tColoredStream = ##class(%GlobalCharacterStream).%New() 61 | Set tColorer = ##class(%Library.SyntaxColor).%New() 62 | Set tLanguage = $Case($ZConvert(tCodeUnit.Type,"L"),"cls":"CLS","int":"MAC","inc":"INC",:"COS") 63 | Set tFlags = "PFES"_$Case(tLanguage,"CLS":"X",:"") 64 | Set tGood = tColorer.Color(tCodeStream,tColoredStream,tLanguage,tFlags,,,,.tColoringErrors,.tErrorEnv,.tColoringWarnings) 65 | If tGood { 66 | For tLineNumber=1:1 { 67 | Set tColoredLine = tColoredStream.ReadLine(,.tSC) 68 | $$$ThrowOnError(tSC) 69 | If (tColoredStream.AtEnd) { 70 | Quit 71 | } 72 | 73 | // Sometimes there are random extra lines inserted. Detect these by looking for a colored line length shorter 74 | // than the non-colored line. 75 | Set tRawLine = $ListGet($$$TempStorage(tLineNumber)) 76 | If ($Length(tColoredLine) < $Length(tRawLine)) && $Increment(tLineNumber,-1) { 77 | Continue 78 | } 79 | 80 | // Remove line breaks 81 | Set tColoredLine = $Replace(tColoredLine,"
","") 82 | Set $List($$$TempStorage(tLineNumber),2) = tColoredLine 83 | } 84 | } 85 | } Catch e { 86 | Set tSC = e.AsStatus() 87 | } 88 | Quit tSC 89 | } 90 | 91 | ClassMethod ColoredTextFetch(ByRef qHandle As %Binary, ByRef Row As %List, ByRef AtEnd As %Integer = 0) As %Status [ PlaceAfter = ColoredTextExecute ] 92 | { 93 | #def1arg TempStorage %TempColoredText 94 | Set qHandle = $Order($$$TempStorage(qHandle),1,Row) 95 | If (qHandle = "") { 96 | Set AtEnd = 1 97 | } 98 | Quit $$$OK 99 | } 100 | 101 | ClassMethod ColoredTextClose(ByRef qHandle As %Binary) As %Status [ PlaceAfter = ColoredTextExecute ] 102 | { 103 | #def1arg TempStorage %TempColoredText 104 | Kill $$$TempStorage 105 | Kill qHandle 106 | Quit $$$OK 107 | } 108 | 109 | } 110 | 111 | -------------------------------------------------------------------------------- /cls/TestCoverage/Utils.cls: -------------------------------------------------------------------------------- 1 | Include (%occInclude, TestCoverage) 2 | 3 | IncludeGenerator TestCoverage 4 | 5 | Class TestCoverage.Utils 6 | { 7 | 8 | /// Removes all data for test coverage, code snapshots. Does not enforce referential integrity (for the sake of speed). 9 | /// @API.Method 10 | ClassMethod Clear() As %Status 11 | { 12 | Set tSC = $$$OK 13 | Try { 14 | Set tClasses("TestCoverage.Data.Aggregate.ByRun") = "" 15 | Set tClasses("TestCoverage.Data.Aggregate.ByCodeUnit") = "" 16 | Set tClasses("TestCoverage.Data.CodeSubUnit") = "" 17 | Set tClasses("TestCoverage.Data.CodeUnitMap") = "" 18 | Set tClasses("TestCoverage.Data.CodeUnit") = "" 19 | Set tClasses("TestCoverage.Data.Coverage") = "" 20 | Set tClasses("TestCoverage.Data.Run") = "" 21 | kill $$$PyMonitorResults 22 | 23 | Set tClass = "" 24 | For { 25 | Set tClass = $Order(tClasses(tClass)) 26 | If (tClass = "") { 27 | Quit 28 | } 29 | Set tOneSC = $ClassMethod(tClass,"%KillExtent") 30 | Set tSC = $$$ADDSC(tSC,tOneSC) 31 | } 32 | } Catch e { 33 | Set tSC = e.AsStatus() 34 | } 35 | Quit tSC 36 | } 37 | 38 | /// Grants SQL SELECT permissions on all TestCoverage tables for the specified username/role 39 | /// @API.Method 40 | ClassMethod GrantSQLReadPermissions(pUsernameOrRole As %String) As %Status 41 | { 42 | Set tSC = $$$OK 43 | Try { 44 | Set tTableList = ..GetTestCoverageTableList() 45 | $$$ThrowOnError($System.SQL.GrantObjPriv("SELECT",$ListToString(tTableList),"TABLE",pUsernameOrRole)) 46 | } Catch e { 47 | Set tSC = e.AsStatus() 48 | } 49 | Quit tSC 50 | } 51 | 52 | /// Revokes SQL SELECT permissions on all TestCoverage tables for the specified username/role 53 | /// @API.Method 54 | ClassMethod RevokeSQLReadPermissions(pUsernameOrRole As %String) As %Status 55 | { 56 | Set tSC = $$$OK 57 | Try { 58 | Set tTableList = ..GetTestCoverageTableList() 59 | $$$ThrowOnError($System.SQL.RevokeObjPriv("SELECT",$ListToString(tTableList),"TABLE",pUsernameOrRole)) 60 | } Catch e { 61 | Set tSC = e.AsStatus() 62 | } 63 | Quit tSC 64 | } 65 | 66 | ClassMethod GetTestCoverageTableList() As %List 67 | { 68 | Set tSC = $$$OK 69 | &sql(select %DLIST(TABLE_SCHEMA || '.' || TABLE_NAME) into :tList from INFORMATION_SCHEMA.TABLES where TABLE_SCHEMA %STARTSWITH 'TestCoverage') 70 | If (SQLCODE < 0) { 71 | Throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) 72 | } ElseIf (SQLCODE = 100) { 73 | Set tList = "" 74 | } 75 | Quit tList 76 | } 77 | 78 | /// Given pIntRoutines, a $ListBuild list of .INT routine names, creates snapshots of the current state of the code of each.
79 | /// This is parallelized using %SYSTEM.WorkMgr for better performance.
80 | /// pRelevantRoutines is a $ListBuild list of .INT routines that map back to a .CLS or .MAC 81 | /// routine with at least one executable line. 82 | ClassMethod Snapshot(pIntRoutines As %List, pPyRoutines As %List, Output pRelevantRoutines As %List = "", Output pPyRelevantRoutines As %List = "") As %Status 83 | { 84 | Set tSC = $$$OK 85 | Try { 86 | #dim tSnapshotQueue As %SYSTEM.WorkMgr 87 | Set tSnapshotQueue = $System.WorkMgr.Initialize(,.tSC) 88 | $$$ThrowOnError(tSC) 89 | 90 | Set tPointer = 0 91 | While $ListNext(pIntRoutines,tPointer,tIntRoutine) { 92 | Set tSC = tSnapshotQueue.Queue("##class(TestCoverage.Data.CodeUnit).GetCurrentByName",tIntRoutine_".INT") 93 | $$$ThrowOnError(tSC) 94 | } 95 | 96 | Set tSC = tSnapshotQueue.WaitForComplete() 97 | $$$ThrowOnError(tSC) 98 | 99 | 100 | 101 | // See which routines are actually relevant (one or more lines mapping back to a class with 1 or more executable lines) 102 | // There's no point in optimizing out .MAC routines; they'll always have code 103 | Set tPointer = 0 104 | While $ListNext(pIntRoutines,tPointer,tIntRoutine) { 105 | Set tOther = ##class(%Library.RoutineMgr).GetOther(tIntRoutine,"INT",-1) 106 | If (tOther '= "") && ($Piece(tOther,".",*) = "CLS") { 107 | // With the code already cached, this will be faster. 108 | // This also snapshots the compiled python routine with it if there is one 109 | #dim tCodeUnit As TestCoverage.Data.CodeUnit 110 | Set tSC = ##class(TestCoverage.Data.CodeUnit).GetCurrentByName(tOther,,.tCodeUnit) 111 | 112 | If $$$ISERR(tSC) { 113 | Continue // Non-fatal. Just skip it. 114 | } 115 | Set tName = tCodeUnit.Name // should be the same as tOther without the .cls, but if I have it already why not 116 | Set SnapshottedClasses(tName) = 1 117 | If ##class(TestCoverage.Manager).HasPython(tName) { 118 | // take a snapshot of the compiled python file 119 | $$$ThrowOnError(##class(TestCoverage.Data.CodeUnit).GetCurrentByName(tName_".PY",,.tPyCodeUnit)) 120 | 121 | // update the executable lines for the .cls file's python 122 | $$$ThrowOnError(tCodeUnit.UpdatePyExecutableLines(tName, .tPyCodeUnit)) 123 | 124 | // update the pythonicity of the lines for the .cls file 125 | $$$ThrowOnError(tCodeUnit.UpdatePythonLines(tName, .tPyCodeUnit)) 126 | 127 | // update the relevant python routines 128 | If ($BitCount(tPyCodeUnit.ExecutableLines, 1)) { 129 | set pPyRelevantRoutines = pPyRelevantRoutines _ $ListBuild(tName) 130 | } ElseIf '$BitCount(tCodeUnit.ExecutableLines,1) { 131 | // if there's no executable python and no executable objectscript, skip it 132 | Continue 133 | } 134 | 135 | } ElseIf '$BitCount(tCodeUnit.ExecutableLines,1) { 136 | // Skip it - no executable lines. 137 | Continue 138 | } 139 | } 140 | 141 | Set pRelevantRoutines = pRelevantRoutines _ $ListBuild(tIntRoutine) 142 | } 143 | 144 | // Snapshot all the python routines and their corresponding classes that haven't already been snapshotted 145 | Set tPointer = 0 146 | While $ListNext(pPyRoutines, tPointer, tPyRoutine) { 147 | If ('$Data(SnapshottedClasses(tPyRoutine))) { 148 | $$$ThrowOnError(##class(TestCoverage.Data.CodeUnit).GetCurrentByName(tPyRoutine_".CLS",,.tCodeUnit)) 149 | $$$ThrowOnError(##class(TestCoverage.Data.CodeUnit).GetCurrentByName(tPyRoutine_".PY",,.tPyCodeUnit)) 150 | $$$ThrowOnError(tCodeUnit.UpdatePyExecutableLines(tPyRoutine, .tPyCodeUnit)) 151 | $$$ThrowOnError(tCodeUnit.UpdatePythonLines(tPyRoutine, .tPyCodeUnit)) 152 | 153 | If ($BitCount(tPyCodeUnit.ExecutableLines, 1)) { 154 | set pPyRelevantRoutines = pPyRelevantRoutines _ $ListBuild(tPyRoutine) 155 | } 156 | Set SnapshottedClasses(tPyRoutine) = 1 157 | } 158 | } 159 | 160 | Write ! 161 | } Catch e { 162 | Set tSC = e.AsStatus() 163 | } 164 | Quit tSC 165 | } 166 | 167 | /// Returns the "TestPath" string used to represent coverage collected at the test coverage run level. 168 | /// In deployed mode, TestCoverage.inc does not exist, but dynamic SQL against persistent classes that include 169 | /// it will try to include it in the generated class. This effectively makes the macro available as a method instead. 170 | ClassMethod GetTestPathAllTests() [ CodeMode = objectgenerator ] 171 | { 172 | Do %code.WriteLine(" Quit "_$$$QUOTE($$$TestPathAllTests)) 173 | Quit $$$OK 174 | } 175 | 176 | /// Aggregates coverage results for test coverage run pTestIndex 177 | ClassMethod AggregateCoverage(pTestIndex As %String) As %Status 178 | { 179 | Set tSC = $$$OK 180 | Set tInitTLevel = $TLevel 181 | Try { 182 | Set tRun = ##class(TestCoverage.Data.Run).%OpenId(pTestIndex,,.tSC) 183 | $$$ThrowOnError(tSC) 184 | TSTART 185 | &sql(delete %NOLOCK %NOCHECK from TestCoverage_Data_Aggregate.ByRun where Run = :pTestIndex) 186 | If (SQLCODE < 0) { 187 | Throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) 188 | } 189 | &sql(delete %NOLOCK %NOCHECK from TestCoverage_Data_Aggregate.ByCodeUnit where Run = :pTestIndex) 190 | If (SQLCODE < 0) { 191 | Throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) 192 | } 193 | 194 | // Rollup: TestCoverage.Data.Coverage at 'all tests' level 195 | Set tRollupTestPath = ..GetTestPathAllTests() 196 | Set tRollupCalculated = (tRun.Detail > 0) 197 | If tRollupCalculated { 198 | &sql(delete %NOLOCK %NOCHECK from TestCoverage_Data.Coverage where Run = :pTestIndex and Calculated = 1) 199 | If (SQLCODE < 0) { 200 | Throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) 201 | } 202 | &sql( 203 | insert %NOLOCK %NOCHECK into TestCoverage_Data.Coverage ( 204 | Run,TestPath,Hash,Calculated,Ignore, 205 | CoveredLines) 206 | select :pTestIndex,:tRollupTestPath,Hash,1,0, 207 | MAX(CoveredLines) 208 | from TestCoverage_Data.Coverage 209 | where Run = :pTestIndex 210 | and Hash->Type in ('CLS','MAC') 211 | and Ignore = 0 212 | and Calculated = 0 213 | group by Hash 214 | ) 215 | If (SQLCODE < 0) { 216 | Throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) 217 | } 218 | 219 | // Copy any other metrics captured/requested as well. 220 | For i=1:1:tRun.Metrics.Count() { 221 | Set tMetric = tRun.Metrics.GetAt(i) 222 | If $System.SQL.IsReservedWord(tMetric) { 223 | // e.g., "Time" -> "_Time" 224 | Set tMetric = "_"_tMetric 225 | } 226 | Set tSQLStatement = "INSERT %NOLOCK %NOCHECK INTO TestCoverage_Data.Coverage_"_tMetric_" "_ 227 | "(Coverage,element_key,"_tMetric_") "_ 228 | "SELECT target.ID,metric.element_key,SUM(metric."_tMetric_") "_ 229 | "FROM TestCoverage_Data.Coverage source "_ 230 | "JOIN TestCoverage_Data.Coverage_"_tMetric_" metric "_ 231 | " ON metric.Coverage = source.ID "_ 232 | "JOIN TestCoverage_Data.Coverage target "_ 233 | " ON target.Hash = source.Hash "_ 234 | " AND target.Run = source.Run "_ 235 | "WHERE source.Run = ? "_ 236 | " AND source.Ignore = 0"_ 237 | " AND source.Calculated = 0"_ 238 | " AND target.TestPath = ?"_ 239 | "GROUP BY target.ID,metric.element_key" 240 | 241 | #dim tResult As %SQL.StatementResult 242 | Set tResult = ##class(%SQL.Statement).%ExecDirect(,tSQLStatement,pTestIndex,tRollupTestPath) 243 | If (tResult.%SQLCODE < 0) { 244 | Throw ##class(%Exception.SQL).CreateFromSQLCODE(tResult.%SQLCODE,tResult.%Message) 245 | } 246 | } 247 | } 248 | 249 | // Aggregate by code unit 250 | &sql( 251 | insert %NOLOCK %NOCHECK into TestCoverage_Data_Aggregate.ByCodeUnit ( 252 | Run, 253 | CodeUnit, 254 | ExecutableLines, 255 | CoveredLines, 256 | ExecutableMethods, 257 | CoveredMethods, 258 | RtnLine, 259 | _Time, 260 | TotalTime) 261 | select :pTestIndex, 262 | CodeUnit, 263 | TestCoverage.BIT_COUNT(ExecutableLines,1), -- Count of executable lines 264 | TestCoverage.BIT_COUNT(CoveredLines,1), -- Count of lines that were covered 265 | 266 | -- Count of executable methods: 267 | -- These have at least one executable line 268 | NVL((select SUM(CASE 269 | TestCoverage.BIT_COUNT(TestCoverage.BITWISE_AND( 270 | method.Mask,ExecutableLines),1) 271 | WHEN 0 THEN 0 272 | ELSE 1 END) 273 | from TestCoverage_Data_CodeSubUnit.Method method 274 | where method.Parent = CodeUnit),0) ExecutableMethods, 275 | 276 | -- Count of covered methods: 277 | -- These have at least one line that was covered 278 | NVL((select SUM(CASE 279 | TestCoverage.BIT_COUNT(TestCoverage.BITWISE_AND( 280 | method.Mask,CoveredLines),1) 281 | WHEN 0 THEN 0 282 | ELSE 1 END) 283 | from TestCoverage_Data_CodeSubUnit.Method method 284 | where method.Parent = CodeUnit),0) CoveredMethods, 285 | 286 | -- Other metrics 287 | RtnLine, _Time, TotalTime 288 | from ( 289 | select Hash CodeUnit, 290 | Hash->ExecutableLines ExecutableLines, 291 | TestCoverage.BITWISE_AND(Hash->ExecutableLines,CoveredLines) CoveredLines, 292 | (select SUM(RtnLine) 293 | from TestCoverage_Data.Coverage_RtnLine r 294 | where r.Coverage = coverage.ID) RtnLine, 295 | (select SUM(_Time) 296 | from TestCoverage_Data.Coverage__Time t 297 | where t.Coverage = coverage.ID) _Time, 298 | (select SUM(TotalTime) 299 | from TestCoverage_Data.Coverage_TotalTime tt 300 | where tt.Coverage = coverage.ID) TotalTime 301 | from TestCoverage_Data.Coverage coverage 302 | where Run = :pTestIndex 303 | and Calculated = :tRollupCalculated 304 | and Ignore = 0 305 | and Hash->Type in ('CLS','MAC') 306 | and TestPath = :tRollupTestPath -- This is guaranteed to exist, so optimize by using it rather than aggregating. 307 | 308 | -- Supported by index: 309 | -- Index MeaningfulCoverageData On (Run, Calculated, Ignore, Hash, TestPath) [ Data = CoveredLines, Unique ]; 310 | ) 311 | order by CodeUnit 312 | ) 313 | If (SQLCODE < 0) { 314 | Throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) 315 | } 316 | 317 | // Aggregate for full run 318 | &sql( 319 | insert %NOLOCK %NOCHECK into TestCoverage_Data_Aggregate.ByRun 320 | (Run, ExecutableLines ,CoveredLines, 321 | ExecutableMethods, CoveredMethods, 322 | RtnLine, _Time, TotalTime) 323 | select Run, SUM(ExecutableLines), SUM(CoveredLines), 324 | SUM(ExecutableMethods), SUM(CoveredMethods), 325 | SUM(RtnLine), SUM(_Time), SUM(TotalTime) 326 | from TestCoverage_Data_Aggregate.ByCodeUnit 327 | where Run = :pTestIndex 328 | group by Run 329 | ) 330 | If (SQLCODE < 0) { 331 | Throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) 332 | } 333 | TCOMMIT 334 | } Catch e { 335 | Set tSC = e.AsStatus() 336 | } 337 | While ($TLevel > tInitTLevel) { 338 | TROLLBACK 1 339 | } 340 | Quit tSC 341 | } 342 | 343 | /// Returns the test coverage measured for pRunIndex, as a percentage, or an empty string if no data was found. 344 | ClassMethod GetAggregateCoverage(pRunIndex As %Integer) As %Numeric 345 | { 346 | Set tCoveragePercent = "" 347 | &sql(select ROUND((SUM(CoveredLines)/SUM(ExecutableLines))*100,2) into :tCoveragePercent 348 | from TestCoverage_Data_Aggregate.ByRun where ExecutableLines > 0 and Run = :pRunIndex) 349 | If (SQLCODE < 0) { 350 | Throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) 351 | } ElseIf (SQLCODE = 100) { 352 | Set tCoveragePercent = "" 353 | } 354 | Quit tCoveragePercent 355 | } 356 | 357 | /// Displays aggregate coverage for pRunIndex 358 | ClassMethod ShowAggregateCoverage(pRunIndex As %String) 359 | { 360 | Set tCoveragePercent = ..GetAggregateCoverage(pRunIndex) 361 | If (tCoveragePercent = "") { 362 | Write !,"No code coverage found (!)" 363 | } Else { 364 | Write !,"Code coverage: ",tCoveragePercent,"%" 365 | } 366 | Write ! 367 | } 368 | 369 | /// For a class, pClassName, with code in pDocumentText as an integer-subscripted array of lines, 370 | /// returns an array (pExecutableFlags) subscripted by line with boolean flags indicating whether the corresponding line is executable. 371 | ClassMethod GetClassLineExecutableFlags(pClassName As %String, ByRef pDocumentText, Output pExecutableFlags) 372 | { 373 | // Now process the class itself. 374 | Set tInMethod = 0 375 | Set tMethodStarted = 0 376 | For tDocLine=1:1:$Get(pDocumentText) { 377 | Set tLine = pDocumentText(tDocLine) 378 | If 'tInMethod { 379 | Set pExecutableFlags(tDocLine) = 0 380 | 381 | // Extract line offset of method in class 382 | Set tStart = $Extract(tLine,1,6) 383 | If (tStart = "ClassM") || (tStart = "Method") { 384 | Set tMethod = $Piece($Piece(tLine,"(")," ",2) 385 | Kill tMethodCode 386 | Set tInMethod = 1 387 | } 388 | } Else { 389 | If $Extract(tLine) = "{" { 390 | Set tMethodStarted = 1 391 | Set tMethodMap(tMethod) = tDocLine + 1 392 | Set pExecutableFlags(tDocLine) = 0 393 | } ElseIf $Extract(tLine) = "}" { 394 | Set tInMethod = 0 395 | Set tMethodStarted = 0 396 | Set pExecutableFlags(tDocLine) = 0 397 | 398 | Set tCodeMode = $$$defMemberKeyGet(pClassName,$$$cCLASSmethod,tMethod,$$$cMETHcodemode) 399 | If $Data(^rMAP(pClassName,"CLS","INT",tMethod)) || (tCodeMode = $$$cMETHCODEMODEGENERATOR) || (tCodeMode = $$$cMETHCODEMODEOBJECTGENERATOR) { 400 | Set tSourceStream = ##class(%Stream.GlobalCharacter).%New() 401 | Set tSourceStream.LineTerminator = $c(13,10) 402 | For tMethodLine=1:1:$Get(tMethodCode) { 403 | Do tSourceStream.WriteLine(tMethodCode(tMethodLine)) 404 | } 405 | 406 | Set tSC = ##class(%Library.SyntaxColorReader).FromCode(tSourceStream,"COS","A",.tSCReader) 407 | $$$ThrowOnError(tSC) 408 | 409 | Set tOffset = tMethodMap(tMethod) 410 | Set tLine = 0 411 | While tSCReader.NextLine(.tLineTokenList) { 412 | If (tLine = 0) && (tCodeMode = $$$cMETHCODEMODEEXPRESSION) { 413 | // Simulate a normal method. 414 | Set tLineTokenList = $ListBuild($ListBuild("COS","Command","Quit"))_tLineTokenList 415 | } 416 | Set pExecutableFlags(tOffset + tLine) = ..LineIsExecutable(tLineTokenList,.tPreviousLineWasExecutable) 417 | If (tPreviousLineWasExecutable) { 418 | Set pExecutableFlags(tOffset + tLine - 1) = pExecutableFlags(tOffset + tLine-1) || tPreviousLineWasExecutable 419 | } 420 | Set tLine = tLine + 1 421 | } 422 | } Else { 423 | // Method is not executable. 424 | Set tOffset = tMethodMap(tMethod) 425 | For tMethodLine = 1:1:$Get(tMethodCode) { 426 | Set pExecutableFlags(tOffset + tMethodLine) = 0 427 | } 428 | } 429 | } ElseIf tMethodStarted { 430 | // Aggregate lines from the method body to look at later. 431 | Set tMethodCode($i(tMethodCode)) = tLine 432 | } 433 | } 434 | } 435 | } 436 | 437 | ClassMethod CodeArrayToList(ByRef pCodeArray, Output pDocumentText As %List) 438 | { 439 | set pDocumentText = "" 440 | for i=1:1:$get(pCodeArray(0)) { 441 | set pDocumentText = pDocumentText _ $ListBuild(pCodeArray(i)) 442 | } 443 | quit 444 | } 445 | 446 | /// returns a python tuple of (line to method info, method map info) 447 | /// linetomethodinfo: a python builtins list where the item at index i is the name of the method that line i is a part of 448 | /// methodmapinfo: a python builtins dict with key = method name, value = the line number of its definition 449 | ClassMethod GetPythonMethodMapping(pDocumentText) [ Language = python ] 450 | { 451 | import iris 452 | import ast 453 | source_lines = iris.cls('%SYS.Python').ToList(pDocumentText) 454 | 455 | source = '\n'.join(source_lines) 456 | tree = ast.parse(source) 457 | line_function_map = [None] * (len(source_lines)+2) 458 | method_map = {} # dictionary from the method name to its start and ending line number 459 | 460 | class FunctionMapper(ast.NodeVisitor): 461 | def __init__(self): 462 | self.current_class = None 463 | self.current_function = None 464 | self.outermost_function = None # for objectscript purposes, we only care about the outer level functions/methods 465 | 466 | def visit_ClassDef(self, node): 467 | prev_class = self.current_class 468 | self.current_class = node.name 469 | self.generic_visit(node) 470 | self.current_class = prev_class 471 | 472 | def visit_FunctionDef(self, node): 473 | if self.outermost_function is None: 474 | self.outermost_function = node.name 475 | start_line = node.lineno 476 | end_line = self.get_end_line(node) 477 | method_map[node.name] = (start_line, end_line) 478 | 479 | self.current_function = node.name 480 | for lineno in range(node.lineno, self.get_end_line(node) + 1): 481 | line_function_map[lineno-1] = self.outermost_function 482 | 483 | self.generic_visit(node) 484 | self.current_function = None 485 | if self.outermost_function == node.name: 486 | self.outermost_function = None 487 | @staticmethod 488 | def get_end_line(node): 489 | return max(child.lineno for child in ast.walk(node) if hasattr(child, 'lineno')) 490 | 491 | FunctionMapper().visit(tree) 492 | return (line_function_map, method_map) 493 | } 494 | 495 | /// returns a python list with a 1 or 0 for subscript i indicating if line i is executable or not 496 | ClassMethod GetPythonLineExecutableFlags(pDocumentText) [ Language = python ] 497 | { 498 | import iris 499 | import ast 500 | source_lines = iris.cls('%SYS.Python').ToList(pDocumentText) 501 | source_lines = [line + "\n" for line in source_lines] # contains a list of each line of the source code 502 | 503 | # create the abstract syntax tree for the code, and walk through it, getting each line of code in its context 504 | source = ''.join(source_lines) 505 | tree = ast.parse(source) 506 | executable_lines = set() # stores the 1-indexed line numbers of the executable lines 507 | 508 | class ExecutableLineVisitor(ast.NodeVisitor): 509 | def __init__(self): 510 | self.function_depth = 0 511 | 512 | def visit(self, node): 513 | if hasattr(node, 'lineno'): 514 | 515 | # decorators for functions and class definitions are executable 516 | if isinstance(node, (ast.FunctionDef, ast.ClassDef, ast.AsyncFunctionDef)): 517 | decorators = [element.id for element in node.decorator_list] 518 | num_decorators = len(decorators) 519 | for i, element in enumerate(decorators): 520 | conjectured_line = (node.lineno-1)-num_decorators+i # change this back if the line numbers aren't 0 indexed 521 | if "@" + element in source_lines[conjectured_line]: 522 | executable_lines.add(conjectured_line+1) # because back to 1-indexing 523 | executable_lines.add(node.lineno) 524 | elif isinstance(node, (ast.Call, 525 | ast.Return, ast.Assign, ast.AugAssign, ast.AnnAssign, 526 | ast.For, ast.AsyncFor, ast.While, ast.If, ast.With, 527 | ast.AsyncWith, ast.Raise, ast.Try, ast.Assert, 528 | ast.Import, ast.ImportFrom, ast.Pass, 529 | ast.Break, ast.Continue, ast.Delete, ast.Yield, 530 | ast.YieldFrom, ast.Await, ast.Nonlocal)): # all executable (determined manually) 531 | executable_lines.add(node.lineno) 532 | elif isinstance(node, ast.ExceptHandler): # except (but not finally) is executable 533 | executable_lines.add(node.lineno) 534 | elif isinstance(node, ast.Expr) and not isinstance(node.value, ast.Constant): # expressions that aren't docstrings are executable 535 | executable_lines.add(node.lineno) 536 | self.generic_visit(node) 537 | ExecutableLineVisitor().visit(tree) 538 | 539 | output = [0] * (len(source_lines)+1) 540 | for line in executable_lines: 541 | output[line] = 1 542 | output[1] = 0 # manually set the class definition to be not executable 543 | def print_executable_lines(): 544 | for i, line in enumerate(source_lines, start=1): 545 | is_exec = output[i] 546 | print(f"{i:2d} {'*' if is_exec else ' '} {line.rstrip()}") 547 | # print_executable_lines() 548 | return output 549 | } 550 | 551 | /// For a routine (.MAC/.INT) with code in pDocumentText as an integer-subscripted array of lines, 552 | /// returns an array (pExecutableFlags) subscripted by line with boolean flags indicating whether the corresponding line is executable. 553 | ClassMethod GetRoutineLineExecutableFlags(ByRef pDocumentText, Output pExecutableFlags) 554 | { 555 | Set tSourceStream = ##class(%Stream.GlobalCharacter).%New() 556 | Set tSourceStream.LineTerminator = $c(13,10) 557 | For tDocLine=1:1:$Get(pDocumentText) { 558 | Do tSourceStream.WriteLine(pDocumentText(tDocLine)) 559 | } 560 | Set tSC = ##class(%Library.SyntaxColorReader).FromCode(tSourceStream,"COS","A",.tSCReader) 561 | $$$ThrowOnError(tSC) 562 | 563 | Set tLine = 1 564 | While tSCReader.NextLine(.tLineTokenList) { 565 | Set pExecutableFlags(tLine) = ..LineIsExecutable(tLineTokenList,.tPreviousLineWasExecutable) 566 | If (tPreviousLineWasExecutable) { 567 | Set pExecutableFlags(tLine-1) = 1 568 | } 569 | Set tLine = tLine + 1 570 | } 571 | } 572 | 573 | /// Given pLineTokenList with a list of tokens from a single line (from %Library.SyntaxColorReader), 574 | /// returns 1 if the line is "executable" (meaning the line-by-line monitor will likely be able to detect whether it was executed) or not. 575 | /// In certain edge cases, pPreviousLineWasExecutable may be set to 1 to correct the previous line's result. 576 | ClassMethod LineIsExecutable(pLineTokenList As %List, Output pPreviousLineWasExecutable As %Boolean) As %Boolean [ Private ] 577 | { 578 | Set tExecutable = 0 579 | Set tPointer = 0 580 | Set pPreviousLineWasExecutable = 0 581 | While $ListNext(pLineTokenList,tPointer,tLineToken) { 582 | Set tExecutable = tExecutable || ..LineTokenIsExecutable(tLineToken, .tLineState, .pPreviousLineWasExecutable) 583 | If (tExecutable) { 584 | Quit 585 | } 586 | } 587 | Quit tExecutable 588 | } 589 | 590 | /// Given pLineToken describing a single token on a given line (from %Library.SyntaxColorReader), and pState passed between calls, 591 | /// returns 1 if the line is "executable" (meaning the line-by-line monitor will likely be able to detect whether it was executed) or not. 592 | /// In certain edge cases, pPreviousLineWasExecutable may be set to 1 to correct the previous line's result. 593 | ClassMethod LineTokenIsExecutable(pLineToken As %List, ByRef pState, ByRef pPreviousLineWasExecutable As %Boolean) As %Boolean [ Private ] 594 | { 595 | If '$Data(pState) { 596 | Set pState("IsDim") = 0 597 | Set pState("PastWhiteSpace") = 0 598 | Set pState("DoCommand") = 0 599 | } 600 | Set tExecutable = 0 601 | Set tTokenType = $ListGet(pLineToken,2) 602 | Set tTokenName = $ListGet(pLineToken,3) 603 | Set tTokenLower = $ZConvert(tTokenName,"L") 604 | If (tTokenType = "Pre-Processor Command") { 605 | If (tTokenLower = "dim") { 606 | Set pState("IsDim") = 1 607 | } 608 | } ElseIf (tTokenType = "Command") { 609 | If pState("IsDim") { 610 | // Not executable unless there's an "=" 611 | } Else { 612 | If (tTokenLower = "do") { 613 | // Special handling for do: is not executable if the line has just: 614 | // Do { 615 | // Will check later for non-whitespace/comment/brace tokens following "Do" on a given line 616 | Set pState("DoCommand") = 1 617 | } ElseIf (tTokenLower = "catch") { 618 | /* 619 | "Catch" is tricky. 620 | Given: 621 | } 622 | catch e { 623 | 624 | The line by line monitor will flag "}" as executed (if an exception was not thrown), 625 | but will never flag never "catch e {" as executed. 626 | 627 | Similarly, 628 | } catch e { 629 | gets credit as being run if no exception is thrown. 630 | */ 631 | 632 | If pState("PastWhiteSpace") { 633 | Set tExecutable = 1 634 | } Else { 635 | Set pPreviousLineWasExecutable = 1 636 | } 637 | } ElseIf (tTokenLower '= "try") && (tTokenLower '[ "else") { 638 | Set tExecutable = 1 639 | } 640 | } 641 | } ElseIf (tTokenType = "Operator") { 642 | If pState("IsDim") { 643 | // #dim is executable if there is later an "Equals" operator 644 | If (tTokenName = "=") { 645 | Set tExecutable = 1 646 | } 647 | } 648 | } ElseIf (tTokenType = "Macro") { 649 | If (tTokenLower [ "throw") || (tTokenLower = "generate") { 650 | // $$$ThrowStatus / $$$ThrowOnError commonly appears as on lines with no explicit command. 651 | // Treat as executable. 652 | // $$$GENERATE(...) is also executable. 653 | Set tExecutable = 1 654 | } 655 | } ElseIf pState("DoCommand") && (tTokenType '= "White Space") && (tTokenType '= "Brace") && (tTokenType '= "Comment") { 656 | Set tExecutable = 1 657 | } 658 | If (tTokenType '= "White Space") { 659 | Set pState("PastWhiteSpace") = 1 660 | } 661 | Quit tExecutable 662 | } 663 | 664 | /// Wrapper for %Monitor.System.LineByLine:Result to present metrics in a format more usable from SQL (as a table-valued function). 665 | Query LineByLineMonitorResult(pRoutine As %String) As %Query(ROWSPEC = "LineNumber:%Integer,LineCovered:%Boolean,RtnLine:%Integer,Time:%Numeric,TotalTime:%Numeric") [ SqlProc ] 666 | { 667 | } 668 | 669 | ClassMethod LineByLineMonitorResultExecute(ByRef qHandle As %Binary, pRoutine As %String) As %Status 670 | { 671 | Quit ##class(%Monitor.System.LineByLine).ResultExecute(.qHandle,pRoutine) 672 | } 673 | 674 | ClassMethod LineByLineMonitorResultFetch(ByRef qHandle As %Binary, ByRef Row As %List, ByRef AtEnd As %Integer = 0) As %Status [ PlaceAfter = ResultExecute ] 675 | { 676 | Set tLine = $Piece(qHandle,"^",2) 677 | Set tSC = ##class(%Monitor.System.LineByLine).ResultFetch(.qHandle,.Row,.AtEnd) 678 | If 'AtEnd { 679 | Set tCounters = $List(Row,1) 680 | Set tRtnLine = $ListGet(tCounters,1) 681 | Set tTime = $ListGet(tCounters,2) 682 | Set tTotalTime = $ListGet(tCounters,3) 683 | 684 | Set tRtnLines = $Case(tRtnLine,0:0,"":0,:tRtnLine) 685 | Set tLineCovered = (tRtnLines > 0) 686 | Set tTime = $Case(tTime,0:0,"":0,:tTime) 687 | Set tTotalTime = $Case(tTotalTime,0:0,"":0,:tTotalTime) 688 | Set Row = $ListBuild(tLine,tLineCovered,tRtnLines,tTime,tTotalTime) 689 | } 690 | Quit $$$OK 691 | } 692 | 693 | ClassMethod LineByLineMonitorResultClose(ByRef qHandle As %Binary) As %Status [ PlaceAfter = ResultExecute ] 694 | { 695 | Quit ##class(%Monitor.System.LineByLine).ResultClose(.qHandle) 696 | } 697 | 698 | } 699 | -------------------------------------------------------------------------------- /cls/TestCoverage/Utils/ComplexityParser.cls: -------------------------------------------------------------------------------- 1 | Class TestCoverage.Utils.ComplexityParser Extends %RegisteredObject 2 | { 3 | 4 | Property CodeStream As %Stream.Object [ Private ]; 5 | 6 | Property Complexity As %Integer [ InitialExpression = 1, Private ]; 7 | 8 | Property State [ MultiDimensional, Private ]; 9 | 10 | Method %OnNew(pStream As %Stream.Object) As %Status [ Private, ServerOnly = 1 ] 11 | { 12 | Set ..CodeStream = pStream 13 | Set ..State = 1 14 | Quit $$$OK 15 | } 16 | 17 | /// This method throws exceptions directly. 18 | Method GetComplexity() As %Integer 19 | { 20 | Set tSC = ##class(%Library.SyntaxColorReader).FromCode(..CodeStream,"COS","A",.tSCReader) 21 | $$$ThrowOnError(tSC) 22 | Set tHasNextLine = tSCReader.NextLine(.tNextLineTokenList) 23 | While tHasNextLine { 24 | Set tLineTokenList = tNextLineTokenList 25 | Set tHasNextLine = tSCReader.NextLine(.tNextLineTokenList) 26 | Do ..ProcessTokenList(.tLineTokenList,'tHasNextLine) 27 | } 28 | Quit ..Complexity 29 | } 30 | 31 | Method ProcessTokenList(pTokenList As %List, pIsLastLine As %Boolean) 32 | { 33 | Set tPointer = 0 34 | While $ListNext(pTokenList,tPointer,tToken) { 35 | Do ..ProcessToken(tToken, pIsLastLine) 36 | } 37 | } 38 | 39 | Method ProcessToken(pToken As %List, pIsLastLine As %Boolean) 40 | { 41 | // Ironically, this is a very complex method. 42 | 43 | Set tLanguage = $ListGet(pToken,1) 44 | Set tType = $ListGet(pToken,2) 45 | Set tText = $ZConvert($ListGet(pToken,3),"L") 46 | 47 | // Skip white space. 48 | If (tType = "White Space") { 49 | Quit 50 | } 51 | 52 | Set tLastToken = $Get(..State("LastToken")) 53 | Set ..State("LastToken") = pToken 54 | 55 | If (tLanguage '= "COS") { 56 | // no-op 57 | } ElseIf (tType = "Operator") { 58 | // Interested in short-circuit logical operators only (because there is a different code path depending on the value of the first operand). 59 | If (tText = "||") || (tText = "&&") { 60 | Set ..Complexity = ..Complexity + 1 61 | Quit 62 | } 63 | } ElseIf (tType = "Command") { 64 | Set ..Complexity = ..Complexity + $Case(tText, 65 | "if":1, 66 | "elseif":1, 67 | "for":1, 68 | "while":1, 69 | "throw":1, 70 | "catch":1, 71 | "continue":1, 72 | "quit":'pIsLastLine, 73 | "return":'pIsLastLine, 74 | :0) 75 | Set ..State(..State,"Command") = tText 76 | } ElseIf (tType = "Delimiter") { 77 | If (tText = ":") { 78 | Set tFunction = $Get(..State(..State,"Function")) 79 | Set tLastText = $ListGet(tLastToken,3) 80 | If (tFunction = "$select") && (tLastText '= 1) { 81 | // Count all but trivial case. (Attempt to figure out if it's the trivial case is a bit lazy.) 82 | Set ..Complexity = ..Complexity + 1 83 | } ElseIf (tFunction = "$case") && (tLastText '= ",") { 84 | // Count all but default case. 85 | Set ..Complexity = ..Complexity + 1 86 | } ElseIf ($ListGet(tLastToken,2) = "Command") { 87 | // Postconditional 88 | Set ..Complexity = ..Complexity + 1 89 | } 90 | } ElseIf (tText = "(") { 91 | If ($ListGet(tLastToken,2) '= "Function") { 92 | Set ..State($Increment(..State)) = "" 93 | } 94 | } ElseIf (tText = ")") { 95 | Kill ..State(..State) 96 | Set ..State = ..State - 1 97 | } ElseIf (tText = ",") { 98 | // See if "," is used with if/elseif as a short-circuit and operator 99 | // ..State will have been incremented if it is in a method/function call 100 | Set tCommand = $Get(..State(..State,"Command")) 101 | If (tCommand = "if") || (tCommand = "elseif") { 102 | Set ..Complexity = ..Complexity + 1 103 | } 104 | } 105 | } ElseIf (tType = "Function") { 106 | Set ..State($Increment(..State),"Function") = tText 107 | } 108 | } 109 | 110 | } 111 | 112 | -------------------------------------------------------------------------------- /cls/TestCoverage/Utils/File.cls: -------------------------------------------------------------------------------- 1 | /// We want to avoid depending on HSMOD.FileUtils to support use of the Test Coverage tool without the package manager (e.g., by QD) 2 | Class TestCoverage.Utils.File 3 | { 4 | 5 | /// Create this directory and all the parent directories if they do not exist. This differs from 6 | /// CreateDirectory as that method only creates one new directory where as 7 | /// this will create the entire chain of directories. Returns true if it succeeds and false otherwise. 8 | /// Pass return by reference to obtain the low level return value in case of errors 9 | ClassMethod CreateDirectoryChain(pName As %String) As %Status 10 | { 11 | Set tSC = $$$OK 12 | If '##class(%Library.File).CreateDirectoryChain(pName,.tReturn) { 13 | Set tSC = $$$ERROR($$$GeneralError,$$$FormatText("Error creating directory chain %1: %2",pName,$zu(209,tReturn))) 14 | } 15 | Quit tSC 16 | } 17 | 18 | } 19 | 20 | -------------------------------------------------------------------------------- /cls/TestCoverage/Utils/LineByLineMonitor.cls: -------------------------------------------------------------------------------- 1 | Include (%occErrors, TestCoverage) 2 | 3 | /// Wrapper around %Monitor.System.LineByLine to ensure that the monitor is stopped when it should be, and also 4 | /// to wrap the decision about whether to stop/start the monitor or to just clear counters. 5 | Class TestCoverage.Utils.LineByLineMonitor Extends %Monitor.System.LineByLine 6 | { 7 | 8 | /// True if the line-by-line monitor has been started. 9 | Property Started As %Boolean [ Calculated, Private, ReadOnly ]; 10 | 11 | /// True if the Python trace has been set 12 | Property PyStarted As %Boolean [ Calculated, Private, ReadOnly ]; 13 | 14 | Method StartedGet() As %Boolean [ CodeMode = expression ] 15 | { 16 | $zu(84,8) 17 | } 18 | 19 | Method PyStartedGet() As %Boolean [ Language = python ] 20 | { 21 | import sys 22 | return sys.gettrace() is not None 23 | } 24 | 25 | /// True if the line-by-line monitor is paused 26 | Property Paused As %Boolean [ Calculated, Private, ReadOnly ]; 27 | 28 | Method PausedGet() As %Boolean [ CodeMode = expression ] 29 | { 30 | ..Started && '$zu(84,1) 31 | } 32 | 33 | /// The current python classes being tracked, so that we know what to store the coverage for 34 | Property PythonClassList As %List; 35 | 36 | Property LastRoutineList As %List [ Private ]; 37 | 38 | Property LastMetricList As %List [ Private ]; 39 | 40 | Property LastProcessList As %List [ Private ]; 41 | 42 | Property LastPythonList As %List [ Private ]; 43 | 44 | /// This callback method is invoked by the %Close method to 45 | /// provide notification that the current object is being closed. 46 | /// 47 | ///

The return value of this method is ignored. 48 | Method %OnClose() As %Status [ Private, ServerOnly = 1 ] 49 | { 50 | If ..Started { 51 | Do ..Stop() 52 | } 53 | 54 | Quit $$$OK 55 | } 56 | 57 | ClassMethod CheckAvailableMemory(pProcessCount As %Integer, pRoutineCount As %Integer, pRequireError As %Boolean = 0) As %Status 58 | { 59 | Set tSC = $$$OK 60 | Set tRequiredPages = $zu(84,0,4,pProcessCount,0,pRoutineCount,0,0) 61 | Set tAvailablePages = $zu(84,0,5) 62 | If pRequireError || (tRequiredPages > tAvailablePages) { 63 | Set tSC = $$$ERROR($$$GeneralError,"Insufficient memory for line by line monitor - consider increasing gmheap. Contiguous memory required: "_(tRequiredPages*64)_" KB; reported available: "_(tAvailablePages*64)_" KB") 64 | } 65 | Quit tSC 66 | } 67 | 68 | ClassMethod PyStartWithScope(pCoverageClasses As %List) [ Language = python ] 69 | { 70 | 71 | from sys import settrace 72 | import iris 73 | 74 | tCoverageClasses = set(iris.cls('%SYS.Python').ToList(pCoverageClasses)) 75 | def my_tracer(frame, event, arg = None): 76 | # extracts frame code 77 | code = frame.f_code 78 | # extracts calling function name and the class that the function is in 79 | class_name = frame.f_globals.get('__name__', None) # Use get to avoid KeyError 80 | # extracts the line number 81 | line_no = frame.f_lineno 82 | if class_name and class_name in tCoverageClasses and line_no > 1: # if this is in a covered class 83 | tGlob = iris.gref('^IRIS.Temp.TestCoveragePY') # python doesn't have macros -- this is $$$PyMonitorResults 84 | # $$$PyMonitorResults(classname, linenumber) = the number of times that linenumber in that class was covered 85 | 86 | curCount = tGlob.get([class_name, line_no]) 87 | if not curCount: 88 | curCount = 0 89 | tGlob[class_name, line_no] = curCount + 1 90 | 91 | return my_tracer 92 | settrace(my_tracer) 93 | } 94 | 95 | ClassMethod PyClearCounters() 96 | { 97 | Kill $$$PyMonitorResults 98 | } 99 | 100 | ClassMethod PyStop() [ Language = python ] 101 | { 102 | from sys import settrace 103 | settrace(None) 104 | } 105 | 106 | /// Tracks current monitoring context and stops/starts or resets counters depending on whether it has changed 107 | Method StartWithScope(pRoutineList As %List, pPyClasses As %List, pMetricList As %List, pProcessList As %List) As %Status 108 | { 109 | Set tSC = $$$OK 110 | Try { 111 | Set ..PythonClassList = pPyClasses 112 | Set tDifferentScope = (..LastRoutineList '= pRoutineList) || (..LastMetricList '= pMetricList) || (..LastProcessList '= pProcessList) || (..LastPythonList '= pPyClasses) 113 | If tDifferentScope && (..Started || ..PyStarted) { 114 | // If we need to track different routines/metrics/processes, need to stop the monitor before restarting with the new context. 115 | If (..Started) { 116 | Do ..Stop() 117 | } 118 | Do ..PyStop() // setting the trace to None can and should always be done 119 | Set ..LastRoutineList = pRoutineList 120 | Set ..LastMetricList = pMetricList 121 | Set ..LastProcessList = pProcessList 122 | Set ..LastPythonList = pPyClasses 123 | } 124 | 125 | // take care of starting the ObjectScript Monitor 126 | If ('..Started && $ListLength(pRoutineList) '= 0) { 127 | Set tSC = ..Start(pRoutineList, pMetricList, pProcessList) 128 | If $System.Status.Equals(tSC,$$$ERRORCODE($$$MonitorMemoryAlloc)) { 129 | // Construct a more helpful error message. 130 | Set tSC = $$$EMBEDSC(..CheckAvailableMemory($ListLength(pProcessList),$ListLength(pRoutineList),1),tSC) 131 | } 132 | $$$ThrowOnError(tSC) 133 | } Else { 134 | // If the monitor was already running, clear the counters. 135 | if (..Started) { 136 | Set tSC = ..ClearCounters() 137 | $$$ThrowOnError(tSC) 138 | } 139 | If (..Paused && $ListLength(pRoutineList) '= 0){ 140 | $$$ThrowOnError(..Resume()) 141 | } 142 | } 143 | 144 | If ('..PyStarted && $ListLength(pPyClasses) '= 0) { 145 | // whether we're resuming or restarting, we either way want to clear counters 146 | // since StoreIntCoverage should have already 147 | Do ..PyClearCounters() 148 | Do ..PyStartWithScope(pPyClasses) 149 | } 150 | } Catch e { 151 | Set tSC = e.AsStatus() 152 | } 153 | Quit tSC 154 | } 155 | 156 | /// Clears all statistics, allowing collection to resume from 0 with the same settings and without needing to stop the monitor. 157 | /// Based on Pause implementation, but with modern exception handling and code style. 158 | ClassMethod ClearCounters() As %Status 159 | { 160 | Set tSC = $$$OK 161 | Set tLocked = 0 162 | Try { 163 | // See if PERFMON is running (vs. line-by-line) 164 | If ($zu(84,8) = 1) && ($zu(84,16) = -1) { 165 | $$$ThrowStatus($$$ERROR($$$MonitorInUse)) 166 | } 167 | Lock +^%SYS("MON-HOLD"):3 168 | If '$Test { 169 | $$$ThrowStatus($$$ERROR($$$MonitorInUse)) 170 | } 171 | Set tLocked = 1 172 | if ($zu(84,8) = 0) { 173 | // Monitor is off. 174 | $$$ThrowStatus($$$ERROR($$$MonitorNotRunning)) 175 | } 176 | // Finally: actually clear the counters. 177 | Do $zu(84,2) 178 | } Catch e { 179 | Set tSC = e.AsStatus() 180 | } 181 | If tLocked { 182 | Lock -^%SYS("MON-HOLD") 183 | } 184 | Quit tSC 185 | } 186 | 187 | ClassMethod IsRunning() As %Boolean [ CodeMode = expression ] 188 | { 189 | $zu(84,8) && $zu(84,1) 190 | } 191 | 192 | /// Overridden and minimally modified from parent implementation 193 | ClassMethod Start(Routine As %List, Metric As %List, Process As %List) As %Status 194 | { 195 | // Before attempting to start monitor, ensure we actually have object code for at least some element of Routine 196 | Set pointer = 0 197 | Set found = 0 198 | While $ListNext(Routine,pointer,routine) { 199 | Set rtnname = routine_".obj" 200 | For { 201 | Set data = "" 202 | Set more = $$LIST^%R(rtnname,32767,0,.data,.ctx) 203 | If (data '= "") { 204 | Set found = 1 205 | Quit 206 | } 207 | } 208 | If (found = 1) { 209 | Quit 210 | } 211 | } 212 | If 'found { 213 | Quit ..GetError("NoObjFound") 214 | } 215 | 216 | Quit ##super(.Routine,.Metric,.Process) 217 | } 218 | 219 | ClassMethod GetError(key As %String, args...) 220 | { 221 | Quit $Case(key, 222 | "NoObjFound":$System.Status.Error($$$GeneralError,"Unable to start monitor: no object code exists for selected classes/routines. "_ 223 | "Possible remediations: ensure that at least some class/routine is selected; ""view other"" for the routine and make sure that the "_ 224 | ".INT code actually has some content that could be covered.")) 225 | } 226 | 227 | } 228 | -------------------------------------------------------------------------------- /cls/TestCoverage/Utils/Projection/SchemaGenerator.cls: -------------------------------------------------------------------------------- 1 | Class TestCoverage.Utils.Projection.SchemaGenerator Extends %Projection.AbstractProjection 2 | { 3 | 4 | /// Name of XData block with schema in class defining this projection. Defaults to XSD. 5 | Parameter XDATA As STRING = "XSD"; 6 | 7 | /// Package in which to generate classes. Defaults to the current class's name. 8 | Parameter PACKAGE As STRING; 9 | 10 | /// See %XML.Utils.SchemaReader for description. 11 | Parameter DEFAULTSEQUENCE As BOOLEAN; 12 | 13 | /// See %XML.Utils.SchemaReader for description. 14 | Parameter JAVA As BOOLEAN; 15 | 16 | /// See %XML.Utils.SchemaReader for description. 17 | Parameter NOSEQUENCE As BOOLEAN; 18 | 19 | /// See %XML.Utils.SchemaReader for description. 20 | Parameter POPULATE As BOOLEAN; 21 | 22 | /// See %XML.Utils.SchemaReader for description. 23 | Parameter SQLCOLUMNS As BOOLEAN; 24 | 25 | /// See %XML.Utils.SchemaReader for description. 26 | Parameter IGNORENULL As BOOLEAN; 27 | 28 | /// Generate classes based on the specified XData block. 29 | ClassMethod CreateProjection(classname As %String, ByRef parameters As %String, modified As %String, qstruct) As %Status 30 | { 31 | Set tSC = $$$OK 32 | Set tInitTLevel = $TLevel 33 | Try { 34 | Merge tParameters = parameters 35 | 36 | // Clear empty parameters 37 | Set tParameter = "" 38 | For { 39 | Set tParameter = $Order(parameters(tParameter),1,tValue) 40 | If (tParameter = "") { 41 | Quit 42 | } 43 | If (tValue = "") { 44 | Kill tParameters(tParameter) 45 | } 46 | } 47 | 48 | Set tXData = ##class(%Dictionary.XDataDefinition).IDKEYOpen(classname,parameters("XDATA"),,.tSC) 49 | $$$ThrowOnError(tSC) 50 | 51 | Set tTempFile = ##class(%Stream.FileCharacter).%New() 52 | Set tTempFile.RemoveOnClose = 1 53 | Set tSC = tTempFile.CopyFromAndSave(tXData.Data) 54 | $$$ThrowOnError(tSC) 55 | 56 | // If no package specified, generate under the class defining this projection. 57 | Set tPackage = $Get(parameters("PACKAGE"),classname) 58 | 59 | // Wrap deletion and regeneration of classes in a transaction. 60 | TSTART 61 | Set tSC = $System.OBJ.DeletePackage(tPackage) 62 | $$$ThrowOnError(tSC) 63 | 64 | Set tReader = ##class(%XML.Utils.SchemaReader).%New() 65 | Set tReader.CompileClasses = 0 // We'll queue this for later. 66 | Set tReader.MakePersistent = 0 67 | Set tReader.MakeNamespace = 1 68 | Set tSC = tReader.Process(tTempFile.Filename,tPackage,.tParameters) 69 | $$$ThrowOnError(tSC) 70 | TCOMMIT 71 | 72 | // Queue compilation of all classes in generated package 73 | Set tSC = $System.OBJ.GetPackageList(.tClasses,tPackage) 74 | $$$ThrowOnError(tSC) 75 | 76 | Set tClass = "" 77 | For { 78 | Set tClass = $Order(tClasses(tClass)) 79 | If (tClass = "") { 80 | Quit 81 | } 82 | Do ..QueueClass(tClass) 83 | } 84 | } Catch e { 85 | Set tSC = e.AsStatus() 86 | } 87 | While ($TLevel > tInitTLevel) { 88 | TROLLBACK 1 89 | } 90 | Quit tSC 91 | } 92 | 93 | /// Cleanup is automatic when parent class is deleted. 94 | ClassMethod RemoveProjection(classname As %String, ByRef parameters As %String, recompile As %Boolean, modified As %String, qstruct) As %Status 95 | { 96 | Quit $$$OK 97 | } 98 | 99 | } 100 | 101 | -------------------------------------------------------------------------------- /inc/TestCoverage.inc: -------------------------------------------------------------------------------- 1 | ROUTINE TestCoverage [Type=INC] 2 | #define StartTimer(%msg) If (..Display["log") { Write !,%msg,": " Set tStartTime = $zh } 3 | #define StopTimer If (..Display["log") { Write ($zh-tStartTime)," seconds" } 4 | #define METRICS "RtnLine","Time","TotalTime" 5 | #define TestPathAllTests "all tests" 6 | #define PyMonitorResults ^IRIS.Temp.TestCoveragePY -------------------------------------------------------------------------------- /internal/testing/unit_tests/UnitTest/TestCoverage/Unit/CodeUnit.cls: -------------------------------------------------------------------------------- 1 | Class UnitTest.TestCoverage.Unit.CodeUnit Extends %UnitTest.TestCase 2 | { 3 | 4 | Method TestCodeUnitCreation() 5 | { 6 | Set tSC = $$$OK 7 | Try { 8 | Set tResult = ##class(%SQL.Statement).%ExecDirect(, 9 | "delete from TestCoverage_Data.CodeUnit where Name %STARTSWITH ?",$classname()) 10 | If (tResult.%SQLCODE < 0) { 11 | Throw ##class(%Exception.SQL).CreateFromSQLCODE(tResult.%SQLCODE,tResult.%Message) 12 | } 13 | 14 | Do $$$AssertStatusOK($System.OBJ.Compile($classname(),"ck-d")) 15 | 16 | #dim tCodeUnit As TestCoverage.Data.CodeUnit 17 | Set tClsName = $classname()_".CLS" 18 | Set tIntName = $classname()_".1.INT" 19 | Set tPyName = $classname()_".PY" 20 | Set tSC = ##class(TestCoverage.Data.CodeUnit).GetCurrentByName(tIntName,$Namespace,.tIntCodeUnit) 21 | Do $$$AssertStatusOK(tSC,"Found test coverage data for "_tIntName) 22 | Do $$$AssertEquals(tIntCodeUnit.Name,$classname()_".1") 23 | Do $$$AssertEquals(tIntCodeUnit.Type,"INT") 24 | 25 | Set tGenIntName = $classname()_".G1.INT" 26 | Set tSC = ##class(TestCoverage.Data.CodeUnit).GetCurrentByName(tGenIntName,$Namespace,.tGenCodeUnit) 27 | Do $$$AssertStatusOK(tSC,"Found test coverage data for "_tGenIntName) 28 | Do $$$AssertEquals(tGenCodeUnit.Name,$classname()_".G1") 29 | Do $$$AssertEquals(tGenCodeUnit.Type,"INT") 30 | 31 | 32 | Set tSC = ##class(TestCoverage.Data.CodeUnit).GetCurrentByName(tClsName,$Namespace,.tClsCodeUnit) 33 | Do $$$AssertStatusOK(tSC,"Found test coverage data for "_tClsName) 34 | Do $$$AssertEquals(tClsCodeUnit.Name,$classname()) 35 | Do $$$AssertEquals(tClsCodeUnit.Type,"CLS") 36 | 37 | Set tSC = ##class(TestCoverage.Data.CodeUnit).GetCurrentByName(tPyName,$Namespace,.tPyCodeUnit) 38 | Do $$$AssertStatusOK(tSC,"Found test coverage data for "_tPyName) 39 | Do $$$AssertEquals(tPyCodeUnit.Name,$classname()) 40 | Do $$$AssertEquals(tPyCodeUnit.Type,"PY") 41 | 42 | Set tSC = tClsCodeUnit.UpdatePyExecutableLines($classname(),.tPyCodeUnit) 43 | Do $$$AssertStatusOK(tSC,"Found updated executable line data for "_tClsName) 44 | 45 | Set tSC = tClsCodeUnit.UpdatePythonLines($classname(),.tPyCodeUnit) 46 | Do $$$AssertStatusOK(tSC,"Found updated pythonicity line data for "_tClsName) 47 | 48 | Set tConstantReturnValueLine = tClsCodeUnit.MethodMap.GetAt("SampleConstantReturnValue") 49 | Set tCodeGeneratorLine = tClsCodeUnit.MethodMap.GetAt("SampleCodeGenerator") 50 | Set tNormalMethodLine = tClsCodeUnit.MethodMap.GetAt("SampleNormalMethod") 51 | Set tPythonMethodLine = tClsCodeUnit.MethodMap.GetAt("SamplePythonMethod") 52 | set tPythonWeirdSpacingMethodLine = tClsCodeUnit.MethodMap.GetAt("PythonWeirdSpacing") 53 | 54 | Do $$$AssertNotEquals(tConstantReturnValueLine,"") 55 | Do $$$AssertNotEquals(tCodeGeneratorLine,"") 56 | Do $$$AssertNotEquals(tNormalMethodLine,"") 57 | Do $$$AssertNotEquals(tPythonMethodLine,"") 58 | Do $$$AssertNotEquals(tPythonWeirdSpacingMethodLine,"") 59 | 60 | // test if LineIsPython is working properly 61 | Do $$$AssertEquals(tClsCodeUnit.LineIsPython.GetAt(tPythonMethodLine+2), 1) 62 | Do $$$AssertEquals(tClsCodeUnit.LineIsPython.GetAt(tNormalMethodLine+2), 0) 63 | 64 | // tTestLines(line number) = $ListBuild(description, executable (default 1), mapped (default 1), mapped from hash (if relevant), mapped from line (if relevant)) 65 | Set tTestLines(tConstantReturnValueLine+2) = $ListBuild("SampleConstantReturnValue+1",0,0) 66 | Set tTestLines(tCodeGeneratorLine+2) = $ListBuild("SampleCodeGenerator+1",,,tGenCodeUnit.Hash,tGenCodeUnit.MethodMap.GetAt("SampleCodeGenerator")+1, "INT") 67 | Set tTestLines(tCodeGeneratorLine+3) = $ListBuild("SampleCodeGenerator+2",,,tGenCodeUnit.Hash,tGenCodeUnit.MethodMap.GetAt("SampleCodeGenerator")+2, "INT") 68 | Set tTestLines(tCodeGeneratorLine+4) = $ListBuild("SampleCodeGenerator+3",,,tGenCodeUnit.Hash,tGenCodeUnit.MethodMap.GetAt("SampleCodeGenerator")+3, "INT") 69 | Set methodLabel = $Select($System.Version.GetMajor()<2023:"z",1:"")_"SampleNormalMethod" 70 | Set tTestLines(tNormalMethodLine+2) = $ListBuild("SampleNormalMethod+1",,,tIntCodeUnit.Hash,tIntCodeUnit.MethodMap.GetAt(methodLabel)+1, "INT") 71 | Set tTestLines(tNormalMethodLine+3) = $ListBuild("SampleNormalMethod+2",,,tIntCodeUnit.Hash,tIntCodeUnit.MethodMap.GetAt(methodLabel)+2, "INT") 72 | Set tTestLines(tPythonMethodLine+2) = $ListBuild("SamplePythonMethod+1",,,tPyCodeUnit.Hash,tPyCodeUnit.MethodMap.GetAt("SamplePythonMethod")+1, "PY") 73 | Set tTestLines(tPythonMethodLine+3) = $ListBuild("SamplePythonMethod+2",,,tPyCodeUnit.Hash,tPyCodeUnit.MethodMap.GetAt("SamplePythonMethod")+2, "PY") 74 | Set tTestLines(tPythonWeirdSpacingMethodLine+4) = $ListBuild("PythonWeirdSpacing+1",,,tPyCodeUnit.Hash,tPyCodeUnit.MethodMap.GetAt("PythonWeirdSpacing")+1, "PY") 75 | Set tTestLines(tPythonWeirdSpacingMethodLine+5) = $ListBuild("PythonWeirdSpacing+2",,,tPyCodeUnit.Hash,tPyCodeUnit.MethodMap.GetAt("PythonWeirdSpacing")+2, "PY") 76 | Set tTestLines(tPythonWeirdSpacingMethodLine+6) = $ListBuild("PythonWeirdSpacing+3",,,tPyCodeUnit.Hash,tPyCodeUnit.MethodMap.GetAt("PythonWeirdSpacing")+3, "PY") 77 | 78 | Set tLine = "" 79 | For { 80 | Set tLine = $Order(tTestLines(tLine),1,tInfo) 81 | If (tLine = "") { 82 | Quit 83 | } 84 | 85 | // Overwrite with defined values, leave defaults in AssertLineHandledCorrectly for undefined values (passing byref) 86 | Kill tDescription, tExecutable, tMapped, tExpectedFromHash, tExpectedFromLine, tType 87 | Set $ListBuild(tDescription, tExecutable, tMapped, tExpectedFromHash, tExpectedFromLine, tType) = tInfo 88 | Do ..AssertLineHandledCorrectly(tClsCodeUnit, tLine, .tDescription, .tExecutable, .tMapped, .tExpectedFromHash, .tExpectedFromLine, .tType) 89 | } 90 | } Catch e { 91 | Set tSC = e.AsStatus() 92 | } 93 | Do $$$AssertStatusOK(tSC,"No unexpected errors occurred.") 94 | } 95 | 96 | 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 97 | { 98 | Set tAllGood = 1 99 | If pExecutable { 100 | Set tAllGood = $$$AssertTrue($Bit(pClassCodeUnit.ExecutableLines,pLine),"Line is executable: "_pDescription) && tAllGood 101 | } Else { 102 | Set tAllGood = $$$AssertNotTrue($Bit(pClassCodeUnit.ExecutableLines,pLine),"Line is not executable: "_pDescription) && tAllGood 103 | } 104 | 105 | &sql(select count(*),FromHash,FromLine into :tCount,:tFromHash,:tFromLine 106 | from TestCoverage_Data.CodeUnitMap 107 | where ToHash = :pClassCodeUnit.Hash and ToLine = :pLine and FromHash->Type = :pType) 108 | If (SQLCODE < 0) { 109 | Throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg) 110 | } 111 | Set tAllGood = $$$AssertTrue(tCount>=pMapped,"Line is "_$Select(pMapped:"",1:"not ")_"mapped: "_pDescription) && tAllGood 112 | Set tAllGood = $$$AssertNotTrue(tCount>1,"Only one line is mapped to "_pDescription) && tAllGood 113 | If (pExpectedFromHash '= "") { 114 | Set tAllGood = $$$AssertEquals($Get(tFromHash),pExpectedFromHash,pDescription_" mapped from expected routine.") && tAllGood 115 | Set tAllGood = $$$AssertEquals($Get(tFromLine),pExpectedFromLine,pDescription_" mapped from expected line.") && tAllGood 116 | } 117 | Quit tAllGood 118 | } 119 | 120 | ClassMethod SampleConstantReturnValue() 121 | { 122 | Quit 42 123 | } 124 | 125 | ClassMethod SampleCodeGenerator() [ CodeMode = objectgenerator ] 126 | { 127 | Do %code.WriteLine(" Set x = $classname() //SampleCodeGenerator") 128 | Do %code.WriteLine(" Quit "_$$$QUOTE($zdt($h,3))) 129 | Quit $$$OK 130 | } 131 | 132 | ClassMethod AnotherSampleCodeGenerator() [ CodeMode = objectgenerator ] 133 | { 134 | Do %code.WriteLine(" Set x = $classname() //AnotherSampleCodeGenerator") 135 | Do %code.WriteLine(" Quit "_$$$QUOTE($zdt($h,3))) 136 | Quit $$$OK 137 | } 138 | 139 | ClassMethod TheThirdSampleCodeGenerator() [ CodeMode = objectgenerator ] 140 | { 141 | Do %code.WriteLine(" Set x = $classname() //TheThirdSampleCodeGenerator") 142 | Do %code.WriteLine(" Quit "_$$$QUOTE($zdt($h,3))) 143 | Quit $$$OK 144 | } 145 | 146 | ClassMethod SampleNormalMethod() 147 | { 148 | Set y = $classname() 149 | Quit y 150 | } 151 | 152 | ClassMethod SamplePythonMethod() [ Language = python ] 153 | { 154 | import iris 155 | return 50 156 | } 157 | 158 | ClassMethod PythonWeirdSpacing() [ Language = python ] 159 | { 160 | 161 | 162 | x = [0] * 10 163 | x.append([50]) 164 | return [element * 2 for element in x] 165 | } 166 | 167 | } 168 | -------------------------------------------------------------------------------- /internal/testing/unit_tests/UnitTest/TestCoverage/Unit/Procedures.cls: -------------------------------------------------------------------------------- 1 | Class UnitTest.TestCoverage.Unit.Procedures Extends %UnitTest.TestCase 2 | { 3 | 4 | Method TestListToBit() 5 | { 6 | Set compare = "" 7 | For i=1:1:3 { 8 | Set $Bit(compare,i) = 1 9 | } 10 | Do $$$AssertEquals(##class(TestCoverage.Procedures).ListToBit($lb(1,2,3,,"")),compare) 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /internal/testing/unit_tests/UnitTest/TestCoverage/Unit/TestComplexity.cls: -------------------------------------------------------------------------------- 1 | Class UnitTest.TestCoverage.Unit.TestComplexity Extends %UnitTest.TestCase 2 | { 3 | 4 | Method TestMethodsInThisClass() 5 | { 6 | #dim tCodeUnit As TestCoverage.Data.CodeUnit 7 | Set tClass = $classname() 8 | &sql(delete from TestCoverage_Data.CodeUnit where Name %STARTSWITH :tClass) 9 | Do $$$AssertTrue(SQLCODE>=0) 10 | Do $$$AssertStatusOK($System.OBJ.Compile($classname(),"ck-d")) 11 | Do $$$AssertStatusOK(##class(TestCoverage.Data.CodeUnit).GetCurrentByName(tClass_".1.INT")) 12 | If $$$AssertStatusOK(##class(TestCoverage.Data.CodeUnit).GetCurrentByName(tClass_".CLS",,.tCodeUnit)) { 13 | Do $$$AssertStatusOK(##class(TestCoverage.Data.CodeUnit).GetCurrentByName(tClass_".PY",,.tPyCodeUnit)) 14 | Do tCodeUnit.UpdatePyExecutableLines(tClass, .tPyCodeUnit) 15 | Set tKey = "" 16 | For { 17 | #dim tMethod As TestCoverage.Data.CodeSubUnit.Method 18 | Set tMethod = tCodeUnit.SubUnits.GetNext(.tKey) 19 | If (tKey = "") { 20 | Quit 21 | } 22 | If 'tMethod.%IsA("TestCoverage.Data.CodeSubUnit.Method") { 23 | Continue 24 | } 25 | Set tDescription = $$$comMemberKeyGet(tClass,$$$cCLASSmethod,tMethod.Name,$$$cMETHdescription) 26 | Set tExpectedComplexity = $Piece(tDescription," ",2) 27 | If (tExpectedComplexity = +tExpectedComplexity) { 28 | Do ..AssertEqualsViaMacro("Complexity("_tMethod.Name_"),"_tExpectedComplexity,tMethod.Complexity,tExpectedComplexity) 29 | } 30 | } 31 | } 32 | } 33 | 34 | /// Complexity: 1 35 | Method AVerySimpleMethod() 36 | { 37 | Quit 42 38 | } 39 | 40 | /// Complexity: 2 (1 + If) 41 | Method IfStatementMethod(pSomething As %Boolean = 0) 42 | { 43 | Set foo = "bar" 44 | If 'pSomething { 45 | Set foo = "actually no bar" 46 | } 47 | Quit foo 48 | } 49 | 50 | /// Complexity: 4 (1 + If + ElseIf + ElseIf) 51 | Method IfElseIfStatementMethod(pSomething As %Boolean = 0) 52 | { 53 | Set foo = "bar" 54 | If 'pSomething { 55 | Set foo = "actually no bar" 56 | } ElseIf pSomething = "w" { 57 | } ElseIf pSomething = "x" { 58 | } Else { 59 | // do nothing 60 | } 61 | Quit foo 62 | } 63 | 64 | /// Complexity: 3 (1 + If + &&) 65 | Method IfWithShortCircuitAnd(pSomething As %Boolean = 0) 66 | { 67 | Set foo = "bar" 68 | If 'pSomething && 'foo { 69 | Set foo = "actually no bar" 70 | } 71 | Quit foo 72 | } 73 | 74 | /// Complexity: 3 (1 + If + ,) 75 | Method IfWithShortCircuitCommaAnd(pSomething As %Boolean = 0) 76 | { 77 | Set foo = "bar" 78 | If pSomething,foo { 79 | Set foo = "actually no bar" 80 | } 81 | Quit foo 82 | } 83 | 84 | /// Complexity: 3 (1 + If + ||) 85 | Method IfWithShortCircuitOr(pSomething As %Boolean = 0) 86 | { 87 | Set foo = "bar" 88 | If '(pSomething || foo) { 89 | Set foo = "actually no bar" 90 | } 91 | Quit foo 92 | } 93 | 94 | /// Complexity: 2 (1 + If, non-short circuit operators not counted) 95 | Method IfWithNonShortCircuitOperators(pSomething As %Boolean = 0) 96 | { 97 | Set foo = "bar" 98 | If 'pSomething!..IfElseIfStatementMethod()&..ForLoopMethod() { 99 | Set foo = "actually no bar" 100 | } 101 | Quit foo 102 | } 103 | 104 | /// Complexity: 2 (1 + While) 105 | Method WhileLoopMethod() 106 | { 107 | Set foo = "" 108 | While (foo = "") { 109 | Set foo = foo_"a" 110 | } 111 | Quit foo 112 | } 113 | 114 | /// Complexity: 2 (1 + For) 115 | Method ForLoopMethod() 116 | { 117 | Set foo = "" 118 | For i=1:1:5 { 119 | Set foo = foo_"a" 120 | } 121 | Return foo 122 | } 123 | 124 | /// Complexity: 3 (1 + Throw + Catch) 125 | Method TryThrowCatchMethod() 126 | { 127 | Set tSC = $$$OK 128 | Try { 129 | Throw ##class(%Exception.General).%New("Nope!") 130 | } Catch e { 131 | Set tSC = e.AsStatus() 132 | } 133 | Quit tSC 134 | } 135 | 136 | /// Complexity: 4 (1 + For + If + Quit) 137 | Method EarlyQuitMethod() 138 | { 139 | Set tSC = $$$OK 140 | For i=1:1:5 { 141 | If (i = 4) { 142 | Quit 143 | } 144 | } 145 | Quit tSC 146 | } 147 | 148 | /// Complexity: 4 (1 + For + If + Return) 149 | Method EarlyReturnMethod() 150 | { 151 | Set tSC = $$$OK 152 | For i=1:1:5 { 153 | If (i = 4) { 154 | Return tSC 155 | } 156 | } 157 | Quit tSC 158 | } 159 | 160 | /// Complexity: 4 (1 + For + If + Continue) 161 | Method ForLoopContinueMethod() 162 | { 163 | Set tSC = $$$OK 164 | For i=1:1:5 { 165 | If (i = 4) { 166 | Continue 167 | } 168 | } 169 | Quit tSC 170 | } 171 | 172 | /// Complexity: 4 (1 + While + If + Continue) 173 | Method WhileLoopContinueMethod() 174 | { 175 | Set tSC = $$$OK 176 | While ($Increment(i) < 6) { 177 | If (i = 4) { 178 | Continue 179 | } 180 | } 181 | Quit tSC 182 | } 183 | 184 | /// Complexity: 4 (1 + For + Continue + :) 185 | Method PostConditionalMethod() 186 | { 187 | Set tSC = $$$OK 188 | For i=1:1:5 { 189 | Continue:i=4 190 | } 191 | Quit tSC 192 | } 193 | 194 | /// Complexity: 3 (1 + two non-default $Select cases) 195 | Method SelectMethod(pArgument As %String) 196 | { 197 | Quit $Select(pArgument=2:4,pArgument=3:9,1:1) 198 | } 199 | 200 | /// Complexity: 1 (1, with a $Select with only a default case) 201 | Method TrivialSelectMethod(pArgument As %String) 202 | { 203 | Quit $Select(1:42) 204 | } 205 | 206 | /// Complexity: 4 (1 + three non-default $Case cases) 207 | /// Has an extra white space before :1 to make sure that doesn't break it. 208 | Method CaseMethod(pArgument As %String) 209 | { 210 | Quit $Case(pArgument,2:4,3:9,4:16, :1) 211 | } 212 | 213 | /// Complexity: 1 (1, with a $Case with only a default case) 214 | Method TrivialCaseMethod(pArgument As %String) 215 | { 216 | Quit $Case(pArgument,:42) 217 | } 218 | 219 | /// Complexity: 4 (1 + two non-default $Case cases + for) 220 | /// Ensures that the ":" delimiters in the for loop are treated properly 221 | Method FunctionWithAppropriateScope(pArgument As %String) 222 | { 223 | Set tSC = $$$OK 224 | Set foo = $Case(pArgument,2:4,3:9,:1) 225 | For i=1:1:5 { 226 | } 227 | Quit tSC 228 | } 229 | 230 | /// Complexity: 14 (1 + UCQ) 231 | /// Was 2, and then 7, on earlier IRIS versions; UCQ adds a bit (and then a bit more) 232 | /// At some point we might want to just remove this test 233 | Method MethodWithEmbeddedSQL() 234 | { 235 | &sql(select top 1 1 into :foo) 236 | If (foo = 0) { 237 | Write !,"After embedded SQL" 238 | } 239 | Quit foo 240 | } 241 | 242 | /// Complexity: 3 (1 + throw + postconditional) 243 | Method MethodWithComplexMacros(pStatus As %Status) 244 | { 245 | $$$ThrowOnError(pStatus) 246 | } 247 | 248 | /// Complexity: 3 (1 + for + if) 249 | ClassMethod ForLoopPython() [ Language = python ] 250 | { 251 | for i in range(5): 252 | if i > 4: 253 | continue 254 | return 1 255 | } 256 | 257 | } 258 | -------------------------------------------------------------------------------- /internal/testing/unit_tests/UnitTest/TestCoverage/Unit/TestCoverageList.cls: -------------------------------------------------------------------------------- 1 | Class UnitTest.TestCoverage.Unit.TestCoverageList Extends %UnitTest.TestCase 2 | { 3 | 4 | /// helper function to find the samplecovlist.list's path 5 | ClassMethod FindCoverageList(directory As %String = "") As %String 6 | { 7 | 8 | set stmt = ##class(%SQL.Statement).%New() 9 | set status = stmt.%PrepareClassQuery("%File", "FileSet") 10 | if $$$ISERR(status) {write "%Prepare failed:" do $SYSTEM.Status.DisplayError(status) quit} 11 | 12 | set rset = stmt.%Execute(directory) 13 | if (rset.%SQLCODE '= 0) {write "%Execute failed:", !, "SQLCODE ", rset.%SQLCODE, ": ", rset.%Message quit} 14 | 15 | while rset.%Next() 16 | { 17 | set name = rset.%Get("Name") 18 | set type = rset.%Get("Type") 19 | 20 | if (type = "F") { 21 | do ##class(%File).Deconstruct(name, .dirs) 22 | if (dirs(dirs) = "samplecovlist.list") { 23 | return name 24 | } 25 | } elseif (type = "D"){ 26 | set retVal = ..FindCoverageList(name) 27 | if (retVal '= "") { 28 | return retVal 29 | } 30 | } 31 | } 32 | if (rset.%SQLCODE < 0) {write "%Next failed:", !, "SQLCODE ", rset.%SQLCODE, ": ", rset.%Message quit} 33 | return "" // didn't find it in this directory 34 | } 35 | 36 | Method TestGettingCoverageList() 37 | { 38 | set tFile = ..FindCoverageList(^UnitTestRoot) // finds the samplecovlist.list 39 | do ##class(TestCoverage.Manager).GetCoverageTargetsForFile(tFile, .tTargetArray) 40 | 41 | Set CorrectCoverageTargets("CLS", "TestCoverage.Data.CodeSubUnit") = "" 42 | Set CorrectCoverageTargets("CLS", "TestCoverage.Data.CodeSubUnit.Method") = "" 43 | Set CorrectCoverageTargets("CLS","TestCoverage.Data.CodeUnit")="" 44 | Set CorrectCoverageTargets("CLS","TestCoverage.Data.CodeUnitMap")="" 45 | Set CorrectCoverageTargets("CLS","TestCoverage.Data.Coverage")="" 46 | Set CorrectCoverageTargets("CLS","TestCoverage.Data.Run")="" 47 | Set CorrectCoverageTargets("CLS","TestCoverage.Manager")="" 48 | Set CorrectCoverageTargets("MAC","UnitTest.TestCoverage.Unit.CodeUnit.G1")="" 49 | Set CorrectCoverageTargets("MAC","UnitTest.TestCoverage.Unit.sampleRoutine")="" 50 | Do $$$AssertEquals(..CompareArrays(.tTargetArray, .CorrectCoverageTargets, .pMessage), 1, "tTargetarray equals CorrectCoverageTargets") 51 | Do $$$LogMessage(pMessage) 52 | } 53 | 54 | /// Taken from Tim's Developer Community post 55 | /// Returns true if arrays first and second have all the same subscripts and all 56 | /// the same values at those subscripts.
57 | /// If first and second both happen to be either undefined or unsubscripted variables, 58 | /// returns true if they're both undefined or have the same value.
59 | /// pMessage has details of the first difference found, if any. 60 | ClassMethod CompareArrays(ByRef first, ByRef second, Output pMessage) As %Boolean [ ProcedureBlock = 0 ] 61 | { 62 | New tEqual,tRef1,tRef2,tRef1Data,tRef1Value,tRef2Data,tRef2Value 63 | 64 | Set pMessage = "" 65 | Set tEqual = 1 66 | Set tRef1 = "first" 67 | Set tRef2 = "second" 68 | While (tRef1 '= "") || (tRef2 '= "") { 69 | #; See if the subscript is the same for both arrays. 70 | #; If not, one of them has a subscript the other doesn't, and they're not equal. 71 | If ($Piece(tRef1,"first",2) '= $Piece(tRef2,"second",2)) { 72 | Set tEqual = 0 73 | Set pMessage = "Different subscripts encountered by $Query: "_ 74 | $Case(tRef1,"":"",:tRef1)_"; "_$Case(tRef2,"":"",:tRef2) 75 | Quit 76 | } 77 | 78 | Kill tRef1Value,tRef2Value 79 | Set tRef1Data = $Data(@tRef1,tRef1Value) 80 | Set tRef2Data = $Data(@tRef2,tRef2Value) 81 | #; See if the $Data values are the same for the two. 82 | #; This is really only useful to detect if one of the arrays is undefined on the first pass; 83 | #; $Query only returns subscripts with data. 84 | #; This will catch only one being defined, or one being an array and 85 | #; ​the other being a regular variable. 86 | If (tRef1Data '= tRef2Data) { 87 | Set tEqual = 0 88 | Set pMessage = "$Data("_tRef1_")="_tRef1Data_"; $Data("_tRef2_")="_tRef2Data 89 | Quit 90 | } ElseIf (tRef1Data#2) && (tRef2Data#2) { 91 | #; See if the value at the subscript is the same for both arrays. 92 | #; If not, they're not equal. 93 | If (tRef1Value '= tRef2Value) { 94 | Set tEqual = 0 95 | Set pMessage = tRef1_"="_@tRef1_"; "_tRef2_"="_@tRef2 96 | Quit 97 | } 98 | } 99 | 100 | Set tRef1 = $Query(@tRef1) 101 | Set tRef2 = $Query(@tRef2) 102 | } 103 | Quit tEqual 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /internal/testing/unit_tests/UnitTest/TestCoverage/Unit/TestDelimitedIdentifiers.cls: -------------------------------------------------------------------------------- 1 | Include TestCoverage 2 | 3 | Class UnitTest.TestCoverage.Unit.TestDelimitedIdentifiers Extends %UnitTest.TestCase 4 | { 5 | 6 | Property InitialDelimitedIdentifiersSetting As %Boolean; 7 | 8 | Property Run As TestCoverage.Data.Run; 9 | 10 | Method TestWithDelimitedIdentifiers() 11 | { 12 | Do $System.SQL.SetDelimitedIdentifiers(0,.old) 13 | Set ..InitialDelimitedIdentifiersSetting = old 14 | Do $$$AssertStatusOK($System.OBJ.CompilePackage("TestCoverage","ck-d/nomulticompile")) 15 | 16 | Set ..Run = ##class(TestCoverage.Data.Run).%New() 17 | Set ..Run.Detail = 1 // Force calculation of rollups 18 | For metric = $$$METRICS { 19 | Do ..Run.Metrics.Insert(metric) 20 | } 21 | Do $$$AssertStatusOK(..Run.%Save()) 22 | 23 | // Other things that are likely to break: 24 | // Dynamic SQL in TestCoverage.Data.Run:MapRunCoverage 25 | Set sc = $$$OK 26 | Try { 27 | Do ##class(TestCoverage.Data.Run).MapRunCoverage(..Run.%Id()) 28 | } Catch e { 29 | Set sc = e.AsStatus() 30 | } 31 | Do $$$AssertStatusOK(sc,"No exceptions thrown by TestCoverage.Data.Run:MapRunCoverage.") 32 | 33 | // Dynamic SQL in TestCoverage.Utils:AggregateCoverage 34 | Set sc = $$$OK 35 | Try { 36 | Do ##class(TestCoverage.Utils).AggregateCoverage(..Run.%Id()) 37 | } Catch e { 38 | Set sc = e.AsStatus() 39 | } 40 | Do $$$AssertStatusOK(sc,"No exceptions thrown by TestCoverage.Utils:AggregateCoverage.") 41 | } 42 | 43 | /// Clean up: Delimited Identifiers setting, run ID 44 | Method %OnClose() As %Status [ Private, ServerOnly = 1 ] 45 | { 46 | Set sc = $$$OK 47 | If (..InitialDelimitedIdentifiersSetting '= "") { 48 | Set sc = $System.SQL.SetDelimitedIdentifiers(..InitialDelimitedIdentifiersSetting,.old) 49 | If (old '= ..InitialDelimitedIdentifiersSetting) { 50 | // Recompile with original setting, just to be safe. 51 | Set sc = $$$ADDSC(sc,$System.OBJ.Compile("TestCoverage","ck-d/nomulticompile")) 52 | } 53 | } 54 | If $IsObject(..Run) { 55 | Set sc = $$$ADDSC(sc,##class(TestCoverage.Data.Run).%DeleteId(..Run.%Id())) 56 | } 57 | Quit sc 58 | } 59 | 60 | } 61 | 62 | -------------------------------------------------------------------------------- /internal/testing/unit_tests/UnitTest/TestCoverage/Unit/TestIsExecutable.cls: -------------------------------------------------------------------------------- 1 | Class UnitTest.TestCoverage.Unit.TestIsExecutable Extends %UnitTest.TestCase 2 | { 3 | 4 | Method TestTryCatch() 5 | { 6 | Do ..RunTestOnMethod("TryCatch1") 7 | Do ..RunTestOnMethod("TryCatch2") 8 | } 9 | 10 | Method TestDo() 11 | { 12 | Do ..RunTestOnMethod("Do1") 13 | Do ..RunTestOnMethod("Do2") 14 | } 15 | 16 | Method RunTestOnMethod(pMethodName As %Dictionary.CacheIdentifier) 17 | { 18 | Do $$$LogMessage("Testing executable flags for method: "_pMethodName) 19 | Merge tLines = ^oddDEF($classname(),"m",pMethodName,$$$cMETHimplementation) 20 | Do ##class(TestCoverage.Utils).GetRoutineLineExecutableFlags(.tLines,.tFlags) 21 | For tLine=1:1:tLines { 22 | Set tExecutable = $Extract(tLines(tLine),*) 23 | Set tRealLine = $Replace($ZStrip($Extract(tLines(tLine),1,*-2),">W"),$c(9)," ") 24 | Set tMsg = "Line "_$Select(tExecutable:"was",1:"NOT")_" executable: "_tRealLine 25 | Do $$$AssertEquals($Get(tFlags(tLine)),tExecutable,tMsg) 26 | } 27 | } 28 | 29 | ClassMethod TryCatch1() 30 | { 31 | Try { ;0 32 | Set foo = "Bar" ;1 33 | } Catch e { ;1 34 | Write e.AsStatus() ;1 35 | } ;0 36 | } 37 | 38 | ClassMethod TryCatch2() 39 | { 40 | Try { ;0 41 | Set foo = "Bar" ;1 42 | } ;1 43 | Catch e { ;0 44 | Write e.AsStatus() ;1 45 | } ;0 46 | } 47 | 48 | ClassMethod Do1() 49 | { 50 | Do { ;0 51 | Set foo = "Bar" ;1 52 | Do ..TryCatch1() ;1 53 | } While 0 ;1 54 | } 55 | 56 | ClassMethod Do2() 57 | { 58 | Do ;0 59 | { ;0 60 | Set foo = "Bar" ;1 61 | Do ..TryCatch1() ;1 62 | } ;0 63 | While 0 ;1 64 | } 65 | 66 | } 67 | 68 | -------------------------------------------------------------------------------- /internal/testing/unit_tests/UnitTest/TestCoverage/Unit/coverage.list: -------------------------------------------------------------------------------- 1 | TestCoverage.PKG 2 | -TestCoverage.UI.PKG -------------------------------------------------------------------------------- /internal/testing/unit_tests/UnitTest/TestCoverage/Unit/sampleRoutine.mac: -------------------------------------------------------------------------------- 1 | ROUTINE UnitTest.TestCoverage.Unit.sampleRoutine 2 | UnitTestTestCoverageUnitsampleRoutine ; -------------------------------------------------------------------------------- /internal/testing/unit_tests/UnitTest/TestCoverage/Unit/samplecovlist.list: -------------------------------------------------------------------------------- 1 | TestCoverage.Data.PKG 2 | - TestCoverage.Data.Aggregate.PKG 3 | // this is a comment 4 | UnitTest*.mac 5 | TestCoverage.Manager.cls -------------------------------------------------------------------------------- /internal/testing/unit_tests/UnitTest/coverage.list: -------------------------------------------------------------------------------- 1 | TestCoverage.PKG 2 | -TestCoverage.UI.PKG -------------------------------------------------------------------------------- /isc.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileList": "TestCoverage*.CLS,TestCoverage.INC", 3 | "projectName": "TestCoverage" 4 | } -------------------------------------------------------------------------------- /module.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TestCoverage 5 | 4.0.5 6 | 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. 7 | module 8 | 9 | 10 | 11 | 12 | Module 13 | . 14 | 15 | 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | radon==6.* --------------------------------------------------------------------------------