Skip to content

Commit

Permalink
feat: added call activity binding type and version tag verification
Browse files Browse the repository at this point in the history
feat: enabled versioned resource deployments and versioned process simulation
  • Loading branch information
gclaussn committed Mar 2, 2025
1 parent bbfe06d commit d72514e
Show file tree
Hide file tree
Showing 21 changed files with 691 additions and 9 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ For information on how to install, configure and use the plugins visit:
### Details
| Feature | Camunda Platform 7 | Camunda Platform 8 |
|:--------|:-------------------|:-------------------|
| Call activity support | Supported via stubbing - see [test](integration-tests/advanced/src/test/java/org/example/it/CallActivityWithMappingTest.java) | Supported via simulation. `TestCaseExecutor#simulateProcess` must be called for every BPMN process ID that should be simulated - see [test](integration-tests-8/simple/src/test/java/org/example/it/SimpleCallActivityTest.java) |
| Call activity support | Supported via automatic stubbing - see [test](integration-tests/advanced/src/test/java/org/example/it/CallActivityWithMappingTest.java) | Supported via simulation. `TestCaseExecutor#simulateProcess` must be called for every BPMN process ID that should be simulated - see [test](integration-tests-8/simple/src/test/java/org/example/it/SimpleCallActivityTest.java). If binding type is **version tag**, `TestCaseExecutor#simulateVersionedProcess` must be called - see [test](integration-tests-8/advanced/src/test/java/org/example/it/CallActivityBindingVersionTagTest.java) |
| Multi instance support | Multi instance activities and embedded subprocesses are supported - see [tests](integration-tests/advanced-multi-instance/src/test/java/org/example/it) | No test code generation implemented yet. But a possibility to write custom test code to handle and verify multi instances exists - see [test](integration-tests-8/advanced-multi-instance/src/test/java/org/example/it/ScopeSequentialTest.java) |
| Spring/Spring Boot test support | Supported - see `advanced-spring*` projects under [integration tests](integration-tests/) | Not needed, since the `TestCaseExecutor` requires only a `ZeebeTestEngine` instance that can be injected via `@ZeebeProcessTest` or `@ZeebeSpringTest` or be manually created |
| Spring/Spring Boot test support | Supported - see `advanced-spring*` projects under [integration tests](integration-tests/) | Not needed, since the `TestCaseExecutor` requires only a `ZeebeTestEngine` instance. It can be injected via `@ZeebeProcessTest` or `@ZeebeSpringTest` or be manually created |
| Process test coverage extension support | Supported - see `coverage*` projects under [integration tests](integration-tests/) | Not verified yet |

