42c0b573bac0f476dbc7f68c6b50be1312d8283b
[odlparent.git] / features-test-plugin / src / main / java / org / opendaylight / odlparent / features / test / plugin / TestProbe.java
1 /*
2  * Copyright (c) 2024 PANTHEON.tech s.r.o. and others. All rights reserved.
3  *
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
7  */
8 package org.opendaylight.odlparent.features.test.plugin;
9
10 import static org.apache.karaf.bundle.core.BundleState.Installed;
11 import static org.apache.karaf.bundle.core.BundleState.Waiting;
12
13 import java.io.File;
14 import java.net.URI;
15 import java.util.Arrays;
16 import java.util.EnumSet;
17 import java.util.HashMap;
18 import java.util.Map;
19 import java.util.stream.Collectors;
20 import javax.inject.Inject;
21 import org.apache.karaf.bundle.core.BundleService;
22 import org.apache.karaf.bundle.core.BundleState;
23 import org.apache.karaf.features.FeaturesService;
24 import org.junit.Test;
25 import org.osgi.framework.Bundle;
26 import org.osgi.framework.BundleContext;
27 import org.slf4j.Logger;
28 import org.slf4j.LoggerFactory;
29
30 /**
31  * The feature test probe artifact.
32  *
33  * <p>
34  * The class is being packaged and deployed to karaf environment on {@link PaxExamExecution#execute()} invocation.
35  * All dependencies which are absent on target environment expected to be packaged using same
36  * {@link org.ops4j.pax.exam.ProbeBuilder}. Input parameters are passed through system properties. in order to be
37  * delivered properly all affected properties require explicit declaration using associated Pax options -- see
38  * {@link PaxOptionUtils#probePropertiesOptions()}.
39  *
40  * <p>
41  * Pax Exam module references:
42  * <ul>
43  *     <li>Probe bundle deployment handling is served by pax-exam-extender-service</li>
44  *     <li>Service instances lookup and injection into probe instance is served by pax-exam-inject</li>
45  *     <li>Test method invocation is served by pax-exam-invoker-junit, uses JUnitCore v.4, which requires @Test
46  *     annotation for method to be eligible for invocation</li>
47  * </ul>
48  */
49 public final class TestProbe {
50
51     static final String FEATURE_FILE_URI_PROP = "feature.test.file.uri";
52     static final String BUNDLE_CHECK_SKIP = "feature.test.bundle.check.skip";
53     static final String BUNDLE_CHECK_TIMEOUT_SECONDS = "feature.test.bundle.check.timeout.seconds";
54     static final String BUNDLE_CHECK_INTERVAL_SECONDS = "feature.test.bundle.check.interval.seconds";
55     static final String DEFAULT_TIMEOUT = "300";
56     static final String DEFAULT_INTERVAL = "1";
57
58     static final String[] ALL_PROPERTY_KEYS =
59         {FEATURE_FILE_URI_PROP, BUNDLE_CHECK_SKIP, BUNDLE_CHECK_TIMEOUT_SECONDS, BUNDLE_CHECK_INTERVAL_SECONDS};
60
61     private static final Logger LOG = LoggerFactory.getLogger(TestProbe.class);
62     private static final Map<Integer, String> OSGI_STATES = Map.of(
63         Bundle.INSTALLED, "Installed", Bundle.RESOLVED, "Resolved",
64         Bundle.STARTING, "Starting", Bundle.ACTIVE, "Active",
65         Bundle.STOPPING, "Stopping", Bundle.UNINSTALLED, "Uninstalled");
66     private static final Map<String, BundleState> ELIGIBLE_STATES = Map.of(
67         "slf4j.log4j12", Installed,
68         "org.apache.karaf.scr.management", Waiting);
69
70     private final Map<Long, CheckResult> bundleCheckResults = new HashMap<>();
71
72     @Inject
73     private BundleContext bundleContext;
74
75     @Inject
76     private FeaturesService featuresService;
77
78     @Inject
79     private BundleService bundleService;
80
81     /**
82      * Performs the project feature installation on karaf environment with subsequent state check of deployed bundles.
83      *
84      * @throws Exception on probe failure
85      */
86     @Test
87     @SuppressWarnings("IllegalCatch")
88     public void testFeature() throws Exception {
89         validateServices();
90         try {
91             installFeatures();
92             checkBundleStates();
93         } catch (Exception e) {
94             LOG.error("Exception executing feature test", e);
95             throw e;
96         }
97     }
98
99     private void validateServices() {
100         if (bundleContext == null) {
101             throw new IllegalStateException("bundleContext is not initialized");
102         }
103         // replace the probe's initial context which expires too fast
104         bundleContext = bundleContext.getBundle(0).getBundleContext();
105
106         if (featuresService == null) {
107             throw new IllegalStateException("featureService is not initialized");
108         }
109         if (bundleService == null) {
110             throw new IllegalStateException("bundleService is not initialized");
111         }
112     }
113
114     private void installFeatures() throws Exception {
115         final var featureUri = URI.create(System.getProperty(FEATURE_FILE_URI_PROP));
116         if (!new File(featureUri).exists()) {
117             throw new IllegalStateException("Feature file with URI " + featureUri + " does not exist");
118         }
119
120         // install repository the feature definition can be read from
121         featuresService.addRepository(featureUri);
122         LOG.info("Feature repository with URI: {} initialized", featureUri);
123
124         // install features
125         for (var feature : featuresService.getRepository(featureUri).getFeatures()) {
126             final var name = feature.getName();
127             final var version = feature.getVersion();
128             LOG.info("Installing feature: {}, {}", name, version);
129             featuresService.installFeature(name, version, EnumSet.of(FeaturesService.Option.Verbose));
130             LOG.info("Feature is installed: {}, isInstalled()={}, getState()={}",
131                 name, featuresService.isInstalled(feature), featuresService.getState(feature.getId()));
132         }
133     }
134
135     private void checkBundleStates() throws InterruptedException {
136         if ("true".equals(System.getProperty(BUNDLE_CHECK_SKIP))) {
137             return;
138         }
139         final int timeout = Integer.parseInt(System.getProperty(BUNDLE_CHECK_TIMEOUT_SECONDS, DEFAULT_TIMEOUT));
140         final int interval = Integer.parseInt(System.getProperty(BUNDLE_CHECK_INTERVAL_SECONDS, DEFAULT_INTERVAL));
141         LOG.info("Checking bundle states. Interval = {} second(s). Timeout = {} second(s).", interval, timeout);
142
143         final var maxTimestamp = System.currentTimeMillis() + timeout * 1000L;
144         CheckResult result = CheckResult.IN_PROGRESS;
145         while (System.currentTimeMillis() < maxTimestamp) {
146             Arrays.stream(bundleContext.getBundles()).forEach(this::captureBundleState);
147             result = aggregatedCheckResults();
148             if (result != CheckResult.IN_PROGRESS) {
149                 break;
150             }
151             Thread.sleep(interval * 1000L);
152         }
153         LOG.info("Bundle state check completed with result {}", result);
154         if (result == CheckResult.IN_PROGRESS) {
155             logNokBundleDetails();
156             throw new IllegalStateException("Bundles states check timeout");
157         }
158         if (result != CheckResult.SUCCESS) {
159             logNokBundleDetails();
160             throw new IllegalStateException("Bundle states check failure");
161         }
162     }
163
164     private void captureBundleState(final Bundle bundle) {
165         if (bundle != null) {
166             final var info = bundleService.getInfo(bundle);
167             final var checkResult = checkResultOf(info.getSymbolicName(), info.getState());
168             if (checkResult != bundleCheckResults.get(bundle.getBundleId())) {
169                 LOG.info("Bundle {} -> State: {} ({})", info.getSymbolicName(), info.getState(), checkResult);
170                 bundleCheckResults.put(bundle.getBundleId(), checkResult);
171             }
172         }
173     }
174
175     private CheckResult aggregatedCheckResults() {
176         final var resultStats = bundleCheckResults.entrySet().stream()
177             .collect(Collectors.groupingBy(Map.Entry::getValue, Collectors.counting()));
178         LOG.info("Bundle states check results: total={}, byResult={}", bundleCheckResults.size(), resultStats);
179
180         if (resultStats.getOrDefault(CheckResult.FAILURE, 0L) > 0) {
181             return CheckResult.FAILURE;
182         }
183         if (resultStats.getOrDefault(CheckResult.STOPPING, 0L) > 0) {
184             return CheckResult.STOPPING;
185         }
186         return resultStats.getOrDefault(CheckResult.IN_PROGRESS, 0L) == 0
187             ? CheckResult.SUCCESS : CheckResult.IN_PROGRESS;
188     }
189
190     private void logNokBundleDetails() {
191         final var nokBundles = bundleCheckResults.entrySet().stream()
192             .filter(entry -> CheckResult.SUCCESS != entry.getValue())
193             .map(Map.Entry::getKey).collect(Collectors.toSet());
194
195         for (var bundle : bundleContext.getBundles()) {
196             if (nokBundles.contains(bundle.getBundleId())) {
197                 final var info = bundleService.getInfo(bundle);
198                 final var diag = bundleService.getDiag(bundle);
199                 final var diagText = diag.isEmpty() ? "" : ", diag: " + diag;
200                 final var osgiState = OSGI_STATES.getOrDefault(bundle.getState(), "Unknown");
201                 LOG.warn("NOK Bundle {}:{} -> OSGi state: {}, Karaf bundle state: {}{}",
202                     info.getSymbolicName(), info.getVersion(), osgiState, info.getState(), diagText);
203             }
204         }
205     }
206
207     static CheckResult checkResultOf(final String bundleName, final BundleState state) {
208         if (bundleName != null && state == ELIGIBLE_STATES.get(bundleName)) {
209             return CheckResult.SUCCESS;
210         }
211         if (state == BundleState.Stopping) {
212             return CheckResult.STOPPING;
213         }
214         if (state == BundleState.Failure) {
215             return CheckResult.FAILURE;
216         }
217         if (state == BundleState.Resolved || state == BundleState.Active) {
218             return CheckResult.SUCCESS;
219         }
220         return CheckResult.IN_PROGRESS;
221     }
222
223     enum CheckResult {
224         SUCCESS, FAILURE, STOPPING, IN_PROGRESS;
225     }
226 }