You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by ac...@apache.org on 2020/03/25 10:22:24 UTC

[camel-karaf] 03/05: Added camel-test-karaf

This is an automated email from the ASF dual-hosted git repository.

acosentino pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/camel-karaf.git

commit c4471396fb5c85449945313568f96ed865a84bbd
Author: Andrea Cosentino <an...@gmail.com>
AuthorDate: Wed Mar 25 10:20:10 2020 +0100

    Added camel-test-karaf
---
 components/camel-test-karaf/pom.xml                | 216 ++++++++++
 .../services/org/apache/camel/other.properties     |   7 +
 .../src/generated/resources/test-karaf.json        |  14 +
 .../camel-test-karaf/src/main/docs/test-karaf.adoc |  10 +
 .../camel/test/karaf/AbstractFeatureTest.java      | 479 +++++++++++++++++++++
 .../camel/test/karaf/CamelKarafTestSupport.java    | 390 +++++++++++++++++
 components/pom.xml                                 |   1 +
 7 files changed, 1117 insertions(+)

diff --git a/components/camel-test-karaf/pom.xml b/components/camel-test-karaf/pom.xml
new file mode 100644
index 0000000..fae9c3a
--- /dev/null
+++ b/components/camel-test-karaf/pom.xml
@@ -0,0 +1,216 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    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.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.camel.karaf</groupId>
+        <artifactId>components</artifactId>
+        <version>3.2.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>camel-test-karaf</artifactId>
+    <packaging>jar</packaging>
+
+    <name>Camel Karaf :: Test :: Karaf</name>
+    <description>Camel integration testing with Apache Karaf</description>
+
+    <properties>
+        <firstVersion>2.18.0</firstVersion>
+        <label>testing,java,osgi</label>
+
+        <karf-test-version>${karaf4-version}</karf-test-version>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.ops4j.pax.exam</groupId>
+            <artifactId>pax-exam-junit4</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.ops4j.pax.exam</groupId>
+            <artifactId>pax-exam</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.ops4j.pax.exam</groupId>
+            <artifactId>pax-exam-container-karaf</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.ops4j.pax.exam</groupId>
+            <artifactId>pax-exam-link-mvn</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>javax.inject</groupId>
+            <artifactId>javax.inject</artifactId>
+            <version>1</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.karaf.features</groupId>
+            <artifactId>org.apache.karaf.features.core</artifactId>
+            <version>${karaf4-version}</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.karaf</groupId>
+            <artifactId>apache-karaf</artifactId>
+            <version>${karf-test-version}</version>
+            <type>tar.gz</type>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.configadmin</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.gogo.runtime</artifactId>
+            <version>1.1.2</version>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>osgi.core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>osgi.cmpn</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-support</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-test</artifactId>
+        </dependency>
+
+        <!-- test -->
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <pluginManagement>
+            <plugins>
+                <!-- Eclipse m2e Lifecycle Management -->
+                <plugin>
+                    <groupId>org.eclipse.m2e</groupId>
+                    <artifactId>lifecycle-mapping</artifactId>
+                    <version>${lifecycle-mapping-version}</version>
+                    <configuration>
+                        <lifecycleMappingMetadata>
+                            <pluginExecutions>
+                                <pluginExecution>
+                                    <pluginExecutionFilter>
+                                        <groupId>org.apache.servicemix.tooling</groupId>
+                                        <artifactId>depends-maven-plugin</artifactId>
+                                        <versionRange>${depends-maven-plugin-version}</versionRange>
+                                        <goals>
+                                            <goal>generate-depends-file</goal>
+                                        </goals>
+                                    </pluginExecutionFilter>
+                                    <action>
+                                        <ignore />
+                                    </action>
+                                </pluginExecution>
+                            </pluginExecutions>
+                        </lifecycleMappingMetadata>
+                    </configuration>
+                </plugin>
+            </plugins>
+        </pluginManagement>
+        <plugins>
+            <!-- generate dependencies versions -->
+            <plugin>
+                <groupId>org.apache.servicemix.tooling</groupId>
+                <artifactId>depends-maven-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>generate-depends-file</id>
+                        <goals>
+                            <goal>generate-depends-file</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+
+            <plugin>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <configuration>
+                    <!-- do not re-run these tests -->
+                    <rerunFailingTestsCount>0</rerunFailingTestsCount>
+                    <systemPropertyVariables>
+                        <karafVersion>${karaf4-version}</karafVersion>
+                    </systemPropertyVariables>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.camel</groupId>
+                <artifactId>camel-bundle-plugin</artifactId>
+                <configuration>
+                    <instructions>
+                        <Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName>
+                        <!-- Export-Package>org.apache.camel.test.karaf</Export-Package -->
+                        <DynamicImport-Package>*</DynamicImport-Package>
+                        <Import-Package />
+                        <_removeheaders>Import-Package, Private-Package, Include-Resource, Karaf-Info,
+                            Require-Capability
+                        </_removeheaders>
+                    </instructions>
+                </configuration>
+            </plugin>
+
+        </plugins>
+    </build>
+
+    <profiles>
+        <profile>
+            <id>ci-build-profile</id>
+            <activation>
+                <property>
+                    <name>maven.repo.local</name>
+                </property>
+            </activation>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-surefire-plugin</artifactId>
+                        <configuration>
+                            <forkedProcessTimeoutInSeconds>300</forkedProcessTimeoutInSeconds>
+                            <!--
+                                when the local repo location has been specified, we need to pass
+                                on this information to PAX mvn url
+                            -->
+                            <argLine>-Dorg.ops4j.pax.url.mvn.localRepository=${maven.repo.local}</argLine>
+                            <systemPropertyVariables>
+                                <karafVersion>${karaf4-version}</karafVersion>
+                            </systemPropertyVariables>
+                        </configuration>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+
+    </profiles>
+</project>
diff --git a/components/camel-test-karaf/src/generated/resources/META-INF/services/org/apache/camel/other.properties b/components/camel-test-karaf/src/generated/resources/META-INF/services/org/apache/camel/other.properties
new file mode 100644
index 0000000..f3258d5
--- /dev/null
+++ b/components/camel-test-karaf/src/generated/resources/META-INF/services/org/apache/camel/other.properties
@@ -0,0 +1,7 @@
+# Generated by camel build tools - do NOT edit this file!
+name=test-karaf
+groupId=org.apache.camel.karaf
+artifactId=camel-test-karaf
+version=3.2.0-SNAPSHOT
+projectName=Camel Karaf :: Test :: Karaf
+projectDescription=Camel integration testing with Apache Karaf
diff --git a/components/camel-test-karaf/src/generated/resources/test-karaf.json b/components/camel-test-karaf/src/generated/resources/test-karaf.json
new file mode 100644
index 0000000..8fee4cc
--- /dev/null
+++ b/components/camel-test-karaf/src/generated/resources/test-karaf.json
@@ -0,0 +1,14 @@
+{
+  "other": {
+    "kind": "other",
+    "name": "test-karaf",
+    "title": "Test Karaf",
+    "description": "Camel integration testing with Apache Karaf",
+    "deprecated": false,
+    "firstVersion": "2.18.0",
+    "label": "testing,java,osgi",
+    "groupId": "org.apache.camel.karaf",
+    "artifactId": "camel-test-karaf",
+    "version": "3.2.0-SNAPSHOT"
+  }
+}
diff --git a/components/camel-test-karaf/src/main/docs/test-karaf.adoc b/components/camel-test-karaf/src/main/docs/test-karaf.adoc
new file mode 100644
index 0000000..5668522
--- /dev/null
+++ b/components/camel-test-karaf/src/main/docs/test-karaf.adoc
@@ -0,0 +1,10 @@
+= Test Karaf
+
+*Available since Camel 2.18*
+
+Camel testing using Apache Karaf with Pax-Exam.
+
+This component allows to perform integration testing by running Karaf containers using Pax-Exam.
+
+The component is in development and needs some more polish to be ready.
+In addition there is also need for documentation.
diff --git a/components/camel-test-karaf/src/main/java/org/apache/camel/test/karaf/AbstractFeatureTest.java b/components/camel-test-karaf/src/main/java/org/apache/camel/test/karaf/AbstractFeatureTest.java
new file mode 100644
index 0000000..4297bdb
--- /dev/null
+++ b/components/camel-test-karaf/src/main/java/org/apache/camel/test/karaf/AbstractFeatureTest.java
@@ -0,0 +1,479 @@
+/*
+ * 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.camel.test.karaf;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Dictionary;
+import java.util.EnumSet;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Locale;
+import java.util.Properties;
+import java.util.function.Consumer;
+
+import javax.inject.Inject;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Component;
+import org.apache.camel.spi.DataFormat;
+import org.apache.camel.spi.Language;
+import org.apache.karaf.features.FeaturesService;
+import org.junit.After;
+import org.junit.Before;
+import org.ops4j.pax.exam.CoreOptions;
+import org.ops4j.pax.exam.Option;
+import org.ops4j.pax.exam.ProbeBuilder;
+import org.ops4j.pax.exam.TestProbeBuilder;
+import org.ops4j.pax.exam.karaf.container.internal.JavaVersionUtil;
+import org.ops4j.pax.exam.karaf.options.KarafDistributionOption;
+import org.ops4j.pax.exam.karaf.options.LogLevelOption;
+import org.ops4j.pax.exam.options.UrlReference;
+import org.ops4j.pax.exam.options.extra.VMOption;
+import org.ops4j.pax.tinybundles.core.TinyBundle;
+import org.ops4j.pax.tinybundles.core.TinyBundles;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.Constants;
+import org.osgi.framework.Filter;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.blueprint.container.BlueprintContainer;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.osgi.util.tracker.ServiceTracker;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.junit.Assert.assertNotNull;
+import static org.ops4j.pax.exam.CoreOptions.maven;
+import static org.ops4j.pax.exam.CoreOptions.mavenBundle;
+import static org.ops4j.pax.exam.CoreOptions.systemProperty;
+import static org.ops4j.pax.exam.CoreOptions.vmOption;
+import static org.ops4j.pax.exam.CoreOptions.when;
+import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.configureConsole;
+import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.editConfigurationFilePut;
+import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.features;
+import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.keepRuntimeFolder;
+import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.logLevel;
+
+public abstract class AbstractFeatureTest {
+
+    public static final Long SERVICE_TIMEOUT = 30000L;
+    protected static final Logger LOG = LoggerFactory.getLogger(AbstractFeatureTest.class);
+
+    @Inject
+    protected BundleContext bundleContext;
+
+    @Inject
+    protected BlueprintContainer blueprintContainer;
+
+    @Inject
+    protected FeaturesService featuresService;
+
+    @ProbeBuilder
+    public TestProbeBuilder probeConfiguration(TestProbeBuilder probe) {
+        // makes sure the generated Test-Bundle contains this import!
+        probe.setHeader(Constants.DYNAMICIMPORT_PACKAGE, "*");
+        return probe;
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        LOG.info("setUp() using BundleContext: {}", bundleContext);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        LOG.info("tearDown()");
+    }
+
+    protected Bundle installBlueprintAsBundle(String name, URL url, boolean start) throws BundleException {
+        return installBlueprintAsBundle(name, url, start, bundle -> { });
+    }
+
+    protected Bundle installBlueprintAsBundle(String name, URL url, boolean start, Consumer<Object> consumer) throws BundleException {
+        // TODO Type Consumer<TinyBundle> cannot be used for this method signature to avoid bundle dependency to pax tinybundles
+        TinyBundle bundle = TinyBundles.bundle();
+        bundle.add("OSGI-INF/blueprint/blueprint-" + name.toLowerCase(Locale.ENGLISH) + ".xml", url);
+        bundle.set("Manifest-Version", "2")
+                .set("Bundle-ManifestVersion", "2")
+                .set("Bundle-SymbolicName", name)
+                .set("Bundle-Version", "1.0.0")
+                .set(Constants.DYNAMICIMPORT_PACKAGE, "*");
+        consumer.accept(bundle);
+        Bundle answer = bundleContext.installBundle(name, bundle.build());
+
+        if (start) {
+            answer.start();
+        }
+        return answer;
+    }
+
+    protected Bundle installSpringAsBundle(String name, URL url, boolean start) throws BundleException {
+        return installSpringAsBundle(name, url, start, bundle -> { });
+    }
+
+    protected Bundle installSpringAsBundle(String name, URL url, boolean start, Consumer<Object> consumer) throws BundleException {
+        // TODO Type Consumer<TinyBundle> cannot be used for this method signature to avoid bundle dependency to pax tinybundles
+        TinyBundle bundle = TinyBundles.bundle();
+        bundle.add("META-INF/spring/spring-" + name.toLowerCase(Locale.ENGLISH) + ".xml", url);
+        bundle.set("Manifest-Version", "2")
+                .set("Bundle-ManifestVersion", "2")
+                .set("Bundle-SymbolicName", name)
+                .set("Bundle-Version", "1.0.0");
+        consumer.accept(bundle);
+        Bundle answer = bundleContext.installBundle(name, bundle.build());
+
+        if (start) {
+            answer.start();
+        }
+        return answer;
+    }
+
+    protected void installCamelFeature(String mainFeature) throws Exception {
+        if (!mainFeature.startsWith("camel-")) {
+            mainFeature = "camel-" + mainFeature;
+        }
+        LOG.info("Install main feature: {}", mainFeature);
+        // do not refresh bundles causing out bundle context to be invalid
+        // TODO: see if we can find a way maybe to install camel.xml as bundle/feature instead of part of unit test (see src/test/resources/OSGI-INF/blueprint)
+        featuresService.installFeature(mainFeature, EnumSet.of(FeaturesService.Option.NoAutoRefreshBundles));
+    }
+
+    protected void overridePropertiesWithConfigAdmin(String pid, Properties props) throws IOException {
+        ConfigurationAdmin configAdmin = getOsgiService(bundleContext, ConfigurationAdmin.class);
+        // passing null as second argument ties the configuration to correct bundle.
+        Configuration config = configAdmin.getConfiguration(pid, null);
+        if (config == null) {
+            throw new IllegalArgumentException("Cannot find configuration with pid " + pid + " in OSGi ConfigurationAdmin service.");
+        }
+
+        // let's merge configurations
+        Dictionary<String, Object> currentProperties = config.getProperties();
+        Dictionary newProps = new Properties();
+        if (currentProperties == null) {
+            currentProperties = newProps;
+        }
+        for (Enumeration<String> ek = currentProperties.keys(); ek.hasMoreElements();) {
+            String k = ek.nextElement();
+            newProps.put(k, currentProperties.get(k));
+        }
+        for (String p : props.stringPropertyNames()) {
+            newProps.put(p, props.getProperty(p));
+        }
+
+        LOG.info("Updating ConfigAdmin {} by overriding properties {}", config, newProps);
+        config.update(newProps);
+    }
+
+    protected void testComponent(String component) throws Exception {
+        testComponent("camel-" + component, component);
+    }
+
+    protected void testComponent(String mainFeature, String component) throws Exception {
+        LOG.info("Looking up CamelContext(myCamel) in OSGi Service Registry");
+
+        installCamelFeature(mainFeature);
+
+        CamelContext camelContext = getOsgiService(bundleContext, CamelContext.class, "(camel.context.name=myCamel)", SERVICE_TIMEOUT);
+        assertNotNull("Cannot find CamelContext with name myCamel", camelContext);
+
+        LOG.info("Getting Camel component: {}", component);
+        // do not auto start the component as it may not have been configured properly and fail in its start method
+        Component comp = camelContext.getComponent(component, true, false);
+        assertNotNull("Cannot get component with name: " + component, comp);
+
+        LOG.info("Found Camel component: {} instance: {} with className: {}", component, comp, comp.getClass());
+    }
+
+    protected void testDataFormat(String dataFormat) throws Exception {
+        testDataFormat("camel-" + dataFormat, dataFormat);
+    }
+
+    protected void testDataFormat(String mainFeature, String dataFormat) throws Exception {
+        LOG.info("Looking up CamelContext(myCamel) in OSGi Service Registry");
+
+        installCamelFeature(mainFeature);
+
+        CamelContext camelContext = getOsgiService(bundleContext, CamelContext.class, "(camel.context.name=myCamel)", SERVICE_TIMEOUT);
+        assertNotNull("Cannot find CamelContext with name myCamel", camelContext);
+
+        LOG.info("Getting Camel dataformat: {}", dataFormat);
+        DataFormat df = camelContext.resolveDataFormat(dataFormat);
+        assertNotNull("Cannot get dataformat with name: " + dataFormat, df);
+
+        LOG.info("Found Camel dataformat: {} instance: {} with className: {}", dataFormat, df, df.getClass());
+    }
+
+    protected void testLanguage(String language) throws Exception {
+        testLanguage("camel-" + language, language);
+    }
+
+    protected void testLanguage(String mainFeature, String language) throws Exception {
+        LOG.info("Looking up CamelContext(myCamel) in OSGi Service Registry");
+
+        installCamelFeature(mainFeature);
+
+        CamelContext camelContext = getOsgiService(bundleContext, CamelContext.class, "(camel.context.name=myCamel)", 20000);
+        assertNotNull("Cannot find CamelContext with name myCamel", camelContext);
+
+        LOG.info("Getting Camel language: {}", language);
+        Language lan = camelContext.resolveLanguage(language);
+        assertNotNull("Cannot get language with name: " + language, lan);
+
+        LOG.info("Found Camel language: {} instance: {} with className: {}", language, lan, lan.getClass());
+    }
+
+    public static String extractName(Class<?> clazz) {
+        String name = clazz.getName();
+        int id0 = name.indexOf("Camel") + "Camel".length();
+        int id1 = name.indexOf("Test");
+        StringBuilder sb = new StringBuilder();
+        for (int i = id0; i < id1; i++) {
+            char c = name.charAt(i);
+            if (Character.isUpperCase(c) && sb.length() > 0) {
+                sb.append("-");
+            }
+            sb.append(Character.toLowerCase(c));
+        }
+        return sb.toString();
+    }
+
+    public static UrlReference getCamelKarafFeatureUrl() {
+        return mavenBundle().
+                groupId("org.apache.camel.karaf").
+                artifactId("apache-camel").
+                version(getCamelKarafFeatureVersion()).
+                type("xml/features");
+    }
+
+    private static String getCamelKarafFeatureVersion() {
+        String camelKarafFeatureVersion = System.getProperty("camelKarafFeatureVersion");
+        if (camelKarafFeatureVersion == null) {
+            throw new RuntimeException("Please specify the maven artifact version to use for org.apache.camel.karaf/apache-camel through the camelKarafFeatureVersion System property");
+        }
+        return camelKarafFeatureVersion;
+    }
+
+    private static void switchPlatformEncodingToUTF8() {
+        try {
+            System.setProperty("file.encoding", "UTF-8");
+            Field charset = Charset.class.getDeclaredField("defaultCharset");
+            charset.setAccessible(true);
+            charset.set(null, null);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static String getKarafVersion() {
+        InputStream ins = AbstractFeatureTest.class.getResourceAsStream("/META-INF/maven/dependencies.properties");
+        Properties p = new Properties();
+        try {
+            p.load(ins);
+        } catch (Throwable t) {
+            // ignore
+        }
+        String karafVersion = p.getProperty("org.apache.karaf/apache-karaf/version");
+        if (karafVersion == null) {
+            karafVersion = System.getProperty("karafVersion");
+        }
+        if (karafVersion == null) {
+            // setup the default version of it
+            karafVersion = "4.1.0";
+        }
+        return karafVersion;
+    }
+
+    public static Option[] configure(String... extra) {
+
+        List<String> camel = new ArrayList<>();
+        camel.add("camel");
+        if (extra != null && extra.length > 0) {
+            for (String e : extra) {
+                camel.add(e);
+            }
+        }
+        final String[] camelFeatures = camel.toArray(new String[camel.size()]);
+
+        switchPlatformEncodingToUTF8();
+        String karafVersion = getKarafVersion();
+        LOG.info("*** Apache Karaf version is " + karafVersion + " ***");
+
+        Option[] options = new Option[]{
+            // for remote debugging
+//            new VMOption("-Xdebug"),
+//            new VMOption("-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5008"),
+
+            KarafDistributionOption.karafDistributionConfiguration()
+                    .frameworkUrl(maven().groupId("org.apache.karaf").artifactId("apache-karaf").type("tar.gz").versionAsInProject())
+                    .karafVersion(karafVersion)
+                    .name("Apache Karaf")
+                    .useDeployFolder(false).unpackDirectory(new File("target/paxexam/unpack/")),
+            logLevel(LogLevelOption.LogLevel.INFO),
+
+            // keep the folder so we can look inside when something fails
+            keepRuntimeFolder(),
+
+            // Disable the SSH port
+            configureConsole().ignoreRemoteShell(),
+
+            // need to modify the jre.properties to export some com.sun packages that some features rely on
+//            KarafDistributionOption.replaceConfigurationFile("etc/jre.properties", new File("src/test/resources/jre.properties")),
+
+            vmOption("-Dfile.encoding=UTF-8"),
+
+            // Disable the Karaf shutdown port
+            editConfigurationFilePut("etc/custom.properties", "karaf.shutdown.port", "-1"),
+
+            // log config
+            editConfigurationFilePut("etc/custom.properties", "karaf.log", "${karaf.data}/log"),
+
+            // Assign unique ports for Karaf
+//            editConfigurationFilePut("etc/org.ops4j.pax.web.cfg", "org.osgi.service.http.port", Integer.toString(AvailablePortFinder.getNextAvailable())),
+//            editConfigurationFilePut("etc/org.apache.karaf.management.cfg", "rmiRegistryPort", Integer.toString(AvailablePortFinder.getNextAvailable())),
+//            editConfigurationFilePut("etc/org.apache.karaf.management.cfg", "rmiServerPort", Integer.toString(AvailablePortFinder.getNextAvailable())),
+
+            // install junit
+            CoreOptions.junitBundles(),
+
+            // install camel
+            features(getCamelKarafFeatureUrl(), camelFeatures),
+
+            // install camel-test-karaf as bundle (not feature as the feature causes a bundle refresh that invalidates the @Inject bundleContext)
+            mavenBundle().groupId("org.apache.camel").artifactId("camel-test-karaf").versionAsInProject(),
+            when(JavaVersionUtil.getMajorVersion() >= 9)
+                    .useOptions(
+                    systemProperty("pax.exam.osgi.`unresolved.fail").value("true"),
+                    systemProperty("java.awt.headless").value("true"),
+                    new VMOption("--add-reads=java.xml=java.logging"),
+                    new VMOption("--add-exports=java.base/org.apache.karaf.specs.locator=java.xml,ALL-UNNAMED"),
+                    new VMOption("--patch-module"),
+                    new VMOption("java.base=lib/endorsed/org.apache.karaf.specs.locator-"
+                            + System.getProperty("karafVersion", "4.2.4") + ".jar"),
+                    new VMOption("--patch-module"),
+                    new VMOption("java.xml=lib/endorsed/org.apache.karaf.specs.java.xml-"
+                            + System.getProperty("karafVersion", "4.2.4") + ".jar"),
+                    new VMOption("--add-opens"),
+                    new VMOption("java.base/java.security=ALL-UNNAMED"),
+                    new VMOption("--add-opens"),
+                    new VMOption("java.base/java.net=ALL-UNNAMED"),
+                    new VMOption("--add-opens"),
+                    new VMOption("java.base/java.lang=ALL-UNNAMED"),
+                    new VMOption("--add-opens"),
+                    new VMOption("java.base/java.util=ALL-UNNAMED"),
+                    new VMOption("--add-opens"),
+                    new VMOption("java.naming/javax.naming.spi=ALL-UNNAMED"),
+                    new VMOption("--add-opens"),
+                    new VMOption("java.rmi/sun.rmi.transport.tcp=ALL-UNNAMED"),
+                    new VMOption("--add-exports=java.base/sun.net.www.protocol.http=ALL-UNNAMED"),
+                    new VMOption("--add-exports=java.base/sun.net.www.protocol.https=ALL-UNNAMED"),
+                    new VMOption("--add-exports=java.base/sun.net.www.protocol.jar=ALL-UNNAMED"),
+                    new VMOption("--add-exports=jdk.naming.rmi/com.sun.jndi.url.rmi=ALL-UNNAMED"),
+                    new VMOption("-classpath"),
+                    new VMOption("lib/jdk9plus/*" + File.pathSeparator + "lib/boot/*")
+            )
+        };
+
+        return options;
+    }
+
+    protected <T> T getOsgiService(BundleContext bundleContext, Class<T> type) {
+        return getOsgiService(bundleContext, type, null, SERVICE_TIMEOUT);
+    }
+
+    protected <T> T getOsgiService(BundleContext bundleContext, Class<T> type, long timeout) {
+        return getOsgiService(bundleContext, type, null, timeout);
+    }
+
+    @SuppressWarnings("unchecked")
+    public static <T> T getOsgiService(BundleContext bundleContext, Class<T> type, String filter, long timeout) {
+        ServiceTracker tracker;
+        try {
+            String flt;
+            if (filter != null) {
+                if (filter.startsWith("(")) {
+                    flt = "(&(" + Constants.OBJECTCLASS + "=" + type.getName() + ")" + filter + ")";
+                } else {
+                    flt = "(&(" + Constants.OBJECTCLASS + "=" + type.getName() + ")(" + filter + "))";
+                }
+            } else {
+                flt = "(" + Constants.OBJECTCLASS + "=" + type.getName() + ")";
+            }
+            Filter osgiFilter = FrameworkUtil.createFilter(flt);
+            tracker = new ServiceTracker(bundleContext, osgiFilter, null);
+            tracker.open(true);
+            // Note that the tracker is not closed to keep the reference
+            // This is buggy, as the service reference may change i think
+            Object svc = tracker.waitForService(timeout);
+
+            if (svc == null) {
+                Dictionary<?, ?> dic = bundleContext.getBundle().getHeaders();
+                LOG.warn("Test bundle headers: " + explode(dic));
+
+                for (ServiceReference ref : asCollection(bundleContext.getAllServiceReferences(null, null))) {
+                    LOG.warn("ServiceReference: " + ref + ", bundle: " + ref.getBundle() + ", symbolicName: " + ref.getBundle().getSymbolicName());
+                }
+
+                for (ServiceReference ref : asCollection(bundleContext.getAllServiceReferences(null, flt))) {
+                    LOG.warn("Filtered ServiceReference: " + ref + ", bundle: " + ref.getBundle() + ", symbolicName: " + ref.getBundle().getSymbolicName());
+                }
+
+                throw new RuntimeException("Gave up waiting for service " + flt);
+            }
+            return type.cast(svc);
+        } catch (InvalidSyntaxException e) {
+            throw new IllegalArgumentException("Invalid filter", e);
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Explode the dictionary into a <code>,</code> delimited list of <code>key=value</code> pairs.
+     */
+    private static String explode(Dictionary<?, ?> dictionary) {
+        Enumeration<?> keys = dictionary.keys();
+        StringBuilder result = new StringBuilder();
+        while (keys.hasMoreElements()) {
+            Object key = keys.nextElement();
+            result.append(String.format("%s=%s", key, dictionary.get(key)));
+            if (keys.hasMoreElements()) {
+                result.append(", ");
+            }
+        }
+        return result.toString();
+    }
+
+    /**
+     * Provides an iterable collection of references, even if the original array is <code>null</code>.
+     */
+    private static Collection<ServiceReference> asCollection(ServiceReference[] references) {
+        return references == null ? new ArrayList<>(0) : Arrays.asList(references);
+    }
+
+}
diff --git a/components/camel-test-karaf/src/main/java/org/apache/camel/test/karaf/CamelKarafTestSupport.java b/components/camel-test-karaf/src/main/java/org/apache/camel/test/karaf/CamelKarafTestSupport.java
new file mode 100644
index 0000000..992af1e
--- /dev/null
+++ b/components/camel-test-karaf/src/main/java/org/apache/camel/test/karaf/CamelKarafTestSupport.java
@@ -0,0 +1,390 @@
+/*
+ * 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.camel.test.karaf;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.PrintStream;
+import java.net.URL;
+import java.security.Principal;
+import java.security.PrivilegedExceptionAction;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+
+import javax.inject.Inject;
+import javax.management.remote.JMXConnector;
+import javax.management.remote.JMXConnectorFactory;
+import javax.management.remote.JMXServiceURL;
+import javax.security.auth.Subject;
+
+import org.apache.camel.RuntimeCamelException;
+import org.apache.camel.test.junit4.CamelTestSupport;
+import org.apache.felix.service.command.CommandProcessor;
+import org.apache.felix.service.command.CommandSession;
+import org.apache.karaf.features.Feature;
+import org.apache.karaf.features.FeaturesService;
+import org.ops4j.pax.exam.Option;
+import org.ops4j.pax.exam.ProbeBuilder;
+import org.ops4j.pax.exam.TestProbeBuilder;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.Filter;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.util.tracker.ServiceTracker;
+
+public class CamelKarafTestSupport extends CamelTestSupport {
+
+    static final Long COMMAND_TIMEOUT = 30000L;
+    static final Long SERVICE_TIMEOUT = 30000L;
+
+    protected ExecutorService executor = Executors.newCachedThreadPool();
+
+    @Inject
+    protected BundleContext bundleContext;
+
+    @Inject
+    protected FeaturesService featuresService;
+
+    @ProbeBuilder
+    public TestProbeBuilder probeConfiguration(TestProbeBuilder probe) {
+        probe.setHeader(Constants.DYNAMICIMPORT_PACKAGE, "*,org.apache.felix.service.*;status=provisional");
+        return probe;
+    }
+
+    public File getConfigFile(String path) {
+        URL res = this.getClass().getResource(path);
+        if (res == null) {
+            throw new RuntimeException("Config resource " + path + " not found");
+        }
+        return new File(res.getFile());
+    }
+
+    public static Option[] configure(String... extra) {
+        return AbstractFeatureTest.configure(extra);
+    }
+
+    /**
+     * Executes a shell command and returns output as a String.
+     * Commands have a default timeout of 10 seconds.
+     *
+     * @param command The command to execute
+     * @param principals The principals (e.g. RolePrincipal objects) to run the command under
+     */
+    protected String executeCommand(final String command, Principal... principals) {
+        return executeCommand(command, COMMAND_TIMEOUT, false, principals);
+    }
+
+    /**
+     * Executes a shell command and returns output as a String.
+     * Commands have a default timeout of 10 seconds.
+     *
+     * @param command    The command to execute.
+     * @param timeout    The amount of time in millis to wait for the command to execute.
+     * @param silent     Specifies if the command should be displayed in the screen.
+     * @param principals The principals (e.g. RolePrincipal objects) to run the command under
+     */
+    protected String executeCommand(final String command, final Long timeout, final Boolean silent, final Principal... principals) {
+
+        waitForCommandService(command);
+        String response;
+        final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+        final PrintStream printStream = new PrintStream(byteArrayOutputStream);
+        final Callable<String> commandCallable = new Callable<String>() {
+            @Override
+            public String call() throws Exception {
+                try {
+                    if (!silent) {
+                        System.err.println(command);
+                    }
+                    final CommandProcessor commandProcessor = getOsgiService(CommandProcessor.class);
+                    final CommandSession commandSession = commandProcessor.createSession(System.in, printStream, System.err);
+                    commandSession.execute(command);
+                } catch (Exception e) {
+                    throw new RuntimeException(e.getMessage(), e);
+                }
+                printStream.flush();
+                return byteArrayOutputStream.toString();
+            }
+        };
+
+        FutureTask<String> commandFuture;
+        if (principals.length == 0) {
+            commandFuture = new FutureTask<>(commandCallable);
+        } else {
+            // If principals are defined, run the command callable via Subject.doAs()
+            commandFuture = new FutureTask<>(new Callable<String>() {
+                @Override
+                public String call() throws Exception {
+                    Subject subject = new Subject();
+                    subject.getPrincipals().addAll(Arrays.asList(principals));
+                    return Subject.doAs(subject, new PrivilegedExceptionAction<String>() {
+                        @Override
+                        public String run() throws Exception {
+                            return commandCallable.call();
+                        }
+                    });
+                }
+            });
+        }
+
+
+        try {
+            executor.submit(commandFuture);
+            response = commandFuture.get(timeout, TimeUnit.MILLISECONDS);
+        } catch (Exception e) {
+            e.printStackTrace(System.err);
+            response = "SHELL COMMAND TIMED OUT: ";
+        }
+
+        return response;
+    }
+
+    private void waitForCommandService(String command) {
+        // the commands are represented by services. Due to the asynchronous nature of services they may not be
+        // immediately available. This code waits the services to be available, in their secured form. It
+        // means that the code waits for the command service to appear with the roles defined.
+    
+        if (command == null || command.length() == 0) {
+            return;
+        }
+       
+        int spaceIdx = command.indexOf(' ');
+        if (spaceIdx > 0) {
+            command = command.substring(0, spaceIdx);
+        }
+        int colonIndx = command.indexOf(':');
+        
+        try {
+            if (colonIndx > 0) {
+                String scope = command.substring(0, colonIndx);
+                String function = command.substring(colonIndx + 1);
+                waitForService("(&(osgi.command.scope=" + scope + ")(osgi.command.function=" + function + "))", SERVICE_TIMEOUT);
+            } else {
+                waitForService("(osgi.command.function=" + command + ")", SERVICE_TIMEOUT);
+            }
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private void waitForService(String filter, long timeout) throws InvalidSyntaxException,
+        InterruptedException {
+        
+        ServiceTracker st = new ServiceTracker(bundleContext,
+                                               bundleContext.createFilter(filter),
+                                               null);
+        try {
+            st.open();
+            st.waitForService(timeout);
+        } finally {
+            st.close();
+        }
+    }
+
+    protected <T> T getOsgiService(Class<T> type, long timeout) {
+        return getOsgiService(type, null, timeout);
+    }
+
+    protected <T> T getOsgiService(Class<T> type) {
+        return getOsgiService(type, null, SERVICE_TIMEOUT);
+    }
+
+    @SuppressWarnings("unchecked")
+    protected <T> T getOsgiService(Class<T> type, String filter, long timeout) {
+        ServiceTracker tracker = null;
+        try {
+            String flt;
+            if (filter != null) {
+                if (filter.startsWith("(")) {
+                    flt = "(&(" + Constants.OBJECTCLASS + "=" + type.getName() + ")" + filter + ")";
+                } else {
+                    flt = "(&(" + Constants.OBJECTCLASS + "=" + type.getName() + ")(" + filter + "))";
+                }
+            } else {
+                flt = "(" + Constants.OBJECTCLASS + "=" + type.getName() + ")";
+            }
+            Filter osgiFilter = FrameworkUtil.createFilter(flt);
+            tracker = new ServiceTracker(bundleContext, osgiFilter, null);
+            tracker.open(true);
+            // Note that the tracker is not closed to keep the reference
+            // This is buggy, as the service reference may change i think
+            Object svc = type.cast(tracker.waitForService(timeout));
+            if (svc == null) {
+                Dictionary dic = bundleContext.getBundle().getHeaders();
+                System.err.println("Test bundle headers: " + explode(dic));
+
+                for (ServiceReference ref : asCollection(bundleContext.getAllServiceReferences(null, null))) {
+                    System.err.println("ServiceReference: " + ref);
+                }
+
+                for (ServiceReference ref : asCollection(bundleContext.getAllServiceReferences(null, flt))) {
+                    System.err.println("Filtered ServiceReference: " + ref);
+                }
+
+                throw new RuntimeException("Gave up waiting for service " + flt);
+            }
+            return type.cast(svc);
+        } catch (InvalidSyntaxException e) {
+            throw new IllegalArgumentException("Invalid filter", e);
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /*
+    * Explode the dictionary into a ,-delimited list of key=value pairs
+    */
+    private static String explode(Dictionary dictionary) {
+        Enumeration keys = dictionary.keys();
+        StringBuilder sb = new StringBuilder();
+        while (keys.hasMoreElements()) {
+            Object key = keys.nextElement();
+            sb.append(String.format("%s=%s", key, dictionary.get(key)));
+            if (keys.hasMoreElements()) {
+                sb.append(", ");
+            }
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Provides an iterable collection of references, even if the original array is null
+     */
+    private static Collection<ServiceReference> asCollection(ServiceReference[] references) {
+        return references != null ? Arrays.asList(references) : Collections.<ServiceReference>emptyList();
+    }
+
+    public JMXConnector getJMXConnector() throws Exception {
+        return getJMXConnector("karaf", "karaf");
+    }
+
+    public JMXConnector getJMXConnector(String userName, String passWord) throws Exception {
+        JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:1099/karaf-root");
+        Hashtable<String, Object> env = new Hashtable<>();
+        String[] credentials = new String[]{userName, passWord};
+        env.put("jmx.remote.credentials", credentials);
+        JMXConnector connector = JMXConnectorFactory.connect(url, env);
+        return connector;
+    }
+
+    public void assertFeatureInstalled(String featureName) {
+        try {
+            Feature[] features = featuresService.listInstalledFeatures();
+            for (Feature feature : features) {
+                if (featureName.equals(feature.getName())) {
+                    return;
+                }
+            }
+            fail("Feature " + featureName + " should be installed but is not");
+        } catch (Exception e) {
+            throw RuntimeCamelException.wrapRuntimeCamelException(e);
+        }
+    }
+
+    public void assertFeatureInstalled(String featureName, String featureVersion) {
+        try {
+            Feature[] features = featuresService.listInstalledFeatures();
+            for (Feature feature : features) {
+                if (featureName.equals(feature.getName()) && featureVersion.equals(feature.getVersion())) {
+                    return;
+                }
+            }
+            fail("Feature " + featureName + "/" + featureVersion + " should be installed but is not");
+        } catch (Exception e) {
+            throw RuntimeCamelException.wrapRuntimeCamelException(e);
+        }
+    }
+    
+    protected void installAndAssertFeature(String feature) throws Exception {
+        featuresService.installFeature(feature);
+        assertFeatureInstalled(feature);
+    }
+
+    protected void installAndAssertFeature(String feature, String version) throws Exception {
+        featuresService.installFeature(feature, version);
+        assertFeatureInstalled(feature, version);
+    }
+
+    protected void installAssertAndUninstallFeature(String feature) throws Exception {
+        Set<Feature> featuresBefore = new HashSet<>(Arrays.asList(featuresService.listInstalledFeatures()));
+        try {
+            featuresService.installFeature(feature);
+            assertFeatureInstalled(feature);
+        } finally {
+            uninstallNewFeatures(featuresBefore);
+        }
+    }
+
+    protected void installAssertAndUninstallFeature(String feature, String version) throws Exception {
+        Set<Feature> featuresBefore = new HashSet<>(Arrays.asList(featuresService.listInstalledFeatures()));
+        try {
+            featuresService.installFeature(feature, version);
+            assertFeatureInstalled(feature, version);
+        } finally {
+            uninstallNewFeatures(featuresBefore);
+        }
+    }
+
+    protected void installAssertAndUninstallFeatures(String... feature) throws Exception {
+        Set<Feature> featuresBefore = new HashSet<>(Arrays.asList(featuresService.listInstalledFeatures()));
+        try {
+            for (String curFeature : feature) {
+                featuresService.installFeature(curFeature);
+                assertFeatureInstalled(curFeature);
+            }
+        } finally {
+            uninstallNewFeatures(featuresBefore);
+        }
+    }
+
+    /**
+     * The feature service does not uninstall feature dependencies when uninstalling a single feature.
+     * So we need to make sure we uninstall all features that were newly installed.
+     */
+    protected void uninstallNewFeatures(Set<Feature> featuresBefore) {
+        try {
+            Feature[] features = featuresService.listInstalledFeatures();
+            for (Feature curFeature : features) {
+                if (!featuresBefore.contains(curFeature)) {
+                    try {
+                        System.out.println("Uninstalling " + curFeature.getName());
+                        featuresService.uninstallFeature(curFeature.getName(), curFeature.getVersion());
+                    } catch (Exception e) {
+                        // ignore
+                    }
+                }
+            }
+        } catch (Exception e) {
+            throw RuntimeCamelException.wrapRuntimeCamelException(e);
+        }
+    }
+
+}
diff --git a/components/pom.xml b/components/pom.xml
index a02240d..c68fc35 100644
--- a/components/pom.xml
+++ b/components/pom.xml
@@ -40,6 +40,7 @@
         <module>camel-kura</module>
         <module>camel-osgi-activator</module>
         <module>camel-paxlogging</module>
+        <module>camel-test-karaf</module>
     </modules>
 
     <properties>