/* * 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; } }