package net.sourceforge.cruisecontrol.builders; import java.io.EOFException; import java.io.File; import java.io.IOException; import java.net.ConnectException; import java.net.MalformedURLException; import java.net.SocketException; import java.net.URL; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Properties; import junit.extensions.TestSetup; import junit.framework.Test; import junit.framework.TestCase; import junit.framework.TestSuite; import net.jini.config.Configuration; import net.jini.config.ConfigurationException; import net.jini.config.ConfigurationProvider; import net.jini.core.discovery.LookupLocator; import net.jini.core.lookup.ServiceID; import net.jini.core.lookup.ServiceRegistrar; import net.jini.lookup.ServiceIDListener; import net.sourceforge.cruisecontrol.MockProject; import net.sourceforge.cruisecontrol.Progress; import net.sourceforge.cruisecontrol.ProjectConfig; import net.sourceforge.cruisecontrol.distributed.BuildAgent; import net.sourceforge.cruisecontrol.distributed.BuildAgentService; import net.sourceforge.cruisecontrol.distributed.BuildAgentServiceImplTest; import net.sourceforge.cruisecontrol.distributed.BuildAgentTest; import net.sourceforge.cruisecontrol.distributed.core.MulticastDiscoveryTest; import net.sourceforge.cruisecontrol.distributed.core.PropertiesHelper; import net.sourceforge.cruisecontrol.distributed.core.ReggieUtil; import net.sourceforge.cruisecontrol.distributed.core.RemoteResultTest; import net.sourceforge.cruisecontrol.labelincrementers.DefaultLabelIncrementer; import net.sourceforge.cruisecontrol.util.Commandline; import net.sourceforge.cruisecontrol.util.DateUtil; import net.sourceforge.cruisecontrol.util.OSEnvironment; import net.sourceforge.cruisecontrol.util.ServerNameSingleton; import net.sourceforge.cruisecontrol.util.StreamConsumer; import net.sourceforge.cruisecontrol.util.StreamPumper; import net.sourceforge.cruisecontrol.util.Util; import org.apache.log4j.Level; import org.apache.log4j.Logger; /** * @author Dan Rollo * Date: May 6, 2005 * Time: 2:34:24 PM */ public class DistributedMasterBuilderTest extends TestCase { private static final Logger LOG = Logger.getLogger(DistributedMasterBuilderTest.class); /** NOTE: Assumes we are executing with current dir cc/contrig/distributed/target */ // @todo Change if we move CCDist into main public static final String MAIN_CCDIST_DIR = "../"; static { final File mainCCDistDir; try { mainCCDistDir = new File(MAIN_CCDIST_DIR).getCanonicalFile(); } catch (IOException e) { throw new RuntimeException(e); } final String msg = "Executing unit tests in unexpected directory: " + mainCCDistDir.getAbsolutePath() + ";"; assertEquals(msg, "distributed", mainCCDistDir.getName()); final File ccDistTargetDir; try { ccDistTargetDir = new File(".").getCanonicalFile(); } catch (IOException e) { throw new RuntimeException(e); } assertEquals(msg, "target", ccDistTargetDir.getName()); } private static final String INSECURE_POLICY_FILENAME = "insecure.policy"; private static Properties origSysProps; private static final String CONFIG_START_JINI = "conf/start-jini.config"; public static final String JINI_URL_LOCALHOST; static { final Configuration config; String hostname; try { config = ConfigurationProvider.getInstance(new String[] {MAIN_CCDIST_DIR + CONFIG_START_JINI}); hostname = (String) config.getEntry("com.sun.jini.start", "hostname", String.class); } catch (ConfigurationException e) { throw new RuntimeException("Error loading jini config: " + CONFIG_START_JINI, e); } if (hostname != null) { JINI_URL_LOCALHOST = "jini://" + hostname; } else { JINI_URL_LOCALHOST = "jini://localhost"; } LOG.info("Using local Jini URL: " + JINI_URL_LOCALHOST); } private static final OSEnvironment OS_ENV = new OSEnvironment(); private static ProcessInfoPump jiniProcessPump; public static final String MSG_PREFIX_STATS = "STATS: "; private static String getTestDMBEntries() { final Map userProps = PropertiesHelper.loadRequiredProperties(BuildAgentServiceImplTest.TEST_USER_DEFINED_PROPERTIES_FILE); final Object retval = userProps.get(BuildAgentServiceImplTest.ENTRY_NAME_BUILD_TYPE); assertNotNull("Missing required entry for DMB unit test: " + BuildAgentServiceImplTest.ENTRY_NAME_BUILD_TYPE, retval); assertTrue(retval instanceof String); return BuildAgentServiceImplTest.ENTRY_NAME_BUILD_TYPE + "=" + retval; } /** * Show what's happening with the Jini Process. */ private static final class PrefixedStreamConsumer implements StreamConsumer { private final String prefix; private final Logger logger; private final Level level; PrefixedStreamConsumer(final String prefix, final Logger logger, final Level level) { this.prefix = prefix; this.logger = logger; this.level = level; } /** {@inheritDoc} */ public void consumeLine(String line) { logger.log(level, prefix + line); } } /** * @return the Process in which Jini Lookup _service is running, for use in killing it. * @throws Exception if we can't start jini lookup service */ public static ProcessInfoPump startJini() throws Exception { final long begin = System.currentTimeMillis(); final Logger logger = LOG; final Level level = Level.INFO; // make sure local lookup service is not already running verifyNoLocalLookupService(); origSysProps = System.getProperties(); // Build Lookup Service command line just like the one in lookup-build.xml // <java jar="jini-lib/start.jar" fork="true" > // <jvmarg value="-Djava.security.policy=conf/${jini.policy.file}" /> // <jvmarg value="-Djini.lib=jini-lib" /> // <jvmarg value="-Djini.lib.dl=jini-lib-dl" /> // <jvmarg value="-Djini.httpPort=${jini.port}" /> // <arg value="conf/${jini.config}"/> final String jiniLibDir = "jini-lib"; final String[] args = new String[] { "-Djava.security.policy=conf/insecure.policy", //${jini.policy.file} "-Djini.lib=" + jiniLibDir, "-Djini.lib.dl=jini-lib-dl", //Downloadable Jini jars "-Djini.httpPort=8050", //${jini.port}" }; final Commandline cmdLine = new Commandline(); cmdLine.addArguments(args); Commandline.Argument argClasspath = cmdLine.createArgument(); argClasspath.setLine("-classpath " + "conf"); Commandline.Argument argStart = cmdLine.createArgument(); argStart.setLine("-jar " + jiniLibDir + "/start.jar"); Commandline.Argument argProg = cmdLine.createArgument(); argProg.setValue(CONFIG_START_JINI); // ${jini.config} cmdLine.setExecutable(getJavaExec()); LOG.debug("jini startup command: " + Arrays.asList(cmdLine.getCommandline())); final Process newJiniProcess = Runtime.getRuntime().exec(cmdLine.getCommandline(), null, new File(MAIN_CCDIST_DIR)); newJiniProcess.getOutputStream().close(); final ProcessInfoPump jiniProcessInfoPump = new ProcessInfoPump(newJiniProcess, // show what's happening with the jiniProcessPump new StreamPumper(newJiniProcess.getErrorStream(), new PrefixedStreamConsumer("[JiniErr] ", logger, level)), new StreamPumper(newJiniProcess.getInputStream(), new PrefixedStreamConsumer("[JiniOut] ", logger, level)), logger, level); Thread.sleep(2000); // allow LUS some spin up time, first time is longest (~3.5 secs) // Verify the Lookup Service started // setup security policy setupInsecurePolicy(); // The startup time for the first LUS appears to be much longer than subsequent startups... ServiceRegistrar serviceRegistrar = findTestLookupService(50 * 1000); try { assertNotNull("Failed to start local lookup _service.", serviceRegistrar); } finally { if (serviceRegistrar == null) { // kill service in case it is just really slow to start, to help avoid orphaned LUS processes killJini(jiniProcessInfoPump); } } assertEquals("Unexpected local lookup _service host", ServerNameSingleton.getServerName(), serviceRegistrar.getLocator().getHost()); LOG.info(MSG_PREFIX_STATS + "Jini Startup took: " + (System.currentTimeMillis() - begin) / 1000f + " sec"); return jiniProcessInfoPump; } /** * Setup an insecure policy file for use during unit tests that require Jini. */ public static void setupInsecurePolicy() { URL policyFile = ClassLoader.getSystemClassLoader().getResource(INSECURE_POLICY_FILENAME); assertNotNull("Can't load policy file resource: " + INSECURE_POLICY_FILENAME + ". Make sure this file is in the classes (bin) directory.", policyFile); System.setProperty(BuildAgent.JAVA_SECURITY_POLICY, policyFile.toExternalForm()); ReggieUtil.setupRMISecurityManager(); } private static String javaExecutable; private static String getJavaExec() { if (javaExecutable == null) { final String javaExecFilename; if (Util.isWindows()) { javaExecFilename = "java.exe"; } else { javaExecFilename = "java"; } // use javaHome env var to find java if (getJavaHome() != null) { File checkExists = new File(getJavaHome() + File.separator + "bin" + File.separator + javaExecFilename); if (checkExists.exists()) { javaExecutable = checkExists.getAbsolutePath(); } else { // maybe JAVA_HOME env var is bad, try sys prop of current vm LOG.warn("Is JAVA_HOME valid? Unit Test couldn't find: " + checkExists.getAbsolutePath()); checkExists = new File(System.getProperty("java.home") + File.separator + "bin" + File.separator + javaExecFilename); if (checkExists.exists()) { javaExecutable = checkExists.getAbsolutePath(); } else { LOG.warn("Unit Test couldn't find java. Might work if java is on the path? Here goes..."); javaExecutable = javaExecFilename; } } } else { final String msg = "Unit Test couldn't find JAVA_HOME env var. Maybe java/bin is in the path? Here goes..."; System.out.println(msg); LOG.warn(msg); javaExecutable = javaExecFilename; } } return javaExecutable; } private static String javaHome; private static String getJavaHome() { if (javaHome == null) { String envJavaHome = OS_ENV.getVariable("JAVA_HOME"); if (envJavaHome != null) { javaHome = envJavaHome; } else { // try system prop for java.home javaHome = System.getProperty("java.home"); } } return javaHome; } public static ServiceRegistrar findTestLookupService(int retryTimeoutMillis) throws IOException, ClassNotFoundException, InterruptedException { // find/wait for lookup _service final long startTime = System.currentTimeMillis(); ServiceRegistrar serviceRegistrar = null; final LookupLocator lookup = new LookupLocator(JINI_URL_LOCALHOST); // making this polling loop pause too short actually slows the unit tests down final int sleepMillisAfterException = 250; while (serviceRegistrar == null && (System.currentTimeMillis() - startTime < retryTimeoutMillis)) { try { serviceRegistrar = lookup.getRegistrar(); } catch (ConnectException e) { Thread.sleep(sleepMillisAfterException); } catch (SocketException e) { Thread.sleep(sleepMillisAfterException); } catch (EOFException e) { Thread.sleep(sleepMillisAfterException); } // more exceptions will likely need to added here as the Jini libraries are updated. // could catch a generic super class, but I kinda like to know what's being thrown. } LOG.info(MSG_PREFIX_STATS + "Find Test LUS took: " + (System.currentTimeMillis() - startTime) / 1000f + " sec; Timeout: " + retryTimeoutMillis + "; Found: " + (serviceRegistrar != null)); return serviceRegistrar; } public static void killJini(final ProcessInfoPump jiniProcessPump) throws Exception { if (jiniProcessPump != null) { final long begin = System.currentTimeMillis(); // first, attempt gracefull LUS.destroy() try { MulticastDiscoveryTest.destroyLocalLUS(); } catch (Throwable t) { LOG.error("Warning: Failed to gracefully destroy Local LUS in unit test. Will force kill of LUS."); } jiniProcessPump.kill(); LOG.debug("Jini process killed."); verifyNoLocalLookupService(); LOG.info(MSG_PREFIX_STATS + "Jini Shutdown took: " + (System.currentTimeMillis() - begin) / 1000f + " sec"); } // restore original system properties System.setProperties(origSysProps); } private static void verifyNoLocalLookupService() throws IOException, ClassNotFoundException, InterruptedException { final String msgLUSFoundCheckForOrphanedProc = "Found local lookup service, but it should be dead. Is an orphaned java process still running?"; final ServiceRegistrar serviceRegistrar; try { serviceRegistrar = findTestLookupService(1000); } catch (ClassNotFoundException e) { assertFalse(msgLUSFoundCheckForOrphanedProc, "com.sun.jini.reggie.ConstrainableRegistrarProxy".equals(e.getMessage())); throw e; } assertNull(msgLUSFoundCheckForOrphanedProc, serviceRegistrar); } /** * Holds a executing process and it's associated stream pump threads. */ public static final class ProcessInfoPump { private final Process process; private final Thread inputPumpThread; private final Thread errorPumpThread; private final Logger logger; private final Level level; public ProcessInfoPump(final Process process, final StreamPumper inputPump, final StreamPumper errorPump, final Logger logger, final Level level) { this.process = process; this.logger = logger; this.level = level; errorPumpThread = new Thread(errorPump); inputPumpThread = new Thread(inputPump); errorPumpThread.start(); inputPumpThread.start(); } public void kill() throws IOException, InterruptedException { process.destroy(); logger.log(level, "Process destroyed."); // wait for stream pumps to end if (errorPumpThread != null) { errorPumpThread.join(); } if (inputPumpThread != null) { inputPumpThread.join(); } process.getInputStream().close(); process.getErrorStream().close(); process.getOutputStream().close(); logger.log(level, "Process pumps finished."); } } /** * Test Decorator to launch Jini LUS once for this class. */ public static final class LUSTestSetup extends TestSetup { public LUSTestSetup(Test test) { super(test); } protected void setUp() throws Exception { jiniProcessPump = DistributedMasterBuilderTest.startJini(); } protected void tearDown() throws Exception { DistributedMasterBuilderTest.killJini(jiniProcessPump); } } /** * Use LUSTestSetup decorator to run Jini LUS once for this test class. * @return a TestSuite wrapper by the LUSTestSetup decorator */ public static Test suite() { final TestSuite ts = new TestSuite(); ts.addTestSuite(DistributedMasterBuilderTest.class); return new LUSTestSetup(ts); } // @todo Add one slash in front of "/*" below to run individual tests in an IDE /* protected void setUp() throws Exception { jiniProcessPump = DistributedMasterBuilderTest.startJini(); } protected void tearDown() throws Exception { DistributedMasterBuilderTest.killJini(jiniProcessPump); } //*/ public void testRemoteProgress() throws Exception { // register agent final BuildAgent agentAvailable = createBuildAgent(); try { final DistributedMasterBuilder masterBuilder = getMasterBuilder_LocalhostONLY(); final MockBuilder mockBuilder = new MockBuilder(); masterBuilder.add(mockBuilder); masterBuilder.validate(); final Map<String, String> projectProperties = new HashMap<String, String>(); projectProperties.put(PropertiesHelper.PROJECT_NAME, "testProjectName"); final MockProject mockProject = new MockProject(); final Progress progress = mockProject.getProgress(); final ProjectConfig projectConfig = new ProjectConfig(); projectConfig.add(new DefaultLabelIncrementer()); mockProject.setProjectConfig(projectConfig); masterBuilder.build(projectProperties, progress); final BuildAgentService agentService = masterBuilder.pickAgent(null, null); assertNotNull("Couldn't find released agent.\n" + MulticastDiscoveryTest.MSG_DISOCVERY_CHECK_FIREWALL, agentService); assertTrue("Claimed agent should show as busy. (Did we find a better way?)", agentService.isBusy()); assertTrue("Wrong progress value: " + progress.getValue(), progress.getValue().indexOf(" retrieving results from ") == DateUtil.SIMPLE_DATE_FORMAT.length()); } finally { // terminate JoinManager in BuildAgent BuildAgentTest.terminateTestAgent(agentAvailable); } } public void testPickAgent2Agents() throws Exception { // register agent final BuildAgent agentAvailable = createBuildAgent(); final BuildAgent agentAvailable2 = createBuildAgent(); try { assertFalse(agentAvailable.getService().isBusy()); assertFalse(agentAvailable2.getService().isBusy()); final DistributedMasterBuilder masterBuilder = getMasterBuilder_LocalhostONLY(); // try to find agents final BuildAgentService agentFoundFirst = masterBuilder.pickAgent(null, null); assertNotNull("Couldn't find first agent.\n" + MulticastDiscoveryTest.MSG_DISOCVERY_CHECK_FIREWALL, agentFoundFirst); assertTrue(agentFoundFirst.isBusy()); final BuildAgentService agentFoundSecond = masterBuilder.pickAgent(null, null); assertNotNull("Couldn't find second agent", agentFoundSecond); assertTrue(agentFoundFirst.isBusy()); assertTrue(agentFoundSecond.isBusy()); assertNull("Shouldn't find third agent", masterBuilder.pickAgent(null, null)); // set Agent to Not busy, then make sure it can be found again. // callTestDoBuildSuccess() only needed to clearOuputFiles() will succeed assertNotNull(BuildAgentServiceImplTest.callTestDoBuildSuccess(agentAvailable.getService())); agentAvailable.getService().clearOutputFiles(); RemoteResultTest.resetTempZippedFile(BuildAgentServiceImplTest.REMOTE_RESULTS_ONE[0]); final BuildAgentService agentRefound = masterBuilder.pickAgent(null, null); assertNotNull("Couldn't find released agent", agentRefound); assertTrue("Claimed agent should show as busy. (Did we find a better way?)", agentRefound.isBusy()); } finally { // terminate JoinManager in BuildAgent BuildAgentTest.terminateTestAgent(agentAvailable); BuildAgentTest.terminateTestAgent(agentAvailable2); } } public void testPickAgentAfterReleased() throws Exception { // register agent final BuildAgent agentAvailable = createBuildAgent(); try { assertFalse(agentAvailable.getService().isBusy()); agentAvailable.getService().claim(); // mark as busy final DistributedMasterBuilder masterBuilder = getMasterBuilder_LocalhostONLY(); // try to find agent, shouldn't find any available assertNull("Shouldn't find any available agents", masterBuilder.pickAgent(null, null)); // set Agent to Not busy, then make sure it can be found again. // callTestDoBuildSuccess() only needed to clearOuputFiles() will succeed assertNotNull(BuildAgentServiceImplTest.callTestDoBuildSuccess(agentAvailable.getService())); agentAvailable.getService().clearOutputFiles(); RemoteResultTest.resetTempZippedFile(BuildAgentServiceImplTest.REMOTE_RESULTS_ONE[0]); final BuildAgentService agentRefound = masterBuilder.pickAgent(null, null); assertNotNull("Couldn't find released agent.\n" + MulticastDiscoveryTest.MSG_DISOCVERY_CHECK_FIREWALL, agentRefound); assertTrue("Claimed agent should show as busy. (Did we find a better way?)", agentRefound.isBusy()); } finally { // terminate JoinManager in BuildAgent BuildAgentTest.terminateTestAgent(agentAvailable); } } public void testPickAgentAgentNotBusy() throws Exception { // register agent final BuildAgent agentAvailable = createBuildAgent(); try { assertFalse(agentAvailable.getService().isBusy()); final DistributedMasterBuilder masterBuilder = getMasterBuilder_LocalhostONLY(); final BuildAgentService agent = masterBuilder.pickAgent(null, null); assertNotNull("Couldn't find agent.\n" + MulticastDiscoveryTest.MSG_DISOCVERY_CHECK_FIREWALL, agent); assertTrue("Claimed agent should show as busy. (Did we find a better way?)", agent.isBusy()); // try to find agent, shouldn't find any available assertNull("Shouldn't find any available agents", masterBuilder.pickAgent(null, null)); // set Agent to Not busy, then make sure it can be found again. // only needed so clearOuputFiles() will succeed assertNotNull(BuildAgentServiceImplTest.callTestDoBuildSuccess(agent)); agent.clearOutputFiles(); RemoteResultTest.resetTempZippedFile(BuildAgentServiceImplTest.REMOTE_RESULTS_ONE[0]); final BuildAgentService agentRefound = masterBuilder.pickAgent(null, null); assertNotNull("Couldn't find released agent", agentRefound); assertTrue("Claimed agent should show as busy. (Did we find a better way?)", agentRefound.isBusy()); } finally { // terminate JoinManager in BuildAgent BuildAgentTest.terminateTestAgent(agentAvailable); } } public void testPickAgentNoAgents() throws Exception { DistributedMasterBuilder masterBuilder = getMasterBuilder_LocalhostONLY(); assertNull("Shouldn't find any available agents", masterBuilder.pickAgent(null, null)); } private static int testAgentID = 0; private static BuildAgent createBuildAgent() throws InterruptedException { return createBuildAgent(true); } public static BuildAgent createBuildAgent(final boolean isDiscoveryRequired) throws InterruptedException { final int thisAgentID = ++testAgentID; final long begin = System.currentTimeMillis(); BuildAgentTest.setSkipMainSystemExit(); BuildAgentTest.setTerminateFast(); // listen for agent to discover LUS final String lock = "agentDiscLock" + thisAgentID; final class MyServiceIDListener implements ServiceIDListener { private volatile ServiceID myServiceID; /** * Called when the JoinManager gets a valid ServiceID from a lookup * service. * * @param serviceID the service ID assigned by the lookup service. */ public void serviceIDNotify(ServiceID serviceID) { myServiceID = serviceID; LOG.info("Agent assigned serviceID: " + serviceID + ". (agentID: " + thisAgentID + ")"); synchronized (lock) { lock.notifyAll(); } } } final MyServiceIDListener utestListener = new MyServiceIDListener(); LOG.info("Creating test Agent (agentID: " + thisAgentID + ")"); final BuildAgent agent = BuildAgentTest.createTestBuildAgent( BuildAgentServiceImplTest.TEST_AGENT_PROPERTIES_FILE, BuildAgentServiceImplTest.TEST_USER_DEFINED_PROPERTIES_FILE, true, utestListener, thisAgentID); if (isDiscoveryRequired) { synchronized (lock) { int count = 0; while (utestListener.myServiceID == null && count < 6) { lock.wait(10 * 1000); count++; } } final float elapsedSecs = (System.currentTimeMillis() - begin) / 1000f; assertNotNull("Unit test Agent was not discovered before timeout. elapsed: " + elapsedSecs + " sec \n" + MulticastDiscoveryTest.MSG_DISOCVERY_CHECK_FIREWALL, utestListener.myServiceID); LOG.info(MSG_PREFIX_STATS + "Unit test Agent (agentID: " + thisAgentID + ") discovery took: " + elapsedSecs + " sec"); } return agent; } static DistributedMasterBuilder getMasterBuilder_LocalhostONLY() throws MalformedURLException, InterruptedException { MulticastDiscoveryTest.locateLocalhostMulticastDiscovery(); final DistributedMasterBuilder masterBuilder = new DistributedMasterBuilder(); // need to set Entries to prevent finding non-local LUS and/or non-local Build Agents masterBuilder.setEntries(getTestDMBEntries()); masterBuilder.setFailFast(); // don't block until an available agent is found return masterBuilder; } }