package hudson.plugins.svnmerge;
import hudson.FilePath;
import hudson.Launcher;
import hudson.model.AbstractBuild;
import hudson.model.BuildListener;
import hudson.model.FreeStyleBuild;
import hudson.model.FreeStyleProject;
import hudson.model.Result;
import hudson.scm.SubversionSCM;
import hudson.util.IOException2;
import org.jvnet.hudson.test.HudsonHomeLoader.CopyExisting;
import org.jvnet.hudson.test.HudsonTestCase;
import org.jvnet.hudson.test.TestBuilder;
import org.tmatesoft.svn.core.SVNCommitInfo;
import static org.tmatesoft.svn.core.SVNDepth.INFINITY;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNProperties;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.io.SVNRepositoryFactory;
import org.tmatesoft.svn.core.wc.SVNClientManager;
import static org.tmatesoft.svn.core.wc.SVNRevision.HEAD;
import org.apache.commons.io.IOUtils;
import java.io.File;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URI;
import java.util.concurrent.ExecutionException;
import java.awt.*;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.html.HtmlForm;
/**
* @author Kohsuke Kawaguchi
*/
public class MergeTest extends HudsonTestCase {
private URL repo;
private FreeStyleProject trunk;
private FreeStyleProject p;
private FeatureBranchProperty upp;
@Override
protected void setUp() throws Exception {
super.setUp();
repo = loadSvn();
// create the trunk project
trunk = createFreeStyleProject("trunk");
trunk.setScm(new SubversionSCM("file://"+new URL(repo,"trunk").getPath(),"trunk"));
trunk.addProperty(new IntegratableProject());
// create a project
p = createFreeStyleProject("b1");
p.setScm(new SubversionSCM("file://"+new URL(repo,"branches/b1").getPath(),"b1"));
upp = new FeatureBranchProperty(trunk.getName());
p.addProperty(upp);
}
/**
* Create a new feature branch
*/
public void testCreateBranch() throws Exception {
createFeatureBranch(new WebClient(), "b2");
// make sure it's created with the right set of linkage to the trunk job
FreeStyleProject fsp = hudson.getItemByFullName("trunk-b2",FreeStyleProject.class);
assertNotNull(fsp);
assertNull(fsp.getProperty(IntegratableProject.class));
FeatureBranchProperty ujp = fsp.getProperty(FeatureBranchProperty.class);
assertNotNull(ujp);
assertSame(ujp.getUpstreamProject(),trunk);
assertTrue(((SubversionSCM)fsp.getScm()).getLocations()[0].getURL().contains("/branches/b2"));
// see if the rename works
trunk.renameTo("somethingElse");
assertSame(ujp.getUpstreamProject(),trunk);
assertEquals(trunk.getName(),ujp.getUpstream());
// try recreating the branch job and make sure Hudson detects an error
fsp.delete();
WebClient wc = new WebClient();
wc.setThrowExceptionOnFailingStatusCode(false);
HtmlPage p = createFeatureBranch(wc, "b2");
assertEquals("Duplicate branch should be detected",400,p.getWebResponse().getStatusCode());
submit(p.getFormByName("new"));
}
private HtmlPage createFeatureBranch(WebClient webClient, String name) throws Exception {
HtmlPage p = webClient.getPage(trunk, "featureBranches");
HtmlForm f = p.getFormByName("new");
f.getInputByName("name").setValueAttribute(name);
return submit(f);
}
/**
* Very basic test that pushes changes to the upstream.
*/
public void testUpstreamMerge() throws Exception {
p.addPublisher(new IntegrationPublisher());
p.getBuildersList().add(nonCollidingChange);
assertBuildStatusSuccess(build());
// we make a commit in the builder, which won't be picked up by the integration
// (since it's keyed to the state of the repository as of a check out.)
// so this one pushes the non-colliding change to the trunk.
p.getBuildersList().clear();
assertBuildStatusSuccess(build());
assertTrue(trunkHasE());
}
/**
* Verify that there's trunk/e, which is created by {@link #nonCollidingChange}.
*/
private boolean trunkHasE() throws SVNException {
// make sure the merge went in by checking if /trunk/e exists.
SVNRepository rep = SVNRepositoryFactory.create(upp.getUpstreamURL());
long latest = rep.getLatestRevision();
try {
return latest==rep.getFile("/trunk/e", latest,new SVNProperties(), null);
} catch (SVNException e) {
return false;
}
}
/**
* Now what if a merge fails with a conflict?
*/
public void testUpstreamConflict() throws Exception {
p.addPublisher(new IntegrationPublisher());
p.getBuildersList().add(collidingChange);
// this should succeed. see testUpstreamMerge for why.
assertBuildStatusSuccess(build());
p.getBuildersList().clear();
// merge should have failed, because of a conflict
FreeStyleBuild build = build();
assertBuildStatus(Result.FAILURE, build);
assertLogContains("Found conflict with the upstream",build);
// workspace should have our d.
assertEquals(MAGIC_CONTENT,IOUtils.toString(p.getModuleRoot().child("d").read()));
}
/**
* Tests manual integration.
*/
public void testManualIntegration() throws Exception {
// make a change in the branch, but don't auto-commit the change
p.getBuildersList().add(nonCollidingChange);
FreeStyleBuild b = assertBuildStatusSuccess(build());
integrateManually(b);
// this should merge the branch as it was checked out, so it should succeed
// but E won't be integrated yet.
assertFalse(trunkHasE());
p.getBuildersList().clear();
b = assertBuildStatusSuccess(build());
integrateManually(b);
assertTrue(trunkHasE());
}
private String integrateManually(FreeStyleBuild b) throws InterruptedException, ExecutionException, IOException {
System.out.println("-- Now Merging manually");
IntegrateAction ma = b.getAction(IntegrateAction.class);
// XXX this can block indefinitely!
IntegrateAction.WorkerThread thread = ma.integrateAsync().get();
String msg = IOUtils.toString(thread.readAll());
System.out.println(msg);
return msg;
}
/**
* Manual integration should also fail if there's a collision
*/
public void testManualIntegrationCollision() throws Exception {
// build should succeed because we aren't auto-committing.
p.getBuildersList().add(collidingChange);
FreeStyleBuild b = assertBuildStatusSuccess(build());
String msg = integrateManually(b);
assertFalse(msg.contains("Conflict found"));
p.getBuildersList().clear();
b = assertBuildStatusSuccess(build());
msg = integrateManually(b);
assertTrue(msg.contains("Conflict found"));
}
public void _testInteractiveDebug() throws Exception {
p.getBuildersList().add(collidingChange);
FreeStyleBuild b = assertBuildStatusSuccess(build());
Desktop.getDesktop().browse(URI.create(new WebClient().getContextPath()));
new BufferedReader(new InputStreamReader(System.in)).readLine();
}
/**
* Add a file and then commit the directory.
*/
private void commitAndUpdate(BuildListener listener, FilePath dir, FilePath newFile) throws IOException2 {
try {
SVNClientManager cm = SubversionSCM.createSvnClientManager();
cm.getWCClient().doAdd(toFile(newFile),false,false,false, INFINITY,false,false);
SVNCommitInfo ci = cm.getCommitClient().doCommit(new File[]{toFile(dir)}, false, "a change in a branch", null, null, false, false, INFINITY);
listener.getLogger().println("Committed "+newFile+" at "+ci.getNewRevision());
cm.getUpdateClient().doUpdate(new File(dir.getRemote()), HEAD, INFINITY, false, true);
} catch (SVNException x) {
throw new IOException2("failed to commit",x);
}
}
private File toFile(FilePath fp) {
return new File(fp.getRemote());
}
private FilePath touch(FilePath mr, String name) throws IOException, InterruptedException {
FilePath d = mr.child(name);
d.write(MAGIC_CONTENT,"UTF-8");
return d;
}
/**
* Lets the build run and merge the change.
*/
private FreeStyleBuild build() throws Exception {
FreeStyleBuild b = p.scheduleBuild2(0).get();
System.out.println(b.getLog());
return b;
}
private URL loadSvn() throws Exception {
return new CopyExisting(getClass().getResource("repo.zip")).allocate().toURI().toURL();
}
private static final String MAGIC_CONTENT = "created in branch";
/**
* Builder that makes a non-colliding change in the branch build.
*/
private final TestBuilder nonCollidingChange = new TestBuilder() {
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
// make a change in the branch and commit it.
// then bring the workspace to that revision
FilePath mr = build.getProject().getModuleRoot();
commitAndUpdate(listener, mr,touch(mr, "e"));
return true;
}
};
/**
* Builder that makes a colliding change in the branch build.
*/
private TestBuilder collidingChange = new TestBuilder() {
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
// create d, which is also added in the trunk after we branched
FilePath mr = build.getProject().getModuleRoot();
commitAndUpdate(listener,mr,touch(mr, "d"));
return true;
}
};
}