/* * Created on Jul 10, 2011 * Copyright 2010 by Eduard Weissmann (edi.weissmann@gmail.com). * * This file is part of the Sejda source code * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.sejda.cli; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import java.io.PrintStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang3.mutable.MutableBoolean; import org.mockito.ArgumentCaptor; import org.sejda.cli.util.OnceWithMessage; import org.sejda.cli.util.SystemOutRecordingStream; import org.sejda.core.notification.context.GlobalNotificationContext; import org.sejda.core.service.DefaultTaskExecutionService; import org.sejda.core.service.TaskExecutionService; import org.sejda.model.exception.SejdaRuntimeException; import org.sejda.model.notification.EventListener; import org.sejda.model.notification.event.TaskExecutionCompletedEvent; import org.sejda.model.parameter.base.TaskParameters; /** * Builder for command line arguments, used in tests * * @author Eduard Weissmann * */ public class CommandLineTestBuilder { private final String taskName; private final Map<String, String> optionsAndValues = new HashMap<>(); public CommandLineTestBuilder(String taskName) { this.taskName = taskName; } /** * Populates 2 default input parameters * * @return this builder (for telescopic usage) */ public CommandLineTestBuilder defaultTwoInputs() { with("-f", "inputs/input.pdf inputs/second_input.pdf"); return this; } /** * Populates multiple input parameters using PDF and non PDf inputs * * @return this builder (for telescopic usage) */ public CommandLineTestBuilder defaultMultipleNonPdfInputs() { with("-f", "inputs/input.pdf inputs/second_input.pdf inputs/file1.txt"); return this; } /** * Populates default single input parameter * * @return this builder (for telescopic usage) */ public CommandLineTestBuilder defaultSingleInput() { with("-f", "inputs/input.pdf"); return this; } /** * Populates default multiple input parameter * * @return this builder (for telescopic usage) */ public CommandLineTestBuilder defaultMultiplePdfInputs() { with("-f", "inputs/input.pdf inputs/second_input.pdf"); return this; } /** * Populates default output parameter as folder ./outputs * * @return this builder (for telescopic usage) */ public CommandLineTestBuilder defaultFolderOutput() { with("-o", "./outputs"); return this; } /** * Populates default output parameter as file fileOutput.pdf * * @return this builder (for telescopic usage) */ public CommandLineTestBuilder defaultFileOutput() { with("-o", "./outputs/fileOutput.pdf"); return this; } /** * Removes flag/option specified * * @param option * option to remove * @return this builder (for telescopic usage) */ public CommandLineTestBuilder without(String option) { optionsAndValues.remove(option); return this; } /** * Removes any flags/options already added * * @return this builder (for telescopic usage) * */ public CommandLineTestBuilder reset() { optionsAndValues.clear(); return this; } /** * Adds a new option and it's value to the command * * @param option * @param value * @return this builder (for telescopic usage) */ public CommandLineTestBuilder with(String option, String value) { optionsAndValues.put(option, value); return this; } /** * Adds a new boolean flag option * * @param option * @return this builder (for telescopic usage) */ public CommandLineTestBuilder withFlag(String option) { optionsAndValues.put(option, null); return this; } /** * Builds a command line string, that calls the task specified as input, using the collected options & values * * @return command line as string */ public String toCommandLineString() { StringBuilder result = new StringBuilder(taskName); for (Map.Entry<String, String> eachOptionAndValue : optionsAndValues.entrySet()) { result.append(" ").append(eachOptionAndValue.getKey()); if (eachOptionAndValue.getValue() != null) { result.append(" ").append(eachOptionAndValue.getValue()); } } return result.toString(); } public void assertConsoleOutputContains(String... expectedOutputContainedLines) { new CommandLineExecuteTestHelper(true).assertConsoleOutputContains(this.toCommandLineString(), expectedOutputContainedLines); } public <T> T invokeSejdaConsole() { return (T) new CommandLineExecuteTestHelper(true) .invokeConsoleAndReturnTaskParameters(this.toCommandLineString()); } } /** * Helper test class for execution of the sejda-console<br/> * Contains helper methods such as {@link #assertConsoleOutputContains(String, String...)}, {@link #invokeConsoleAndReturnTaskParameters(String)}<br/> * * @author Eduard Weissmann * */ class CommandLineExecuteTestHelper { protected TaskExecutionService taskExecutionService; private SystemOutRecordingStream newSystemOut = new SystemOutRecordingStream(System.out); private Map<CustomizableProps, String> customs; CommandLineExecuteTestHelper(boolean useMockTaskExecutionService) { this(useMockTaskExecutionService, new HashMap<>()); customs.put(CustomizableProps.APP_NAME, "Sejda Console"); customs.put(CustomizableProps.LICENSE_PATH, "/sejda-console/LICENSE.txt"); } CommandLineExecuteTestHelper(boolean useMockTaskExecutionService, Map<CustomizableProps, String> customs) { if (useMockTaskExecutionService) { taskExecutionService = mock(TaskExecutionService.class); } else { taskExecutionService = new DefaultTaskExecutionService(); } this.customs = customs; } private SejdaConsole getConsole(String commandLine) { String[] args = parseCommandLineArgs(commandLine); return new SejdaConsole(args, new DefaultTaskExecutionAdapter(taskExecutionService), customs); } /** * Simulate's java's cli argument parsing. That means {@code java MyProgram 1234 www.caltech.edu "olive festival"} has 3 arguments: <br/> * <ul> * <li>args[0] = "1234"</li> * <li>args[1] = "www.caltech.edu"</li> * <li>args[2] = "olive festival"</li> * <ul/> * */ static String[] parseCommandLineArgs(String commandLine) { List<String> result = new ArrayList<String>(); Matcher m = Pattern.compile("\"([^\"]*)\"|(\\S+)").matcher(commandLine); while (m.find()) { if (m.group(1) != null) { result.add(m.group(1)); } else { result.add(m.group(2)); } } return result.toArray(new String[] {}); } public void assertConsoleOutputContains(String commandLine, String... expectedOutputContainedLines) { String consoleOutput = invokeConsoleAndReturnSystemOut(commandLine); for (String eachExpected : expectedOutputContainedLines) { assertThat(consoleOutput, containsString(eachExpected)); } } public void assertTaskCompletes(String commandLine) { final MutableBoolean taskCompleted = new MutableBoolean(false); GlobalNotificationContext.getContext().addListener(new EventListener<TaskExecutionCompletedEvent>() { @Override public void onEvent(TaskExecutionCompletedEvent event) { taskCompleted.setValue(true); } }); String consoleOutput = invokeConsoleAndReturnSystemOut(commandLine); assertThat("Task did not complete. Console output was:\n" + consoleOutput, taskCompleted.toBoolean(), is(true)); } private String invokeConsoleAndReturnSystemOut(String commandLine) { invokeConsoleIgnoringExpectedExceptions(commandLine); return getCapturedSystemOut(); } private void invokeConsoleIgnoringExpectedExceptions(String commandLine) { prepareSystemOutCapture(); try { getConsole(commandLine).execute(); // fail("Console execution should have failed, no? " + commandLine); } catch (SejdaRuntimeException e) { // no-op } catch (Exception e) { throw new SejdaRuntimeException("An unexpected exception occured while executing the console", e); } } private void prepareSystemOutCapture() { newSystemOut = new SystemOutRecordingStream(System.out); System.setOut(new PrintStream(newSystemOut)); } private String getCapturedSystemOut() { return newSystemOut.getCapturedSystemOut(); } @SuppressWarnings("unchecked") public <T extends TaskParameters> T invokeConsoleAndReturnTaskParameters(String commandLine) { ArgumentCaptor<TaskParameters> taskPrametersCaptor = ArgumentCaptor.forClass(TaskParameters.class); invokeConsoleIgnoringExpectedExceptions(commandLine); // now Mockito can provide some context to verification failures, yay verify(taskExecutionService, once("Command '" + commandLine + "' did not reach task execution, as was expected. Console output was: \n" + getCapturedSystemOut())).execute(taskPrametersCaptor.capture()); return (T) taskPrametersCaptor.getValue(); } private static OnceWithMessage once(String failureDescribingMessage) { return new OnceWithMessage(failureDescribingMessage); } }