blob: d4367da13baa2e10229c9c791b2b54f5fadb9ed1 [file] [log] [blame]
/*
* 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;
}
}