View Javadoc
1   /*
2    * Copyright 2017-2022 original authors
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package io.micronaut.build.testresources;
17  
18  import io.micronaut.build.services.DependencyResolutionService;
19  import io.micronaut.testresources.buildtools.*;
20  import org.apache.maven.execution.MavenSession;
21  import org.apache.maven.model.Dependency;
22  import org.apache.maven.plugin.MojoExecutionException;
23  import org.apache.maven.plugin.logging.Log;
24  import org.apache.maven.plugin.logging.SystemStreamLog;
25  import org.apache.maven.project.MavenProject;
26  import org.apache.maven.toolchain.ToolchainManager;
27  import org.eclipse.aether.artifact.Artifact;
28  import org.eclipse.aether.resolution.DependencyResolutionException;
29  
30  import java.io.File;
31  import java.io.IOException;
32  import java.nio.file.*;
33  import java.nio.file.attribute.BasicFileAttributes;
34  import java.time.Duration;
35  import java.util.*;
36  import java.util.concurrent.TimeUnit;
37  import java.util.concurrent.atomic.AtomicBoolean;
38  import java.util.stream.Collectors;
39  import java.util.stream.Stream;
40  
41  import static io.micronaut.build.MojoUtils.findJavaExecutable;
42  import static io.micronaut.build.services.DependencyResolutionService.testResourcesModuleToAetherArtifact;
43  import static io.micronaut.build.services.DependencyResolutionService.toClasspathFiles;
44  import static java.util.stream.Stream.concat;
45  
46  /**
47   * Utility class to stop Test Resources service.
48   */
49  public class TestResourcesHelper {
50  
51      private static final String TEST_RESOURCES_PROPERTIES = "test-resources.properties";
52      private static final String PORT_FILE_NAME = "test-resources-port.txt";
53  
54      private final boolean enabled;
55  
56      private final boolean keepAlive;
57  
58      private final boolean shared;
59  
60      private final File buildDirectory;
61  
62      private final Log log;
63  
64      private Integer explicitPort;
65  
66      private Integer clientTimeout;
67  
68      private MavenProject mavenProject;
69  
70      private MavenSession mavenSession;
71  
72      private DependencyResolutionService dependencyResolutionService;
73  
74      private ToolchainManager toolchainManager;
75  
76      private String testResourcesVersion;
77  
78      private boolean classpathInference;
79  
80      private List<Dependency> testResourcesDependencies;
81  
82      private String sharedServerNamespace;
83  
84      public TestResourcesHelper(boolean enabled, boolean keepAlive, boolean shared, File buildDirectory,
85                                 Integer explicitPort, Integer clientTimeout, MavenProject mavenProject,
86                                 MavenSession mavenSession, DependencyResolutionService dependencyResolutionService,
87                                 ToolchainManager toolchainManager, String testResourcesVersion,
88                                 boolean classpathInference, List<Dependency> testResourcesDependencies,
89                                 String sharedServerNamespace) {
90          this(enabled, keepAlive, shared, buildDirectory);
91          this.explicitPort = explicitPort;
92          this.clientTimeout = clientTimeout;
93          this.mavenProject = mavenProject;
94          this.mavenSession = mavenSession;
95          this.dependencyResolutionService = dependencyResolutionService;
96          this.toolchainManager = toolchainManager;
97          this.testResourcesVersion = testResourcesVersion;
98          this.classpathInference = classpathInference;
99          this.testResourcesDependencies = testResourcesDependencies;
100         this.sharedServerNamespace = sharedServerNamespace;
101     }
102 
103     public TestResourcesHelper(boolean enabled, boolean keepAlive, boolean shared, File buildDirectory) {
104         this.enabled = enabled;
105         this.keepAlive = keepAlive;
106         this.shared = shared;
107         this.buildDirectory = buildDirectory;
108         this.log = new SystemStreamLog();
109     }
110 
111     /**
112      * Starts the Test Resources Service.
113      */
114     public void start() throws MojoExecutionException {
115         if (!enabled) {
116             return;
117         }
118         try {
119             doStart();
120         } catch (Exception e) {
121             throw new MojoExecutionException("Unable to start test resources server", e);
122         }
123     }
124 
125     private void doStart() throws DependencyResolutionException, IOException {
126         if (shared) {
127             if (sharedServerNamespace != null) {
128                 log.info("Test Resources is configured in shared mode with the namespace: " + sharedServerNamespace);
129             } else {
130                 log.info("Test Resources is configured in shared mode");
131             }
132         }
133         String accessToken = UUID.randomUUID().toString();
134         Path buildDir = buildDirectory.toPath();
135         Path serverSettingsDirectory = getServerSettingsDirectory();
136         AtomicBoolean serverStarted = new AtomicBoolean(false);
137         ServerUtils.startOrConnectToExistingServer(
138                 explicitPort,
139                 buildDir.resolve(PORT_FILE_NAME),
140                 serverSettingsDirectory,
141                 accessToken,
142                 resolveServerClasspath(),
143                 clientTimeout,
144                 new ServerFactory() {
145                     Process process;
146 
147                     @Override
148                     public void startServer(ServerUtils.ProcessParameters processParameters) throws IOException {
149                         log.info("Starting Micronaut Test Resources service");
150                         String javaBin = findJavaExecutable(toolchainManager, mavenSession);
151                         List<String> cli = new ArrayList<>();
152                         cli.add(javaBin);
153                         cli.addAll(processParameters.getJvmArguments());
154                         processParameters.getSystemProperties().forEach((key, value) -> cli.add("-D" + key + "=" + value));
155                         cli.add("-cp");
156                         cli.add(processParameters.getClasspath().stream().map(File::getAbsolutePath).collect(Collectors.joining(File.pathSeparator)));
157                         cli.add(processParameters.getMainClass());
158                         cli.addAll(processParameters.getArguments());
159                         ProcessBuilder builder = new ProcessBuilder(cli);
160                         process = builder.inheritIO().start();
161                         serverStarted.set(true);
162                     }
163 
164                     @Override
165                     public void waitFor(Duration duration) throws InterruptedException {
166                         if (process != null) {
167                             process.waitFor(duration.toMillis(), TimeUnit.MILLISECONDS);
168                         }
169                     }
170                 }
171         );
172         if (keepAlive) {
173             log.info("Micronaut Test Resources service is started in the background. To stop it, run the following command: 'mvn mn:" + StopTestResourcesServerMojo.NAME + "'");
174         }
175         if (!serverStarted.get()) {
176             // A server was already listening, which means it was running before
177             // the build was started, so we put a file to indicate to the stop
178             // mojo that it should not stop the server.
179             Path keepalive = getKeepAliveFile();
180             Files.write(keepalive, "true".getBytes());
181         }
182         // In order for the test resources client to connect, we need
183         // to copy the test resources file to the test classes directory
184         copyServerSettingsToClasspath(buildDir, serverSettingsDirectory);
185     }
186 
187     private void copyServerSettingsToClasspath(Path buildDir, Path serverSettingsDirectory) throws IOException {
188         Path testClassesDir = buildDir.resolve("test-classes");
189         if (!Files.exists(testClassesDir)) {
190             Files.createDirectories(testClassesDir);
191         }
192         Files.copy(serverSettingsDirectory.resolve(TEST_RESOURCES_PROPERTIES), testClassesDir.resolve(TEST_RESOURCES_PROPERTIES), StandardCopyOption.REPLACE_EXISTING);
193     }
194 
195     private List<File> resolveServerClasspath() throws DependencyResolutionException {
196         Stream<Artifact> clientDependencies = Stream.of(testResourcesModuleToAetherArtifact("client", testResourcesVersion));
197 
198         List<MavenDependency> applicationDependencies = Collections.emptyList();
199         if (classpathInference) {
200             applicationDependencies = getApplicationDependencies();
201         }
202         Stream<Artifact> serverDependencies =
203                 TestResourcesClasspath.inferTestResourcesClasspath(applicationDependencies, testResourcesVersion)
204                         .stream()
205                         .map(DependencyResolutionService::testResourcesDependencyToAetherArtifact);
206 
207         List<org.apache.maven.model.Dependency> extraDependencies =
208                 testResourcesDependencies != null ? testResourcesDependencies : Collections.emptyList();
209 
210         Stream<Artifact> extraDependenciesStream = extraDependencies.stream()
211                 .map(DependencyResolutionService::mavenDependencyToAetherArtifact);
212 
213         Stream<Artifact> artifacts = concat(concat(clientDependencies, serverDependencies), extraDependenciesStream);
214 
215         return toClasspathFiles(dependencyResolutionService.artifactResultsFor(artifacts, true));
216     }
217 
218     private List<MavenDependency> getApplicationDependencies() {
219         return this.mavenProject.getDependencies().stream()
220                 .map(DependencyResolutionService::mavenDependencyToTestResourcesDependency)
221                 .collect(Collectors.toList());
222     }
223 
224     /**
225      * Contains the logic to stop the Test Resources Service.
226      */
227     public void stop() throws MojoExecutionException {
228         if (!enabled || Boolean.TRUE.equals(keepAlive)) {
229             return;
230         }
231         if (Files.exists(getKeepAliveFile())) {
232             try {
233                 Files.delete(getKeepAliveFile());
234             } catch (IOException e) {
235                 throw new MojoExecutionException("Failed to delete keepalive file", e);
236             }
237             return;
238         }
239         try {
240             Optional<ServerSettings> optionalServerSettings = ServerUtils.readServerSettings(getServerSettingsDirectory());
241             if (optionalServerSettings.isPresent() && ServerUtils.isServerStarted(optionalServerSettings.get().getPort())) {
242                 log.info("Shutting down Micronaut Test Resources service");
243                 doStop();
244             }
245         } catch (Exception e) {
246             throw new MojoExecutionException("Unable to stop test resources server", e);
247         }
248     }
249 
250     private void doStop() throws IOException {
251         Path settingsDirectory = getServerSettingsDirectory();
252         ServerUtils.stopServer(settingsDirectory);
253         Files.walkFileTree(settingsDirectory, new SimpleFileVisitor<Path>() {
254             @Override
255             public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
256                 Files.delete(file);
257                 return super.visitFile(file, attrs);
258             }
259 
260             @Override
261             public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
262                 Files.delete(dir);
263                 return super.postVisitDirectory(dir, exc);
264             }
265         });
266     }
267 
268     private Path getServerSettingsDirectory() {
269         if (shared) {
270             return ServerUtils.getDefaultSharedSettingsPath(sharedServerNamespace);
271         }
272         return serverSettingsDirectoryOf(buildDirectory.toPath());
273     }
274 
275     private Path getKeepAliveFile() {
276         return getServerSettingsDirectory().resolve("keepalive");
277     }
278 
279     private Path serverSettingsDirectoryOf(Path buildDir) {
280         return buildDir.resolve("../.micronaut/test-resources");
281     }
282 
283     /**
284      * @param sharedServerNamespace The shared server namespace (if any).
285      */
286     public void setSharedServerNamespace(String sharedServerNamespace) {
287         this.sharedServerNamespace = sharedServerNamespace;
288     }
289 }