diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..916cddf --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties + +# Avoid ignoring Maven wrapper jar file (.jar files are usually ignored) +!/.mvn/wrapper/maven-wrapper.jar + +.settings +bin/ +build-reports/ + +# Eclipse +.classpath +.project +.settings/ + +# Intellij +.idea/ +*.iml +*.iws + +# Mac +.DS_Store + +# Maven +log/ +target/ + +.vscode \ No newline at end of file diff --git a/README.md b/README.md index 526bc7b..45317d0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,28 @@ -# camunda-coverage-generation-groovy -Camunda BPM Unit testing coverage generation using BPMNJS, Groovy, and Camunda Test Assertion Libraries +# Camunda Coverage Generation + +A Unit testing tool for generating Camunda BPMN Coverage diagrams using BPMN.js + +The tool uses Groovy and the Camunda Assertion framework. + +# How it works + +The coverage generation helpers provide easy to use methods to take +"Coverage Snapshots" at any point during a unit test. + +Each snapshot generates "Coverage Data" which is used to generate HTML +files in the build target folder. + +The HTML files have all the required data injected into them and are self-contained. +Each .html file uses the bpmnjs renderer loaded through CDN. + +# How to install + +... + +# Usage example with Spock Framework + +... + +# Output Examples + +... \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100755 index 0000000..e71672b --- /dev/null +++ b/pom.xml @@ -0,0 +1,185 @@ + + + 4.0.0 + com.github.digitalstate + camunda-coverage-genertion-groovy + v0.5 + jar + Camunda Converage Generation Groovy + + + UTF-8 + UTF-8 + + 7.9.0 + 1.4.190 + 1.8 + + + + + + + org.camunda.bpm + camunda-bom + pom + import + ${version.camunda} + + + org.camunda.bpm.extension + camunda-bpm-assert + 1.2 + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + ${version.java} + ${version.java} + + + + maven-assembly-plugin + 2.2.1 + + + jar-with-dependencies + + + + + make-assembly + package + + single + + + + + + + + + + org.codehaus.gmavenplus + gmavenplus-plugin + 1.5 + + + compile + + compile + testCompile + + + + + + + + + maven-surefire-plugin + 2.18.1 + + false + + **/*Spec.java + + + + + + + + + + + + org.spockframework + spock-core + 1.1-groovy-2.4 + test + + + + org.codehaus.groovy + groovy-all + 2.4.10 + provided + + + + + org.camunda.bpm + camunda-engine + provided + + + + + org.camunda.bpm + camunda-engine-plugin-spin + test + + + + + org.camunda.spin + camunda-spin-dataformat-json-jackson + test + + + org.camunda.spin + camunda-spin-dataformat-xml-dom + 1.5.1 + test + + + + + org.camunda.bpm.extension + camunda-bpm-assert + provided + + + + + + com.h2database + h2 + test + ${version.h2} + + + + org.camunda.bpm.extension + camunda-bpm-process-test-coverage + 0.3.2 + test + + + + org.slf4j + slf4j-api + 1.7.13 + test + + + org.slf4j + slf4j-simple + 1.7.13 + test + + + + diff --git a/src/main/groovy/io/digitalstate/camunda/coverage/bpmn/BpmnCoverageBuilder.groovy b/src/main/groovy/io/digitalstate/camunda/coverage/bpmn/BpmnCoverageBuilder.groovy new file mode 100644 index 0000000..b73beb3 --- /dev/null +++ b/src/main/groovy/io/digitalstate/camunda/coverage/bpmn/BpmnCoverageBuilder.groovy @@ -0,0 +1,153 @@ +package io.digitalstate.camunda.coverage.bpmn + +import io.digitalstate.camunda.coverage.bpmn.CoverageData +import org.camunda.bpm.engine.ProcessEngine +import org.camunda.bpm.engine.history.HistoricDetail +import org.camunda.bpm.engine.runtime.ProcessInstance +import org.camunda.bpm.model.bpmn.Bpmn +import org.camunda.bpm.model.bpmn.BpmnModelInstance +import org.camunda.bpm.model.bpmn.instance.IntermediateCatchEvent +import org.camunda.bpm.model.bpmn.instance.ReceiveTask +import org.camunda.bpm.model.bpmn.instance.ServiceTask +import org.camunda.bpm.model.bpmn.instance.SequenceFlow +import org.camunda.bpm.model.bpmn.instance.UserTask +import org.camunda.bpm.model.bpmn.instance.camunda.CamundaExecutionListener +import org.camunda.bpm.model.bpmn.instance.camunda.CamundaScript +import org.camunda.bpm.model.bpmn.instance.FlowNode +import groovy.json.StringEscapeUtils + +class BpmnCoverageBuilder { + + // helper method to shorten the .addInputStream params in createDeployment() + static InputStream resourceStream(String path){ + return this.getClassLoader().getResource(path.toString()).newInputStream() + } + + static InputStream sequenceFlowHistoryGenerator(){ + return resourceStream("io/digitalstate/camunda/coverage/${sequenceFlowHistoryFileName()}") + } + static String sequenceFlowHistoryFileName(){ + return "sequenceFlowHistoryEventGenerator.js" + } + + static BpmnModelInstance prepModelForCoverage( String modelPath, + String scriptResource = 'deployment://sequenceFlowHistoryEventGenerator.js', + String scriptFormat = 'javascript') { + + InputStream resource = resourceStream(modelPath) + BpmnModelInstance model = Bpmn.readModelFromStream(resource) + BpmnModelInstance preppedModel = setupSequenceFlowListeners(model, scriptResource, scriptFormat) + return preppedModel + } + + private static BpmnModelInstance addExecutionListener(BpmnModelInstance model, String elementId, String scriptResource, String scriptFormat){ + CamundaExecutionListener extLis = model.newInstance(CamundaExecutionListener.class) + CamundaScript camScript = model.newInstance(CamundaScript.class) + camScript.setCamundaResource(scriptResource) + camScript.setCamundaScriptFormat(scriptFormat) + extLis.setCamundaEvent('take') + extLis.setCamundaScript(camScript) + + BpmnModelInstance newModel = model.getModelElementById(elementId).builder().addExtensionElement(extLis).done() + return newModel + } + + private static BpmnModelInstance setupSequenceFlowListeners(BpmnModelInstance model, String scriptResource, String scriptFormat){ + def sequenceFlows = model.getModelElementsByType(SequenceFlow.class).collect {it.getId()} + BpmnModelInstance newModel = model + sequenceFlows.each { + newModel = addExecutionListener(newModel, it, scriptResource, scriptFormat) + } + return newModel + } + + static generateCoverageData(ProcessEngine processEngine, ProcessInstance processInstance, String coverageDataName = null, Integer coverageDataWeight = 0){ + CoverageData coverageData = new CoverageData() + coverageData.name = coverageDataName + coverageData.weight = coverageDataWeight + + String processInstanceId = processInstance.getProcessInstanceId() +// def reportData = [:] + + // Get Activity Events + def events = processEngine.getHistoryService() + .createHistoricActivityInstanceQuery() + .processInstanceId(processInstanceId) + .finished() + .orderPartiallyByOccurrence() + .asc() + .list() + def activityEvents = events.findAll {it.activityType != 'sequenceFlow' && it.activityType != 'multiInstanceBody'} + .collect {it.activityId} + .countBy {it} + coverageData.activityInstancesFinished = activityEvents + + // Activity Events That are still active + def eventsStillActive = processEngine.getHistoryService() + .createHistoricActivityInstanceQuery() + .processInstanceId(processInstanceId) + .unfinished() + .orderPartiallyByOccurrence() + .asc() + .list() + def activityEventsStillActive = eventsStillActive.findAll {it.activityType != 'sequenceFlow'} + .collect {it.activityId} + coverageData.activityInstancesUnfinished = activityEventsStillActive + + def sequenceFlows = events.findAll {it.activityType == 'sequenceFlow'} + .collect {it.activityId} + coverageData.sequenceFlowsFinished = sequenceFlows + + String processDefinitionId = processEngine.getHistoryService() + .createHistoricProcessInstanceQuery() + .processInstanceId(processInstanceId) + .singleResult() + .getProcessDefinitionId() + + def model = processEngine.getRepositoryService() + .getBpmnModelInstance(processDefinitionId) + + def asyncData = model.getModelElementsByType(FlowNode.class).collect {[ + 'id': it.getId(), + 'asyncBefore': it.isCamundaAsyncBefore().toBoolean(), + 'asyncAfter': it.isCamundaAsyncAfter().toBoolean(), + 'exclusive': it.isCamundaExclusive().toBoolean() + ]} + coverageData.modelAsyncData = asyncData + + def userTasks = model.getModelElementsByType(UserTask.class).collect {it.getId()} + coverageData.modelUserTasks = userTasks + + def receiveTasks = model.getModelElementsByType(ReceiveTask.class).collect {it.getId()} + coverageData.modelReceiveTasks = receiveTasks + + def externalTasks = model.getModelElementsByType(ServiceTask.class).findAll {it.getCamundaType() == 'external'}.collect {it.getId()} + coverageData.modelExternalTasks = externalTasks + + def intermediateCatchEvents = model.getModelElementsByType(IntermediateCatchEvent.class).collect {it.getId()} + + coverageData.modelIntermediateCatchEvents = intermediateCatchEvents + + coverageData.bpmnModel = StringEscapeUtils.escapeJavaScript(Bpmn.convertToString(model)) + + +// Generate Variable Usage / Dataflow data + Collection variableHistory = processEngine.getHistoryService().createHistoricDetailQuery() + .processInstanceId(processInstanceId) + .disableBinaryFetching() + .variableUpdates() + .list() + + List> activityVariableMappings = variableHistory.collect { historyItem -> + [('activityId'): processEngine.getHistoryService().createHistoricActivityInstanceQuery() + .processInstanceId(processInstanceId) + .activityInstanceId(historyItem.getActivityInstanceId()) + .singleResult().getActivityId(), + ('variableInstance') : historyItem.toString() + ] + } + coverageData.activityInstanceVariableMapping = activityVariableMappings + + return coverageData + } +} diff --git a/src/main/groovy/io/digitalstate/camunda/coverage/bpmn/CoverageBuilder.groovy b/src/main/groovy/io/digitalstate/camunda/coverage/bpmn/CoverageBuilder.groovy new file mode 100644 index 0000000..4bed27e --- /dev/null +++ b/src/main/groovy/io/digitalstate/camunda/coverage/bpmn/CoverageBuilder.groovy @@ -0,0 +1,175 @@ +package io.digitalstate.camunda.coverage.bpmn + +import groovy.json.StringEscapeUtils +import io.digitalstate.camunda.coverage.bpmn.bpmnjs.TemplateGeneration +import org.camunda.bpm.engine.history.HistoricDetail +import org.camunda.bpm.engine.runtime.ProcessInstance +import org.camunda.bpm.model.bpmn.Bpmn +import org.camunda.bpm.model.bpmn.instance.FlowNode +import org.camunda.bpm.model.bpmn.instance.IntermediateCatchEvent +import org.camunda.bpm.model.bpmn.instance.ReceiveTask +import org.camunda.bpm.model.bpmn.instance.ServiceTask +import org.camunda.bpm.model.bpmn.instance.UserTask +import static groovy.json.JsonOutput.toJson +import static org.camunda.bpm.engine.test.assertions.ProcessEngineTests.historyService +import static org.camunda.bpm.engine.test.assertions.ProcessEngineTests.repositoryService + +trait CoverageBuilder implements TemplateGeneration{ + + static HashMap coverageSnapshots = [:] + + void coverageSnapshot(ProcessInstance processInstance, String coverageDataName = UUID.randomUUID(), Integer coverageDataWeight = 0){ + CoverageData coverageData = generateCoverageData(processInstance, coverageDataName, coverageDataWeight) + coverageSnapshots.put(coverageDataName, coverageData) + } + + void saveCoverageSnapshots(HashMap data = coverageSnapshots, String buildDir = 'target') { + FileTreeBuilder treeBuilder = new FileTreeBuilder() + + data.eachWithIndex { key, value, index -> + // Generate the compiled template using the CoverageData + String output = compileTemplate(value) + // Determine if the coverage data name is a UUID of a custom name + Closure isUUID = { + try { + UUID.fromString(value.name) + return true + } catch(all) { + return false + } + } + // Setup the file name based on whether the coverage data name is a UUID + Closure fileName = { + if (isUUID()) { + return "${index}.html" + } else { + return "${index}_${key}.html" + } + } + // Generate the file output for the coverage + treeBuilder { + "${buildDir}" { + "bpmn-coverage" { + file(fileName(), output) + } + } + } + } + } + + static CoverageData generateCoverageData(ProcessInstance processInstance, String coverageDataName = null, Integer coverageDataWeight = 0){ + CoverageData coverageData = new CoverageData() + coverageData.name = coverageDataName + coverageData.weight = coverageDataWeight + + String processInstanceId = processInstance.getProcessInstanceId() + + // Get Activity Events + def events = historyService() + .createHistoricActivityInstanceQuery() + .processInstanceId(processInstanceId) + .finished() + .orderPartiallyByOccurrence() + .asc() + .list() + def activityEvents = events.findAll {it.activityType != 'sequenceFlow' && it.activityType != 'multiInstanceBody'} + .collect {it.activityId} + .countBy {it} + coverageData.activityInstancesFinished = activityEvents + + // Activity Events That are still active + def eventsStillActive = historyService() + .createHistoricActivityInstanceQuery() + .processInstanceId(processInstanceId) + .unfinished() + .orderPartiallyByOccurrence() + .asc() + .list() + def activityEventsStillActive = eventsStillActive.findAll {it.activityType != 'sequenceFlow'} + .collect {it.activityId} + coverageData.activityInstancesUnfinished = activityEventsStillActive + + def sequenceFlows = events.findAll {it.activityType == 'sequenceFlow'} + .collect {it.activityId} + coverageData.sequenceFlowsFinished = sequenceFlows + + String processDefinitionId = historyService() + .createHistoricProcessInstanceQuery() + .processInstanceId(processInstanceId) + .singleResult() + .getProcessDefinitionId() + + def model = repositoryService() + .getBpmnModelInstance(processDefinitionId) + + def asyncData = model.getModelElementsByType(FlowNode.class).collect {[ + 'id': it.getId(), + 'asyncBefore': it.isCamundaAsyncBefore().toBoolean(), + 'asyncAfter': it.isCamundaAsyncAfter().toBoolean(), + 'exclusive': it.isCamundaExclusive().toBoolean() + ]} + coverageData.modelAsyncData = asyncData + + def userTasks = model.getModelElementsByType(UserTask.class).collect {it.getId()} + coverageData.modelUserTasks = userTasks + + def receiveTasks = model.getModelElementsByType(ReceiveTask.class).collect {it.getId()} + coverageData.modelReceiveTasks = receiveTasks + + def externalTasks = model.getModelElementsByType(ServiceTask.class).findAll {it.getCamundaType() == 'external'}.collect {it.getId()} + coverageData.modelExternalTasks = externalTasks + + def intermediateCatchEvents = model.getModelElementsByType(IntermediateCatchEvent.class).collect {it.getId()} + + coverageData.modelIntermediateCatchEvents = intermediateCatchEvents + + coverageData.bpmnModel = StringEscapeUtils.escapeJavaScript(Bpmn.convertToString(model)) + + +// Generate Variable Usage / Dataflow data + Collection variableHistory = historyService().createHistoricDetailQuery() + .processInstanceId(processInstanceId) + .disableBinaryFetching() + .variableUpdates() + .list() + + ArrayList> activityVariableMappings = variableHistory.collect { historyItem -> + [('activityId'): historyService().createHistoricActivityInstanceQuery() + .processInstanceId(processInstanceId) + .activityInstanceId(historyItem.getActivityInstanceId()) + .singleResult().getActivityId(), + ('variableInstance') : historyItem.toString() + ] + } + coverageData.activityInstanceVariableMapping = activityVariableMappings + + return coverageData + } + + def compileTemplate(CoverageData coverageData){ + def head = generateTemplateHead() + def body = generateTemplateBody( + "${UUID.randomUUID().toString().replaceAll("\\W", "")}", // Creates a UUID for the coverage name for uniqueness and removes all hyphens + coverageData.bpmnModel, + toJson(coverageData.modelUserTasks), + toJson(coverageData.activityInstancesFinished), + toJson(coverageData.sequenceFlowsFinished), + toJson(coverageData.modelAsyncData), + toJson(coverageData.modelReceiveTasks), + toJson(coverageData.modelExternalTasks), + toJson(coverageData.modelIntermediateCatchEvents), + toJson(coverageData.activityInstancesUnfinished), + toJson(coverageData.activityInstanceVariableMapping) + ) + def footer = generateTemplateFooter() +// @TODO Update Template Generation code to be a cleaner usage of scripting + + return """ + ${head} + ${body} + ${footer} + """ + } + + +} \ No newline at end of file diff --git a/src/main/groovy/io/digitalstate/camunda/coverage/bpmn/CoverageData.groovy b/src/main/groovy/io/digitalstate/camunda/coverage/bpmn/CoverageData.groovy new file mode 100644 index 0000000..116d287 --- /dev/null +++ b/src/main/groovy/io/digitalstate/camunda/coverage/bpmn/CoverageData.groovy @@ -0,0 +1,20 @@ +package io.digitalstate.camunda.coverage.bpmn + +class CoverageData extends Object { + public String name + public Integer weight + public String bpmnModel + public Map activityInstancesFinished + public ArrayList activityInstancesUnfinished + public ArrayList sequenceFlowsFinished + public ArrayList modelAsyncData + public ArrayList modelUserTasks + public ArrayList modelExternalTasks + public ArrayList modelReceiveTasks + public ArrayList modelIntermediateCatchEvents + public ArrayList activityInstanceVariableMapping + +} + +// @TODO make ActivityInstancesUnfinished track the counts of active instances per ActivityId +// @TODO make activityInstanceVariableMapping have a count value of the number of variable activity diff --git a/src/main/groovy/io/digitalstate/camunda/coverage/bpmn/Helpers.groovy b/src/main/groovy/io/digitalstate/camunda/coverage/bpmn/Helpers.groovy new file mode 100644 index 0000000..72169ca --- /dev/null +++ b/src/main/groovy/io/digitalstate/camunda/coverage/bpmn/Helpers.groovy @@ -0,0 +1,9 @@ +package io.digitalstate.camunda.coverage.bpmn + +trait Helpers{ + + // helper method to shorten the .addInputStream params in createDeployment() + static InputStream resourceStream(String path){ + return this.getClassLoader().getResource(path.toString()).newInputStream() + } +} \ No newline at end of file diff --git a/src/main/groovy/io/digitalstate/camunda/coverage/bpmn/SequenceFlowHistory.groovy b/src/main/groovy/io/digitalstate/camunda/coverage/bpmn/SequenceFlowHistory.groovy new file mode 100644 index 0000000..aba9ae5 --- /dev/null +++ b/src/main/groovy/io/digitalstate/camunda/coverage/bpmn/SequenceFlowHistory.groovy @@ -0,0 +1,52 @@ +package io.digitalstate.camunda.coverage.bpmn + +import org.camunda.bpm.model.bpmn.Bpmn +import org.camunda.bpm.model.bpmn.BpmnModelInstance +import org.camunda.bpm.model.bpmn.instance.SequenceFlow +import org.camunda.bpm.model.bpmn.instance.camunda.CamundaExecutionListener +import org.camunda.bpm.model.bpmn.instance.camunda.CamundaScript + +trait SequenceFlowHistory implements Helpers{ + + public String sequenceFlowListenerScriptPath = 'sequenceflowhistory/sequenceFlowHistoryEventGenerator.js' + + InputStream getSequenceFlowListenerScript(){ + return resourceStream(this.sequenceFlowListenerScriptPath) + } + + public String getSequenceFlowFileName(){ + return sequenceFlowListenerScriptPath.substring(sequenceFlowListenerScriptPath.lastIndexOf('/') + 1) + } + + + private static BpmnModelInstance setupSequenceFlowListeners(BpmnModelInstance model, String scriptResource, String scriptFormat){ + def sequenceFlows = model.getModelElementsByType(SequenceFlow.class).collect {it.getId()} + BpmnModelInstance newModel = model + sequenceFlows.each { + newModel = addExecutionListener(newModel, it, scriptResource, scriptFormat) + } + return newModel + } + + private static BpmnModelInstance addExecutionListener(BpmnModelInstance model, String elementId, String scriptResource, String scriptFormat){ + CamundaExecutionListener extLis = model.newInstance(CamundaExecutionListener.class) + CamundaScript camScript = model.newInstance(CamundaScript.class) + camScript.setCamundaResource(scriptResource) + camScript.setCamundaScriptFormat(scriptFormat) + extLis.setCamundaEvent('take') + extLis.setCamundaScript(camScript) + + BpmnModelInstance newModel = model.getModelElementById(elementId).builder().addExtensionElement(extLis).done() + return newModel + } + + static BpmnModelInstance addSequenceFlowListeners( String modelPath, + String scriptResource = 'deployment://sequenceFlowHistoryEventGenerator.js', + String scriptFormat = 'javascript') { + + InputStream resource = resourceStream(modelPath) + BpmnModelInstance model = Bpmn.readModelFromStream(resource) + BpmnModelInstance preppedModel = setupSequenceFlowListeners(model, scriptResource, scriptFormat) + return preppedModel + } +} \ No newline at end of file diff --git a/src/main/groovy/io/digitalstate/camunda/coverage/bpmn/bpmnjs/TemplateGeneration.groovy b/src/main/groovy/io/digitalstate/camunda/coverage/bpmn/bpmnjs/TemplateGeneration.groovy new file mode 100644 index 0000000..e434d04 --- /dev/null +++ b/src/main/groovy/io/digitalstate/camunda/coverage/bpmn/bpmnjs/TemplateGeneration.groovy @@ -0,0 +1,66 @@ +package io.digitalstate.camunda.coverage.bpmn.bpmnjs + +import groovy.text.SimpleTemplateEngine +import io.digitalstate.camunda.coverage.bpmn.Helpers + +trait TemplateGeneration implements Helpers{ + + String templateDir = 'templates' + + /** + * + * @param templateDir relative to resources folder. No leading slash + */ + void setTemplateDir(String templateDir){ + this.templateDir = templateDir + } + + String getTemplateDir(){ + return this.templateDir + } + + def generateTemplateHead(String file = 'head.html'){ + return resourceStream("${getTemplateDir()}/${file}").getText() + } + + def generateTemplateFooter(String file = 'footer.html'){ + return resourceStream("${getTemplateDir()}/${file}").getText() + } + + def generateTemplateBody( String featureName = '', + String xml = '', + userTasks = [], + activityInstances =[], + executedSequenceFlows =[], + asyncData =[], + receiveTasks =[], + externalTasks =[], + intermediateCatchEvents =[], + activityInstancesStillActive =[], + activityInstanceVariableMapping =[], + String file = 'body.html'){ + + def binding = [ + 'featureName': featureName, + 'xml': xml, + 'userTasks': userTasks, + 'activityInstances': activityInstances, + 'executedSequenceFlows': executedSequenceFlows, + 'asyncData': asyncData, + 'receiveTasks': receiveTasks, + 'externalTasks': externalTasks, + 'intermediateCatchEvents': intermediateCatchEvents, + 'activityInstancesStillActive': activityInstancesStillActive, + 'activityInstanceVariableMapping':activityInstanceVariableMapping + ] + + String template = resourceStream("${getTemplateDir()}/${file}").getText() + def engine = new SimpleTemplateEngine() + + String rendered = engine.createTemplate(template).make(binding) + + return rendered + + } + +} \ No newline at end of file diff --git a/src/main/resources/sequenceflowhistory/sequenceFlowHistoryEventGenerator.js b/src/main/resources/sequenceflowhistory/sequenceFlowHistoryEventGenerator.js new file mode 100644 index 0000000..655db40 --- /dev/null +++ b/src/main/resources/sequenceflowhistory/sequenceFlowHistoryEventGenerator.js @@ -0,0 +1,18 @@ +var Date = Java.type("java.util.Date") + +// https://docs.camunda.org/javadoc/camunda-bpm-platform/7.9/org/camunda/bpm/engine/impl/history/event/HistoricActivityInstanceEventEntity.html +var HistoricActivityInstanceEventEntity = Java.type('org.camunda.bpm.engine.impl.history.event.HistoricActivityInstanceEventEntity') + +var historicActivityInstance = new HistoricActivityInstanceEventEntity() + +historicActivityInstance.setActivityId(execution.getCurrentTransitionId()) +historicActivityInstance.setExecutionId(execution.getId()) +historicActivityInstance.setActivityType('sequenceFlow') +historicActivityInstance.setStartTime(new Date()) +historicActivityInstance.setEndTime(new Date()) +historicActivityInstance.setDurationInMillis(0) +historicActivityInstance.setProcessDefinitionId(execution.getProcessDefinitionId()) +historicActivityInstance.setProcessInstanceId(execution.getProcessInstanceId()) + +var historyHandler = execution.getProcessEngineServices().getProcessEngineConfiguration().getHistoryEventHandler() +historyHandler.handleEvent(historicActivityInstance) \ No newline at end of file diff --git a/src/main/resources/templates/body.html b/src/main/resources/templates/body.html new file mode 100644 index 0000000..5218234 --- /dev/null +++ b/src/main/resources/templates/body.html @@ -0,0 +1,153 @@ + +
+ + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/footer.html b/src/main/resources/templates/footer.html new file mode 100644 index 0000000..62d09b8 --- /dev/null +++ b/src/main/resources/templates/footer.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/templates/head.html b/src/main/resources/templates/head.html new file mode 100644 index 0000000..dd51d3d --- /dev/null +++ b/src/main/resources/templates/head.html @@ -0,0 +1,84 @@ + + + + + + \ No newline at end of file diff --git a/src/test/groovy/coveragetest/CallActivitySingleFeatureSpec.groovy b/src/test/groovy/coveragetest/CallActivitySingleFeatureSpec.groovy new file mode 100644 index 0000000..acc27fe --- /dev/null +++ b/src/test/groovy/coveragetest/CallActivitySingleFeatureSpec.groovy @@ -0,0 +1,106 @@ +package coveragetest + +import io.digitalstate.camunda.coverage.bpmn.CoverageBuilder +import io.digitalstate.camunda.coverage.bpmn.SequenceFlowHistory +import org.junit.ClassRule +import spock.lang.Shared +import spock.lang.Specification + +import org.camunda.bpm.engine.test.ProcessEngineRule +import org.camunda.bpm.engine.history.HistoricActivityInstance +import org.camunda.bpm.engine.runtime.ProcessInstance + + +//brings in Camunda BPM Assertion + AssertJ core.api.Assertions +import static org.camunda.bpm.engine.test.assertions.ProcessEngineTests.* +// http://joel-costigliola.github.io/assertj/core/api/index.html +// http://camunda.github.io/camunda-bpm-assert/apidocs/org/camunda/bpm/engine/test/assertions/ProcessEngineTests.html +// http://joel-costigliola.github.io/assertj/ + + +import static org.camunda.spin.Spin.S + +class CallActivitySingleFeatureSpec extends Specification implements CoverageBuilder, SequenceFlowHistory { + + @ClassRule + @Shared ProcessEngineRule processEngineRule = new ProcessEngineRule('camunda_config/camunda.cfg.xml') + @Shared String deploymentId + + def setupSpec(){ + def deployment = repositoryService().createDeployment() + .addInputStream(getSequenceFlowFileName(), getSequenceFlowListenerScript()) + .addModelInstance('CallActivityCoverage.bpmn', addSequenceFlowListeners('bpmn/conditionalstart/CallActivityCoverage.bpmn')) + .addModelInstance('CallActivityCoverage2.bpmn', addSequenceFlowListeners('bpmn/conditionalstart/CallActivityCoverage2.bpmn')) + .name('CallActivitiesCoverage') + .enableDuplicateFiltering(false) + .deploy() + deploymentId = deployment.getId() + println "Deployment ID: '${deploymentId}' has been created" + } + + def 'Manage CallActivityCoverage1'() { + when: 'Setting up variables' + def json = S("{\"customer\": \"Kermit\"}") + def startingVariables = [ + 'json': json + ] + + and: 'We start the CallActivityCoverage process definition' + def callActivityCoverage1ProcessInstance = runtimeService().startProcessInstanceByKey('CallActivityCoverage') + + then: 'Process is Active and waiting for user task completion' + assertThat(callActivityCoverage1ProcessInstance).isActive() + + then: 'The current process variables are equal to the starting variables' + def processVariables = runtimeService().getVariables(callActivityCoverage1ProcessInstance.getProcessInstanceId()) + assertThat(processVariables == startingVariables) + + then: 'The process instance should be waiting for the Call Activity to Complete' + assertThat(callActivityCoverage1ProcessInstance).isWaitingAt('Task_1gdn63n') + + coverageSnapshot(callActivityCoverage1ProcessInstance, 'some1') +// reportInfo(generateCoverageData(processEngine(), callActivityCoverage1ProcessInstance, "CallActivityCoverage1.bpmn Waiting for Called Process")) + + and: 'get the called called process instance' + HistoricActivityInstance callActInstance = historyService().createHistoricActivityInstanceQuery() + .processInstanceId(callActivityCoverage1ProcessInstance.getProcessInstanceId()) + .activityId('Task_1gdn63n') + .singleResult() + + ProcessInstance callActivityCoverage2ProcessInstance = calledProcessInstance(processInstanceQuery().processInstanceId(callActInstance.getCalledProcessInstanceId())) + + then: 'CallActivityCoverage2 is running' + assertThat(callActivityCoverage2ProcessInstance).isActive() + + then: 'CallActivityCoverage2 is waiting at the User Task' + assertThat(callActivityCoverage2ProcessInstance).isWaitingAt('Task_0xjkfyv') + + then: 'Complete the User Task' + complete(task(callActivityCoverage2ProcessInstance)) + + then: 'CallActivityCoverage2 has completed' + assertThat(callActivityCoverage2ProcessInstance).isEnded() + + coverageSnapshot(callActivityCoverage2ProcessInstance, 'some2') +// reportInfo(generateCoverageData(processEngine(), callActivityCoverage2ProcessInstance, "CallActivityCoverage2.bpmn Completion")) + + then: 'CallActivityCoverage1 has ended' + assertThat(callActivityCoverage1ProcessInstance).isEnded() + + coverageSnapshot(callActivityCoverage1ProcessInstance) +// reportInfo(generateCoverageData(processEngine(), callActivityCoverage1ProcessInstance, "CallActivityCoverage1.bpmn Completion")) + } + + def cleanupSpec() { +// https://docs.camunda.org/javadoc/camunda-bpm-platform/7.8/org/camunda/bpm/engine/RepositoryService.html#deleteDeployment(java.lang.String,%20boolean,%20boolean,%20boolean) + + saveCoverageSnapshots() + repositoryService().deleteDeployment(deploymentId, + true, // cascade + true, // skipCustomListeners + true) // skipIoMappings + println "Deployment ID: '${deploymentId}' has been deleted" + } + +} + diff --git a/src/test/resources/SpockConfig.groovy b/src/test/resources/SpockConfig.groovy new file mode 100644 index 0000000..0bc02ba --- /dev/null +++ b/src/test/resources/SpockConfig.groovy @@ -0,0 +1,18 @@ +import spock.lang.Specification + +class LabelPrinter { + def _(def message) { + println message + true + } +} + +Specification.mixin LabelPrinter + +report { + enabled false + logFileDir './build-reports' + logFileName 'spock-report.json' + logFileSuffix new Date().format('yyyy-MM-dd_HH_mm_ss') +} + diff --git a/src/test/resources/bpmn/conditionalstart/CallActivityCoverage.bpmn b/src/test/resources/bpmn/conditionalstart/CallActivityCoverage.bpmn new file mode 100644 index 0000000..12e839d --- /dev/null +++ b/src/test/resources/bpmn/conditionalstart/CallActivityCoverage.bpmn @@ -0,0 +1,87 @@ + + + + + + + execution.setVariable('myVar', 'hahahahah') + + + SequenceFlow_1dgmqpc + + + + + SequenceFlow_1wu9rzw + SequenceFlow_0h433ye + + + + SequenceFlow_1hj4fxe + + + + SequenceFlow_1dgmqpc + SequenceFlow_1wu9rzw + execution.setVariable('dog','cat') + + + SequenceFlow_0h433ye + SequenceFlow_1hj4fxe + execution.setVariable('dog','123456789') + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/bpmn/conditionalstart/CallActivityCoverage2.bpmn b/src/test/resources/bpmn/conditionalstart/CallActivityCoverage2.bpmn new file mode 100644 index 0000000..0d35c34 --- /dev/null +++ b/src/test/resources/bpmn/conditionalstart/CallActivityCoverage2.bpmn @@ -0,0 +1,65 @@ + + + + + SequenceFlow_179y264 + + + + + SequenceFlow_1m292zv + SequenceFlow_09js0pl + + + SequenceFlow_09js0pl + + + + SequenceFlow_179y264 + SequenceFlow_1m292zv + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/camunda_config/camunda.cfg.xml b/src/test/resources/camunda_config/camunda.cfg.xml new file mode 100755 index 0000000..ba73cb2 --- /dev/null +++ b/src/test/resources/camunda_config/camunda.cfg.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +