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