Skip to content

Commit dac0044

Browse files
authored
codecoverage: initial support for code coverage in LCOV format (#181)
1 parent ea4e6c1 commit dac0044

File tree

9 files changed

+784
-0
lines changed

9 files changed

+784
-0
lines changed

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
<module>tasks/xml</module>
5454
<module>tasks/zoom</module>
5555
<module>runtime/opentelemetry</module>
56+
<module>runtime/codecoverage</module>
5657
</modules>
5758

5859
<properties>

runtime/codecoverage/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# codecoverage
2+
3+
A plugin for Concord runtime-v2 that adds code coverage capabilities.
4+
5+
## Usage
6+
7+
To use the plugin, add the following dependency to your Concord process:
8+
9+
```yaml
10+
configuration:
11+
dependencies:
12+
- mvn://com.walmartlabs.concord.plugins:codecoverage:<VERSION>
13+
```
14+
15+
## Generating HTML report with LCOV
16+
17+
The plugin produces a file in [the LCOV format](https://github.com/linux-test-project/lcov).
18+
19+
1. Download coverage info: `/api/v1/process/${INSTANCE_ID}/attachment/coverage.info`
20+
2. Download and unzip process flows: `/api/v1/process/${INSTANCE_ID}/attachment/flows.zip`
21+
3. Generate HTML with: `genhtml "coverage.info" --output-directory "html"`

runtime/codecoverage/pom.xml

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
3+
4+
<modelVersion>4.0.0</modelVersion>
5+
6+
<parent>
7+
<groupId>com.walmartlabs.concord.plugins</groupId>
8+
<artifactId>concord-plugins-parent</artifactId>
9+
<version>2.6.1-SNAPSHOT</version>
10+
<relativePath>../../pom.xml</relativePath>
11+
</parent>
12+
13+
<artifactId>codecoverage</artifactId>
14+
<packaging>takari-jar</packaging>
15+
16+
<properties>
17+
</properties>
18+
19+
<dependencies>
20+
<dependency>
21+
<groupId>com.walmartlabs.concord</groupId>
22+
<artifactId>concord-common</artifactId>
23+
<scope>provided</scope>
24+
</dependency>
25+
<dependency>
26+
<groupId>com.walmartlabs.concord</groupId>
27+
<artifactId>concord-client2</artifactId>
28+
<scope>provided</scope>
29+
</dependency>
30+
<dependency>
31+
<groupId>com.walmartlabs.concord.runtime.v2</groupId>
32+
<artifactId>concord-runtime-model-v2</artifactId>
33+
<scope>provided</scope>
34+
</dependency>
35+
<dependency>
36+
<groupId>com.walmartlabs.concord.runtime.v2</groupId>
37+
<artifactId>concord-runner-v2</artifactId>
38+
<scope>provided</scope>
39+
</dependency>
40+
<dependency>
41+
<groupId>com.walmartlabs.concord.runtime.v2</groupId>
42+
<artifactId>concord-runtime-vm-v2</artifactId>
43+
<scope>provided</scope>
44+
</dependency>
45+
<dependency>
46+
<groupId>com.walmartlabs.concord</groupId>
47+
<artifactId>concord-sdk</artifactId>
48+
<scope>provided</scope>
49+
</dependency>
50+
<dependency>
51+
<groupId>com.walmartlabs.concord.runtime.v2</groupId>
52+
<artifactId>concord-runtime-sdk-v2</artifactId>
53+
<scope>provided</scope>
54+
</dependency>
55+
56+
<dependency>
57+
<groupId>org.apache.commons</groupId>
58+
<artifactId>commons-compress</artifactId>
59+
<scope>provided</scope>
60+
</dependency>
61+
62+
<dependency>
63+
<groupId>org.slf4j</groupId>
64+
<artifactId>slf4j-api</artifactId>
65+
<scope>provided</scope>
66+
</dependency>
67+
68+
<dependency>
69+
<groupId>javax.inject</groupId>
70+
<artifactId>javax.inject</artifactId>
71+
<scope>provided</scope>
72+
</dependency>
73+
<dependency>
74+
<groupId>com.google.inject</groupId>
75+
<artifactId>guice</artifactId>
76+
<scope>provided</scope>
77+
</dependency>
78+
79+
<dependency>
80+
<groupId>com.fasterxml.jackson.core</groupId>
81+
<artifactId>jackson-core</artifactId>
82+
<scope>provided</scope>
83+
</dependency>
84+
<dependency>
85+
<groupId>com.fasterxml.jackson.core</groupId>
86+
<artifactId>jackson-annotations</artifactId>
87+
<scope>provided</scope>
88+
</dependency>
89+
<dependency>
90+
<groupId>com.fasterxml.jackson.core</groupId>
91+
<artifactId>jackson-databind</artifactId>
92+
<scope>provided</scope>
93+
</dependency>
94+
<dependency>
95+
<groupId>com.fasterxml.jackson.dataformat</groupId>
96+
<artifactId>jackson-dataformat-yaml</artifactId>
97+
<scope>provided</scope>
98+
</dependency>
99+
100+
<!-- Immutables -->
101+
<dependency>
102+
<groupId>org.immutables</groupId>
103+
<artifactId>value</artifactId>
104+
<scope>provided</scope>
105+
</dependency>
106+
<dependency>
107+
<groupId>org.immutables</groupId>
108+
<artifactId>builder</artifactId>
109+
<scope>provided</scope>
110+
</dependency>
111+
<dependency>
112+
<groupId>com.google.code.findbugs</groupId>
113+
<artifactId>jsr305</artifactId>
114+
<scope>provided</scope>
115+
</dependency>
116+
<dependency>
117+
<groupId>com.google.errorprone</groupId>
118+
<artifactId>error_prone_annotations</artifactId>
119+
<scope>provided</scope>
120+
</dependency>
121+
122+
<dependency>
123+
<groupId>org.junit.jupiter</groupId>
124+
<artifactId>junit-jupiter-api</artifactId>
125+
<scope>test</scope>
126+
</dependency>
127+
<dependency>
128+
<groupId>org.junit.jupiter</groupId>
129+
<artifactId>junit-jupiter-engine</artifactId>
130+
<scope>test</scope>
131+
</dependency>
132+
</dependencies>
133+
134+
<build>
135+
<plugins>
136+
<plugin>
137+
<groupId>dev.ybrig.concord</groupId>
138+
<artifactId>concord-maven-plugin</artifactId>
139+
</plugin>
140+
</plugins>
141+
</build>
142+
</project>
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package com.walmartlabs.concord.plugins.codecoverage;
2+
3+
/*-
4+
* *****
5+
* Concord
6+
* -----
7+
* Copyright (C) 2017 - 2024 Walmart Inc.
8+
* -----
9+
* Licensed under the Apache License, Version 2.0 (the "License");
10+
* you may not use this file except in compliance with the License.
11+
* You may obtain a copy of the License at
12+
*
13+
* http://www.apache.org/licenses/LICENSE-2.0
14+
*
15+
* Unless required by applicable law or agreed to in writing, software
16+
* distributed under the License is distributed on an "AS IS" BASIS,
17+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
* See the License for the specific language governing permissions and
19+
* limitations under the License.
20+
* =====
21+
*/
22+
23+
import com.walmartlabs.concord.common.IOUtils;
24+
import com.walmartlabs.concord.runtime.v2.ProcessDefinitionUtils;
25+
import com.walmartlabs.concord.runtime.v2.model.FlowCall;
26+
import com.walmartlabs.concord.runtime.v2.model.ProcessDefinition;
27+
import com.walmartlabs.concord.runtime.v2.model.Step;
28+
import com.walmartlabs.concord.runtime.v2.runner.PersistenceService;
29+
import com.walmartlabs.concord.runtime.v2.runner.vm.ElementEventProducer;
30+
import com.walmartlabs.concord.runtime.v2.runner.vm.FlowCallCommand;
31+
import com.walmartlabs.concord.runtime.v2.runner.vm.TaskCallCommand;
32+
import com.walmartlabs.concord.runtime.v2.sdk.WorkingDirectory;
33+
import com.walmartlabs.concord.svm.*;
34+
import com.walmartlabs.concord.svm.Runtime;
35+
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
36+
import org.slf4j.Logger;
37+
import org.slf4j.LoggerFactory;
38+
39+
import javax.inject.Inject;
40+
import java.io.IOException;
41+
import java.nio.file.Files;
42+
import java.nio.file.Path;
43+
import java.nio.file.StandardOpenOption;
44+
import java.util.Objects;
45+
import java.util.stream.Collectors;
46+
47+
public class CodeCoverage implements ExecutionListener {
48+
49+
public static final Logger log = LoggerFactory.getLogger(CodeCoverage.class);
50+
51+
private static final String COVERAGE_INFO_FILENAME = "coverage.info";
52+
private static final String FLOWS_FILENAME = "flows.zip";
53+
54+
private final StepsRecorder steps;
55+
private final PersistenceService persistenceService;
56+
private final Path workDir;
57+
58+
@Inject
59+
public CodeCoverage(StepsRecorder steps, PersistenceService persistenceService, WorkingDirectory workingDirectory) {
60+
this.steps = steps;
61+
this.persistenceService = persistenceService;
62+
this.workDir = workingDirectory.getValue();
63+
}
64+
65+
@Override
66+
public void beforeProcessStart(Runtime runtime, State state) {
67+
saveFlows(runtime.getService(ProcessDefinition.class));
68+
}
69+
70+
@Override
71+
public Result beforeCommand(Runtime runtime, VM vm, State state, ThreadId threadId, Command cmd) {
72+
// we need the name of the flow, so we can handle the call step only in `afterCommand`
73+
if (cmd instanceof FlowCallCommand) {
74+
return Result.CONTINUE;
75+
}
76+
77+
if (cmd instanceof ElementEventProducer eep) {
78+
processStep(eep.getStep(), runtime, state, threadId);
79+
} else if (cmd instanceof TaskCallCommand tcc) {
80+
processStep(tcc.getStep(), runtime, state, threadId);
81+
}
82+
83+
return Result.CONTINUE;
84+
}
85+
86+
@Override
87+
public Result afterCommand(Runtime runtime, VM vm, State state, ThreadId threadId, Command cmd) {
88+
if (cmd instanceof FlowCallCommand fcc) {
89+
processStep(fcc.getStep(), runtime, state, threadId);
90+
}
91+
return Result.CONTINUE;
92+
}
93+
94+
private void processStep(Step step, Runtime runtime, State state, ThreadId threadId) {
95+
var loc = step.getLocation();
96+
if (loc == null || loc.lineNum() < 0 || loc.fileName() == null) {
97+
return;
98+
}
99+
100+
var pd = runtime.getService(ProcessDefinition.class);
101+
102+
steps.record(StepInfo.builder()
103+
.fileName(Objects.requireNonNull(loc.fileName()))
104+
.line(loc.lineNum())
105+
.processDefinitionId(ProcessDefinitionUtils.getCurrentFlowName(pd, step))
106+
.flowCallName(flowCallName(step, state, threadId))
107+
.build());
108+
}
109+
110+
@Override
111+
public void onProcessError(Runtime runtime, State state, Exception e) {
112+
generateReport(runtime);
113+
}
114+
115+
@Override
116+
public void afterProcessEnds(Runtime runtime, State state, Frame lastFrame) {
117+
if (isSuspended(state)) {
118+
return;
119+
}
120+
121+
generateReport(runtime);
122+
}
123+
124+
private void generateReport(Runtime runtime) {
125+
log.info("Generating code coverage info...");
126+
127+
try {
128+
var reportProducer = new LcovReportProducer(runtime.getService(ProcessDefinition.class));
129+
reportProducer.onSteps(steps.list());
130+
131+
steps.cleanup();
132+
133+
persistenceService.persistFile(COVERAGE_INFO_FILENAME, reportProducer::produce, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
134+
} catch (Exception e) {
135+
throw new RuntimeException("Can't generate code coverage report", e);
136+
}
137+
138+
log.info("Coverage info saved as attachment with name '{}'", COVERAGE_INFO_FILENAME);
139+
}
140+
141+
private static boolean isSuspended(State state) {
142+
return state.threadStatus().entrySet().stream()
143+
.anyMatch(e -> e.getValue() == ThreadStatus.SUSPENDED);
144+
}
145+
146+
private static String flowCallName(Step step, State state, ThreadId threadId) {
147+
if (step instanceof FlowCall) {
148+
return FlowCallCommand.getFlowName(state, threadId);
149+
}
150+
return null;
151+
}
152+
153+
private void saveFlows(ProcessDefinition processDefinition) {
154+
var fileNames = processDefinition.flows().values().stream()
155+
.map(v -> v.location().fileName())
156+
.filter(Objects::nonNull)
157+
.collect(Collectors.toSet());
158+
159+
persistenceService.persistFile(FLOWS_FILENAME, out -> {
160+
try (ZipArchiveOutputStream zip = new ZipArchiveOutputStream(out)) {
161+
162+
for (var fileName : fileNames) {
163+
var file = workDir.resolve(fileName);
164+
if (Files.notExists(file)) {
165+
log.warn("CodeCoverage: can't save flow '{}' -> file not exists. This is most likely a bug", fileName);
166+
continue;
167+
}
168+
169+
try {
170+
IOUtils.zipFile(zip, file, fileName);
171+
} catch (IOException ex) {
172+
log.error("CodeCoverage: failed to add file '{}'. Error: {}", fileName, ex.getMessage());
173+
throw ex;
174+
}
175+
}
176+
}
177+
});
178+
log.debug("CodeCoverage: flows saved as '{}' process attachment", FLOWS_FILENAME);
179+
}
180+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.walmartlabs.concord.plugins.codecoverage;
2+
3+
/*-
4+
* *****
5+
* Concord
6+
* -----
7+
* Copyright (C) 2017 - 2024 Walmart Inc., Concord Authors
8+
* -----
9+
* Licensed under the Apache License, Version 2.0 (the "License");
10+
* you may not use this file except in compliance with the License.
11+
* You may obtain a copy of the License at
12+
*
13+
* http://www.apache.org/licenses/LICENSE-2.0
14+
*
15+
* Unless required by applicable law or agreed to in writing, software
16+
* distributed under the License is distributed on an "AS IS" BASIS,
17+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
* See the License for the specific language governing permissions and
19+
* limitations under the License.
20+
* =====
21+
*/
22+
23+
import com.google.inject.Binder;
24+
import com.google.inject.Module;
25+
import com.google.inject.multibindings.Multibinder;
26+
import com.walmartlabs.concord.svm.ExecutionListener;
27+
28+
import javax.inject.Named;
29+
30+
@Named
31+
public class CodecoverageModule implements Module {
32+
33+
@Override
34+
public void configure (Binder binder){
35+
var executionListeners = Multibinder.newSetBinder(binder, ExecutionListener.class);
36+
executionListeners.addBinding().to(CodeCoverage.class);
37+
}
38+
}

0 commit comments

Comments
 (0)