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  		String emptyToolName = "";
85  
86  		StoredConfig config = db.getConfig();
87  		// the default diff tool is configured without a subsection
88  		String subsection = null;
89  		config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_TOOL,
90  				emptyToolName);
91  
92  		createUnstagedChanges();
93  
94  		String araxisErrorLine = "compare: unrecognized option `-wait' @ error/compare.c/CompareImageCommand/1123.";
95  		String[] expectedErrorOutput = { araxisErrorLine, araxisErrorLine, };
96  		runAndCaptureUsingInitRaw(Arrays.asList(expectedErrorOutput), DIFF_TOOL,
97  				"--no-prompt");
98  		fail("Expected exception to be thrown due to external tool exiting with an error");
99  	}
100 
101 	@Test
102 	public void testToolWithPrompt() throws Exception {
103 		String[] inputLines = {
104 				"y", // accept launching diff tool
105 				"y", // accept launching diff tool
106 		};
107 
108 		String[] conflictingFilenames = createUnstagedChanges();
109 		String[] expectedOutput = getExpectedCompareOutput(conflictingFilenames);
110 
111 		String option = "--tool";
112 
113 		InputStream inputStream = createInputStream(inputLines);
114 		assertArrayOfLinesEquals("Incorrect output for option: " + option,
115 				expectedOutput, runAndCaptureUsingInitRaw(inputStream,
116 						DIFF_TOOL, "--prompt", option, TOOL_NAME));
117 	}
118 
119 	@Test
120 	public void testToolAbortLaunch() throws Exception {
121 		String[] inputLines = {
122 				"y", // accept launching diff tool
123 				"n", // don't launch diff tool
124 		};
125 
126 		String[] conflictingFilenames = createUnstagedChanges();
127 		int abortIndex = 1;
128 		String[] expectedOutput = getExpectedAbortOutput(conflictingFilenames, abortIndex);
129 
130 		String option = "--tool";
131 
132 		InputStream inputStream = createInputStream(inputLines);
133 		assertArrayOfLinesEquals("Incorrect output for option: " + option,
134 				expectedOutput,
135 				runAndCaptureUsingInitRaw(inputStream, DIFF_TOOL, "--prompt", option,
136 						TOOL_NAME));
137 	}
138 
139 	@Test(expected = Die.class)
140 	public void testNotDefinedTool() throws Exception {
141 		createUnstagedChanges();
142 
143 		runAndCaptureUsingInitRaw(DIFF_TOOL, "--tool", "undefined");
144 		fail("Expected exception when trying to run undefined tool");
145 	}
146 
147 	@Test
148 	public void testTool() throws Exception {
149 		String[] conflictFilenames = createUnstagedChanges();
150 		String[] expectedOutput = getExpectedToolOutputNoPrompt(conflictFilenames);
151 
152 		String[] options = {
153 				"--tool",
154 				"-t",
155 		};
156 
157 		for (String option : options) {
158 			assertArrayOfLinesEquals("Incorrect output for option: " + option,
159 					expectedOutput,
160 					runAndCaptureUsingInitRaw(DIFF_TOOL, option,
161 							TOOL_NAME));
162 		}
163 	}
164 
165 	@Test
166 	public void testToolTrustExitCode() throws Exception {
167 		String[] conflictingFilenames = createUnstagedChanges();
168 		String[] expectedOutput = getExpectedToolOutputNoPrompt(conflictingFilenames);
169 
170 		String[] options = { "--tool", "-t", };
171 
172 		for (String option : options) {
173 			assertArrayOfLinesEquals("Incorrect output for option: " + option,
174 					expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL,
175 							"--trust-exit-code", option, TOOL_NAME));
176 		}
177 	}
178 
179 	@Test
180 	public void testToolNoGuiNoPromptNoTrustExitcode() throws Exception {
181 		String[] conflictingFilenames = createUnstagedChanges();
182 		String[] expectedOutput = getExpectedToolOutputNoPrompt(conflictingFilenames);
183 
184 		String[] options = { "--tool", "-t", };
185 
186 		for (String option : options) {
187 			assertArrayOfLinesEquals("Incorrect output for option: " + option,
188 					expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL,
189 							"--no-gui", "--no-prompt", "--no-trust-exit-code",
190 							option, TOOL_NAME));
191 		}
192 	}
193 
194 	@Test
195 	public void testToolCached() throws Exception {
196 		String[] conflictingFilenames = createStagedChanges();
197 		Pattern[] expectedOutput = getExpectedCachedToolOutputNoPrompt(conflictingFilenames);
198 
199 		String[] options = { "--cached", "--staged", };
200 
201 		for (String option : options) {
202 			assertArrayOfMatchingLines("Incorrect output for option: " + option,
203 					expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL,
204 							option, "--tool", TOOL_NAME));
205 		}
206 	}
207 
208 	@Test
209 	public void testToolHelp() throws Exception {
210 		List<String> expectedOutput = new ArrayList<>();
211 
212 		DiffTools diffTools = new DiffTools(db);
213 		Map<String, ExternalDiffTool> predefinedTools = diffTools
214 				.getPredefinedTools(true);
215 		List<ExternalDiffTool> availableTools = new ArrayList<>();
216 		List<ExternalDiffTool> notAvailableTools = new ArrayList<>();
217 		for (ExternalDiffTool tool : predefinedTools.values()) {
218 			if (tool.isAvailable()) {
219 				availableTools.add(tool);
220 			} else {
221 				notAvailableTools.add(tool);
222 			}
223 		}
224 
225 		expectedOutput.add(
226 				"'git difftool --tool=<tool>' may be set to one of the following:");
227 		for (ExternalDiffTool tool : availableTools) {
228 			String toolName = tool.getName();
229 			expectedOutput.add(toolName);
230 		}
231 		String customToolHelpLine = TOOL_NAME + "." + CONFIG_KEY_CMD + " "
232 				+ getEchoCommand();
233 		expectedOutput.add("user-defined:");
234 		expectedOutput.add(customToolHelpLine);
235 		expectedOutput.add(
236 				"The following tools are valid, but not currently available:");
237 		for (ExternalDiffTool tool : notAvailableTools) {
238 			String toolName = tool.getName();
239 			expectedOutput.add(toolName);
240 		}
241 		String[] userDefinedToolsHelp = {
242 				"Some of the tools listed above only work in a windowed",
243 				"environment. If run in a terminal-only session, they will fail.",
244 		};
245 		expectedOutput.addAll(Arrays.asList(userDefinedToolsHelp));
246 
247 		String option = "--tool-help";
248 		assertArrayOfLinesEquals("Incorrect output for option: " + option,
249 				expectedOutput.toArray(new String[0]),
250 				runAndCaptureUsingInitRaw(DIFF_TOOL, option));
251 	}
252 
253 	private void configureEchoTool(String toolName) {
254 		StoredConfig config = db.getConfig();
255 		// the default diff tool is configured without a subsection
256 		String subsection = null;
257 		config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_TOOL,
258 				toolName);
259 
260 		String command = getEchoCommand();
261 
262 		config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD,
263 				command);
264 		/*
265 		 * prevent prompts as we are running in tests and there is no user to
266 		 * interact with on the command line
267 		 */
268 		config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_PROMPT,
269 				String.valueOf(false));
270 	}
271 
272 	private String[] getExpectedToolOutputNoPrompt(String[] conflictingFilenames) {
273 		String[] expectedToolOutput = new String[conflictingFilenames.length];
274 		for (int i = 0; i < conflictingFilenames.length; ++i) {
275 			String newPath = conflictingFilenames[i];
276 			Path fullPath = getFullPath(newPath);
277 			expectedToolOutput[i] = fullPath.toString();
278 		}
279 		return expectedToolOutput;
280 	}
281 
282 	private Pattern[] getExpectedCachedToolOutputNoPrompt(String[] conflictingFilenames) {
283 		String tmpDir = System.getProperty("java.io.tmpdir");
284 		if (tmpDir.endsWith(File.separator)) {
285 			tmpDir = tmpDir.substring(0, tmpDir.length() - 1);
286 		}
287 		Pattern emptyPattern = Pattern.compile("");
288 		List<Pattern> expectedToolOutput = new ArrayList<>();
289 		for (int i = 0; i < conflictingFilenames.length; ++i) {
290 			String changedFilename = conflictingFilenames[i];
291 			Path fullPath = getFullPath(changedFilename);
292 			String filename = fullPath.getFileName().toString();
293 			String regexp = tmpDir + File.separatorChar + filename
294 					+ "_REMOTE_.*";
295 			Pattern pattern = Pattern.compile(regexp);
296 			expectedToolOutput.add(pattern);
297 			expectedToolOutput.add(emptyPattern);
298 		}
299 		expectedToolOutput.add(emptyPattern);
300 		return expectedToolOutput.toArray(new Pattern[0]);
301 	}
302 
303 	private String[] getExpectedCompareOutput(String[] conflictingFilenames) {
304 		List<String> expected = new ArrayList<>();
305 		int n = conflictingFilenames.length;
306 		for (int i = 0; i < n; ++i) {
307 			String changedFilename = conflictingFilenames[i];
308 			expected.add(
309 					"Viewing (" + (i + 1) + "/" + n + "): '" + changedFilename
310 							+ "'");
311 			expected.add("Launch '" + TOOL_NAME + "' [Y/n]?");
312 			Path fullPath = getFullPath(changedFilename);
313 			expected.add(fullPath.toString());
314 		}
315 		return expected.toArray(new String[0]);
316 	}
317 
318 	private String[] getExpectedAbortOutput(String[] conflictingFilenames,
319 			int abortIndex) {
320 		List<String> expected = new ArrayList<>();
321 		int n = conflictingFilenames.length;
322 		for (int i = 0; i < n; ++i) {
323 			String changedFilename = conflictingFilenames[i];
324 			expected.add(
325 					"Viewing (" + (i + 1) + "/" + n + "): '" + changedFilename
326 							+ "'");
327 			expected.add("Launch '" + TOOL_NAME + "' [Y/n]?");
328 			if (i == abortIndex) {
329 				break;
330 			}
331 			Path fullPath = getFullPath(changedFilename);
332 			expected.add(fullPath.toString());
333 		}
334 		return expected.toArray(new String[0]);
335 	}
336 
337 	private static String getEchoCommand() {
338 		/*
339 		 * use 'REMOTE' placeholder, as it will be replaced by a file path
340 		 * within the repository.
341 		 */
342 		return "(echo \"$REMOTE\")";
343 	}
344 }