### Handler
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import org.camunda.community.bpmndt.api.AbstractJUnit5TestCase;
import org.camunda.community.bpmndt.api.AbstractTestCase;
import org.camunda.community.bpmndt.api.CallActivityBindingType;
import org.camunda.community.bpmndt.api.CallActivityHandler;
import org.camunda.community.bpmndt.api.CustomMultiInstanceHandler;
import org.camunda.community.bpmndt.api.JobHandler;
Expand Down Expand Up @@ -90,6 +91,7 @@ public void generate(GeneratorContext ctx) {

apiClasses.add(AbstractJUnit5TestCase.class);
apiClasses.add(AbstractTestCase.class);
apiClasses.add(CallActivityBindingType.class);
apiClasses.add(CallActivityHandler.class);
apiClasses.add(CustomMultiInstanceHandler.class);
apiClasses.add(JobHandler.class);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.camunda.community.bpmndt.api;

/**
* Possible binding type values.
*/
public enum CallActivityBindingType {

DEPLOYMENT,
LATEST,
VERSION_TAG
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,15 @@ public class CallActivityHandler {
private Object variables;
private boolean waitForBoundaryEvent;

private Consumer<CallActivityBindingType> bindingTypeConsumer;
private Consumer<String> processIdExpressionConsumer;
private Consumer<String> versionTagConsumer;

private CallActivityBindingType expectedBindingType;
private String expectedProcessId;
private Boolean expectedPropagateAllChildVariables;
private Boolean expectedPropagateAllParentVariables;
private String expectedVersionTag;

private Consumer<String> processIdConsumer;

Expand Down Expand Up @@ -70,10 +74,26 @@ void apply(TestCaseInstance instance, long flowScopeKey) {
verifier.accept(new ProcessInstanceAssert(processInstanceKey, BpmnAssert.getRecordStream()));
}

if (bindingTypeConsumer != null) {
bindingTypeConsumer.accept(element.bindingType);
}
if (expectedBindingType != null && expectedBindingType != element.bindingType) {
var message = "expected call activity %s to have binding type %s, but was %s";
throw new AssertionError(String.format(message, element.id, expectedBindingType, element.bindingType));
}

if (processIdExpressionConsumer != null) {
processIdExpressionConsumer.accept(element.processId);
}

if (versionTagConsumer != null) {
versionTagConsumer.accept(element.versionTag);
}
if (expectedVersionTag != null && !expectedVersionTag.equals(element.versionTag)) {
var message = "expected call activity %s to have version tag '%s', but was '%s'";
throw new AssertionError(String.format(message, element.id, expectedVersionTag, element.versionTag));
}

var calledProcessInstance = getCalledProcessInstance(instance, flowScopeKey);
var job = instance.getJob(calledProcessInstance.key, SIMULATE_ELEMENT_ID);

Expand Down Expand Up @@ -210,6 +230,28 @@ public CallActivityHandler verify(Consumer<ProcessInstanceAssert> verifier) {
return this;
}

/**
* Verifies the call activity's binding type.
*
* @param expectedBindingType The expected binding type.
* @return The handler.
*/
public CallActivityHandler verifyBindingType(CallActivityBindingType expectedBindingType) {
this.expectedBindingType = expectedBindingType;
return this;
}

/**
* Verifies the call activity's binding type, using a consumer.
*
* @param bindingTypeConsumer A consumer asserting the binding type.
* @return The handler.
*/
public CallActivityHandler verifyBindingType(Consumer<CallActivityBindingType> bindingTypeConsumer) {
this.bindingTypeConsumer = bindingTypeConsumer;
return this;
}

/**
* Verifies the call activity's input propagation, which parent variables have been propagated to the child process instance.
*
Expand Down Expand Up @@ -289,6 +331,28 @@ public CallActivityHandler verifyPropagateAllParentVariables(Boolean expectedPro
return this;
}

/**
* Verifies the call activity's version tag.
*
* @param expectedVersionTag The expected version tag, when binding type is "version tag".
* @return The handler.
*/
public CallActivityHandler verifyVersionTag(String expectedVersionTag) {
this.expectedVersionTag = expectedVersionTag;
return this;
}

/**
* Verifies the call activity's version tag, using a consumer.
*
* @param versionTagConsumer A consumer asserting the version tag, when binding type is "version tag".
* @return The handler.
*/
public CallActivityHandler verifyVersionTag(Consumer<String> versionTagConsumer) {
this.versionTagConsumer = versionTagConsumer;
return this;
}

/**
* Applies no action at the wait state. This is required when waiting for events (e.g. message, signal or timer events) that are attached as boundary events
* on the element itself or on the surrounding scope (e.g. embedded subprocess).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

import com.fasterxml.jackson.databind.ObjectMapper;
Expand Down Expand Up @@ -49,6 +51,7 @@ public class TestCaseExecutor {

private final List<String> additionalResourceNames = new ArrayList<>(0);
private final List<String> additionalResources = new ArrayList<>(0);
private final List<String> additionalResourceVersionTags = new ArrayList<>(0);

private ObjectMapper objectMapper;
private long waitTimeout = 5000;
Expand Down Expand Up @@ -90,6 +93,7 @@ public TestCaseExecutor customize(Consumer<TestCaseExecutor> customizer) {
*/
public long execute() {
try (ZeebeClient client = createClient()) {
deployVersionedResources(client);

var deployResourceCommandStep1 = client.newDeployResourceCommand();

Expand All @@ -102,10 +106,16 @@ public long execute() {
}

for (int i = 0; i < additionalResources.size(); i++) {
var versionTag = additionalResourceVersionTags.get(i);
if (versionTag != null) {
// skip versioned resources
continue;
}

var resourceName = additionalResourceNames.get(i);
var resource = additionalResources.get(i);

deployResourceCommandStep2.addResourceStringUtf8(resource, resourceName);
deployResourceCommandStep2 = deployResourceCommandStep2.addResourceStringUtf8(resource, resourceName);
}

if (tenantId != null) {
Expand Down Expand Up @@ -277,12 +287,39 @@ public void execute(long processInstanceKey) {
*
* @param processId The ID of the process to simulate.
* @return The executor.
* @see #simulateVersionedProcess(String, String)
*/
public TestCaseExecutor simulateProcess(String processId) {
if (processId == null || processId.isBlank()) {
throw new IllegalArgumentException("process ID is null or blank");
}
var resource = simulateSubProcessResource.replace("processId", processId);
return withAdditionalResource(processId + ".bpmn", resource);
}

/**
* Simulates the process with the given ID by adding a stub process to a separate versioned resource deployment.
*
* @param processId The ID of the process to simulate.
* @param versionTag A version tag, corresponding to a call activity's version tag.
* @return The executor.
* @see #simulateProcess(String)
*/
public TestCaseExecutor simulateVersionedProcess(String processId, String versionTag) {
if (processId == null || processId.isBlank()) {
throw new IllegalArgumentException("process ID is null or blank");
}
if (versionTag == null || versionTag.isBlank()) {
throw new IllegalArgumentException("version tag is null or blank");
}

var resource = simulateSubProcessResource
.replace("processId", processId)
.replace("processVersion", versionTag);

return withAdditionalVersionedResource(processId + ".bpmn", resource, versionTag);
}

/**
* Verifies the state after the test case execution has finished.
*
Expand All @@ -300,10 +337,44 @@ public TestCaseExecutor verify(Consumer<ProcessInstanceAssert> verifier) {
* @param resourceName Name of the resource.
* @param resource The resource as UTF-8 string.
* @return The executor.
* @see #withAdditionalVersionedResource(String, String, String)
*/
public TestCaseExecutor withAdditionalResource(String resourceName, String resource) {
if (resourceName == null || resourceName.isBlank()) {
throw new IllegalArgumentException("resource name is null or blank");
}
if (resource == null) {
throw new IllegalArgumentException("resource is null");
}
additionalResourceNames.add(resourceName);
additionalResources.add(resource);
additionalResourceVersionTags.add(null);
return this;
}

/**
* Adds a resource to a separate versioned resource deployment ({@link ZeebeClient#newDeployResourceCommand()}). Versioned resources are needed to test
* business rule tasks with a DMN decision or user tasks with a form that have the binding type "version tag".
*
* @param resourceName Name of the resource.
* @param resource The resource as UTF-8 string.
* @param versionTag A specific version tag.
* @return The executor.
* @see #withAdditionalResource(String, String)
*/
public TestCaseExecutor withAdditionalVersionedResource(String resourceName, String resource, String versionTag) {
if (resourceName == null || resourceName.isBlank()) {
throw new IllegalArgumentException("resource name is null or blank");
}
if (resource == null) {
throw new IllegalArgumentException("resource is null");
}
if (versionTag == null || versionTag.isBlank()) {
throw new IllegalArgumentException("version tag is null or blank");
}
additionalResourceNames.add(resourceName);
additionalResources.add(resource);
additionalResourceVersionTags.add(versionTag);
return this;
}

Expand Down Expand Up @@ -415,6 +486,39 @@ ZeebeClient createClient() {
.build();
}

void deployVersionedResources(ZeebeClient client) {
var versionTags = additionalResourceVersionTags.stream().filter(Objects::nonNull).collect(Collectors.toSet());
for (String versionTag : versionTags) {
var deployResourceCommandStep1 = client.newDeployResourceCommand();

DeployResourceCommandStep2 deployResourceCommandStep2 = null;
for (int i = 0; i < additionalResources.size(); i++) {
if (!versionTag.equals(additionalResourceVersionTags.get(i))) {
continue;
}

var resourceName = additionalResourceNames.get(i);
var resource = additionalResources.get(i);

if (deployResourceCommandStep2 == null) {
deployResourceCommandStep2 = deployResourceCommandStep1.addResourceStringUtf8(resource, resourceName);
} else {
deployResourceCommandStep2 = deployResourceCommandStep2.addResourceStringUtf8(resource, resourceName);
}
}

if (deployResourceCommandStep2 == null) {
continue;
}

if (tenantId != null) {
deployResourceCommandStep2 = deployResourceCommandStep2.tenantId(tenantId);
}

deployResourceCommandStep2.send().join();
}
}

void executeTestCase(ZeebeClient client, long processInstanceKey) {
try (TestCaseInstance testCaseInstance = new TestCaseInstance(engine, client, waitTimeout, printRecordStreamEnabled)) {
testCase.execute(testCaseInstance, processInstanceKey);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ public abstract class TestCaseInstanceElement {

public static class CallActivityElement extends TestCaseInstanceElement {

public CallActivityBindingType bindingType;
public String processId;
public boolean propagateAllChildVariables;
public boolean propagateAllParentVariables;
public String versionTag;
}

public static class JobElement extends TestCaseInstanceElement {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.camunda.community.bpmndt.strategy;

import org.camunda.community.bpmndt.api.CallActivityBindingType;
import org.camunda.community.bpmndt.api.TestCaseInstanceElement.CallActivityElement;
import org.camunda.community.bpmndt.model.BpmnElement;
import org.camunda.community.bpmndt.model.BpmnElementType;
Expand All @@ -9,6 +10,7 @@
import com.squareup.javapoet.TypeName;

import io.camunda.zeebe.model.bpmn.instance.BoundaryEvent;
import io.camunda.zeebe.model.bpmn.instance.zeebe.ZeebeBindingType;
import io.camunda.zeebe.model.bpmn.instance.zeebe.ZeebeCalledElement;

public class CallActivityStrategy extends DefaultHandlerStrategy {
Expand Down Expand Up @@ -62,12 +64,37 @@ public void initHandlerElement(MethodSpec.Builder methodBuilder) {

var calledElement = (ZeebeCalledElement) extensionElements.getUniqueChildElementByType(ZeebeCalledElement.class);
if (calledElement != null) {
var bindingType = mapBindingType(calledElement.getBindingType());
if (bindingType != null) {
methodBuilder.addStatement("$LElement.bindingType = $T.$L", literal, CallActivityBindingType.class, bindingType.name());
}

if (calledElement.getProcessId() != null) {
methodBuilder.addStatement("$LElement.processId = $S", literal, calledElement.getProcessId());
}

if (calledElement.getVersionTag() != null) {
methodBuilder.addStatement("$LElement.versionTag = $S", literal, calledElement.getVersionTag());
}

methodBuilder.addStatement("$LElement.propagateAllChildVariables = $L", literal, calledElement.isPropagateAllChildVariablesEnabled());
methodBuilder.addStatement("$LElement.propagateAllParentVariables = $L", literal, calledElement.isPropagateAllParentVariablesEnabled());
}
}

private CallActivityBindingType mapBindingType(ZeebeBindingType source) {
if (source == null) {
return CallActivityBindingType.LATEST;
}
switch (source) {
case deployment:
return CallActivityBindingType.DEPLOYMENT;
case latest:
return CallActivityBindingType.LATEST;
case versionTag:
return CallActivityBindingType.VERSION_TAG;
default:
return null;
}
}
}
5 changes: 4 additions & 1 deletion impl-8/src/main/resources/simulate-sub-process.bpmn
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:zeebe="http://camunda.org/schema/zeebe/1.0" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="definitions" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.20.0" modeler:executionPlatform="Camunda Cloud" modeler:executionPlatformVersion="8.4.0">
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:zeebe="http://camunda.org/schema/zeebe/1.0" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="definitions" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.31.0" modeler:executionPlatform="Camunda Cloud" modeler:executionPlatformVersion="8.6.0">
<bpmn:process id="processId" isExecutable="true">
<bpmn:extensionElements>
<zeebe:versionTag value="processVersion" />
</bpmn:extensionElements>
<bpmn:startEvent id="startEvent">
<bpmn:outgoing>f1</bpmn:outgoing>
</bpmn:startEvent>
Expand Down
Loading

0 comments on commit d72514e

Please sign in to comment.