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