-
Notifications
You must be signed in to change notification settings - Fork 154
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add script to generate SonarCloud test coverage report (#222)
Add script to generate SonarCloud test coverage report Signed-off-by: Kuba Odias <[email protected]>
- Loading branch information
Kuba Odias
authored
May 26, 2023
1 parent
c903509
commit b63c03a
Showing
5 changed files
with
151 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
# OPA Test Coverage to SonarCloud Format | ||
|
||
Example code to transform the OPA test coverage JSON report to | ||
the [SonarCloud](https://docs.sonarqube.org/latest/analyzing-source-code/test-coverage/generic-test-data/) coverage report format. | ||
|
||
## Why? | ||
|
||
SonarCloud allows collecting test coverage data to generate rich reports. Integration can be done using [generic test data format](https://docs.sonarqube.org/latest/analyzing-source-code/test-coverage/generic-test-data/). | ||
|
||
## Example | ||
1. generate json format coverage result with `--coverage` and pipe it into a file | ||
2. call `python opa_coverage_to_sonarcloud.py <input json> <output xml>` to generate xml format coverage report | ||
```shell | ||
$ opa test --coverage example > coverage.json | ||
$ python opa_coverage_to_sonarcloud.py coverage.json coverage.xml | ||
``` | ||
**Output** | ||
```xml | ||
<?xml version='1.0' encoding='utf-8'?> | ||
<coverage version="1"> | ||
<file path="policy.rego"> | ||
<lineToCover lineNumber="3" covered="true" /> | ||
<lineToCover lineNumber="4" covered="true" /> | ||
<lineToCover lineNumber="7" covered="false" /> | ||
<lineToCover lineNumber="8" covered="false" /> | ||
</file> | ||
<file path="policy_test.rego"> | ||
<lineToCover lineNumber="3" covered="true" /> | ||
<lineToCover lineNumber="4" covered="true" /> | ||
</file> | ||
</coverage> | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package policy | ||
|
||
deny["foo"] { | ||
input.foo == "bar" | ||
} | ||
|
||
deny["bar"] { | ||
input.bar == "foo" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package policy | ||
|
||
test_deny { | ||
deny with input as {"foo": "bar"} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
import argparse | ||
from dataclasses import dataclass, field | ||
from typing import List | ||
import json | ||
import xml.etree.ElementTree as ET | ||
import os | ||
import time | ||
import math | ||
|
||
|
||
@dataclass | ||
class RowInfo: | ||
row: int | ||
|
||
|
||
@dataclass | ||
class LineCoverage: | ||
start: RowInfo | ||
end: RowInfo | ||
|
||
|
||
@dataclass | ||
class BaseCoverage: | ||
coverage: int | ||
covered_lines: int = 0 | ||
not_covered_lines: int = 0 | ||
|
||
|
||
@dataclass | ||
class FileCoverage(BaseCoverage): | ||
covered: List[LineCoverage] = field(default_factory=list) | ||
not_covered: List[LineCoverage] = field(default_factory=list) | ||
|
||
|
||
@dataclass | ||
class OPACoverage(BaseCoverage): | ||
files: dict[str, FileCoverage] = field(default_factory=dict) | ||
|
||
@property | ||
def lines_valid(self): | ||
return self.covered_lines + self.not_covered_lines | ||
|
||
|
||
def generate_sonarcloud_xml(coverage: OPACoverage) -> ET.Element: | ||
coverage_xml = ET.Element("coverage", attrib={ | ||
"version": "1", | ||
}) | ||
for path, data in coverage.files.items(): | ||
if isinstance(data, dict): | ||
if not "coverage" in data: | ||
data["coverage"] = 0 | ||
data = FileCoverage(**data) | ||
file_data = ET.Element("file", attrib={ | ||
"path": path, | ||
}) | ||
coverage_xml.append(file_data) | ||
cover_map = {} | ||
max_line = 1 | ||
for c in data.covered: | ||
if isinstance(c, dict): | ||
c = LineCoverage(**c) | ||
if isinstance(c.start, dict): | ||
c.start = RowInfo(**c.start) | ||
if isinstance(c.end, dict): | ||
c.end = RowInfo(**c.end) | ||
max_line = max(max_line, c.end.row) | ||
for i in range(c.start.row, c.end.row+1): | ||
cover_map[i] = 1 | ||
for c in data.not_covered: | ||
if isinstance(c, dict): | ||
c = LineCoverage(**c) | ||
if isinstance(c.start, dict): | ||
c.start = RowInfo(**c.start) | ||
if isinstance(c.end, dict): | ||
c.end = RowInfo(**c.end) | ||
max_line = max(max_line, c.end.row) | ||
for i in range(c.start.row, c.end.row+1): | ||
cover_map[i] = 0 | ||
for i in range(1, max_line+1): | ||
if cover_map.get(i) is None: | ||
continue | ||
ET.SubElement(file_data, "lineToCover", attrib={ | ||
"lineNumber": str(i), | ||
"covered": "true" if (cover_map[i] == 1) else "false", | ||
}) | ||
return coverage_xml | ||
|
||
|
||
if __name__ == "__main__": | ||
# init args | ||
parser = argparse.ArgumentParser( | ||
prog='opa-coverage-to-sonarcloud', | ||
description='Convert opa coverage report to SonarCloud format') | ||
parser.add_argument("input", help='input json file') | ||
parser.add_argument("output", help='output xml file') | ||
args = parser.parse_args() | ||
with open(args.input, encoding="utf-8") as fp: | ||
content = json.load(fp) | ||
overall_coverage = OPACoverage(**content) | ||
sonarcloud_xml = generate_sonarcloud_xml(overall_coverage) | ||
|
||
tree = ET.ElementTree(sonarcloud_xml) | ||
ET.indent(tree) | ||
tree.write(args.output, encoding="utf-8", xml_declaration=True) |