Rework time keeping
[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.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;
30
31 /**
32  * The feature test probe artifact.
33  *
34  * <p>
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()}.
40  *
41  * <p>
42  * Pax Exam module references:
43  * <ul>
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>
48  * </ul>
49  */
50 public final class TestProbe {
51
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";
58
59     static final String[] ALL_PROPERTY_KEYS =
60         {FEATURE_FILE_URI_PROP, BUNDLE_CHECK_SKIP, BUNDLE_CHECK_TIMEOUT_SECONDS, BUNDLE_CHECK_INTERVAL_SECONDS};
61
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);
70
71     private final Map<Long, CheckResult> bundleCheckResults = new HashMap<>();
72
73     @Inject
74     private BundleContext bundleContext;
75
76     @Inject
77     private FeaturesService featuresService;
78
79     @Inject
80     private BundleService bundleService;
81
82     /**
83      * Performs the project feature installation on karaf environment with subsequent state check of deployed bundles.
84      *
85      * @throws Exception on probe failure
86      */
87     @Test
88     @SuppressWarnings("IllegalCatch")
89     public void testFeature() throws Exception {
90         validateServices();
91         try {
92             installFeatures();
93             checkBundleStates();
94         } catch (Exception e) {
95             LOG.error("Exception executing feature test", e);
96             throw e;
97         }
98     }
99
100     private void validateServices() {
101         if (bundleContext == null) {
102             throw new IllegalStateException("bundleContext is not initialized");
103         }
104         // replace the probe's initial context which expires too fast
105         bundleContext = bundleContext.getBundle(0).getBundleContext();
106
107         if (featuresService == null) {
108             throw new IllegalStateException("featureService is not initialized");
109         }
110         if (bundleService == null) {
111             throw new IllegalStateException("bundleService is not initialized");
112         }
113     }
114
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");
119         }
120
121         // install repository the feature definition can be read from
122         featuresService.addRepository(featureUri);
123         LOG.info("Feature repository with URI: {} initialized", featureUri);
124
125         // install features
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()));
133         }
134     }
135
136     private void checkBundleStates() throws InterruptedException {
137         if ("true".equals(System.getProperty(BUNDLE_CHECK_SKIP))) {
138             return;
139         }
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);
143
144         final var maxNanos = TimeUnit.SECONDS.toNanos(timeout);
145         final var started = System.nanoTime();
146         while (true) {
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");
154                 }
155
156                 TimeUnit.SECONDS.sleep(interval);
157                 continue;
158             }
159
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");
164             }
165             break;
166         }
167     }
168
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);
176             }
177         }
178     }
179
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);
184
185         if (resultStats.getOrDefault(CheckResult.FAILURE, 0L) > 0) {
186             return CheckResult.FAILURE;
187         }
188         if (resultStats.getOrDefault(CheckResult.STOPPING, 0L) > 0) {
189             return CheckResult.STOPPING;
190         }
191         return resultStats.getOrDefault(CheckResult.IN_PROGRESS, 0L) == 0
192             ? CheckResult.SUCCESS : CheckResult.IN_PROGRESS;
193     }
194
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());
199
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);
208             }
209         }
210     }
211
212     static CheckResult checkResultOf(final String bundleName, final BundleState state) {
213         if (bundleName != null && state == ELIGIBLE_STATES.get(bundleName)) {
214             return CheckResult.SUCCESS;
215         }
216         if (state == BundleState.Stopping) {
217             return CheckResult.STOPPING;
218         }
219         if (state == BundleState.Failure) {
220             return CheckResult.FAILURE;
221         }
222         if (state == BundleState.Resolved || state == BundleState.Active) {
223             return CheckResult.SUCCESS;
224         }
225         return CheckResult.IN_PROGRESS;
226     }
227
228     enum CheckResult {
229         SUCCESS, FAILURE, STOPPING, IN_PROGRESS;
230     }
231 }