Rework SingleFeatureTest
[odlparent.git] / features-test-plugin / src / main / java / org / opendaylight / odlparent / features / test / plugin / TestFeaturesMojo.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.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;
18
19 import java.io.File;
20 import java.io.IOException;
21 import java.util.List;
22 import java.util.Map;
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;
43
44 /**
45  * Feature test plugin mojo.
46  */
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
52     private static final Logger LOG = LoggerFactory.getLogger(TestFeaturesMojo.class);
53     private static final String[] FEATURE_FILENAMES = {"feature.xml", "features.xml"};
54     private static final PluginDescriptor STATIC_DESCRIPTOR = staticPluginDescriptor();
55
56     @Parameter(defaultValue = "${project}", readonly = true, required = true)
57     private MavenProject project;
58     @Parameter(defaultValue = "${session}", readonly = true, required = true)
59     private MavenSession session;
60     @Parameter(defaultValue = "${settings.localRepository}")
61     private String localRepository;
62     @Parameter(defaultValue = "${repositorySystemSession}", readonly = true, required = true)
63     private RepositorySystemSession repoSession;
64     @Parameter(defaultValue = "${project.remoteProjectRepositories}", readonly = true, required = true)
65     private List<RemoteRepository> repositories;
66     @Component
67     private RepositorySystem repoSystem;
68
69     @Parameter(property = "sft.skip", defaultValue = "false")
70     private boolean skip;
71     @Deprecated(since = "13.0.10", forRemoval = true)
72     @Parameter(property = "skip.karaf.featureTest", defaultValue = "false")
73     private boolean legacySkip;
74     @Parameter(property = "sft.concurrent", defaultValue = "false")
75     private boolean concurrent;
76
77     // bundle state check probe settings
78     @Parameter(property = "sft.diag.skip", defaultValue = "false")
79     private boolean bundleStateCheckSkip;
80     @Parameter(property = "sft.diag.timeout", defaultValue = "120")
81     private int bundleStateCheckTimeout;
82
83     // vm and profile options for karaf container
84     @Parameter(property = "sft.heap.max", defaultValue = "2g")
85     private String maxHeap;
86     @Parameter(property = "sft.heap.dump.path", defaultValue = "/dev/null")
87     private String heapDumpPath;
88     @Parameter(property = "karaf.featureTest.profile", defaultValue = "false")
89     private boolean profile;
90
91     // Backing distribution details
92     @Parameter(property = "karaf.distro.groupId", defaultValue = "org.opendaylight.odlparent")
93     private String distGroupId;
94     @Parameter(property = "karaf.distro.artifactId", defaultValue = "opendaylight-karaf-empty")
95     private String distArtifactId;
96     @Parameter(property = "karaf.distro.type", defaultValue = "tar.gz")
97     private String distType;
98     @Parameter(property = "karaf.keep.unpack", defaultValue = "false")
99     private boolean keepUnpack;
100
101     @Override
102     public void execute() throws MojoExecutionException {
103         if (skip) {
104             LOG.debug("Skipping execution");
105             return;
106         }
107         if (legacySkip) {
108             LOG.warn("Skipping execution due to legacy karaf.featureTest.skip, please migrate to sft.skip");
109             return;
110         }
111         if (!"feature".equals(project.getPackaging())) {
112             LOG.info("Project packaging is not 'feature', skipping execution");
113             return;
114         }
115
116         LOG.info("Starting SFT for {}:{}", project.getGroupId(), project.getArtifactId());
117
118         final var buildDir = project.getBuild().getDirectory();
119         final var featureFile = getFeatureFile(new File(buildDir + File.separator + "feature"));
120
121         // resolve dependencies (ensure all are in local repository)
122         final var resolver = new DependencyResolver(repoSystem, repoSession, repositories);
123         resolver.resolveFeatureFile(featureFile);
124
125         // using file:* url instead of mvn:* to avoid MalformedUrlException (unknown protocol 'mvn');
126         // no reason to involve external maven resolver (pax-exam-aether-url) to fetch distro artifact via maven,
127         // while we can use local repository file directly (URL has built-in handler for 'file' protocol)
128         final var karafDistroUrl = resolver.resolveKarafDistroUrl(distGroupId, distArtifactId, distType);
129         LOG.debug("Distro URL resolved: {}", karafDistroUrl);
130
131         // dependency features (incl test scope) to be pre-installed
132         final var pluginDependencyFeatures = resolver.resolvePluginFeatures();
133         final var projectDependencyFeatures = resolver.resolveFeatures(project.getArtifacts());
134         LOG.info("Project dependency features detected: {}", projectDependencyFeatures);
135
136         // pax exam options
137         final var options = concat(
138             vmOptions(maxHeap, heapDumpPath),
139             profileOptions(profile),
140             karafDistroOptions(karafDistroUrl, keepUnpack, buildDir),
141             karafConfigOptions(buildDir, localRepository),
142             dependencyFeaturesOptions(pluginDependencyFeatures),
143             dependencyFeaturesOptions(projectDependencyFeatures),
144             probePropertiesOptions(),
145             miscOptions()
146         );
147
148         // probe parameters
149         System.setProperty(TestProbe.FEATURE_FILE_URI_PROP, featureFile.toURI().toString());
150         System.setProperty(TestProbe.BUNDLE_CHECK_SKIP, String.valueOf(bundleStateCheckSkip));
151         System.setProperty(TestProbe.BUNDLE_CHECK_TIMEOUT_SECONDS, String.valueOf(bundleStateCheckTimeout));
152
153         final ExamSystem system;
154         try {
155             system = PaxExamRuntime.createTestSystem(options);
156         } catch (IOException e) {
157             throw new MojoExecutionException("Cannot create pax-exam system", e);
158         }
159
160         final var execution = new PaxExamExecution(localRepository, system,
161             new KarafTestContainerFactory().create(system));
162         if (concurrent) {
163             execution.execute();
164             return;
165         }
166
167         // We create a plugin context in the top-level project of the build. There we store a single object which acts
168         // as the global lock protecting execution.
169         final Map<String, Object> topContext;
170         synchronized (session) {
171             // This is as careful as we can be. We guard against concurrent executions on the same top-level project.
172             topContext = session.getPluginContext(STATIC_DESCRIPTOR, session.getTopLevelProject());
173         }
174
175         final var lock = (Lock) topContext.computeIfAbsent("lock", key -> new ReentrantLock());
176         LOG.debug("Using lock {}", lock);
177         try {
178             lock.lockInterruptibly();
179         } catch (InterruptedException e) {
180             Thread.currentThread().interrupt();
181             throw new MojoExecutionException("Interrupted while acquiring lock", e);
182         }
183
184         LOG.debug("Acquired lock {}", lock);
185         try {
186             execution.execute();
187         } finally {
188             lock.unlock();
189             LOG.debug("Released lock {}", lock);
190         }
191     }
192
193     private static PluginDescriptor staticPluginDescriptor() {
194         final var desc = new PluginDescriptor();
195         desc.setGroupId("org.opendaylight.odlparent");
196         desc.setArtifactId("features-test-plugin");
197         return desc;
198     }
199
200     static File getFeatureFile(final File dir) throws MojoExecutionException {
201         for (var filename : FEATURE_FILENAMES) {
202             final File file = new File(dir, filename);
203             if (file.exists()) {
204                 return file;
205             }
206         }
207         throw new MojoExecutionException("No feature XML file found in " + dir);
208     }
209 }