/* 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. */ package org.activiti.engine.test.api.repository.diagram; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.BufferedOutputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; import org.activiti.engine.RepositoryService; import org.activiti.engine.impl.bpmn.diagram.ProcessDiagramLayoutFactory; import org.activiti.engine.repository.DiagramElement; import org.activiti.engine.repository.DiagramLayout; import org.activiti.engine.repository.DiagramNode; import org.activiti.engine.repository.ProcessDefinition; import org.activiti.engine.repository.ProcessDefinitionQuery; import org.activiti.engine.test.ActivitiRule; import org.apache.commons.io.FileUtils; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; /** * Tests process model and diagram retrieval features of the * {@link RepositoryService}. * * This test generates HTML code containing the positions and dimensions of all * elements in a BPMN process and compares that to HTML files stored in * 'src/test/resources/org/activiti/engine/test/api/repository/diagram'. * * If the expected HTML code needs to be changed due to changes in * {@link ProcessDiagramLayoutFactory}, the files can be regenerated by running the * test while {@link ProcessDiagramRetrievalTest#OVERWRITE_EXPECTED_HTML_FILES} * is set to true. * * @author Falko Menge */ @RunWith(Parameterized.class) public class ProcessDiagramRetrievalTest { /** * Set this to true and run the tests to regenerate the HTML files located in * src/test/resources/org/activiti/engine/test/api/repository/diagram, which * contain expected values for the HTML code generated by test cases. */ private static final boolean OVERWRITE_EXPECTED_HTML_FILES = false; @Rule public ActivitiRule activitiRule = new ActivitiRule(); /** * Provides a list of parameters for * {@link ProcessDiagramRetrievalTest#ProcessDiagramRetrievalTest(String, String, String, String)} */ @Parameters public static Collection<Object[]> data() { return Arrays.asList(new Object[][] { { "testStartEventWithNegativeCoordinates", ".bpmn", ".png", "sid-61D1FC47-8031-4834-A9B4-84158E73F7B9" }, { "testStartAndEndEventWithNegativeCoordinates", ".bpmn", ".png", "sid-61D1FC47-8031-4834-A9B4-84158E73F7B9" }, { "testProcessWithTask", ".bpmn", ".png", "sid-1E142B16-AFAF-429E-A441-D1232CFBD560" }, { "testProcessFromCamundaFoxDesigner", ".bpmn", ".png", "UserTask_1" }, { "testProcessFromCamundaFoxDesigner", ".bpmn", ".jpg", "UserTask_1" }, { "testProcessFromActivitiDesigner", ".bpmn20.xml", ".png", "Send_rejection_notification_via_email__3" }, { "testSequenceFlowOutOfBounds", ".bpmn", ".png", "sid-61D1FC47-8031-4834-A9B4-84158E73F7B9" }, { "testProcessFromAdonis", ".bpmn", ".png", "_16615" }, { "testProcessFromIboPrometheus", ".bpmn", ".png", "ibo-5784efbe-35ac-44bc-bcbe-4c18a2f23d5d" }, { "testProcessFromIboPrometheus", ".bpmn", ".jpg", "ibo-5784efbe-35ac-44bc-bcbe-4c18a2f23d5d" }, { "testInvoiceProcessCamundaFoxDesigner", ".bpmn20.xml", ".jpg", "Rechnung_freigeben_125" }, { "testInvoiceProcessSignavio", ".bpmn", ".png", "Freigebenden_zuordnen_143" }, { "testInvoiceProcessFromBusinessProcessIncubator", ".bpmn", ".png", "Rechnung_kl_ren_148" }, { "testProcessFromYaoqiang", ".bpmn", ".png", "_3" }, }); } private final String xmlFileName; private final String imageFileName; private final String highlightedActivityId; private RepositoryService repositoryService; private String deploymentId; private ProcessDefinitionQuery processDefinitionQuery; public ProcessDiagramRetrievalTest(String modelName, String xmlFileExtension, String imageFileExtension, String highlightedActivityId) { this.xmlFileName = modelName + xmlFileExtension; this.imageFileName = modelName + imageFileExtension; this.highlightedActivityId = highlightedActivityId; } @Before public void setup() { repositoryService = activitiRule.getRepositoryService(); deploymentId = repositoryService.createDeployment() .addClasspathResource("org/activiti/engine/test/api/repository/diagram/" + xmlFileName) .addClasspathResource("org/activiti/engine/test/api/repository/diagram/" + imageFileName) .deploy() .getId(); processDefinitionQuery = repositoryService.createProcessDefinitionQuery(); } @After public void teardown() { repositoryService.deleteDeployment(deploymentId, true); } /** * Tests {@link RepositoryService#getProcessModel(String)}. */ @Test public void testGetProcessModel() throws Exception { if (1 == processDefinitionQuery.count()) { ProcessDefinition processDefinition = processDefinitionQuery.singleResult(); InputStream expectedStream = new FileInputStream("src/test/resources/org/activiti/engine/test/api/repository/diagram/" + xmlFileName); InputStream actualStream = repositoryService.getProcessModel(processDefinition.getId()); assertTrue(isEqual(expectedStream, actualStream)); } else { // some test diagrams do not contain executable processes // and are therefore ignored by the engine } } /** * Tests {@link RepositoryService#getProcessDiagram(String)}. */ @Test public void testGetProcessDiagram() throws Exception { if (1 == processDefinitionQuery.count()) { ProcessDefinition processDefinition = processDefinitionQuery.singleResult(); InputStream expectedStream = new FileInputStream("src/test/resources/org/activiti/engine/test/api/repository/diagram/" + imageFileName); InputStream actualStream = repositoryService.getProcessDiagram(processDefinition.getId()); // writeToFile(repositoryService.getProcessDiagram(processDefinition.getId()), // new File("src/test/resources/org/activiti/engine/test/api/repository/diagram/" + imageFileName + ".actual.png")); assertTrue(isEqual(expectedStream, actualStream)); } else { // some test diagrams do not contain executable processes // and are therefore ignored by the engine } } /** * Tests {@link RepositoryService#getProcessDiagramLayout(String)} and * {@link ProcessDiagramLayoutFactory#getProcessDiagramLayout(InputStream, InputStream)}. */ @Test public void testGetProcessDiagramLayout() throws Exception { DiagramLayout processDiagramLayout; if (1 == processDefinitionQuery.count()) { ProcessDefinition processDefinition = processDefinitionQuery.singleResult(); assertNotNull(processDefinition); processDiagramLayout = repositoryService.getProcessDiagramLayout(processDefinition.getId()); } else { // some test diagrams do not contain executable processes // and are therefore ignored by the engine InputStream bpmnXmlStream = new FileInputStream("src/test/resources/org/activiti/engine/test/api/repository/diagram/" + xmlFileName); InputStream imageStream = new FileInputStream("src/test/resources/org/activiti/engine/test/api/repository/diagram/" + imageFileName); assertNotNull(bpmnXmlStream); assertNotNull(imageStream); processDiagramLayout = new ProcessDiagramLayoutFactory().getProcessDiagramLayout(bpmnXmlStream, imageStream); } assertLayoutCorrect(processDiagramLayout); } private void assertLayoutCorrect(DiagramLayout processDiagramLayout) throws IOException { String html = generateHtmlCode(imageFileName, processDiagramLayout, highlightedActivityId); File htmlFile = new File("src/test/resources/org/activiti/engine/test/api/repository/diagram/" + imageFileName + ".html"); if (OVERWRITE_EXPECTED_HTML_FILES) { FileUtils.writeStringToFile(htmlFile, html); fail("The assertions of this test only work if ProcessDiagramRetrievalTest#OVERWRITE_EXPECTED_HTML_FILES is set to false."); } assertEquals(FileUtils.readFileToString(htmlFile).replace("\r", ""), html); // remove carriage returns in case the files have been fetched via Git on Windows } private static String generateHtmlCode(String imageUrl, DiagramLayout processDiagramLayout, String highlightedActivityId) { StringBuilder html = new StringBuilder(); html.append("<!DOCTYPE html>\n"); html.append("<html>\n"); html.append(" <head>\n"); html.append(" <style type=\"text/css\"><!--\n"); html.append(" .BPMNElement {\n"); html.append(" position: absolute;\n"); html.append(" border: 2px dashed lightBlue;\n"); html.append(" border-radius: 5px; -moz-border-radius: 5px;\n"); html.append(" }\n"); if (highlightedActivityId != null && highlightedActivityId.length() > 0) { html.append(" #" + highlightedActivityId + " {border: 2px solid red;}\n"); } html.append(" --></style>"); html.append(" </head>\n"); html.append(" <body>\n"); html.append(" <div style=\"position: relative\">\n"); html.append(" <img src=\"" + imageUrl + "\" />\n"); List<DiagramNode> nodes = processDiagramLayout.getNodes(); Collections.sort(nodes, new DiagramElementComparator()); for (DiagramNode node : nodes) { html.append(" <div"); html.append(" class=\"BPMNElement\""); html.append(" id=\"" + node.getId() + "\""); html.append(" style=\""); html.append(" left: " + (int) (node.getX() - 2) + "px;"); html.append(" top: " + (int) (node.getY() - 2) + "px;"); html.append(" width: " + node.getWidth().intValue() + "px;"); html.append(" height: " + node.getHeight().intValue() + "px;\"></div>\n"); } html.append(" </div>\n"); html.append(" </body>\n"); html.append("</html>"); return html.toString(); } private static boolean isEqual(InputStream stream1, InputStream stream2) throws IOException { ReadableByteChannel channel1 = Channels.newChannel(stream1); ReadableByteChannel channel2 = Channels.newChannel(stream2); ByteBuffer buffer1 = ByteBuffer.allocateDirect(1024); ByteBuffer buffer2 = ByteBuffer.allocateDirect(1024); try { while (true) { int bytesReadFromStream1 = channel1.read(buffer1); int bytesReadFromStream2 = channel2.read(buffer2); if (bytesReadFromStream1 == -1 || bytesReadFromStream2 == -1) return bytesReadFromStream1 == bytesReadFromStream2; buffer1.flip(); buffer2.flip(); for (int i = 0; i < Math.min(bytesReadFromStream1, bytesReadFromStream2); i++) if (buffer1.get() != buffer2.get()) return false; buffer1.compact(); buffer2.compact(); } } finally { if (stream1 != null) stream1.close(); if (stream2 != null) stream2.close(); } } /** * Might be used for debugging {@link ProcessDiagramRetrievalTest#testGetProcessDiagram()}. */ @SuppressWarnings("unused") private static void writeToFile(InputStream is, File file) throws Exception { DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(file))); int c; while((c = is.read()) != -1) { out.writeByte(c); } is.close(); out.close(); } static class DiagramElementComparator implements Comparator<DiagramElement> { public int compare(DiagramElement a, DiagramElement b) { return a.getId().compareTo(b.getId()); } // Don't need it here public boolean equals(Object obj) { return this == obj; } } }