/* * RHQ Management Platform * Copyright (C) 2005-2014 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation version 2 of the License. * * 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, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ package org.rhq.test.pc; import java.io.File; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URI; import java.net.URL; import java.util.Collections; import java.util.HashMap; import java.util.Map; import org.apache.commons.io.FileUtils; import org.jmock.Mockery; import org.testng.ITestResult; import org.testng.annotations.AfterSuite; import org.testng.annotations.Listeners; import org.rhq.core.clientapi.server.bundle.BundleServerService; import org.rhq.core.clientapi.server.configuration.ConfigurationServerService; import org.rhq.core.clientapi.server.content.ContentServerService; import org.rhq.core.clientapi.server.core.CoreServerService; import org.rhq.core.clientapi.server.discovery.DiscoveryServerService; import org.rhq.core.clientapi.server.drift.DriftServerService; import org.rhq.core.clientapi.server.event.EventServerService; import org.rhq.core.clientapi.server.inventory.ResourceFactoryServerService; import org.rhq.core.clientapi.server.measurement.MeasurementServerService; import org.rhq.core.clientapi.server.operation.OperationServerService; import org.rhq.core.pc.PluginContainer; import org.rhq.core.pc.PluginContainerConfiguration; import org.rhq.core.pc.ServerServices; import org.rhq.core.pc.inventory.InventoryManager; import org.rhq.core.pc.plugin.FileSystemPluginFinder; import org.rhq.core.util.file.FileUtil; import org.rhq.test.JMockTest; /** * This class is similar to {@link JMockTest} in that it can be used both * as a base class to your test or as a TestNG {@link Listeners listener} * on your test class. * <p> * This class is used to declaratively setup a plugin container a test wants * to use using the {@link PluginContainerSetup} annotation on a test method or class. * * @author Lukas Krejci */ public class PluginContainerTest extends JMockTest { private static final File ROOT; private static boolean cleanedUp = false; private static final String PLUGINS_DIR_NAME = "plugins"; private static final String DATA_DIR_NAME = "data"; private static final String TMP_DIR_NAME = "tmp"; private static final ThreadLocal<PluginContainerConfiguration> STATICALLY_ACCESSIBLE_PLUGIN_CONTAINER_CONFIGURATION = new ThreadLocal<PluginContainerConfiguration>(); private static final ThreadLocal<PluginContainerSetup> CURRENT_SETUP = new ThreadLocal<PluginContainerSetup>(); private static final Map<String, Object> SERVERSIDE_FAKES = Collections .synchronizedMap(new HashMap<String, Object>()); protected PluginContainerConfiguration pluginContainerConfiguration; static { File f = new File(System.getProperty("java.io.tmpdir"), "plugin-container-test-" + System.currentTimeMillis()); while (f.exists() || !f.mkdir()) { f = new File(System.getProperty("java.io.tmpdir"), "plugin-container-test-" + System.currentTimeMillis()); } ROOT = f; } /** * During the tests, one might need to "fake" responses that come to the plugin container * from external sources, like RHQ server. These are usually mocked out using the facilities * provided by JMock. * <p> * This method together with {@link #setServerSideFake(String, Object)} provides a generic * "storage" for tests to share these objects and is only provided as a convenience to * the test writers. There's nothing that would manadate using it. * * @param name * @return */ public static Object getServerSideFake(String name) { return SERVERSIDE_FAKES.get(name); } /** * The opposite of {@link #getServerSideFake(String)}. */ public static void setServerSideFake(String name, Object object) { SERVERSIDE_FAKES.put(name, object); } /** * Returns the {@link PluginContainerConfiguration} as configured using * the {@link PluginContainerSetup} annotation on the current test (or null * if no such thing is configured). * @return */ public static PluginContainerConfiguration getCurrentPluginContainerConfiguration() { return STATICALLY_ACCESSIBLE_PLUGIN_CONTAINER_CONFIGURATION.get(); } @Override protected void initBeforeTest(Object testObject, Method testMethod) { super.initBeforeTest(testObject, testMethod); // clear any current thread interruptions Thread.interrupted(); CURRENT_SETUP.set(getSetup(testMethod)); if (CURRENT_SETUP.get() != null) { try { initPluginContainerConfiguration(testObject, testMethod); initDirectoryStructure(); deployPlugins(testObject, pluginContainerConfiguration.getPluginDirectory(), CURRENT_SETUP.get()); PluginContainer.getInstance().setConfiguration(pluginContainerConfiguration); if (CURRENT_SETUP.get().clearInventoryDat()) { File inventoryDat = new File(pluginContainerConfiguration.getDataDirectory(), "inventory.dat"); inventoryDat.delete(); } if (CURRENT_SETUP.get().startImmediately()) { startConfiguredPluginContainer(); } } catch (Throwable t) { throw new IllegalStateException("Failed to setup the plugin container.", t); } } } @Override protected void tearDownAfterTest(ITestResult testResult) { if (CURRENT_SETUP.get() != null) { try { PluginContainer.getInstance().shutdown(); } finally { try { deletePlugins(pluginContainerConfiguration.getPluginDirectory()); } catch (Throwable t) { //hmmm } } } super.tearDownAfterTest(testResult); } /** * Starts the plugin container. This method is supposed to be called from tests that * need to start the plugin container manually after it has been configured using * the {@link PluginContainerSetup} annotation. */ public static void startConfiguredPluginContainer() { PluginContainer.getInstance().setConfiguration(getCurrentPluginContainerConfiguration()); PluginContainer.getInstance().initialize(); InventoryManager im = PluginContainer.getInstance().getInventoryManager(); for (int i = 0; i < CURRENT_SETUP.get().numberOfInitialDiscoveries(); ++i) { im.executeServerScanImmediately(); im.executeServiceScanImmediately(); } } /** * This method can be called in the after methods of the tests to clear up data * left after the plugin container run. * <p> * This is not done automatically to support sharing the plugin container among * multiple tests to simulate upgrades, etc. * * @throws IOException */ public static void clearStorageOfCurrentPluginContainer() throws IOException { File f = STATICALLY_ACCESSIBLE_PLUGIN_CONTAINER_CONFIGURATION.get().getPluginDirectory(); f = f.getParentFile(); if (f.exists()) { FileUtil.purge(f, false); //FileUtils.cleanDirectory(f); f.delete(); } } /** * This method clears the storage of all tests made. This is useful in {@link AfterSuite} * method to clean up after all the tests (annotated with {@link PluginContainerSetup}) * that have been run. * * @throws IOException */ public synchronized static void clearStorage() throws IOException { if (!cleanedUp) { FileUtil.purge(ROOT, false); //FileUtils.cleanDirectory(ROOT); ROOT.delete(); cleanedUp = true; } } /** * If PluginContainerTest is used as a base class to your tests (and not as a {@link Listeners listener}, * this method is provided to automatically clean up after all the plugin container tests that ran * in the test suite. * <p> * If you use PluginContainerTest as a listener, you have to call {@link #clearStorage()} method * on your own. * * @throws IOException */ @AfterSuite public void cleanUpAfterPluginContainerTests() throws IOException { clearStorage(); } /** * This method returns the {@link PluginContainerConfiguration} that will be used in * the current test as was configured by the PluginContainerSetup annotation. * <p> * If your test class inherits from PluginContainerTest, you can override this method * to provide custom configuration. * * @param testObject the object of the current test * @param testMethod the test method currently being executed on the test object * @return */ protected PluginContainerConfiguration createPluginConfigurationToUse(Object testObject, Method testMethod) { PluginContainerSetup setup = CURRENT_SETUP.get(); if (setup.pluginConfigurationProviderMethod().isEmpty()) { return createDefaultPluginConfiguration(setup, context); } else { String providerName = setup.pluginConfigurationProviderMethod(); Class<?> testClass = testMethod.getDeclaringClass(); try { Method pluginContainerProvider = testClass.getMethod(providerName, (Class<?>[]) null); if (!PluginContainerConfiguration.class.isAssignableFrom(pluginContainerProvider.getReturnType())) { throw new IllegalStateException("The configured pluginConfigurationProviderMethod '" + providerName + "' on the test class '" + testClass + "' does not return a PluginContainerConfiguration."); } return (PluginContainerConfiguration) pluginContainerProvider.invoke(testObject, (Object[]) null); } catch (SecurityException e) { throw new IllegalStateException("The configured pluginConfigurationProviderMethod '" + providerName + "' could not be found on the test class '" + testClass + "'.", e); } catch (NoSuchMethodException e) { throw new IllegalStateException("The configured pluginConfigurationProviderMethod '" + providerName + "' could not be found on the test class '" + testClass + "'.", e); } catch (IllegalAccessException e) { throw new IllegalStateException("Failed to invoke method '" + providerName + "' on test class '" + testClass + "'.", e); } catch (InvocationTargetException e) { throw new IllegalStateException("Failed to invoke method '" + providerName + "' on test class '" + testClass + "'.", e); } } } /** * This method is called after the test to tear down resources associated with the * current plugin container configuration. */ protected void tearDownPluginContainerConfiguration() { pluginContainerConfiguration = null; STATICALLY_ACCESSIBLE_PLUGIN_CONTAINER_CONFIGURATION.set(null); } private void initPluginContainerConfiguration(Object testObject, Method testMethod) { pluginContainerConfiguration = createPluginConfigurationToUse(testObject, testMethod); STATICALLY_ACCESSIBLE_PLUGIN_CONTAINER_CONFIGURATION.set(pluginContainerConfiguration); } private static PluginContainerSetup getSetup(Method method) { PluginContainerSetup setup = method.getAnnotation(PluginContainerSetup.class); if (setup == null) { setup = method.getDeclaringClass().getAnnotation(PluginContainerSetup.class); } return setup; } private void initDirectoryStructure() { File pluginDir = pluginContainerConfiguration.getPluginDirectory(); File dataDir = pluginContainerConfiguration.getDataDirectory(); File tempDir = pluginContainerConfiguration.getTemporaryDirectory(); pluginDir.mkdirs(); dataDir.mkdirs(); tempDir.mkdirs(); } private static PluginContainerConfiguration createDefaultPluginConfiguration(PluginContainerSetup setup, Mockery context) { PluginContainerConfiguration conf = new PluginContainerConfiguration(); File tmpDir = createTemporaryDirectory(setup); conf.setPluginDirectory(new File(tmpDir, PLUGINS_DIR_NAME)); conf.setDataDirectory(new File(tmpDir, DATA_DIR_NAME)); conf.setTemporaryDirectory(new File(tmpDir, TMP_DIR_NAME)); conf.setInsideAgent(setup.inAgent()); conf.setPluginFinder(new FileSystemPluginFinder(conf.getPluginDirectory())); conf.setCreateResourceClassloaders(false); //we're not interested in any scans happening out of our control conf.setAvailabilityScanInitialDelay(Long.MAX_VALUE); conf.setConfigurationDiscoveryInitialDelay(Long.MAX_VALUE); conf.setContentDiscoveryInitialDelay(Long.MAX_VALUE); conf.setEventSenderInitialDelay(Long.MAX_VALUE); conf.setMeasurementCollectionInitialDelay(Long.MAX_VALUE); conf.setServerDiscoveryInitialDelay(Long.MAX_VALUE); conf.setServiceDiscoveryInitialDelay(Long.MAX_VALUE); ServerServices serverServices = new ServerServices(); serverServices.setBundleServerService(context.mock(BundleServerService.class)); serverServices.setConfigurationServerService(context.mock(ConfigurationServerService.class)); serverServices.setContentServerService(context.mock(ContentServerService.class)); serverServices.setCoreServerService(context.mock(CoreServerService.class)); serverServices.setDiscoveryServerService(context.mock(DiscoveryServerService.class)); serverServices.setEventServerService(context.mock(EventServerService.class)); serverServices.setMeasurementServerService(context.mock(MeasurementServerService.class)); serverServices.setOperationServerService(context.mock(OperationServerService.class)); serverServices.setResourceFactoryServerService(context.mock(ResourceFactoryServerService.class)); serverServices.setDriftServerService(context.mock(DriftServerService.class)); conf.setServerServices(serverServices); return conf; } private void deployPlugins(Object testObject, File destination, PluginContainerSetup setup) throws IOException { for (String plugin : setup.plugins()) { copyPluginToDestination(testObject, plugin, destination); } } // Note that on WINDOWS this does not always/usually work and therefore test failures // tend to happen. For unknown reasons windows/jvm keeps a lock on the plugin jar and it // fails to delete, thus allowing for multiple versions of the plugin in the same path. I've // added a system out to make it more clear in the log. There was no obvious workaround. private void deletePlugins(File deployDirectory) throws IOException { if (deployDirectory.exists()) { FileUtil.purge(deployDirectory, true); } if (deployDirectory.exists()) { throw new IllegalStateException("Failed to clean up plugins in [" + deployDirectory.getPath() + "]"); } } private static File createTemporaryDirectory(PluginContainerSetup setup) { String name; boolean mustBeNew = true; if (setup.sharedGroup().length() > 0) { name = setup.sharedGroup(); mustBeNew = false; } else { name = Long.toString(System.currentTimeMillis()); } File ret = new File(ROOT, name); while (mustBeNew && (ret.exists() || !ret.mkdir())) { ret = new File(ROOT, Long.toString(System.currentTimeMillis())); } return ret; } private File copyPluginToDestination(Object testObject, String plugin, File destination) throws IOException { URI pluginUri = URI.create(plugin); URL pluginUrl; if ("classpath".equals(pluginUri.getScheme())) { String path = pluginUri.getPath(); pluginUrl = testObject.getClass().getResource(path); if (pluginUrl == null) throw new IOException("could not find " + pluginUri); } else { pluginUrl = pluginUri.toURL(); } String pluginFileName = pluginUrl.getPath().substring(pluginUrl.getPath().lastIndexOf('/') + 1); File pluginJar = new File(destination, pluginFileName); FileUtils.copyURLToFile(pluginUrl, pluginJar); return pluginJar; } }