Java tutorial
/* * Copyright (C) 2011 Laurent Caillette * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation, either * version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.novelang.outfit.shell; import java.io.EOFException; import java.io.File; import java.io.IOException; import java.lang.management.RuntimeMXBean; import java.lang.reflect.UndeclaredThrowableException; import java.net.ConnectException; import java.util.List; import java.util.MissingResourceException; import java.util.concurrent.TimeUnit; import javax.management.InstanceNotFoundException; import com.google.common.base.Predicate; import com.google.common.base.Throwables; import org.apache.commons.io.FileUtils; import org.fest.reflect.core.Reflection; import org.junit.Rule; import org.junit.Test; import static org.fest.assertions.Assertions.assertThat; import static org.junit.Assert.assertNotNull; import org.novelang.logger.Logger; import org.novelang.logger.LoggerFactory; import org.novelang.outfit.TcpPortBooker; import org.novelang.outfit.shell.insider.Insider; import org.novelang.testing.RepeatedAssert; import org.novelang.testing.StandalonePredicate; import org.novelang.testing.junit.MethodSupport; /** * Tests for {@link JavaShell}. * This test needs the embedded Insider jar in the classpath (see insider Maven project). * For running from an IDE, needs parameters like this: * <pre> -Dlogback.configurationFile=configuration/test/logback-test.xml -Dorg.novelang.outfit.shell.agentjarfile=/Users/currentuser/.m2/repository/org/novelang/Novelang-insider/${project.version}/Novelang-insider-${project.version}.jar -Dorg.novelang.outfit.shell.fixturejarfile=/Users/currentuser/.m2/repository/org/novelang/Novelang-shell-fixture/${project.version}/Novelang-shell-fixture-${project.version}.jar -Dorg.novelang.outfit.shell.versionoverride=SNAPSHOT * </pre> * * @author Laurent Caillette */ public class JavaShellTest { @Test public void getTheOfficialJar() throws IOException { final File jarFile = AgentFileInstaller.getInstance().getJarFile(); assertThat(jarFile).isNotNull(); } @Test(expected = ProcessInitializationException.class) public void cannotStart() throws Exception { final ShellFixture shellFixture = new ShellFixture(methodSupport); final JavaShell javaShell = new JavaShell(shellFixture.getParameters().withJavaClasses( new JavaClasses.ClasspathAndMain("ThisClassDoesNotExist", shellFixture.getJarFile()))); javaShell.start(); } @Test // @Ignore( "Takes too long (about 40 s)" ) public void startTwice() throws Exception { startAndShutdown(new JavaShell(new ShellFixture(methodSupport).getParameters())); startAndShutdown(new JavaShell(new ShellFixture(methodSupport).getParameters())); } @Test public void useAsJmxConnector() throws Exception { final JavaShell javaShell = new JavaShell(new ShellFixture(methodSupport).getParameters()); javaShell.start(); try { final String virtualMachineName = queryJvmName(javaShell); assertNotNull(virtualMachineName); LOGGER.info("Returned VM name: '", virtualMachineName, "'"); } finally { javaShell.shutdown(ShutdownStyle.FORCED); } } @Test public void detectProgramExitedOnItsOwn() throws Exception { final ShellFixture shellFixture = new ShellFixture(methodSupport); final int heartbeatFatalDelay = 1000; final JavaShell javaShell = new JavaShell(shellFixture.getParameters() .withHeartbeatFatalDelayMilliseconds(heartbeatFatalDelay).withHeartbeatPeriodMilliseconds(100)); javaShell.start(); final MaybeDown maybeDown = new MaybeDown(javaShell); // Be sure that JMX stuff started in watched JVM. assertThat(queryJvmName(javaShell)).isNotNull(); shellFixture.askForSelfTermination(); Thread.sleep((long) heartbeatFatalDelay); assertThat(maybeDown.apply()).isTrue(); } @Test public void missHeartbeat() throws Exception { final int heartbeatPeriod = 100; final int heartbeatFatalDelay = 1000; final long maybeDownCheckPeriod = 100; final int maybeDownRetryCount = 2; final ShellFixture shellFixture = new ShellFixture(methodSupport); final JavaShellParameters parameters = shellFixture.getParameters() .withHeartbeatPeriodMilliseconds(heartbeatPeriod) .withHeartbeatFatalDelayMilliseconds(heartbeatFatalDelay) .withJmxPortConfiguredAtJvmStartup(TcpPortBooker.THIS.find()).withJmxKit(new DefaultJmxKit()); final JavaShell javaShell = new JavaShell(parameters); try { javaShell.start(); final MaybeDown maybeDown = new MaybeDown(javaShell); LOGGER.debug("At this time, the JVM should be up."); assertThat(maybeDown.apply()).isFalse(); // Test health. final HeartbeatSender heartbeatSender = Reflection.field("heartbeatSender") .ofType(HeartbeatSender.class).in(javaShell).get(); heartbeatSender.stop(); RepeatedAssert.assertEventually(maybeDown, maybeDownCheckPeriod, TimeUnit.MILLISECONDS, maybeDownRetryCount); final List<String> log = readLines(shellFixture.getLogFile()); assertThat(log).isNotEmpty(); assertThat(log.get(0)).contains("Starting up").contains("listening..."); } finally { javaShell.shutdown(ShutdownStyle.FORCED); } } @Test public void justStartForeignProgram() throws Exception { final ShellFixture shellFixture = new ShellFixture(methodSupport); final JavaShell javaShell = new JavaShell(shellFixture.getParameters()); try { javaShell.start(); LOGGER.info("Started process known as ", javaShell.getNickname(), "."); javaShell.shutdown(ShutdownStyle.GENTLE); } catch (Exception e) { javaShell.shutdown(ShutdownStyle.FORCED); throw e; } final List<String> log = readLines(shellFixture.getLogFile()); assertThat(log).hasSize(2); assertThat(log.get(0)).contains("Starting up").contains("listening"); assertThat(log.get(1)).contains("Terminated."); } // ======= // Fixture // ======= static final Logger LOGGER = LoggerFactory.getLogger(JavaShellTest.class); static final Predicate<String> STUPID_LISTENER_STARTED = new Predicate<String>() { @Override public boolean apply(final String input) { return input.startsWith("Started."); } }; @Rule public final MethodSupport methodSupport = new MethodSupport() { @Override protected String mayEvaluateInContext() { return mayEvaluate(); } }; @SuppressWarnings({ "ThrowableInstanceNeverThrown" }) private String mayEvaluate() { for (final StackTraceElement element : new Exception().getStackTrace()) { if (element.getClassName().contains("org.apache.maven.surefire.Surefire")) { // Maven tests should work all time unless broken for good reason. return null; } } final String warning = "Not running as Maven test, nor couldn't find agent jar file" + " (check system properties). Skipping " + methodSupport.getTestName() + " because needed resources may be missing."; String message = warning; if (AgentFileInstaller.mayHaveValidInstance()) { try { AgentFileInstaller.getInstance().getJarFile(); message = null; } catch (MissingResourceException ignore) { } } if (message == null) { return null; } else { return message; } } @SuppressWarnings({ "unchecked" }) private static List<String> readLines(final File logFile) throws IOException { return FileUtils.readLines(logFile); } private static final String FIXTUREJARFILE_PROPERTYNAME = "org.novelang.outfit.shell.fixturejarfile"; static File installFixturePrograms(final File directory) throws IOException { final String fixtureJarFileAsString = System.getProperty(FIXTUREJARFILE_PROPERTYNAME); if (fixtureJarFileAsString == null) { final File jarFile = new File(directory, "java-program.jar"); AgentFileInstaller.getInstance().copyVersionedJarToFile(FIXTURE_PROGRAM_JAR_RESOURCE_RADIX, jarFile); return jarFile; } else { final File existingJarFile = AgentFileInstaller.getInstance() .resolveWithVersion(fixtureJarFileAsString); if (!existingJarFile.isFile()) { throw new IllegalArgumentException("Not an existing file: '" + existingJarFile + "'"); } return existingJarFile; } } private static void startAndShutdown(final JavaShell javaShell) throws Exception { javaShell.start(); try { assertNotNull(javaShell.getManagedBean(RuntimeMXBean.class, JavaShellTools.RUNTIME_MX_BEAN_OBJECTNAME) .getVmName()); } finally { javaShell.shutdown(ShutdownStyle.GENTLE); } } /** * TODO: Make this work for non-SNAPSHOT versions. */ @SuppressWarnings({ "HardcodedFileSeparator" }) private static final String FIXTURE_PROGRAM_JAR_RESOURCE_RADIX = "/Novelang-shell-fixture-"; private static class MaybeDown implements StandalonePredicate { private final Insider insider; public MaybeDown(final JavaShell javaShell) throws IOException, InterruptedException { insider = javaShell.getManagedBean(Insider.class, Insider.NAME); } @Override public boolean apply() { try { return !insider.isAlive(); } catch (UndeclaredThrowableException e) { final Throwable cause = Throwables.getRootCause(e); if (denotesConnectionLoss(cause)) { return true; } else { throw e; } } } private static boolean denotesConnectionLoss(final Throwable cause) { return cause instanceof java.rmi.ConnectException || cause instanceof InstanceNotFoundException || cause instanceof ConnectException || cause instanceof EOFException || (cause instanceof IOException && cause.getMessage().contains("The client has been closed.")); } @Override public String toString() { return getClass().getSimpleName(); } } private static String queryJvmName(final JavaShell javaShell) throws IOException, InterruptedException { final RuntimeMXBean runtimeMXBean = javaShell.getManagedBean(RuntimeMXBean.class, JavaShellTools.RUNTIME_MX_BEAN_OBJECTNAME); final String virtualMachineName = runtimeMXBean.getVmName(); return virtualMachineName; } }