View Javadoc
1   /*
2    * Copyright (C) 2021-2022, Simeon Andreev <simeon.danailov.andreev@gmail.com> and others.
3    *
4    * This program and the accompanying materials are made available under the
5    * terms of the Eclipse Distribution License v. 1.0 which is available at
6    * https://www.eclipse.org/org/documents/edl-v10.php.
7    *
8    * SPDX-License-Identifier: BSD-3-Clause
9    */
10  package org.eclipse.jgit.pgm;
11  
12  import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DIFFTOOL_SECTION;
13  import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DIFF_SECTION;
14  import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CMD;
15  import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PROMPT;
16  import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL;
17  import static org.junit.Assert.fail;
18  
19  import java.io.File;
20  import java.io.InputStream;
21  import java.nio.file.Path;
22  import java.util.ArrayList;
23  import java.util.Arrays;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.regex.Pattern;
27  
28  import org.eclipse.jgit.internal.diffmergetool.DiffTools;
29  import org.eclipse.jgit.internal.diffmergetool.ExternalDiffTool;
30  import org.eclipse.jgit.lib.StoredConfig;
31  import org.junit.Before;
32  import org.junit.Test;
33  
34  /**
35   * Testing the {@code difftool} command.
36   */
37  public class DiffToolTest extends ToolTestCase {
38  
39  	private static final String DIFF_TOOL = CONFIG_DIFFTOOL_SECTION;
40  
41  	@Override
42  	@Before
43  	public void setUp() throws Exception {
44  		super.setUp();
45  		configureEchoTool(TOOL_NAME);
46  	}
47  
48  	@Test(expected = Die.class)
49  	public void testUndefinedTool() throws Exception {
50  		String toolName = "undefined";
51  		String[] conflictingFilenames = createUnstagedChanges();
52  
53  		List<String> expectedErrors = new ArrayList<>();
54  		for (String changedFilename : conflictingFilenames) {
55  			expectedErrors.add("External diff tool is not defined: " + toolName);
56  			expectedErrors.add("compare of " + changedFilename + " failed");
57  		}
58  
59  		runAndCaptureUsingInitRaw(expectedErrors, DIFF_TOOL, "--no-prompt",
60  				"--tool", toolName);
61  		fail("Expected exception to be thrown due to undefined external tool");
62  	}
63  
64  	@Test(expected = Die.class)
65  	public void testUserToolWithCommandNotFoundError() throws Exception {
66  		String toolName = "customTool";
67  
68  		int errorReturnCode = 127; // command not found
69  		String command = "exit " + errorReturnCode;
70  
71  		StoredConfig config = db.getConfig();
72  		config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD,
73  				command);
74  
75  		createMergeConflict();
76  		runAndCaptureUsingInitRaw(DIFF_TOOL, "--no-prompt", "--tool", toolName);
77  
78  		fail("Expected exception to be thrown due to external tool exiting with error code: "
79  				+ errorReturnCode);
80  	}
81  
82  	@Test(expected = Die.class)
83  	public void testEmptyToolName() throws Exception {
84  		assumeLinuxPlatform();
85  
86  		String emptyToolName = "";
87  
88  		StoredConfig config = db.getConfig();
89  		// the default diff tool is configured without a subsection
90  		String subsection = null;
91  		config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_TOOL,
92  				emptyToolName);
93  
94  		createUnstagedChanges();
95  
96  		String araxisErrorLine = "compare: unrecognized option `-wait' @ error/compare.c/CompareImageCommand/1123.";
97  		String[] expectedErrorOutput = { araxisErrorLine, araxisErrorLine, };
98  		runAndCaptureUsingInitRaw(Arrays.asList(expectedErrorOutput), DIFF_TOOL,
99  				"--no-prompt");
100 		fail("Expected exception to be thrown due to external tool exiting with an error");
101 	}
102 
103 	@Test
104 	public void testToolWithPrompt() throws Exception {
105 		String[] inputLines = {
106 				"y", // accept launching diff tool
107 				"y", // accept launching diff tool
108 		};
109 
110 		String[] conflictingFilenames = createUnstagedChanges();
111 		String[] expectedOutput = getExpectedCompareOutput(conflictingFilenames);
112 
113 		String option = "--tool";
114 
115 		InputStream inputStream = createInputStream(inputLines);
116 		assertArrayOfLinesEquals("Incorrect output for option: " + option,
117 				expectedOutput, runAndCaptureUsingInitRaw(inputStream,
118 						DIFF_TOOL, "--prompt", option, TOOL_NAME));
119 	}
120 
121 	@Test
122 	public void testToolAbortLaunch() throws Exception {
123 		String[] inputLines = {
124 				"y", // accept launching diff tool
125 				"n", // don't launch diff tool
126 		};
127 
128 		String[] conflictingFilenames = createUnstagedChanges();
129 		int abortIndex = 1;
130 		String[] expectedOutput = getExpectedAbortOutput(conflictingFilenames, abortIndex);
131 
132 		String option = "--tool";
133 
134 		InputStream inputStream = createInputStream(inputLines);
135 		assertArrayOfLinesEquals("Incorrect output for option: " + option,
136 				expectedOutput,
137 				runAndCaptureUsingInitRaw(inputStream, DIFF_TOOL, "--prompt", option,
138 						TOOL_NAME));
139 	}
140 
141 	@Test(expected = Die.class)
142 	public void testNotDefinedTool() throws Exception {
143 		createUnstagedChanges();
144 
145 		runAndCaptureUsingInitRaw(DIFF_TOOL, "--tool", "undefined");
146 		fail("Expected exception when trying to run undefined tool");
147 	}
148 
149 	@Test
150 	public void testTool() throws Exception {
151 		String[] conflictFilenames = createUnstagedChanges();
152 		String[] expectedOutput = getExpectedToolOutputNoPrompt(conflictFilenames);
153 
154 		String[] options = {
155 				"--tool",
156 				"-t",
157 		};
158 
159 		for (String option : options) {
160 			assertArrayOfLinesEquals("Incorrect output for option: " + option,
161 					expectedOutput,
162 					runAndCaptureUsingInitRaw(DIFF_TOOL, option,
163 							TOOL_NAME));
164 		}
165 	}
166 
167 	@Test
168 	public void testToolTrustExitCode() throws Exception {
169 		String[] conflictingFilenames = createUnstagedChanges();
170 		String[] expectedOutput = getExpectedToolOutputNoPrompt(conflictingFilenames);
171 
172 		String[] options = { "--tool", "-t", };
173 
174 		for (String option : options) {
175 			assertArrayOfLinesEquals("Incorrect output for option: " + option,
176 					expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL,
177 							"--trust-exit-code", option, TOOL_NAME));
178 		}
179 	}
180 
181 	@Test
182 	public void testToolNoGuiNoPromptNoTrustExitcode() throws Exception {
183 		String[] conflictingFilenames = createUnstagedChanges();
184 		String[] expectedOutput = getExpectedToolOutputNoPrompt(conflictingFilenames);
185 
186 		String[] options = { "--tool", "-t", };
187 
188 		for (String option : options) {
189 			assertArrayOfLinesEquals("Incorrect output for option: " + option,
190 					expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL,
191 							"--no-gui", "--no-prompt", "--no-trust-exit-code",
192 							option, TOOL_NAME));
193 		}
194 	}
195 
196 	@Test
197 	public void testToolCached() throws Exception {
198 		String[] conflictingFilenames = createStagedChanges();
199 		Pattern[] expectedOutput = getExpectedCachedToolOutputNoPrompt(conflictingFilenames);
200 
201 		String[] options = { "--cached", "--staged", };
202 
203 		for (String option : options) {
204 			assertArrayOfMatchingLines("Incorrect output for option: " + option,
205 					expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL,
206 							option, "--tool", TOOL_NAME));
207 		}
208 	}
209 
210 	@Test
211 	public void testToolHelp() throws Exception {
212 		List<String> expectedOutput = new ArrayList<>();
213 
214 		DiffTools diffTools = new DiffTools(db);
215 		Map<String, ExternalDiffTool> predefinedTools = diffTools
216 				.getPredefinedTools(true);
217 		List<ExternalDiffTool> availableTools = new ArrayList<>();
218 		List<ExternalDiffTool> notAvailableTools = new ArrayList<>();
219 		for (ExternalDiffTool tool : predefinedTools.values()) {
220 			if (tool.isAvailable()) {
221 				availableTools.add(tool);
222 			} else {
223 				notAvailableTools.add(tool);
224 			}
225 		}
226 
227 		expectedOutput.add(
228 				"'git difftool --tool=<tool>' may be set to one of the following:");
229 		for (ExternalDiffTool tool : availableTools) {
230 			String toolName = tool.getName();
231 			expectedOutput.add(toolName);
232 		}
233 		String customToolHelpLine = TOOL_NAME + "." + CONFIG_KEY_CMD + " "
234 				+ getEchoCommand();
235 		expectedOutput.add("user-defined:");
236 		expectedOutput.add(customToolHelpLine);
237 		expectedOutput.add(
238 				"The following tools are valid, but not currently available:");
239 		for (ExternalDiffTool tool : notAvailableTools) {
240 			String toolName = tool.getName();
241 			expectedOutput.add(toolName);
242 		}
243 		String[] userDefinedToolsHelp = {
244 				"Some of the tools listed above only work in a windowed",
245 				"environment. If run in a terminal-only session, they will fail.",
246 		};
247 		expectedOutput.addAll(Arrays.asList(userDefinedToolsHelp));
248 
249 		String option = "--tool-help";
250 		assertArrayOfLinesEquals("Incorrect output for option: " + option,
251 				expectedOutput.toArray(new String[0]),
252 				runAndCaptureUsingInitRaw(DIFF_TOOL, option));
253 	}
254 
255 	private void configureEchoTool(String toolName) {
256 		StoredConfig config = db.getConfig();
257 		// the default diff tool is configured without a subsection
258 		String subsection = null;
259 		config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_TOOL,
260 				toolName);
261 
262 		String command = getEchoCommand();
263 
264 		config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD,
265 				command);
266 		/*
267 		 * prevent prompts as we are running in tests and there is no user to
268 		 * interact with on the command line
269 		 */
270 		config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_PROMPT,
271 				String.valueOf(false));
272 	}
273 
274 	private String[] getExpectedToolOutputNoPrompt(String[] conflictingFilenames) {
275 		String[] expectedToolOutput = new String[conflictingFilenames.length];
276 		for (int i = 0; i < conflictingFilenames.length; ++i) {
277 			String newPath = conflictingFilenames[i];
278 			Path fullPath = getFullPath(newPath);
279 			expectedToolOutput[i] = fullPath.toString();
280 		}
281 		return expectedToolOutput;
282 	}
283 
284 	private Pattern[] getExpectedCachedToolOutputNoPrompt(String[] conflictingFilenames) {
285 		String tmpDir = System.getProperty("java.io.tmpdir");
286 		if (tmpDir.endsWith(File.separator)) {
287 			tmpDir = tmpDir.substring(0, tmpDir.length() - 1);
288 		}
289 		Pattern emptyPattern = Pattern.compile("");
290 		List<Pattern> expectedToolOutput = new ArrayList<>();
291 		for (int i = 0; i < conflictingFilenames.length; ++i) {
292 			String changedFilename = conflictingFilenames[i];
293 			Path fullPath = getFullPath(changedFilename);
294 			String filename = fullPath.getFileName().toString();
295 			String regexp = tmpDir + File.separatorChar + filename
296 					+ "_REMOTE_.*";
297 			Pattern pattern = Pattern.compile(regexp);
298 			expectedToolOutput.add(pattern);
299 			expectedToolOutput.add(emptyPattern);
300 		}
301 		expectedToolOutput.add(emptyPattern);
302 		return expectedToolOutput.toArray(new Pattern[0]);
303 	}
304 
305 	private String[] getExpectedCompareOutput(String[] conflictingFilenames) {
306 		List<String> expected = new ArrayList<>();
307 		int n = conflictingFilenames.length;
308 		for (int i = 0; i < n; ++i) {
309 			String changedFilename = conflictingFilenames[i];
310 			expected.add(
311 					"Viewing (" + (i + 1) + "/" + n + "): '" + changedFilename
312 							+ "'");
313 			expected.add("Launch '" + TOOL_NAME + "' [Y/n]?");
314 			Path fullPath = getFullPath(changedFilename);
315 			expected.add(fullPath.toString());
316 		}
317 		return expected.toArray(new String[0]);
318 	}
319 
320 	private String[] getExpectedAbortOutput(String[] conflictingFilenames,
321 			int abortIndex) {
322 		List<String> expected = new ArrayList<>();
323 		int n = conflictingFilenames.length;
324 		for (int i = 0; i < n; ++i) {
325 			String changedFilename = conflictingFilenames[i];
326 			expected.add(
327 					"Viewing (" + (i + 1) + "/" + n + "): '" + changedFilename
328 							+ "'");
329 			expected.add("Launch '" + TOOL_NAME + "' [Y/n]?");
330 			if (i == abortIndex) {
331 				break;
332 			}
333 			Path fullPath = getFullPath(changedFilename);
334 			expected.add(fullPath.toString());
335 		}
336 		return expected.toArray(new String[0]);
337 	}
338 
339 	private static String getEchoCommand() {
340 		/*
341 		 * use 'REMOTE' placeholder, as it will be replaced by a file path
342 		 * within the repository.
343 		 */
344 		return "(echo \"$REMOTE\")";
345 	}
346 }