2 * Copyright (c) 2024 PANTHEON.tech s.r.o. and others. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
8 package org.opendaylight.odlparent.features.test.plugin;
10 import static org.apache.karaf.bundle.core.BundleState.Installed;
11 import static org.apache.karaf.bundle.core.BundleState.Waiting;
15 import java.util.Arrays;
16 import java.util.EnumSet;
17 import java.util.HashMap;
19 import java.util.concurrent.TimeUnit;
20 import java.util.stream.Collectors;
21 import javax.inject.Inject;
22 import org.apache.karaf.bundle.core.BundleService;
23 import org.apache.karaf.bundle.core.BundleState;
24 import org.apache.karaf.features.FeaturesService;
25 import org.junit.Test;
26 import org.osgi.framework.Bundle;
27 import org.osgi.framework.BundleContext;
28 import org.slf4j.Logger;
29 import org.slf4j.LoggerFactory;
32 * The feature test probe artifact.
35 * The class is being packaged and deployed to karaf environment on {@link PaxExamExecution#execute()} invocation.
36 * All dependencies which are absent on target environment expected to be packaged using same
37 * {@link org.ops4j.pax.exam.ProbeBuilder}. Input parameters are passed through system properties. in order to be
38 * delivered properly all affected properties require explicit declaration using associated Pax options -- see
39 * {@link PaxOptionUtils#probePropertiesOptions()}.
42 * Pax Exam module references:
44 * <li>Probe bundle deployment handling is served by pax-exam-extender-service</li>
45 * <li>Service instances lookup and injection into probe instance is served by pax-exam-inject</li>
46 * <li>Test method invocation is served by pax-exam-invoker-junit, uses JUnitCore v.4, which requires @Test
47 * annotation for method to be eligible for invocation</li>
50 public final class TestProbe {
52 static final String FEATURE_FILE_URI_PROP = "feature.test.file.uri";
53 static final String BUNDLE_CHECK_SKIP = "feature.test.bundle.check.skip";
54 static final String BUNDLE_CHECK_TIMEOUT_SECONDS = "feature.test.bundle.check.timeout.seconds";
55 static final String BUNDLE_CHECK_INTERVAL_SECONDS = "feature.test.bundle.check.interval.seconds";
56 static final String DEFAULT_TIMEOUT = "300";
57 static final String DEFAULT_INTERVAL = "1";
59 static final String[] ALL_PROPERTY_KEYS =
60 {FEATURE_FILE_URI_PROP, BUNDLE_CHECK_SKIP, BUNDLE_CHECK_TIMEOUT_SECONDS, BUNDLE_CHECK_INTERVAL_SECONDS};
62 private static final Logger LOG = LoggerFactory.getLogger(TestProbe.class);
63 private static final Map<Integer, String> OSGI_STATES = Map.of(
64 Bundle.INSTALLED, "Installed", Bundle.RESOLVED, "Resolved",
65 Bundle.STARTING, "Starting", Bundle.ACTIVE, "Active",
66 Bundle.STOPPING, "Stopping", Bundle.UNINSTALLED, "Uninstalled");
67 private static final Map<String, BundleState> ELIGIBLE_STATES = Map.of(
68 "slf4j.log4j12", Installed,
69 "org.apache.karaf.scr.management", Waiting);
71 private final Map<Long, CheckResult> bundleCheckResults = new HashMap<>();
74 private BundleContext bundleContext;
77 private FeaturesService featuresService;
80 private BundleService bundleService;
83 * Performs the project feature installation on karaf environment with subsequent state check of deployed bundles.
85 * @throws Exception on probe failure
88 @SuppressWarnings("IllegalCatch")
89 public void testFeature() throws Exception {
94 } catch (Exception e) {
95 LOG.error("Exception executing feature test", e);
100 private void validateServices() {
101 if (bundleContext == null) {
102 throw new IllegalStateException("bundleContext is not initialized");
104 // replace the probe's initial context which expires too fast
105 bundleContext = bundleContext.getBundle(0).getBundleContext();
107 if (featuresService == null) {
108 throw new IllegalStateException("featureService is not initialized");
110 if (bundleService == null) {
111 throw new IllegalStateException("bundleService is not initialized");
115 private void installFeatures() throws Exception {
116 final var featureUri = URI.create(System.getProperty(FEATURE_FILE_URI_PROP));
117 if (!new File(featureUri).exists()) {
118 throw new IllegalStateException("Feature file with URI " + featureUri + " does not exist");
121 // install repository the feature definition can be read from
122 featuresService.addRepository(featureUri);
123 LOG.info("Feature repository with URI: {} initialized", featureUri);
126 for (var feature : featuresService.getRepository(featureUri).getFeatures()) {
127 final var name = feature.getName();
128 final var version = feature.getVersion();
129 LOG.info("Installing feature: {}, {}", name, version);
130 featuresService.installFeature(name, version, EnumSet.of(FeaturesService.Option.Verbose));
131 LOG.info("Feature is installed: {}, isInstalled()={}, getState()={}",
132 name, featuresService.isInstalled(feature), featuresService.getState(feature.getId()));
136 private void checkBundleStates() throws InterruptedException {
137 if ("true".equals(System.getProperty(BUNDLE_CHECK_SKIP))) {
140 final int timeout = Integer.parseInt(System.getProperty(BUNDLE_CHECK_TIMEOUT_SECONDS, DEFAULT_TIMEOUT));
141 final int interval = Integer.parseInt(System.getProperty(BUNDLE_CHECK_INTERVAL_SECONDS, DEFAULT_INTERVAL));
142 LOG.info("Checking bundle states. Interval = {} second(s). Timeout = {} second(s).", interval, timeout);
144 final var maxNanos = TimeUnit.SECONDS.toNanos(timeout);
145 final var started = System.nanoTime();
147 Arrays.stream(bundleContext.getBundles()).forEach(this::captureBundleState);
148 final var result = aggregatedCheckResults();
149 if (result == CheckResult.IN_PROGRESS) {
150 final var now = System.nanoTime();
151 if (now - started >= maxNanos) {
152 logNokBundleDetails();
153 throw new IllegalStateException("Bundles states check timeout");
156 TimeUnit.SECONDS.sleep(interval);
160 LOG.info("Bundle state check completed with result {}", result);
161 if (result != CheckResult.SUCCESS) {
162 logNokBundleDetails();
163 throw new IllegalStateException("Bundle states check failure");
169 private void captureBundleState(final Bundle bundle) {
170 if (bundle != null) {
171 final var info = bundleService.getInfo(bundle);
172 final var checkResult = checkResultOf(info.getSymbolicName(), info.getState());
173 if (checkResult != bundleCheckResults.get(bundle.getBundleId())) {
174 LOG.info("Bundle {} -> State: {} ({})", info.getSymbolicName(), info.getState(), checkResult);
175 bundleCheckResults.put(bundle.getBundleId(), checkResult);
180 private CheckResult aggregatedCheckResults() {
181 final var resultStats = bundleCheckResults.entrySet().stream()
182 .collect(Collectors.groupingBy(Map.Entry::getValue, Collectors.counting()));
183 LOG.info("Bundle states check results: total={}, byResult={}", bundleCheckResults.size(), resultStats);
185 if (resultStats.getOrDefault(CheckResult.FAILURE, 0L) > 0) {
186 return CheckResult.FAILURE;
188 if (resultStats.getOrDefault(CheckResult.STOPPING, 0L) > 0) {
189 return CheckResult.STOPPING;
191 return resultStats.getOrDefault(CheckResult.IN_PROGRESS, 0L) == 0
192 ? CheckResult.SUCCESS : CheckResult.IN_PROGRESS;
195 private void logNokBundleDetails() {
196 final var nokBundles = bundleCheckResults.entrySet().stream()
197 .filter(entry -> CheckResult.SUCCESS != entry.getValue())
198 .map(Map.Entry::getKey).collect(Collectors.toSet());
200 for (var bundle : bundleContext.getBundles()) {
201 if (nokBundles.contains(bundle.getBundleId())) {
202 final var info = bundleService.getInfo(bundle);
203 final var diag = bundleService.getDiag(bundle);
204 final var diagText = diag.isEmpty() ? "" : ", diag: " + diag;
205 final var osgiState = OSGI_STATES.getOrDefault(bundle.getState(), "Unknown");
206 LOG.warn("NOK Bundle {}:{} -> OSGi state: {}, Karaf bundle state: {}{}",
207 info.getSymbolicName(), info.getVersion(), osgiState, info.getState(), diagText);
212 static CheckResult checkResultOf(final String bundleName, final BundleState state) {
213 if (bundleName != null && state == ELIGIBLE_STATES.get(bundleName)) {
214 return CheckResult.SUCCESS;
216 if (state == BundleState.Stopping) {
217 return CheckResult.STOPPING;
219 if (state == BundleState.Failure) {
220 return CheckResult.FAILURE;
222 if (state == BundleState.Resolved || state == BundleState.Active) {
223 return CheckResult.SUCCESS;
225 return CheckResult.IN_PROGRESS;
229 SUCCESS, FAILURE, STOPPING, IN_PROGRESS;