/*
* #%L
* JavaHg
* %%
* Copyright (C) 2011 aragost Trifork ag
* %%
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
* #L%
*/
package com.aragost.javahg.internals;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.List;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
import com.aragost.javahg.BaseRepository;
import com.aragost.javahg.Bundle;
import com.aragost.javahg.Changeset;
import com.aragost.javahg.Repository;
import com.aragost.javahg.RepositoryConfiguration;
import com.aragost.javahg.commands.AddCommand;
import com.aragost.javahg.commands.CommitCommand;
import com.aragost.javahg.commands.ExecutionException;
import com.aragost.javahg.commands.IncomingCommand;
import com.aragost.javahg.commands.LogCommand;
import com.aragost.javahg.commands.VersionCommand;
import com.aragost.javahg.test.AbstractTestCase;
import com.aragost.javahg.test.JavaHgTestExtension;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.io.Files;
public class ServerTest extends AbstractTestCase {
private List<String> empty = Collections.emptyList();
@Test
public void testStartStop() throws IOException {
File dir = Files.createTempDir();
Server server = new Server(RepositoryConfiguration.DEFAULT.getHgBin(),
RepositoryConfiguration.DEFAULT.getEncoding());
server.initMecurialRepository(dir);
server.start(dir, null, empty, null, null);
server.stop();
deleteTempDir(dir);
}
@Test
public void testStopWhileProducingOutput() throws IOException {
Repository repo = getTestRepository();
Server server = getFirstServer(repo);
InputStream stdout = server.runCommand(Lists.newArrayList("version"), VersionCommand.on(repo));
// Nothing is yet read from stdout, stop the server. You
// should then get
// an IOException reading from stdout.
server.stop();
try {
stdout.read();
Assert.fail("IOException expected");
} catch (IOException e) {
// success
}
server.start(repo.getDirectory(), null, empty, null, null);
VersionCommand.on(repo).execute();
}
@Test
public void testLock() throws IOException, InterruptedException {
Repository repo = getTestRepository();
TestableCommand command = new TestableCommand(repo, "version");
// The command must produce output so that it wont finish
// before we empty the stdout. That way we can keep two
// commands running at the same time and trigger the
// IllegalStateException below .
InputStream stdout = command.executeToStream();
try {
Server server = getFirstServer(repo);
server.runCommand(Lists.newArrayList("version"), command);
Assert.fail("Exception expected");
} catch (IllegalStateException e) {
Utils.consumeAll(stdout);
}
command.executeToStream();
}
@Test
public void testServerRefCount() throws IOException {
BaseRepository repo = getTestRepository();
BaseRepository repo2 = getTestRepository2();
writeFile("a");
commit();
Bundle bundle = IncomingCommand.on(repo2).execute(repo);
Repository repo3 = bundle.getOverlayRepository();
Assert.assertSame(repo2.getServerPool(), repo3.getServerPool());
ServerPool pool = repo2.getServerPool();
Assert.assertEquals(1, pool.getServers().size());
Assert.assertNotNull(pool.getServers().get(0).getProcess());
repo2.close();
Assert.assertNotNull(pool.getServers().get(0).getProcess());
Server server = pool.getServers().get(0);
bundle.close();
Assert.assertNull(server.getProcess());
Assert.assertTrue(pool.getServers().isEmpty());
}
@Test
public void testConfigChanges() throws IOException {
BaseRepository repo = getTestRepository();
GenericCommand cmd = new GenericCommand(repo, "version") {
{
{
cmdAppend("--config", "ui.username=xxx");
}
}
};
cmd.execute();
writeFile("A");
repo.workingCopy().add("A");
try {
// The previous config change is not forgotten
CommitCommand commit = CommitCommand.on(repo).message("m");
Changeset cs = commit.execute();
assertFailedExecution(commit, "Username is " + cs.getUser());
} catch (ExecutionException e) {
Assert.assertTrue(e.getMessage().startsWith("no username supplied "));
}
}
@Test
public void testKillServerProcess() throws IOException {
File dir = Files.createTempDir();
RepositoryConfiguration repoConfig = new RepositoryConfiguration();
repoConfig.addExtension(JavaHgTestExtension.class);
Repository repo = Repository.create(repoConfig, dir);
Process process = getFirstServer(repo).getProcess();
process.destroy();
// Process is dead and we can't send command
try {
VersionCommand cmd = VersionCommand.on(repo);
cmd.execute();
assertFailedExecution(cmd);
} catch (UnexpectedServerTerminationException e) {
// success
}
repo.close();
repo = Repository.open(repoConfig, dir);
String longStringThatDoesntFitInBuffers = Strings.repeat("x", 10 * 1000 * 1000);
GenericCommand cmd = new GenericCommand(repo, "javahg-write");
HgInputStream stream = cmd.launchStream("o", longStringThatDoesntFitInBuffers);
boolean killed = killProcess(process);
if (killed) {
// Command is now sent, but we can't read output
try {
// String s =
Utils.readStream(stream, repo.getServerPool().newDecoder());
// TODO This doesn't work on Linux, why?
// Assert.fail("Exception expected. Read " +
// s.length() + " bytes");
} catch (UnexpectedServerTerminationException e) {
System.err.println("Exit value in testKillServerProcess: " + e.getExitValue());
// success
}
}
repo.close();
deleteTempDir(dir);
}
@Test
@Ignore
public void testStderrDuringStartup() throws IOException {
RepositoryConfiguration conf = new RepositoryConfiguration();
String stderr = retrieveStartupStderr(conf);
Assert.assertTrue("stderr=" + stderr,
stderr.startsWith("*** failed to import extension javahgmissing from javahgmissing: [Errno 2]"));
}
@Test
public void testStderrDuringStartupWillFullBuffer() throws IOException {
RepositoryConfiguration conf = new RepositoryConfiguration();
conf.setStderrBufferSize(1);
String stderr = retrieveStartupStderr(conf);
Assert.assertEquals("*", stderr);
}
/** validate that clone requiring auth will use auth in hgrc */
@Test
public void testCloneRequiringAuth() throws Exception {
BaseRepository repoA = getTestRepository();
writeFile(repoA, "x", "abc");
AddCommand.on(repoA).execute();
CommitCommand.on(repoA).message("added x").user("user").execute();
// extension requires auth for requests via hg serve
String extConfig = "extensions.ra="
+ Utils.resourceAsFile("/require-auth.py").getPath();
// repository accessed via http requires auth
ServeState serveState = startServing(repoA, "--config", extConfig);
try {
int port = serveState.getPort();
// cloning with an hgrc with valid auth section should succeed
File cloneDir = Files.createTempDir();
Server server = new Server(
RepositoryConfiguration.DEFAULT.getHgBin(),
RepositoryConfiguration.DEFAULT.getEncoding());
server.cloneMercurialRepository(cloneDir,
Utils.resourceAsFile("/test-hgrc-auth").getPath(),
"http://localhost:" + port);
String xContents = Files.readFirstLine(new File(cloneDir, "x"),
utf8());
Assert.assertEquals("abc", xContents);
deleteTempDir(cloneDir);
// cloning with no hgrc should generate auth exception
File cloneDir2 = Files.createTempDir();
Server server2 = new Server(
RepositoryConfiguration.DEFAULT.getHgBin(),
RepositoryConfiguration.DEFAULT.getEncoding());
try {
server2.cloneMercurialRepository(cloneDir, "",
"http://localhost:" + port);
Assert.fail("Didn't get expected http authorization exception");
} catch (Exception e) {
if (!e.getMessage().contains("http authorization required")) {
Assert.fail("Didn't get expected http authorization exception. Got "
+ e);
}
}
deleteTempDir(cloneDir2);
} finally {
serveState.stop();
}
}
/**
* Helper function for a couple of test cases.
*
* @param conf
* @return
* @throws IOException
*/
private String retrieveStartupStderr(RepositoryConfiguration conf) throws IOException {
conf.setHgrcPath(Utils.resourceAsFile("/missing-extension.hgrc").getPath());
File dir = Files.createTempDir();
BaseRepository repo = Repository.create(conf, dir);
String stderr = getFirstServer(repo).getStartupStderr();
repo.close();
deleteTempDir(dir);
return stderr;
}
/**
* Kill the process with 'kill -9'. The 'kill -9' does not give
* the process a change to react to the kill signal. Using the
* {@link Process#destroy()} the process is performing cleanup
* <p>
* This method calls the 'kill' via system call, and it will for
* example not work on Windows. It is also using platform
* dependent reflection code to obtain the pid of the process.
*
* @return true if 'kill' command was successful, false otherwise
* @param process
*/
private boolean killProcess(Process process) {
int pid = readPid(process);
if (pid != 0) {
killProcess(pid);
return true;
}
return false;
}
/**
* Attempt to read the pid for the process. If not possible return
* 0 otherwise return -1
*
* @param process
* @return
*/
private static int readPid(Process process) {
try {
Field pidField = process.getClass().getDeclaredField("pid");
pidField.setAccessible(true);
if (pidField.getType().equals(Integer.TYPE)) {
String osName = System.getProperty("os.name");
int pid = pidField.getInt(process);
if (osName.startsWith("Mac OS X")) {
// Hmm, strange but it actuall seems like the
// value of
// the pid is 1 less than the actual pid?!
pid++;
}
return pid;
}
} catch (NoSuchFieldException e) {
return 0;
} catch (Exception e) {
throw Utils.asRuntime(e);
}
return 0;
}
@Test
public void testServerIdle() throws InterruptedException {
RepositoryConfiguration conf = makeRepoConf();
conf.setServerIdleTime(1);
BaseRepository repo = Repository.create(conf, Files.createTempDir());
Assert.assertEquals(1, repo.getServerPool().getNumIdleServers());
Thread.sleep(2000);
Assert.assertEquals(0, repo.getServerPool().getNumIdleServers());
LogCommand.on(repo).execute();
Assert.assertEquals(1, repo.getServerPool().getNumIdleServers());
}
}