| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.sling.junit.impl; |
| |
| import java.net.URL; |
| import java.util.ArrayList; |
| import java.util.Enumeration; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.regex.Pattern; |
| import java.util.regex.PatternSyntaxException; |
| |
| import org.apache.felix.scr.annotations.Component; |
| import org.apache.felix.scr.annotations.Service; |
| import org.apache.sling.junit.TestsProvider; |
| import org.osgi.framework.Bundle; |
| import org.osgi.framework.BundleContext; |
| import org.osgi.framework.BundleEvent; |
| import org.osgi.framework.BundleListener; |
| import org.osgi.service.component.ComponentContext; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** A TestProvider that gets test classes from bundles |
| * that have a Sling-Test-Regexp header and corresponding |
| * exported classes. |
| */ |
| @Component |
| @Service |
| public class BundleTestsProvider implements TestsProvider, BundleListener { |
| private final Logger log = LoggerFactory.getLogger(getClass()); |
| private static final String COMPONENT_NAME = "component.name"; |
| private long lastModified; |
| private BundleContext bundleContext; |
| private String componentName; |
| |
| public static final String SLING_TEST_REGEXP = "Sling-Test-Regexp"; |
| |
| /** Symbolic names of bundles that changed state - if not empty, need |
| * to adjust the list of tests |
| */ |
| private final List<String> changedBundles = new ArrayList<String>(); |
| |
| /** List of (candidate) test classes, keyed by bundle so that we can |
| * update them easily when bundles come and go |
| */ |
| private final Map<String, List<String>> testClassesMap = new HashMap<String, List<String>>(); |
| |
| protected void activate(ComponentContext ctx) { |
| bundleContext = ctx.getBundleContext(); |
| bundleContext.addBundleListener(this); |
| |
| // Initially consider all bundles as "changed" |
| for(Bundle b : bundleContext.getBundles()) { |
| if(getSlingTestRegexp(b) != null) { |
| changedBundles.add(b.getSymbolicName()); |
| log.debug("Will look for test classes inside bundle {}", b.getSymbolicName()); |
| } |
| } |
| |
| lastModified = System.currentTimeMillis(); |
| componentName = (String)ctx.getProperties().get(COMPONENT_NAME); |
| } |
| |
| protected void deactivate(ComponentContext ctx) { |
| bundleContext.removeBundleListener(this); |
| bundleContext = null; |
| changedBundles.clear(); |
| } |
| |
| @Override |
| public String toString() { |
| return getClass().getSimpleName() + ", componentName(pid)=" + componentName; |
| } |
| |
| /** Update testClasses if bundle changes require it */ |
| private void maybeUpdateTestClasses() { |
| if(changedBundles.isEmpty()) { |
| return; |
| } |
| |
| // Get the list of bundles that have changed |
| final List<String> bundlesToUpdate = new ArrayList<String>(); |
| synchronized (changedBundles) { |
| bundlesToUpdate.addAll(changedBundles); |
| changedBundles.clear(); |
| } |
| |
| // Remove test classes that belong to changed bundles |
| for(String symbolicName : bundlesToUpdate) { |
| testClassesMap.remove(symbolicName); |
| } |
| |
| // Get test classes from bundles that are in our list |
| for(Bundle b : bundleContext.getBundles()) { |
| if(bundlesToUpdate.contains(b.getSymbolicName())) { |
| final List<String> testClasses = getTestClasses(b); |
| if(testClasses != null) { |
| testClassesMap.put(b.getSymbolicName(), testClasses); |
| log.debug("{} test classes found in bundle {}, added to our list", |
| testClasses.size(), b.getSymbolicName()); |
| } else { |
| log.debug("No test classes found in bundle {}", b.getSymbolicName()); |
| } |
| } |
| } |
| } |
| |
| /** Called when a bundle changes state */ |
| public void bundleChanged(BundleEvent event) { |
| // Only consider bundles which contain tests |
| final Bundle b = event.getBundle(); |
| if(getSlingTestRegexp(b) == null) { |
| log.debug("Bundle {} does not have {} header, ignored", |
| b.getSymbolicName(), SLING_TEST_REGEXP); |
| return; |
| } |
| synchronized (changedBundles) { |
| log.debug("Got BundleEvent for Bundle {}, will rebuild its lists of tests"); |
| changedBundles.add(b.getSymbolicName()); |
| } |
| lastModified = System.currentTimeMillis(); |
| } |
| |
| private String getSlingTestRegexp(Bundle b) { |
| return (String)b.getHeaders().get(SLING_TEST_REGEXP); |
| } |
| |
| /** Get test classes that bundle b provides (as done in Felix/Sigil) */ |
| private List<String> getTestClasses(Bundle b) { |
| final List<String> result = new ArrayList<String>(); |
| Pattern testClassRegexp = null; |
| final String headerValue = getSlingTestRegexp(b); |
| if (headerValue != null) { |
| try { |
| testClassRegexp = Pattern.compile(headerValue); |
| } |
| catch (PatternSyntaxException pse) { |
| log.warn("Invalid pattern '" + headerValue + "' for bundle " |
| + b.getSymbolicName() + ", ignored", pse); |
| } |
| } |
| |
| if (testClassRegexp == null) { |
| log.info("Bundle {} does not have {} header, not looking for test classes", SLING_TEST_REGEXP); |
| } else if (Bundle.ACTIVE != b.getState()) { |
| log.info("Bundle {} is not active, no test classes considered", b.getSymbolicName()); |
| } else { |
| @SuppressWarnings("unchecked") |
| Enumeration<URL> classUrls = b.findEntries("", "*.class", true); |
| while (classUrls.hasMoreElements()) { |
| URL url = classUrls.nextElement(); |
| final String name = toClassName(url); |
| if(testClassRegexp.matcher(name).matches()) { |
| result.add(name); |
| } else { |
| log.debug("Class {} does not match {} pattern {} of bundle {}, ignored", |
| new Object[] { name, SLING_TEST_REGEXP, testClassRegexp, b.getSymbolicName() }); |
| } |
| } |
| log.info("{} test classes found in bundle {}", result.size(), b.getSymbolicName()); |
| } |
| |
| return result; |
| } |
| |
| /** Convert class URL to class name */ |
| private String toClassName(URL url) { |
| final String f = url.getFile(); |
| final String cn = f.substring(1, f.length() - ".class".length()); |
| return cn.replace('/', '.'); |
| } |
| |
| /** Find bundle by symbolic name */ |
| private Bundle findBundle(String symbolicName) { |
| for(Bundle b : bundleContext.getBundles()) { |
| if(b.getSymbolicName().equals(symbolicName)) { |
| return b; |
| } |
| } |
| return null; |
| } |
| |
| public Class<?> createTestClass(String testName) throws ClassNotFoundException { |
| // Find the bundle to which the class belongs |
| Bundle b = null; |
| for(Map.Entry<String, List<String>> e : testClassesMap.entrySet()) { |
| if(e.getValue().contains(testName)) { |
| b = findBundle(e.getKey()); |
| break; |
| } |
| } |
| |
| if(b == null) { |
| throw new IllegalArgumentException("No Bundle found that supplies test class " + testName); |
| } |
| return b.loadClass(testName); |
| } |
| |
| public long lastModified() { |
| return lastModified; |
| } |
| |
| public String getServicePid() { |
| return componentName; |
| } |
| |
| public List<String> getTestNames() { |
| maybeUpdateTestClasses(); |
| final List<String> result = new ArrayList<String>(); |
| for(List<String> list : testClassesMap.values()) { |
| result.addAll(list); |
| } |
| return result; |
| } |
| } |