Skip to content

Commit

Permalink
codecoverage: initial support for code coverage in LCOV format (#181)
Browse files Browse the repository at this point in the history
  • Loading branch information
brig authored Dec 5, 2024
1 parent ea4e6c1 commit dac0044
Show file tree
Hide file tree
Showing 9 changed files with 784 additions and 0 deletions.
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
<module>tasks/xml</module>
<module>tasks/zoom</module>
<module>runtime/opentelemetry</module>
<module>runtime/codecoverage</module>
</modules>

<properties>
Expand Down
21 changes: 21 additions & 0 deletions runtime/codecoverage/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# codecoverage

A plugin for Concord runtime-v2 that adds code coverage capabilities.

## Usage

To use the plugin, add the following dependency to your Concord process:

```yaml
configuration:
dependencies:
- mvn://com.walmartlabs.concord.plugins:codecoverage:<VERSION>
```
## Generating HTML report with LCOV
The plugin produces a file in [the LCOV format](https://github.com/linux-test-project/lcov).
1. Download coverage info: `/api/v1/process/${INSTANCE_ID}/attachment/coverage.info`
2. Download and unzip process flows: `/api/v1/process/${INSTANCE_ID}/attachment/flows.zip`
3. Generate HTML with: `genhtml "coverage.info" --output-directory "html"`
142 changes: 142 additions & 0 deletions runtime/codecoverage/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">

<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.walmartlabs.concord.plugins</groupId>
<artifactId>concord-plugins-parent</artifactId>
<version>2.6.1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

<artifactId>codecoverage</artifactId>
<packaging>takari-jar</packaging>

<properties>
</properties>

<dependencies>
<dependency>
<groupId>com.walmartlabs.concord</groupId>
<artifactId>concord-common</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.walmartlabs.concord</groupId>
<artifactId>concord-client2</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.walmartlabs.concord.runtime.v2</groupId>
<artifactId>concord-runtime-model-v2</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.walmartlabs.concord.runtime.v2</groupId>
<artifactId>concord-runner-v2</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.walmartlabs.concord.runtime.v2</groupId>
<artifactId>concord-runtime-vm-v2</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.walmartlabs.concord</groupId>
<artifactId>concord-sdk</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.walmartlabs.concord.runtime.v2</groupId>
<artifactId>concord-runtime-sdk-v2</artifactId>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<scope>provided</scope>
</dependency>

<!-- Immutables -->
<dependency>
<groupId>org.immutables</groupId>
<artifactId>value</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.immutables</groupId>
<artifactId>builder</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_annotations</artifactId>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>dev.ybrig.concord</groupId>
<artifactId>concord-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package com.walmartlabs.concord.plugins.codecoverage;

/*-
* *****
* Concord
* -----
* Copyright (C) 2017 - 2024 Walmart Inc.
* -----
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* =====
*/

import com.walmartlabs.concord.common.IOUtils;
import com.walmartlabs.concord.runtime.v2.ProcessDefinitionUtils;
import com.walmartlabs.concord.runtime.v2.model.FlowCall;
import com.walmartlabs.concord.runtime.v2.model.ProcessDefinition;
import com.walmartlabs.concord.runtime.v2.model.Step;
import com.walmartlabs.concord.runtime.v2.runner.PersistenceService;
import com.walmartlabs.concord.runtime.v2.runner.vm.ElementEventProducer;
import com.walmartlabs.concord.runtime.v2.runner.vm.FlowCallCommand;
import com.walmartlabs.concord.runtime.v2.runner.vm.TaskCallCommand;
import com.walmartlabs.concord.runtime.v2.sdk.WorkingDirectory;
import com.walmartlabs.concord.svm.*;
import com.walmartlabs.concord.svm.Runtime;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Objects;
import java.util.stream.Collectors;

public class CodeCoverage implements ExecutionListener {

public static final Logger log = LoggerFactory.getLogger(CodeCoverage.class);

private static final String COVERAGE_INFO_FILENAME = "coverage.info";
private static final String FLOWS_FILENAME = "flows.zip";

private final StepsRecorder steps;
private final PersistenceService persistenceService;
private final Path workDir;

@Inject
public CodeCoverage(StepsRecorder steps, PersistenceService persistenceService, WorkingDirectory workingDirectory) {
this.steps = steps;
this.persistenceService = persistenceService;
this.workDir = workingDirectory.getValue();
}

@Override
public void beforeProcessStart(Runtime runtime, State state) {
saveFlows(runtime.getService(ProcessDefinition.class));
}

@Override
public Result beforeCommand(Runtime runtime, VM vm, State state, ThreadId threadId, Command cmd) {
// we need the name of the flow, so we can handle the call step only in `afterCommand`
if (cmd instanceof FlowCallCommand) {
return Result.CONTINUE;
}

if (cmd instanceof ElementEventProducer eep) {
processStep(eep.getStep(), runtime, state, threadId);
} else if (cmd instanceof TaskCallCommand tcc) {
processStep(tcc.getStep(), runtime, state, threadId);
}

return Result.CONTINUE;
}

@Override
public Result afterCommand(Runtime runtime, VM vm, State state, ThreadId threadId, Command cmd) {
if (cmd instanceof FlowCallCommand fcc) {
processStep(fcc.getStep(), runtime, state, threadId);
}
return Result.CONTINUE;
}

private void processStep(Step step, Runtime runtime, State state, ThreadId threadId) {
var loc = step.getLocation();
if (loc == null || loc.lineNum() < 0 || loc.fileName() == null) {
return;
}

var pd = runtime.getService(ProcessDefinition.class);

steps.record(StepInfo.builder()
.fileName(Objects.requireNonNull(loc.fileName()))
.line(loc.lineNum())
.processDefinitionId(ProcessDefinitionUtils.getCurrentFlowName(pd, step))
.flowCallName(flowCallName(step, state, threadId))
.build());
}

@Override
public void onProcessError(Runtime runtime, State state, Exception e) {
generateReport(runtime);
}

@Override
public void afterProcessEnds(Runtime runtime, State state, Frame lastFrame) {
if (isSuspended(state)) {
return;
}

generateReport(runtime);
}

private void generateReport(Runtime runtime) {
log.info("Generating code coverage info...");

try {
var reportProducer = new LcovReportProducer(runtime.getService(ProcessDefinition.class));
reportProducer.onSteps(steps.list());

steps.cleanup();

persistenceService.persistFile(COVERAGE_INFO_FILENAME, reportProducer::produce, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
} catch (Exception e) {
throw new RuntimeException("Can't generate code coverage report", e);
}

log.info("Coverage info saved as attachment with name '{}'", COVERAGE_INFO_FILENAME);
}

private static boolean isSuspended(State state) {
return state.threadStatus().entrySet().stream()
.anyMatch(e -> e.getValue() == ThreadStatus.SUSPENDED);
}

private static String flowCallName(Step step, State state, ThreadId threadId) {
if (step instanceof FlowCall) {
return FlowCallCommand.getFlowName(state, threadId);
}
return null;
}

private void saveFlows(ProcessDefinition processDefinition) {
var fileNames = processDefinition.flows().values().stream()
.map(v -> v.location().fileName())
.filter(Objects::nonNull)
.collect(Collectors.toSet());

persistenceService.persistFile(FLOWS_FILENAME, out -> {
try (ZipArchiveOutputStream zip = new ZipArchiveOutputStream(out)) {

for (var fileName : fileNames) {
var file = workDir.resolve(fileName);
if (Files.notExists(file)) {
log.warn("CodeCoverage: can't save flow '{}' -> file not exists. This is most likely a bug", fileName);
continue;
}

try {
IOUtils.zipFile(zip, file, fileName);
} catch (IOException ex) {
log.error("CodeCoverage: failed to add file '{}'. Error: {}", fileName, ex.getMessage());
throw ex;
}
}
}
});
log.debug("CodeCoverage: flows saved as '{}' process attachment", FLOWS_FILENAME);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.walmartlabs.concord.plugins.codecoverage;

/*-
* *****
* Concord
* -----
* Copyright (C) 2017 - 2024 Walmart Inc., Concord Authors
* -----
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* =====
*/

import com.google.inject.Binder;
import com.google.inject.Module;
import com.google.inject.multibindings.Multibinder;
import com.walmartlabs.concord.svm.ExecutionListener;

import javax.inject.Named;

@Named
public class CodecoverageModule implements Module {

@Override
public void configure (Binder binder){
var executionListeners = Multibinder.newSetBinder(binder, ExecutionListener.class);
executionListeners.addBinding().to(CodeCoverage.class);
}
}
Loading

0 comments on commit dac0044

Please sign in to comment.