/******************************************************************************* * 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.dash.test; import static org.junit.Assert.assertArrayEquals; 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.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.springframework.ide.eclipse.boot.dash.test.BootDashViewModelHarness.assertLabelContains; import static org.springframework.ide.eclipse.boot.dash.test.BootDashViewModelHarness.getLabel; import static org.springframework.ide.eclipse.boot.dash.test.requestmappings.RequestMappingAsserts.assertRequestMappingWithPath; import static org.springframework.ide.eclipse.boot.test.BootProjectTestHarness.bootVersionAtLeast; import static org.springframework.ide.eclipse.boot.test.BootProjectTestHarness.withStarters; import static org.springsource.ide.eclipse.commons.tests.util.StsTestCase.assertElements; import static org.springsource.ide.eclipse.commons.tests.util.StsTestCase.createFile; import static org.springsource.ide.eclipse.commons.tests.util.StsTestCase.setContents; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.preferences.IEclipsePreferences; import org.eclipse.core.runtime.preferences.InstanceScope; import org.eclipse.debug.core.DebugPlugin; import org.eclipse.debug.core.ILaunch; import org.eclipse.debug.core.ILaunchConfiguration; import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.IMethod; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.JavaCore; import org.eclipse.ui.IWorkingSet; import org.eclipse.ui.IWorkingSetManager; import org.eclipse.ui.PlatformUI; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestRule; import org.springframework.ide.eclipse.boot.core.IMavenCoordinates; 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.dash.model.AbstractLaunchConfigurationsDashElement; import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; import org.springframework.ide.eclipse.boot.dash.model.BootDashElementsFilterBoxModel; import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; import org.springframework.ide.eclipse.boot.dash.model.BootDashModel.ElementStateListener; import org.springframework.ide.eclipse.boot.dash.model.BootProjectDashElement; import org.springframework.ide.eclipse.boot.dash.model.LaunchConfDashElement; import org.springframework.ide.eclipse.boot.dash.model.RunState; import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; import org.springframework.ide.eclipse.boot.dash.model.requestmappings.RequestMapping; import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.RunTargetTypes; import org.springframework.ide.eclipse.boot.dash.util.CollectionUtils; import org.springframework.ide.eclipse.boot.dash.views.BootDashLabels; import org.springframework.ide.eclipse.boot.dash.views.sections.BootDashColumn; import org.springframework.ide.eclipse.boot.launch.AbstractBootLaunchConfigurationDelegate.PropVal; import org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate; import org.springframework.ide.eclipse.boot.launch.util.PortFinder; import org.springframework.ide.eclipse.boot.test.AutobuildingEnablement; import org.springframework.ide.eclipse.boot.test.BootProjectTestHarness; import org.springframework.ide.eclipse.boot.test.BootProjectTestHarness.WizardConfigurer; import org.springframework.ide.eclipse.boot.test.util.TestBracketter; import org.springframework.ide.eclipse.boot.ui.EnableDisableBootDevtools; import org.springsource.ide.eclipse.commons.frameworks.core.maintype.MainTypeFinder; import org.springsource.ide.eclipse.commons.frameworks.test.util.ACondition; import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; import org.springsource.ide.eclipse.commons.livexp.util.Filter; import org.springsource.ide.eclipse.commons.tests.util.StsTestUtil; import com.google.common.collect.ImmutableSet; /** * @author Kris De Volder */ public class BootDashModelTest { private static final long MODEL_UPDATE_TIMEOUT = 10000; // short, should be nearly instant private static final long RUN_STATE_CHANGE_TIMEOUT = 40000; private static final long MAVEN_BUILD_TIMEOUT = 40000; private SpringBootCore springBootCore = SpringBootCore.getDefault(); // should be getting this via projects harness? private TestBootDashModelContext context; private BootProjectTestHarness projects; private BootDashModel model; private PortFinder portFinder = new PortFinder(); @Rule public AutobuildingEnablement autobuild = new AutobuildingEnablement(false); @Rule public TestBracketter testBracketer = new TestBracketter(); @Rule public DumpBootProcessOutput processOutput = new DumpBootProcessOutput(); /** * Test that newly created spring boot project gets added to the model. */ @Test public void testNewSpringBootProject() throws Exception { // assertWorkspaceProjects(/*none*/); assertModelElements(/*none*/); String projectName = "testProject"; createBootProject(projectName); new ACondition("Model update", MODEL_UPDATE_TIMEOUT) { public boolean test() throws Exception { assertModelElements("testProject"); return true; } }; BootDashElement projectEl = getElement("testProject"); assertTrue(projectEl.getCurrentChildren().isEmpty()); } /** * Test that project with multiple associated launch configs has * a child for each config. */ @Test public void testSpringBootProjectChildren() throws Exception { // assertWorkspaceProjects(/*none*/); assertModelElements(/*none*/); String projectName = "testProject"; IProject project = createBootProject(projectName); IJavaProject javaProject = JavaCore.create(project); new ACondition("Model update", MODEL_UPDATE_TIMEOUT) { public boolean test() throws Exception { assertModelElements("testProject"); return true; } }; BootDashElement projectEl = getElement("testProject"); assertTrue(projectEl.getCurrentChildren().isEmpty()); ILaunchConfiguration conf1 = BootLaunchConfigurationDelegate.createConf(javaProject); ILaunchConfiguration conf2 = BootLaunchConfigurationDelegate.createConf(javaProject); assertFalse(conf1.equals(conf2)); ACondition.waitFor("Children to appear", 3000, () -> { assertEquals(2, projectEl.getCurrentChildren().size()); }); conf1.delete(); ACondition.waitFor("Child to disappear", 3000, () -> { assertEquals(1, projectEl.getCurrentChildren().size()); }); } /** * Test that when a launch config is marked as 'hidden' it is not part of the model. */ @Test public void testSpringBootProjectHiddenChildren() throws Exception { assertModelElements(/*none*/); String projectName = "testProject"; IProject project = createBootProject(projectName); IJavaProject javaProject = JavaCore.create(project); new ACondition("Model update", MODEL_UPDATE_TIMEOUT) { public boolean test() throws Exception { assertModelElements("testProject"); return true; } }; final BootDashElement projectEl = getElement("testProject"); assertTrue(projectEl.getCurrentChildren().isEmpty()); final ILaunchConfiguration[] conf = new ILaunchConfiguration[3]; final BootDashElement[] el = new BootDashElement[conf.length]; for (int i = 0; i < conf.length; i++) { conf[i] = BootLaunchConfigurationDelegate.createConf(javaProject); el[i] = harness.getElementFor(conf[i]); } new ACondition("Wait for children", MODEL_UPDATE_TIMEOUT) { public boolean test() throws Exception { assertEquals(ImmutableSet.copyOf(el), projectEl.getCurrentChildren()); return true; } }; hide(conf[2]); new ACondition("Wait for child to disapear", MODEL_UPDATE_TIMEOUT) { public boolean test() throws Exception { assertEquals(ImmutableSet.of(el[0], el[1]), projectEl.getCurrentChildren()); return true; } }; hide(conf[1]); new ACondition("Wait for another child to disapear", MODEL_UPDATE_TIMEOUT) { public boolean test() throws Exception { //since there's just one conf left it is not shown as a child assertEquals(ImmutableSet.of(el[0]), projectEl.getCurrentChildren()); return true; } }; hide(conf[0]); new ACondition("Wait for last child to disapear", MODEL_UPDATE_TIMEOUT) { public boolean test() throws Exception { //since there's just one conf left it is not shown as a child assertEquals(ImmutableSet.of(), projectEl.getCurrentChildren()); return true; } }; } private void hide(ILaunchConfiguration conf) throws Exception { ILaunchConfigurationWorkingCopy wc = conf.getWorkingCopy(); BootLaunchConfigurationDelegate.setHiddenFromBootDash(wc, true); wc.doSave(); } private IProject createBootProject(String projectName, WizardConfigurer... extraConfs) throws Exception { return projects.createBootWebProject(projectName, extraConfs); } /** * Test that when project is deleted from workspace it also deleted from the model */ @Test public void testDeleteProject() throws Exception { String projectName = "testProject"; IProject project = createBootProject(projectName); waitModelElements("testProject"); project.delete(/*delete content*/true, /*force*/true, /*progress*/null); waitModelElements(/*none*/); } /** * Test that when closed/opened it is removed/added to the model */ @Test public void testCloseAndOpenProject() throws Exception { String projectName = "testProject"; IProject project = createBootProject(projectName); waitModelElements("testProject"); project.close(null); waitModelElements(); project.open(null); waitModelElements("testProject"); } /** * Test that deleting a running launch conf works properly: * 1) orphaned launch is terminated * 2) BootProjectDashElement runstate is updated. */ @Test public void testDeleteRunningLaunchConfig() throws Exception { doTestDeleteRunningLaunchConf(RunState.RUNNING); } /** * Test that deleting a running launch conf, (in debug mode) works properly: * 1) orphaned launch is terminated * 2) BootProjectDashElement runstate is updated. */ @Test public void testDeleteDebuggingLaunchConfig() throws Exception { doTestDeleteRunningLaunchConf(RunState.DEBUGGING); } private void doTestDeleteRunningLaunchConf(RunState runState) throws Exception, CoreException { String projectName = "testProject"; createBootProject(projectName); waitModelElements(projectName); BootProjectDashElement element = getElement(projectName); element.openConfig(ui); //Ensure that at least one launch config exists. verify(ui).openLaunchConfigurationDialogOnGroup(any(ILaunchConfiguration.class), any(String.class)); verifyNoMoreInteractions(ui); ACondition.waitFor("child", 3000, () -> { assertNotNull(getSingleValue(element.getCurrentChildren())); }); LaunchConfDashElement launchConfElement = (LaunchConfDashElement) getSingleValue(element.getCurrentChildren()); ILaunchConfiguration launchConf = getSingleValue(launchConfElement.getLaunchConfigs()); element.restart(runState, null); waitForState(element, runState); waitForState(launchConfElement, runState); ILaunch launch = getSingleValue(launchConfElement.getLaunches()); assertFalse(launch.isTerminated()); launchConf.delete(); ACondition.waitFor("Expectations after launchConf deleted", 2000, () -> { assertTrue("launch terminated", launch.isTerminated()); assertEquals(ImmutableSet.of(), element.getChildren().getValues()); assertEquals(RunState.INACTIVE, element.getRunState()); }); } /** * Test that element state listener for launch conf element is notified when it is * launched via its project. */ @Test public void testLaunchConfRunStateChanges() throws Exception { doTestLaunchConfRunStateChanges(RunState.RUNNING); } /** * Test that element state listener for launch conf element is notified when it is * launched via its project. */ @Test public void testLaunchConfDebugStateChanges() throws Exception { doTestLaunchConfRunStateChanges(RunState.DEBUGGING); } protected void doTestLaunchConfRunStateChanges(RunState runState) throws Exception { String projectName = "testProject"; createBootProject(projectName); waitModelElements(projectName); BootProjectDashElement element = getElement(projectName); element.openConfig(ui); //Ensure that at least one launch config exists. verify(ui).openLaunchConfigurationDialogOnGroup(any(ILaunchConfiguration.class), any(String.class)); verifyNoMoreInteractions(ui); ACondition.waitFor("child", 3000, () -> { assertNotNull(getSingleValue(element.getCurrentChildren())); }); BootDashElement childElement = getSingleValue(element.getCurrentChildren()); ElementStateListener listener = mock(ElementStateListener.class); model.addElementStateListener(listener); // System.out.println("Element state listener ADDED"); // model.addElementStateListener(new ElementStateListener() { // public void stateChanged(BootDashElement e) { // System.out.println("Changed: "+e); // } // }); element.restart(runState, null); waitForState(element, runState); waitForState(childElement, runState); ElementStateListener oldListener = listener; ACondition.waitFor("listener calls", 1000, () -> { verify(oldListener, times(4)).stateChanged(element); verify(oldListener, times(4)).stateChanged(childElement); }); model.removeElementStateListener(oldListener); // System.out.println("Element state listener REMOVED"); listener = mock(ElementStateListener.class); model.addElementStateListener(listener); element.stopAsync(ui); waitForState(element, RunState.INACTIVE); waitForState(childElement, RunState.INACTIVE); //4 changes: INACTIVE -> STARTING, STARTING -> RUNNING, livePort(set), actualInstances verify(oldListener, times(4)).stateChanged(element); verify(oldListener, times(4)).stateChanged(childElement); //3 changes: RUNNING -> INACTIVE, liveport(unset), actualInstances verify(listener, times(3)).stateChanged(element); verify(listener, times(3)).stateChanged(childElement); } private <T> T getSingleValue(ImmutableSet<T> values) { assertEquals("Unexpected number of values in "+values, 1, values.size()); for (T e : values) { return e; } throw new IllegalStateException("This code should be unreachable"); } @Test public void startLifeCycleDisabledApp() throws Exception { String projectName = "some-app"; IProject project = createBootProject(projectName, bootVersionAtLeast("1.3")); BootProjectDashElement element = getElement(projectName); element.openConfig(ui); //Ensure that at least one launch config exists. verify(ui).openLaunchConfigurationDialogOnGroup(any(ILaunchConfiguration.class), any(String.class)); verifyNoMoreInteractions(ui); //Disable lifecycle mgmt on config ACondition.waitFor("child", 3000, () -> { assertNotNull(getSingleValue(element.getCurrentChildren())); }); LaunchConfDashElement childElement = (LaunchConfDashElement) getSingleValue(element.getCurrentChildren()); ILaunchConfigurationWorkingCopy wc = childElement.getActiveConfig().getWorkingCopy(); assertNotNull(BootLaunchConfigurationDelegate.getMainType(wc)); BootLaunchConfigurationDelegate.setEnableLifeCycle(wc, false); wc.doSave(); doStartBootAppWithoutLifeCycleTest(element, RunState.RUNNING); doStartBootAppWithoutLifeCycleTest(element, RunState.DEBUGGING); } @Test public void startOldBootApp() throws Exception { String projectName = "boot12"; createPredefinedMavenProject(projectName); BootProjectDashElement element = getElement(projectName); doStartBootAppWithoutLifeCycleTest(element, RunState.RUNNING); doStartBootAppWithoutLifeCycleTest(element, RunState.DEBUGGING); } private void doStartBootAppWithoutLifeCycleTest(BootProjectDashElement app, RunState runOrDebug) throws Exception { try { waitForState(app, RunState.INACTIVE); app.restart(runOrDebug, ui); waitForState(app, runOrDebug); } finally { ACondition.waitFor("stop hammering", 20000, () -> { app.stopAsync(ui); assertEquals(RunState.INACTIVE, app.getRunState()); }); } } private IProject createPredefinedMavenProject(String projectName) throws Exception { return BootProjectTestHarness.createPredefinedMavenProject(projectName, "org.springframework.ide.eclipse.boot.dash.test"); } /** * Test that element state listener is notified when a project is launched and terminated. */ @Test public void testRunStateChanges() throws Exception { doTestRunStateChanges(RunState.RUNNING); } /** * Test that element state listener is notified when a project is launched in Debug mode and terminated. */ @Test public void testDebugStateChanges() throws Exception { doTestRunStateChanges(RunState.DEBUGGING); } protected void doTestRunStateChanges(RunState runState) throws Exception { String projectName = "testProject"; createBootProject(projectName); waitModelElements(projectName); ElementStateListener listener = mock(ElementStateListener.class); model.addElementStateListener(listener); //System.out.println("Element state listener ADDED"); BootDashElement element = getElement(projectName); element.restart(runState, null); waitForState(element, runState); ElementStateListener oldListener = listener; ACondition.waitFor("listener calls", 1000, () -> verify(oldListener, times(4)).stateChanged(element) ); model.removeElementStateListener(oldListener); //System.out.println("Element state listener REMOVED"); listener = mock(ElementStateListener.class); model.addElementStateListener(listener); element.stopAsync(ui); waitForState(element, RunState.INACTIVE); //4 changes: INACTIVE -> STARTING, STARTING -> RUNNING, livePort(set), actualInstances++ verify(oldListener, times(4)).stateChanged(element); //3 changes: RUNNING -> INACTIVE, liveport(unset), actualInstances-- verify(listener, times(3)).stateChanged(element); } @Test public void projectElementDisposedWhenProjectClosed() throws Exception { String projectName = "testProject"; IProject project = createBootProject(projectName); waitModelElements(projectName); BootProjectDashElement projectElement = getElement(projectName); LiveVariable<Boolean> disposed = new LiveVariable<>(false); projectElement.onDispose((d) -> disposed.setValue(true)); project.close(new NullProgressMonitor()); ACondition.waitFor("Element disposed", 100, () -> { assertTrue(disposed.getValue()); }); } @Test public void testRestartRunningProcessTest() throws Exception { String projectName = "testProject"; createBootProject(projectName); waitModelElements(projectName); final RunState[] RUN_STATES = { RunState.RUNNING, RunState.DEBUGGING }; for (RunState fromState : RUN_STATES) { for (RunState toState : RUN_STATES) { doRestartTest(projectName, fromState, toState); } } } @Test public void testDevtoolsPortRefreshedOnRestart() throws Exception { //Test that the local bootdash element 'liveport' is updated when boot devtools // does an in-place restart of the app, changing the port that it runs on. String projectName = "some-project-with-devtools"; createBootProject(projectName, bootVersionAtLeast("1.3.0"), //1.3.0 required for lifecycle & devtools support withStarters("devtools") ); final BootDashElement project = getElement(projectName); try { waitForState(project, RunState.INACTIVE); System.out.println("Starting "+project); project.restart(RunState.RUNNING, ui); waitForState(project, RunState.STARTING); waitForState(project, RunState.RUNNING); BootDashElement launch = CollectionUtils.getSingle(project.getChildren().getValues()); int defaultPort = 8080; int changedPort = 8765; waitForPort(project, defaultPort); System.out.println("Changing port in application.properties to "+changedPort); IFile props = project.getProject().getFile(new Path("src/main/resources/application.properties")); setContents(props, "server.port="+changedPort); System.out.println("Rebuilding project..."); StsTestUtil.assertNoErrors(project.getProject()); System.out.println("Rebuilding project... DONE"); //builds the project... should trigger devtools to 'refresh'. waitForPort(project, changedPort); waitForPort(launch, changedPort); //Now try that this also works in debug mode... System.out.println("Restart project in DEBUG mode..."); project.restart(RunState.DEBUGGING, ui); waitForState(project, RunState.STARTING); waitForState(project, RunState.DEBUGGING); waitForPort(project, changedPort); waitForPort(launch, changedPort); System.out.println("Changing port in application.properties to "+defaultPort); setContents(props, "server.port="+defaultPort); System.out.println("Rebuilding project..."); StsTestUtil.assertNoErrors(project.getProject()); System.out.println("Rebuilding project... DONE"); //builds the project... should trigger devtools to 'refresh'. waitForPort(project, defaultPort); waitForPort(launch, defaultPort); } finally { System.out.println("Cleanup: stop "+project); project.stopAsync(ui); waitForState(project, RunState.INACTIVE); } } protected void waitForPort(final BootDashElement element, final int expectedPort) throws Exception { new ACondition("Wait for port on "+element.getName()+" to change to "+expectedPort, 5000) { //Devtools should restart really fast @Override public boolean test() throws Exception { assertEquals(ImmutableSet.of(expectedPort), element.getLivePorts()); assertEquals(expectedPort, element.getLivePort()); return true; } }; } @Test public void testStartingStateObservable() throws Exception { //Test that, for boot project that supports it, the 'starting' state // is observable in the model. String projectName = "some-project"; createBootProject(projectName, bootVersionAtLeast("1.3.0") //1.3.0 required for lifecycle support ); BootDashElement element = getElement(projectName); try { waitForState(element, RunState.INACTIVE); element.restart(RunState.RUNNING, ui); waitForState(element, RunState.STARTING); waitForState(element, RunState.RUNNING); element.restart(RunState.DEBUGGING, ui); waitForState(element, RunState.STARTING); waitForState(element, RunState.DEBUGGING); } finally { element.stopAsync(ui); waitForState(element, RunState.INACTIVE); } } private void doRestartTest(String projectName, RunState fromState, RunState toState) throws Exception { BootDashElement element = getElement(projectName); try { element.restart(fromState, ui); waitForState(element, fromState); final ILaunch launch = getActiveLaunch(element); element.restart(toState, ui); //Watch out for race conditions... we can't really reliably observe the // 'terminated' state of the element, as we don't know how long it will // last and the 'restart' operation may happen concurrently with the testing // thread. Therefore we observe the terminated state of the actual launch. // Restarting the project will/should terminate the old launch and then // create a new launch. new ACondition("Wait for launch termination", RUN_STATE_CHANGE_TIMEOUT) { public boolean test() throws Exception { return launch.isTerminated(); } }; waitForState(element, toState); } finally { element.stopAsync(ui); waitForState(element, RunState.INACTIVE); } } @Test public void livePortSummaryAndInstanceCounts() throws Exception { String projectName = "some-project"; createBootProject(projectName, bootVersionAtLeast("1.3.0")); //1.3.0 required for lifecycle support. final BootProjectDashElement project = getElement(projectName); try { assertEquals(RunState.INACTIVE, project.getRunState()); assertTrue(project.getLivePorts().isEmpty()); // live port is 'unknown' if app is not running assertInstances("0/1", project); assertInstancesLabel("", project); //label hidden for ?/1 case IType mainType = MainTypeFinder.guessMainTypes(project.getJavaProject(), new NullProgressMonitor())[0]; final int port1 = portFinder.findUniqueFreePort(); ILaunchConfiguration config1 = BootLaunchConfigurationDelegate.createConf(mainType); setPort(config1, port1); final int port2 = portFinder.findUniqueFreePort(); ILaunchConfiguration config2 = BootLaunchConfigurationDelegate.createConf(mainType); setPort(config2, port2); final BootDashElement el1 = harness.getElementFor(config1); final BootDashElement el2 = harness.getElementFor(config2); assertInstances("0/1", el1); assertInstancesLabel("", el1); // hidden label for ?/1 case assertInstances("0/1", el2); assertInstancesLabel("", el2); // hidden label for ?/1 case el1.restart(RunState.RUNNING, ui); el2.restart(RunState.RUNNING, ui); waitForState(el1, RunState.RUNNING); waitForState(el2, RunState.RUNNING); new ACondition("check port summary", MODEL_UPDATE_TIMEOUT) { public boolean test() throws Exception { assertInstances("2/2", project); assertInstancesLabel("2/2", project); assertInstances("1/1", el1); assertInstancesLabel("", el1); // hidden label for ?/1 case assertInstances("1/1", el2); assertInstancesLabel("", el2); // hidden label for ?/1 case assertEquals(port1, el1.getLivePort()); assertEquals(port2, el2.getLivePort()); assertEquals(ImmutableSet.of(port1, port2), project.getLivePorts()); return true; } }; el1.stopAsync(ui); new ACondition("check port summary", MODEL_UPDATE_TIMEOUT) { public boolean test() throws Exception { assertEquals(ImmutableSet.of(port2), project.getLivePorts()); assertEquals(1, project.getActualInstances()); assertEquals(2, project.getDesiredInstances()); assertInstances("1/2", project); assertInstancesLabel("1/2", project); assertInstances("0/1", el1); assertInstancesLabel("", el1); // hidden label for ?/1 case assertInstances("1/1", el2); assertInstancesLabel("", el2); // hidden label for ?/1 case return true; } }; } finally { project.stopSync(); } } private void assertInstances(String expect, BootDashElement e) { assertEquals(expect, e.getActualInstances()+"/"+e.getDesiredInstances()); } private void assertInstancesLabel(String expect, BootDashElement e) { BootDashLabels labels = new BootDashLabels(null); // we test this without styler, still better than not testing. String actual = labels.getStyledText(e, BootDashColumn.INSTANCES).toString(); assertEquals(expect, actual); } @Test public void livePort() throws Exception { String projectName = "some-project"; createBootProject(projectName, bootVersionAtLeast("1.3.0")); //1.3.0 required for lifecycle support. final BootProjectDashElement element = getElement(projectName); assertEquals(RunState.INACTIVE, element.getRunState()); assertEquals(-1, element.getLivePort()); // live port is 'unknown' if app is not running try { waitForState(element, RunState.INACTIVE); element.restart(RunState.RUNNING, ui); waitForState(element, RunState.STARTING); waitForState(element, RunState.RUNNING); new ACondition(4000) { public boolean test() throws Exception { assertEquals(8080, element.getLivePort()); return true; } }; //Change port in launch conf and restart ILaunchConfiguration conf = element.getActiveConfig(); ILaunchConfigurationWorkingCopy wc = conf.getWorkingCopy(); BootLaunchConfigurationDelegate.setProperties(wc, Collections.singletonList( new PropVal("server.port", "6789", true) )); wc.doSave(); final BootDashElement childElement = getSingleValue(element.getCurrentChildren()); new ACondition(4000) { public boolean test() throws Exception { assertEquals(8080, element.getLivePort()); // port still the same until we restart assertEquals(8080, childElement.getLivePort()); return true; } }; element.restart(RunState.RUNNING, ui); waitForState(element, RunState.STARTING); waitForState(element, RunState.RUNNING); new ACondition(4000) { public boolean test() throws Exception { assertEquals(6789, element.getLivePort()); assertEquals(6789, childElement.getLivePort()); return true; } }; } finally { element.stopAsync(ui); waitForState(element, RunState.INACTIVE); } } @Test public void testRequestMappings() throws Exception { String projectName = "actuated-project"; IProject project = createBootProject(projectName, bootVersionAtLeast("1.3.0"), //required for us to be able to determine the actuator port withStarters("web", "actuator") //required to actually *have* an actuator ); createFile(project, "src/main/java/com/example/HelloController.java", "package com.example;\n" + "\n" + "import org.springframework.web.bind.annotation.RequestMapping;\n" + "import org.springframework.web.bind.annotation.RestController;\n" + "\n" + "@RestController\n" + "public class HelloController {\n" + "\n" + " @RequestMapping(\"/hello\")\n" + " public String hello() {\n" + " return \"Hello, World!\";\n" + " }\n" + "\n" + "}\n" ); StsTestUtil.assertNoErrors(project); final BootDashElement element = getElement(projectName); try { waitForState(element, RunState.INACTIVE); assertNull(element.getLiveRequestMappings()); // unknown since can only be determined when app is running element.restart(RunState.RUNNING, ui); waitForState(element, RunState.RUNNING); new ACondition("Wait for request mappings", MODEL_UPDATE_TIMEOUT) { public boolean test() throws Exception { List<RequestMapping> mappings = element.getLiveRequestMappings(); assertNotNull(mappings); //Why is the test sometimes failing here? assertTrue(!mappings.isEmpty()); //Even though this is an 'empty' app should have some mappings, // for example an 'error' page. return true; } }; List<RequestMapping> mappings = element.getLiveRequestMappings(); System.out.println(">>> Found RequestMappings"); for (RequestMapping m : mappings) { System.out.println(m.getPath()); assertNotNull(m.getPath()); } System.out.println("<<< Found RequestMappings"); RequestMapping rm; //Case 2 examples (path extracted from 'pseudo' json in the key) rm = assertRequestMappingWithPath(mappings, "/hello"); //We defined it so should be there assertEquals("com.example.HelloController", rm.getFullyQualifiedClassName()); assertEquals("hello", rm.getMethodName()); assertEquals("com.example.HelloController", rm.getType().getFullyQualifiedName()); IMethod method = rm.getMethod(); assertEquals(rm.getType(), method.getDeclaringType()); assertEquals("hello", method.getElementName()); assertTrue(rm.isUserDefined()); rm = assertRequestMappingWithPath(mappings, "/error"); //Even empty apps should have a 'error' mapping assertFalse(rm.isUserDefined()); rm = assertRequestMappingWithPath(mappings, "/mappings"); //Since we are using this, it should be there. assertNotNull(rm.getMethod()); assertNotNull(rm.getType()); assertFalse(rm.isUserDefined()); rm = assertRequestMappingWithPath(mappings, "/mappings.json"); //Since we are using this, it should be there. assertNotNull(rm.getMethod()); assertNotNull(rm.getType()); assertFalse(rm.isUserDefined()); //Case 1 example (path represented directly in the json key). rm = assertRequestMappingWithPath(mappings, "/**/favicon.ico"); assertFalse(rm.isUserDefined()); } finally { element.stopAsync(ui); waitForState(element, RunState.INACTIVE); } } @Test public void testRequestMappingsOnlyJmxEnabled() throws Exception { do_testRequestMappingsOnlyJmxEnabled(RunState.RUNNING); } @Test public void testRequestMappingsOnlyJmxEnabled_DEBUG() throws Exception { do_testRequestMappingsOnlyJmxEnabled(RunState.DEBUGGING); } private void do_testRequestMappingsOnlyJmxEnabled(RunState runMode) throws Exception, CoreException { String projectName = "actuated-project"; IProject project = createBootProject(projectName, bootVersionAtLeast("1.3.0"), //required for us to be able to determine the actuator port withStarters("web", "actuator") //required to actually *have* an actuator ); createFile(project, "src/main/java/com/example/HelloController.java", "package com.example;\n" + "\n" + "import org.springframework.web.bind.annotation.RequestMapping;\n" + "import org.springframework.web.bind.annotation.RestController;\n" + "\n" + "@RestController\n" + "public class HelloController {\n" + "\n" + " @RequestMapping(\"/hello\")\n" + " public String hello() {\n" + " return \"Hello, World!\";\n" + " }\n" + "\n" + "}\n" ); StsTestUtil.assertNoErrors(project); final BootProjectDashElement element = getElement(projectName); ILaunchConfigurationWorkingCopy wc = element.createLaunchConfigForEditing().getWorkingCopy(); BootLaunchConfigurationDelegate.setEnableLifeCycle(wc, false); BootLaunchConfigurationDelegate.setEnableLiveBeanSupport(wc,false); wc.doSave(); try { waitForState(element, RunState.INACTIVE); assertNull(element.getLiveRequestMappings()); // unknown since can only be determined when app is running element.restart(runMode, ui); waitForState(element, runMode); ACondition.waitFor("Wait for request mappings", MODEL_UPDATE_TIMEOUT, () -> { List<RequestMapping> mappings = element.getLiveRequestMappings(); assertNotNull(mappings); assertTrue(!mappings.isEmpty()); //Even though this is an 'empty' app should have some mappings, // for example an 'error' page. }); List<RequestMapping> mappings = element.getLiveRequestMappings(); System.out.println(">>> Found RequestMappings"); for (RequestMapping m : mappings) { System.out.println(m.getPath()); assertNotNull(m.getPath()); } System.out.println("<<< Found RequestMappings"); RequestMapping rm = assertRequestMappingWithPath(mappings, "/hello"); //We defined it so should be there assertEquals("com.example.HelloController", rm.getFullyQualifiedClassName()); assertEquals("hello", rm.getMethodName()); assertEquals("com.example.HelloController", rm.getType().getFullyQualifiedName()); IMethod method = rm.getMethod(); assertEquals(rm.getType(), method.getDeclaringType()); assertEquals("hello", method.getElementName()); assertTrue(rm.isUserDefined()); rm = assertRequestMappingWithPath(mappings, "/error"); //Even empty apps should have a 'error' mapping assertFalse(rm.isUserDefined()); } finally { element.stopAsync(ui); waitForState(element, RunState.INACTIVE); } } @Test public void testDefaultRequestMapping() throws Exception { String projectName = "sdfsd-project"; createBootProject(projectName); BootDashElement element = getElement(projectName); assertNull(element.getDefaultRequestMappingPath()); element.setDefaultRequestMappingPath("something"); assertProjectProperty(element.getProject(), "default.request-mapping.path", "something"); assertEquals("something", element.getDefaultRequestMappingPath()); } @Test public void testDevtoolsTextDecorationOnLocalElements() throws Exception { final String projectName = "project-hahaha"; IProject project = createBootProject(projectName, withStarters("web", "actuator", "devtools")); final BootDashElement element = getElement(projectName); assertTrue(element.hasDevtools()); assertLabelContains("[devtools]", element); //Also check that we do not add 'devtools' label to launch configs. ILaunchConfiguration conf = BootLaunchConfigurationDelegate.createConf(project); String confName = conf.getName(); assertEquals(confName, getLabel(harness.getElementFor(conf))); //Try and see that if we remove the devtools dependency from the project then the label updates. StsTestUtil.setAutoBuilding(true); // so that autobuild causes classpath update as would // happen in a 'real' workspace when pom is changed. IMavenCoordinates devtools = removeDevtools(project); new ACondition("Wait for devtools to disapear", MAVEN_BUILD_TIMEOUT) { public boolean test() throws Exception { assertFalse(element.hasDevtools()); assertEquals(projectName, getLabel(element)); return true; } }; springBootCore.project(project).addMavenDependency(devtools, true); new ACondition("Wait for devtools to re-apear", MAVEN_BUILD_TIMEOUT) { public boolean test() throws Exception { assertFalse(element.hasDevtools()); assertEquals(projectName, getLabel(element)); return true; } }; } /************************************************************************************** * TAGS Tests START *************************************************************************************/ private void testSettingTags(String[] tagsToSet, String[] expectedTags) throws Exception { String projectName = "alex-project"; createBootProject(projectName); BootDashElement element = getElement(projectName); IProject project = element.getProject(); if (tagsToSet==null || tagsToSet.length==0) { element.setTags(new LinkedHashSet<>(Arrays.asList("foo", "bar"))); assertFalse(element.getTags().isEmpty()); } else { assertArrayEquals(new String[]{}, element.getTags().toArray(new String[0])); } element.setTags(linkedHashSet(tagsToSet)); waitForJobsToComplete(); assertArrayEquals(expectedTags, element.getTags().toArray(new String[0])); // Reopen the project to load tags from the resource project.close(null); project.open(null); element = getElement(projectName); assertArrayEquals(expectedTags, element.getTags().toArray(new String[0])); } private LinkedHashSet<String> linkedHashSet(String[] tagsToSet) { if (tagsToSet!=null) { return new LinkedHashSet<>(Arrays.asList(tagsToSet)); } return null; } @Test public void setUniqueTagsForProject() throws Exception { testSettingTags(new String[] {"xd", "spring"}, new String[] {"xd", "spring"}); } @Test public void setDuplicateTagsForProject() throws Exception { testSettingTags(new String[] {"xd", "spring", "xd", "spring", "spring"}, new String[] {"xd", "spring"}); } @Test public void setTagsWithWhiteSpaceCharsForProject() throws Exception { testSettingTags(new String[] {"#xd", "\tspring", "xd ko ko", "spring!!-@", "@@@ - spring"}, new String[] {"#xd", "\tspring", "xd ko ko", "spring!!-@", "@@@ - spring"}); } @Test public void setNoTags() throws Exception { testSettingTags(new String[0], new String[0]); } @Test public void setNullTags() throws Exception { testSettingTags(null, new String[0]); } private class BdeInfo { String name; String[] tags; String workingSet; BdeInfo(String name, String[] tags, String workingSet) { this.name = name; this.tags = tags; this.workingSet = workingSet; } } /************************************************************************************** * TAGS Tests END *************************************************************************************/ /************************************************************************************** * BDEs Filtering Tests START *************************************************************************************/ private void testBdeFiltering(BdeInfo[] bdeInfo, String filterText, String[] expectedBdes) throws Exception { Map<String, List<IProject>> wsMap = new HashMap<>(); List<BootDashElement> bdes = new ArrayList<>(bdeInfo.length); for (BdeInfo info : bdeInfo) { createBootProject(info.name); BootDashElement element = getElement(info.name); IProject project = element.getProject(); if (info.tags != null && info.tags.length > 0) { element.setTags(new LinkedHashSet<>(Arrays.asList(info.tags))); } if (info.workingSet != null && !info.workingSet.isEmpty() && project != null) { List<IProject> projects = wsMap.get(info.workingSet); if (projects == null) { projects = new ArrayList<>(); wsMap.put(info.workingSet, projects); } projects.add(project); } bdes.add(element); } IWorkingSetManager wsManager = PlatformUI.getWorkbench().getWorkingSetManager(); for (Map.Entry<String, List<IProject>> entry : wsMap.entrySet()) { IWorkingSet ws = wsManager.getWorkingSet(entry.getKey()); if (ws == null) { ws = wsManager.createWorkingSet(entry.getKey(), entry.getValue().toArray(new IProject[entry.getValue().size()])); wsManager.addWorkingSet(ws); } else { ws.setElements(entry.getValue().toArray(new IProject[entry.getValue().size()])); } } waitForJobsToComplete(); BootDashElementsFilterBoxModel filterModel = new BootDashElementsFilterBoxModel(); filterModel.getText().setValue(filterText); Filter<BootDashElement> filter = filterModel.getFilter().getValue(); List<String> result = new ArrayList<>(); for (BootDashElement bde : bdes) { if (filter.accept(bde) && bde.getProject() != null) { result.add(bde.getProject().getName()); } } String[] actualBdes = result.toArray(new String[result.size()]); assertArrayEquals(expectedBdes, actualBdes); } @Test public void testNoWorkingSetMatch_1() throws Exception { testBdeFiltering(new BdeInfo[]{new BdeInfo("a", null, null), new BdeInfo("b", null, null)}, "x", new String[0]); } @Test public void testNoWorkingSetMatch_2() throws Exception { testBdeFiltering(new BdeInfo[]{new BdeInfo("a", null, "xxx"), new BdeInfo("b", null, "xxx")}, "x,", new String[0]); } @Test public void testNoWorkingSetMatch_3() throws Exception { testBdeFiltering( new BdeInfo[]{ new BdeInfo("b", null, "xxx"), new BdeInfo("c", null, "xxx") }, "xxx, a", new String[0] ); } @Test public void testNoWorkingSetMatch_4() throws Exception { testBdeFiltering( new BdeInfo[]{ new BdeInfo("a", null, "xxx"), new BdeInfo("b", null, "xxx") }, "c, x", new String[0] ); } @Test public void testWorkingSetMatch_1() throws Exception { testBdeFiltering(new BdeInfo[]{new BdeInfo("a", null, "x"), new BdeInfo("b", null, null), new BdeInfo("c", null, "x")}, "x", new String[]{"a", "c"}); } @Test public void testWorkingSetMatch_2() throws Exception { testBdeFiltering(new BdeInfo[]{new BdeInfo("a", null, "xxx"), new BdeInfo("b", null, null), new BdeInfo("c", null, "xxxx")}, "xxx,", new String[]{"a"}); } @Test public void testWorkingSetMatch_3() throws Exception { testBdeFiltering( new BdeInfo[]{ new BdeInfo("a", new String[]{"aaa", "bbb"}, "xxx"), new BdeInfo("b", new String[]{"a", "c"}, "xxx") }, "xxx, a", new String[]{"a", "b"}); } @Test public void testWorkingSetMatch_4() throws Exception { testBdeFiltering( new BdeInfo[]{ new BdeInfo("z", new String[]{"aaa", "bbb"}, "xxx" ), new BdeInfo("b", new String[]{"a", "c"}, "xxx") }, "a, x", new String[]{"b"} ); } /************************************************************************************** * BDEs Filtering Tests END *************************************************************************************/ ///////////////// harness code //////////////////////// private void assertProjectProperty(IProject project, String prop, String value) { assertEquals(value, context.getProjectProperties().get(project, prop)); } @Rule public TestRule listenerLeakDetector = new ListenerLeakDetector(); @Rule public LaunchCleanups launchCleanups = new LaunchCleanups(); private UserInteractions ui; private BootDashViewModelHarness harness; @Before public void setup() throws Exception { //As part of its normal operation, devtools will throw some uncaucht exceptions. // We don't want our tests to be disrupted when running the process in debug mode... so disable // suspending on such exceptions: suspendOnUncaughtException(false); StsTestUtil.deleteAllProjects(); this.context = new TestBootDashModelContext( ResourcesPlugin.getWorkspace(), DebugPlugin.getDefault().getLaunchManager() ); this.harness = new BootDashViewModelHarness(context, RunTargetTypes.LOCAL); this.model = harness.getRunTargetModel(RunTargetTypes.LOCAL); this.projects = new BootProjectTestHarness(context.getWorkspace()); StsTestUtil.setAutoBuilding(false); this.ui = mock(UserInteractions.class); } public static void suspendOnUncaughtException(boolean enable) { String suspendOption = "org.eclipse.jdt.debug.ui.javaDebug.SuspendOnUncaughtExceptions"; IEclipsePreferences debugPrefs = InstanceScope.INSTANCE.getNode("org.eclipse.jdt.debug.ui"); debugPrefs.putBoolean(suspendOption, enable); } @After public void tearDown() throws Exception { /* * Remove any working sets created by the tests (BDEs filtering tests create working sets) */ IWorkingSetManager wsManager = PlatformUI.getWorkbench().getWorkingSetManager(); for (IWorkingSet ws : wsManager.getAllWorkingSets()) { if (!ws.isAggregateWorkingSet()) { wsManager.removeWorkingSet(ws); } } this.harness.dispose(); } /** * Returns the only active (i.e. not terminated launch for a project). If there is more * than one active launch, or no active launch this returns null. */ public ILaunch getActiveLaunch(BootDashElement element) { ImmutableSet<ILaunch> ls = getBootLaunches(element); ILaunch activeLaunch = null; for (ILaunch l : ls) { if (!l.isTerminated()) { if (activeLaunch==null) { activeLaunch = l; } else { //More than one active launch return null; } } } return activeLaunch; } private ImmutableSet<ILaunch> getBootLaunches(BootDashElement element) { if (element instanceof BootProjectDashElement) { BootProjectDashElement project = (BootProjectDashElement) element; return project.getLaunches(); } return ImmutableSet.of(); } private static void waitForState(final BootDashElement element, final RunState state) throws Exception { new ACondition("Wait for state "+state, RUN_STATE_CHANGE_TIMEOUT) { @Override public boolean test() throws Exception { assertEquals(state, element.getRunState()); return true; } }; } private BootProjectDashElement getElement(String name) { for (BootDashElement el : model.getElements().getValues()) { if (name.equals(el.getName())) { return (BootProjectDashElement) el; } } return null; } private void assertModelElements(String... expectedElementNames) { Set<BootDashElement> elements = model.getElements().getValue(); Set<String> names = new HashSet<>(); for (BootDashElement e : elements) { names.add(e.getName()); } assertElements(names, expectedElementNames); } public void waitModelElements(final String... expectedElementNames) throws Exception { new ACondition("Model update", MODEL_UPDATE_TIMEOUT) { public boolean test() throws Exception { assertModelElements(expectedElementNames); return true; } }; } public static void waitForJobsToComplete() throws Exception { new ACondition("Wait for Jobs", 3 * 60 * 1000) { @Override public boolean test() throws Exception { assertJobManagerIdle(); return true; } }; } private void setPort(ILaunchConfiguration conf, int port) throws Exception { ILaunchConfigurationWorkingCopy wc = conf.getWorkingCopy(); assertTrue("Only supported on 'empty' configs", BootLaunchConfigurationDelegate.getProperties(wc).isEmpty()); List<PropVal> props = Arrays.asList(new PropVal("server.port", ""+port, true)); BootLaunchConfigurationDelegate.setProperties(wc, props); wc.doSave(); } private IMavenCoordinates removeDevtools(IProject project) throws Exception { ISpringBootProject bootProject = springBootCore.project(project); MavenId devtoolsId = new MavenId( EnableDisableBootDevtools.SPRING_BOOT_DEVTOOLS_GID, EnableDisableBootDevtools.SPRING_BOOT_DEVTOOLS_AID ); IMavenCoordinates devtools = null; for (IMavenCoordinates dep : bootProject.getDependencies()) { if (new MavenId(dep.getGroupId(), dep.getArtifactId()).equals(devtoolsId)) { devtools = dep; } } assertNotNull("Devtools dependency not found, so can't remove it", devtools); bootProject.removeMavenDependency(devtoolsId); return devtools; } }