/*******************************************************************************
* 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.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import static org.springsource.ide.eclipse.commons.livexp.ui.ProjectLocationSection.getDefaultProjectLocation;
import java.io.File;
import java.util.Arrays;
import java.util.Comparator;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.m2e.core.ui.internal.UpdateMavenProjectJob;
import org.osgi.framework.Version;
import org.osgi.framework.VersionRange;
import org.springframework.ide.eclipse.boot.core.ISpringBootProject;
import org.springframework.ide.eclipse.boot.core.SpringBootCore;
import org.springframework.ide.eclipse.boot.test.util.CopyFromFolder;
import org.springframework.ide.eclipse.boot.util.RetryUtil;
import org.springframework.ide.eclipse.boot.wizard.NewSpringBootWizardModel;
import org.springframework.ide.eclipse.boot.wizard.RadioGroup;
import org.springframework.ide.eclipse.boot.wizard.RadioInfo;
import org.springframework.ide.eclipse.boot.wizard.content.BuildType;
import org.springframework.ide.eclipse.boot.wizard.content.CodeSet;
import org.springframework.ide.eclipse.boot.wizard.importing.ImportConfiguration;
import org.springframework.ide.eclipse.boot.wizard.importing.ImportStrategies;
import org.springframework.ide.eclipse.boot.wizard.importing.ImportStrategy;
import org.springsource.ide.eclipse.commons.frameworks.test.util.ACondition;
import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil;
import org.springsource.ide.eclipse.commons.tests.util.StsTestUtil;
/**
* @author Kris De Volder
*/
public class BootProjectTestHarness {
private static final boolean DEBUG = true;
private static void debug(String string) {
if (DEBUG) {
System.out.println(string);
}
}
public static final long BOOT_PROJECT_CREATION_TIMEOUT = 5*60*1000; // long, may download maven dependencies
private IWorkspace workspace;
public BootProjectTestHarness(IWorkspace workspace) {
this.workspace = workspace;
}
@FunctionalInterface
public interface WizardConfigurer {
void apply(NewSpringBootWizardModel wizard);
WizardConfigurer NULL = new WizardConfigurer(){
public void apply(NewSpringBootWizardModel wizard) {/*do nothing*/}
};
}
public static WizardConfigurer withImportStrategy(final String id) {
final ImportStrategy is = ImportStrategies.withId(id);
Assert.isNotNull(is);
return new WizardConfigurer() {
public void apply(NewSpringBootWizardModel wizard) {
wizard.setImportStrategy(is);
}
};
}
public static WizardConfigurer withPackaging(final String packagingTypeName) {
return (wizard) -> {
RadioGroup packagingRadio = wizard.getRadioGroups().getGroup("packaging");
assertNotNull("Couldn't find 'packaging' radiogroup in the wizard model", packagingRadio);
for (RadioInfo r : packagingRadio.getRadios()) {
if (r.getValue().equals(packagingTypeName)) {
packagingRadio.getSelection().selection.setValue(r);
return;
}
}
fail("Couldn't find packaging type '"+packagingTypeName+"' in the wizard model");
};
}
public static WizardConfigurer withStarters(final String... ids) {
if (ids.length>0) {
return new WizardConfigurer() {
public void apply(NewSpringBootWizardModel wizard) {
for (String id : ids) {
wizard.addDependency(id);
}
}
};
}
return WizardConfigurer.NULL;
}
public static WizardConfigurer setPackage(final String pkgName) {
return new WizardConfigurer() {
public void apply(NewSpringBootWizardModel wizard) {
wizard.getStringInput("packageName").setValue(pkgName);
}
};
}
/**
* @return A wizard configurer that ensures the selected 'boot version' is exactly
* a given version of boot.
*/
public static WizardConfigurer bootVersion(final String wantedVersion) throws Exception {
return new WizardConfigurer() {
public void apply(NewSpringBootWizardModel wizard) {
RadioGroup bootVersionRadio = wizard.getBootVersion();
for (RadioInfo option : bootVersionRadio.getRadios()) {
if (option.getValue().equals(wantedVersion)) {
bootVersionRadio.setValue(option);
return;
}
}
fail("The wanted bootVersion '"+wantedVersion+"'is not found in the wizard");
}
};
}
/**
* @return A wizard configurer that ensures the selected 'boot version' is at least
* a given version of boot.
*/
public static WizardConfigurer bootVersionAtLeast(final String wantedVersion) throws Exception {
final VersionRange WANTED_RANGE = new VersionRange(wantedVersion);
return new WizardConfigurer() {
public void apply(NewSpringBootWizardModel wizard) {
RadioGroup bootVersionRadio = wizard.getBootVersion();
RadioInfo selected = bootVersionRadio.getValue();
Version selectedVersion = getVersion(selected);
if (WANTED_RANGE.includes(selectedVersion)) {
//existing selection is fine
} else {
//try to select the latest available version and verify it meets the requirement
bootVersionRadio.setValue(selected = getLatestVersion(bootVersionRadio));
selectedVersion = getVersion(selected);
Assert.isTrue(WANTED_RANGE.includes(selectedVersion));
}
}
private RadioInfo getLatestVersion(RadioGroup bootVersionRadio) {
RadioInfo[] infos = bootVersionRadio.getRadios();
Arrays.sort(infos, new Comparator<RadioInfo>() {
public int compare(RadioInfo o1, RadioInfo o2) {
Version v1 = getVersion(o1);
Version v2 = getVersion(o2);
return v2.compareTo(v1);
}
});
return infos[0];
}
private Version getVersion(RadioInfo info) {
String versionString = info.getValue();
Version v = new Version(versionString);
if ("BUILD-SNAPSHOT".equals(v.getQualifier())) {
// Caveat "M1" will be treated as 'later' than "BUILD-SNAPSHOT" so that is wrong.
return new Version(v.getMajor(), v.getMinor(), v.getMicro(), "SNAPSHOT"); //Comes after "MX" but before "RELEASE"
}
return v;
}
};
}
public IProject createBootWebProject(final String projectName, final WizardConfigurer... extraConfs) throws Exception {
return createBootProject(projectName, merge(extraConfs, withStarters("web")));
}
private WizardConfigurer[] merge(WizardConfigurer[] confs, WizardConfigurer... moreConfs) {
WizardConfigurer[] merged = new WizardConfigurer[confs.length + moreConfs.length];
System.arraycopy(confs, 0, merged, 0, confs.length);
System.arraycopy(moreConfs, 0, merged, confs.length, moreConfs.length);
return merged;
}
public IProject createBootProject(final String projectName, final WizardConfigurer... extraConfs) throws Exception {
RetryUtil.retryWhen("createBootProject("+projectName+")", 3, RetryUtil.errorWithMsg("Read timed out"), () -> {
final Job job = new Job("Create boot project '"+projectName+"'") {
protected IStatus run(IProgressMonitor monitor) {
try {
//No point doing a retry if we will just fail because project already exists!
IProject p = getProject(projectName);
if (p.exists()) {
p.delete(true, true, new NullProgressMonitor());
}
NewSpringBootWizardModel wizard = new NewSpringBootWizardModel(new MockPrefsStore());
wizard.allowUIThread(true);
wizard.getProjectName().setValue(projectName);
wizard.getArtifactId().setValue(projectName);
//Note: unlike most of the rest of the wizard's behavior, the 'use default location'
// checkbox and its effect is not part of the model but part of the GUI code (this is
// wrong, really, but that's how it is, so we have to explictly set the project
// location in the model.
wizard.getLocation().setValue(getDefaultProjectLocation(projectName));
for (WizardConfigurer extraConf : extraConfs) {
extraConf.apply(wizard);
}
wizard.performFinish(new NullProgressMonitor()/*new SysOutProgressMonitor()*/);
return Status.OK_STATUS;
} catch (Throwable e) {
return ExceptionUtil.status(e);
}
}
};
//job.setRule(workspace.getRuleFactory().buildRule());
job.schedule();
waitForImportJob(getProject(projectName), job);
});
return getProject(projectName);
}
public static void waitForImportJob(final IProject project, final Job job) throws Exception {
new ACondition("Wait for import of "+project.getName(), BOOT_PROJECT_CREATION_TIMEOUT) {
@Override
public boolean test() throws Exception {
assertOk(job.getResult());
if (project.hasNature("org.eclipse.m2e.core.maven2Nature")) {
updateMavenProjectDependencies(project);
}
StsTestUtil.assertNoErrors(project);
return true;
}
};
}
public IProject getProject(String projectName) {
return workspace.getRoot().getProject(projectName);
}
public static void updateMavenProjectDependencies(IProject project) throws InterruptedException {
debug("updateMavenProjectDependencies("+project.getName()+") ...");
boolean refreshFromLocal = true;
boolean cleanProjects = true;
boolean updateConfig = true;
IProject[] projects = {project};
boolean offline = false;
boolean forceUpdateDeps = true;
UpdateMavenProjectJob job = new UpdateMavenProjectJob(projects, offline, forceUpdateDeps,
updateConfig, cleanProjects, refreshFromLocal);
job.schedule();
job.join();
debug("updateMavenProjectDependencies("+project.getName()+") DONE");
}
public static IProject createPredefinedMavenProject(final String projectName, final String bundleName)
throws CoreException, Exception {
IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(projectName);
if (project.exists()) {
return project;
}
StsTestUtil.setAutoBuilding(false);
ImportConfiguration importConf = new ImportConfiguration() {
@Override
public String getProjectName() {
return projectName;
}
@Override
public String getLocation() {
return ResourcesPlugin.getWorkspace().getRoot().getLocation().append(projectName).toString();
}
@Override
public CodeSet getCodeSet() {
File sourceWorkspace = new File(StsTestUtil.getSourceWorkspacePath(bundleName));
File sourceProject = new File(sourceWorkspace, projectName);
return new CopyFromFolder(projectName, sourceProject);
}
};
final IRunnableWithProgress importOp = BuildType.MAVEN.getDefaultStrategy().createOperation(importConf);
Job runner = new Job("Import "+projectName) {
@Override
protected IStatus run(IProgressMonitor monitor) {
try {
importOp.run(monitor);
} catch (Throwable e) {
return ExceptionUtil.status(e);
}
return Status.OK_STATUS;
}
};
runner.setRule(ResourcesPlugin.getWorkspace().getRuleFactory().buildRule());
runner.schedule();
waitForImportJob(project, runner);
// BootProjectTestHarness.assertNoErrors(project);
return project;
}
public static void buildMavenProject(IProject p) throws Exception {
ISpringBootProject bp = SpringBootCore.create(p);
updateMavenProjectDependencies(bp.getProject());
bp.getProject().build(IncrementalProjectBuilder.FULL_BUILD, new NullProgressMonitor());
}
public static void assertOk(IStatus result) throws Exception {
if (result==null || !result.isOK()) {
throw ExceptionUtil.coreException(result);
}
}
/**
* Create the most basic project possible. It has no natures, no builders, not nothing.
* This project is suitable as a test fixture for a test that only needs a project to
* exist and nothing more.
*/
public IProject createProject(String projectName) throws Exception {
IProject project = workspace.getRoot().getProject(projectName);
project.create(new NullProgressMonitor());
project.open(new NullProgressMonitor());
return project;
}
public IProject rename(IProject project, String newName) throws Exception {
IProjectDescription description = project.getDescription();
description.setName(newName);
project.move(description, true, new NullProgressMonitor());
return workspace.getRoot().getProject(newName);
}
}