Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
RangeDifferencerThreeWayDiffTest.class,
CompareUIPluginTest.class,
CompareOpenEfficiencyTest.class,
UnifiedDiffOpenTest.class,
StructureCreatorTest.class,
CompareFileRevisionEditorInputTest.class})
public class AllCompareTests {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,341 @@
/*******************************************************************************
* Copyright (c) 2026 Lars Vogel and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Eclipse contributors - initial API and implementation
*******************************************************************************/
package org.eclipse.compare.tests;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BooleanSupplier;

import org.eclipse.compare.CompareConfiguration;
import org.eclipse.compare.CompareEditorInput;
import org.eclipse.compare.CompareUI;
import org.eclipse.compare.IEncodedStreamContentAccessor;
import org.eclipse.compare.IResourceProvider;
import org.eclipse.compare.ISharedDocumentAdapter;
import org.eclipse.compare.ITypedElement;
import org.eclipse.compare.SharedDocumentAdapter;
import org.eclipse.compare.internal.CompareEditor;
import org.eclipse.compare.internal.ComparePreferencePage;
import org.eclipse.compare.internal.CompareUIPlugin;
import org.eclipse.compare.structuremergeviewer.DiffNode;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.source.Annotation;
import org.eclipse.jface.text.source.IAnnotationModel;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.texteditor.IDocumentProvider;
import org.eclipse.ui.texteditor.ITextEditor;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

/**
* Tests the unified diff path of opening a compare editor: with the unified
* diff preference enabled, a qualifying input opens a plain text editor with
* unified diff annotations instead of the classic compare editor, and a
* non-qualifying input falls back to the classic compare editor.
*/
public class UnifiedDiffOpenTest {

private static final String UNIFIED_DIFF_ANNOTATION_PREFIX = "org.eclipse.compare.unifieddiff"; //$NON-NLS-1$
private static final long TIMEOUT_MILLIS = 30_000;

private boolean originalUnifiedDiff;
private IProject project;

/**
* A workspace-file element providing stream contents and a shared document
* adapter, so it qualifies for the unified diff display.
*/
private static final class WorkspaceFileElement
implements ITypedElement, IEncodedStreamContentAccessor, IResourceProvider, IAdaptable {

private final IFile file;
private final ISharedDocumentAdapter sharedDocumentAdapter = new SharedDocumentAdapter() {
@Override
public void flushDocument(IDocumentProvider provider, IEditorInput documentKey, IDocument document,
boolean overwrite) {
// not needed by these tests
}
};

WorkspaceFileElement(IFile file) {
this.file = file;
}

@Override
public InputStream getContents() throws CoreException {
return file.getContents();
}

@Override
public String getCharset() {
return "UTF-8"; //$NON-NLS-1$
}

@Override
public String getName() {
return file.getName();
}

@Override
public String getType() {
return TEXT_TYPE;
}

@Override
public Image getImage() {
return null;
}

@Override
public IResource getResource() {
return file;
}

@Override
public <T> T getAdapter(Class<T> adapter) {
if (adapter == ISharedDocumentAdapter.class) {
return adapter.cast(sharedDocumentAdapter);
}
return null;
}
}

/** A text element without a shared document adapter (does not qualify). */
private static final class InMemoryElement implements ITypedElement, IEncodedStreamContentAccessor {

private final String name;
private final byte[] bytes;

InMemoryElement(String name, String content) {
this.name = name;
this.bytes = content.getBytes(StandardCharsets.UTF_8);
}

@Override
public InputStream getContents() {
return new ByteArrayInputStream(bytes);
}

@Override
public String getCharset() {
return "UTF-8"; //$NON-NLS-1$
}

@Override
public String getName() {
return name;
}

@Override
public String getType() {
return TEXT_TYPE;
}

@Override
public Image getImage() {
return null;
}
}

/** Records prepareInput invocations and the thread they ran on. */
private static final class RecordingCompareEditorInput extends CompareEditorInput {

private final ITypedElement left;
private final ITypedElement right;
private final AtomicInteger prepareInputCount = new AtomicInteger();
private volatile boolean anyRunOnUiThread;

RecordingCompareEditorInput(ITypedElement left, ITypedElement right) {
super(new CompareConfiguration());
this.left = left;
this.right = right;
setTitle("Unified diff open"); //$NON-NLS-1$
}

@Override
protected Object prepareInput(IProgressMonitor monitor) {
prepareInputCount.incrementAndGet();
if (Display.getCurrent() != null) {
anyRunOnUiThread = true;
}
return new DiffNode(left, right);
}

@Override
public boolean canRunAsJob() {
return true;
}
}

@BeforeEach
public void setUp() throws CoreException {
assertNotNull(Display.getCurrent(), "tests require a UI thread / Display"); //$NON-NLS-1$
originalUnifiedDiff = store().getBoolean(ComparePreferencePage.UNIFIED_DIFF);
store().setValue(ComparePreferencePage.UNIFIED_DIFF, true);
project = ResourcesPlugin.getWorkspace().getRoot().getProject("UnifiedDiffOpenTest"); //$NON-NLS-1$
if (!project.exists()) {
project.create(null);
}
if (!project.isOpen()) {
project.open(null);
}
}

@AfterEach
public void tearDown() throws CoreException {
store().setValue(ComparePreferencePage.UNIFIED_DIFF, originalUnifiedDiff);
IWorkbenchPage page = activePage();
if (page != null) {
page.closeAllEditors(false);
}
processQueuedEvents();
if (project != null && project.exists()) {
project.delete(true, null);
}
}

@Test
public void testUnifiedEditorOpensForWorkspaceFileInput() throws Exception {
RecordingCompareEditorInput input = openQualifyingInput();

IEditorPart editorPart = activePage().getActiveEditor();
assertNotNull(editorPart, "an editor must be open"); //$NON-NLS-1$
assertFalse(editorPart instanceof CompareEditor,
"a qualifying input must open a text editor, not the compare editor"); //$NON-NLS-1$
ITextEditor textEditor = assertInstanceOf(ITextEditor.class, editorPart);
IFileEditorInput editorInput = assertInstanceOf(IFileEditorInput.class, textEditor.getEditorInput());
assertEquals("left.txt", editorInput.getFile().getName()); //$NON-NLS-1$

IAnnotationModel model = textEditor.getDocumentProvider().getAnnotationModel(textEditor.getEditorInput());
assertNotNull(model, "the unified diff editor must have an annotation model"); //$NON-NLS-1$
pumpUntil(() -> hasUnifiedDiffAnnotation(model), "unified diff annotations did not appear"); //$NON-NLS-1$

assertEquals(1, input.prepareInputCount.get(), "prepareInput must run exactly once per unified open"); //$NON-NLS-1$
}

// Documents current behavior: the unified diff path prepares the input on the
// UI thread. This assertion flips once the preparation moves to a background job.
@Test
public void testUnifiedOpenRunsPrepareInputOnUiThreadDocumentsCurrentBehavior() throws Exception {
RecordingCompareEditorInput input = openQualifyingInput();
assertTrue(input.prepareInputCount.get() >= 1, "prepareInput did not run"); //$NON-NLS-1$
assertEquals(true, input.anyRunOnUiThread,
"documents current behavior: the unified diff path runs prepareInput on the UI thread"); //$NON-NLS-1$
}

@Test
public void testClassicEditorOpensWhenInputDoesNotQualify() {
RecordingCompareEditorInput input = new RecordingCompareEditorInput(
new InMemoryElement("left.txt", "alpha\nbravo\n"), //$NON-NLS-1$ //$NON-NLS-2$
new InMemoryElement("right.txt", "alpha\nBRAVO\n")); //$NON-NLS-1$ //$NON-NLS-2$
CompareUI.openCompareEditor(input);
pumpUntil(() -> activePage().getActiveEditor() != null, "no editor opened"); //$NON-NLS-1$
assertInstanceOf(CompareEditor.class, activePage().getActiveEditor(),
"an input without shared document adapters must fall back to the compare editor"); //$NON-NLS-1$
}

private RecordingCompareEditorInput openQualifyingInput() throws CoreException {
IFile left = createFile("left.txt", "alpha\nbravo\ncharlie\ndelta\n"); //$NON-NLS-1$ //$NON-NLS-2$
IFile right = createFile("right.txt", "alpha\nBRAVO\ncharlie\ndelta\n"); //$NON-NLS-1$ //$NON-NLS-2$
RecordingCompareEditorInput input = new RecordingCompareEditorInput(
new WorkspaceFileElement(left), new WorkspaceFileElement(right));
CompareUI.openCompareEditor(input);
pumpUntil(() -> activePage().getActiveEditor() != null, "no editor opened"); //$NON-NLS-1$
return input;
}

private IFile createFile(String name, String content) throws CoreException {
IFile file = project.getFile(name);
ByteArrayInputStream source = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8));
if (file.exists()) {
file.setContents(source, true, false, null);
} else {
file.create(source, true, null);
}
return file;
}

