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