/* * Copyright 2015 Red Hat, Inc. and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * 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. */ package org.jbpm.eclipse.action; import java.io.InputStreamReader; import java.io.Reader; import java.io.StringReader; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.drools.compiler.builder.impl.KnowledgeBuilderConfigurationImpl; import org.kie.api.definition.process.Connection; import org.kie.api.definition.process.Node; import org.kie.api.definition.process.NodeContainer; import org.kie.api.definition.process.Process; import org.drools.core.xml.SemanticModules; import org.eclipse.core.internal.resources.File; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IFolder; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.IPackageFragment; import org.eclipse.jdt.core.IPackageFragmentRoot; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jface.action.IAction; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.dialogs.ProgressMonitorDialog; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.ui.IObjectActionDelegate; import org.eclipse.ui.IWorkbenchPart; import org.eclipse.ui.actions.WorkspaceModifyOperation; import org.jbpm.bpmn2.xml.BPMNDISemanticModule; import org.jbpm.bpmn2.xml.BPMNExtensionsSemanticModule; import org.jbpm.bpmn2.xml.BPMNSemanticModule; import org.jbpm.compiler.xml.XmlProcessReader; import org.jbpm.compiler.xml.processes.RuleFlowMigrator; import org.jbpm.eclipse.JBPMEclipsePlugin; import org.jbpm.process.core.context.variable.Variable; import org.jbpm.process.core.context.variable.VariableScope; import org.jbpm.ruleflow.core.RuleFlowProcess; import org.jbpm.workflow.core.impl.NodeImpl; import org.jbpm.workflow.core.node.CompositeNode; import org.jbpm.workflow.core.node.EndNode; import org.jbpm.workflow.core.node.EventNode; import org.jbpm.workflow.core.node.FaultNode; import org.jbpm.workflow.core.node.ForEachNode; import org.jbpm.workflow.core.node.HumanTaskNode; import org.jbpm.workflow.core.node.Join; import org.jbpm.workflow.core.node.RuleSetNode; import org.jbpm.workflow.core.node.Split; import org.jbpm.workflow.core.node.StartNode; import org.jbpm.workflow.core.node.SubProcessNode; import org.jbpm.workflow.core.node.TimerNode; import org.jbpm.workflow.core.node.WorkItemNode; public class GenerateBPMN2JUnitTests implements IObjectActionDelegate { private IFile file; private IWorkbenchPart targetPart; private boolean firstHumanTask = true; public void setActivePart(IAction action, IWorkbenchPart targetPart) { this.targetPart = targetPart; } public void run(IAction action) { if (file != null && file.exists()) { try { generateJUnitTests(); } catch (Throwable t) { JBPMEclipsePlugin.log(t); } } } public void selectionChanged(IAction action, ISelection selection) { if (selection instanceof IStructuredSelection) { IStructuredSelection structured = (IStructuredSelection) selection; if (structured.size() == 1) { Object element = structured.getFirstElement(); if (element instanceof IFile) { file = (IFile) element; } } } } public void generateJUnitTests() { firstHumanTask = true; try { final IJavaProject javaProject = JavaCore.create(file.getProject()); if (javaProject == null || !javaProject.exists()) { return; } InputStreamReader isr = new InputStreamReader(((File) file).getContents()); KnowledgeBuilderConfigurationImpl configuration = new KnowledgeBuilderConfigurationImpl(); SemanticModules modules = configuration.getSemanticModules(); modules.addSemanticModule(new BPMNSemanticModule()); modules.addSemanticModule(new BPMNDISemanticModule()); modules.addSemanticModule(new BPMNExtensionsSemanticModule()); XmlProcessReader xmlReader = new XmlProcessReader( modules, Thread.currentThread().getContextClassLoader() ); String xml = RuleFlowMigrator.convertReaderToString(isr); Reader reader = new StringReader(xml); List<Process> processes = xmlReader.read(reader); if (processes != null && processes.size() == 1) { final RuleFlowProcess process = (RuleFlowProcess) processes.get(0); String packageName = process.getPackageName(); if (packageName == null || packageName.trim().length() == 0 || "org.drools.bpmn2".equals(packageName)) { packageName = "org.jbpm"; } final String pName = packageName; WorkspaceModifyOperation op = new WorkspaceModifyOperation() { public void execute(final IProgressMonitor monitor) throws CoreException { try { IFolder folder = file.getProject().getFolder("src/main/java"); IPackageFragmentRoot packageFragmentRoot = javaProject .getPackageFragmentRoot(folder); IPackageFragment packageFragment = packageFragmentRoot .createPackageFragment(pName, true, monitor); String processName = process.getName(); processName = processName.replaceAll("\\s", "_"); if (processName == null || processName.trim().length() == 0) { processName = "Process"; } String fileName = processName + "JUnitTest"; String output = "package " + pName + ";\n" + "\n" + "import java.util.ArrayList;\n" + "import java.util.HashMap;\n" + "import java.util.List;\n" + "import java.util.Map;\n" + "\n" + "import org.jbpm.process.instance.impl.demo.SystemOutWorkItemHandler;\n" + "import org.jbpm.test.JbpmJUnitTestCase;\n" + "\n" + "import org.junit.Test;\n" + "\n" + "import org.kie.api.runtime.KieSession;\n" + "import org.kie.api.runtime.process.ProcessInstance;\n" + "import org.kie.api.task.TaskService;\n" + "import org.kie.api.task.model.TaskSummary;\n" + "\n" + "public class " + fileName + " extends JbpmJUnitTestCase {\n" + "\n"; boolean containsHumanTasks = containsHumanTasks(process); if (containsHumanTasks) { output += " public " + fileName + "() {\n" + " super(true);\n" + " }\n" + "\n"; } Map<String, String> cases = new HashMap<String, String>(); Map<String, String> ongoingCases = new HashMap<String, String>(); boolean done = processNodes("", process.getStart(null), "", cases, ongoingCases); if (!done) { if (ongoingCases.size() == 1) { cases.put("Implicit", ongoingCases.values().iterator().next()); } else { throw new IllegalArgumentException("Could not create implicit case: " + ongoingCases.size()); } } for (Map.Entry<String, String> entry: cases.entrySet()) { output += " @Test\n" + " public void test" + entry.getKey() + "() {\n" + " KieSession ksession = createKnowledgeSession(\"" + file.getName() + "\");\n"; Set<String> serviceTasks = new HashSet<String>(); containsServiceTasks(serviceTasks, process); for (String service: serviceTasks) { output += " ksession.getWorkItemManager().registerWorkItemHandler(\"" + service + "\", new SystemOutWorkItemHandler());\n"; } if (containsHumanTasks) { output += " TaskService taskService = getTaskService();\n"; } List<Variable> variables = process.getVariableScope().getVariables(); if (variables != null && variables.size() > 0) { output += " Map<String, Object> params = new HashMap<String, Object>();\n" + " // initialize variables here if necessary\n"; for (Variable v: variables) { output += " // params.put(\"" + v.getName() + "\", value); // type " + v.getType().getStringType() + "\n"; } output += " ProcessInstance processInstance = ksession.startProcess(\"" + process.getId() + "\", params);\n"; } else { output += " ProcessInstance processInstance = ksession.startProcess(\"" + process.getId() + "\");\n"; } output += entry.getValue() + " // do your checks here\n" + " // for example, assertProcessInstanceCompleted(processInstance.getId(), ksession);\n" + " }\n" + "\n"; } output +="}"; packageFragment.createCompilationUnit(fileName + ".java", output, true, monitor); } catch (Exception e) { e.printStackTrace(); MessageDialog.openError(null, "Error", e.getMessage()); } } }; try { new ProgressMonitorDialog(targetPart.getSite().getShell()).run(false, true, op); } catch (Exception e) { e.printStackTrace(); } } } catch (Throwable t) { t.printStackTrace(); } } private boolean processNodes(String name, Node currentNode, String testCode, Map<String, String> cases, Map<String, String> ongoingCases) { if (currentNode instanceof Split) { Split split = (Split) currentNode; switch (split.getType()) { case Split.TYPE_AND: boolean done = false; String startTestCode = testCode; int counter = 1; for (Connection c: split.getDefaultOutgoingConnections()) { if (processNodes(name + counter++, c.getTo(), startTestCode, cases, ongoingCases)) { done = true; } } if (!done) { String implicitCompleteTestCode = startTestCode; for (String ongoingCase: ongoingCases.values()) { implicitCompleteTestCode += ongoingCase.substring(startTestCode.length(), ongoingCase.length()); } ongoingCases.clear(); ongoingCases.put(name, implicitCompleteTestCode); } return done; case Split.TYPE_XOR: case Split.TYPE_OR: int i = 1; done = true; for (Connection c: split.getDefaultOutgoingConnections()) { String newTestCode = testCode + " // please make sure that the following constraint is selected to node " + c.getTo().getName() + ":\n" + " // " + split.getConstraint(c).getConstraint() + "\n"; if (!processNodes(name + "Constraint" + i++, c.getTo(), newTestCode, cases, ongoingCases)) { done = false; } } return done; default: throw new IllegalArgumentException("Unknown split type " + split.getType()); } } else if (currentNode instanceof EndNode) { EndNode endNode = (EndNode) currentNode; if (endNode.isTerminate()) { cases.put(name, testCode); return true; } else { ongoingCases.put(name, testCode); return false; } } else if (currentNode instanceof HumanTaskNode) { HumanTaskNode taskNode = (HumanTaskNode) currentNode; String actorId = (String) taskNode.getWork().getParameter("ActorId"); if (actorId == null) { actorId = ""; } String groupId = (String) taskNode.getWork().getParameter("GroupId"); if (groupId == null) { groupId = ""; } String[] actorIds = new String[0]; if (actorId.trim().length() > 0) { actorIds = actorId.split(","); } String[] groupIds = new String[0]; if (groupId.trim().length() > 0) { groupIds = groupId.split(","); } actorId = actorIds.length > 0 ? actorIds[0] : ""; if (firstHumanTask) { testCode += " // execute task\n" + " String actorId = \"" + actorId + "\";\n" + " List<TaskSummary> list = taskService.getTasksAssignedAsPotentialOwner(actorId, \"en-UK\");\n" + " TaskSummary task = list.get(0);\n"; } else { testCode += " // execute task\n" + " actorId = \"" + actorId + "\";\n" + " list = taskService.getTasksAssignedAsPotentialOwner(actorId, \"en-UK\");\n" + " task = list.get(0);\n"; } if (actorIds.length > 1 || groupIds.length > 0) { testCode += " taskService.claim(task.getId(), actorId);\n"; } if (firstHumanTask) { testCode += " taskService.start(task.getId(), actorId);\n" + " Map<String, Object> results = new HashMap<String, Object>();\n" + " // add results here\n"; firstHumanTask = false; } else { testCode += " taskService.start(task.getId(), actorId);\n" + " results = new HashMap<String, Object>();\n" + " // add results here\n"; } for (Map.Entry<String, String> entry: taskNode.getOutMappings().entrySet()) { String type = null; VariableScope variableScope = (VariableScope) taskNode.resolveContext(VariableScope.VARIABLE_SCOPE, entry.getValue()); if (variableScope != null) { type = variableScope.findVariable(entry.getValue()).getType().getStringType(); } testCode += " // results.put(\"" + entry.getKey() + "\", value);" + (type == null ? "" : " // type " + type) + "\n"; } testCode += " taskService.complete(task.getId(), actorId, results);\n" + "\n"; return processNodes(name, ((NodeImpl) currentNode).getTo().getTo(), testCode, cases, ongoingCases); } else if (currentNode instanceof WorkItemNode) { WorkItemNode taskNode = (WorkItemNode) currentNode; testCode += " // if necessary, complete request for service task \"" + taskNode.getWork().getName() + "\"\n"; return processNodes(name, ((NodeImpl) currentNode).getTo().getTo(), testCode, cases, ongoingCases); } else if (currentNode instanceof EventNode) { EventNode eventNode = (EventNode) currentNode; testCode += " ksession.signalEvent(\"" + eventNode.getType() + "\", null, processInstance.getId());\n"; return processNodes(name, ((NodeImpl) currentNode).getTo().getTo(), testCode, cases, ongoingCases); } else if (currentNode instanceof TimerNode) { testCode += " // wait for timer to expire\n" + " // for example, try { Thread.sleep(delay); } catch (Exception e) { /* Do nothing */ }"; // TODO simulation time return processNodes(name, ((NodeImpl) currentNode).getTo().getTo(), testCode, cases, ongoingCases); } else if (currentNode instanceof ForEachNode) { CompositeNode compositeNode = ((ForEachNode) currentNode).getCompositeNode(); testCode += " // --> triggering each element in the collection:\n"; boolean done = true; for (Node node: compositeNode.getNodes()) { if (node instanceof StartNode) { StartNode startNode = (StartNode) node; if (startNode.getTriggers() == null || startNode.getTriggers().isEmpty()) { done = processNodes(name, startNode.getTo().getTo(), testCode, cases, ongoingCases); break; } } } if (done) { for (Map.Entry<String, String> c: cases.entrySet()) { if (c.getKey().startsWith(name)) { cases.put(c.getKey(), c.getValue() + " // <-- do this for one element in the collection:\n"); } } return true; } else { if (ongoingCases.size() == 1) { testCode = ongoingCases.values().iterator().next() + " // <-- do this for each element in the collection:\n"; return processNodes(name + "Implicit", ((NodeImpl) currentNode).getTo().getTo(), testCode, cases, ongoingCases); } else { throw new IllegalArgumentException("Could not create implicit case: " + ongoingCases.size()); } } } else if (currentNode instanceof CompositeNode) { CompositeNode compositeNode = (CompositeNode) currentNode; boolean done = true; for (Node node: compositeNode.getNodes()) { if (node instanceof StartNode) { StartNode startNode = (StartNode) node; if (startNode.getTriggers() == null || startNode.getTriggers().isEmpty()) { done = processNodes(name, startNode.getTo().getTo(), testCode, cases, ongoingCases); break; } } } if (done) { return true; } else { if (ongoingCases.size() == 1) { return processNodes(name + "Implicit", ((NodeImpl) currentNode).getTo().getTo(), ongoingCases.values().iterator().next(), cases, ongoingCases); } else { throw new IllegalArgumentException("Could not create implicit case: " + ongoingCases.size()); } } } else if (currentNode instanceof RuleSetNode) { testCode += " ksession.fireAllRules();\n"; return processNodes(name, ((NodeImpl) currentNode).getTo().getTo(), testCode, cases, ongoingCases); } else if (currentNode instanceof SubProcessNode) { SubProcessNode subProcessNode = (SubProcessNode) currentNode; if (subProcessNode.isWaitForCompletion()) { testCode += " // invoking subprocess " + subProcessNode.getProcessId() + ", if necessary make sure it is completed\n"; } return processNodes(name, ((NodeImpl) currentNode).getTo().getTo(), testCode, cases, ongoingCases); } else if (currentNode instanceof FaultNode) { FaultNode faultNode = (FaultNode) currentNode; testCode += " // handle fault " + faultNode.getFaultName() + " if necessary\n"; return true; } else if (currentNode instanceof Join) { // TODO looping Join join = (Join) currentNode; switch (join.getType()) { case Join.TYPE_AND: // TODO: cannot just call processNodes as this will add it to the cases if an end is reached, // while it should also still include the work that is necessary to complete the other branches // processNodes(name, ((NodeImpl) currentNode).getTo().getTo(), testCode, cases, ongoingCases); // TODO: this isn't 100% correct, as a join should only wait for the other incoming connections, // not all other non-terminating connections, but not doing this would make test ignore the other // branches of the divering parallel gateway // return false; throw new IllegalArgumentException("Generation of tests that include a convering parallel gateway is not yet supported"); case Join.TYPE_XOR: return processNodes(name, ((NodeImpl) currentNode).getTo().getTo(), testCode, cases, ongoingCases); default: throw new IllegalArgumentException("Unknown join type " + join.getType()); } } else if (currentNode instanceof NodeImpl) { return processNodes(name, ((NodeImpl) currentNode).getTo().getTo(), testCode, cases, ongoingCases); } else { throw new IllegalArgumentException("Unknown node " + currentNode); } } private static boolean containsHumanTasks(NodeContainer c) { for (Node node: c.getNodes()) { if (node instanceof HumanTaskNode) { return true; } if (node instanceof NodeContainer) { if (containsHumanTasks((NodeContainer) node)) { return true; } } } return false; } private void containsServiceTasks(Set<String> result, NodeContainer c) { for (Node node: c.getNodes()) { if (node instanceof WorkItemNode && !(node instanceof HumanTaskNode)) { result.add(((WorkItemNode) node).getWork().getName()); } if (node instanceof NodeContainer) { containsServiceTasks(result, (NodeContainer) node); } } } }