Skip to content

Commit f9ef003

Browse files
committed
enable code coverage for opa
1 parent 7ddf6d2 commit f9ef003

File tree

5 files changed

+225
-2
lines changed

5 files changed

+225
-2
lines changed

MODULE.bazel.lock

Lines changed: 100 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

opa/private/opa_check.bzl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def _opa_check_test_impl(ctx):
1414
if ctx.files.schema_files:
1515
files.extend(ctx.files.schema_files)
1616

17-
args = ["set -xe\n"]
17+
args = ["set -e\n"]
1818

1919
args.append(toolchain.opa.short_path)
2020
args.append("check")

opa/private/opa_test.bzl

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,15 @@ def _opa_test_impl(ctx):
1616
if ctx.attr.verbose:
1717
args.append("--verbose")
1818

19+
tool_path = toolchain.opa.short_path
20+
21+
if ctx.configuration.coverage_enabled:
22+
tool_path = "%s %s %s" % (ctx.executable._opa_coverage.short_path, str(ctx.label), tool_path)
23+
runfiles = runfiles.merge(ctx.attr._opa_coverage[DefaultInfo].default_runfiles)
24+
1925
ctx.actions.write(
2026
output = test_file,
21-
content = "%s test %s %s" % (toolchain.opa.short_path, bundle.short_path, " ".join(args + [f.short_path for f in ctx.files.srcs])),
27+
content = "%s test %s %s" % (tool_path, bundle.short_path, " ".join(args + [f.short_path for f in ctx.files.srcs])),
2228
is_executable = True,
2329
)
2430

@@ -27,6 +33,7 @@ def _opa_test_impl(ctx):
2733
executable = test_file,
2834
runfiles = runfiles,
2935
),
36+
coverage_common.instrumented_files_info(ctx, source_attributes = ["srcs"], dependency_attributes = ["deps"]),
3037
]
3138

3239
opa_test = rule(
@@ -51,6 +58,7 @@ opa_test = rule(
5158
doc = "set verbose reporting mode",
5259
default = False,
5360
),
61+
"_opa_coverage": attr.label(default = "//tools:opa_coverage", executable = True, cfg = "exec"),
5462
},
5563
toolchains = ["//tools:toolchain_type"],
5664
)

tools/BUILD.bazel

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ py_binary(
1818
srcs = ["opa_upgrade.py"],
1919
)
2020

21+
py_binary(
22+
name = "opa_coverage",
23+
srcs = ["opa_coverage.py"],
24+
visibility = ["//visibility:public"],
25+
)
26+
2127
toolchain_type(name = "toolchain_type")
2228

2329
opa_toolchain(

tools/opa_coverage.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
from typing import List, Dict, Any, cast
2+
from subprocess import run, PIPE
3+
from dataclasses import dataclass
4+
from os import environ
5+
6+
import json
7+
import sys
8+
9+
10+
@dataclass
11+
class CoverageRangeContent:
12+
row: int
13+
14+
@classmethod
15+
def from_dict(cls, data):
16+
return cls(row=data.get("data") or 0)
17+
18+
19+
@dataclass
20+
class CoverageRange:
21+
start: CoverageRangeContent
22+
end: CoverageRangeContent
23+
24+
@classmethod
25+
def from_dict(cls, data):
26+
return cls(
27+
start=CoverageRangeContent.from_dict(data.get("start")),
28+
end=CoverageRangeContent.from_dict(data.get("end")),
29+
)
30+
31+
32+
@dataclass
33+
class CoverageFile:
34+
covered: List[CoverageRange]
35+
not_covered: List[CoverageRange]
36+
covered_lines: int
37+
not_covered_lines: int
38+
coverage: float
39+
40+
@classmethod
41+
def from_dict(cls, data):
42+
return cls(
43+
covered=[CoverageRange.from_dict(r) for r in data.get("covered") or []],
44+
not_covered=[
45+
CoverageRange.from_dict(r) for r in data.get("not_covered") or []
46+
],
47+
covered_lines=int(data.get("covered_lines") or 0),
48+
not_covered_lines=int(data.get("not_covered_lines") or 0),
49+
coverage=float(data.get("coverage")),
50+
)
51+
52+
53+
@dataclass
54+
class CoverageReport:
55+
files: Dict[str, CoverageFile]
56+
covered_lines: int
57+
not_covered_lines: int
58+
coverage: float
59+
60+
@classmethod
61+
def from_dict(cls, data):
62+
return cls(
63+
files={
64+
file: CoverageFile.from_dict(report)
65+
for file, report in data.get("files").items()
66+
},
67+
covered_lines=int(data.get("covered_lines")),
68+
not_covered_lines=int(data.get("not_covered_lines")),
69+
coverage=float(data.get("coverage")),
70+
)
71+
72+
73+
def main(argv: List[str]):
74+
_program, test_name, opa, cmd, *args = argv
75+
76+
coverage_enabled = bool(environ.get("COVERAGE", 0))
77+
coverage_output_file = environ.get("COVERAGE_OUTPUT_FILE")
78+
79+
assert coverage_enabled
80+
assert coverage_output_file is not None
81+
82+
proc = run([opa, cmd, "--format=json", "--coverage"] + args, stdout=PIPE)
83+
84+
if proc.returncode != 0:
85+
exit(proc.returncode)
86+
87+
result = CoverageReport.from_dict(json.loads(proc.stdout))
88+
89+
with open(coverage_output_file, "w") as f:
90+
for file, report in result.files.items():
91+
if file in args:
92+
continue # Skip test sources
93+
print(f"TN:{test_name.removeprefix('@@')}", file=f)
94+
print(f"SF:{file}", file=f)
95+
print(f"FNF:0", file=f)
96+
print(f"FNH:0", file=f)
97+
98+
for r in report.covered:
99+
for i in range(r.start.row, r.end.row + 1):
100+
print(f"DA:{i},1", file=f)
101+
102+
print(f"LF:{report.covered_lines+report.not_covered_lines}", file=f)
103+
print(f"LH:{report.covered_lines}", file=f)
104+
print("end_of_record", file=f)
105+
print(file=f)
106+
107+
108+
if __name__ == "__main__":
109+
main(sys.argv)

0 commit comments

Comments
 (0)