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.opendaylight.odlparent.features.test.plugin.PaxOptionUtils.concat;
11 import static org.opendaylight.odlparent.features.test.plugin.PaxOptionUtils.dependencyFeaturesOptions;
12 import static org.opendaylight.odlparent.features.test.plugin.PaxOptionUtils.karafConfigOptions;
13 import static org.opendaylight.odlparent.features.test.plugin.PaxOptionUtils.karafDistroOptions;
14 import static org.opendaylight.odlparent.features.test.plugin.PaxOptionUtils.miscOptions;
15 import static org.opendaylight.odlparent.features.test.plugin.PaxOptionUtils.probePropertiesOptions;
16 import static org.opendaylight.odlparent.features.test.plugin.PaxOptionUtils.profileOptions;
17 import static org.opendaylight.odlparent.features.test.plugin.PaxOptionUtils.vmOptions;
20 import java.io.IOException;
21 import java.util.List;
23 import java.util.concurrent.locks.Lock;
24 import java.util.concurrent.locks.ReentrantLock;
25 import org.apache.maven.execution.MavenSession;
26 import org.apache.maven.plugin.AbstractMojo;
27 import org.apache.maven.plugin.MojoExecutionException;
28 import org.apache.maven.plugin.descriptor.PluginDescriptor;
29 import org.apache.maven.plugins.annotations.Component;
30 import org.apache.maven.plugins.annotations.LifecyclePhase;
31 import org.apache.maven.plugins.annotations.Mojo;
32 import org.apache.maven.plugins.annotations.Parameter;
33 import org.apache.maven.plugins.annotations.ResolutionScope;
34 import org.apache.maven.project.MavenProject;
35 import org.eclipse.aether.RepositorySystem;
36 import org.eclipse.aether.RepositorySystemSession;
37 import org.eclipse.aether.repository.RemoteRepository;
38 import org.ops4j.pax.exam.ExamSystem;
39 import org.ops4j.pax.exam.karaf.container.internal.KarafTestContainerFactory;
40 import org.ops4j.pax.exam.spi.PaxExamRuntime;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
45 * Feature test plugin mojo.
47 @Mojo(name = "test", defaultPhase = LifecyclePhase.INTEGRATION_TEST,
48 requiresDependencyResolution = ResolutionScope.TEST,
49 requiresDependencyCollection = ResolutionScope.TEST, threadSafe = true)
50 public final class TestFeaturesMojo extends AbstractMojo {
51 private static final Logger LOG = LoggerFactory.getLogger(TestFeaturesMojo.class);
52 private static final String[] FEATURE_FILENAMES = {"feature.xml", "features.xml"};
53 private static final PluginDescriptor STATIC_DESCRIPTOR;
56 final var desc = new PluginDescriptor();
57 desc.setGroupId("org.opendaylight.odlparent");
58 desc.setArtifactId("features-test-plugin");
59 STATIC_DESCRIPTOR = desc;
62 @Parameter(defaultValue = "${project}", readonly = true, required = true)
63 private MavenProject project;
64 @Parameter(defaultValue = "${session}", readonly = true, required = true)
65 private MavenSession session;
66 @Parameter(defaultValue = "${settings.localRepository}")
67 private String localRepository;
68 @Parameter(defaultValue = "${repositorySystemSession}", readonly = true, required = true)
69 private RepositorySystemSession repoSession;
70 @Parameter(defaultValue = "${project.remoteProjectRepositories}", readonly = true, required = true)
71 private List<RemoteRepository> repositories;
73 private RepositorySystem repoSystem;
75 @Parameter(property = "sft.skip", defaultValue = "false")
77 @Deprecated(since = "13.0.10", forRemoval = true)
78 @Parameter(property = "skip.karaf.featureTest", defaultValue = "false")
79 private boolean legacySkip;
80 @Parameter(property = "sft.concurrent", defaultValue = "false")
81 private boolean concurrent;
83 // bundle state check probe settings
84 @Parameter(property = "sft.diag.skip", defaultValue = "false")
85 private boolean bundleStateCheckSkip;
86 @Parameter(property = "sft.diag.timeout", defaultValue = TestProbe.DEFAULT_TIMEOUT)
87 private int bundleStateCheckTimeout;
88 @Parameter(property = "sft.diag.interval", defaultValue = TestProbe.DEFAULT_TIMEOUT)
89 private int bundleStateCheckInterval;
91 // vm and profile options for karaf container
92 @Parameter(property = "sft.heap.max", defaultValue = "2g")
93 private String maxHeap;
94 @Parameter(property = "sft.heap.dump.path", defaultValue = "/dev/null")
95 private String heapDumpPath;
96 @Parameter(property = "karaf.featureTest.profile", defaultValue = "false")
97 private boolean profile;
99 // Backing distribution details
100 @Parameter(property = "karaf.distro.groupId", defaultValue = "org.opendaylight.odlparent")
101 private String distGroupId;
102 @Parameter(property = "karaf.distro.artifactId", defaultValue = "opendaylight-karaf-empty")
103 private String distArtifactId;
104 @Parameter(property = "karaf.distro.type", defaultValue = "tar.gz")
105 private String distType;
106 @Parameter(property = "karaf.keep.unpack", defaultValue = "false")
107 private boolean keepUnpack;
110 public void execute() throws MojoExecutionException {
112 LOG.debug("Skipping execution");
116 LOG.warn("Skipping execution due to legacy karaf.featureTest.skip, please migrate to sft.skip");
119 if (!"feature".equals(project.getPackaging())) {
120 LOG.info("Project packaging is not 'feature', skipping execution");
124 LOG.info("Starting SFT for {}:{}", project.getGroupId(), project.getArtifactId());
126 final var buildDir = project.getBuild().getDirectory();
127 final var featureFile = getFeatureFile(new File(buildDir + File.separator + "feature"));
129 // resolve dependencies (ensure all are in local repository)
130 final var resolver = new DependencyResolver(repoSystem, repoSession, repositories);
131 resolver.resolveFeatureFile(featureFile);
133 // using file:* url instead of mvn:* to avoid MalformedUrlException (unknown protocol 'mvn');
134 // no reason to involve external maven resolver (pax-exam-aether-url) to fetch distro artifact via maven,
135 // while we can use local repository file directly (URL has built-in handler for 'file' protocol)
136 final var karafDistroUrl = resolver.resolveKarafDistroUrl(distGroupId, distArtifactId, distType);
137 LOG.debug("Distro URL resolved: {}", karafDistroUrl);
139 // dependency features (incl test scope) to be pre-installed
140 final var pluginDependencyFeatures = resolver.resolvePluginFeatures();
141 final var projectDependencyFeatures = resolver.resolveFeatures(project.getArtifacts());
142 LOG.info("Project dependency features detected: {}", projectDependencyFeatures);
145 final var options = concat(
146 vmOptions(maxHeap, heapDumpPath),
147 profileOptions(profile),
148 karafDistroOptions(karafDistroUrl, keepUnpack, buildDir),
149 karafConfigOptions(buildDir, localRepository),
150 dependencyFeaturesOptions(pluginDependencyFeatures),
151 dependencyFeaturesOptions(projectDependencyFeatures),
152 probePropertiesOptions(),
157 System.setProperty(TestProbe.FEATURE_FILE_URI_PROP, featureFile.toURI().toString());
158 System.setProperty(TestProbe.BUNDLE_CHECK_SKIP, String.valueOf(bundleStateCheckSkip));
159 System.setProperty(TestProbe.BUNDLE_CHECK_TIMEOUT_SECONDS, String.valueOf(bundleStateCheckTimeout));
160 System.setProperty(TestProbe.BUNDLE_CHECK_INTERVAL_SECONDS, String.valueOf(bundleStateCheckInterval));
162 final ExamSystem system;
164 system = PaxExamRuntime.createTestSystem(options);
165 } catch (IOException e) {
166 throw new MojoExecutionException("Cannot create pax-exam system", e);
169 final var execution = new PaxExamExecution(localRepository, system,
170 new KarafTestContainerFactory().create(system));
176 // We create a plugin context in the top-level project of the build. There we store a single object which acts
177 // as the global lock protecting execution.
178 final Map<String, Object> topContext;
179 synchronized (session) {
180 // This is as careful as we can be. We guard against concurrent executions on the same top-level project.
181 topContext = session.getPluginContext(STATIC_DESCRIPTOR, session.getTopLevelProject());
184 // Note: we are using a fair lock to get first-come, first-serve rather than some unpredictable order
185 final var lock = (Lock) topContext.computeIfAbsent("lock", key -> new ReentrantLock(true));
186 LOG.debug("Using lock {}", lock);
188 lock.lockInterruptibly();
189 } catch (InterruptedException e) {
190 Thread.currentThread().interrupt();
191 throw new MojoExecutionException("Interrupted while acquiring lock", e);
194 LOG.debug("Acquired lock {}", lock);
199 LOG.debug("Released lock {}", lock);
203 static File getFeatureFile(final File dir) throws MojoExecutionException {
204 for (var filename : FEATURE_FILENAMES) {
205 final File file = new File(dir, filename);
210 throw new MojoExecutionException("No feature XML file found in " + dir);