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 * bstefanescu 018 */ 019package org.nuxeo.runtime.test.runner; 020 021import java.lang.annotation.Annotation; 022import java.lang.reflect.Method; 023import java.net.URL; 024import java.util.Arrays; 025import java.util.Collection; 026import java.util.HashMap; 027import java.util.HashSet; 028import java.util.LinkedList; 029import java.util.List; 030import java.util.Map; 031import java.util.Set; 032 033import javax.inject.Inject; 034 035import org.junit.rules.MethodRule; 036import org.junit.runners.model.FrameworkMethod; 037import org.junit.runners.model.Statement; 038import org.nuxeo.runtime.model.ComponentManager; 039import org.nuxeo.runtime.model.RuntimeContext; 040import org.nuxeo.runtime.osgi.OSGiRuntimeService; 041import org.osgi.framework.Bundle; 042 043import com.google.common.collect.Multimaps; 044import com.google.common.collect.SetMultimap; 045 046/** 047 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> 048 */ 049public class RuntimeDeployment { 050 051 Set<String> bundles = new HashSet<>(); 052 053 Map<String, Set<TargetExtensions>> partialBundles = new HashMap<>(); 054 055 Map<String, Collection<String>> mainContribs = new HashMap<>(); 056 057 SetMultimap<String, String> mainIndex = Multimaps.newSetMultimap(mainContribs, HashSet::new); 058 059 /** 060 * @deprecated since 10.1 061 */ 062 @Deprecated 063 Map<String, Collection<String>> localContribs = new HashMap<>(); 064 065 /** 066 * @deprecated since 10.1 067 */ 068 @Deprecated 069 SetMultimap<String, String> localIndex = Multimaps.newSetMultimap(localContribs, HashSet::new); 070 071 /** 072 * @deprecated since 9.2 we cannot undeploy components while they are started. So we don't need anymore to store the 073 * contexts 074 */ 075 @Deprecated 076 protected LinkedList<RuntimeContext> contexts = new LinkedList<>(); 077 078 protected void index(Class<?> clazz) { 079 AnnotationScanner scanner = FeaturesRunner.scanner; 080 scanner.scan(clazz); 081 List<? extends Annotation> annos = scanner.getAnnotations(clazz); 082 if (annos == null) { 083 return; 084 } 085 for (Annotation anno : annos) { 086 if (anno.annotationType() == Deploy.class) { 087 index((Deploy) anno); 088 } else if (anno.annotationType() == Deploys.class) { 089 index((Deploys) anno); 090 } else if (anno.annotationType() == LocalDeploy.class) { 091 index((LocalDeploy) anno); 092 } else if (anno.annotationType() == PartialDeploy.class) { 093 index((PartialDeploy) anno); 094 } 095 } 096 } 097 098 protected void index(RunnerFeature feature) { 099 index(feature.getClass()); 100 } 101 102 protected void index(Method method) { 103 index(method.getAnnotation(Deploy.class)); 104 index(method.getAnnotation(Deploys.class)); 105 index(method.getAnnotation(LocalDeploy.class)); 106 } 107 108 protected void index(Deploy config) { 109 if (config == null) { 110 return; 111 } 112 for (String each : config.value()) { 113 index(each, mainIndex); 114 } 115 } 116 117 private void index(Deploys deploys) { 118 if (deploys == null) { 119 return; 120 } 121 for (Deploy value : deploys.value()) { 122 index(value); 123 } 124 } 125 126 /** 127 * @deprecated since 10.1, use {@link #index(Deploy)} 128 */ 129 @Deprecated 130 protected void index(LocalDeploy config) { 131 if (config == null) { 132 return; 133 } 134 for (String each : config.value()) { 135 index(each, localIndex); 136 } 137 } 138 139 /** 140 * @since 9.1 141 */ 142 protected void index(PartialDeploy config) { 143 if (config == null) { 144 return; 145 } 146 147 Set<TargetExtensions> pairs = partialBundles.computeIfAbsent(config.bundle(), key -> new HashSet<>()); 148 Arrays.stream(config.extensions()).map(c -> { 149 try { 150 return c.newInstance(); 151 } catch (ReflectiveOperationException e) { 152 throw new IllegalStateException(e); 153 } 154 }).forEach(pairs::add); 155 } 156 157 protected void index(Features features) { 158 for (Class<?> each : features.value()) { 159 index(each); 160 } 161 } 162 163 protected void index(String directive, SetMultimap<String, String> contribs) { 164 int sepIndex = directive.indexOf(':'); 165 if (sepIndex == -1) { 166 bundles.add(directive); 167 } else { 168 String bundle = directive.substring(0, sepIndex); 169 String resource = directive.substring(sepIndex + 1); 170 contribs.put(bundle, resource); 171 } 172 } 173 174 protected void deploy(FeaturesRunner runner, RuntimeHarness harness) { 175 AssertionError errors = new AssertionError("deployment errors"); 176 OSGiRuntimeService runtime = (OSGiRuntimeService) harness.getContext().getRuntime(); 177 for (String name : bundles) { 178 Bundle bundle = harness.getOSGiAdapter().getBundle(name); 179 if (bundle == null) { 180 try { 181 harness.deployBundle(name); 182 bundle = harness.getOSGiAdapter().getBundle(name); 183 if (bundle == null) { 184 throw new UnsupportedOperationException("Should not occur"); 185 } 186 } catch (Exception error) { 187 errors.addSuppressed(error); 188 continue; 189 } 190 contexts.add(runtime.getContext(bundle)); 191 } 192 try { 193 // deploy bundle contribs 194 for (String resource : mainIndex.removeAll(name)) { 195 try { 196 harness.deployContrib(name, resource); 197 } catch (Exception error) { 198 errors.addSuppressed(error); 199 } 200 } 201 // deploy local contribs 202 // this block is dreprecated since 10.1 with @LocalDeploy 203 for (String resource : localIndex.removeAll(name)) { 204 URL url = runner.getTargetTestResource(resource); 205 if (url == null) { 206 url = bundle.getEntry(resource); 207 } 208 if (url == null) { 209 url = runner.getTargetTestClass().getClassLoader().getResource(resource); 210 } 211 if (url == null) { 212 throw new AssertionError("Cannot find " + resource + " in " + name); 213 } 214 contexts.add(harness.deployTestContrib(name, url)); 215 } 216 } catch (Exception error) { 217 errors.addSuppressed(error); 218 } 219 } 220 221 for (Map.Entry<String, String> resource : mainIndex.entries()) { 222 try { 223 harness.deployContrib(resource.getKey(), resource.getValue()); 224 } catch (Exception error) { 225 errors.addSuppressed(error); 226 } 227 } 228 // this block is dreprecated since 10.1 with @LocalDeploy 229 for (Map.Entry<String, String> resource : localIndex.entries()) { 230 try { 231 contexts.add(harness.deployTestContrib(resource.getKey(), resource.getValue())); 232 } catch (Exception error) { 233 errors.addSuppressed(error); 234 } 235 } 236 237 for (Map.Entry<String, Set<TargetExtensions>> resource : partialBundles.entrySet()) { 238 try { 239 contexts.add(harness.deployPartial(resource.getKey(), resource.getValue())); 240 } catch (Exception e) { 241 errors.addSuppressed(e); 242 } 243 } 244 245 if (errors.getSuppressed().length > 0) { 246 throw errors; 247 } 248 249 } 250 251 public static RuntimeDeployment onTest(FeaturesRunner runner) { 252 RuntimeDeployment deployment = new RuntimeDeployment(); 253 deployment.index(runner.getDescription().getTestClass()); 254 for (RunnerFeature each : runner.getFeatures()) { 255 deployment.index(each); 256 } 257 return deployment; 258 } 259 260 public static MethodRule onMethod() { 261 return new OnMethod(); 262 } 263 264 protected static class OnMethod implements MethodRule { 265 266 @Inject 267 protected FeaturesRunner runner; 268 269 @Override 270 public Statement apply(Statement base, FrameworkMethod method, Object target) { 271 RuntimeDeployment deployment = new RuntimeDeployment(); 272 deployment.index(method.getMethod()); 273 return deployment.onStatement(runner, runner.getFeature(RuntimeFeature.class).harness, method, base); 274 } 275 276 } 277 278 protected Statement onStatement(FeaturesRunner runner, RuntimeHarness harness, FrameworkMethod method, 279 Statement base) { 280 return new DeploymentStatement(runner, harness, method, base); 281 } 282 283 protected class DeploymentStatement extends Statement { 284 285 protected final FeaturesRunner runner; 286 287 protected final RuntimeHarness harness; 288 289 // useful for debugging 290 protected final FrameworkMethod method; 291 292 protected final Statement base; 293 294 public DeploymentStatement(FeaturesRunner runner, RuntimeHarness harness, FrameworkMethod method, 295 Statement base) { 296 this.runner = runner; 297 this.harness = harness; 298 this.method = method; 299 this.base = base; 300 } 301 302 protected void tryDeploy() { 303 // the registry is updated here and not using before or teardown methods. 304 // this approach ensure the components are not stopped between tearDown and the next test 305 // (so that custom feature that relly on the runtime between the two tests are not affected by stopping 306 // components) 307 ComponentManager mgr = harness.getContext().getRuntime().getComponentManager(); 308 // the stash may already contains contribs (from @Setup methods) 309 if (mgr.hasChanged()) { // first reset the registry if it was changed by the last test 310 mgr.reset(); 311 // the registry is now stopped 312 } 313 // deploy current test contributions if any 314 deploy(runner, harness); 315 mgr.refresh(true); 316 // now the stash is empty 317 mgr.start(); // ensure components are started 318 } 319 320 @Override 321 public void evaluate() throws Throwable { 322 // make sure the clear the stash 323 tryDeploy(); 324 try { 325 base.evaluate(); 326 } finally { 327 // undeploy cannot be done while the components are started 328 // RuntimeFeature will do a reset if needed 329 // see RuntimeFeature.afterTeardown 330 // undeploy(); 331 } 332 } 333 334 } 335 336}