/*
* Copyright 2017 ThoughtWorks, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.thoughtworks.go.agent;
import com.googlecode.junit.ext.checkers.OSChecker;
import com.thoughtworks.go.agent.common.AgentBootstrapperArgs;
import com.thoughtworks.go.agent.common.util.Downloader;
import com.thoughtworks.go.agent.common.util.LoggingHelper;
import com.thoughtworks.go.agent.testhelper.FakeBootstrapperServer;
import com.thoughtworks.go.mothers.ServerUrlGeneratorMother;
import com.thoughtworks.go.util.LogFixture;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Level;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import static com.thoughtworks.go.agent.common.util.Downloader.*;
import static com.thoughtworks.go.agent.testhelper.FakeBootstrapperServer.TestResource.*;
import static com.thoughtworks.go.util.DataStructureUtils.m;
import static com.thoughtworks.go.util.LogFixture.logFixtureFor;
import static java.lang.System.getProperty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsNot.not;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.*;
@RunWith(FakeBootstrapperServer.class)
public class AgentProcessParentImplTest {
private static final OSChecker OS_CHECKER = new OSChecker(OSChecker.WINDOWS);
private final File stderrLog = new File(AgentProcessParentImpl.GO_AGENT_STDERR_LOG);
private final File stdoutLog = new File(AgentProcessParentImpl.GO_AGENT_STDOUT_LOG);
@BeforeClass
public static void setup() {
System.setProperty(LoggingHelper.LOG_DIR, ".");
System.setProperty("sleep.for.download", "10");
}
@Before
public void setUp() throws Exception {
cleanup();
}
@After
public void tearDown() {
System.clearProperty("sleep.for.download");
FileUtils.deleteQuietly(stdoutLog);
FileUtils.deleteQuietly(stderrLog);
cleanup();
}
private void cleanup() {
FileUtils.deleteQuietly(AGENT_BINARY_JAR);
FileUtils.deleteQuietly(AGENT_PLUGINS_ZIP);
FileUtils.deleteQuietly(AGENT_LAUNCHER_JAR);
FileUtils.deleteQuietly(TFS_IMPL_JAR);
}
@Test
public void shouldStartSubprocessWithCommandLine() throws InterruptedException, IOException {
final List<String> cmd = new ArrayList<>();
String expectedAgentMd5 = TEST_AGENT.getMd5();
String expectedAgentPluginsMd5 = TEST_AGENT_PLUGINS.getMd5();
String expectedTfsMd5 = TEST_TFS_IMPL.getMd5();
AgentProcessParentImpl bootstrapper = createBootstrapper(cmd);
int returnCode = bootstrapper.run("launcher_version", "bar", getURLGenerator(), new HashMap<>(), context());
assertThat(returnCode, is(42));
assertThat(cmd.toArray(new String[]{}), equalTo(new String[]{
(getProperty("java.home") + getProperty("file.separator") + "bin" + getProperty("file.separator") + "java"),
"-Dagent.plugins.md5=" + expectedAgentPluginsMd5,
"-Dagent.binary.md5=" + expectedAgentMd5,
"-Dagent.launcher.md5=bar",
"-Dagent.tfs.md5=" + expectedTfsMd5,
"-jar",
"agent.jar",
"-serverUrl",
"https://localhost:9091/go/",
"-sslVerificationMode",
"NONE",
"-rootCertFile",
"/path/to/cert.pem"
}));
}
private Process mockProcess() throws InterruptedException {
return mockProcess(new ByteArrayInputStream(new byte[0]), new ByteArrayInputStream(new byte[0]), new ByteArrayOutputStream());
}
private Process mockProcess(final InputStream outputStream, final InputStream errorStream, final OutputStream inputStream) throws InterruptedException {
final Process subProcess = mock(Process.class);
when(subProcess.waitFor()).thenReturn(42);
when(subProcess.getInputStream()).thenReturn(outputStream);
when(subProcess.getErrorStream()).thenReturn(errorStream);
when(subProcess.getOutputStream()).thenReturn(inputStream);
return subProcess;
}
@Test
public void shouldStartSubprocess_withOverriddenArgs() throws InterruptedException, IOException {
final List<String> cmd = new ArrayList<>();
AgentProcessParentImpl bootstrapper = createBootstrapper(cmd);
int returnCode = bootstrapper.run("launcher_version", "bar", getURLGenerator(), m(AgentProcessParentImpl.AGENT_STARTUP_ARGS, "foo bar baz with%20some%20space"), context());
String expectedAgentMd5 = TEST_AGENT.getMd5();
String expectedAgentPluginsMd5 = TEST_AGENT_PLUGINS.getMd5();
String expectedTfsMd5 = TEST_TFS_IMPL.getMd5();
assertThat(returnCode, is(42));
assertThat(cmd.toArray(new String[]{}), equalTo(new String[]{
(getProperty("java.home") + getProperty("file.separator") + "bin" + getProperty("file.separator") + "java"),
"foo",
"bar",
"baz",
"with some space",
"-Dagent.plugins.md5=" + expectedAgentPluginsMd5,
"-Dagent.binary.md5=" + expectedAgentMd5,
"-Dagent.launcher.md5=bar",
"-Dagent.tfs.md5=" + expectedTfsMd5,
"-jar",
"agent.jar",
"-serverUrl",
"https://localhost:9091/go/",
"-sslVerificationMode",
"NONE",
"-rootCertFile",
"/path/to/cert.pem"
}));
}
private Map context() {
HashMap hashMap = new HashMap();
hashMap.put(AgentBootstrapperArgs.SERVER_URL, getURLGenerator().serverUrlFor(""));
hashMap.put(AgentBootstrapperArgs.SSL_VERIFICATION_MODE, "NONE");
hashMap.put(AgentBootstrapperArgs.ROOT_CERT_FILE, "/path/to/cert.pem");
return hashMap;
}
private AgentProcessParentImpl createBootstrapper(final List<String> cmd) throws InterruptedException {
final Process subProcess = mockProcess();
return createBootstrapper(cmd, subProcess);
}
private AgentProcessParentImpl createBootstrapper(final List<String> cmd, final Process subProcess) {
return new AgentProcessParentImpl() {
@Override
Process invoke(String[] command) throws IOException {
cmd.addAll(Arrays.asList(command));
return subProcess;
}
};
}
@Test
public void shouldLogInterruptOnAgentProcess() throws InterruptedException {
final List<String> cmd = new ArrayList<>();
try (LogFixture logFixture = logFixtureFor(AgentProcessParentImpl.class, Level.DEBUG)) {
Process subProcess = mockProcess();
when(subProcess.waitFor()).thenThrow(new InterruptedException("bang bang!"));
AgentProcessParentImpl bootstrapper = createBootstrapper(cmd, subProcess);
int returnCode = bootstrapper.run("bootstrapper_version", "bar", getURLGenerator(), new HashMap<>(), context());
assertThat(returnCode, is(0));
assertThat(logFixture.contains(Level.ERROR, "Agent was interrupted. Terminating agent and respawning. java.lang.InterruptedException: bang bang!"), is(true));
verify(subProcess).destroy();
}
}
@Test(timeout = 10 * 1000)//if it fails with timeout, that means stderr was not flushed -jj
public void shouldLogErrorStreamOfSubprocess() throws InterruptedException, IOException {
final List<String> cmd = new ArrayList<>();
Process subProcess = mockProcess();
String stdErrMsg = "Mr. Agent writes to stderr!";
when(subProcess.getErrorStream()).thenReturn(new ByteArrayInputStream(stdErrMsg.getBytes()));
String stdOutMsg = "Mr. Agent writes to stdout!";
when(subProcess.getInputStream()).thenReturn(new ByteArrayInputStream(stdOutMsg.getBytes()));
when(subProcess.waitFor()).thenAnswer(new Answer<Object>() {
public Object answer(InvocationOnMock invocation) throws Throwable {
return 42;
}
});
AgentProcessParentImpl bootstrapper = createBootstrapper(cmd, subProcess);
int returnCode = bootstrapper.run("bootstrapper_version", "bar", getURLGenerator(), new HashMap<>(), context());
assertThat(returnCode, is(42));
assertThat(FileUtils.readFileToString(stderrLog).contains(stdErrMsg), is(true));
assertThat(FileUtils.readFileToString(stdoutLog).contains(stdOutMsg), is(true));
}
@Test
public void shouldLogFailureToStartSubprocess() throws InterruptedException {
final List<String> cmd = new ArrayList<>();
try (LogFixture logFixture = logFixtureFor(AgentProcessParentImpl.class, Level.DEBUG)) {
AgentProcessParentImpl bootstrapper = new AgentProcessParentImpl() {
@Override
Process invoke(String[] command) throws IOException {
cmd.addAll(Arrays.asList(command));
throw new RuntimeException("something failed!");
}
};
int returnCode = bootstrapper.run("bootstrapper_version", "bar", getURLGenerator(), new HashMap<>(), context());
assertThat(returnCode, is(-373));
assertThat(logFixture.contains(Level.ERROR, "Exception while executing command: " + StringUtils.join(cmd, " ") + " - java.lang.RuntimeException: something failed!"), is(true));
}
}
@Test
public void shouldClose_STDIN_and_STDOUT_ofSubprocess() throws InterruptedException {
final List<String> cmd = new ArrayList<>();
final OutputStream stdin = mock(OutputStream.class);
Process subProcess = mockProcess(new ByteArrayInputStream(new byte[0]), new ByteArrayInputStream(new byte[0]), stdin);
when(subProcess.waitFor()).thenAnswer(new Answer<Object>() {
public Object answer(InvocationOnMock invocation) throws Throwable {
verify(stdin).close();
return 21;
}
});
AgentProcessParentImpl bootstrapper = createBootstrapper(cmd, subProcess);
int returnCode = bootstrapper.run("bootstrapper_version", "bar", getURLGenerator(), new HashMap<>(), context());
assertThat(returnCode, is(21));
}
@Test
public void shouldNotDownloadPluginsZipIfPresent() throws Exception {
if (!OS_CHECKER.satisfy()) {
TEST_AGENT_PLUGINS.copyTo(AGENT_PLUGINS_ZIP);
AGENT_PLUGINS_ZIP.setLastModified(System.currentTimeMillis() - 10 * 1000);
long expectedModifiedDate = AGENT_PLUGINS_ZIP.lastModified();
AgentProcessParentImpl bootstrapper = createBootstrapper(new ArrayList<>());
bootstrapper.run("launcher_version", "bar", getURLGenerator(), m(AgentProcessParentImpl.AGENT_STARTUP_ARGS, "foo bar baz with%20some%20space"), context());
assertThat(Downloader.AGENT_PLUGINS_ZIP.lastModified(), is(expectedModifiedDate));
}
}
@Test
public void shouldDownloadPluginsZipIfMissing() throws Exception {
if (!OS_CHECKER.satisfy()) {
File stalePluginZip = randomFile(AGENT_PLUGINS_ZIP);
long original = stalePluginZip.length();
AgentProcessParentImpl bootstrapper = createBootstrapper(new ArrayList<>());
bootstrapper.run("launcher_version", "bar", getURLGenerator(), m(AgentProcessParentImpl.AGENT_STARTUP_ARGS, "foo bar baz with%20some%20space"), context());
assertThat(stalePluginZip.length(), not(original));
}
}
@Test
public void shouldDownload_TfsImplJar_IfTheCurrentJarIsStale() throws Exception {
if (!OS_CHECKER.satisfy()) {
File staleFile = randomFile(TFS_IMPL_JAR);
long original = staleFile.length();
AgentProcessParentImpl bootstrapper = createBootstrapper(new ArrayList<>());
bootstrapper.run("launcher_version", "bar", getURLGenerator(), m(AgentProcessParentImpl.AGENT_STARTUP_ARGS, "foo bar baz with%20some%20space"), context());
assertThat(staleFile.length(), not(original));
}
}
private File randomFile(final File pathname) throws IOException {
FileUtils.write(pathname, "some rubbish", StandardCharsets.UTF_8);
return pathname;
}
private ServerUrlGenerator getURLGenerator() {
return ServerUrlGeneratorMother.generatorFor("localhost", 9090);
}
}