private static boolean hasUnifiedDiffAnnotation(IAnnotationModel model) {
Iterator<Annotation> it = model.getAnnotationIterator();
while (it.hasNext()) {
String type = it.next().getType();
if (type != null && type.startsWith(UNIFIED_DIFF_ANNOTATION_PREFIX)) {
return true;
}
}
return false;
}

private static void pumpUntil(BooleanSupplier condition, String failMessage) {
Display display = Display.getCurrent();
long deadline = System.currentTimeMillis() + TIMEOUT_MILLIS;
// A self-rescheduling timer keeps the loop waking so the deadline is
// enforced even while blocked in Display.sleep().
Runnable[] wake = new Runnable[1];
wake[0] = () -> display.timerExec(50, wake[0]);
display.timerExec(50, wake[0]);
try {
while (!condition.getAsBoolean()) {
if (System.currentTimeMillis() > deadline) {
fail(failMessage + " within " + TIMEOUT_MILLIS + "ms"); //$NON-NLS-1$ //$NON-NLS-2$
}
if (!display.readAndDispatch()) {
display.sleep();
}
}
} finally {
display.timerExec(-1, wake[0]);
}
}

private static void processQueuedEvents() {
Display display = Display.getCurrent();
while (display.readAndDispatch()) {
// drain the event queue
}
}

private static IWorkbenchPage activePage() {
return PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage();
}

private static IPreferenceStore store() {
return CompareUIPlugin.getDefault().getPreferenceStore();
}
}
Loading