001/* 002 * (C) Copyright 2006-2018 Nuxeo (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * Nuxeo - initial API and implementation 018 */ 019package org.nuxeo.runtime.test; 020 021import static org.junit.Assert.assertEquals; 022import static org.junit.Assert.fail; 023 024import java.io.IOException; 025import java.net.URL; 026import java.util.ArrayList; 027import java.util.Enumeration; 028import java.util.List; 029 030import org.apache.commons.io.FileUtils; 031import org.apache.commons.logging.Log; 032import org.apache.commons.logging.LogFactory; 033import org.jmock.Mockery; 034import org.jmock.integration.junit4.JUnit4Mockery; 035import org.junit.After; 036import org.junit.Before; 037import org.junit.Ignore; 038import org.junit.runner.RunWith; 039import org.nuxeo.runtime.AbstractRuntimeService; 040import org.nuxeo.runtime.RuntimeServiceException; 041import org.nuxeo.runtime.api.Framework; 042import org.nuxeo.runtime.osgi.OSGiRuntimeService; 043import org.nuxeo.runtime.test.runner.ConditionalIgnoreRule; 044import org.nuxeo.runtime.test.runner.Features; 045import org.nuxeo.runtime.test.runner.FeaturesRunner; 046import org.nuxeo.runtime.test.runner.MDCFeature; 047import org.nuxeo.runtime.test.runner.RandomBug; 048 049/** 050 * Abstract base class for test cases that require a test runtime service. 051 * <p> 052 * The runtime service itself is conveniently available as the <code>runtime</code> instance variable in derived 053 * classes. 054 * <p> 055 * <b>Warning:</b> NXRuntimeTestCase subclasses <b>must</b> 056 * <ul> 057 * <li>not declare they own @Before and @After. 058 * <li>override doSetUp and doTearDown (and postSetUp if needed) instead of setUp and tearDown. 059 * <li>never call deployXXX methods outside the doSetUp method. 060 * </ul> 061 * 062 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> 063 * @deprecated since 10.2 this class <b>must</b> not be subclassed anymore, for RuntimeHarness implementation use 064 * {@code RuntimeHarnessImpl} 065 */ 066// Make sure this class is kept in sync with with RuntimeHarness 067@RunWith(FeaturesRunner.class) 068@Features({ MDCFeature.class, ConditionalIgnoreRule.Feature.class, RandomBug.Feature.class }) 069@Ignore 070@Deprecated 071public class NXRuntimeTestCase extends RuntimeHarnessImpl { 072 073 protected Mockery jmcontext = new JUnit4Mockery(); 074 075 static { 076 // jul to jcl redirection may pose problems (infinite loops) in some 077 // environment 078 // where slf4j to jul, and jcl over slf4j is deployed 079 System.setProperty(AbstractRuntimeService.REDIRECT_JUL, "false"); 080 } 081 082 private static final Log log = LogFactory.getLog(NXRuntimeTestCase.class); 083 084 protected boolean restart = false; 085 086 protected List<String[]> deploymentStack = new ArrayList<>(); 087 088 /** 089 * Set to true when the instance of this class is a JUnit test case. Set to false when the instance of this class is 090 * instantiated by the FeaturesRunner to manage the framework If the class is a JUnit test case then the runtime 091 * components will be started at the end of the setUp method 092 */ 093 protected final boolean isTestUnit; 094 095 /** 096 * Used when subclassing to create standalone test cases 097 */ 098 public NXRuntimeTestCase() { 099 super(); 100 isTestUnit = true; 101 } 102 103 /** 104 * Used by the features runner to manage the Nuxeo framework 105 */ 106 public NXRuntimeTestCase(Class<?> clazz) { 107 super(clazz); 108 isTestUnit = false; 109 } 110 111 /** 112 * Restarts the runtime and preserve homes directory. 113 */ 114 @Override 115 public void restart() throws Exception { 116 restart = true; 117 try { 118 tearDown(); 119 setUp(); 120 } finally { 121 restart = false; 122 } 123 } 124 125 @Override 126 public void start() throws Exception { 127 startRuntime(); 128 } 129 130 @Before 131 public void startRuntime() throws Exception { 132 System.setProperty("org.nuxeo.runtime.testing", "true"); 133 wipeRuntime(); 134 initUrls(); 135 if (urls == null) { 136 throw new UnsupportedOperationException("no bundles available"); 137 } 138 initOsgiRuntime(); 139 setUp(); // let a chance to the subclasses to contribute bundles and/or components 140 if (isTestUnit) { // if this class is running as a test case start the runtime components 141 fireFrameworkStarted(); 142 } 143 postSetUp(); 144 } 145 146 /** 147 * Implementors should override this method to setup tests and not the {@link #startRuntime()} method. This method 148 * should contain all the bundle or component deployments needed by the tests. At the time this method is called the 149 * components are not yet started. If you need to perform component/service lookups use instead the 150 * {@link #postSetUp()} method 151 */ 152 protected void setUp() throws Exception { // NOSONAR 153 } 154 155 /** 156 * Implementors should override this method to implement any specific test tear down and not the 157 * {@link #stopRuntime()} method 158 */ 159 protected void tearDown() throws Exception { // NOSONAR 160 deploymentStack = new ArrayList<>(); 161 } 162 163 /** 164 * Called after framework was started (at the end of setUp). Implementors may use this to use deployed services to 165 * initialize fields etc. 166 */ 167 protected void postSetUp() throws Exception { // NOSONAR 168 } 169 170 @After 171 public void stopRuntime() throws Exception { 172 tearDown(); 173 wipeRuntime(); 174 if (workingDir != null && !restart) { 175 if (workingDir.exists() && !FileUtils.deleteQuietly(workingDir)) { 176 log.warn("Cannot delete " + workingDir); 177 } 178 workingDir = null; 179 } 180 readUris = null; 181 bundles = null; 182 } 183 184 @Override 185 public void stop() throws Exception { 186 stopRuntime(); 187 } 188 189 protected OSGiRuntimeService handleNewRuntime(OSGiRuntimeService aRuntime) { 190 return aRuntime; 191 } 192 193 public static URL getResource(String name) { 194 final ClassLoader loader = Thread.currentThread().getContextClassLoader(); 195 String callerName = Thread.currentThread().getStackTrace()[2].getClassName(); 196 final String relativePath = callerName.replace('.', '/').concat(".class"); 197 final String fullPath = loader.getResource(relativePath).getPath(); 198 final String basePath = fullPath.substring(0, fullPath.indexOf(relativePath)); 199 Enumeration<URL> resources; 200 try { 201 resources = loader.getResources(name); 202 while (resources.hasMoreElements()) { 203 URL resource = resources.nextElement(); 204 if (resource.getPath().startsWith(basePath)) { 205 return resource; 206 } 207 } 208 } catch (IOException e) { 209 return null; 210 } 211 return loader.getResource(name); 212 } 213 214 protected void deployContrib(URL url) { 215 assertEquals(runtime, Framework.getRuntime()); 216 log.info("Deploying contribution from " + url.toString()); 217 try { 218 runtime.getContext().deploy(url); 219 } catch (Exception e) { 220 fail("Failed to deploy contrib " + url.toString()); 221 } 222 } 223 224 /** 225 * Deploy a contribution specified as a "bundleName:path" uri 226 */ 227 public void deployContrib(String uri) throws Exception { 228 int i = uri.indexOf(':'); 229 if (i == -1) { 230 throw new IllegalArgumentException( 231 "Invalid deployment URI: " + uri + ". Must be of the form bundleSymbolicName:pathInBundleJar"); 232 } 233 deployContrib(uri.substring(0, i), uri.substring(i + 1)); 234 } 235 236 public void undeployContrib(String uri) throws Exception { 237 int i = uri.indexOf(':'); 238 if (i == -1) { 239 throw new IllegalArgumentException( 240 "Invalid deployment URI: " + uri + ". Must be of the form bundleSymbolicName:pathInBundleJar"); 241 } 242 undeployContrib(uri.substring(0, i), uri.substring(i + 1)); 243 } 244 245 protected static boolean isVersionSuffix(String s) { 246 if (s.length() == 0) { 247 return true; 248 } 249 return s.matches("-(\\d+\\.?)+(-SNAPSHOT)?(\\.\\w+)?"); 250 } 251 252 /** 253 * Resolves an URL for bundle deployment code. 254 * <p> 255 * TODO: Implementation could be finer... 256 * 257 * @return the resolved url 258 */ 259 protected URL lookupBundleUrl(String bundle) { // NOSONAR 260 for (URL url : urls) { 261 String[] pathElts = url.getPath().split("/"); 262 for (int i = 0; i < pathElts.length; i++) { 263 if (pathElts[i].startsWith(bundle) && isVersionSuffix(pathElts[i].substring(bundle.length()))) { 264 // we want the main version of the bundle 265 boolean isTestVersion = false; 266 for (int j = i + 1; j < pathElts.length; j++) { 267 // ok for Eclipse (/test) and Maven (/test-classes) 268 if (pathElts[j].startsWith("test")) { 269 isTestVersion = true; 270 break; 271 } 272 } 273 if (!isTestVersion) { 274 log.info("Resolved " + bundle + " as " + url.toString()); 275 return url; 276 } 277 } 278 } 279 } 280 throw new RuntimeServiceException("Could not resolve bundle " + bundle); 281 } 282 283 /** 284 * Should be called by subclasses after one or more inline deployments are made inside a test method. Without 285 * calling this the inline deployment(s) will not have any effects. 286 * <p> 287 * <b>Be Warned</b> that if you reference runtime services or components you should lookup them again after calling 288 * this method! 289 * <p> 290 * This method also calls {@link #postSetUp()} for convenience. 291 */ 292 protected void applyInlineDeployments() throws Exception { 293 runtime.getComponentManager().refresh(false); 294 runtime.getComponentManager().start(); // make sure components are started 295 postSetUp(); 296 } 297 298 /** 299 * Should be called by subclasses to remove any inline deployments made in the current test method. 300 * <p> 301 * <b>Be Warned</b> that if you reference runtime services or components you should lookup them again after calling 302 * this method! 303 * <p> 304 * This method also calls {@link #postSetUp()} for convenience. 305 */ 306 protected void removeInlineDeployments() throws Exception { 307 runtime.getComponentManager().reset(); 308 runtime.getComponentManager().start(); 309 postSetUp(); 310 } 311 312 /** 313 * Hot deploy the given components (identified by an URI). All the started components are stopped, the new ones are 314 * registered and then all components are started. You can undeploy these components by calling 315 * {@link #popInlineDeployments()} 316 * <p> 317 * A component URI is of the form: bundleSymbolicName:pathToComponentXmlInBundle 318 */ 319 public void pushInlineDeployments(String... deploymentUris) throws Exception { 320 deploymentStack.add(deploymentUris); 321 for (String uri : deploymentUris) { 322 deployContrib(uri); 323 } 324 applyInlineDeployments(); 325 } 326 327 /** 328 * Remove the latest deployed components using {@link #pushInlineDeployments(String...)}. 329 */ 330 public void popInlineDeployments() throws Exception { 331 if (deploymentStack.isEmpty()) { 332 throw new IllegalStateException("deployment stack is empty"); 333 } 334 popInlineDeployments(deploymentStack.size() - 1); 335 } 336 337 public void popInlineDeployments(int index) throws Exception { 338 if (index < 0 || index > deploymentStack.size() - 1) { 339 throw new IllegalStateException("deployment stack index is invalid: " + index); 340 } 341 deploymentStack.remove(index); 342 343 runtime.getComponentManager().reset(); 344 for (String[] ar : deploymentStack) { 345 for (String element : ar) { 346 deployContrib(element); 347 } 348 } 349 applyInlineDeployments(); 350 } 351 352}