All Downloads are FREE. Search and download functionalities are using the official Maven repository.

uk.bot_by.ijhttp_tools.maven_plugin.RunMojo Maven / Gradle / Ivy

/*
 * Copyright 2023 Vitalij Berdinskih
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package uk.bot_by.ijhttp_tools.maven_plugin;

import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.exec.ExecuteException;
import org.apache.commons.exec.ExecuteWatchdog;
import org.apache.commons.exec.Executor;
import org.apache.commons.exec.LogOutputStream;
import org.apache.commons.exec.PumpStreamHandler;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.jetbrains.annotations.VisibleForTesting;
import uk.bot_by.ijhttp_tools.command_line.HttpClientCommandLine;
import uk.bot_by.ijhttp_tools.command_line.LogLevel;

/**
 * Run integration tests using IntelliJ HTTP Client.
 * 

* Sample configuration: *


 * <configuration>
 *   <directories>
 *     <directory>src/test/resources</directory>
 *   </directories>
 *   <environmentFile>public-env.json</environmentFile>
 *   <environmentName>dev</environmentName>
 *   <files>
 *     <file>sample-1-queries.http</file>
 *     <file>sample-2-queries.http</file>
 *   </files>
 *   <logLevel>HEADERS</logLevel>
 *   <report>true</report>
 *   <workingDirectory>target</workingDirectory>
 * </configuration>
 * 
* Environment variables: *

 * ...
 * <environmentVariables>
 *   <environmentVariable>id=1234</environmentVariable>
 *   <environmentVariable>field=name</environmentVariable>
 * </environmentVariables>
 * ...
 * 
* To manage plugin's output use {@link #useMavenLogger}, {@link #quietLogs} and * {@link #outputFile}. * * @author Vitalij Berdinskih * @see HTTP Client CLI * @see Lve stream: The New HTTP * Client CLI * @since 1.0.0 */ @Mojo(name = "run", defaultPhase = LifecyclePhase.INTEGRATION_TEST, requiresProject = false) public class RunMojo extends AbstractMojo { private Integer connectTimeout; private List directories; private boolean dockerMode; private File environmentFile; private List environmentVariables; private String environmentName; private String executable; private List files; private boolean insecure; private LogLevel logLevel; private File outputFile; private File privateEnvironmentFile; private List privateEnvironmentVariables; private String proxy; private boolean quietLogs; private boolean report; private File reportPath; private boolean skip; private Integer socketTimeout; private Integer timeout; private boolean useMavenLogger; private File workingDirectory; /** * Run HTTP requests. * * @throws MojoExecutionException if an error happens * @throws MojoFailureException if a failure happens */ @Override public void execute() throws MojoExecutionException, MojoFailureException { if (skip) { getLog().info("skipping execute as per configuration"); return; } try { var commandLine = getCommandLine(); var executor = getExecutor(); if (getLog().isDebugEnabled()) { getLog().debug("Executing command line: " + commandLine); } try { runHttpClient(commandLine, executor); } catch (ExecuteException exception) { if (nonNull(executor.getWatchdog()) && executor.getWatchdog().killedProcess()) { var message = "Timeout. Process runs longer that " + timeout + " ms."; getLog().error(message); throw new MojoExecutionException(message); } else { var message = "Execution failed: " + exception.getMessage(); getLog().error(message, exception); throw new MojoExecutionException(message, exception); } } } catch (IOException exception) { var message = new StringBuilder("I/O Error"); if (nonNull(exception.getMessage()) && !exception.getMessage().isBlank()) { message.append(": ").append(exception.getMessage()); } getLog().warn(message); throw new MojoExecutionException(message.toString(), exception); } } /** * Number of milliseconds for connection. Defaults to 3000. */ @Parameter(property = "ijhttp.connect-timeout") public void setConnectTimeout(Integer connectTimeout) { this.connectTimeout = connectTimeout; } /** * Directories to look up HTTP files. At least one {@code file} or {@code directory} is required. *

* Example: *


   *   <directories>
   *     <directory>src/test/resources/orders</directory>
   *     <directory>src/test/resources/catalog/products</directory>
   *   </directories>
   * 
* * @see #setFiles(List) * @since 1.2.0 */ @Parameter(property = "ijhttp.files", required = true) public void setDirectories(List directories) { this.directories = directories; } /** * Enables Docker mode. Treat {@code localhost} as {@code host.docker.internal}. Defaults to * false. */ @Parameter(property = "ijhttp.docker-mode", defaultValue = "false") public void setDockerMode(boolean dockerMode) { this.dockerMode = dockerMode; } /** * Name of the public environment file, e.g. {@code http-client.env.json}. */ @Parameter(property = "ijhttp.env-file") public void setEnvironmentFile(File environmentFile) { this.environmentFile = environmentFile; } /** * Public environment variables. *

* Example: *


   *   <configuration>
   *     <environmentVariables>
   *       <environmentVariable>id=1234</environmentVariable>
   *       <environmentVariable>field=name</environmentVariable>
   *     </environmentVariables>
   *   </configuration>
   * 
*/ @Parameter(property = "ijhttp.env-variables") public void setEnvironmentVariables(List environmentVariables) { this.environmentVariables = environmentVariables; } /** * Name of the environment in a configuration file. */ @Parameter(property = "ijhttp.env") public void setEnvironmentName(String environmentName) { this.environmentName = environmentName; } /** * The executable. Can be a full path or the name of the executable. */ @Parameter(property = "ijhttp.executable", defaultValue = "ijhttp") public void setExecutable(String executable) { this.executable = executable; } /** * HTTP file paths. At least one {@code file} or {@code directory} is required. *

* Example: *


   *   <files>
   *     <file>simple-run.http</file>
   *   </files>
   * 
* * @see #setDirectories(List) */ @Parameter(property = "ijhttp.files", required = true) public void setFiles(List files) { this.files = files; } /** * Allow insecure SSL connection. Defaults to false. */ @Parameter(property = "ijhttp.insecure", defaultValue = "false") public void setInsecure(boolean insecure) { this.insecure = insecure; } /** * Logging level: BASIC, HEADERS, VERBOSE. Defaults to BASIC. */ @Parameter(property = "ijhttp.log-level", defaultValue = "BASIC") public void setLogLevel(LogLevel logLevel) { this.logLevel = logLevel; } /** * Program standard and error output will be redirected to the file specified by this optional * field. If not enabled the traditional behavior of program output being directed to standard * {@code System.out} and {@code System.err} is used. * * @see #setUseMavenLogger(boolean) */ @Parameter(property = "ijhttp.output-file") public void setOutputFile(File outputFile) { this.outputFile = outputFile; } /** * Name of the private environment file, e.g. {@code http-client.private.env.json}. */ @Parameter(property = "ijhttp.private-env-file") public void setPrivateEnvironmentFile(File privateEnvironmentFile) { this.privateEnvironmentFile = privateEnvironmentFile; } /** * Private environment variables. * * @see #environmentVariables */ @Parameter(property = "ijhttp.private-env-variables") public void setPrivateEnvironmentVariables(List privateEnvironmentVariables) { this.privateEnvironmentVariables = privateEnvironmentVariables; } /** * Proxy URI. *

* Proxy setting in format {@code scheme://login:password@host:port}, scheme can be * socks for SOCKS or http for HTTP. */ @Parameter(property = "ijhttp.proxy") public void setProxy(String proxy) { this.proxy = proxy; } /** * When combined with {@code ijhttp.useMavenLogger=true}, prints all executed program output at * debug level instead of the default info level to the Maven logger. * * @see #setOutputFile(File) * @see #setUseMavenLogger(boolean) */ @Parameter(property = "ijhttp.quietLogs", defaultValue = "false") public void setQuietLogs(boolean quietLogs) { this.quietLogs = quietLogs; } /** * Creates report about execution in JUnit XML Format. Defaults to false. * * @see RunMojo#setReportPath(File) */ @Parameter(property = "ijhttp.report", defaultValue = "false") public void setReport(boolean report) { this.report = report; } /** * Path to a report folder. Default value {@code reports } in the current directory. * * @see RunMojo#setReport(boolean) */ @Parameter(property = "ijhttp.report-path") public void setReportPath(File reportPath) { this.reportPath = reportPath; } /** * Skip the execution. Defaults to false. */ @Parameter(property = "ijhttp.skip", defaultValue = "false") public void setSkip(boolean skip) { this.skip = skip; } /** * Number of milliseconds for socket read. Defaults to 10000. */ @Parameter(property = "ijhttp.socket-timeout") public void setSocketTimeout(Integer socketTimeout) { this.socketTimeout = socketTimeout; } /** * Number of milliseconds for execution. */ @Parameter(property = "ijhttp.timeout") public void setTimeout(Integer timeout) { this.timeout = timeout; } /** * When enabled, program standard and error output will be redirected to the Maven logger as * Info and Error level logs, respectively. If not enabled the traditional * behavior of program output being directed to standard {@code System.out} and {@code System.err} * is used. * * @see #setOutputFile(File) * @see #setQuietLogs(boolean) */ @Parameter(property = "ijhttp.useMavenLogger", defaultValue = "false") public void setUseMavenLogger(boolean useMavenLogger) { this.useMavenLogger = useMavenLogger; } /** * The working directory. Defaults to ${basedir}. */ @Parameter(property = "ijhttp.workingdir", defaultValue = "${basedir}") public void setWorkingDirectory(File workingDirectory) { this.workingDirectory = workingDirectory; } @VisibleForTesting CommandLine getCommandLine() throws IOException, MojoExecutionException { var httpClientCommandLine = new HttpClientCommandLine(); environment(httpClientCommandLine); executable(httpClientCommandLine); files(httpClientCommandLine); flags(httpClientCommandLine); logLevel(httpClientCommandLine); proxy(httpClientCommandLine); timeouts(httpClientCommandLine); return httpClientCommandLine.getCommandLine(); } private void environment(HttpClientCommandLine httpClientCommandLine) { if (nonNull(environmentName)) { httpClientCommandLine.environmentName(environmentName); } if (nonNull(environmentFile)) { httpClientCommandLine.environmentFile(environmentFile.toPath()); } if (nonNull(environmentVariables)) { httpClientCommandLine.environmentVariables(environmentVariables); } if (nonNull(privateEnvironmentFile)) { httpClientCommandLine.privateEnvironmentFile(privateEnvironmentFile.toPath()); } if (nonNull(privateEnvironmentVariables)) { httpClientCommandLine.privateEnvironmentVariables(privateEnvironmentVariables); } } private void executable(HttpClientCommandLine httpClientCommandLine) { httpClientCommandLine.executable(executable); } private void files(HttpClientCommandLine httpClientCommandLine) throws MojoExecutionException { if (isNull(directories) && isNull(files)) { throw new MojoExecutionException("files are required"); } if (nonNull(directories)) { httpClientCommandLine.directories( directories.stream().map(File::toPath).toArray(Path[]::new)); } if (nonNull(files)) { httpClientCommandLine.files(files.stream().map(File::toPath).toArray(Path[]::new)); } } private void flags(HttpClientCommandLine httpClientCommandLine) { httpClientCommandLine.dockerMode(dockerMode); httpClientCommandLine.insecure(insecure); httpClientCommandLine.report(report); if (nonNull(reportPath)) { httpClientCommandLine.reportPath(reportPath.toPath()); } } private void logLevel(HttpClientCommandLine httpClientCommandLine) { httpClientCommandLine.logLevel(logLevel); } private void proxy(HttpClientCommandLine httpClientCommandLine) { if (nonNull(proxy)) { httpClientCommandLine.proxy(proxy); } } private void timeouts(HttpClientCommandLine httpClientCommandLine) { if (nonNull(connectTimeout)) { httpClientCommandLine.connectTimeout(connectTimeout); } if (nonNull(socketTimeout)) { httpClientCommandLine.socketTimeout(socketTimeout); } } @VisibleForTesting Executor getExecutor() throws IOException, MojoExecutionException { var executor = new DefaultExecutor(); handleWatchdog(executor); handleWorkingDirectory(executor); return executor; } private void handleWatchdog(DefaultExecutor executor) { if (nonNull(timeout)) { var watchdog = new ExecuteWatchdog(timeout); executor.setWatchdog(watchdog); if (getLog().isDebugEnabled()) { getLog().debug(String.format("Set the watchdog (%s) ms", timeout)); } } } private void handleWorkingDirectory(DefaultExecutor executor) throws IOException, MojoExecutionException { if (nonNull(workingDirectory)) { if (!workingDirectory.exists()) { Files.createDirectory(workingDirectory.toPath()); } else if (!workingDirectory.isDirectory()) { throw new MojoExecutionException( "the working directory is a file: " + workingDirectory.getPath()); } executor.setWorkingDirectory(workingDirectory); } if (getLog().isDebugEnabled()) { getLog().debug("Working directory: " + executor.getWorkingDirectory()); } } private void runHttpClient(CommandLine commandLine, Executor executor) throws IOException { if (nonNull(outputFile)) { if (!outputFile.getParentFile().exists() && !outputFile.getParentFile().mkdirs()) { getLog().warn( "Could not create non existing parent directories for the log file: " + outputFile); } var outputStream = new FileOutputStream(outputFile); executor.setStreamHandler(new PumpStreamHandler(new BufferedOutputStream(outputStream))); if (getLog().isDebugEnabled()) { getLog().debug("Will redirect program output to the log file: " + outputFile); } try (outputStream) { executor.getStreamHandler().start(); executor.execute(commandLine); } finally { executor.getStreamHandler().stop(); } } else if (useMavenLogger) { var loggerErrStream = new LogOutputStream() { @Override protected void processLine(String line, int logLevel) { getLog().error(line); } }; var loggerOutStream = new LogOutputStream() { @Override protected void processLine(String line, int logLevel) { if (quietLogs) { getLog().debug(line); } else { getLog().info(line); } } }; executor.setStreamHandler(new PumpStreamHandler(loggerOutStream, loggerErrStream)); if (getLog().isDebugEnabled()) { getLog().debug("Will redirect program output to Maven logger"); } try (loggerErrStream; loggerOutStream) { executor.getStreamHandler().start(); executor.execute(commandLine); } finally { executor.getStreamHandler().stop(); } } else { executor.setStreamHandler(new PumpStreamHandler()); try { executor.getStreamHandler().start(); executor.execute(commandLine); } finally { executor.getStreamHandler().stop(); } } } }