/*******************************************************************************
* Copyright (c) 2015, 2016 Pivotal, Inc.
* 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:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.eclipse.boot.test;
import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.ARTIFACT_ID;
import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.DEPENDENCIES;
import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.DEPENDENCY;
import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.DEPENDENCY_MANAGEMENT;
import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.GROUP_ID;
import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.SCOPE;
import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.TYPE;
import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.VERSION;
import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.childEquals;
import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.findChild;
import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.findChilds;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.springframework.ide.eclipse.boot.test.BootProjectTestHarness.bootVersion;
import static org.springframework.ide.eclipse.boot.test.BootProjectTestHarness.withStarters;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.m2e.core.ui.internal.editing.PomEdits;
import org.eclipse.wst.sse.core.StructuredModelManager;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMDocument;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.ide.eclipse.boot.core.ISpringBootProject;
import org.springframework.ide.eclipse.boot.core.MavenId;
import org.springframework.ide.eclipse.boot.core.SpringBootCore;
import org.springframework.ide.eclipse.boot.core.SpringBootStarter;
import org.springframework.ide.eclipse.boot.core.SpringBootStarters;
import org.springframework.ide.eclipse.boot.core.initializr.InitializrDependencySpec;
import org.springframework.ide.eclipse.boot.core.initializr.InitializrService;
import org.springframework.ide.eclipse.boot.core.initializr.InitializrServiceSpec;
import org.springframework.ide.eclipse.boot.core.initializr.InitializrServiceSpec.Dependency;
import org.springframework.ide.eclipse.boot.test.util.TestBracketter;
import org.springframework.ide.eclipse.boot.util.Log;
import org.springframework.ide.eclipse.boot.wizard.EditStartersModel;
import org.springframework.ide.eclipse.boot.wizard.PopularityTracker;
import org.springsource.ide.eclipse.commons.frameworks.core.util.IOUtil;
import org.springsource.ide.eclipse.commons.tests.util.StsTestUtil;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
/**
* @author Kris De Volder
*/
@SuppressWarnings("restriction")
public class EditStartersModelTest {
private static final String BOOT_1_3_X_RELEASE = "1.3.8.RELEASE";
private static final String REPOSITORY = "repository";
private static final String REPOSITORIES = "repositories";
private MockInitializrService initializr = new MockInitializrService();
private SpringBootCore springBootCore = new SpringBootCore(initializr);
private BootProjectTestHarness harness = new BootProjectTestHarness(ResourcesPlugin.getWorkspace());
private IPreferenceStore prefs = new MockPrefsStore();
private static boolean wasAutobuilding;
@BeforeClass
public static void setupClass() throws Exception {
wasAutobuilding = StsTestUtil.isAutoBuilding();
StsTestUtil.setAutoBuilding(false);
}
@AfterClass
public static void teardownClass() throws Exception {
StsTestUtil.setAutoBuilding(wasAutobuilding);
}
@Before
public void setup() throws Exception {
StsTestUtil.cleanUpProjects();
}
@Rule
public TestBracketter testBracketer = new TestBracketter();
/**
* Tests that the EditStartersModel is parsed and that existing starters already present on the
* project are initially selected.
*/
@Test
public void existingStartersSelected() throws Exception {
IProject project = harness.createBootProject("existingStartersSelected", withStarters("web", "actuator"));
ISpringBootProject bootProject = springBootCore.project(project);
EditStartersModel wizard = createWizard(project);
assertEquals(bootProject.getBootVersion(), wizard.getBootVersion());
assertStarterDeps(wizard.dependencies.getCurrentSelection(), "web", "actuator");
}
/**
* Tests that we are able to remove a starter.
*/
@Test
public void removeStarter() throws Exception {
IProject project = harness.createBootProject("removeStarter", withStarters("web", "actuator"));
final ISpringBootProject bootProject = springBootCore.project(project);
EditStartersModel wizard = createWizard(project);
assertEquals(bootProject.getBootVersion(), wizard.getBootVersion());
assertStarterDeps(wizard.dependencies.getCurrentSelection(), "web", "actuator");
wizard.removeDependency("web");
assertStarterDeps(wizard.dependencies.getCurrentSelection(), /* removed: "web",*/ "actuator");
performOk(wizard);
StsTestUtil.assertNoErrors(project); //force project build
assertStarters(bootProject.getBootStarters(), "actuator");
}
private void performOk(EditStartersModel wizard) throws Exception {
wizard.performOk();
Job.getJobManager().join(EditStartersModel.JOB_FAMILY, null);
}
/**
* Tests that we are able to add a basic starter.
*/
@Test
public void addStarter() throws Exception {
IProject project = harness.createBootProject("addStarter", withStarters("web"));
final ISpringBootProject bootProject = springBootCore.project(project);
EditStartersModel wizard = createWizard(project);
assertEquals(bootProject.getBootVersion(), wizard.getBootVersion());
assertStarterDeps(wizard.dependencies.getCurrentSelection(), "web");
PopularityTracker popularities = new PopularityTracker(prefs);
assertUsageCounts(bootProject, popularities /*none*/);
wizard.addDependency("actuator");
assertStarterDeps(wizard.dependencies.getCurrentSelection(), "web", "actuator");
performOk(wizard);
assertUsageCounts(bootProject, popularities, "actuator:1");
StsTestUtil.assertNoErrors(project); //force project build
assertStarters(bootProject.getBootStarters(), "web", "actuator");
//check that the 'scope' is not set in the pom.xml:
IDOMDocument pom = parsePom(project);
Element depEl = findDependency(bootProject, pom, "actuator");
assertEquals("org.springframework.boot", getGroupId(depEl));
assertEquals("spring-boot-starter-actuator", getArtifactId(depEl));
assertEquals(null, getScope(depEl));
}
@Test
public void addStarterWithTestScope() throws Exception {
IProject project = harness.createBootProject("addStarterWithTestScope", withStarters("web"));
final ISpringBootProject bootProject = springBootCore.project(project);
EditStartersModel wizard = createWizard(project);
wizard.addDependency("restdocs");
performOk(wizard);
StsTestUtil.assertNoErrors(project); //force project build
assertStarters(bootProject.getBootStarters(), "web", "restdocs");
//check that the 'scope' is set properly
IDOMDocument pom = parsePom(project);
Element depEl = findDependency(bootProject, pom, "restdocs");
assertEquals("spring-restdocs-mockmvc", getArtifactId(depEl));
assertEquals("test", getScope(depEl));
}
@Test
public void addStarterWithBom() throws Exception {
// We'll be adding this starter:
// "id": "cloud-eureka",
// "groupId": "org.springframework.cloud",
// "artifactId": "spring-cloud-starter-eureka",
// "scope": "compile",
// "bom": "cloud-bom"
IProject project = harness.createBootProject("addStarterWithBom", withStarters("web"));
final ISpringBootProject bootProject = springBootCore.project(project);
EditStartersModel wizard = createWizard(project);
wizard.addDependency("cloud-eureka");
performOk(wizard);
System.out.println(">>> pom.xml (after dialog closed)");
System.out.println(IOUtil.toString(project.getFile("pom.xml").getContents()));
System.out.println("<<< pom.xml (after dialog closed)");
StsTestUtil.assertNoErrors(project); //force project build
assertStarters(bootProject.getBootStarters(), "web", "cloud-eureka");
}
@Test
public void addMultipleStartersWithSameBom() throws Exception {
//This test uses more 'controlled' parameters:
IProject project = harness.createBootProject("addMultipleStartersWithSameBom",
bootVersion(BOOT_1_3_X_RELEASE), // boot version fixed
withStarters("web")
);
initializr.setInputs("sample"); // sample intializr json captured for this version
final ISpringBootProject bootProject = springBootCore.project(project);
int initialBomCount = getBomCount(parsePom(project));
EditStartersModel wizard = createWizard(project);
wizard.addDependency("cloud-eureka");
wizard.addDependency("cloud-config-client");
performOk(wizard);
StsTestUtil.assertNoErrors(project); //force project build
assertStarters(bootProject.getBootStarters(), "web", "cloud-eureka", "cloud-config-client");
IDOMDocument pom = parsePom(project);
int finalBomCount = getBomCount(pom);
assertEquals(initialBomCount+1, finalBomCount);
//check that both repos got added
assertRepoCount(2, pom);
Element repo = getRepo(pom, "spring-snapshots");
assertNotNull(repo);
assertEquals("Spring Snapshots", getTextChild(repo, "name"));
assertEquals("https://repo.spring.io/snapshot", getTextChild(repo, "url"));
assertEquals("true", getSnapshotsEnabled(repo));
repo = getRepo(pom, "spring-milestones");
assertNotNull(repo);
assertEquals("Spring Milestones", getTextChild(repo, "name"));
assertEquals("https://repo.spring.io/milestone", getTextChild(repo, "url"));
assertEquals("false", getSnapshotsEnabled(repo));
}
@Test
public void addMultipleStartersWithDifferentBom() throws Exception {
//This test uses more 'controlled' parameters:
IProject project = harness.createBootProject("addMultipleStartersWithDifferentBom",
bootVersion(BOOT_1_3_X_RELEASE), // boot version fixed
withStarters("web")
);
initializr.setInputs("sample"); // sample intializr json captured for this version
final ISpringBootProject bootProject = springBootCore.project(project);
int initialBomCount = getBomCount(parsePom(project));
EditStartersModel wizard = createWizard(project);
wizard.addDependency("cloud-eureka");
wizard.addDependency("vaadin");
performOk(wizard);
StsTestUtil.assertNoErrors(project); //force project build
assertStarters(bootProject.getBootStarters(), "web", "cloud-eureka", "vaadin");
IDOMDocument pom = parsePom(project);
int finalBomCount = getBomCount(pom);
assertEquals(initialBomCount+2, finalBomCount);
{
Element bom = getBom(pom, "spring-cloud-starter-parent");
assertEquals("org.springframework.cloud", getTextChild(bom,GROUP_ID));
assertEquals("Brixton.M3", getTextChild(bom,VERSION));
assertEquals("pom", getTextChild(bom,TYPE));
assertEquals("import", getTextChild(bom,SCOPE));
}
{
Element bom = getBom(pom, "vaadin-bom");
assertEquals("com.vaadin", getTextChild(bom,GROUP_ID));
assertEquals("7.5.5", getTextChild(bom,VERSION));
assertEquals("pom", getTextChild(bom,TYPE));
assertEquals("import", getTextChild(bom,SCOPE));
}
}
@Test
public void addBomWithSubsetOfRepos() throws Exception {
//This test uses more 'controlled' parameters:
String bootVersion = BOOT_1_3_X_RELEASE;
IProject project = harness.createBootProject("addBomWithSubsetOfRepos",
bootVersion(bootVersion), // boot version fixed
withStarters("web")
);
initializr.setInputs("sample-with-fakes"); // must use 'fake' data because the situation we are after doesn't exist in the real data
EditStartersModel wizard = createWizard(project);
wizard.addDependency("cloud-eureka");
performOk(wizard);
//!!! fake data may not produce a project that builds without
//!!! problem so don't check for build errors in this test
//check that only ONE repo got added
IDOMDocument pom = parsePom(project);
assertNotNull(getRepo(pom, "spring-milestones"));
assertNull(getRepo(pom, "spring-snapshots"));
assertRepoCount(1, pom);
}
@Test
public void addDependencyWithRepo() throws Exception {
//This test uses more 'controlled' parameters:
String bootVersion = BOOT_1_3_X_RELEASE;
IProject project = harness.createBootProject("addDependencyWithRepo",
bootVersion(bootVersion), // boot version fixed
withStarters("web")
);
initializr.setInputs("sample-with-fakes"); // must use 'fake' data because the situation we are after doesn't exist in the real data
EditStartersModel wizard = createWizard(project);
wizard.addDependency("fake-dep");
performOk(wizard);
//!!! fake data may not produce a project that builds without
//!!! problem so don't check for build errors in this test
IDOMDocument pom = parsePom(project);
//check the dependency got added to the pom
assertNotNull(findDependency(pom, new MavenId("org.springframework.fake", "spring-fake-dep")));
//check that just the expected repo got added
assertNotNull(getRepo(pom, "spring-milestones"));
assertNull(getRepo(pom, "spring-snapshots"));
assertRepoCount(1, pom);
}
// @Test
// public void serviceUnavailable() throws Exception {
// String bootVersion = BOOT_1_3_X_RELEASE;
// IProject project = harness.createBootProject("serviceUnavailable",
// bootVersion(bootVersion), // boot version fixed
// withStarters("web")
// );
//
// initializr.makeUnavailable();
//
// EditStartersModel wizard = createWizard(project);
// assertFalse(wizard.isSupported());
// }
//TODO: testing of...
// - repository field in individual dependency taken into account?
////////////// Harness code below ///////////////////////////////////////////////
private String getSnapshotsEnabled(Element repo) {
if (repo!=null) {
Element snapshots = findChild(repo, "snapshots");
if (snapshots!=null) {
return getTextChild(snapshots, "enabled");
}
}
return null;
}
private Element getRepo(IDOMDocument pom, String id) {
Element doc = pom.getDocumentElement();
Element repos = findChild(doc, REPOSITORIES);
if (repos!=null) {
return findChild(repos, REPOSITORY, childEquals("id", id));
}
return null;
}
private String getScope(Element depEl) {
return getTextChild(depEl, SCOPE);
}
private String getTextChild(Element depEl, String name) {
Element child = findChild(depEl, name);
if (child!=null) {
return PomEdits.getTextValue(child);
}
return null;
}
private String getGroupId(Element depEl) {
return getTextChild(depEl, GROUP_ID);
}
private String getArtifactId(Element depEl) {
return getTextChild(depEl, ARTIFACT_ID);
}
public IDOMDocument parsePom(IProject project) throws IOException, CoreException {
return ((IDOMModel) StructuredModelManager.getModelManager().createUnManagedStructuredModelFor(project.getFile("pom.xml"))).getDocument();
}
private Element findDependency(ISpringBootProject project, Document pom, String id) {
try {
SpringBootStarters starters = project.getStarterInfos();
MavenId mid = starters.getMavenId(id);
if (mid!=null) {
return findDependency(pom, mid);
}
} catch (Exception e) {
Log.log(e);
}
return null;
}
public static Element findDependency(Document document, MavenId dependency) {
Element dependenciesElement = findChild(document.getDocumentElement(), DEPENDENCIES);
return findChild(dependenciesElement, DEPENDENCY, childEquals(GROUP_ID, dependency.getGroupId()),
childEquals(ARTIFACT_ID, dependency.getArtifactId()));
}
private EditStartersModel createWizard(IProject project) throws Exception {
return new EditStartersModel(
project,
springBootCore,
prefs
);
}
private void assertStarters(List<SpringBootStarter> starters, String... expectedIds) {
Set<String> expecteds = new HashSet<>(Arrays.asList(expectedIds));
for (SpringBootStarter starter : starters) {
String id = starter.getId();
if (expecteds.remove(id)) {
//okay
} else {
fail("Unexpected starter found: "+starter);
}
}
if (!expecteds.isEmpty()) {
fail("Expected starters not found: "+expecteds);
}
}
private void assertStarterDeps(List<Dependency> starters, String... expectedIds) {
Set<String> expecteds = new HashSet<>(Arrays.asList(expectedIds));
for (Dependency starter : starters) {
String id = starter.getId();
if (expecteds.remove(id)) {
//okay
} else {
fail("Unexpected starter found: "+starter);
}
}
if (!expecteds.isEmpty()) {
fail("Expected starters not found: "+expecteds);
}
}
private void assertRepoCount(int expect, IDOMDocument pom) {
assertEquals(expect, getRepoCount(pom));
}
private int getRepoCount(IDOMDocument pom) {
Element reposEl = findChild(pom.getDocumentElement(), REPOSITORIES);
if (reposEl!=null) {
List<Element> repos = findChilds(reposEl, REPOSITORY);
if (repos!=null) {
return repos.size();
}
}
return 0;
}
/**
* Find a bom element in pom based on its artifactId (can't use its own id as that isn't
* found anywhere in the element's xml).
*/
private Element getBom(IDOMDocument pom, String aid) {
Element depman = findChild(pom.getDocumentElement(), DEPENDENCY_MANAGEMENT);
if (depman!=null) {
Element deps = findChild(depman, DEPENDENCIES);
if (deps!=null) {
return findChild(deps, DEPENDENCY, childEquals(ARTIFACT_ID, aid));
}
}
return null;
}
private int getBomCount(IDOMDocument pom) {
Element depman = findChild(pom.getDocumentElement(), DEPENDENCY_MANAGEMENT);
if (depman!=null) {
Element deps = findChild(depman, DEPENDENCIES);
if (deps!=null) {
List<Element> boms = findChilds(deps, DEPENDENCY);
if (boms!=null) {
return boms.size();
}
}
}
return 0;
}
private void assertUsageCounts(ISpringBootProject project, PopularityTracker popularities, String... idAndCount) throws Exception {
Map<String, Integer> expect = new HashMap<>();
for (String pair : idAndCount) {
String[] pieces = pair.split(":");
assertEquals(2, pieces.length);
String id = pieces[0];
int count = Integer.parseInt(pieces[1]);
expect.put(id, count);
}
List<SpringBootStarter> knownStarters = project.getKnownStarters();
assertFalse(knownStarters.isEmpty());
for (SpringBootStarter starter : knownStarters) {
String id = starter.getId();
Integer expectedCountOrNull = expect.get(id);
int expectedCount = expectedCountOrNull==null ? 0 : expectedCountOrNull;
assertEquals("Usage count for '"+id+"'", expectedCount, popularities.getUsageCount(id));
expect.remove(id);
}
assertTrue("Expected usage counts not found: "+expect, expect.isEmpty());
}
public class MockInitializrService implements InitializrService {
private SpringBootStarters starters;
private boolean unavailable = false;
/**
* Causes the mock to parse input from given input streams instead of calling out to
* the real web service.
*/
public void setInputs(InputStream main, InputStream dependencies) throws Exception {
starters = new SpringBootStarters(
InitializrServiceSpec.parseFrom(main),
InitializrDependencySpec.parseFrom(dependencies)
);
}
/**
* Causes the mock to parse input from some resources located relative to the test
* class.
*/
public void setInputs(String name) throws Exception {
setInputs(getResource(name, "main"), getResource(name, "dependencies"));
}
private InputStream getResource(String name, String endPoint) {
return getClass().getResourceAsStream("edit-starters-test-inputs/"+name+"-"+endPoint+".json");
}
@Override
public SpringBootStarters getStarters(String bootVersion) throws Exception {
if (unavailable) {
throw new IOException("Initializr Service Unavailable");
} else if (starters!=null) {
return starters;
} else {
return InitializrService.DEFAULT.getStarters(bootVersion);
}
}
/**
* Make the mock behave as if the 'dependencies' endpoint is not available (either the service is down,
* there is no internet connection, or this is an old service that doesn't implement the endpoint yet).
*/
public void makeUnavailable() {
this.unavailable = true;
}
}
}