| /******************************************************************************* |
| * Copyright (c) 2015 EclipseSource Munich GmbH and others. |
| * |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * Philip Langer - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.emf.compare.ide.ui.tests.unit; |
| |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertTrue; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.Set; |
| |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.NullProgressMonitor; |
| import org.eclipse.core.runtime.Path; |
| import org.eclipse.emf.common.util.URI; |
| import org.eclipse.emf.compare.ide.ui.dependency.DependencyProviderDescriptor; |
| import org.eclipse.emf.compare.ide.ui.dependency.IDependencyProvider; |
| import org.eclipse.emf.compare.ide.ui.dependency.ModelDependencyProviderRegistry; |
| import org.eclipse.emf.compare.ide.ui.internal.EMFCompareIDEUIPlugin; |
| import org.eclipse.emf.ecore.EClass; |
| import org.eclipse.emf.ecore.EObject; |
| import org.eclipse.emf.ecore.EPackage; |
| import org.eclipse.emf.ecore.resource.Resource; |
| import org.eclipse.emf.ecore.resource.URIConverter; |
| import org.junit.Test; |
| |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.ImmutableSet; |
| |
| @SuppressWarnings({"nls", "restriction" }) |
| public class GitLogicalMergeWithCustomDependenciesTest extends AbstractGitLogicalModelTest { |
| |
| private static final String DEPENDENT_FILE_NAME = "dependentFile.ecore"; |
| |
| /** |
| * We have a file1 and file2. The file1 contains package P1, which in turn contains the classes C1 and C2. |
| * The file2 contains the package P2 containing the classes C3 and C4. The class C1 (in file1) is a |
| * super-class of C3 (in file2). Thus, there is a cross-reference from a model element in file1 to file2. |
| */ |
| @Override |
| public void setUp() throws Exception { |
| super.setUp(); |
| |
| // set up the initial models in file1 and file2 |
| final EPackage packageP1 = createPackage(null, "P1"); |
| final EClass classC1 = createClass(packageP1, "C1"); |
| createClass(packageP1, "C2"); |
| resource1.getContents().add(packageP1); |
| |
| final EPackage packageP2 = createPackage(null, "P2"); |
| final EClass classC3 = createClass(packageP2, "C3"); |
| createClass(packageP2, "C4"); |
| resource2.getContents().add(packageP2); |
| |
| classC3.getESuperTypes().add(classC1); |
| |
| save(resource1, resource2); |
| } |
| |
| @Override |
| public void tearDown() throws Exception { |
| // clear all dependency providers |
| getModelDependencyProviderRegistry().clear(); |
| super.tearDown(); |
| } |
| |
| private ModelDependencyProviderRegistry getModelDependencyProviderRegistry() { |
| return EMFCompareIDEUIPlugin.getDefault().getModelDependencyProviderRegistry(); |
| } |
| |
| /** |
| * In this test, a commit in a branch <em>adds</em> a <em>non-empty</em> file that is specified as a |
| * custom dependency but is not linked using cross-references; also it adds a class to file1. A commit in |
| * the master branch also adds a new class to file1 and file2. Adding the new classes on both sides, is |
| * only done to make sure the logical merge kicked in. If the logical merge wouldn't kick in, we'd get a |
| * textual conflict. |
| * <p> |
| * Since the logical merge has to deal with two existing and one added file, the EMFResourceMappingMerger |
| * is also responsible for merging the addition of the file in the branch although it is not directly |
| * linked to any other model. |
| * </p> |
| */ |
| @Test |
| public void testRemoteBranchAddsUnlinkedNonEmptyDependentFile() throws Exception { |
| final Collection<EObject> contentsOfNewResource = new HashSet<EObject>(); |
| contentsOfNewResource.add(createPackage(null, "P3")); |
| assertCorrectMergingIfRemoteBranchAddsUnlinkedButDependentFile(contentsOfNewResource); |
| } |
| |
| /** |
| * In this test, a commit in a branch <em>adds</em> a <em>empty</em> file that is specified as a custom |
| * dependency but is not linked using cross-references; also it adds a class to file1. A commit in the |
| * master branch also adds a new class to file1 and file2. Adding the new classes on both sides, is only |
| * done to make sure the logical merge kicked in. If the logical merge wouldn't kick in, we'd get a |
| * textual conflict. |
| * <p> |
| * Since the logical merge has to deal with two existing and one added file, the EMFResourceMappingMerger |
| * is also responsible for merging the addition of the file in the branch although it is not directly |
| * linked to any other model. |
| * </p> |
| */ |
| @Test |
| public void testRemoteBranchAddsUnlinkedEmptyDependentFile() throws Exception { |
| assertCorrectMergingIfRemoteBranchAddsUnlinkedButDependentFile(new HashSet<EObject>()); |
| } |
| |
| private void assertCorrectMergingIfRemoteBranchAddsUnlinkedButDependentFile( |
| final Collection<EObject> contentsOfAddedFile) throws Exception, IOException, CoreException { |
| // commit initial state as common ancestor commit |
| repository.addAllAndCommit("initial-commit"); |
| |
| // create branch but stay on master |
| repository.createBranch(MASTER, BRANCH); |
| |
| // add class C4 to file1, add class C5 to file2, and commit to master |
| createClass((EPackage)findObject(resource1, "P1"), "C4"); |
| createClass((EPackage)findObject(resource2, "P2"), "C5"); |
| save(resource1, resource2); |
| repository.addAndCommit(project, "master-commit", file1, file2); |
| |
| // checkout branch and add a file that will be declared as dependency |
| repository.checkoutBranch(BRANCH); |
| save(createResourceWithContents(DEPENDENT_FILE_NAME, contentsOfAddedFile)); |
| final File dependentFile = getFile(DEPENDENT_FILE_NAME); |
| assertTrue(dependentFile.exists()); |
| |
| // also add a new class to produce a textual conflict |
| createClass((EPackage)findObject(resource1, "P1"), "C6"); |
| save(resource1); |
| repository.addAndCommit(project, "branch-commit", dependentFile, file1); |
| |
| // install mock dependency provider |
| installMockModelDependencyProvider( |
| ImmutableMap.of(file1.getName(), ImmutableSet.of(DEPENDENT_FILE_NAME))); |
| |
| // checkout master and merge branch |
| repository.checkoutBranch(MASTER); |
| // dependent file does not exit before the merge |
| assertFalse(iProject.exists(new Path(DEPENDENT_FILE_NAME))); |
| |
| repository.mergeLogical(BRANCH); |
| // there shouldn't be a conflict, because the model-based merge handled this merge |
| assertTrue(repository.status().getConflicting().isEmpty()); |
| // dependent file does exit after the merge |
| assertTrue(iProject.exists(new Path(DEPENDENT_FILE_NAME))); |
| } |
| |
| /** |
| * In this test, a commit in a branch <em>deletes</em> a <em>non-empty</em> file that is specified as a |
| * custom dependency but is not linked using cross-references; also it adds a new class. A commit in the |
| * master branch adds a new class to file1 and to file2. Adding the new classes on both sides, is only |
| * done to make sure the logical merge kicked in. If the logical merge wouldn't kick in, we'd get a |
| * textual conflict. |
| * <p> |
| * Since the logical merge has to deal with two existing and one deleted file, the |
| * EMFResourceMappingMerger is also responsible for merging the deletion of the file in the branch |
| * although it is not directly linked to any other model. |
| * </p> |
| */ |
| @Test |
| public void testRemoteBranchDeletesUnlinkedNonEmptyDependentFile() throws Exception { |
| final Collection<EObject> contentsOfDeletedResource = new HashSet<EObject>(); |
| contentsOfDeletedResource.add(createPackage(null, "P3")); |
| assertCorrectMergingIfRemoteBranchDeletesUnlinkedButDependentFile(contentsOfDeletedResource); |
| } |
| |
| /** |
| * In this test, a commit in a branch <em>deletes</em> a <em>empty</em> file that is specified as a custom |
| * dependency but is not linked using cross-references; also it adds a new class. A commit in the master |
| * branch adds a new class to file1 and to file2. Adding the new classes on both sides, is only done to |
| * make sure the logical merge kicked in. If the logical merge wouldn't kick in, we'd get a textual |
| * conflict. |
| * <p> |
| * Since the logical merge has to deal with two existing and one deleted file, the |
| * EMFResourceMappingMerger is also responsible for merging the deletion of the file in the branch |
| * although it is not directly linked to any other model. |
| * </p> |
| */ |
| @Test |
| public void testRemoteBranchDeletesUnlinkedEmptyDependentFile() throws Exception { |
| assertCorrectMergingIfRemoteBranchDeletesUnlinkedButDependentFile(new HashSet<EObject>()); |
| } |
| |
| private void assertCorrectMergingIfRemoteBranchDeletesUnlinkedButDependentFile( |
| final Collection<EObject> contentsOfDeletedResource) |
| throws IOException, CoreException, Exception { |
| // add another dependent file and commit state as common ancestor commit |
| save(createResourceWithContents(DEPENDENT_FILE_NAME, contentsOfDeletedResource)); |
| repository.addAllAndCommit("initial-commit"); |
| |
| // create branch but stay on master |
| repository.createBranch(MASTER, BRANCH); |
| |
| // add class C4 to file1, add class C5 to file2, and commit to master |
| createClass((EPackage)findObject(resource1, "P1"), "C4"); |
| createClass((EPackage)findObject(resource2, "P2"), "C5"); |
| save(resource1, resource2); |
| repository.addAndCommit(project, "master-commit", file1, file2); |
| |
| // checkout branch, delete the dependent file, and commit |
| repository.checkoutBranch(BRANCH); |
| final File dependentFile = getFile(DEPENDENT_FILE_NAME); |
| iProject.getFile(DEPENDENT_FILE_NAME).delete(true, new NullProgressMonitor()); |
| assertFalse(iProject.exists(new Path(DEPENDENT_FILE_NAME))); |
| |
| // also add a new class to produce a textual conflict |
| createClass((EPackage)findObject(resource1, "P1"), "C6"); |
| save(resource1); |
| repository.addAndCommit(project, "branch-commit", file1, dependentFile); |
| |
| // install mock dependency provider |
| installMockModelDependencyProvider( |
| ImmutableMap.of(file1.getName(), ImmutableSet.of(DEPENDENT_FILE_NAME))); |
| |
| // checkout master and merge branch |
| repository.checkoutBranch(MASTER); |
| // dependent file exits before the merge |
| assertTrue(iProject.exists(new Path(DEPENDENT_FILE_NAME))); |
| |
| repository.mergeLogical(BRANCH); |
| // there shouldn't be a conflict, because the model-based merge handled this merge |
| assertTrue(repository.status().getConflicting().isEmpty()); |
| // dependent file does not exit after the merge |
| assertFalse(iProject.exists(new Path(DEPENDENT_FILE_NAME))); |
| } |
| |
| private Resource createResourceWithContents(String fileName, Collection<EObject> contents) |
| throws Exception { |
| final Resource newResource = createAndConnectResource(fileName); |
| newResource.getContents().addAll(contents); |
| return newResource; |
| } |
| |
| private void installMockModelDependencyProvider( |
| final ImmutableMap<String, ImmutableSet<String>> dependencies) { |
| getModelDependencyProviderRegistry().addProvider("mock", |
| new DependencyProviderDescriptor(null, null) { |
| private final IDependencyProvider mock = new MockDependencyProvider(dependencies); |
| |
| @Override |
| public IDependencyProvider getDependencyProvider() { |
| return mock; |
| } |
| }); |
| } |
| |
| private class MockDependencyProvider implements IDependencyProvider { |
| |
| private ImmutableMap<String, ImmutableSet<String>> dependencies; |
| |
| public MockDependencyProvider(ImmutableMap<String, ImmutableSet<String>> dependencies) { |
| this.dependencies = dependencies; |
| } |
| |
| public boolean apply(URI uri) { |
| return dependencies.containsKey(uri.lastSegment()); |
| } |
| |
| public Set<URI> getDependencies(URI uri, URIConverter uriConverter) { |
| if (dependencies.containsKey(uri.lastSegment())) { |
| final Set<URI> uris = new HashSet<URI>(); |
| for (String fileName : dependencies.get(uri.lastSegment())) { |
| final URI dependentUri = uri.trimSegments(1).appendSegment(fileName); |
| if (uriConverter.exists(dependentUri, Collections.emptyMap())) { |
| uris.add(dependentUri); |
| } |
| } |
| return uris; |
| } else { |
| return Collections.emptySet(); |
| } |
| } |
| |
| } |
| |
| } |