From aeff27d9aba0ec8e77264d1ef2eefeae570bfad6 Mon Sep 17 00:00:00 2001 From: Constructor Date: Tue, 5 Nov 2024 03:12:00 +0100 Subject: [PATCH 1/9] TS-38628 Partially raw translation --- .../upload/teamscale/TeamscaleUploader.java | 6 +- .../agent/options/AgentOptionsTest.java | 8 +- .../options/TeamscaleProxyOptionsTest.java | 5 +- teamscale-client/build.gradle.kts | 1 + .../client/ClusteredTestDetails.java | 48 ---- .../teamscale/client/CommitDescriptor.java | 65 ----- .../client/FileLoggingInterceptor.java | 71 ----- .../java/com/teamscale/client/HttpUtils.java | 228 ---------------- .../teamscale/client/ITeamscaleService.java | 216 --------------- .../java/com/teamscale/client/JsonUtils.java | 75 ------ .../teamscale/client/PrioritizableTest.java | 62 ----- .../client/PrioritizableTestCluster.java | 60 ----- .../teamscale/client/ProcessInformation.java | 20 -- .../client/ProfilerConfiguration.java | 11 - .../com/teamscale/client/ProfilerInfo.java | 16 -- .../client/ProxySystemProperties.java | 164 ----------- .../com/teamscale/client/TeamscaleClient.java | 215 --------------- .../TeamscaleProxySystemProperties.java | 31 --- .../com/teamscale/client/TeamscaleServer.java | 184 ------------- .../client/TeamscaleServiceGenerator.java | 90 ------- .../java/com/teamscale/client/TestData.java | 79 ------ .../com/teamscale/client/TestDetails.java | 56 ---- .../teamscale/client/TestWithClusterId.java | 48 ---- .../com/teamscale/client/AntPatternUtils.kt} | 125 ++++----- .../teamscale/client/ClusteredTestDetails.kt | 48 ++++ .../com/teamscale/client/CommitDescriptor.kt | 56 ++++ .../com/teamscale/client/EReportFormat.kt} | 106 ++++---- .../teamscale/client/ETestImpactOptions.kt} | 7 +- .../client/FileLoggingInterceptor.kt | 69 +++++ .../com/teamscale/client/FileSystemUtils.kt} | 128 +++++---- .../kotlin/com/teamscale/client/HttpUtils.kt | 228 ++++++++++++++++ .../com/teamscale/client/ITeamscaleService.kt | 172 ++++++++++++ .../kotlin/com/teamscale/client/JsonUtils.kt | 82 ++++++ .../com/teamscale/client/PrioritizableTest.kt | 54 ++++ .../client/PrioritizableTestCluster.kt | 53 ++++ .../teamscale/client/ProcessInformation.kt | 11 + .../teamscale/client/ProfilerConfiguration.kt | 12 + .../com/teamscale/client/ProfilerInfo.kt | 9 + .../teamscale/client/ProfilerRegistration.kt} | 15 +- .../teamscale/client/ProxySystemProperties.kt | 78 ++++++ .../com/teamscale/client/StringUtils.kt} | 159 +++++------ .../com/teamscale/client/TeamscaleClient.kt | 254 ++++++++++++++++++ .../client/TeamscaleProxySystemProperties.kt | 22 ++ .../com/teamscale/client/TeamscaleServer.kt | 174 ++++++++++++ .../client/TeamscaleServiceGenerator.kt | 97 +++++++ .../kotlin/com/teamscale/client/TestData.kt | 76 ++++++ .../com/teamscale/client/TestDetails.kt | 42 +++ .../com/teamscale/client/TestWithClusterId.kt | 38 +++ .../client/ProxySystemPropertiesTest.java | 33 --- .../teamscale/client/TeamscaleServerTest.java | 22 -- ...mscaleServiceGeneratorProxyServerTest.java | 95 ------- .../com/teamscale/client/TestDataTest.java | 12 - .../client/ProxySystemPropertiesTest.kt | 33 +++ .../teamscale/client/TeamscaleServerTest.kt | 21 ++ ...eamscaleServiceGeneratorProxyServerTest.kt | 94 +++++++ .../com/teamscale/client/TestDataTest.kt | 10 + .../com/teamscale/TeamscaleUploadTask.kt | 2 +- .../kotlin/com/teamscale/config/Commit.kt | 20 +- .../com/teamscale/TeamscalePluginTest.kt | 6 +- teamscale-maven-plugin/pom.xml | 4 +- 60 files changed, 2024 insertions(+), 2202 deletions(-) delete mode 100644 teamscale-client/src/main/java/com/teamscale/client/ClusteredTestDetails.java delete mode 100644 teamscale-client/src/main/java/com/teamscale/client/CommitDescriptor.java delete mode 100644 teamscale-client/src/main/java/com/teamscale/client/FileLoggingInterceptor.java delete mode 100644 teamscale-client/src/main/java/com/teamscale/client/HttpUtils.java delete mode 100644 teamscale-client/src/main/java/com/teamscale/client/ITeamscaleService.java delete mode 100644 teamscale-client/src/main/java/com/teamscale/client/JsonUtils.java delete mode 100644 teamscale-client/src/main/java/com/teamscale/client/PrioritizableTest.java delete mode 100644 teamscale-client/src/main/java/com/teamscale/client/PrioritizableTestCluster.java delete mode 100644 teamscale-client/src/main/java/com/teamscale/client/ProcessInformation.java delete mode 100644 teamscale-client/src/main/java/com/teamscale/client/ProfilerConfiguration.java delete mode 100644 teamscale-client/src/main/java/com/teamscale/client/ProfilerInfo.java delete mode 100644 teamscale-client/src/main/java/com/teamscale/client/ProxySystemProperties.java delete mode 100644 teamscale-client/src/main/java/com/teamscale/client/TeamscaleClient.java delete mode 100644 teamscale-client/src/main/java/com/teamscale/client/TeamscaleProxySystemProperties.java delete mode 100644 teamscale-client/src/main/java/com/teamscale/client/TeamscaleServer.java delete mode 100644 teamscale-client/src/main/java/com/teamscale/client/TeamscaleServiceGenerator.java delete mode 100644 teamscale-client/src/main/java/com/teamscale/client/TestData.java delete mode 100644 teamscale-client/src/main/java/com/teamscale/client/TestDetails.java delete mode 100644 teamscale-client/src/main/java/com/teamscale/client/TestWithClusterId.java rename teamscale-client/src/main/{java/com/teamscale/client/AntPatternUtils.java => kotlin/com/teamscale/client/AntPatternUtils.kt} (54%) create mode 100644 teamscale-client/src/main/kotlin/com/teamscale/client/ClusteredTestDetails.kt create mode 100644 teamscale-client/src/main/kotlin/com/teamscale/client/CommitDescriptor.kt rename teamscale-client/src/main/{java/com/teamscale/client/EReportFormat.java => kotlin/com/teamscale/client/EReportFormat.kt} (62%) rename teamscale-client/src/main/{java/com/teamscale/client/ETestImpactOptions.java => kotlin/com/teamscale/client/ETestImpactOptions.kt} (93%) create mode 100644 teamscale-client/src/main/kotlin/com/teamscale/client/FileLoggingInterceptor.kt rename teamscale-client/src/main/{java/com/teamscale/client/FileSystemUtils.java => kotlin/com/teamscale/client/FileSystemUtils.kt} (50%) create mode 100644 teamscale-client/src/main/kotlin/com/teamscale/client/HttpUtils.kt create mode 100644 teamscale-client/src/main/kotlin/com/teamscale/client/ITeamscaleService.kt create mode 100644 teamscale-client/src/main/kotlin/com/teamscale/client/JsonUtils.kt create mode 100644 teamscale-client/src/main/kotlin/com/teamscale/client/PrioritizableTest.kt create mode 100644 teamscale-client/src/main/kotlin/com/teamscale/client/PrioritizableTestCluster.kt create mode 100644 teamscale-client/src/main/kotlin/com/teamscale/client/ProcessInformation.kt create mode 100644 teamscale-client/src/main/kotlin/com/teamscale/client/ProfilerConfiguration.kt create mode 100644 teamscale-client/src/main/kotlin/com/teamscale/client/ProfilerInfo.kt rename teamscale-client/src/main/{java/com/teamscale/client/ProfilerRegistration.java => kotlin/com/teamscale/client/ProfilerRegistration.kt} (54%) create mode 100644 teamscale-client/src/main/kotlin/com/teamscale/client/ProxySystemProperties.kt rename teamscale-client/src/main/{java/com/teamscale/client/StringUtils.java => kotlin/com/teamscale/client/StringUtils.kt} (56%) create mode 100644 teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleClient.kt create mode 100644 teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleProxySystemProperties.kt create mode 100644 teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServer.kt create mode 100644 teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServiceGenerator.kt create mode 100644 teamscale-client/src/main/kotlin/com/teamscale/client/TestData.kt create mode 100644 teamscale-client/src/main/kotlin/com/teamscale/client/TestDetails.kt create mode 100644 teamscale-client/src/main/kotlin/com/teamscale/client/TestWithClusterId.kt delete mode 100644 teamscale-client/src/test/java/com/teamscale/client/ProxySystemPropertiesTest.java delete mode 100644 teamscale-client/src/test/java/com/teamscale/client/TeamscaleServerTest.java delete mode 100644 teamscale-client/src/test/java/com/teamscale/client/TeamscaleServiceGeneratorProxyServerTest.java delete mode 100644 teamscale-client/src/test/java/com/teamscale/client/TestDataTest.java create mode 100644 teamscale-client/src/test/kotlin/com/teamscale/client/ProxySystemPropertiesTest.kt create mode 100644 teamscale-client/src/test/kotlin/com/teamscale/client/TeamscaleServerTest.kt create mode 100644 teamscale-client/src/test/kotlin/com/teamscale/client/TeamscaleServiceGeneratorProxyServerTest.kt create mode 100644 teamscale-client/src/test/kotlin/com/teamscale/client/TestDataTest.kt diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/upload/teamscale/TeamscaleUploader.java b/agent/src/main/java/com/teamscale/jacoco/agent/upload/teamscale/TeamscaleUploader.java index 3dc8e196b..25e9dc5d8 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/upload/teamscale/TeamscaleUploader.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/upload/teamscale/TeamscaleUploader.java @@ -5,6 +5,7 @@ import com.teamscale.client.EReportFormat; import com.teamscale.client.HttpUtils; import com.teamscale.client.ITeamscaleService; +import com.teamscale.client.ITeamscaleServiceKt; import com.teamscale.client.TeamscaleServer; import com.teamscale.client.TeamscaleServiceGenerator; import com.teamscale.jacoco.agent.upload.IUploadRetry; @@ -133,9 +134,8 @@ private boolean tryUploading(CoverageFile coverageFile, TeamscaleServer teamscal // Cannot be executed in the constructor as this causes issues in WildFly server // (See #100) ITeamscaleService api = TeamscaleServiceGenerator.createService(ITeamscaleService.class, - teamscaleServer.url, teamscaleServer.userName, teamscaleServer.userAccessToken, - HttpUtils.DEFAULT_READ_TIMEOUT, HttpUtils.DEFAULT_WRITE_TIMEOUT); - api.uploadReport(teamscaleServer.project, teamscaleServer.commit, teamscaleServer.revision, + teamscaleServer.url, teamscaleServer.userName, teamscaleServer.userAccessToken); + ITeamscaleServiceKt.uploadReport(api, teamscaleServer.project, teamscaleServer.commit, teamscaleServer.revision, teamscaleServer.repository, teamscaleServer.partition, EReportFormat.JACOCO, teamscaleServer.getMessage(), coverageFile.createFormRequestBody()); return true; diff --git a/agent/src/test/java/com/teamscale/jacoco/agent/options/AgentOptionsTest.java b/agent/src/test/java/com/teamscale/jacoco/agent/options/AgentOptionsTest.java index e35e053f5..203434dfb 100644 --- a/agent/src/test/java/com/teamscale/jacoco/agent/options/AgentOptionsTest.java +++ b/agent/src/test/java/com/teamscale/jacoco/agent/options/AgentOptionsTest.java @@ -447,7 +447,7 @@ private static AgentOptions parseProxyOptions(String otherOptionsString, ProxySy String proxyPasswordOption = String.format("proxy-%s-password=%s", protocol, expectedPassword); String optionsString = String.format("%s%s,%s,%s,%s", otherOptionsString, proxyHostOption, proxyPortOption, proxyUserOption, proxyPasswordOption); - if(passwordFile != null) { + if (passwordFile != null) { String proxyPasswordFileOption = String.format("proxy-password-file=%s", passwordFile.getAbsoluteFile()); optionsString += "," + proxyPasswordFileOption; } @@ -465,11 +465,7 @@ private void assertTeamscaleProxySystemPropertiesAreCorrect(ProxySystemPropertie } private void clearTeamscaleProxySystemProperties(ProxySystemProperties.Protocol protocol) { - TeamscaleProxySystemProperties teamscaleProxySystemProperties = new TeamscaleProxySystemProperties(protocol); - teamscaleProxySystemProperties.setProxyHost(""); - teamscaleProxySystemProperties.setProxyPort(""); - teamscaleProxySystemProperties.setProxyUser(""); - teamscaleProxySystemProperties.setProxyPassword(""); + new TeamscaleProxySystemProperties(protocol).clear(); } /** Returns the include filter predicate for the given filter expression. */ private static Predicate includeFilter(String filterString) throws Exception { diff --git a/agent/src/test/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptionsTest.java b/agent/src/test/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptionsTest.java index 44cf2e411..e1fb23b99 100644 --- a/agent/src/test/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptionsTest.java +++ b/agent/src/test/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptionsTest.java @@ -26,9 +26,6 @@ void testTeamscaleProxyOptionsFilledWithJVMOptionsOnInit() { assertThat(teamscaleProxyOptions.proxyUser).isEqualTo(expectedUser); assertThat(teamscaleProxyOptions.proxyPassword).isEqualTo(expectedPassword); - proxySystemProperties.setProxyHost(""); - proxySystemProperties.setProxyPort(""); - proxySystemProperties.setProxyUser(""); - proxySystemProperties.setProxyPassword(""); + proxySystemProperties.clear(); } } \ No newline at end of file diff --git a/teamscale-client/build.gradle.kts b/teamscale-client/build.gradle.kts index f0e2da8e7..952098003 100644 --- a/teamscale-client/build.gradle.kts +++ b/teamscale-client/build.gradle.kts @@ -3,6 +3,7 @@ plugins { com.teamscale.`java-convention` com.teamscale.coverage com.teamscale.publish + kotlin("jvm") } publishAs { diff --git a/teamscale-client/src/main/java/com/teamscale/client/ClusteredTestDetails.java b/teamscale-client/src/main/java/com/teamscale/client/ClusteredTestDetails.java deleted file mode 100644 index 13c7c6687..000000000 --- a/teamscale-client/src/main/java/com/teamscale/client/ClusteredTestDetails.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.teamscale.client; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * {@link TestDetails} with additional information about which cluster of tests the test case belongs to during - * prioritization. - */ -public class ClusteredTestDetails extends TestDetails { - - /** - * A unique identifier for the cluster this test should be prioritized within. If null the test gets assigned its - * own unique cluster. - */ - public String clusterId; - - /** - * The partition for the cluster this test should be prioritized within and the result will be uploaded to. - */ - public String partition; - - @JsonCreator - public ClusteredTestDetails(@JsonProperty("uniformPath") String uniformPath, - @JsonProperty("sourcePath") String sourcePath, @JsonProperty("content") String content, - @JsonProperty("clusterId") String clusterId, - @JsonProperty("partition") String partition) { - super(uniformPath, sourcePath, content); - this.clusterId = clusterId; - this.partition = partition; - } - - /** - * Creates clustered test details with the given additional {@link TestData}. - *

- * Use this to easily mark additional files or data as belonging to that test case. Whenever the given - * {@link TestData} changes, this test will be selected to be run by the TIA. - *

- * Example: For a test that reads test data from an XML file, you should pass the contents of that XML file as its - * test data. Then, whenever the XML is modified, the corresponding test will be run by the TIA. - */ - public static ClusteredTestDetails createWithTestData(String uniformPath, String sourcePath, TestData testData, - String clusterId, String partition) { - return new ClusteredTestDetails(uniformPath, sourcePath, testData.hash, clusterId, partition); - } - -} - diff --git a/teamscale-client/src/main/java/com/teamscale/client/CommitDescriptor.java b/teamscale-client/src/main/java/com/teamscale/client/CommitDescriptor.java deleted file mode 100644 index fee858ff7..000000000 --- a/teamscale-client/src/main/java/com/teamscale/client/CommitDescriptor.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.teamscale.client; - -import java.io.Serializable; -import java.util.Objects; - -/** Holds the branch and timestamp of a commit. */ -public class CommitDescriptor implements Serializable { - - /** Branch name of the commit. */ - public final String branchName; - - /** - * Timestamp of the commit. The timestamp is a string here because be also want to be able to handle HEAD and - * 123456p1. - */ - public final String timestamp; - - /** Constructor. */ - public CommitDescriptor(String branchName, String timestamp) { - this.branchName = branchName; - this.timestamp = timestamp; - } - - /** Constructor. */ - public CommitDescriptor(String branchName, long timestamp) { - this(branchName, String.valueOf(timestamp)); - } - - /** Parses the given commit descriptor string. */ - public static CommitDescriptor parse(String commit) { - if (commit == null) { - return null; - } - if (commit.contains(":")) { - String[] split = commit.split(":"); - return new CommitDescriptor(split[0], split[1]); - } else { - return new CommitDescriptor("master", commit); - } - } - - /** Returns a string representation of the commit in a Teamscale REST API compatible format. */ - @Override - public String toString() { - return branchName + ":" + timestamp; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - CommitDescriptor that = (CommitDescriptor) o; - return Objects.equals(branchName, that.branchName) && - Objects.equals(timestamp, that.timestamp); - } - - @Override - public int hashCode() { - return Objects.hash(branchName, timestamp); - } -} diff --git a/teamscale-client/src/main/java/com/teamscale/client/FileLoggingInterceptor.java b/teamscale-client/src/main/java/com/teamscale/client/FileLoggingInterceptor.java deleted file mode 100644 index 351f489a1..000000000 --- a/teamscale-client/src/main/java/com/teamscale/client/FileLoggingInterceptor.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.teamscale.client; - -import okhttp3.Interceptor; -import okhttp3.MediaType; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.ResponseBody; -import okio.Buffer; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.io.PrintWriter; - -/** - * OkHttpInterceptor which prints out the full request and server response of requests to a file. - */ -public class FileLoggingInterceptor implements Interceptor { - - private final File logfile; - - /** Constructor. */ - public FileLoggingInterceptor(File logfile) { - this.logfile = logfile; - } - - @Override - public Response intercept(Chain chain) throws IOException { - Request request = chain.request(); - - long requestStartTime = System.nanoTime(); - try (PrintWriter fileWriter = new PrintWriter(new FileWriter(logfile))) { - fileWriter.write(String.format("--> Sending request %s on %s %s%n%s%n", request.method(), request.url(), - chain.connection(), - request.headers())); - - Buffer requestBuffer = new Buffer(); - if (request.body() != null) { - request.body().writeTo(requestBuffer); - } - fileWriter.write(requestBuffer.readUtf8()); - - Response response = getResponse(chain, request, fileWriter); - - long requestEndTime = System.nanoTime(); - fileWriter.write(String - .format("<-- Received response for %s %s in %.1fms%n%s%n%n", response.code(), - response.request().url(), (requestEndTime - requestStartTime) / 1e6d, response.headers())); - - ResponseBody wrappedBody = null; - if (response.body() != null) { - MediaType contentType = response.body().contentType(); - String content = response.body().string(); - fileWriter.write(content); - - wrappedBody = ResponseBody.create(contentType, content); - } - return response.newBuilder().body(wrappedBody).build(); - } - } - - private Response getResponse(Chain chain, Request request, PrintWriter fileWriter) throws IOException { - try { - return chain.proceed(request); - } catch (Exception e) { - fileWriter.write("\n\nRequest failed!\n"); - e.printStackTrace(fileWriter); - throw e; - } - } -} diff --git a/teamscale-client/src/main/java/com/teamscale/client/HttpUtils.java b/teamscale-client/src/main/java/com/teamscale/client/HttpUtils.java deleted file mode 100644 index 1e4ab08bc..000000000 --- a/teamscale-client/src/main/java/com/teamscale/client/HttpUtils.java +++ /dev/null @@ -1,228 +0,0 @@ -package com.teamscale.client; - -import okhttp3.Authenticator; -import okhttp3.Credentials; -import okhttp3.Interceptor; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.ResponseBody; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import retrofit2.Response; -import retrofit2.Retrofit; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSession; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.Proxy; -import java.security.GeneralSecurityException; -import java.security.SecureRandom; -import java.security.cert.X509Certificate; -import java.time.Duration; -import java.util.Base64; -import java.util.function.Consumer; - -/** - * Utility functions to set up {@link Retrofit} and {@link OkHttpClient}. - */ -public class HttpUtils { - - private static final Logger LOGGER = LoggerFactory.getLogger(HttpUtils.class); - - /** - * Default read timeout in seconds. - */ - public static final Duration DEFAULT_READ_TIMEOUT = Duration.ofSeconds(60); - - /** - * Default write timeout in seconds. - */ - public static final Duration DEFAULT_WRITE_TIMEOUT = Duration.ofSeconds(60); - - /** - * HTTP header used for authenticating against a proxy server - */ - public static final String PROXY_AUTHORIZATION_HTTP_HEADER = "Proxy-Authorization"; - - /** Controls whether {@link OkHttpClient}s built with this class will validate SSL certificates. */ - private static boolean shouldValidateSsl = true; - - /** @see #shouldValidateSsl */ - public static void setShouldValidateSsl(boolean shouldValidateSsl) { - HttpUtils.shouldValidateSsl = shouldValidateSsl; - } - - /** - * Creates a new {@link Retrofit} with proper defaults. The instance and the corresponding {@link OkHttpClient} can - * be customized with the given action. Read and write timeouts are set according to the default values. - */ - public static Retrofit createRetrofit(Consumer retrofitBuilderAction, - Consumer okHttpBuilderAction) { - return createRetrofit(retrofitBuilderAction, okHttpBuilderAction, DEFAULT_READ_TIMEOUT, DEFAULT_WRITE_TIMEOUT); - } - - /** - * Creates a new {@link Retrofit} with proper defaults. The instance and the corresponding {@link OkHttpClient} can - * be customized with the given action. Timeouts for reading and writing can be customized. - */ - public static Retrofit createRetrofit(Consumer retrofitBuilderAction, - Consumer okHttpBuilderAction, Duration readTimeout, - Duration writeTimeout) { - OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder(); - setTimeouts(httpClientBuilder, readTimeout, writeTimeout); - setUpSslValidation(httpClientBuilder); - setUpProxyServer(httpClientBuilder); - okHttpBuilderAction.accept(httpClientBuilder); - - Retrofit.Builder builder = new Retrofit.Builder().client(httpClientBuilder.build()); - retrofitBuilderAction.accept(builder); - return builder.build(); - } - - /** - * Java and/or OkHttp do not pick up the http.proxy* and https.proxy* system properties reliably. We need to teach - * OkHttp to always pick them up. - *

- * Sources: https://memorynotfound.com/configure-http-proxy-settings-java/ - * & - * https://stackoverflow.com/a/35567936 - */ - private static void setUpProxyServer(OkHttpClient.Builder httpClientBuilder) { - boolean setHttpsProxyWasSuccessful = setUpProxyServerForProtocol(ProxySystemProperties.Protocol.HTTPS, - httpClientBuilder); - if (!setHttpsProxyWasSuccessful) { - setUpProxyServerForProtocol(ProxySystemProperties.Protocol.HTTP, httpClientBuilder); - } - } - - private static boolean setUpProxyServerForProtocol(ProxySystemProperties.Protocol protocol, - OkHttpClient.Builder httpClientBuilder) { - TeamscaleProxySystemProperties teamscaleProxySystemProperties = new TeamscaleProxySystemProperties(protocol); - try { - if (!teamscaleProxySystemProperties.proxyServerIsSet()) { - return false; - } - - useProxyServer(httpClientBuilder, teamscaleProxySystemProperties.getProxyHost(), - teamscaleProxySystemProperties.getProxyPort()); - - } catch (ProxySystemProperties.IncorrectPortFormatException e) - { - LOGGER.warn(e.getMessage()); - return false; - } - - if (teamscaleProxySystemProperties.proxyAuthIsSet()) { - useProxyAuthenticator(httpClientBuilder, teamscaleProxySystemProperties.getProxyUser(), teamscaleProxySystemProperties.getProxyPassword()); - } - - return true; - } - - private static void useProxyServer(OkHttpClient.Builder httpClientBuilder, String proxyHost, int proxyPort) { - httpClientBuilder.proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort))); - } - - private static void useProxyAuthenticator(OkHttpClient.Builder httpClientBuilder, String user, String password) { - Authenticator proxyAuthenticator = (route, response) -> { - String credential = Credentials.basic(user, password); - return response.request().newBuilder() - .header(PROXY_AUTHORIZATION_HTTP_HEADER, credential) - .build(); - }; - httpClientBuilder.proxyAuthenticator(proxyAuthenticator); - } - - - /** - * Sets sensible defaults for the {@link OkHttpClient}. - */ - private static void setTimeouts(OkHttpClient.Builder builder, Duration readTimeout, Duration writeTimeout) { - builder.connectTimeout(Duration.ofSeconds(60)); - builder.readTimeout(readTimeout); - builder.writeTimeout(writeTimeout); - } - - /** - * Enables or disables SSL certificate validation for the {@link Retrofit} instance - */ - private static void setUpSslValidation(OkHttpClient.Builder builder) { - if (shouldValidateSsl) { - // this is the default behaviour of OkHttp, so we don't need to do anything - return; - } - - SSLSocketFactory sslSocketFactory; - try { - SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, new TrustManager[]{TrustAllCertificatesManager.INSTANCE}, new SecureRandom()); - sslSocketFactory = sslContext.getSocketFactory(); - } catch (GeneralSecurityException e) { - LOGGER.error("Could not disable SSL certificate validation. Leaving it enabled", e); - return; - } - - // this causes OkHttp to accept all certificates - builder.sslSocketFactory(sslSocketFactory, TrustAllCertificatesManager.INSTANCE); - // this causes it to ignore invalid host names in the certificates - builder.hostnameVerifier((String hostName, SSLSession session) -> true); - } - - /** - * A simple implementation of {@link X509TrustManager} that simple trusts every certificate. - */ - public static class TrustAllCertificatesManager implements X509TrustManager { - - /** Singleton instance. */ - /*package*/ static final TrustAllCertificatesManager INSTANCE = new TrustAllCertificatesManager(); - - /** Returns null. */ - @Override - public X509Certificate[] getAcceptedIssuers() { - return new X509Certificate[0]; - } - - /** Does nothing. */ - @Override - public void checkServerTrusted(X509Certificate[] certs, String authType) { - // Nothing to do - } - - /** Does nothing. */ - @Override - public void checkClientTrusted(X509Certificate[] certs, String authType) { - // Nothing to do - } - - } - - /** - * Returns the error body of the given response or a replacement string in case it is null. - */ - public static String getErrorBodyStringSafe(Response response) throws IOException { - ResponseBody errorBody = response.errorBody(); - if (errorBody == null) { - return ""; - } - return errorBody.string(); - } - - /** - * Returns an interceptor, which adds a basic auth header to a request. - */ - public static Interceptor getBasicAuthInterceptor(String username, String password) { - String credentials = username + ":" + password; - String basic = "Basic " + Base64.getEncoder().encodeToString(credentials.getBytes()); - - return chain -> { - Request newRequest = chain.request().newBuilder().header("Authorization", basic).build(); - return chain.proceed(newRequest); - }; - } - -} diff --git a/teamscale-client/src/main/java/com/teamscale/client/ITeamscaleService.java b/teamscale-client/src/main/java/com/teamscale/client/ITeamscaleService.java deleted file mode 100644 index 565966167..000000000 --- a/teamscale-client/src/main/java/com/teamscale/client/ITeamscaleService.java +++ /dev/null @@ -1,216 +0,0 @@ -package com.teamscale.client; - -import okhttp3.MultipartBody; -import okhttp3.RequestBody; -import okhttp3.ResponseBody; -import retrofit2.Call; -import retrofit2.Response; -import retrofit2.Retrofit; -import retrofit2.http.Body; -import retrofit2.http.DELETE; -import retrofit2.http.GET; -import retrofit2.http.Multipart; -import retrofit2.http.POST; -import retrofit2.http.PUT; -import retrofit2.http.Part; -import retrofit2.http.Path; -import retrofit2.http.Query; - -import java.io.IOException; -import java.util.List; - -/** {@link Retrofit} API specification for Teamscale. */ -public interface ITeamscaleService { - - /** - * Report upload API. - * - * @param commit A branch and timestamp to upload the report to. Can be null if revision is specified. - * @param moveToLastCommit Whether to move the upload timestamp to right after the last commit - * @param revision This parameter allows to pass a revision instead of a timestamp. Can be null if a - * timestamp is given. - * @param partition The name of the logical partition to store the results into. All existing data in this - * partition will be invalidated. A partition typically corresponds to one analysis run, - * i.e. if there are two independent builds/runs, they must use different partitions. - * @apiNote How to Upload - * External Analysis Results to Teamscale for details. - */ - @Multipart - @POST("api/v5.9.0/projects/{projectAliasOrId}/external-analysis/session/auto-create/report") - Call uploadExternalReport( - @Path("projectAliasOrId") String projectAliasOrId, - @Query("format") String format, - @Query("t") CommitDescriptor commit, - @Query("revision") String revision, - @Query("repository") String repository, - @Query("movetolastcommit") Boolean moveToLastCommit, - @Query("partition") String partition, - @Query("message") String message, - @Part("report") RequestBody report - ); - - /** - * Report upload API with {@link EReportFormat}. - * - * @see #uploadExternalReport(String, String, CommitDescriptor, String, String, Boolean, String, String, - * RequestBody) - */ - default Call uploadExternalReport( - String projectName, - EReportFormat format, - CommitDescriptor commit, - String revision, - String repository, - Boolean moveToLastCommit, - String partition, - String message, - RequestBody report - ) { - return uploadExternalReport(projectName, format.name(), commit, revision, repository, moveToLastCommit, - partition, message, report); - } - - /** - * Report upload API for multiple reports at once. - * - * @see #uploadExternalReport(String, String, CommitDescriptor, String, String, Boolean, String, String, - * RequestBody) - */ - @Multipart - @POST("api/v5.9.0/projects/{projectName}/external-analysis/session/auto-create/report") - Call uploadExternalReports( - @Path("projectName") String projectName, - @Query("format") EReportFormat format, - @Query("t") CommitDescriptor commit, - @Query("revision") String revision, - @Query("repository") String repository, - @Query("movetolastcommit") boolean moveToLastCommit, - @Query("partition") String partition, - @Query("message") String message, - @Part List report - ); - - /** - * Report upload API for multiple reports at once. This is an overloaded version that takes a string as report - * format so that consumers can add support for new report formats without the requirement that the teamscale-client - * needs to be adjusted beforehand. - * - * @see #uploadExternalReport(String, String, CommitDescriptor, String, String, Boolean, String, String, - * RequestBody) - */ - @Multipart - @POST("api/v5.9.0/projects/{projectName}/external-analysis/session/auto-create/report") - Call uploadExternalReports( - @Path("projectName") String projectName, - @Query("format") String format, - @Query("t") CommitDescriptor commit, - @Query("revision") String revision, - @Query("repository") String repository, - @Query("movetolastcommit") boolean moveToLastCommit, - @Query("partition") String partition, - @Query("message") String message, - @Part List report - ); - - /** Retrieve clustered impacted tests based on the given available tests and baseline timestamp. */ - @PUT("api/v9.4.0/projects/{projectName}/impacted-tests") - Call> getImpactedTests( - @Path("projectName") String projectName, - @Query("baseline") String baseline, - @Query("baseline-revision") String baselineRevision, - @Query("end") CommitDescriptor end, - @Query("end-revision") String endRevision, - @Query("repository") String repository, - @Query("partitions") List partitions, - @Query("include-non-impacted") boolean includeNonImpacted, - @Query("include-failed-and-skipped") boolean includeFailedAndSkippedTests, - @Query("ensure-processed") boolean ensureProcessed, - @Query("include-added-tests") boolean includeAddedTests, - @Body List availableTests - ); - - /** Retrieve unclustered impacted tests based on all tests known to Teamscale and the given baseline timestamp. */ - @GET("api/v9.4.0/projects/{projectName}/impacted-tests") - Call> getImpactedTests( - @Path("projectName") String projectName, - @Query("baseline") String baseline, - @Query("baseline-revision") String baselineRevision, - @Query("end") CommitDescriptor end, - @Query("end-revision") String endRevision, - @Query("repository") String repository, - @Query("partitions") List partitions, - @Query("include-non-impacted") boolean includeNonImpacted, - @Query("include-failed-and-skipped") boolean includeFailedAndSkippedTests, - @Query("ensure-processed") boolean ensureProcessed, - @Query("include-added-tests") boolean includeAddedTests - ); - - /** Registers a profiler to Teamscale and returns the profiler configuration it should be started with. */ - @POST("api/v9.4.0/running-profilers") - Call registerProfiler( - @Query("configuration-id") String configurationId, - @Body ProcessInformation processInformation - ); - - /** Updates the profiler infos and sets the profiler to still alive. */ - @PUT("api/v9.4.0/running-profilers/{profilerId}") - Call sendHeartbeat( - @Path("profilerId") String profilerId, - @Body ProfilerInfo profilerInfo - ); - - /** Removes the profiler identified by given ID. */ - @DELETE("api/v9.4.0/running-profilers/{profilerId}") - Call unregisterProfiler(@Path("profilerId") String profilerId); - - /** - * Uploads the given report body to Teamscale as blocking call with movetolastcommit set to false. - * - * @return Returns the request body if successful, otherwise throws an IOException. - */ - default String uploadReport( - String projectName, - CommitDescriptor commit, - String revision, - String repository, - String partition, - EReportFormat reportFormat, - String message, - RequestBody report - ) throws IOException { - Boolean moveToLastCommit = false; - if (revision != null) { - // When uploading to a revision, we don't need commit adjustment. - commit = null; - moveToLastCommit = null; - } - - try { - Response response = uploadExternalReport( - projectName, - reportFormat, - commit, - revision, - repository, - moveToLastCommit, - partition, - message, - report - ).execute(); - - ResponseBody body = response.body(); - if (response.isSuccessful()) { - if (body == null) { - return ""; - } - return body.string(); - } - - String errorBody = HttpUtils.getErrorBodyStringSafe(response); - throw new IOException( - "Request failed with error code " + response.code() + ". Response body: " + errorBody); - } catch (IOException e) { - throw new IOException("Failed to upload report. " + e.getMessage(), e); - } - } -} diff --git a/teamscale-client/src/main/java/com/teamscale/client/JsonUtils.java b/teamscale-client/src/main/java/com/teamscale/client/JsonUtils.java deleted file mode 100644 index d08a3c463..000000000 --- a/teamscale-client/src/main/java/com/teamscale/client/JsonUtils.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.teamscale.client; - -import com.fasterxml.jackson.annotation.JsonAutoDetect; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.PropertyAccessor; -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.json.JsonMapper; - -import java.io.File; -import java.io.IOException; -import java.util.List; - -/** - * Utility class for serializing and deserializing JSON using Jackson. - */ -public class JsonUtils { - - /** - * Jackson ObjectMapper that is used for serializing and deserializing JSON objects. The visibility settings of the - * OBJECT_MAPPER are configured to include all fields when serializing or deserializing objects, regardless of their - * visibility modifiers (public, private, etc.). - */ - public static final ObjectMapper OBJECT_MAPPER = JsonMapper.builder() - .visibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE) - .visibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .serializationInclusion(JsonInclude.Include.NON_NULL) - .build(); - - /** - * Creates a new instance of {@link JsonFactory} using the default {@link ObjectMapper}. - */ - public static JsonFactory createFactory() { - return new JsonFactory(OBJECT_MAPPER); - } - - /** - * Deserializes a JSON string into an object of the given class. - */ - public static T deserialize(String json, Class clazz) throws JsonProcessingException { - return OBJECT_MAPPER.readValue(json, clazz); - } - - /** - * Deserializes the contents of the given file into an object of the given class. - */ - public static T deserializeFile(File file, Class clazz) throws IOException { - return OBJECT_MAPPER.readValue(file, clazz); - } - - /** - * Deserializes a JSON string into a list of objects of the given class. - */ - public static List deserializeList(String json, Class elementClass) throws JsonProcessingException { - return OBJECT_MAPPER.readValue(json, - OBJECT_MAPPER.getTypeFactory().constructCollectionLikeType(List.class, elementClass)); - } - - /** - * Serializes an object into its JSON representation. - */ - public static String serialize(Object value) throws JsonProcessingException { - return OBJECT_MAPPER.writeValueAsString(value); - } - - /** - * Serializes an object to a file with pretty printing enabled. - */ - public static void serializeToFile(File file, T value) throws IOException { - OBJECT_MAPPER.writer().withDefaultPrettyPrinter().writeValue(file, value); - } -} diff --git a/teamscale-client/src/main/java/com/teamscale/client/PrioritizableTest.java b/teamscale-client/src/main/java/com/teamscale/client/PrioritizableTest.java deleted file mode 100644 index d634c50fc..000000000 --- a/teamscale-client/src/main/java/com/teamscale/client/PrioritizableTest.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.teamscale.client; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.StringJoiner; - -/** - * {@link TestDetails} with information about their partition as well as tracking data used during prioritization of - * tests. Two instances are considered equal if the test details are equals. - */ -public class PrioritizableTest { - - /** The uniform path without the "-test-execution" or "-execution-unit-" prefix */ - public String testName; - - /** The uniform path of the test including the "-test-execution" or "-execution-unit-" prefix. */ - public String uniformPath; - - /** The reason the test has been selected. */ - public String selectionReason; - - /** Partition of the test. */ - public String partition; - - /** - * Duration in ms. May be null if not set. This can happen when the uploaded testwise coverage data does not include - * duration information or for new tests that have not been executed yet. - */ - public Long durationInMs; - - /** - * The score determined by the TIA algorithm. The value is guaranteed to be positive. Higher values describe a - * higher probability of the test to detect potential bugs. The value can only express a relative importance - * compared to other scores of the same request. It makes no sense to compare the score against absolute values. - */ - @JsonProperty("currentScore") - public double score; - - /** - * Field for storing the tests rank. The rank is the 1-based index of the test in the prioritized list. - */ - public int rank; - - @JsonCreator - public PrioritizableTest(@JsonProperty("testName") String testName) { - this.testName = testName; - } - - @Override - public String toString() { - return new StringJoiner(", ", PrioritizableTest.class.getSimpleName() + "[", "]") - .add("testName='" + testName + "'") - .add("uniformPath='" + uniformPath + "'") - .add("selectionReason='" + selectionReason + "'") - .add("partition='" + partition + "'") - .add("durationInMs=" + durationInMs) - .add("score=" + score) - .add("rank=" + rank) - .toString(); - } -} diff --git a/teamscale-client/src/main/java/com/teamscale/client/PrioritizableTestCluster.java b/teamscale-client/src/main/java/com/teamscale/client/PrioritizableTestCluster.java deleted file mode 100644 index 61bb23f31..000000000 --- a/teamscale-client/src/main/java/com/teamscale/client/PrioritizableTestCluster.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.teamscale.client; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.List; -import java.util.StringJoiner; - -/** - * A {@link PrioritizableTestCluster} represents an ordered {@link List} of {@link PrioritizableTest}s which should be - * executed together to avoid overhead. The order of the {@link PrioritizableTest}s is determined by the prioritization - * of the {@link PrioritizableTest}s w.r.t. to each other. - *

- * A {@link PrioritizableTestCluster} assumes that possibly resource intensive setup or teardown operations (e.g. a - * class containing a method annotated with {@code BeforeClass} in JUnit4 or {@code BeforeAll} in JUnit5) can be - * executed once for a {@link PrioritizableTestCluster} instead of executing them for each {@link PrioritizableTest}. - */ -public class PrioritizableTestCluster { - - /** - * The unique cluster id to which all {@link PrioritizableTest}s belong. - * - * @see ClusteredTestDetails#clusterId - */ - public String clusterId; - - /** - * The score determined by the TIA algorithm. The value is guaranteed to be positive. Higher values describe a - * higher probability of the test to detect potential bugs. The value can only express a relative importance - * compared to other scores of the same request. It makes no sense to compare the score against absolute values. - * The value is 0 if no availableTests are given. - */ - @JsonProperty("currentScore") - public double score; - - /** - * Field for storing the tests rank. The rank is the 1-based index of the test - * in the prioritized list. - */ - public int rank; - - /** The {@link PrioritizableTest}s in this cluster. */ - public List tests; - - @JsonCreator - public PrioritizableTestCluster(@JsonProperty("clusterId") String clusterId, @JsonProperty("tests") List tests) { - this.clusterId = clusterId; - this.tests = tests; - } - - @Override - public String toString() { - return new StringJoiner(", ", PrioritizableTestCluster.class.getSimpleName() + "[", "]") - .add("clusterId='" + clusterId + "'") - .add("score=" + score) - .add("rank=" + rank) - .add("tests=" + tests) - .toString(); - } -} diff --git a/teamscale-client/src/main/java/com/teamscale/client/ProcessInformation.java b/teamscale-client/src/main/java/com/teamscale/client/ProcessInformation.java deleted file mode 100644 index 078e742b7..000000000 --- a/teamscale-client/src/main/java/com/teamscale/client/ProcessInformation.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.teamscale.client; - -/** Information about the process and machine the profiler is attached to. */ -public class ProcessInformation { - - /** Hostname of the machine it is running on */ - public final String hostname; - - /** Profiled PID */ - public final String pid; - - /** The timestamp at which the process was started. */ - public final long startedAtTimestamp; - - public ProcessInformation(String hostname, String pid, long startedAtTimestamp) { - this.hostname = hostname; - this.pid = pid; - this.startedAtTimestamp = startedAtTimestamp; - } -} diff --git a/teamscale-client/src/main/java/com/teamscale/client/ProfilerConfiguration.java b/teamscale-client/src/main/java/com/teamscale/client/ProfilerConfiguration.java deleted file mode 100644 index 222c10174..000000000 --- a/teamscale-client/src/main/java/com/teamscale/client/ProfilerConfiguration.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.teamscale.client; - -/** Configuration options for a profiler. */ -public class ProfilerConfiguration { - - /** The ID if this configuration. */ - public String configurationId; - - /** The options that should be applied to the profiler. */ - public String configurationOptions; -} diff --git a/teamscale-client/src/main/java/com/teamscale/client/ProfilerInfo.java b/teamscale-client/src/main/java/com/teamscale/client/ProfilerInfo.java deleted file mode 100644 index badff0cd6..000000000 --- a/teamscale-client/src/main/java/com/teamscale/client/ProfilerInfo.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.teamscale.client; - -/** Information about the profiler including the process it is attached to as well as the configuration it is running with. */ -public class ProfilerInfo { - - /** Information about the machine and process the profiler is running on. */ - public ProcessInformation processInformation; - - /** Concrete config that the profiler is running with. */ - public ProfilerConfiguration profilerConfiguration; - - public ProfilerInfo(ProcessInformation processInformation, ProfilerConfiguration profilerConfiguration) { - this.processInformation = processInformation; - this.profilerConfiguration = profilerConfiguration; - } -} diff --git a/teamscale-client/src/main/java/com/teamscale/client/ProxySystemProperties.java b/teamscale-client/src/main/java/com/teamscale/client/ProxySystemProperties.java deleted file mode 100644 index a327069ec..000000000 --- a/teamscale-client/src/main/java/com/teamscale/client/ProxySystemProperties.java +++ /dev/null @@ -1,164 +0,0 @@ -package com.teamscale.client; - -import org.jetbrains.annotations.NotNull; - -/** - * Reads and writes Java system properties values for - *

- * or the corresponding HTTPS counterpart (starting with https instead of http). - * These values set the proxy server and credentials that should be used later to reach Teamscale. - */ -public class ProxySystemProperties { - - private static final String PROXY_HOST_SYSTEM_PROPERTY = ".proxyHost"; - private static final String PROXY_PORT_SYSTEM_PROPERTY = ".proxyPort"; - private static final String PROXY_USER_SYSTEM_PROPERTY = ".proxyUser"; - private static final String PROXY_PASSWORD_SYSTEM_PROPERTY = ".proxyPassword"; - - private final Protocol protocol; - - /** - * Indicates, whether the {@link ProxySystemProperties} should return values for the http.proxy* system properties - * or the https.proxy* ones - */ - public enum Protocol { - HTTP, - HTTPS; - - @Override - public String toString() { - return name().toLowerCase(); - } - } - - /** - * @return a prefix to the system properties. Used in {@link TeamscaleProxySystemProperties} to differentiate them - * from the JVM system properties for proxies. - * */ - @NotNull - protected String getPropertyPrefix() { - return ""; - } - - /** - * @param protocol Indicates, whether the {@link ProxySystemProperties} should use values for the http.proxy* system - * properties or the https.proxy* ones - */ - public ProxySystemProperties(Protocol protocol) { - this.protocol = protocol; - } - - /** - * Checks whether proxyHost and proxyPort are set - */ - public boolean proxyServerIsSet() throws IncorrectPortFormatException { - return !StringUtils.isEmpty(getProxyHost()) && getProxyPort() > 0; - } - - /** Checks whether proxyUser and proxyPassword are set */ - public boolean proxyAuthIsSet() { - return !StringUtils.isEmpty(getProxyUser()) && !StringUtils.isEmpty(getProxyPassword()); - } - - /** @return the http(s).proxyHost system variable */ - public String getProxyHost() { - return System.getProperty(getProxyHostSystemPropertyName()); - } - - /** @return the http(s).proxyPort system variable. Returns -1 if no or an invalid port was set. */ - public int getProxyPort() throws IncorrectPortFormatException { - return parsePort(System.getProperty(getProxyPortSystemPropertyName())); - } - - /** Set the http(s).proxyHost system variable. */ - public void setProxyHost(String proxyHost) { - System.setProperty(getProxyHostSystemPropertyName(), proxyHost); - } - - /** @return the name of the system property specifying the proxy host. */ - @NotNull - protected String getProxyHostSystemPropertyName() { - return getPropertyPrefix() + protocol + PROXY_HOST_SYSTEM_PROPERTY; - } - - /** Set the http(s).proxyPort system variable. */ - public void setProxyPort(int proxyPort) { - setProxyPort(proxyPort + ""); - } - - /** Set the http(s).proxyPort system variable. */ - public void setProxyPort(String proxyPort) { - System.setProperty(getProxyPortSystemPropertyName(), proxyPort); - } - - /** Removes the http(s).proxyPort system variable. For testing. */ - /*package*/ void removeProxyPort() { - System.clearProperty(getProxyPortSystemPropertyName()); - } - - /** @return the name of the system property specifying the proxy port. */ - @NotNull - protected String getProxyPortSystemPropertyName() { - return getPropertyPrefix() + protocol + PROXY_PORT_SYSTEM_PROPERTY; - } - - /** @return the http(s).proxyUser system variable. */ - public String getProxyUser() { - return System.getProperty(getProxyUserSystemPropertyName()); - } - - /** Set the http(s).proxyUser system variable. */ - public void setProxyUser(String proxyUser) { - System.setProperty(getProxyUserSystemPropertyName(), proxyUser); - } - - /** @return the name of the system property specifying the proxy user. */ - @NotNull - protected String getProxyUserSystemPropertyName() { - return getPropertyPrefix() + protocol + PROXY_USER_SYSTEM_PROPERTY; - } - - /** @return the http(s).proxyPassword system variable. */ - public String getProxyPassword() { - return System.getProperty(getProxyPasswordSystemPropertyName()); - } - - - /** Set the http(s).proxyPassword system variable. */ - public void setProxyPassword(String proxyPassword) { - System.setProperty(getProxyPasswordSystemPropertyName(), proxyPassword); - } - - /** @return the name of the system property specifying the proxy password. */ - @NotNull - protected String getProxyPasswordSystemPropertyName() { - return getPropertyPrefix() + protocol + PROXY_PASSWORD_SYSTEM_PROPERTY; - } - - /** Exception thrown if the port is in an unknown format and cannot be read from the system properties. */ - public static class IncorrectPortFormatException extends IllegalArgumentException { - - IncorrectPortFormatException(String message, Throwable cause) { - super(message, cause); - } - } - - /** Parses the given port string. Returns -1 if the string is null or not a valid number. */ - private int parsePort(String portString) throws IncorrectPortFormatException { - if (StringUtils.isEmpty(portString)) { - return -1; - } - - try { - return Integer.parseInt(portString); - } catch (NumberFormatException e) { - throw new IncorrectPortFormatException("Could not parse proxy port \"" + portString + - "\" set via \"" + getProxyPortSystemPropertyName() + "\"", e); - } - } -} diff --git a/teamscale-client/src/main/java/com/teamscale/client/TeamscaleClient.java b/teamscale-client/src/main/java/com/teamscale/client/TeamscaleClient.java deleted file mode 100644 index 94731d468..000000000 --- a/teamscale-client/src/main/java/com/teamscale/client/TeamscaleClient.java +++ /dev/null @@ -1,215 +0,0 @@ -package com.teamscale.client; - -import static com.teamscale.client.ETestImpactOptions.ENSURE_PROCESSED; -import static com.teamscale.client.ETestImpactOptions.INCLUDE_ADDED_TESTS; -import static com.teamscale.client.ETestImpactOptions.INCLUDE_FAILED_AND_SKIPPED; -import static com.teamscale.client.ETestImpactOptions.INCLUDE_NON_IMPACTED; - -import java.io.File; -import java.io.IOException; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.EnumSet; -import java.util.List; -import java.util.stream.Collectors; - -import okhttp3.HttpUrl; -import okhttp3.MultipartBody; -import okhttp3.RequestBody; -import okhttp3.ResponseBody; -import retrofit2.Response; - -/** Helper class to interact with Teamscale. */ -public class TeamscaleClient { - - /** Teamscale service implementation. */ - public final ITeamscaleService service; - - /** The project ID within Teamscale. */ - private final String projectId; - - /** Constructor with parameters for read and write timeout in seconds. */ - public TeamscaleClient(String baseUrl, String user, String accessToken, String projectId, Duration readTimeout, - Duration writeTimeout) { - this.projectId = projectId; - service = TeamscaleServiceGenerator - .createService(ITeamscaleService.class, HttpUrl.parse(baseUrl), user, accessToken, readTimeout, - writeTimeout); - } - - /** Constructor. */ - public TeamscaleClient(String baseUrl, String user, String accessToken, String projectId) { - this.projectId = projectId; - service = TeamscaleServiceGenerator - .createService(ITeamscaleService.class, HttpUrl.parse(baseUrl), user, accessToken, - HttpUtils.DEFAULT_READ_TIMEOUT, HttpUtils.DEFAULT_WRITE_TIMEOUT); - } - - /** Constructor with parameters for read and write timeout in seconds and logfile. */ - public TeamscaleClient(String baseUrl, String user, String accessToken, String projectId, File logfile, - Duration readTimeout, Duration writeTimeout) { - this.projectId = projectId; - service = TeamscaleServiceGenerator - .createServiceWithRequestLogging(ITeamscaleService.class, HttpUrl.parse(baseUrl), user, accessToken, - logfile, readTimeout, writeTimeout); - } - - /** Constructor with logfile. */ - public TeamscaleClient(String baseUrl, String user, String accessToken, String projectId, File logfile) { - this.projectId = projectId; - service = TeamscaleServiceGenerator - .createServiceWithRequestLogging(ITeamscaleService.class, HttpUrl.parse(baseUrl), user, accessToken, - logfile, HttpUtils.DEFAULT_READ_TIMEOUT, HttpUtils.DEFAULT_WRITE_TIMEOUT); - } - - /** - * Tries to retrieve the impacted tests from Teamscale. This should be used in a CI environment, because it ensures - * that the given commit has been processed by Teamscale and also considers previous failing tests for - * re-execution. - * - * @param availableTests A list of tests that is locally available for execution. This allows TIA to consider newly - * added tests in addition to those that are already known and allows to filter e.g. if the - * user has already selected a subset of relevant tests. This can be null to - * indicate that only tests known to Teamscale should be suggested. - * @param baseline The baseline timestamp AFTER which changes should be considered. Changes that happened - * exactly at the baseline will be excluded. In case you want to retrieve impacted tests for a - * single commit with a known timestamp you can append a "p1" suffix to the - * timestamp to indicate that you are interested in the changes that happened after the parent - * of the given commit. - * @param baselineRevision Same as baseline but accepts a revision (e.g. git SHA1) instead of a branch and timestamp - * @param endCommit The last commit for which changes should be considered. - * @param endRevision Same as endCommit but accepts a revision (e.g. git SHA1) instead of a branch and timestamp - * @param repository The repository id in your Teamscale project which Teamscale should use to look up the revision, if given. - * Null or empty will lead to a lookup in all repositories in the Teamscale project. - * @param partitions The partitions that should be considered for retrieving impacted tests. Can be - * null to indicate that tests from all partitions should be returned. - * @return A list of test clusters to execute. If availableTests is null, a single dummy cluster is returned with - * all prioritized tests. - */ - public Response> getImpactedTests( - List availableTests, - String baseline, - String baselineRevision, - CommitDescriptor endCommit, - String endRevision, - String repository, - List partitions, - boolean includeNonImpacted, - boolean includeAddedTests, boolean includeFailedAndSkipped) throws IOException { - List selectedOptions = new ArrayList<>(Collections.singletonList(ENSURE_PROCESSED)); - if (includeNonImpacted) { - selectedOptions.add(INCLUDE_NON_IMPACTED); - } - if (includeAddedTests) { - selectedOptions.add(INCLUDE_ADDED_TESTS); - } - if (includeFailedAndSkipped) { - selectedOptions.add(INCLUDE_FAILED_AND_SKIPPED); - } - return getImpactedTests(availableTests, baseline, baselineRevision, endCommit, endRevision, repository, partitions, - selectedOptions.toArray(new ETestImpactOptions[0])); - } - - /** - * Tries to retrieve the impacted tests from Teamscale. Use this method if you want to query time range based or you - * want to exclude failed and skipped tests from previous test runs. - * - * @param availableTests A list of tests that is locally available for execution. This allows TIA to consider newly - * added tests in addition to those that are already known and allows to filter e.g. if the - * user has already selected a subset of relevant tests. This can be null to - * indicate that only tests known to Teamscale should be suggested. - * @param baseline The baseline timestamp AFTER which changes should be considered. Changes that happened - * exactly at the baseline will be excluded. In case you want to retrieve impacted tests for a - * single commit with a known timestamp you can append a "p1" suffix to the - * timestamp to indicate that you are interested in the changes that happened after the parent - * of the given commit. - * @param baselineRevision Same as baseline but accepts a revision (e.g. git SHA1) instead of a branch and timestamp - * @param endCommit The last commit for which changes should be considered. - * @param endRevision Same as endCommit but accepts a revision (e.g. git SHA1) instead of a branch and timestamp - * @param repository The repository id in your Teamscale project which Teamscale should use to look up the revision, if given. - * Null or empty will lead to a lookup in all repositories in the Teamscale project. - * @param partitions The partitions that should be considered for retrieving impacted tests. Can be - * null to indicate that tests from all partitions should be returned. - * @param options A list of options (See {@link ETestImpactOptions} for more details) - * @return A list of test clusters to execute. If availableTests is null, a single dummy cluster is returned with - * all prioritized tests. - */ - private Response> getImpactedTests( - List availableTests, - String baseline, - String baselineRevision, - CommitDescriptor endCommit, - String endRevision, - String repository, - List partitions, - ETestImpactOptions... options) throws IOException { - EnumSet testImpactOptions = EnumSet.copyOf(Arrays.asList(options)); - boolean includeNonImpacted = testImpactOptions.contains(INCLUDE_NON_IMPACTED); - boolean includeFailedAndSkipped = testImpactOptions.contains(INCLUDE_FAILED_AND_SKIPPED); - boolean ensureProcessed = testImpactOptions.contains(ENSURE_PROCESSED); - boolean includeAddedTests = testImpactOptions.contains(INCLUDE_ADDED_TESTS); - - if (availableTests == null) { - return wrapInCluster( - service.getImpactedTests(projectId, baseline, baselineRevision, endCommit, endRevision, repository, partitions, - includeNonImpacted, - includeFailedAndSkipped, - ensureProcessed, includeAddedTests) - .execute()); - } else { - return service - .getImpactedTests(projectId, baseline, baselineRevision, endCommit, endRevision, repository, partitions, - includeNonImpacted, - includeFailedAndSkipped, - ensureProcessed, includeAddedTests, availableTests.stream() - .map(TestWithClusterId::fromClusteredTestDetails).collect( - Collectors.toList())) - .execute(); - } - } - - private static Response> wrapInCluster( - Response> testListResponse) { - if (testListResponse.isSuccessful()) { - return Response.success( - Collections.singletonList(new PrioritizableTestCluster("dummy", testListResponse.body())), - testListResponse.raw()); - } else { - return Response.error(testListResponse.errorBody(), testListResponse.raw()); - } - } - - /** Uploads multiple reports to Teamscale in the given {@link EReportFormat}. */ - public void uploadReports(EReportFormat reportFormat, Collection reports, CommitDescriptor commitDescriptor, - String revision, String repository, - String partition, String message) throws IOException { - uploadReports(reportFormat.name(), reports, commitDescriptor, revision, repository, partition, message); - } - - /** Uploads multiple reports to Teamscale. */ - public void uploadReports(String reportFormat, Collection reports, CommitDescriptor commitDescriptor, - String revision, String repository, - String partition, String message) throws IOException { - List partList = reports.stream().map(file -> { - RequestBody requestBody = RequestBody.create(MultipartBody.FORM, file); - return MultipartBody.Part.createFormData("report", file.getName(), requestBody); - }).collect(Collectors.toList()); - - Response response = service - .uploadExternalReports(projectId, reportFormat, commitDescriptor, revision, repository, true, partition, message, - partList).execute(); - if (!response.isSuccessful()) { - throw new IOException("HTTP request failed: " + HttpUtils.getErrorBodyStringSafe(response)); - } - } - - /** Uploads one in-memory report to Teamscale. */ - public void uploadReport(EReportFormat reportFormat, String report, CommitDescriptor commitDescriptor, - String revision, String repository, String partition, String message) throws IOException { - RequestBody requestBody = RequestBody.create(MultipartBody.FORM, report); - service.uploadReport(projectId, commitDescriptor, revision, repository, partition, reportFormat, message, requestBody); - } -} diff --git a/teamscale-client/src/main/java/com/teamscale/client/TeamscaleProxySystemProperties.java b/teamscale-client/src/main/java/com/teamscale/client/TeamscaleProxySystemProperties.java deleted file mode 100644 index 94245b9b7..000000000 --- a/teamscale-client/src/main/java/com/teamscale/client/TeamscaleProxySystemProperties.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.teamscale.client; - -import org.jetbrains.annotations.NotNull; - -/** - * Reads and writes Java system properties values for - *
    - *
  • teamscale.http.proxyHost
  • - *
  • teamscale.http.proxyPort
  • - *
  • teamscale.http.proxyUser
  • - *
  • teamscale.http.proxyPassword
  • - *
- * or the corresponding HTTPS counterpart (starting with https instead of http). - * These values set the proxy server and credentials that should be used later to reach Teamscale and take precedence - * over the default proxy settings (see {@link ProxySystemProperties}). - */ -public class TeamscaleProxySystemProperties extends ProxySystemProperties { - - private static final String TEAMSCALE_PREFIX = "teamscale."; - - /** @see ProxySystemProperties#ProxySystemProperties */ - public TeamscaleProxySystemProperties(Protocol protocol) { - super(protocol); - } - - @Override - @NotNull - protected String getPropertyPrefix() { - return TEAMSCALE_PREFIX; - } -} \ No newline at end of file diff --git a/teamscale-client/src/main/java/com/teamscale/client/TeamscaleServer.java b/teamscale-client/src/main/java/com/teamscale/client/TeamscaleServer.java deleted file mode 100644 index 6f55ed24e..000000000 --- a/teamscale-client/src/main/java/com/teamscale/client/TeamscaleServer.java +++ /dev/null @@ -1,184 +0,0 @@ -package com.teamscale.client; - -import okhttp3.HttpUrl; - -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; - -/** Holds Teamscale server details. */ -public class TeamscaleServer { - - /** The URL of the Teamscale server. */ - public HttpUrl url; - - /** The project id within Teamscale. */ - public String project; - - /** The user name used to authenticate against Teamscale. */ - public String userName; - - /** The user's access token. */ - public String userAccessToken; - - /** The partition to upload reports to. */ - public String partition; - - /** - * The corresponding code commit to which the coverage belongs. If this is null, the Agent is supposed to - * auto-detect the commit from the profiled code. - */ - public CommitDescriptor commit; - - /** - * The corresponding code revision to which the coverage belongs. This is currently only supported for testwise - * mode. - */ - public String revision; - - /** - * The repository id in your Teamscale project which Teamscale should use to look up the revision, if given. - * Null or empty will lead to a lookup in all repositories in the Teamscale project. - */ - public String repository; - - /** - * The configuration ID that was used to retrieve the profiler configuration. This is only set here to append it to - * the default upload message. - */ - public String configId; - - private String message = null; - - /** - * The commit message shown in the Teamscale UI for the coverage upload. If the message is null, auto-generates a - * sensible message. - */ - public String getMessage() { - if (message == null) { - return createDefaultMessage(); - } - return message; - } - - private String createDefaultMessage() { - // we do not include the IP address here as one host may have - // - multiple network interfaces - // - each with multiple IP addresses - // - in either IPv4 or IPv6 format - // - and it is unclear which of those is "the right one" or even just which is useful (e.g. loopback or virtual - // adapters are not useful and might even confuse readers) - String hostnamePart = "uploaded from "; - try { - hostnamePart += "hostname: " + InetAddress.getLocalHost().getHostName(); - } catch (UnknownHostException e) { - hostnamePart += "an unknown computer"; - } - - String revisionPart = ""; - if (revision != null) { - revisionPart = "\nfor revision: " + revision; - } - - String configIdPart = ""; - if (configId != null) { - configIdPart = "\nprofiler configuration ID: " + configId; - } - - return partition + " coverage uploaded at " + - DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now()) + "\n\n" + - hostnamePart + revisionPart + configIdPart; - } - - public void setMessage(String message) { - this.message = message; - } - - /** Checks if all fields required for a single-project Teamscale upload are non-null. */ - public boolean isConfiguredForSingleProjectTeamscaleUpload() { - return isConfiguredForServerConnection() && partition != null && project != null; - } - - /** Checks if all fields required for a Teamscale upload are non-null, except the project which must be null. */ - public boolean isConfiguredForMultiProjectUpload() { - return isConfiguredForServerConnection() && partition != null && project == null; - } - - /** Checks if all required fields to access a Teamscale server are non-null. */ - public boolean isConfiguredForServerConnection() { - return url != null && - userName != null && - userAccessToken != null; - } - - /** Whether a URL, user and access token were provided. */ - public boolean canConnectToTeamscale() { - return url != null && userName != null && userAccessToken != null; - } - - /** Returns whether all fields are null. */ - public boolean hasAllFieldsNull() { - return url == null && - project == null && - userName == null && - userAccessToken == null && - partition == null && - commit == null && - revision == null; - } - - /** Returns whether either a commit or revision has been set. */ - public boolean hasCommitOrRevision() { - return commit != null || revision != null; - } - - /** Checks if another TeamscaleServer has the same project and revision/commit as this TeamscaleServer instance. */ - public boolean hasSameProjectAndCommit(TeamscaleServer other) { - if (!this.project.equals(other.project)) { - return false; - } - if (this.revision != null) { - return this.revision.equals(other.revision); - } - return this.commit.equals(other.commit); - } - - @Override - public String toString() { - String at; - if (revision != null) { - at = "revision " + revision; - if (repository != null) { - at += "in repository " + repository; - } - } else { - at = "commit " + commit; - } - return "Teamscale " + url + " as user " + userName + " for " + project + " to " + partition + " at " + at; - } - - /** Creates a copy of the {@link TeamscaleServer} configuration, but with the given project and commit set. */ - public TeamscaleServer withProjectAndCommit(String teamscaleProject, CommitDescriptor commitDescriptor) { - TeamscaleServer teamscaleServer = new TeamscaleServer(); - teamscaleServer.url = url; - teamscaleServer.userName = userName; - teamscaleServer.userAccessToken = userAccessToken; - teamscaleServer.partition = partition; - teamscaleServer.project = teamscaleProject; - teamscaleServer.commit = commitDescriptor; - return teamscaleServer; - } - - /** Creates a copy of the {@link TeamscaleServer} configuration, but with the given project and revision set. */ - public TeamscaleServer withProjectAndRevision(String teamscaleProject, String revision) { - TeamscaleServer teamscaleServer = new TeamscaleServer(); - teamscaleServer.url = url; - teamscaleServer.userName = userName; - teamscaleServer.userAccessToken = userAccessToken; - teamscaleServer.partition = partition; - teamscaleServer.project = teamscaleProject; - teamscaleServer.revision = revision; - return teamscaleServer; - } -} diff --git a/teamscale-client/src/main/java/com/teamscale/client/TeamscaleServiceGenerator.java b/teamscale-client/src/main/java/com/teamscale/client/TeamscaleServiceGenerator.java deleted file mode 100644 index 4da346738..000000000 --- a/teamscale-client/src/main/java/com/teamscale/client/TeamscaleServiceGenerator.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.teamscale.client; - -import okhttp3.HttpUrl; -import okhttp3.Interceptor; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import org.jetbrains.annotations.NotNull; -import retrofit2.Retrofit; -import retrofit2.converter.jackson.JacksonConverterFactory; - -import java.io.File; -import java.io.IOException; -import java.time.Duration; - -/** Helper class for generating a teamscale compatible service. */ -public class TeamscaleServiceGenerator { - - /** Custom user agent of the requests, used to monitor API traffic. */ - public static final String USER_AGENT = "Teamscale JaCoCo Agent"; - - /** - * Generates a {@link Retrofit} instance for the given service, which uses basic auth to authenticate against the - * server and which sets the accept header to json. - */ - public static S createService(Class serviceClass, HttpUrl baseUrl, String username, String accessToken, - Duration readTimeout, Duration writeTimeout, Interceptor... interceptors) { - return createServiceWithRequestLogging(serviceClass, baseUrl, username, accessToken, null, readTimeout, - writeTimeout, - interceptors); - } - - /** - * Generates a {@link Retrofit} instance for the given service, which uses basic auth to authenticate against the - * server and which sets the accept-header to json. Logs requests and responses to the given logfile. - */ - public static S createServiceWithRequestLogging(Class serviceClass, HttpUrl baseUrl, String username, - String accessToken, File logfile, Duration readTimeout, - Duration writeTimeout, Interceptor... interceptors) { - Retrofit retrofit = HttpUtils.createRetrofit( - retrofitBuilder -> retrofitBuilder.baseUrl(baseUrl) - .addConverterFactory(JacksonConverterFactory.create(JsonUtils.OBJECT_MAPPER)), - okHttpBuilder -> { - addInterceptors(okHttpBuilder, interceptors) - .addInterceptor(HttpUtils.getBasicAuthInterceptor(username, accessToken)) - .addInterceptor(new AcceptJsonInterceptor()) - .addNetworkInterceptor(new CustomUserAgentInterceptor()); - if (logfile != null) { - okHttpBuilder.addInterceptor(new FileLoggingInterceptor(logfile)); - } - }, - readTimeout, writeTimeout - ); - return retrofit.create(serviceClass); - } - - private static OkHttpClient.Builder addInterceptors(OkHttpClient.Builder builder, Interceptor... interceptors) { - for (Interceptor interceptor : interceptors) { - builder.addInterceptor(interceptor); - } - return builder; - } - - - /** - * Sets an Accept: application/json header on all requests. - */ - private static class AcceptJsonInterceptor implements Interceptor { - - @NotNull - @Override - public Response intercept(Chain chain) throws IOException { - Request newRequest = chain.request().newBuilder().header("Accept", "application/json").build(); - return chain.proceed(newRequest); - } - } - - /** - * Sets the custom user agent {@link #USER_AGENT} header on all requests. - */ - public static class CustomUserAgentInterceptor implements Interceptor { - @NotNull - @Override - public Response intercept(Chain chain) throws IOException { - Request newRequest = chain.request().newBuilder().header("User-Agent", USER_AGENT).build(); - return chain.proceed(newRequest); - } - } - -} diff --git a/teamscale-client/src/main/java/com/teamscale/client/TestData.java b/teamscale-client/src/main/java/com/teamscale/client/TestData.java deleted file mode 100644 index bcdd73cd7..000000000 --- a/teamscale-client/src/main/java/com/teamscale/client/TestData.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.teamscale.client; - -import org.apache.commons.codec.binary.Hex; -import org.apache.commons.codec.digest.DigestUtils; - -import java.io.IOException; -import java.nio.file.Path; -import java.security.MessageDigest; - -/** - * Represents additional test data to attach to {@link ClusteredTestDetails}. Use the {@link Builder} to create new - * {@link TestData} objects. - *

- * Internally, the data you pass to the builder is hashed and only the hash is transferred as {@link - * ClusteredTestDetails#content} to Teamscale to save network bandwidth and RAM. Whenever a test case's hash changes, - * Teamscale will select it for the next TIA test run. - */ -public class TestData { - - /** The hash of the test data which will be sent to Teamscale as the {@link ClusteredTestDetails#content}. */ - /*package*/ final String hash; - - private TestData(String hash) { - this.hash = hash; - } - - /** - * Builder for {@link TestData} objects. This class is thread-safe and ensures that reading the test data does not - * result in {@link OutOfMemoryError}s. - */ - public static class Builder { - - private static final byte[] DIGEST_SEPARATOR = "-!#!-".getBytes(); - - private MessageDigest digest = DigestUtils.getSha1Digest(); - - /** Adds the given bytes as additional test data. */ - public synchronized Builder addByteArray(byte[] content) { - ensureHasNotBeenFinalized(); - DigestUtils.updateDigest(digest, content); - DigestUtils.updateDigest(digest, DIGEST_SEPARATOR); - return this; - } - - private void ensureHasNotBeenFinalized() { - if (digest == null) { - throw new IllegalStateException( - "You tried to use this TestData.Builder after calling #build() on it. Builders cannot be reused."); - } - } - - /** Adds the given String as additional test data. */ - public synchronized Builder addString(String content) { - ensureHasNotBeenFinalized(); - DigestUtils.updateDigest(digest, content); - DigestUtils.updateDigest(digest, DIGEST_SEPARATOR); - return this; - } - - /** Adds the contents of the given file path as additional test data. */ - public synchronized Builder addFileContent(Path fileWithContent) throws IOException { - ensureHasNotBeenFinalized(); - DigestUtils.updateDigest(digest, fileWithContent); - DigestUtils.updateDigest(digest, DIGEST_SEPARATOR); - return this; - } - - /** - * Builds the {@link TestData} object. After calling this method, you cannot use this builder anymore. - */ - public synchronized TestData build() { - ensureHasNotBeenFinalized(); - String hash = Hex.encodeHexString(digest.digest()); - digest = null; - return new TestData(hash); - } - } - -} diff --git a/teamscale-client/src/main/java/com/teamscale/client/TestDetails.java b/teamscale-client/src/main/java/com/teamscale/client/TestDetails.java deleted file mode 100644 index 20e047bb0..000000000 --- a/teamscale-client/src/main/java/com/teamscale/client/TestDetails.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.teamscale.client; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.Objects; - -/** - * Contains details about a test. - */ -public class TestDetails { - - /** Unique name of the test case by using a path like hierarchical description, which can be shown in the UI. */ - public String uniformPath; - - /** - * Path to the source of the method. Will be equal to uniformPath in most cases, but e.g. @Test methods in a base - * class will have the sourcePath pointing to the base class which contains the actual implementation whereas - * uniformPath will contain the the class name of the most specific subclass, from where it was actually executed. - */ - public String sourcePath; - - /** - * Some kind of content to tell whether the test specification has changed. Can be revision number or hash over the - * specification or similar. You can include e.g. a hash of each test's test data so that whenever the test data - * changes, the corresponding test is re-run. - */ - public String content; - - @JsonCreator - public TestDetails(@JsonProperty("uniformPath") String uniformPath, @JsonProperty("sourcePath") String sourcePath, - @JsonProperty("content") String content) { - this.uniformPath = uniformPath; - this.sourcePath = sourcePath; - this.content = content; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - TestDetails that = (TestDetails) o; - return Objects.equals(uniformPath, that.uniformPath) && - Objects.equals(sourcePath, that.sourcePath) && - Objects.equals(content, that.content); - } - - @Override - public int hashCode() { - return Objects.hash(uniformPath, sourcePath, content); - } -} diff --git a/teamscale-client/src/main/java/com/teamscale/client/TestWithClusterId.java b/teamscale-client/src/main/java/com/teamscale/client/TestWithClusterId.java deleted file mode 100644 index 1e6d6d823..000000000 --- a/teamscale-client/src/main/java/com/teamscale/client/TestWithClusterId.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.teamscale.client; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * Test with additional information about which cluster of tests the test case belongs to during prioritization. - */ -public class TestWithClusterId { - - - /** - * The uniform path of the test (unescaped and without -test-execution- prefix). - */ - public final String testName; - - /** - * The hashed content of the test. - */ - public final String hash; - - /** - * The partition of the test. - */ - public final String partition; - - /** - * A unique identifier for the cluster this test should be prioritized within. May not be null. - */ - public final String clusterId; - - @JsonCreator - public TestWithClusterId(@JsonProperty("testName") String testName, @JsonProperty("hash") String hash, - @JsonProperty("partition") String partition, @JsonProperty("clusterId") String clusterId) { - this.testName = testName; - this.hash = hash; - this.partition = partition; - this.clusterId = clusterId; - } - - /** - * Creates a #TestWithClusterId from a #ClusteredTestDetails object. - */ - public static TestWithClusterId fromClusteredTestDetails(ClusteredTestDetails clusteredTestDetails) { - return new TestWithClusterId(clusteredTestDetails.uniformPath, clusteredTestDetails.content, - clusteredTestDetails.partition, clusteredTestDetails.clusterId); - } -} diff --git a/teamscale-client/src/main/java/com/teamscale/client/AntPatternUtils.java b/teamscale-client/src/main/kotlin/com/teamscale/client/AntPatternUtils.kt similarity index 54% rename from teamscale-client/src/main/java/com/teamscale/client/AntPatternUtils.java rename to teamscale-client/src/main/kotlin/com/teamscale/client/AntPatternUtils.kt index 79d80fd7b..60b69a6e1 100644 --- a/teamscale-client/src/main/java/com/teamscale/client/AntPatternUtils.java +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/AntPatternUtils.kt @@ -14,95 +14,100 @@ | See the License for the specific language governing permissions and | | limitations under the License. | +-------------------------------------------------------------------------*/ -package com.teamscale.client; +package com.teamscale.client -import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; +import java.util.regex.Pattern +import java.util.regex.PatternSyntaxException /** * Utility methods for dealing with Ant pattern as defined at http://ant.apache.org/manual/dirtasks.html#patterns - *

+ * + * * We implement a special version where a trailing '.' can be used to only match files without file extension (i.e. file * names without dot). */ -public class AntPatternUtils { - - /** Converts an ANT pattern to a regex pattern. */ - public static Pattern convertPattern(String antPattern, boolean caseSensitive) throws PatternSyntaxException { - - antPattern = normalizePattern(antPattern); +object AntPatternUtils { + /** Converts an ANT pattern to a regex pattern. */ + @Throws(PatternSyntaxException::class) + fun convertPattern(antPattern: String, caseSensitive: Boolean): Pattern { + var antPattern = antPattern + antPattern = normalizePattern(antPattern) // ant specialty: trailing /** is optional // for example **/e*/** will also match foo/entry - boolean addTrailAll = false; + var addTrailAll = false if (antPattern.endsWith("/**")) { - addTrailAll = true; - antPattern = StringUtils.stripSuffix(antPattern, "/**"); + addTrailAll = true + antPattern = StringUtils.stripSuffix(antPattern, "/**") } - StringBuilder patternBuilder = new StringBuilder(); - convertPlainPattern(antPattern, patternBuilder); + val patternBuilder = StringBuilder() + convertPlainPattern(antPattern, patternBuilder) if (addTrailAll) { // the tail pattern is optional (i.e. we do not require the '/'), // but the "**" is only in effect if the '/' occurs - patternBuilder.append("(/.*)?"); + patternBuilder.append("(/.*)?") } - return compileRegex(patternBuilder.toString(), antPattern, caseSensitive); + return compileRegex(patternBuilder.toString(), antPattern, caseSensitive) } - /** Compiles the given regex. */ - private static Pattern compileRegex(String regex, String antPattern, boolean caseSensitive) { + /** Compiles the given regex. */ + private fun compileRegex(regex: String, antPattern: String, caseSensitive: Boolean): Pattern { try { - return Pattern.compile(regex, determineRegexFlags(caseSensitive)); - } catch (PatternSyntaxException e) { + return Pattern.compile(regex, determineRegexFlags(caseSensitive)) + } catch (e: PatternSyntaxException) { // make pattern syntax exception more understandable - throw new PatternSyntaxException( - "Error compiling ANT pattern '" + antPattern + "' to regular expression. " + e.getDescription(), - e.getPattern(), e.getIndex()); + throw PatternSyntaxException( + "Error compiling ANT pattern '" + antPattern + "' to regular expression. " + e.description, + e.pattern, e.index + ) } } - /** Returns the flags to be used for the regular expression. */ - private static int determineRegexFlags(boolean caseSensitive) { + /** Returns the flags to be used for the regular expression. */ + private fun determineRegexFlags(caseSensitive: Boolean): Int { // Use DOTALL flag, as on Unix the file names can contain line breaks - int flags = Pattern.DOTALL; + var flags = Pattern.DOTALL if (!caseSensitive) { - flags |= Pattern.CASE_INSENSITIVE; + flags = flags or Pattern.CASE_INSENSITIVE } - return flags; + return flags } /** - * Normalizes the given pattern by ensuring forward slashes and mapping trailing slash to '/**'. + * Normalizes the given pattern by ensuring forward slashes and mapping trailing slash to '/ **'. */ - private static String normalizePattern(String antPattern) { - antPattern = FileSystemUtils.normalizeSeparators(antPattern); + private fun normalizePattern(antPattern: String): String { + var antPattern = antPattern + antPattern = FileSystemUtils.normalizeSeparators(antPattern) // ant pattern syntax: if a pattern ends with /, then ** is // appended if (antPattern.endsWith("/")) { - antPattern += "**"; + antPattern += "**" } - return antPattern; + return antPattern } /** * Converts a plain ANT pattern to a regular expression, by replacing special characters, such as '?', '*', and - * '**'. The created pattern is appended to the given {@link StringBuilder}. The pattern must be plain, i.e. all ANT + * '**'. The created pattern is appended to the given [StringBuilder]. The pattern must be plain, i.e. all ANT * specialties, such as trailing double stars have to be dealt with beforehand. */ - private static void convertPlainPattern(String antPattern, StringBuilder patternBuilder) { - for (int i = 0; i < antPattern.length(); ++i) { - char c = antPattern.charAt(i); + private fun convertPlainPattern(antPattern: String, patternBuilder: StringBuilder) { + var i = 0 + while (i < antPattern.length) { + val c = antPattern[i] if (c == '?') { - patternBuilder.append("[^/]"); + patternBuilder.append("[^/]") } else if (c != '*') { - patternBuilder.append(Pattern.quote(Character.toString(c))); + patternBuilder.append(Pattern.quote(c.toString())) } else { - i = convertStarSequence(antPattern, patternBuilder, i); + i = convertStarSequence(antPattern, patternBuilder, i) } + ++i } } @@ -110,50 +115,50 @@ private static void convertPlainPattern(String antPattern, StringBuilder pattern * Converts a sequence of the ant pattern starting with a star at the given index. Appends the pattern fragment the * the builder and returns the index to continue scanning from. */ - private static int convertStarSequence(String antPattern, StringBuilder patternBuilder, int index) { - boolean doubleStar = isCharAt(antPattern, index + 1, '*'); + private fun convertStarSequence(antPattern: String, patternBuilder: StringBuilder, index: Int): Int { + val doubleStar = isCharAt(antPattern, index + 1, '*') if (doubleStar) { // if the double star is followed by a slash, the entire // group becomes optional, as we want "**/foo" to also // match a top-level "foo" - boolean doubleStarSlash = isCharAt(antPattern, index + 2, '/'); + val doubleStarSlash = isCharAt(antPattern, index + 2, '/') if (doubleStarSlash) { - patternBuilder.append("(.*/)?"); - return index + 2; + patternBuilder.append("(.*/)?") + return index + 2 } - boolean doubleStarDot = isCharAtBeforeSlashOrEnd(antPattern, index + 2, '.'); + val doubleStarDot = isCharAtBeforeSlashOrEnd(antPattern, index + 2, '.') if (doubleStarDot) { - patternBuilder.append("(.*/)?[^/.]*[.]?"); - return index + 2; + patternBuilder.append("(.*/)?[^/.]*[.]?") + return index + 2 } - patternBuilder.append(".*"); - return index + 1; + patternBuilder.append(".*") + return index + 1 } - boolean starDot = isCharAtBeforeSlashOrEnd(antPattern, index + 1, '.'); + val starDot = isCharAtBeforeSlashOrEnd(antPattern, index + 1, '.') if (starDot) { - patternBuilder.append("[^/.]*[.]?"); - return index + 1; + patternBuilder.append("[^/.]*[.]?") + return index + 1 } - patternBuilder.append("[^/]*"); - return index; + patternBuilder.append("[^/]*") + return index } /** * Returns whether the given position exists in the string and equals the given character, and the given character * is either at the end or right before a slash. */ - private static boolean isCharAtBeforeSlashOrEnd(String s, int position, char character) { - return isCharAt(s, position, character) && (position + 1 == s.length() || isCharAt(s, position + 1, '/')); + private fun isCharAtBeforeSlashOrEnd(s: String, position: Int, character: Char): Boolean { + return isCharAt(s, position, character) && (position + 1 == s.length || isCharAt(s, position + 1, '/')) } /** * Returns whether the given position exists in the string and equals the given character. */ - private static boolean isCharAt(String s, int position, char character) { - return position < s.length() && s.charAt(position) == character; + private fun isCharAt(s: String, position: Int, character: Char): Boolean { + return position < s.length && s[position] == character } } \ No newline at end of file diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/ClusteredTestDetails.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/ClusteredTestDetails.kt new file mode 100644 index 000000000..3d137227b --- /dev/null +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/ClusteredTestDetails.kt @@ -0,0 +1,48 @@ +package com.teamscale.client + +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty + +/** + * [TestDetails] with additional information about which cluster of tests the test case belongs to during + * prioritization. + */ +class ClusteredTestDetails @JsonCreator constructor( + @JsonProperty("uniformPath") uniformPath: String, + @JsonProperty("sourcePath") sourcePath: String?, + @JsonProperty("content") content: String?, + /** + * A unique identifier for the cluster this test should be prioritized within. If null the test gets assigned its + * own unique cluster. + */ + @param:JsonProperty( + "clusterId" + ) var clusterId: String?, + /** + * The partition for the cluster this test should be prioritized within and the result will be uploaded to. + */ + @param:JsonProperty( + "partition" + ) var partition: String? +) : TestDetails(uniformPath, sourcePath, content) { + companion object { + /** + * Creates clustered test details with the given additional [TestData]. + * + * + * Use this to easily mark additional files or data as belonging to that test case. Whenever the given + * [TestData] changes, this test will be selected to be run by the TIA. + * + * + * Example: For a test that reads test data from an XML file, you should pass the contents of that XML file as its + * test data. Then, whenever the XML is modified, the corresponding test will be run by the TIA. + */ + fun createWithTestData( + uniformPath: String, sourcePath: String, testData: TestData, + clusterId: String, partition: String + ): ClusteredTestDetails { + return ClusteredTestDetails(uniformPath, sourcePath, testData.hash, clusterId, partition) + } + } +} + diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/CommitDescriptor.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/CommitDescriptor.kt new file mode 100644 index 000000000..3be47156b --- /dev/null +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/CommitDescriptor.kt @@ -0,0 +1,56 @@ +package com.teamscale.client + +import java.io.Serializable +import java.util.* + +/** Holds the branch and timestamp of a commit. */ +class CommitDescriptor +/** Constructor. */( + /** Branch name of the commit. */ + @JvmField val branchName: String, + /** + * Timestamp of the commit. The timestamp is a string here because be also want to be able to handle HEAD and + * 123456p1. + */ + @JvmField val timestamp: String +) : Serializable { + /** Constructor. */ + constructor(branchName: String, timestamp: Long) : this(branchName, timestamp.toString()) + + /** Returns a string representation of the commit in a Teamscale REST API compatible format. */ + override fun toString(): String { + return "$branchName:$timestamp" + } + + override fun equals(o: Any?): Boolean { + if (this === o) { + return true + } + if (o == null || javaClass != o.javaClass) { + return false + } + val that = o as CommitDescriptor + return branchName == that.branchName && + timestamp == that.timestamp + } + + override fun hashCode(): Int { + return Objects.hash(branchName, timestamp) + } + + companion object { + /** Parses the given commit descriptor string. */ + @JvmStatic + fun parse(commit: String?): CommitDescriptor? { + if (commit == null) { + return null + } + if (commit.contains(":")) { + val split = commit.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + return CommitDescriptor(split[0], split[1]) + } else { + return CommitDescriptor("master", commit) + } + } + } +} diff --git a/teamscale-client/src/main/java/com/teamscale/client/EReportFormat.java b/teamscale-client/src/main/kotlin/com/teamscale/client/EReportFormat.kt similarity index 62% rename from teamscale-client/src/main/java/com/teamscale/client/EReportFormat.java rename to teamscale-client/src/main/kotlin/com/teamscale/client/EReportFormat.kt index 60b59612f..3a9ee0d68 100644 --- a/teamscale-client/src/main/java/com/teamscale/client/EReportFormat.java +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/EReportFormat.kt @@ -1,61 +1,63 @@ -package com.teamscale.client; +package com.teamscale.client /** * Enum of report formats. * This is the subset of the report formats supported by Teamscale that the plugin currently implements support for. * See https://docs.teamscale.com/reference/upload-formats-and-samples/#supported-formats-for-upload */ -public enum EReportFormat { - - /** Astree xml report format. */ +enum class EReportFormat( + /** Each ReportFormat needs a readable name for the UI */ + val readableName: String +) { + /** Astree xml report format. */ ASTREE("Astree"), - /** JaCoCo (Java Code Coverage) xml report format. */ + /** JaCoCo (Java Code Coverage) xml report format. */ JACOCO("JaCoCo"), - /** Cobertura (Java test coverage) xml report format. */ + /** Cobertura (Java test coverage) xml report format. */ COBERTURA("Cobertura"), - /** Gcov (Profiling tool for code compiled with gcc) report format. */ + /** Gcov (Profiling tool for code compiled with gcc) report format. */ GCOV("Gcov"), - /** Lcov (Linux Test Project (LTP) front-end for Gcov) report format. */ + /** Lcov (Linux Test Project (LTP) front-end for Gcov) report format. */ LCOV("Lcov"), - /** Ctc (Testwell CTC++ coverage for C/C++) report format. */ + /** Ctc (Testwell CTC++ coverage for C/C++) report format. */ CTC("Testwell CTC++"), - /** XR.Baboon (code coverage for C# on Mono) report format. */ + /** XR.Baboon (code coverage for C# on Mono) report format. */ XR_BABOON("XR.Baboon"), - /** MS Coverage report format (CQSE Coverage Merger). */ + /** MS Coverage report format (CQSE Coverage Merger). */ MS_COVERAGE("MS Coverage"), - /** MS Coverage report format (Visual Studio Coverage Merger). */ + /** MS Coverage report format (Visual Studio Coverage Merger). */ VS_COVERAGE("VS Coverage"), - /** dotCover (Jetbrains coverage tool for .NET) report format. */ + /** dotCover (Jetbrains coverage tool for .NET) report format. */ DOT_COVER("dotCover"), - /** Roslyn (Microsoft .NET) report format. */ + /** Roslyn (Microsoft .NET) report format. */ ROSLYN("Roslyn"), - /** Golang coverage report format @see https://golang.org/cmd/cover/ */ + /** Golang coverage report format @see https://golang.org/cmd/cover/ */ GOLANG_COVERAGE("Go Cover"), - /** Simple coverage report format for testing. */ + /** Simple coverage report format for testing. */ SIMPLE("Teamscale Simple Coverage"), - /** Cppcheck (static analysis for C/C++) results in XML format. */ + /** Cppcheck (static analysis for C/C++) results in XML format. */ CPPCHECK("Cppcheck"), - /** PClint/FlexeLint (C/C++) coverage report format. */ + /** PClint/FlexeLint (C/C++) coverage report format. */ PCLINT("PClint/FlexeLint"), - /** Clang (C, C++, Objective C/C++) findings report format. */ + /** Clang (C, C++, Objective C/C++) findings report format. */ CLANG("Clang"), - /** Pylint (static analysis for Python) findings report format. */ + /** Pylint (static analysis for Python) findings report format. */ PYLINT("Pylint"), /** @@ -74,53 +76,53 @@ public enum EReportFormat { */ FINDBUGS("FindBugs/SpotBugs"), - /** Bullseye (C++) coverage report format. */ + /** Bullseye (C++) coverage report format. */ BULLSEYE("Bullseye"), - /** FxCop (.NET) findings report format. */ + /** FxCop (.NET) findings report format. */ FXCOP("FxCop"), - /** SpCop (Sharepoint Code Analysis) findings report format. */ + /** SpCop (Sharepoint Code Analysis) findings report format. */ SPCOP("SpCop"), - /** JUnit (Java unit tests) report format. */ + /** JUnit (Java unit tests) report format. */ JUNIT("JUnit"), - /** XUnit (.NET unit tests) report format. */ + /** XUnit (.NET unit tests) report format. */ XUNIT("XUnit"), - /** MS Test report format. */ + /** MS Test report format. */ MS_TEST("MSTest"), - /** Istanbul (JavaScript coverage) report format. */ + /** Istanbul (JavaScript coverage) report format. */ ISTANBUL("Istanbul"), - /** C# Compiler warnings format */ + /** C# Compiler warnings format */ CS_COMPILER_WARNING("C# Compiler Warning"), - /** Simulink Model Advisor report format. */ + /** Simulink Model Advisor report format. */ MODEL_ADVISOR("Simulink Model Advisor"), - /** CSV issues report format */ + /** CSV issues report format */ ISSUE_CSV("CSV Issues"), - /** CSV spec items report format */ + /** CSV spec items report format */ REQUIREMENTS_CSV("CSV Spec Items"), - /** Our own export format for SAP code inspector findings. */ + /** Our own export format for SAP code inspector findings. */ SAP_CODE_INSPECTOR("SAP Code Inspector Export"), - /** Custom testwise coverage report format. */ + /** Custom testwise coverage report format. */ TESTWISE_COVERAGE("Testwise Coverage"), - /** Line coverage data in txt format from Xcode (xccov). */ + /** Line coverage data in txt format from Xcode (xccov). */ XCODE("Xcode Coverage"), - /** Clover test coverage */ + /** Clover test coverage */ CLOVER("Clover"), - /** OpenCover test coverage */ + /** OpenCover test coverage */ OPEN_COVER("OpenCover"), /** @@ -128,16 +130,16 @@ public enum EReportFormat { */ IEC_COVERAGE("IEC Coverage"), - /** LLVM coverage report format. */ + /** LLVM coverage report format. */ LLVM("LLVM Coverage"), - /** Our own generic finding format. */ + /** Our own generic finding format. */ GENERIC_FINDINGS("Teamscale Generic Findings"), - /** Our own generic non-code metric format. */ + /** Our own generic non-code metric format. */ GENERIC_NON_CODE("Teamscale Non-Code Metrics"), - /** Parasoft C/C++text. */ + /** Parasoft C/C++text. */ PARASOFT_CPP_TEST("Parasoft C/C++test"), /** @@ -145,19 +147,19 @@ public enum EReportFormat { * compilers (e.g., clang) and contain included paths and initial defines. * * @see "https://sarcasm.github.io/notes/dev/compilation-database.html" + * * @see "http://clang.llvm.org/docs/JSONCompilationDatabase.html" */ COMPILATION_DATABASE("JSON Compilation Database"), - /** Mypy (static type checker for Python) findings report format. */ + /** Mypy (static type checker for Python) findings report format. */ MYPY("Mypy"), /** * Coverage report generated with the Lauterbach Trace32 tool. See section for - * Supported - * Upload Formats and Samples in the user guide for more information about - * the Lauterbach Trace32 tool. See the {@code trace32_example_reports.zip} for + * [Supported + * Upload Formats and Samples](https://docs.teamscale.com/reference/upload-formats-and-samples/) in the user guide for more information about + * the Lauterbach Trace32 tool. See the `trace32_example_reports.zip` for * additional report examples. */ LAUTERBACH_TRACE32("Lauterbach Trace32"), @@ -165,17 +167,5 @@ public enum EReportFormat { /** * jQAssistant report format. */ - JQASSISTANT("jQAssistant"); - - /** Each ReportFormat needs a readable name for the UI */ - private final String readableName; - - EReportFormat(String readableName) { - this.readableName = readableName; - } - - public String getReadableName() { - return this.readableName; - } - + JQASSISTANT("jQAssistant") } diff --git a/teamscale-client/src/main/java/com/teamscale/client/ETestImpactOptions.java b/teamscale-client/src/main/kotlin/com/teamscale/client/ETestImpactOptions.kt similarity index 93% rename from teamscale-client/src/main/java/com/teamscale/client/ETestImpactOptions.java rename to teamscale-client/src/main/kotlin/com/teamscale/client/ETestImpactOptions.kt index 506df2ec9..47d276920 100644 --- a/teamscale-client/src/main/java/com/teamscale/client/ETestImpactOptions.java +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/ETestImpactOptions.kt @@ -1,8 +1,7 @@ -package com.teamscale.client; - -/** Described all feature toggles of the impacted-tests services. */ -public enum ETestImpactOptions { +package com.teamscale.client +/** Described all feature toggles of the impacted-tests services. */ +enum class ETestImpactOptions { /** * Returns impacted tests first and then appends all non-impacted tests. This always returns all tests, but still * allows to fail faster as impacted tests are executed first. diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/FileLoggingInterceptor.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/FileLoggingInterceptor.kt new file mode 100644 index 000000000..0f45fd5d0 --- /dev/null +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/FileLoggingInterceptor.kt @@ -0,0 +1,69 @@ +package com.teamscale.client + +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response +import okhttp3.ResponseBody +import okio.Buffer +import java.io.File +import java.io.FileWriter +import java.io.IOException +import java.io.PrintWriter + +/** + * OkHttpInterceptor which prints out the full request and server response of requests to a file. + */ +class FileLoggingInterceptor +/** Constructor. */(private val logfile: File) : Interceptor { + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + val request: Request = chain.request() + + val requestStartTime = System.nanoTime() + PrintWriter(FileWriter(logfile)).use { fileWriter -> + fileWriter.write( + String.format( + "--> Sending request %s on %s %s%n%s%n", request.method, request.url, + chain.connection(), + request.headers + ) + ) + val requestBuffer = Buffer() + if (request.body != null) { + request.body?.writeTo(requestBuffer) + } + fileWriter.write(requestBuffer.readUtf8()) + + val response = getResponse(chain, request, fileWriter) + + val requestEndTime = System.nanoTime() + fileWriter.write( + String.format( + "<-- Received response for %s %s in %.1fms%n%s%n%n", response.code, + response.request.url, (requestEndTime - requestStartTime) / 1e6, response.headers + ) + ) + + var wrappedBody: ResponseBody? = null + if (response.body != null) { + val contentType = response.body!!.contentType() + val content = response.body!!.string() + fileWriter.write(content) + + wrappedBody = ResponseBody.create(contentType, content) + } + return response.newBuilder().body(wrappedBody).build() + } + } + + @Throws(IOException::class) + private fun getResponse(chain: Interceptor.Chain, request: Request, fileWriter: PrintWriter): Response { + try { + return chain.proceed(request) + } catch (e: Exception) { + fileWriter.write("\n\nRequest failed!\n") + e.printStackTrace(fileWriter) + throw e + } + } +} diff --git a/teamscale-client/src/main/java/com/teamscale/client/FileSystemUtils.java b/teamscale-client/src/main/kotlin/com/teamscale/client/FileSystemUtils.kt similarity index 50% rename from teamscale-client/src/main/java/com/teamscale/client/FileSystemUtils.java rename to teamscale-client/src/main/kotlin/com/teamscale/client/FileSystemUtils.kt index f9748f5d1..c46985fef 100644 --- a/teamscale-client/src/main/java/com/teamscale/client/FileSystemUtils.java +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/FileSystemUtils.kt @@ -1,105 +1,100 @@ -package com.teamscale.client; +package com.teamscale.client -import java.io.File; -import java.io.FileFilter; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; +import java.io.* +import java.nio.charset.StandardCharsets /** * File system utilities. */ -public class FileSystemUtils { +object FileSystemUtils { + /** Encoding for UTF-8. */ + val UTF8_ENCODING: String = StandardCharsets.UTF_8.name() - /** Encoding for UTF-8. */ - public static final String UTF8_ENCODING = StandardCharsets.UTF_8.name(); - - /** Unix file path separator */ - private static final char UNIX_SEPARATOR = '/'; + /** Unix file path separator */ + private const val UNIX_SEPARATOR = '/' /** * Checks if a directory exists. If not it creates the directory and all necessary parent directories. * * @throws IOException if directories couldn't be created. */ - public static void ensureDirectoryExists(File directory) throws IOException { + @Throws(IOException::class) + fun ensureDirectoryExists(directory: File) { if (!directory.exists() && !directory.mkdirs()) { - throw new IOException("Couldn't create directory: " + directory); + throw IOException("Couldn't create directory: $directory") } } /** * Returns a list of all files and directories contained in the given directory and all subdirectories matching the * filter provided. The given directory itself is not included in the result. - *

+ * + * * The file filter may or may not exclude directories. - *

+ * + * * This method knows nothing about (symbolic and hard) links, so care should be taken when traversing directories * containing recursive links. * * @param directory the directory to start the search from. If this is null or the directory does not exist, an - * empty list is returned. + * empty list is returned. * @param filter the filter used to determine whether the result should be included. If the filter is null, all - * files and directories are included. + * files and directories are included. * @return the list of files found (the order is determined by the file system). */ - public static List listFilesRecursively(File directory, FileFilter filter) { - if (directory == null || !directory.isDirectory()) { - return Collections.emptyList(); + @JvmStatic + fun listFilesRecursively(directory: File?, filter: FileFilter?): List { + if (directory == null || !directory.isDirectory) { + return emptyList() } - List result = new ArrayList<>(); - listFilesRecursively(directory, result, filter); - return result; + val result: MutableList = ArrayList() + listFilesRecursively(directory, result, filter) + return result } /** * Returns the extension of the file. * * @return File extension, i.e. "java" for "FileSystemUtils.java", or - * null, if the file has no extension (i.e. if a filename + * `null`, if the file has no extension (i.e. if a filename * contains no '.'), returns the empty string if the '.' is the filename's last character. */ - public static String getFileExtension(File file) { - String name = file.getName(); - int posLastDot = name.lastIndexOf('.'); + @JvmStatic + fun getFileExtension(file: File): String? { + val name = file.name + val posLastDot = name.lastIndexOf('.') if (posLastDot < 0) { - return null; + return null } - return name.substring(posLastDot + 1); + return name.substring(posLastDot + 1) } /** * Finds all files and directories contained in the given directory and all subdirectories matching the filter * provided and put them into the result collection. The given directory itself is not included in the result. - *

+ * + * * This method knows nothing about (symbolic and hard) links, so care should be taken when traversing directories * containing recursive links. * * @param directory the directory to start the search from. * @param result the collection to add to all files found. * @param filter the filter used to determine whether the result should be included. If the filter is null, all - * files and directories are included. + * files and directories are included. */ - private static void listFilesRecursively(File directory, Collection result, FileFilter filter) { - File[] files = directory.listFiles(); - if (files == null) { - // From the docs of `listFiles`: + private fun listFilesRecursively(directory: File, result: MutableCollection, filter: FileFilter?) { + val files = directory.listFiles() + ?: // From the docs of `listFiles`: // "If this abstract pathname does not denote a directory, then this method returns null." // Based on this, it seems to be ok to just return here without throwing an exception. - return; - } + return - for (File file : files) { - if (file.isDirectory()) { - listFilesRecursively(file, result, filter); + for (file in files) { + if (file.isDirectory) { + listFilesRecursively(file, result, filter) } if (filter == null || filter.accept(file)) { - result.add(file); + result.add(file) } } } @@ -107,46 +102,47 @@ private static void listFilesRecursively(File directory, Collection result /** * Replace platform dependent separator char with forward slashes to create system-independent paths. */ - public static String normalizeSeparators(String path) { - return path.replace(File.separatorChar, UNIX_SEPARATOR); + @JvmStatic + fun normalizeSeparators(path: String): String { + return path.replace(File.separatorChar, UNIX_SEPARATOR) } /** - * Copy an input stream to an output stream. This does not close the + * Copy an input stream to an output stream. This does *not* close the * streams. * * @param input - * input stream + * input stream * @param output - * output stream + * output stream * @return number of bytes copied * @throws IOException - * if an IO exception occurs. + * if an IO exception occurs. */ - public static int copy(InputStream input, OutputStream output) throws IOException { - byte[] buffer = new byte[1024]; - int size = 0; - int len; - while ((len = input.read(buffer)) > 0) { - output.write(buffer, 0, len); - size += len; + @Throws(IOException::class) + fun copy(input: InputStream, output: OutputStream): Int { + val buffer = ByteArray(1024) + var size = 0 + var len: Int + while ((input.read(buffer).also { len = it }) > 0) { + output.write(buffer, 0, len) + size += len } - return size; + return size } /** * Returns the name of the given file without extension. Example: * '/home/joe/data.dat' returns 'data'. */ - public static String getFilenameWithoutExtension(File file) { - return getFilenameWithoutExtension(file.getName()); + fun getFilenameWithoutExtension(file: File): String { + return getFilenameWithoutExtension(file.name) } /** * Returns the name of the given file without extension. Example: 'data.dat' returns 'data'. */ - public static String getFilenameWithoutExtension(String fileName) { - return StringUtils.removeLastPart(fileName, '.'); + fun getFilenameWithoutExtension(fileName: String): String { + return StringUtils.removeLastPart(fileName, '.') } - } \ No newline at end of file diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/HttpUtils.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/HttpUtils.kt new file mode 100644 index 000000000..e8a1d8e98 --- /dev/null +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/HttpUtils.kt @@ -0,0 +1,228 @@ +package com.teamscale.client + +import okhttp3.Authenticator +import okhttp3.Credentials.basic +import okhttp3.Interceptor +import okhttp3.OkHttpClient.Builder +import okhttp3.Response +import okhttp3.Route +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import retrofit2.Retrofit +import java.io.IOException +import java.net.InetSocketAddress +import java.net.Proxy +import java.security.GeneralSecurityException +import java.security.SecureRandom +import java.security.cert.X509Certificate +import java.time.Duration +import java.util.* +import java.util.function.Consumer +import javax.net.ssl.* + +/** + * Utility functions to set up [Retrofit] and [OkHttpClient]. + */ +object HttpUtils { + private val LOGGER: Logger = LoggerFactory.getLogger(HttpUtils::class.java) + + /** + * Default read timeout in seconds. + */ + @JvmField + val DEFAULT_READ_TIMEOUT: Duration = Duration.ofSeconds(60) + + /** + * Default write timeout in seconds. + */ + @JvmField + val DEFAULT_WRITE_TIMEOUT: Duration = Duration.ofSeconds(60) + + /** + * HTTP header used for authenticating against a proxy server + */ + const val PROXY_AUTHORIZATION_HTTP_HEADER: String = "Proxy-Authorization" + + /** Controls whether [OkHttpClient]s built with this class will validate SSL certificates. */ + private var shouldValidateSsl = true + + /** @see .shouldValidateSsl + */ + @JvmStatic + fun setShouldValidateSsl(shouldValidateSsl: Boolean) { + HttpUtils.shouldValidateSsl = shouldValidateSsl + } + + /** + * Creates a new [Retrofit] with proper defaults. The instance and the corresponding [OkHttpClient] can + * be customized with the given action. Timeouts for reading and writing can be customized. + */ + /** + * Creates a new [Retrofit] with proper defaults. The instance and the corresponding [OkHttpClient] can + * be customized with the given action. Read and write timeouts are set according to the default values. + */ + @JvmOverloads + @JvmStatic + fun createRetrofit( + retrofitBuilderAction: Consumer, + okHttpBuilderAction: Consumer, readTimeout: Duration = DEFAULT_READ_TIMEOUT, + writeTimeout: Duration = DEFAULT_WRITE_TIMEOUT + ): Retrofit { + val httpClientBuilder = Builder() + setTimeouts(httpClientBuilder, readTimeout, writeTimeout) + setUpSslValidation(httpClientBuilder) + setUpProxyServer(httpClientBuilder) + okHttpBuilderAction.accept(httpClientBuilder) + + val builder = Retrofit.Builder().client(httpClientBuilder.build()) + retrofitBuilderAction.accept(builder) + return builder.build() + } + + /** + * Java and/or OkHttp do not pick up the http.proxy* and https.proxy* system properties reliably. We need to teach + * OkHttp to always pick them up. + * + * + * Sources: [https://memorynotfound.com/configure-http-proxy-settings-java/](https://memorynotfound.com/configure-http-proxy-settings-java/) + * & + * [https://stackoverflow.com/a/35567936](https://stackoverflow.com/a/35567936) + */ + private fun setUpProxyServer(httpClientBuilder: Builder) { + val setHttpsProxyWasSuccessful = setUpProxyServerForProtocol( + ProxySystemProperties.Protocol.HTTPS, + httpClientBuilder + ) + if (!setHttpsProxyWasSuccessful) { + setUpProxyServerForProtocol(ProxySystemProperties.Protocol.HTTP, httpClientBuilder) + } + } + + private fun setUpProxyServerForProtocol( + protocol: ProxySystemProperties.Protocol, + httpClientBuilder: Builder + ): Boolean { + val teamscaleProxySystemProperties = TeamscaleProxySystemProperties(protocol) + try { + if (!teamscaleProxySystemProperties.isProxyServerSet()) { + return false + } + + useProxyServer( + httpClientBuilder, teamscaleProxySystemProperties.proxyHost!!, + teamscaleProxySystemProperties.proxyPort + ) + } catch (e: ProxySystemProperties.IncorrectPortFormatException) { + LOGGER.warn(e.message) + return false + } + + if (teamscaleProxySystemProperties.isProxyAuthSet()) { + useProxyAuthenticator( + httpClientBuilder, + teamscaleProxySystemProperties.proxyUser!!, + teamscaleProxySystemProperties.proxyPassword!! + ) + } + + return true + } + + private fun useProxyServer(httpClientBuilder: Builder, proxyHost: String, proxyPort: Int) { + httpClientBuilder.proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress(proxyHost, proxyPort))) + } + + private fun useProxyAuthenticator(httpClientBuilder: Builder, user: String, password: String) { + val proxyAuthenticator = Authenticator { route: Route?, response: Response -> + val credential = basic(user, password) + response.request.newBuilder() + .header(PROXY_AUTHORIZATION_HTTP_HEADER, credential) + .build() + } + httpClientBuilder.proxyAuthenticator(proxyAuthenticator) + } + + + /** + * Sets sensible defaults for the [OkHttpClient]. + */ + private fun setTimeouts(builder: Builder, readTimeout: Duration, writeTimeout: Duration) { + builder.connectTimeout(Duration.ofSeconds(60)) + builder.readTimeout(readTimeout) + builder.writeTimeout(writeTimeout) + } + + /** + * Enables or disables SSL certificate validation for the [Retrofit] instance + */ + private fun setUpSslValidation(builder: Builder) { + if (shouldValidateSsl) { + // this is the default behaviour of OkHttp, so we don't need to do anything + return + } + + val sslSocketFactory: SSLSocketFactory + try { + val sslContext = SSLContext.getInstance("TLS") + sslContext.init(null, arrayOf(TrustAllCertificatesManager.INSTANCE), SecureRandom()) + sslSocketFactory = sslContext.socketFactory + } catch (e: GeneralSecurityException) { + LOGGER.error("Could not disable SSL certificate validation. Leaving it enabled", e) + return + } + + // this causes OkHttp to accept all certificates + builder.sslSocketFactory(sslSocketFactory, TrustAllCertificatesManager.INSTANCE) + // this causes it to ignore invalid host names in the certificates + builder.hostnameVerifier(HostnameVerifier { hostName: String?, session: SSLSession? -> true }) + } + + /** + * Returns the error body of the given response or a replacement string in case it is null. + */ + @Throws(IOException::class) + @JvmStatic + fun getErrorBodyStringSafe(response: retrofit2.Response): String { + val errorBody = response.errorBody() ?: return "" + return errorBody.string() + } + + /** + * Returns an interceptor, which adds a basic auth header to a request. + */ + @JvmStatic + fun getBasicAuthInterceptor(username: String, password: String): Interceptor { + val credentials = "$username:$password" + val basic = "Basic " + Base64.getEncoder().encodeToString(credentials.toByteArray()) + + return Interceptor { chain: Interceptor.Chain -> + val newRequest = chain.request().newBuilder().header("Authorization", basic).build() + chain.proceed(newRequest) + } + } + + /** + * A simple implementation of [X509TrustManager] that simple trusts every certificate. + */ + class TrustAllCertificatesManager : X509TrustManager { + /** Returns `null`. */ + override fun getAcceptedIssuers(): Array { + return arrayOf() + } + + /** Does nothing. */ + override fun checkServerTrusted(certs: Array, authType: String) { + // Nothing to do + } + + /** Does nothing. */ + override fun checkClientTrusted(certs: Array, authType: String) { + // Nothing to do + } + + companion object { + /** Singleton instance. */ /*package*/ + val INSTANCE: TrustAllCertificatesManager = TrustAllCertificatesManager() + } + } +} diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/ITeamscaleService.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/ITeamscaleService.kt new file mode 100644 index 000000000..7b751458b --- /dev/null +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/ITeamscaleService.kt @@ -0,0 +1,172 @@ +package com.teamscale.client + +import okhttp3.MultipartBody +import okhttp3.RequestBody +import okhttp3.ResponseBody +import retrofit2.Call +import retrofit2.Response +import retrofit2.Retrofit +import retrofit2.http.* +import java.io.IOException + +/** [Retrofit] API specification for Teamscale. */ +interface ITeamscaleService { + + /** + * Report upload API. + * + * @param commit A branch and timestamp to upload the report to. Can be null if revision is specified. + * @param moveToLastCommit Whether to move the upload timestamp to right after the last commit + * @param revision This parameter allows passing a revision instead of a timestamp. Can be null if a + * timestamp is given. + * @param partition The name of the logical partition to store the results into. All existing data in this + * partition will be invalidated. A partition typically corresponds to one analysis run, + * i.e., if there are two independent builds/runs, they must use different partitions. + * @apiNote [How to Upload External Analysis Results to Teamscale](https://docs.teamscale.com/howto/uploading-external-results/#upload-via-command-line) + * for details. + */ + @Multipart + @POST("api/v5.9.0/projects/{projectAliasOrId}/external-analysis/session/auto-create/report") + fun uploadExternalReport( + @Path("projectAliasOrId") projectAliasOrId: String, + @Query("format") format: String, + @Query("t") commit: CommitDescriptor?, + @Query("revision") revision: String?, + @Query("repository") repository: String?, + @Query("movetolastcommit") moveToLastCommit: Boolean?, + @Query("partition") partition: String, + @Query("message") message: String, + @Part("report") report: RequestBody + ): Call + + /** + * Report upload API for multiple reports at once. + * + * @see uploadExternalReport + */ + @Multipart + @POST("api/v5.9.0/projects/{projectName}/external-analysis/session/auto-create/report") + fun uploadExternalReports( + @Path("projectName") projectName: String, + @Query("format") format: EReportFormat, + @Query("t") commit: CommitDescriptor?, + @Query("revision") revision: String?, + @Query("repository") repository: String?, + @Query("movetolastcommit") moveToLastCommit: Boolean, + @Query("partition") partition: String, + @Query("message") message: String, + @Part report: List + ): Call + + /** + * Report upload API for multiple reports at once. This is an overloaded version that takes a string as report + * format so that consumers can add support for new report formats without requiring changes to teamscale-client. + * + * @see uploadExternalReport + */ + @Multipart + @POST("api/v5.9.0/projects/{projectName}/external-analysis/session/auto-create/report") + fun uploadExternalReports( + @Path("projectName") projectName: String, + @Query("format") format: String, + @Query("t") commit: CommitDescriptor?, + @Query("revision") revision: String?, + @Query("repository") repository: String?, + @Query("movetolastcommit") moveToLastCommit: Boolean, + @Query("partition") partition: String, + @Query("message") message: String, + @Part report: List + ): Call + + /** Retrieve clustered impacted tests based on the given available tests and baseline timestamp. */ + @PUT("api/v9.4.0/projects/{projectName}/impacted-tests") + fun getImpactedTests( + @Path("projectName") projectName: String, + @Query("baseline") baseline: String?, + @Query("baseline-revision") baselineRevision: String?, + @Query("end") end: CommitDescriptor?, + @Query("end-revision") endRevision: String?, + @Query("repository") repository: String?, + @Query("partitions") partitions: List, + @Query("include-non-impacted") includeNonImpacted: Boolean, + @Query("include-failed-and-skipped") includeFailedAndSkippedTests: Boolean, + @Query("ensure-processed") ensureProcessed: Boolean, + @Query("include-added-tests") includeAddedTests: Boolean, + @Body availableTests: List + ): Call> + + /** Retrieve unclustered impacted tests based on all tests known to Teamscale and the given baseline timestamp. */ + @GET("api/v9.4.0/projects/{projectName}/impacted-tests") + fun getImpactedTests( + @Path("projectName") projectName: String, + @Query("baseline") baseline: String?, + @Query("baseline-revision") baselineRevision: String?, + @Query("end") end: CommitDescriptor?, + @Query("end-revision") endRevision: String?, + @Query("repository") repository: String?, + @Query("partitions") partitions: List, + @Query("include-non-impacted") includeNonImpacted: Boolean, + @Query("include-failed-and-skipped") includeFailedAndSkippedTests: Boolean, + @Query("ensure-processed") ensureProcessed: Boolean, + @Query("include-added-tests") includeAddedTests: Boolean + ): Call> + + /** Registers a profiler to Teamscale and returns the profiler configuration it should be started with. */ + @POST("api/v9.4.0/running-profilers") + fun registerProfiler( + @Query("configuration-id") configurationId: String, + @Body processInformation: ProcessInformation + ): Call + + /** Updates the profiler infos and sets the profiler to still alive. */ + @PUT("api/v9.4.0/running-profilers/{profilerId}") + fun sendHeartbeat( + @Path("profilerId") profilerId: String, + @Body profilerInfo: ProfilerInfo + ): Call + + /** Removes the profiler identified by the given ID. */ + @DELETE("api/v9.4.0/running-profilers/{profilerId}") + fun unregisterProfiler(@Path("profilerId") profilerId: String): Call +} + +/** + * Uploads the given report body to Teamscale as blocking call with movetolastcommit set to false. + * + * @return Returns the request body if successful, otherwise throws an IOException. + */ +@Throws(IOException::class) +fun ITeamscaleService.uploadReport( + projectName: String, + commit: CommitDescriptor?, + revision: String?, + repository: String?, + partition: String, + reportFormat: EReportFormat, + message: String, + report: RequestBody +): String { + var commit = commit + var moveToLastCommit: Boolean? = false + if (revision != null) { + // When uploading to a revision, we don't need commit adjustment. + commit = null + moveToLastCommit = null + } + + try { + val response = uploadExternalReport( + projectName, reportFormat.name, commit, revision, repository, moveToLastCommit, partition, message, report + ).execute() + + val body = response.body() + if (response.isSuccessful) { + return body?.string() ?: "" + } + + val errorBody = HttpUtils.getErrorBodyStringSafe(response) + throw IOException("Request failed with error code ${response.code()}. Response body: $errorBody") + } catch (e: IOException) { + throw IOException("Failed to upload report. ${e.message}", e) + } +} diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/JsonUtils.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/JsonUtils.kt new file mode 100644 index 000000000..305205c56 --- /dev/null +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/JsonUtils.kt @@ -0,0 +1,82 @@ +package com.teamscale.client + +import com.fasterxml.jackson.annotation.JsonAutoDetect +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.PropertyAccessor +import com.fasterxml.jackson.core.JsonFactory +import com.fasterxml.jackson.core.JsonProcessingException +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.json.JsonMapper +import java.io.File +import java.io.IOException + +/** + * Utility class for serializing and deserializing JSON using Jackson. + */ +object JsonUtils { + /** + * Jackson ObjectMapper that is used for serializing and deserializing JSON objects. The visibility settings of the + * OBJECT_MAPPER are configured to include all fields when serializing or deserializing objects, regardless of their + * visibility modifiers (public, private, etc.). + */ + val OBJECT_MAPPER: ObjectMapper = JsonMapper.builder() + .visibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE) + .visibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .serializationInclusion(JsonInclude.Include.NON_NULL) + .build() + + /** + * Creates a new instance of [JsonFactory] using the default [ObjectMapper]. + */ + fun createFactory(): JsonFactory { + return JsonFactory(OBJECT_MAPPER) + } + + /** + * Deserializes a JSON string into an object of the given class. + */ + @Throws(JsonProcessingException::class) + @JvmStatic + fun deserialize(json: String?, clazz: Class?): T { + return OBJECT_MAPPER.readValue(json, clazz) + } + + /** + * Deserializes the contents of the given file into an object of the given class. + */ + @Throws(IOException::class) + fun deserializeFile(file: File?, clazz: Class?): T { + return OBJECT_MAPPER.readValue(file, clazz) + } + + /** + * Deserializes a JSON string into a list of objects of the given class. + */ + @Throws(JsonProcessingException::class) + @JvmStatic + fun deserializeList(json: String?, elementClass: Class?): List { + return OBJECT_MAPPER.readValue( + json, + OBJECT_MAPPER.typeFactory.constructCollectionLikeType(MutableList::class.java, elementClass) + ) + } + + /** + * Serializes an object into its JSON representation. + */ + @JvmStatic + @Throws(JsonProcessingException::class) + fun serialize(value: Any?): String { + return OBJECT_MAPPER.writeValueAsString(value) + } + + /** + * Serializes an object to a file with pretty printing enabled. + */ + @Throws(IOException::class) + fun serializeToFile(file: File?, value: T) { + OBJECT_MAPPER.writer().withDefaultPrettyPrinter().writeValue(file, value) + } +} diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/PrioritizableTest.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/PrioritizableTest.kt new file mode 100644 index 000000000..1c2a98937 --- /dev/null +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/PrioritizableTest.kt @@ -0,0 +1,54 @@ +package com.teamscale.client + +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty +import java.util.* + +/** + * [TestDetails] with information about their partition as well as tracking data used during prioritization of + * tests. Two instances are considered equal if the test details are equals. + */ +class PrioritizableTest @JsonCreator constructor( + /** The uniform path without the "-test-execution" or "-execution-unit-" prefix */ + @JvmField @param:JsonProperty("testName") var testName: String +) { + /** The uniform path of the test including the "-test-execution" or "-execution-unit-" prefix. */ + var uniformPath: String? = null + + /** The reason the test has been selected. */ + var selectionReason: String? = null + + /** Partition of the test. */ + var partition: String? = null + + /** + * Duration in ms. May be null if not set. This can happen when the uploaded testwise coverage data does not include + * duration information or for new tests that have not been executed yet. + */ + var durationInMs: Long? = null + + /** + * The score determined by the TIA algorithm. The value is guaranteed to be positive. Higher values describe a + * higher probability of the test to detect potential bugs. The value can only express a relative importance + * compared to other scores of the same request. It makes no sense to compare the score against absolute values. + */ + @JsonProperty("currentScore") + var score: Double = 0.0 + + /** + * Field for storing the tests rank. The rank is the 1-based index of the test in the prioritized list. + */ + var rank: Int = 0 + + override fun toString(): String { + return StringJoiner(", ", PrioritizableTest::class.java.simpleName + "[", "]") + .add("testName='$testName'") + .add("uniformPath='$uniformPath'") + .add("selectionReason='$selectionReason'") + .add("partition='$partition'") + .add("durationInMs=$durationInMs") + .add("score=$score") + .add("rank=$rank") + .toString() + } +} diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/PrioritizableTestCluster.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/PrioritizableTestCluster.kt new file mode 100644 index 000000000..6b0b92cea --- /dev/null +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/PrioritizableTestCluster.kt @@ -0,0 +1,53 @@ +package com.teamscale.client + +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty +import java.util.* + +/** + * A [PrioritizableTestCluster] represents an ordered [List] of [PrioritizableTest]s which should be + * executed together to avoid overhead. The order of the [PrioritizableTest]s is determined by the prioritization + * of the [PrioritizableTest]s w.r.t. to each other. + * + * + * A [PrioritizableTestCluster] assumes that possibly resource intensive setup or teardown operations (e.g. a + * class containing a method annotated with `BeforeClass` in JUnit4 or `BeforeAll` in JUnit5) can be + * executed once for a [PrioritizableTestCluster] instead of executing them for each [PrioritizableTest]. + */ +class PrioritizableTestCluster @JsonCreator constructor( + /** + * The unique cluster id to which all [PrioritizableTest]s belong. + * + * @see ClusteredTestDetails.clusterId + */ + @param:JsonProperty("clusterId") var clusterId: String, + /** The [PrioritizableTest]s in this cluster. */ + @JvmField @param:JsonProperty("tests") var tests: List? +) { + /** + * The score determined by the TIA algorithm. The value is guaranteed to be positive. Higher values describe a + * higher probability of the test to detect potential bugs. The value can only express a relative importance + * compared to other scores of the same request. It makes no sense to compare the score against absolute values. + * The value is 0 if no availableTests are given. + */ + @JsonProperty("currentScore") + var score: Double = 0.0 + + /** + * Field for storing the tests rank. The rank is the 1-based index of the test + * in the prioritized list. + */ + var rank: Int = 0 + + override fun toString(): String { + return StringJoiner( + ", ", + PrioritizableTestCluster::class.java.simpleName + "[", "]" + ) + .add("clusterId='$clusterId'") + .add("score=$score") + .add("rank=$rank") + .add("tests=$tests") + .toString() + } +} diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/ProcessInformation.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/ProcessInformation.kt new file mode 100644 index 000000000..a77dfdd0a --- /dev/null +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/ProcessInformation.kt @@ -0,0 +1,11 @@ +package com.teamscale.client + +/** Information about the process and machine the profiler is attached to. */ +class ProcessInformation( + /** Hostname of the machine it is running on */ + val hostname: String, + /** Profiled PID */ + val pid: String, + /** The timestamp at which the process was started. */ + val startedAtTimestamp: Long +) diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/ProfilerConfiguration.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/ProfilerConfiguration.kt new file mode 100644 index 000000000..59326c512 --- /dev/null +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/ProfilerConfiguration.kt @@ -0,0 +1,12 @@ +package com.teamscale.client + +/** Configuration options for a profiler. */ +class ProfilerConfiguration { + /** The ID if this configuration. */ + @JvmField + var configurationId: String? = null + + /** The options that should be applied to the profiler. */ + @JvmField + var configurationOptions: String? = null +} diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/ProfilerInfo.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/ProfilerInfo.kt new file mode 100644 index 000000000..86bbec363 --- /dev/null +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/ProfilerInfo.kt @@ -0,0 +1,9 @@ +package com.teamscale.client + +/** Information about the profiler including the process it is attached to as well as the configuration it is running with. */ +class ProfilerInfo( + /** Information about the machine and process the profiler is running on. */ + var processInformation: ProcessInformation, + /** Concrete config that the profiler is running with. */ + @JvmField var profilerConfiguration: ProfilerConfiguration? +) diff --git a/teamscale-client/src/main/java/com/teamscale/client/ProfilerRegistration.java b/teamscale-client/src/main/kotlin/com/teamscale/client/ProfilerRegistration.kt similarity index 54% rename from teamscale-client/src/main/java/com/teamscale/client/ProfilerRegistration.java rename to teamscale-client/src/main/kotlin/com/teamscale/client/ProfilerRegistration.kt index d8e23d96a..bf02c98b7 100644 --- a/teamscale-client/src/main/java/com/teamscale/client/ProfilerRegistration.java +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/ProfilerRegistration.kt @@ -1,15 +1,16 @@ -package com.teamscale.client; +package com.teamscale.client /** * DTO that is sent to the profiler as a response of registering against * Teamscale and contains the profiler ID that was assigned to it as well as the * configuration it should pick up. */ -public class ProfilerRegistration { +class ProfilerRegistration { + /** The ID that was assigned to this instance of the profiler. */ + @JvmField + var profilerId: String? = null - /** The ID that was assigned to this instance of the profiler. */ - public String profilerId; - - /** The profiler configuration to use. */ - public ProfilerConfiguration profilerConfiguration; + /** The profiler configuration to use. */ + @JvmField + var profilerConfiguration: ProfilerConfiguration? = null } diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/ProxySystemProperties.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/ProxySystemProperties.kt new file mode 100644 index 000000000..e7b1f233f --- /dev/null +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/ProxySystemProperties.kt @@ -0,0 +1,78 @@ +package com.teamscale.client + +/** + * Manages Java system properties for: + * - http.proxyHost + * - http.proxyPort + * - http.proxyUser + * - http.proxyPassword + * + * Or their HTTPS counterparts (https.*). + * These values set the proxy server and credentials used to reach Teamscale. + */ +open class ProxySystemProperties(private val protocol: Protocol) { + + companion object { + private const val PROXY_HOST_SYSTEM_PROPERTY = ".proxyHost" + private const val PROXY_PORT_SYSTEM_PROPERTY = ".proxyPort" + private const val PROXY_USER_SYSTEM_PROPERTY = ".proxyUser" + private const val PROXY_PASSWORD_SYSTEM_PROPERTY = ".proxyPassword" + } + + enum class Protocol { + HTTP, HTTPS; + + override fun toString() = name.lowercase() + } + + protected open val propertyPrefix = "" + + var proxyHost: String? + get() = getProperty(PROXY_HOST_SYSTEM_PROPERTY) + set(value) { + setProperty(PROXY_HOST_SYSTEM_PROPERTY, value) + } + + var proxyPort: Int + get() = getProperty(PROXY_PORT_SYSTEM_PROPERTY)?.toIntOrNull() ?: -1 + set(value) { + check(value > 0) { "Port must be a positive integer" } + check(value <= 65535) { "Port must be less than or equal to 65535" } + setProperty(PROXY_PORT_SYSTEM_PROPERTY, value.toString()) + } + + var proxyUser: String? + get() = getProperty(PROXY_USER_SYSTEM_PROPERTY) + set(value) { + setProperty(PROXY_USER_SYSTEM_PROPERTY, value) + } + + var proxyPassword: String? + get() = getProperty(PROXY_PASSWORD_SYSTEM_PROPERTY) + set(value) { + setProperty(PROXY_PASSWORD_SYSTEM_PROPERTY, value) + } + + private fun getProperty(property: String) = + System.getProperty("$propertyPrefix${protocol}.$property") + + private fun setProperty(property: String, value: String?) { + value?.let { + check(it.isNotBlank()) { "Value must not be blank" } + System.setProperty("$propertyPrefix${protocol}.$property", it) + } + } + + fun isProxyServerSet() = !proxyHost.isNullOrEmpty() && proxyPort > 0 + + fun isProxyAuthSet() = !proxyUser.isNullOrEmpty() && !proxyPassword.isNullOrEmpty() + + fun clear() { + System.clearProperty("$propertyPrefix${protocol}.$PROXY_HOST_SYSTEM_PROPERTY") + System.clearProperty("$propertyPrefix${protocol}.$PROXY_PORT_SYSTEM_PROPERTY") + System.clearProperty("$propertyPrefix${protocol}.$PROXY_USER_SYSTEM_PROPERTY") + System.clearProperty("$propertyPrefix${protocol}.$PROXY_PASSWORD_SYSTEM_PROPERTY") + } + + class IncorrectPortFormatException(message: String, cause: Throwable) : IllegalArgumentException(message, cause) +} diff --git a/teamscale-client/src/main/java/com/teamscale/client/StringUtils.java b/teamscale-client/src/main/kotlin/com/teamscale/client/StringUtils.kt similarity index 56% rename from teamscale-client/src/main/java/com/teamscale/client/StringUtils.java rename to teamscale-client/src/main/kotlin/com/teamscale/client/StringUtils.kt index 82facb6d7..83b292720 100644 --- a/teamscale-client/src/main/java/com/teamscale/client/StringUtils.java +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/StringUtils.kt @@ -14,65 +14,66 @@ | See the License for the specific language governing permissions and | | limitations under the License. | +-------------------------------------------------------------------------*/ -package com.teamscale.client; +package com.teamscale.client -import java.text.NumberFormat; -import java.util.Iterator; -import java.util.Map; +import java.text.NumberFormat +import kotlin.math.min /** * A utility class providing some advanced string functionality. */ -public class StringUtils { +object StringUtils { + /** Line separator of the current platform. */ + val LINE_SEPARATOR: String = System.getProperty("line.separator") - /** Line separator of the current platform. */ - public static final String LINE_SEPARATOR = System.getProperty("line.separator"); - - /** The empty string. */ - public static final String EMPTY_STRING = ""; + /** The empty string. */ + const val EMPTY_STRING: String = "" /** * Checks if a string is empty (after trimming). * * @param text the string to check. - * @return true if string is empty or null, - * false otherwise. + * @return `true` if string is empty or `null`, + * `false` otherwise. */ - public static boolean isEmpty(String text) { + @JvmStatic + fun isEmpty(text: String?): Boolean { if (text == null) { - return true; + return true } - return EMPTY_STRING.equals(text.trim()); + return EMPTY_STRING == text.trim { it <= ' ' } } /** - * Determine if the supplied {@link String} is blank (i.e., {@code null} or consisting only of whitespace + * Determine if the supplied [String] is *blank* (i.e., `null` or consisting only of whitespace * characters). * - * @param str the string to check; may be {@code null} - * @return {@code true} if the string is blank + * @param str the string to check; may be `null` + * @return `true` if the string is blank */ - public static boolean isBlank(String str) { - return (str == null || str.trim().isEmpty()); + @JvmStatic + fun isBlank(str: String?): Boolean { + return (str == null || str.trim { it <= ' ' }.isEmpty()) } /** * Returns the beginning of a String, cutting off the last part which is separated by the given character. - *

+ * + * * E.g., removeLastPart("org.conqat.lib.commons.string.StringUtils", '.') gives "org.conqat.lib.commons.string". * * @param string the String * @param separator separation character * @return the String without the last part, or the original string if the separation character is not found. */ - public static String removeLastPart(String string, char separator) { - int idx = string.lastIndexOf(separator); + fun removeLastPart(string: String, separator: Char): String { + val idx = string.lastIndexOf(separator) if (idx == -1) { - return string; + return string } - return string.substring(0, idx); + return string.substring(0, idx) } /** @@ -82,11 +83,12 @@ public static String removeLastPart(String string, char separator) { * @param prefix the prefix * @return the string without the prefix or the original string if it does not start with the prefix. */ - public static String stripPrefix(String string, String prefix) { + @JvmStatic + fun stripPrefix(string: String, prefix: String): String { if (string.startsWith(prefix)) { - return string.substring(prefix.length()); + return string.substring(prefix.length) } - return string; + return string } /** @@ -96,18 +98,12 @@ public static String stripPrefix(String string, String prefix) { * @param suffix the suffix * @return the string without the suffix or the original string if it does not end with the suffix. */ - public static String stripSuffix(String string, String suffix) { + @JvmStatic + fun stripSuffix(string: String, suffix: String): String { if (string.endsWith(suffix)) { - return string.substring(0, string.length() - suffix.length()); + return string.substring(0, string.length - suffix.length) } - return string; - } - - /** - * Create string representation of a map. - */ - public static String toString(Map map) { - return toString(map, EMPTY_STRING); + return string } /** @@ -116,33 +112,37 @@ public static String toString(Map map) { * @param map the map * @param indent a line indent */ - public static String toString(Map map, String indent) { - StringBuilder result = new StringBuilder(); - Iterator keyIterator = map.keySet().iterator(); + /** + * Create string representation of a map. + */ + @JvmOverloads + fun toString(map: Map<*, *>, indent: String? = EMPTY_STRING): String { + val result = StringBuilder() + val keyIterator = map.keys.iterator() while (keyIterator.hasNext()) { - result.append(indent); - Object key = keyIterator.next(); - result.append(key); - result.append(" = "); - result.append(map.get(key)); + result.append(indent) + val key = keyIterator.next()!! + result.append(key) + result.append(" = ") + result.append(map[key]) if (keyIterator.hasNext()) { - result.append(LINE_SEPARATOR); + result.append(LINE_SEPARATOR) } } - return result.toString(); + return result.toString() } /** * Format number with number formatter, if number formatter is - * null, this uses {@link String#valueOf(double)}. + * `null`, this uses [String.valueOf]. */ - public static String format(double number, NumberFormat numberFormat) { + fun format(number: Double, numberFormat: NumberFormat?): String { if (numberFormat == null) { - return String.valueOf(number); + return number.toString() } - return numberFormat.format(number); + return numberFormat.format(number) } /** @@ -151,41 +151,46 @@ public static String format(double number, NumberFormat numberFormat) { * complexity is O(n+m), where n/m are the lengths of the strings. Note that due to the high running time, for long * strings the Diff class should be used, that has a more efficient algorithm, but only for insert/delete (not * replace operation). - *

+ * + * * Although this is a clean reimplementation, the basic algorithm is explained here: * http://en.wikipedia.org/wiki/Levenshtein_distance# Iterative_with_two_matrix_rows */ - public static int editDistance(String s, String t) { - char[] sChars = s.toCharArray(); - char[] tChars = t.toCharArray(); - int m = s.length(); - int n = t.length(); - - int[] distance = new int[m + 1]; - for (int i = 0; i <= m; ++i) { - distance[i] = i; + @JvmStatic + fun editDistance(s: String, t: String): Int { + val sChars = s.toCharArray() + val tChars = t.toCharArray() + val m = s.length + val n = t.length + + var distance = IntArray(m + 1) + for (i in 0..m) { + distance[i] = i } - int[] oldDistance = new int[m + 1]; - for (int j = 1; j <= n; ++j) { - + var oldDistance = IntArray(m + 1) + for (j in 1..n) { // swap distance and oldDistance - int[] tmp = oldDistance; - oldDistance = distance; - distance = tmp; - - distance[0] = j; - for (int i = 1; i <= m; ++i) { - int cost = 1 + Math.min(distance[i - 1], oldDistance[i]); - if (sChars[i - 1] == tChars[j - 1]) { - cost = Math.min(cost, oldDistance[i - 1]); + + val tmp = oldDistance + oldDistance = distance + distance = tmp + + distance[0] = j + for (i in 1..m) { + var cost = (1 + min( + distance[i - 1].toDouble(), + oldDistance[i].toDouble() + )).toInt() + cost = if (sChars[i - 1] == tChars[j - 1]) { + min(cost.toDouble(), oldDistance[i - 1].toDouble()).toInt() } else { - cost = Math.min(cost, 1 + oldDistance[i - 1]); + min(cost.toDouble(), (1 + oldDistance[i - 1]).toDouble()).toInt() } - distance[i] = cost; + distance[i] = cost } } - return distance[m]; + return distance[m] } } diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleClient.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleClient.kt new file mode 100644 index 000000000..1f24b0f4a --- /dev/null +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleClient.kt @@ -0,0 +1,254 @@ +package com.teamscale.client + +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.MultipartBody +import okhttp3.MultipartBody.Companion.FORM +import okhttp3.RequestBody.Companion.asRequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import retrofit2.Response +import java.io.File +import java.io.IOException +import java.time.Duration +import java.util.* + +/** Helper class to interact with Teamscale. */ +open class TeamscaleClient { + /** Teamscale service implementation. */ + val service: ITeamscaleService + + /** The project ID within Teamscale. */ + private val projectId: String + + /** Constructor with parameters for read and write timeout in seconds. */ + @JvmOverloads + constructor( + baseUrl: String, + user: String, + accessToken: String, + projectId: String, + readTimeout: Duration = HttpUtils.DEFAULT_READ_TIMEOUT, + writeTimeout: Duration = HttpUtils.DEFAULT_WRITE_TIMEOUT + ) { + val url = baseUrl.toHttpUrlOrNull() ?: throw IllegalArgumentException("Invalid URL: $baseUrl") + this.projectId = projectId + service = TeamscaleServiceGenerator.createService( + ITeamscaleService::class.java, url, user, accessToken, readTimeout, writeTimeout + ) + } + + /** Constructor with parameters for read and write timeout in seconds and logfile. */ + @JvmOverloads + constructor( + baseUrl: String, + user: String, + accessToken: String, + projectId: String, + logfile: File?, + readTimeout: Duration = HttpUtils.DEFAULT_READ_TIMEOUT, + writeTimeout: Duration = HttpUtils.DEFAULT_WRITE_TIMEOUT + ) { + val url = baseUrl.toHttpUrlOrNull() ?: throw IllegalArgumentException("Invalid URL: $baseUrl") + this.projectId = projectId + service = TeamscaleServiceGenerator.createServiceWithRequestLogging( + ITeamscaleService::class.java, url, user, accessToken, logfile, readTimeout, writeTimeout + ) + } + + /** + * Tries to retrieve the impacted tests from Teamscale. This should be used in a CI environment, because it ensures + * that the given commit has been processed by Teamscale and also considers previous failing tests for + * re-execution. + * + * @param availableTests A list of tests that is locally available for execution. This allows TIA to consider newly + * added tests in addition to those that are already known and allows to filter e.g. if the + * user has already selected a subset of relevant tests. This can be `null` to + * indicate that only tests known to Teamscale should be suggested. + * @param baseline The baseline timestamp AFTER which changes should be considered. Changes that happened + * exactly at the baseline will be excluded. In case you want to retrieve impacted tests for a + * single commit with a known timestamp you can append a `"p1"` suffix to the + * timestamp to indicate that you are interested in the changes that happened after the parent + * of the given commit. + * @param baselineRevision Same as baseline but accepts a revision (e.g. git SHA1) instead of a branch and timestamp + * @param endCommit The last commit for which changes should be considered. + * @param endRevision Same as endCommit but accepts a revision (e.g. git SHA1) instead of a branch and timestamp + * @param repository The repository id in your Teamscale project which Teamscale should use to look up the revision, if given. + * Null or empty will lead to a lookup in all repositories in the Teamscale project. + * @param partitions The partitions that should be considered for retrieving impacted tests. Can be + * `null` to indicate that tests from all partitions should be returned. + * @return A list of test clusters to execute. If availableTests is null, a single dummy cluster is returned with + * all prioritized tests. + */ + @Throws(IOException::class) + open fun getImpactedTests( + availableTests: List?, + baseline: String, + baselineRevision: String, + endCommit: CommitDescriptor, + endRevision: String, + repository: String, + partitions: List, + includeNonImpacted: Boolean, + includeAddedTests: Boolean, includeFailedAndSkipped: Boolean + ): Response?> { + val selectedOptions: MutableList = ArrayList(listOf(ETestImpactOptions.ENSURE_PROCESSED)) + if (includeNonImpacted) { + selectedOptions.add(ETestImpactOptions.INCLUDE_NON_IMPACTED) + } + if (includeAddedTests) { + selectedOptions.add(ETestImpactOptions.INCLUDE_ADDED_TESTS) + } + if (includeFailedAndSkipped) { + selectedOptions.add(ETestImpactOptions.INCLUDE_FAILED_AND_SKIPPED) + } + return getImpactedTests( + availableTests, baseline, baselineRevision, endCommit, endRevision, repository, partitions, + *selectedOptions.toTypedArray() + ) + } + + /** + * Tries to retrieve the impacted tests from Teamscale. Use this method if you want to query time range based or you + * want to exclude failed and skipped tests from previous test runs. + * + * @param availableTests A list of tests that is locally available for execution. This allows TIA to consider newly + * added tests in addition to those that are already known and allows to filter e.g. if the + * user has already selected a subset of relevant tests. This can be `null` to + * indicate that only tests known to Teamscale should be suggested. + * @param baseline The baseline timestamp AFTER which changes should be considered. Changes that happened + * exactly at the baseline will be excluded. In case you want to retrieve impacted tests for a + * single commit with a known timestamp you can append a `"p1"` suffix to the + * timestamp to indicate that you are interested in the changes that happened after the parent + * of the given commit. + * @param baselineRevision Same as baseline but accepts a revision (e.g. git SHA1) instead of a branch and timestamp + * @param endCommit The last commit for which changes should be considered. + * @param endRevision Same as endCommit but accepts a revision (e.g. git SHA1) instead of a branch and timestamp + * @param repository The repository id in your Teamscale project which Teamscale should use to look up the revision, if given. + * Null or empty will lead to a lookup in all repositories in the Teamscale project. + * @param partitions The partitions that should be considered for retrieving impacted tests. Can be + * `null` to indicate that tests from all partitions should be returned. + * @param options A list of options (See [ETestImpactOptions] for more details) + * @return A list of test clusters to execute. If availableTests is null, a single dummy cluster is returned with + * all prioritized tests. + */ + @Throws(IOException::class) + private fun getImpactedTests( + availableTests: List?, + baseline: String, + baselineRevision: String, + endCommit: CommitDescriptor, + endRevision: String, + repository: String, + partitions: List, + vararg options: ETestImpactOptions + ): Response?> { + val testImpactOptions = EnumSet.copyOf(listOf(*options)) + val includeNonImpacted = testImpactOptions.contains(ETestImpactOptions.INCLUDE_NON_IMPACTED) + val includeFailedAndSkipped = testImpactOptions.contains(ETestImpactOptions.INCLUDE_FAILED_AND_SKIPPED) + val ensureProcessed = testImpactOptions.contains(ETestImpactOptions.ENSURE_PROCESSED) + val includeAddedTests = testImpactOptions.contains(ETestImpactOptions.INCLUDE_ADDED_TESTS) + + return if (availableTests == null) { + wrapInCluster( + service.getImpactedTests( + projectId, baseline, baselineRevision, endCommit, endRevision, repository, partitions, + includeNonImpacted, + includeFailedAndSkipped, + ensureProcessed, includeAddedTests + ).execute() + ) + } else { + val availableTestsMap = availableTests.map { clusteredTestDetails -> + TestWithClusterId.fromClusteredTestDetails( + clusteredTestDetails + ) + } + service.getImpactedTests( + projectId, baseline, baselineRevision, endCommit, endRevision, repository, partitions, + includeNonImpacted, includeFailedAndSkipped, ensureProcessed, includeAddedTests, availableTestsMap + ).execute() + } + } + + /** Uploads multiple reports to Teamscale in the given [EReportFormat]. */ + @Throws(IOException::class) + open fun uploadReports( + reportFormat: EReportFormat, + reports: Collection, + commitDescriptor: CommitDescriptor?, + revision: String?, + repository: String?, + partition: String, + message: String + ) { + uploadReports(reportFormat.name, reports, commitDescriptor, revision, repository, partition, message) + } + + /** Uploads multiple reports to Teamscale. */ + @Throws(IOException::class) + open fun uploadReports( + reportFormat: String, + reports: Collection, + commitDescriptor: CommitDescriptor?, + revision: String?, + repository: String?, + partition: String, + message: String + ) { + val partList = reports.map { file -> + val requestBody = file.asRequestBody(FORM) + MultipartBody.Part.createFormData("report", file.name, requestBody) + } + + val response = service + .uploadExternalReports( + projectId, reportFormat, commitDescriptor, revision, repository, true, partition, message, partList + ).execute() + if (!response.isSuccessful) { + throw IOException("HTTP request failed: " + HttpUtils.getErrorBodyStringSafe(response)) + } + } + + /** Uploads one in-memory report to Teamscale. */ + @Throws(IOException::class) + open fun uploadReport( + reportFormat: EReportFormat, + report: String, + commitDescriptor: CommitDescriptor?, + revision: String?, + repository: String?, + partition: String, + message: String + ) { + service.uploadReport( + projectId, + commitDescriptor, + revision, + repository, + partition, + reportFormat, + message, + report.toRequestBody(FORM) + ) + } + + companion object { + private fun wrapInCluster( + testListResponse: Response> + ): Response?> { + return if (testListResponse.isSuccessful) { + Response.success( + listOf(PrioritizableTestCluster( + "dummy", + testListResponse.body() + )), + testListResponse.raw() + ) + } else { + Response.error( + testListResponse.errorBody()!!, + testListResponse.raw() + ) + } + } + } +} diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleProxySystemProperties.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleProxySystemProperties.kt new file mode 100644 index 000000000..2eb813c10 --- /dev/null +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleProxySystemProperties.kt @@ -0,0 +1,22 @@ +package com.teamscale.client + +/** + * Reads and writes Java system properties values for + * + * * teamscale.http.proxyHost + * * teamscale.http.proxyPort + * * teamscale.http.proxyUser + * * teamscale.http.proxyPassword + * + * or the corresponding HTTPS counterpart (starting with https instead of http). + * These values set the proxy server and credentials that should be used later to reach Teamscale and take precedence + * over the default proxy settings (see [ProxySystemProperties.ProxySystemProperties]). + */ +class TeamscaleProxySystemProperties(protocol: Protocol) : ProxySystemProperties(protocol) { + override val propertyPrefix: String + get() = TEAMSCALE_PREFIX + + companion object { + const val TEAMSCALE_PREFIX = "teamscale." + } +} \ No newline at end of file diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServer.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServer.kt new file mode 100644 index 000000000..43a271399 --- /dev/null +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServer.kt @@ -0,0 +1,174 @@ +package com.teamscale.client + +import okhttp3.HttpUrl +import java.net.InetAddress +import java.net.UnknownHostException +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter + +/** Holds Teamscale server details. */ +class TeamscaleServer { + /** The URL of the Teamscale server. */ + @JvmField + var url: HttpUrl? = null + + /** The project id within Teamscale. */ + @JvmField + var project: String? = null + + /** The user name used to authenticate against Teamscale. */ + @JvmField + var userName: String? = null + + /** The user's access token. */ + @JvmField + var userAccessToken: String? = null + + /** The partition to upload reports to. */ + @JvmField + var partition: String? = null + + /** + * The corresponding code commit to which the coverage belongs. If this is null, the Agent is supposed to + * auto-detect the commit from the profiled code. + */ + @JvmField + var commit: CommitDescriptor? = null + + /** + * The corresponding code revision to which the coverage belongs. This is currently only supported for testwise + * mode. + */ + @JvmField + var revision: String? = null + + /** + * The repository id in your Teamscale project which Teamscale should use to look up the revision, if given. + * Null or empty will lead to a lookup in all repositories in the Teamscale project. + */ + @JvmField + var repository: String? = null + + /** + * The configuration ID that was used to retrieve the profiler configuration. This is only set here to append it to + * the default upload message. + */ + @JvmField + var configId: String? = null + + var message: String? = null + /** + * The commit message shown in the Teamscale UI for the coverage upload. If the message is null, auto-generates a + * sensible message. + */ + get() { + if (field == null) { + return createDefaultMessage() + } + return field + } + + private fun createDefaultMessage(): String { + // we do not include the IP address here as one host may have + // - multiple network interfaces + // - each with multiple IP addresses + // - in either IPv4 or IPv6 format + // - and it is unclear which of those is "the right one" or even just which is useful (e.g. loopback or virtual + // adapters are not useful and might even confuse readers) + var hostnamePart = "uploaded from " + hostnamePart += try { + "hostname: " + InetAddress.getLocalHost().hostName + } catch (e: UnknownHostException) { + "an unknown computer" + } + + var revisionPart = "" + if (revision != null) { + revisionPart = "\nfor revision: $revision" + } + + var configIdPart = "" + if (configId != null) { + configIdPart = "\nprofiler configuration ID: $configId" + } + + return """$partition coverage uploaded at ${DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now())} + +$hostnamePart$revisionPart$configIdPart""" + } + + val isConfiguredForSingleProjectTeamscaleUpload: Boolean + /** Checks if all fields required for a single-project Teamscale upload are non-null. */ + get() = isConfiguredForServerConnection && partition != null && project != null + + val isConfiguredForMultiProjectUpload: Boolean + /** Checks if all fields required for a Teamscale upload are non-null, except the project which must be null. */ + get() = isConfiguredForServerConnection && partition != null && project == null + + val isConfiguredForServerConnection: Boolean + /** Checks if all required fields to access a Teamscale server are non-null. */ + get() = url != null && userName != null && userAccessToken != null + + /** Whether a URL, user and access token were provided. */ + fun canConnectToTeamscale(): Boolean { + return url != null && userName != null && userAccessToken != null + } + + /** Returns whether all fields are null. */ + fun hasAllFieldsNull(): Boolean { + return url == null && project == null && userName == null && userAccessToken == null && partition == null && commit == null && revision == null + } + + /** Returns whether either a commit or revision has been set. */ + fun hasCommitOrRevision(): Boolean { + return commit != null || revision != null + } + + /** Checks if another TeamscaleServer has the same project and revision/commit as this TeamscaleServer instance. */ + fun hasSameProjectAndCommit(other: TeamscaleServer): Boolean { + if (this.project != other.project) { + return false + } + if (this.revision != null) { + return this.revision == other.revision + } + return this.commit == other.commit + } + + override fun toString(): String { + var at: String + if (revision != null) { + at = "revision $revision" + if (repository != null) { + at += "in repository $repository" + } + } else { + at = "commit $commit" + } + return "Teamscale $url as user $userName for $project to $partition at $at" + } + + /** Creates a copy of the [TeamscaleServer] configuration, but with the given project and commit set. */ + fun withProjectAndCommit(teamscaleProject: String?, commitDescriptor: CommitDescriptor?): TeamscaleServer { + val teamscaleServer = TeamscaleServer() + teamscaleServer.url = url + teamscaleServer.userName = userName + teamscaleServer.userAccessToken = userAccessToken + teamscaleServer.partition = partition + teamscaleServer.project = teamscaleProject + teamscaleServer.commit = commitDescriptor + return teamscaleServer + } + + /** Creates a copy of the [TeamscaleServer] configuration, but with the given project and revision set. */ + fun withProjectAndRevision(teamscaleProject: String?, revision: String?): TeamscaleServer { + val teamscaleServer = TeamscaleServer() + teamscaleServer.url = url + teamscaleServer.userName = userName + teamscaleServer.userAccessToken = userAccessToken + teamscaleServer.partition = partition + teamscaleServer.project = teamscaleProject + teamscaleServer.revision = revision + return teamscaleServer + } +} diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServiceGenerator.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServiceGenerator.kt new file mode 100644 index 000000000..92554a862 --- /dev/null +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServiceGenerator.kt @@ -0,0 +1,97 @@ +package com.teamscale.client + +import okhttp3.* +import retrofit2.Retrofit +import retrofit2.converter.jackson.JacksonConverterFactory +import java.io.File +import java.io.IOException +import java.time.Duration + +/** Helper class for generating a teamscale compatible service. */ +object TeamscaleServiceGenerator { + /** Custom user agent of the requests, used to monitor API traffic. */ + const val USER_AGENT: String = "Teamscale JaCoCo Agent" + + /** + * Generates a [Retrofit] instance for the given service, which uses basic auth to authenticate against the + * server and which sets the accept header to json. + */ + @JvmStatic + @JvmOverloads + // ToDo: Should use reified type parameter when all usages are in Kotlin + fun createService( + serviceClass: Class, + baseUrl: HttpUrl, + username: String, + accessToken: String, + readTimeout: Duration = HttpUtils.DEFAULT_READ_TIMEOUT, + writeTimeout: Duration = HttpUtils.DEFAULT_WRITE_TIMEOUT, + vararg interceptors: Interceptor + ) = createServiceWithRequestLogging( + serviceClass, baseUrl, username, accessToken, null, readTimeout, writeTimeout, *interceptors + ) + + /** + * Generates a [Retrofit] instance for the given service, which uses basic auth to authenticate against the + * server and which sets the accept-header to json. Logs requests and responses to the given logfile. + */ + // ToDo: Should use reified type parameter when all usages are in Kotlin + fun createServiceWithRequestLogging( + serviceClass: Class, + baseUrl: HttpUrl, + username: String, + accessToken: String, + logfile: File?, + readTimeout: Duration, + writeTimeout: Duration, + vararg interceptors: Interceptor + ): S { + val retrofit = HttpUtils.createRetrofit( + { retrofitBuilder: Retrofit.Builder -> + retrofitBuilder.baseUrl(baseUrl) + .addConverterFactory(JacksonConverterFactory.create(JsonUtils.OBJECT_MAPPER)) + }, + { okHttpBuilder: OkHttpClient.Builder -> + addInterceptors(okHttpBuilder, *interceptors) + .addInterceptor(HttpUtils.getBasicAuthInterceptor(username, accessToken)) + .addInterceptor(AcceptJsonInterceptor()) + .addNetworkInterceptor(CustomUserAgentInterceptor()) + if (logfile != null) { + okHttpBuilder.addInterceptor(FileLoggingInterceptor(logfile)) + } + }, + readTimeout, writeTimeout + ) + return retrofit.create(serviceClass) + } + + private fun addInterceptors(builder: OkHttpClient.Builder, vararg interceptors: Interceptor): OkHttpClient.Builder { + interceptors.forEach { interceptor -> + builder.addInterceptor(interceptor) + } + return builder + } + + + /** + * Sets an `Accept: application/json` header on all requests. + */ + private class AcceptJsonInterceptor : Interceptor { + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + val newRequest = chain.request().newBuilder().header("Accept", "application/json").build() + return chain.proceed(newRequest) + } + } + + /** + * Sets the custom user agent [.USER_AGENT] header on all requests. + */ + class CustomUserAgentInterceptor : Interceptor { + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + val newRequest = chain.request().newBuilder().header("User-Agent", USER_AGENT).build() + return chain.proceed(newRequest) + } + } +} diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/TestData.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/TestData.kt new file mode 100644 index 000000000..4c8058666 --- /dev/null +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/TestData.kt @@ -0,0 +1,76 @@ +package com.teamscale.client + +import com.teamscale.client.TestData.Builder +import org.apache.commons.codec.binary.Hex +import org.apache.commons.codec.digest.DigestUtils +import java.io.IOException +import java.nio.file.Path +import java.security.MessageDigest + +/** + * Represents additional test data to attach to [ClusteredTestDetails]. Use the [Builder] to create new + * [TestData] objects. + * + * + * Internally, the data you pass to the builder is hashed and only the hash is transferred as [ ][ClusteredTestDetails.content] to Teamscale to save network bandwidth and RAM. Whenever a test case's hash changes, + * Teamscale will select it for the next TIA test run. + */ +class TestData private constructor( + /** The hash of the test data which will be sent to Teamscale as the [ClusteredTestDetails.content]. */ /*package*/ + val hash: String +) { + /** + * Builder for [TestData] objects. This class is thread-safe and ensures that reading the test data does not + * result in [OutOfMemoryError]s. + */ + class Builder { + private var digest: MessageDigest? = DigestUtils.getSha1Digest() + + /** Adds the given bytes as additional test data. */ + @Synchronized + fun addByteArray(content: ByteArray?): Builder { + ensureHasNotBeenFinalized() + DigestUtils.updateDigest(digest, content) + DigestUtils.updateDigest(digest, DIGEST_SEPARATOR) + return this + } + + private fun ensureHasNotBeenFinalized() { + checkNotNull(digest) { "You tried to use this TestData.Builder after calling #build() on it. Builders cannot be reused." } + } + + /** Adds the given String as additional test data. */ + @Synchronized + fun addString(content: String?): Builder { + ensureHasNotBeenFinalized() + DigestUtils.updateDigest(digest, content) + DigestUtils.updateDigest(digest, DIGEST_SEPARATOR) + return this + } + + /** Adds the contents of the given file path as additional test data. */ + @Synchronized + @Throws(IOException::class) + fun addFileContent(fileWithContent: Path): Builder { + ensureHasNotBeenFinalized() + DigestUtils.updateDigest(digest, fileWithContent) + DigestUtils.updateDigest(digest, DIGEST_SEPARATOR) + return this + } + + /** + * Builds the [TestData] object. After calling this method, you cannot use this builder anymore. + */ + @Synchronized + fun build(): TestData { + ensureHasNotBeenFinalized() + val hash = Hex.encodeHexString(digest!!.digest()) + digest = null + return TestData(hash) + } + + companion object { + private val DIGEST_SEPARATOR = "-!#!-".toByteArray() + } + } +} diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/TestDetails.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/TestDetails.kt new file mode 100644 index 000000000..1293efa5d --- /dev/null +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/TestDetails.kt @@ -0,0 +1,42 @@ +package com.teamscale.client + +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty +import java.util.* + +/** + * Contains details about a test. + */ +open class TestDetails @JsonCreator constructor( + /** Unique name of the test case by using a path like hierarchical description, which can be shown in the UI. */ + @JvmField @param:JsonProperty("uniformPath") var uniformPath: String, + /** + * Path to the source of the method. Will be equal to uniformPath in most cases, but e.g. @Test methods in a base + * class will have the sourcePath pointing to the base class which contains the actual implementation whereas + * uniformPath will contain the the class name of the most specific subclass, from where it was actually executed. + */ + @JvmField @param:JsonProperty("sourcePath") var sourcePath: String?, + /** + * Some kind of content to tell whether the test specification has changed. Can be revision number or hash over the + * specification or similar. You can include e.g. a hash of each test's test data so that whenever the test data + * changes, the corresponding test is re-run. + */ + @param:JsonProperty("content") var content: String? +) { + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + if (other == null || javaClass != other.javaClass) { + return false + } + val that = other as TestDetails + return uniformPath == that.uniformPath && + sourcePath == that.sourcePath && + content == that.content + } + + override fun hashCode(): Int { + return Objects.hash(uniformPath, sourcePath, content) + } +} diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/TestWithClusterId.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/TestWithClusterId.kt new file mode 100644 index 000000000..d280848d5 --- /dev/null +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/TestWithClusterId.kt @@ -0,0 +1,38 @@ +package com.teamscale.client + +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty + +/** + * Test with additional information about which cluster of tests the test case belongs to during prioritization. + */ +class TestWithClusterId @JsonCreator constructor( + /** + * The uniform path of the test (unescaped and without -test-execution- prefix). + */ + @param:JsonProperty("testName") val testName: String?, + /** + * The hashed content of the test. + */ + @param:JsonProperty("hash") val hash: String?, + /** + * The partition of the test. + */ + @param:JsonProperty("partition") val partition: String?, + /** + * A unique identifier for the cluster this test should be prioritized within. May not be null. + */ + @param:JsonProperty("clusterId") val clusterId: String? +) { + companion object { + /** + * Creates a #TestWithClusterId from a #ClusteredTestDetails object. + */ + fun fromClusteredTestDetails(clusteredTestDetails: ClusteredTestDetails): TestWithClusterId { + return TestWithClusterId( + clusteredTestDetails.uniformPath, clusteredTestDetails.content, + clusteredTestDetails.partition, clusteredTestDetails.clusterId + ) + } + } +} diff --git a/teamscale-client/src/test/java/com/teamscale/client/ProxySystemPropertiesTest.java b/teamscale-client/src/test/java/com/teamscale/client/ProxySystemPropertiesTest.java deleted file mode 100644 index 29eb83cd3..000000000 --- a/teamscale-client/src/test/java/com/teamscale/client/ProxySystemPropertiesTest.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.teamscale.client; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; - -class ProxySystemPropertiesTest { - - private static final ProxySystemProperties properties = new ProxySystemProperties(ProxySystemProperties.Protocol.HTTP); - - @AfterAll - static void teardown() { - properties.removeProxyPort(); - } - - @Test - void testPortParsing() { - properties.setProxyPort(9876); - assertThat(properties.getProxyPort()).isEqualTo(9876); - properties.setProxyPort(""); - assertThat(properties.getProxyPort()).isEqualTo(-1); - String incorrectFormatValue = "nonsense"; - properties.setProxyPort(incorrectFormatValue); - ProxySystemProperties.IncorrectPortFormatException exception = assertThrows(ProxySystemProperties.IncorrectPortFormatException.class, - properties::getProxyPort); - assertThat(exception.getMessage()).isEqualTo(String.format("Could not parse proxy port \"%s\" set via \"%s\"", incorrectFormatValue, properties.getProxyPortSystemPropertyName())); - properties.removeProxyPort(); - assertThat(properties.getProxyPort()).isEqualTo(-1); - } - -} \ No newline at end of file diff --git a/teamscale-client/src/test/java/com/teamscale/client/TeamscaleServerTest.java b/teamscale-client/src/test/java/com/teamscale/client/TeamscaleServerTest.java deleted file mode 100644 index ef02d847d..000000000 --- a/teamscale-client/src/test/java/com/teamscale/client/TeamscaleServerTest.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.teamscale.client; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -class TeamscaleServerTest { - - @Test - void testDefaultMessage() { - TeamscaleServer server = new TeamscaleServer(); - server.partition = "Unit Test"; - server.revision = "rev123"; - - String message = server.getMessage(); - String normalizedMessage = message.replaceAll("uploaded at .*", "uploaded at DATE") - .replaceAll("hostname: .*", "hostname: HOST"); - assertEquals("Unit Test coverage uploaded at DATE\n\nuploaded from hostname: HOST\nfor revision: rev123", - normalizedMessage); - } - -} \ No newline at end of file diff --git a/teamscale-client/src/test/java/com/teamscale/client/TeamscaleServiceGeneratorProxyServerTest.java b/teamscale-client/src/test/java/com/teamscale/client/TeamscaleServiceGeneratorProxyServerTest.java deleted file mode 100644 index e4e19aa3a..000000000 --- a/teamscale-client/src/test/java/com/teamscale/client/TeamscaleServiceGeneratorProxyServerTest.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.teamscale.client; - -import okhttp3.HttpUrl; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import okhttp3.mockwebserver.RecordedRequest; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Base64; - -import static com.teamscale.client.HttpUtils.PROXY_AUTHORIZATION_HTTP_HEADER; -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests that our Retrofit + OkHttp client is using the Java proxy system properties ({@code http.proxy*}) if set - */ -class TeamscaleServiceGeneratorProxyServerTest { - - private MockWebServer mockProxyServer; - private final ProxySystemProperties proxySystemProperties = new ProxySystemProperties( - ProxySystemProperties.Protocol.HTTP); - - private final TeamscaleProxySystemProperties teamscaleProxySystemProperties = new TeamscaleProxySystemProperties( - ProxySystemProperties.Protocol.HTTP); - - @BeforeEach - void setUp() throws IOException { - mockProxyServer = new MockWebServer(); - mockProxyServer.start(); - } - - - @Test - void testTeamscaleProxyAuthentication() throws Exception { - String incorrectValue = "incorrect"; - // the teamscale-specific options should take precedence over the global ones - proxySystemProperties.setProxyHost(incorrectValue); - proxySystemProperties.setProxyPort(incorrectValue); - proxySystemProperties.setProxyUser(incorrectValue); - proxySystemProperties.setProxyPassword(incorrectValue); - - teamscaleProxySystemProperties.setProxyHost(mockProxyServer.getHostName()); - teamscaleProxySystemProperties.setProxyPort(mockProxyServer.getPort()); - - String proxyUser = "myProxyUser"; - String proxyPassword = "myProxyPassword"; - String base64EncodedBasicAuth = Base64.getEncoder().encodeToString((proxyUser + ":" + proxyPassword).getBytes( - StandardCharsets.UTF_8)); - teamscaleProxySystemProperties.setProxyUser(proxyUser); - teamscaleProxySystemProperties.setProxyPassword(proxyPassword); - - assertProxyAuthenticationIsUsed(base64EncodedBasicAuth); - } - - private void assertProxyAuthenticationIsUsed(String base64EncodedBasicAuth) throws InterruptedException, IOException { - ITeamscaleService service = TeamscaleServiceGenerator.createService(ITeamscaleService.class, - HttpUrl.parse("http://localhost:1337"), - "someUser", "someAccesstoken", HttpUtils.DEFAULT_READ_TIMEOUT, - HttpUtils.DEFAULT_WRITE_TIMEOUT); - - // First time Retrofit/OkHttp tires without proxy auth. - // When we return 407 Proxy Authentication Required, it retries with proxy authentication. - mockProxyServer.enqueue(new MockResponse().setResponseCode(407)); - mockProxyServer.enqueue(new MockResponse().setResponseCode(200)); - service.sendHeartbeat("", new ProfilerInfo(new ProcessInformation("", "", 0), null)).execute(); - - assertThat(mockProxyServer.getRequestCount()).isEqualTo(2); - - mockProxyServer.takeRequest(); // First request which doesn't have the proxy authentication set yet - RecordedRequest requestWithProxyAuth = mockProxyServer.takeRequest();// Request we are actually interested in - - assertThat(requestWithProxyAuth.getHeader(PROXY_AUTHORIZATION_HTTP_HEADER)).isEqualTo( - "Basic " + base64EncodedBasicAuth); - } - - @AfterEach - void tearDown() throws IOException { - clearProxySystemProperties(proxySystemProperties); - clearProxySystemProperties(teamscaleProxySystemProperties); - - mockProxyServer.shutdown(); - mockProxyServer.close(); - } - - private void clearProxySystemProperties(ProxySystemProperties proxySystemProperties) { - proxySystemProperties.setProxyHost(""); - proxySystemProperties.setProxyPort(""); - proxySystemProperties.setProxyUser(""); - proxySystemProperties.setProxyPassword(""); - } -} \ No newline at end of file diff --git a/teamscale-client/src/test/java/com/teamscale/client/TestDataTest.java b/teamscale-client/src/test/java/com/teamscale/client/TestDataTest.java deleted file mode 100644 index 095acd88e..000000000 --- a/teamscale-client/src/test/java/com/teamscale/client/TestDataTest.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.teamscale.client; - -import org.junit.jupiter.api.Test; - -class TestDataTest { - - @Test - public void ensureHashingDoesNotThrowException() { - new TestData.Builder().addByteArray(new byte[]{1, 2, 3}).addString("string").build(); - } - -} \ No newline at end of file diff --git a/teamscale-client/src/test/kotlin/com/teamscale/client/ProxySystemPropertiesTest.kt b/teamscale-client/src/test/kotlin/com/teamscale/client/ProxySystemPropertiesTest.kt new file mode 100644 index 000000000..9a1111fe5 --- /dev/null +++ b/teamscale-client/src/test/kotlin/com/teamscale/client/ProxySystemPropertiesTest.kt @@ -0,0 +1,33 @@ +package com.teamscale.client + +import org.assertj.core.api.Assertions +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.Test + +internal class ProxySystemPropertiesTest { + @Test + fun testPortParsing() { + properties.proxyPort = 9876 + assertThat(properties.proxyPort).isEqualTo(9876) + assertThatThrownBy { + properties.proxyPort = 0 + }.hasMessage("Port must be a positive integer") + assertThatThrownBy { + properties.proxyPort = 65536 + }.hasMessage("Port must be less than or equal to 65535") + properties.clear() + assertThat(properties.proxyPort).isEqualTo(-1) + } + + companion object { + private val properties = ProxySystemProperties(ProxySystemProperties.Protocol.HTTP) + + @JvmStatic + @AfterAll + fun teardown() { + properties.clear() + } + } +} \ No newline at end of file diff --git a/teamscale-client/src/test/kotlin/com/teamscale/client/TeamscaleServerTest.kt b/teamscale-client/src/test/kotlin/com/teamscale/client/TeamscaleServerTest.kt new file mode 100644 index 000000000..7f6576d77 --- /dev/null +++ b/teamscale-client/src/test/kotlin/com/teamscale/client/TeamscaleServerTest.kt @@ -0,0 +1,21 @@ +package com.teamscale.client + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +internal class TeamscaleServerTest { + @Test + fun testDefaultMessage() { + val server = TeamscaleServer() + server.partition = "Unit Test" + server.revision = "rev123" + + val message = server.message + val normalizedMessage = message!!.replace("uploaded at .*".toRegex(), "uploaded at DATE") + .replace("hostname: .*".toRegex(), "hostname: HOST") + Assertions.assertEquals( + "Unit Test coverage uploaded at DATE\n\nuploaded from hostname: HOST\nfor revision: rev123", + normalizedMessage + ) + } +} \ No newline at end of file diff --git a/teamscale-client/src/test/kotlin/com/teamscale/client/TeamscaleServiceGeneratorProxyServerTest.kt b/teamscale-client/src/test/kotlin/com/teamscale/client/TeamscaleServiceGeneratorProxyServerTest.kt new file mode 100644 index 000000000..2e91476a6 --- /dev/null +++ b/teamscale-client/src/test/kotlin/com/teamscale/client/TeamscaleServiceGeneratorProxyServerTest.kt @@ -0,0 +1,94 @@ +package com.teamscale.client + +import com.teamscale.client.HttpUtils.PROXY_AUTHORIZATION_HTTP_HEADER +import com.teamscale.client.TeamscaleServiceGenerator.createService +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.io.IOException +import java.nio.charset.StandardCharsets +import java.util.* + +/** + * Tests that our Retrofit + OkHttp client is using the Java proxy system properties (`http.proxy*`) if set + */ +internal class TeamscaleServiceGeneratorProxyServerTest { + private var mockProxyServer: MockWebServer? = null + private val proxySystemProperties = ProxySystemProperties( + ProxySystemProperties.Protocol.HTTP + ) + private val teamscaleProxySystemProperties = TeamscaleProxySystemProperties( + ProxySystemProperties.Protocol.HTTP + ) + + @BeforeEach + @Throws(IOException::class) + fun setUp() { + mockProxyServer = MockWebServer() + mockProxyServer?.start() + } + + @Test + @Throws(Exception::class) + fun testTeamscaleProxyAuthentication() { + val incorrectValue = "incorrect" + // the teamscale-specific options should take precedence over the global ones + proxySystemProperties.proxyHost = incorrectValue + proxySystemProperties.proxyPort = 1 + proxySystemProperties.proxyUser = incorrectValue + proxySystemProperties.proxyPassword = incorrectValue + + teamscaleProxySystemProperties.proxyHost = mockProxyServer?.hostName + teamscaleProxySystemProperties.proxyPort = mockProxyServer?.port ?: 1 + + val proxyUser = "myProxyUser" + val proxyPassword = "myProxyPassword" + val base64EncodedBasicAuth = Base64.getEncoder().encodeToString( + ("$proxyUser:$proxyPassword").toByteArray(StandardCharsets.UTF_8) + ) + teamscaleProxySystemProperties.proxyUser = proxyUser + teamscaleProxySystemProperties.proxyPassword = proxyPassword + + assertProxyAuthenticationIsUsed(base64EncodedBasicAuth) + } + + @Throws(InterruptedException::class, IOException::class) + private fun assertProxyAuthenticationIsUsed(base64EncodedBasicAuth: String) { + val service = createService( + ITeamscaleService::class.java, + "http://localhost:1337".toHttpUrl(), + "someUser", "someAccesstoken" + ) + + // First time Retrofit/OkHttp tires without proxy auth. + // When we return 407 Proxy Authentication Required, it retries with proxy authentication. + mockProxyServer?.enqueue(MockResponse().setResponseCode(407)) + mockProxyServer?.enqueue(MockResponse().setResponseCode(200)) + service.sendHeartbeat( + "", + ProfilerInfo(ProcessInformation("", "", 0), null) + ).execute() + + Assertions.assertThat(mockProxyServer?.requestCount).isEqualTo(2) + + mockProxyServer?.takeRequest() // First request which doesn't have the proxy authentication set yet + val requestWithProxyAuth = mockProxyServer?.takeRequest() // Request we are actually interested in + + Assertions.assertThat(requestWithProxyAuth?.getHeader(PROXY_AUTHORIZATION_HTTP_HEADER)) + .isEqualTo("Basic $base64EncodedBasicAuth") + } + + @AfterEach + @Throws(IOException::class) + fun tearDown() { + proxySystemProperties.clear() + teamscaleProxySystemProperties.clear() + + mockProxyServer?.shutdown() + mockProxyServer?.close() + } +} \ No newline at end of file diff --git a/teamscale-client/src/test/kotlin/com/teamscale/client/TestDataTest.kt b/teamscale-client/src/test/kotlin/com/teamscale/client/TestDataTest.kt new file mode 100644 index 000000000..6b39c2f6e --- /dev/null +++ b/teamscale-client/src/test/kotlin/com/teamscale/client/TestDataTest.kt @@ -0,0 +1,10 @@ +package com.teamscale.client + +import org.junit.jupiter.api.Test + +internal class TestDataTest { + @Test + fun ensureHashingDoesNotThrowException() { + TestData.Builder().addByteArray(byteArrayOf(1, 2, 3)).addString("string").build() + } +} \ No newline at end of file diff --git a/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/TeamscaleUploadTask.kt b/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/TeamscaleUploadTask.kt index 3ca610e42..a2750669e 100755 --- a/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/TeamscaleUploadTask.kt +++ b/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/TeamscaleUploadTask.kt @@ -105,7 +105,7 @@ abstract class TeamscaleUploadTask : DefaultTask() { val commitDescriptorOrNull = if (revision != null) null else commitDescriptor!! retry(3) { val client = - TeamscaleClient(server.url, server.userName, server.userAccessToken, server.project) + TeamscaleClient(server.url!!, server.userName!!, server.userAccessToken!!, server.project!!) client.uploadReports( format, reportFiles, diff --git a/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/config/Commit.kt b/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/config/Commit.kt index 283340366..8de6d36e1 100644 --- a/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/config/Commit.kt +++ b/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/config/Commit.kt @@ -16,7 +16,7 @@ class Commit : Serializable { * Use [getOrResolveCommitDescriptor] to get a revision or branch and timestamp. * It falls back to retrieving the values from the git repository, if not given manually. */ - var branchName: String? = null + private var branchName: String? = null set(value) { field = value?.trim() } @@ -27,7 +27,7 @@ class Commit : Serializable { * Use [getOrResolveCommitDescriptor] to get a revision or branch and timestamp. * It falls back to retrieving the values from the git repository, if not given manually. */ - var timestamp: String? = null + private var timestamp: String? = null set(value) { field = value?.trim() } @@ -39,7 +39,7 @@ class Commit : Serializable { * Use [getOrResolveCommitDescriptor] to get a revision or branch and timestamp. * It falls back to retrieving the values from the git repository, if not given manually. */ - var revision: String? = null + private var revision: String? = null set(value) { field = value?.trim() } @@ -56,20 +56,20 @@ class Commit : Serializable { fun getOrResolveCommitDescriptor(project: Project): Pair { try { // If timestamp and branch are set manually, prefer to use them - if (branchName != null && timestamp != null) { - return Pair(CommitDescriptor(branchName, timestamp), null) - } + branchName?.let { branch -> timestamp?.let { time -> + return CommitDescriptor(branch, time) to null + }} // If revision is set manually, use as 2nd option - if (revision != null) { - return Pair(null, revision) - } + revision?.let { rev -> + return null to rev + } // Otherwise fall back to getting the information from the git repository if (resolvedRevision == null && resolvedCommit == null) { val (commit, ref) = GitRepositoryHelper.getHeadCommitDescriptor(project.rootDir) resolvedRevision = ref resolvedCommit = commit } - return Pair(resolvedCommit, resolvedRevision) + return resolvedCommit to resolvedRevision } catch (e: IOException) { throw GradleException("Could not determine Teamscale upload commit", e) } diff --git a/teamscale-gradle-plugin/src/test/kotlin/com/teamscale/TeamscalePluginTest.kt b/teamscale-gradle-plugin/src/test/kotlin/com/teamscale/TeamscalePluginTest.kt index 7f586fd40..d55c7eccb 100644 --- a/teamscale-gradle-plugin/src/test/kotlin/com/teamscale/TeamscalePluginTest.kt +++ b/teamscale-gradle-plugin/src/test/kotlin/com/teamscale/TeamscalePluginTest.kt @@ -38,9 +38,9 @@ class TeamscalePluginTest { @BeforeEach fun startFakeTeamscaleServer() { - teamscaleMockServer = TeamscaleMockServer( - FAKE_TEAMSCALE_PORT - ).acceptingReportUploads().withImpactedTests("com/example/project/JUnit4Test/systemTest") + teamscaleMockServer = TeamscaleMockServer(FAKE_TEAMSCALE_PORT) + .acceptingReportUploads() + .withImpactedTests("com/example/project/JUnit4Test/systemTest") } @AfterEach diff --git a/teamscale-maven-plugin/pom.xml b/teamscale-maven-plugin/pom.xml index 9c2c8bbad..296199d0d 100644 --- a/teamscale-maven-plugin/pom.xml +++ b/teamscale-maven-plugin/pom.xml @@ -53,12 +53,12 @@ 1.8 1.8 - 1.0.0-SNAPSHOT + 34.1.1-SNAPSHOT - 34.0.0 + 34.1.1-SNAPSHOT From 61cb789b7cf43004093b5b69326a0daf72928b9b Mon Sep 17 00:00:00 2001 From: Constructor Date: Tue, 12 Nov 2024 21:38:13 +0100 Subject: [PATCH 2/9] TS-38628 Fix method signatures --- .../com/teamscale/client/AntPatternUtils.kt | 20 +++++------ .../com/teamscale/client/ITeamscaleService.kt | 7 ++-- .../com/teamscale/client/TeamscaleClient.kt | 35 ++++++++++--------- .../client/TeamscaleServiceGenerator.kt | 15 +++----- .../client/ProxySystemPropertiesTest.kt | 1 - .../teamscale/client/TeamscaleServerTest.kt | 6 ++-- .../com/teamscale/TeamscaleUploadTask.kt | 13 ++++--- .../teamscale/config/ServerConfiguration.kt | 28 ++++++--------- .../com/teamscale/TeamscalePluginTest.kt | 2 +- 9 files changed, 57 insertions(+), 70 deletions(-) diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/AntPatternUtils.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/AntPatternUtils.kt index 60b69a6e1..840b2c0a7 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/AntPatternUtils.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/AntPatternUtils.kt @@ -30,19 +30,18 @@ object AntPatternUtils { /** Converts an ANT pattern to a regex pattern. */ @Throws(PatternSyntaxException::class) fun convertPattern(antPattern: String, caseSensitive: Boolean): Pattern { - var antPattern = antPattern - antPattern = normalizePattern(antPattern) + var normalized = normalizePattern(antPattern) // ant specialty: trailing /** is optional // for example **/e*/** will also match foo/entry var addTrailAll = false - if (antPattern.endsWith("/**")) { + if (normalized.endsWith("/**")) { addTrailAll = true - antPattern = StringUtils.stripSuffix(antPattern, "/**") + normalized = StringUtils.stripSuffix(normalized, "/**") } val patternBuilder = StringBuilder() - convertPlainPattern(antPattern, patternBuilder) + convertPlainPattern(normalized, patternBuilder) if (addTrailAll) { // the tail pattern is optional (i.e. we do not require the '/'), @@ -50,7 +49,7 @@ object AntPatternUtils { patternBuilder.append("(/.*)?") } - return compileRegex(patternBuilder.toString(), antPattern, caseSensitive) + return compileRegex(patternBuilder.toString(), normalized, caseSensitive) } /** Compiles the given regex. */ @@ -80,15 +79,14 @@ object AntPatternUtils { * Normalizes the given pattern by ensuring forward slashes and mapping trailing slash to '/ **'. */ private fun normalizePattern(antPattern: String): String { - var antPattern = antPattern - antPattern = FileSystemUtils.normalizeSeparators(antPattern) + var normalized = FileSystemUtils.normalizeSeparators(antPattern) // ant pattern syntax: if a pattern ends with /, then ** is // appended - if (antPattern.endsWith("/")) { - antPattern += "**" + if (normalized.endsWith("/")) { + normalized += "**" } - return antPattern + return normalized } /** diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/ITeamscaleService.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/ITeamscaleService.kt index 7b751458b..b304932e4 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/ITeamscaleService.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/ITeamscaleService.kt @@ -4,7 +4,6 @@ import okhttp3.MultipartBody import okhttp3.RequestBody import okhttp3.ResponseBody import retrofit2.Call -import retrofit2.Response import retrofit2.Retrofit import retrofit2.http.* import java.io.IOException @@ -146,17 +145,17 @@ fun ITeamscaleService.uploadReport( message: String, report: RequestBody ): String { - var commit = commit + var commitNull = commit var moveToLastCommit: Boolean? = false if (revision != null) { // When uploading to a revision, we don't need commit adjustment. - commit = null + commitNull = null moveToLastCommit = null } try { val response = uploadExternalReport( - projectName, reportFormat.name, commit, revision, repository, moveToLastCommit, partition, message, report + projectName, reportFormat.name, commitNull, revision, repository, moveToLastCommit, partition, message, report ).execute() val body = response.body() diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleClient.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleClient.kt index 1f24b0f4a..1def61fb8 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleClient.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleClient.kt @@ -14,7 +14,7 @@ import java.util.* /** Helper class to interact with Teamscale. */ open class TeamscaleClient { /** Teamscale service implementation. */ - val service: ITeamscaleService + var service: ITeamscaleService /** The project ID within Teamscale. */ private val projectId: String @@ -22,14 +22,14 @@ open class TeamscaleClient { /** Constructor with parameters for read and write timeout in seconds. */ @JvmOverloads constructor( - baseUrl: String, + baseUrl: String?, user: String, accessToken: String, projectId: String, readTimeout: Duration = HttpUtils.DEFAULT_READ_TIMEOUT, writeTimeout: Duration = HttpUtils.DEFAULT_WRITE_TIMEOUT ) { - val url = baseUrl.toHttpUrlOrNull() ?: throw IllegalArgumentException("Invalid URL: $baseUrl") + val url = baseUrl?.toHttpUrlOrNull() ?: throw IllegalArgumentException("Invalid URL: $baseUrl") this.projectId = projectId service = TeamscaleServiceGenerator.createService( ITeamscaleService::class.java, url, user, accessToken, readTimeout, writeTimeout @@ -39,7 +39,7 @@ open class TeamscaleClient { /** Constructor with parameters for read and write timeout in seconds and logfile. */ @JvmOverloads constructor( - baseUrl: String, + baseUrl: String?, user: String, accessToken: String, projectId: String, @@ -47,7 +47,7 @@ open class TeamscaleClient { readTimeout: Duration = HttpUtils.DEFAULT_READ_TIMEOUT, writeTimeout: Duration = HttpUtils.DEFAULT_WRITE_TIMEOUT ) { - val url = baseUrl.toHttpUrlOrNull() ?: throw IllegalArgumentException("Invalid URL: $baseUrl") + val url = baseUrl?.toHttpUrlOrNull() ?: throw IllegalArgumentException("Invalid URL: $baseUrl") this.projectId = projectId service = TeamscaleServiceGenerator.createServiceWithRequestLogging( ITeamscaleService::class.java, url, user, accessToken, logfile, readTimeout, writeTimeout @@ -81,16 +81,17 @@ open class TeamscaleClient { @Throws(IOException::class) open fun getImpactedTests( availableTests: List?, - baseline: String, - baselineRevision: String, - endCommit: CommitDescriptor, - endRevision: String, - repository: String, + baseline: String?, + baselineRevision: String?, + endCommit: CommitDescriptor?, + endRevision: String?, + repository: String?, partitions: List, includeNonImpacted: Boolean, - includeAddedTests: Boolean, includeFailedAndSkipped: Boolean + includeAddedTests: Boolean, + includeFailedAndSkipped: Boolean ): Response?> { - val selectedOptions: MutableList = ArrayList(listOf(ETestImpactOptions.ENSURE_PROCESSED)) + val selectedOptions = mutableListOf(ETestImpactOptions.ENSURE_PROCESSED) if (includeNonImpacted) { selectedOptions.add(ETestImpactOptions.INCLUDE_NON_IMPACTED) } @@ -133,11 +134,11 @@ open class TeamscaleClient { @Throws(IOException::class) private fun getImpactedTests( availableTests: List?, - baseline: String, - baselineRevision: String, - endCommit: CommitDescriptor, - endRevision: String, - repository: String, + baseline: String?, + baselineRevision: String?, + endCommit: CommitDescriptor?, + endRevision: String?, + repository: String?, partitions: List, vararg options: ETestImpactOptions ): Response?> { diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServiceGenerator.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServiceGenerator.kt index 92554a862..57db9e256 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServiceGenerator.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServiceGenerator.kt @@ -45,25 +45,20 @@ object TeamscaleServiceGenerator { readTimeout: Duration, writeTimeout: Duration, vararg interceptors: Interceptor - ): S { - val retrofit = HttpUtils.createRetrofit( - { retrofitBuilder: Retrofit.Builder -> + ): S = HttpUtils.createRetrofit( + { retrofitBuilder -> retrofitBuilder.baseUrl(baseUrl) .addConverterFactory(JacksonConverterFactory.create(JsonUtils.OBJECT_MAPPER)) }, - { okHttpBuilder: OkHttpClient.Builder -> + { okHttpBuilder -> addInterceptors(okHttpBuilder, *interceptors) .addInterceptor(HttpUtils.getBasicAuthInterceptor(username, accessToken)) .addInterceptor(AcceptJsonInterceptor()) .addNetworkInterceptor(CustomUserAgentInterceptor()) - if (logfile != null) { - okHttpBuilder.addInterceptor(FileLoggingInterceptor(logfile)) - } + logfile?.let { okHttpBuilder.addInterceptor(FileLoggingInterceptor(it)) } }, readTimeout, writeTimeout - ) - return retrofit.create(serviceClass) - } + ).create(serviceClass) private fun addInterceptors(builder: OkHttpClient.Builder, vararg interceptors: Interceptor): OkHttpClient.Builder { interceptors.forEach { interceptor -> diff --git a/teamscale-client/src/test/kotlin/com/teamscale/client/ProxySystemPropertiesTest.kt b/teamscale-client/src/test/kotlin/com/teamscale/client/ProxySystemPropertiesTest.kt index 9a1111fe5..dac5a70b8 100644 --- a/teamscale-client/src/test/kotlin/com/teamscale/client/ProxySystemPropertiesTest.kt +++ b/teamscale-client/src/test/kotlin/com/teamscale/client/ProxySystemPropertiesTest.kt @@ -1,6 +1,5 @@ package com.teamscale.client -import org.assertj.core.api.Assertions import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.jupiter.api.AfterAll diff --git a/teamscale-client/src/test/kotlin/com/teamscale/client/TeamscaleServerTest.kt b/teamscale-client/src/test/kotlin/com/teamscale/client/TeamscaleServerTest.kt index 7f6576d77..0ac7c680e 100644 --- a/teamscale-client/src/test/kotlin/com/teamscale/client/TeamscaleServerTest.kt +++ b/teamscale-client/src/test/kotlin/com/teamscale/client/TeamscaleServerTest.kt @@ -10,9 +10,9 @@ internal class TeamscaleServerTest { server.partition = "Unit Test" server.revision = "rev123" - val message = server.message - val normalizedMessage = message!!.replace("uploaded at .*".toRegex(), "uploaded at DATE") - .replace("hostname: .*".toRegex(), "hostname: HOST") + val normalizedMessage = server.message + ?.replace("uploaded at .*".toRegex(), "uploaded at DATE") + ?.replace("hostname: .*".toRegex(), "hostname: HOST") Assertions.assertEquals( "Unit Test coverage uploaded at DATE\n\nuploaded from hostname: HOST\nfor revision: rev123", normalizedMessage diff --git a/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/TeamscaleUploadTask.kt b/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/TeamscaleUploadTask.kt index a2750669e..9a610a494 100755 --- a/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/TeamscaleUploadTask.kt +++ b/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/TeamscaleUploadTask.kt @@ -102,11 +102,14 @@ abstract class TeamscaleUploadTask : DefaultTask() { try { // Prefer to upload to revision and fallback to branch timestamp - val commitDescriptorOrNull = if (revision != null) null else commitDescriptor!! + val commitDescriptorOrNull = if (revision != null) null else commitDescriptor retry(3) { - val client = - TeamscaleClient(server.url!!, server.userName!!, server.userAccessToken!!, server.project!!) - client.uploadReports( + TeamscaleClient( + server.url, + server.userName!!, + server.userAccessToken!!, + server.project!! + ).uploadReports( format, reportFiles, commitDescriptorOrNull, @@ -127,7 +130,7 @@ abstract class TeamscaleUploadTask : DefaultTask() { /** * Retries the given block numOfRetries-times catching any thrown exceptions. - * If none of the retries succeeded the latest catched exception is rethrown. + * If none of the retries succeeded, the latest caught exception is rethrown. */ fun retry(numOfRetries: Int, block: () -> T): T { var throwable: Throwable? = null diff --git a/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/config/ServerConfiguration.kt b/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/config/ServerConfiguration.kt index 3f453db87..dfe2df485 100644 --- a/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/config/ServerConfiguration.kt +++ b/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/config/ServerConfiguration.kt @@ -3,24 +3,16 @@ package com.teamscale.config import org.gradle.api.GradleException import java.io.Serializable -class ServerConfiguration : Serializable { - - /** The url of the Teamscale server. */ - var url: String? = null - - /** The project id for which artifacts should be uploaded. */ - var project: String? = null - - /** The user name of the Teamscale user. */ - var userName: String? = null - - /** The access token of the user. */ - var userAccessToken: String? = null - - override fun toString(): String { - return "ServerConfiguration(url=$url, project=$project, userName=$userName, userAccessToken=$userAccessToken)" - } - +data class ServerConfiguration( + /** The url of the Teamscale server. */ + var url: String? = null, + /** The project id for which artifacts should be uploaded. */ + var project: String? = null, + /** The username of the Teamscale user. */ + var userName: String? = null, + /** The access token of the user. */ + var userAccessToken: String? = null +) : Serializable { fun validate() { if (url.isNullOrBlank()) { throw GradleException("Teamscale server url must not be empty!") diff --git a/teamscale-gradle-plugin/src/test/kotlin/com/teamscale/TeamscalePluginTest.kt b/teamscale-gradle-plugin/src/test/kotlin/com/teamscale/TeamscalePluginTest.kt index d55c7eccb..ebc0bfafc 100644 --- a/teamscale-gradle-plugin/src/test/kotlin/com/teamscale/TeamscalePluginTest.kt +++ b/teamscale-gradle-plugin/src/test/kotlin/com/teamscale/TeamscalePluginTest.kt @@ -249,7 +249,7 @@ class TeamscalePluginTest { private fun assertFullCoverage(source: String) { val testwiseCoverageReport = JsonUtils.deserialize(source, TestwiseCoverageReport::class.java) - assertThat(testwiseCoverageReport!!) + assertThat(testwiseCoverageReport) .hasPartial(false) .containsExecutionResult("com/example/project/IgnoredJUnit4Test/systemTest", ETestExecutionResult.SKIPPED) .containsExecutionResult("com/example/project/JUnit4Test/systemTest", ETestExecutionResult.PASSED) From 6266b79af5223ca980e6c02696db03bfe74ec86a Mon Sep 17 00:00:00 2001 From: Constructor Date: Tue, 26 Nov 2024 01:19:11 +0100 Subject: [PATCH 3/9] TS-38628 Resolve cucumber test issue and various refactorings --- .../teamscale/report/jacoco/OpenAnalyzer.java | 2 + .../analysis/CachingInstructionsBuilder.java | 2 + .../com/teamscale/client/AntPatternUtils.kt | 10 +- .../teamscale/client/ClusteredTestDetails.kt | 4 +- .../com/teamscale/client/CommitDescriptor.kt | 23 +--- .../com/teamscale/client/EReportFormat.kt | 2 +- .../client/FileLoggingInterceptor.kt | 32 ++--- .../com/teamscale/client/FileSystemUtils.kt | 30 ----- .../kotlin/com/teamscale/client/HttpUtils.kt | 73 +++++------ .../kotlin/com/teamscale/client/JsonUtils.kt | 29 ++--- .../com/teamscale/client/PrioritizableTest.kt | 11 +- .../client/PrioritizableTestCluster.kt | 13 +- .../com/teamscale/client/StringUtils.kt | 4 +- .../client/TeamscaleServiceGenerator.kt | 4 +- .../kotlin/com/teamscale/client/TestData.kt | 2 +- .../com/teamscale/client/TestWithClusterId.kt | 7 +- .../kotlin/com/teamscale/config/Commit.kt | 117 +++++++++--------- teamscale-maven-plugin/pom.xml | 4 +- 18 files changed, 144 insertions(+), 225 deletions(-) diff --git a/report-generator/src/main/java/com/teamscale/report/jacoco/OpenAnalyzer.java b/report-generator/src/main/java/com/teamscale/report/jacoco/OpenAnalyzer.java index 7e679b05b..b0baf36b9 100644 --- a/report-generator/src/main/java/com/teamscale/report/jacoco/OpenAnalyzer.java +++ b/report-generator/src/main/java/com/teamscale/report/jacoco/OpenAnalyzer.java @@ -54,6 +54,8 @@ * {@link Analyzer} requires a {@link ExecutionDataStore} instance that holds * the execution data for the classes to analyze. The {@link Analyzer} offers * several methods to analyze classes from a variety of sources. + *

+ * CAUTION: Do not convert to Kotlin. This class has to stay in Java for future maintenance reasons! */ public class OpenAnalyzer { diff --git a/report-generator/src/main/java/org/jacoco/core/internal/analysis/CachingInstructionsBuilder.java b/report-generator/src/main/java/org/jacoco/core/internal/analysis/CachingInstructionsBuilder.java index f91e52202..21c8fda4c 100644 --- a/report-generator/src/main/java/org/jacoco/core/internal/analysis/CachingInstructionsBuilder.java +++ b/report-generator/src/main/java/org/jacoco/core/internal/analysis/CachingInstructionsBuilder.java @@ -29,6 +29,8 @@ *

* When updating JaCoCo make a diff of the previous {@link org.jacoco.core.internal.analysis.InstructionsBuilder} * implementation and the new implementation and update this class accordingly. + *

+ * CAUTION: Do not convert to Kotlin. This class has to stay in Java for future maintenance reasons! */ public class CachingInstructionsBuilder extends InstructionsBuilder { diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/AntPatternUtils.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/AntPatternUtils.kt index 840b2c0a7..c38333363 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/AntPatternUtils.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/AntPatternUtils.kt @@ -149,14 +149,12 @@ object AntPatternUtils { * Returns whether the given position exists in the string and equals the given character, and the given character * is either at the end or right before a slash. */ - private fun isCharAtBeforeSlashOrEnd(s: String, position: Int, character: Char): Boolean { - return isCharAt(s, position, character) && (position + 1 == s.length || isCharAt(s, position + 1, '/')) - } + private fun isCharAtBeforeSlashOrEnd(s: String, position: Int, character: Char) = + isCharAt(s, position, character) && (position + 1 == s.length || isCharAt(s, position + 1, '/')) /** * Returns whether the given position exists in the string and equals the given character. */ - private fun isCharAt(s: String, position: Int, character: Char): Boolean { - return position < s.length && s[position] == character - } + private fun isCharAt(s: String, position: Int, character: Char) = + position < s.length && s[position] == character } \ No newline at end of file diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/ClusteredTestDetails.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/ClusteredTestDetails.kt index 3d137227b..e8c3fac0b 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/ClusteredTestDetails.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/ClusteredTestDetails.kt @@ -40,9 +40,7 @@ class ClusteredTestDetails @JsonCreator constructor( fun createWithTestData( uniformPath: String, sourcePath: String, testData: TestData, clusterId: String, partition: String - ): ClusteredTestDetails { - return ClusteredTestDetails(uniformPath, sourcePath, testData.hash, clusterId, partition) - } + ) = ClusteredTestDetails(uniformPath, sourcePath, testData.hash, clusterId, partition) } } diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/CommitDescriptor.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/CommitDescriptor.kt index 3be47156b..d9829883a 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/CommitDescriptor.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/CommitDescriptor.kt @@ -4,8 +4,7 @@ import java.io.Serializable import java.util.* /** Holds the branch and timestamp of a commit. */ -class CommitDescriptor -/** Constructor. */( +data class CommitDescriptor( /** Branch name of the commit. */ @JvmField val branchName: String, /** @@ -18,25 +17,7 @@ class CommitDescriptor constructor(branchName: String, timestamp: Long) : this(branchName, timestamp.toString()) /** Returns a string representation of the commit in a Teamscale REST API compatible format. */ - override fun toString(): String { - return "$branchName:$timestamp" - } - - override fun equals(o: Any?): Boolean { - if (this === o) { - return true - } - if (o == null || javaClass != o.javaClass) { - return false - } - val that = o as CommitDescriptor - return branchName == that.branchName && - timestamp == that.timestamp - } - - override fun hashCode(): Int { - return Objects.hash(branchName, timestamp) - } + override fun toString() = "$branchName:$timestamp" companion object { /** Parses the given commit descriptor string. */ diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/EReportFormat.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/EReportFormat.kt index 3a9ee0d68..f52009a21 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/EReportFormat.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/EReportFormat.kt @@ -158,7 +158,7 @@ enum class EReportFormat( /** * Coverage report generated with the Lauterbach Trace32 tool. See section for * [Supported - * Upload Formats and Samples](https://docs.teamscale.com/reference/upload-formats-and-samples/) in the user guide for more information about + * Upload Formats and Samples](https://docs.teamscale.com/reference/upload-formats-and-samples/) in the user guide for more information about * the Lauterbach Trace32 tool. See the `trace32_example_reports.zip` for * additional report examples. */ diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/FileLoggingInterceptor.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/FileLoggingInterceptor.kt index 0f45fd5d0..9540bfa7c 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/FileLoggingInterceptor.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/FileLoggingInterceptor.kt @@ -4,6 +4,7 @@ import okhttp3.Interceptor import okhttp3.Request import okhttp3.Response import okhttp3.ResponseBody +import okhttp3.ResponseBody.Companion.toResponseBody import okio.Buffer import java.io.File import java.io.FileWriter @@ -13,44 +14,35 @@ import java.io.PrintWriter /** * OkHttpInterceptor which prints out the full request and server response of requests to a file. */ -class FileLoggingInterceptor -/** Constructor. */(private val logfile: File) : Interceptor { +class FileLoggingInterceptor( + private val logfile: File +) : Interceptor { @Throws(IOException::class) override fun intercept(chain: Interceptor.Chain): Response { - val request: Request = chain.request() + val request = chain.request() val requestStartTime = System.nanoTime() PrintWriter(FileWriter(logfile)).use { fileWriter -> fileWriter.write( - String.format( - "--> Sending request %s on %s %s%n%s%n", request.method, request.url, - chain.connection(), - request.headers - ) + "--> Sending request ${request.method} on ${request.url} ${chain.connection()}\n${request.headers}\n" ) val requestBuffer = Buffer() - if (request.body != null) { - request.body?.writeTo(requestBuffer) - } + request.body?.writeTo(requestBuffer) fileWriter.write(requestBuffer.readUtf8()) val response = getResponse(chain, request, fileWriter) - val requestEndTime = System.nanoTime() fileWriter.write( - String.format( - "<-- Received response for %s %s in %.1fms%n%s%n%n", response.code, - response.request.url, (requestEndTime - requestStartTime) / 1e6, response.headers - ) + "<-- Received response for ${response.code} ${response.request.url} in ${(requestEndTime - requestStartTime) / 1e6}ms\n${response.headers}\n\n" ) var wrappedBody: ResponseBody? = null - if (response.body != null) { - val contentType = response.body!!.contentType() - val content = response.body!!.string() + response.body?.let { + val contentType = it.contentType() + val content = it.string() fileWriter.write(content) - wrappedBody = ResponseBody.create(contentType, content) + wrappedBody = content.toResponseBody(contentType) } return response.newBuilder().body(wrappedBody).build() } diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/FileSystemUtils.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/FileSystemUtils.kt index c46985fef..107158588 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/FileSystemUtils.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/FileSystemUtils.kt @@ -7,24 +7,9 @@ import java.nio.charset.StandardCharsets * File system utilities. */ object FileSystemUtils { - /** Encoding for UTF-8. */ - val UTF8_ENCODING: String = StandardCharsets.UTF_8.name() - /** Unix file path separator */ private const val UNIX_SEPARATOR = '/' - /** - * Checks if a directory exists. If not it creates the directory and all necessary parent directories. - * - * @throws IOException if directories couldn't be created. - */ - @Throws(IOException::class) - fun ensureDirectoryExists(directory: File) { - if (!directory.exists() && !directory.mkdirs()) { - throw IOException("Couldn't create directory: $directory") - } - } - /** * Returns a list of all files and directories contained in the given directory and all subdirectories matching the * filter provided. The given directory itself is not included in the result. @@ -130,19 +115,4 @@ object FileSystemUtils { } return size } - - /** - * Returns the name of the given file without extension. Example: - * '/home/joe/data.dat' returns 'data'. - */ - fun getFilenameWithoutExtension(file: File): String { - return getFilenameWithoutExtension(file.name) - } - - /** - * Returns the name of the given file without extension. Example: 'data.dat' returns 'data'. - */ - fun getFilenameWithoutExtension(fileName: String): String { - return StringUtils.removeLastPart(fileName, '.') - } } \ No newline at end of file diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/HttpUtils.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/HttpUtils.kt index e8a1d8e98..e2fe18450 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/HttpUtils.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/HttpUtils.kt @@ -4,9 +4,6 @@ import okhttp3.Authenticator import okhttp3.Credentials.basic import okhttp3.Interceptor import okhttp3.OkHttpClient.Builder -import okhttp3.Response -import okhttp3.Route -import org.slf4j.Logger import org.slf4j.LoggerFactory import retrofit2.Retrofit import java.io.IOException @@ -24,24 +21,24 @@ import javax.net.ssl.* * Utility functions to set up [Retrofit] and [OkHttpClient]. */ object HttpUtils { - private val LOGGER: Logger = LoggerFactory.getLogger(HttpUtils::class.java) + private val LOGGER = LoggerFactory.getLogger(HttpUtils::class.java) /** * Default read timeout in seconds. */ @JvmField - val DEFAULT_READ_TIMEOUT: Duration = Duration.ofSeconds(60) + val DEFAULT_READ_TIMEOUT = Duration.ofSeconds(60) /** * Default write timeout in seconds. */ @JvmField - val DEFAULT_WRITE_TIMEOUT: Duration = Duration.ofSeconds(60) + val DEFAULT_WRITE_TIMEOUT = Duration.ofSeconds(60) /** * HTTP header used for authenticating against a proxy server */ - const val PROXY_AUTHORIZATION_HTTP_HEADER: String = "Proxy-Authorization" + const val PROXY_AUTHORIZATION_HTTP_HEADER = "Proxy-Authorization" /** Controls whether [OkHttpClient]s built with this class will validate SSL certificates. */ private var shouldValidateSsl = true @@ -68,10 +65,11 @@ object HttpUtils { okHttpBuilderAction: Consumer, readTimeout: Duration = DEFAULT_READ_TIMEOUT, writeTimeout: Duration = DEFAULT_WRITE_TIMEOUT ): Retrofit { - val httpClientBuilder = Builder() - setTimeouts(httpClientBuilder, readTimeout, writeTimeout) - setUpSslValidation(httpClientBuilder) - setUpProxyServer(httpClientBuilder) + val httpClientBuilder = Builder().apply { + setTimeouts(readTimeout, writeTimeout) + setUpSslValidation() + setUpProxyServer() + } okHttpBuilderAction.accept(httpClientBuilder) val builder = Retrofit.Builder().client(httpClientBuilder.build()) @@ -88,13 +86,13 @@ object HttpUtils { * & * [https://stackoverflow.com/a/35567936](https://stackoverflow.com/a/35567936) */ - private fun setUpProxyServer(httpClientBuilder: Builder) { + private fun Builder.setUpProxyServer() { val setHttpsProxyWasSuccessful = setUpProxyServerForProtocol( ProxySystemProperties.Protocol.HTTPS, - httpClientBuilder + this ) if (!setHttpsProxyWasSuccessful) { - setUpProxyServerForProtocol(ProxySystemProperties.Protocol.HTTP, httpClientBuilder) + setUpProxyServerForProtocol(ProxySystemProperties.Protocol.HTTP, this) } } @@ -102,27 +100,25 @@ object HttpUtils { protocol: ProxySystemProperties.Protocol, httpClientBuilder: Builder ): Boolean { - val teamscaleProxySystemProperties = TeamscaleProxySystemProperties(protocol) + val proxySystemProperties = TeamscaleProxySystemProperties(protocol) try { - if (!teamscaleProxySystemProperties.isProxyServerSet()) { + if (!proxySystemProperties.isProxyServerSet()) { return false } useProxyServer( - httpClientBuilder, teamscaleProxySystemProperties.proxyHost!!, - teamscaleProxySystemProperties.proxyPort + httpClientBuilder, proxySystemProperties.proxyHost!!, + proxySystemProperties.proxyPort ) } catch (e: ProxySystemProperties.IncorrectPortFormatException) { LOGGER.warn(e.message) return false } - if (teamscaleProxySystemProperties.isProxyAuthSet()) { - useProxyAuthenticator( - httpClientBuilder, - teamscaleProxySystemProperties.proxyUser!!, - teamscaleProxySystemProperties.proxyPassword!! - ) + if (proxySystemProperties.isProxyAuthSet()) { + val user = proxySystemProperties.proxyUser ?: return false + val password = proxySystemProperties.proxyPassword ?: return false + useProxyAuthenticator(httpClientBuilder, user, password) } return true @@ -133,7 +129,7 @@ object HttpUtils { } private fun useProxyAuthenticator(httpClientBuilder: Builder, user: String, password: String) { - val proxyAuthenticator = Authenticator { route: Route?, response: Response -> + val proxyAuthenticator = Authenticator { _, response -> val credential = basic(user, password) response.request.newBuilder() .header(PROXY_AUTHORIZATION_HTTP_HEADER, credential) @@ -146,16 +142,16 @@ object HttpUtils { /** * Sets sensible defaults for the [OkHttpClient]. */ - private fun setTimeouts(builder: Builder, readTimeout: Duration, writeTimeout: Duration) { - builder.connectTimeout(Duration.ofSeconds(60)) - builder.readTimeout(readTimeout) - builder.writeTimeout(writeTimeout) + private fun Builder.setTimeouts(readTimeout: Duration, writeTimeout: Duration) { + connectTimeout(Duration.ofSeconds(60)) + readTimeout(readTimeout) + writeTimeout(writeTimeout) } /** * Enables or disables SSL certificate validation for the [Retrofit] instance */ - private fun setUpSslValidation(builder: Builder) { + private fun Builder.setUpSslValidation() { if (shouldValidateSsl) { // this is the default behaviour of OkHttp, so we don't need to do anything return @@ -164,7 +160,7 @@ object HttpUtils { val sslSocketFactory: SSLSocketFactory try { val sslContext = SSLContext.getInstance("TLS") - sslContext.init(null, arrayOf(TrustAllCertificatesManager.INSTANCE), SecureRandom()) + sslContext.init(null, arrayOf(TrustAllCertificatesManager), SecureRandom()) sslSocketFactory = sslContext.socketFactory } catch (e: GeneralSecurityException) { LOGGER.error("Could not disable SSL certificate validation. Leaving it enabled", e) @@ -172,9 +168,9 @@ object HttpUtils { } // this causes OkHttp to accept all certificates - builder.sslSocketFactory(sslSocketFactory, TrustAllCertificatesManager.INSTANCE) + sslSocketFactory(sslSocketFactory, TrustAllCertificatesManager) // this causes it to ignore invalid host names in the certificates - builder.hostnameVerifier(HostnameVerifier { hostName: String?, session: SSLSession? -> true }) + hostnameVerifier { _, _ -> true } } /** @@ -204,11 +200,9 @@ object HttpUtils { /** * A simple implementation of [X509TrustManager] that simple trusts every certificate. */ - class TrustAllCertificatesManager : X509TrustManager { + object TrustAllCertificatesManager : X509TrustManager { /** Returns `null`. */ - override fun getAcceptedIssuers(): Array { - return arrayOf() - } + override fun getAcceptedIssuers() = arrayOf() /** Does nothing. */ override fun checkServerTrusted(certs: Array, authType: String) { @@ -219,10 +213,5 @@ object HttpUtils { override fun checkClientTrusted(certs: Array, authType: String) { // Nothing to do } - - companion object { - /** Singleton instance. */ /*package*/ - val INSTANCE: TrustAllCertificatesManager = TrustAllCertificatesManager() - } } } diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/JsonUtils.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/JsonUtils.kt index 305205c56..67a269f4e 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/JsonUtils.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/JsonUtils.kt @@ -30,53 +30,46 @@ object JsonUtils { /** * Creates a new instance of [JsonFactory] using the default [ObjectMapper]. */ - fun createFactory(): JsonFactory { - return JsonFactory(OBJECT_MAPPER) - } + fun createFactory() = JsonFactory(OBJECT_MAPPER) /** * Deserializes a JSON string into an object of the given class. */ @Throws(JsonProcessingException::class) @JvmStatic - fun deserialize(json: String?, clazz: Class?): T { - return OBJECT_MAPPER.readValue(json, clazz) - } + fun deserialize(json: String, clazz: Class): T = + OBJECT_MAPPER.readValue(json, clazz) /** * Deserializes the contents of the given file into an object of the given class. */ @Throws(IOException::class) - fun deserializeFile(file: File?, clazz: Class?): T { - return OBJECT_MAPPER.readValue(file, clazz) - } + fun deserializeFile(file: File, clazz: Class): T = + OBJECT_MAPPER.readValue(file, clazz) /** * Deserializes a JSON string into a list of objects of the given class. */ @Throws(JsonProcessingException::class) @JvmStatic - fun deserializeList(json: String?, elementClass: Class?): List { - return OBJECT_MAPPER.readValue( - json, - OBJECT_MAPPER.typeFactory.constructCollectionLikeType(MutableList::class.java, elementClass) + fun deserializeList(json: String, elementClass: Class): List = + OBJECT_MAPPER.readValue( + json, OBJECT_MAPPER.typeFactory.constructCollectionLikeType(MutableList::class.java, elementClass) ) - } /** * Serializes an object into its JSON representation. */ @JvmStatic @Throws(JsonProcessingException::class) - fun serialize(value: Any?): String { - return OBJECT_MAPPER.writeValueAsString(value) - } + fun serialize(value: Any): String = + OBJECT_MAPPER.writeValueAsString(value) /** * Serializes an object to a file with pretty printing enabled. */ @Throws(IOException::class) - fun serializeToFile(file: File?, value: T) { + fun serializeToFile(file: File, value: T) { OBJECT_MAPPER.writer().withDefaultPrettyPrinter().writeValue(file, value) } } diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/PrioritizableTest.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/PrioritizableTest.kt index 1c2a98937..02438878e 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/PrioritizableTest.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/PrioritizableTest.kt @@ -8,7 +8,7 @@ import java.util.* * [TestDetails] with information about their partition as well as tracking data used during prioritization of * tests. Two instances are considered equal if the test details are equals. */ -class PrioritizableTest @JsonCreator constructor( +data class PrioritizableTest @JsonCreator constructor( /** The uniform path without the "-test-execution" or "-execution-unit-" prefix */ @JvmField @param:JsonProperty("testName") var testName: String ) { @@ -33,15 +33,15 @@ class PrioritizableTest @JsonCreator constructor( * compared to other scores of the same request. It makes no sense to compare the score against absolute values. */ @JsonProperty("currentScore") - var score: Double = 0.0 + var score = 0.0 /** * Field for storing the tests rank. The rank is the 1-based index of the test in the prioritized list. */ - var rank: Int = 0 + var rank = 0 - override fun toString(): String { - return StringJoiner(", ", PrioritizableTest::class.java.simpleName + "[", "]") + override fun toString() = + StringJoiner(", ", PrioritizableTest::class.java.simpleName + "[", "]") .add("testName='$testName'") .add("uniformPath='$uniformPath'") .add("selectionReason='$selectionReason'") @@ -50,5 +50,4 @@ class PrioritizableTest @JsonCreator constructor( .add("score=$score") .add("rank=$rank") .toString() - } } diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/PrioritizableTestCluster.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/PrioritizableTestCluster.kt index 6b0b92cea..102fd0de0 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/PrioritizableTestCluster.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/PrioritizableTestCluster.kt @@ -22,7 +22,7 @@ class PrioritizableTestCluster @JsonCreator constructor( */ @param:JsonProperty("clusterId") var clusterId: String, /** The [PrioritizableTest]s in this cluster. */ - @JvmField @param:JsonProperty("tests") var tests: List? + @JvmField @param:JsonProperty("tests") var tests: List? ) { /** * The score determined by the TIA algorithm. The value is guaranteed to be positive. Higher values describe a @@ -31,7 +31,7 @@ class PrioritizableTestCluster @JsonCreator constructor( * The value is 0 if no availableTests are given. */ @JsonProperty("currentScore") - var score: Double = 0.0 + var score = 0.0 /** * Field for storing the tests rank. The rank is the 1-based index of the test @@ -39,15 +39,10 @@ class PrioritizableTestCluster @JsonCreator constructor( */ var rank: Int = 0 - override fun toString(): String { - return StringJoiner( - ", ", - PrioritizableTestCluster::class.java.simpleName + "[", "]" - ) - .add("clusterId='$clusterId'") + override fun toString() = + StringJoiner(", ", PrioritizableTestCluster::class.java.simpleName + "[", "]").add("clusterId='$clusterId'") .add("score=$score") .add("rank=$rank") .add("tests=$tests") .toString() - } } diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/StringUtils.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/StringUtils.kt index 83b292720..f4955989d 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/StringUtils.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/StringUtils.kt @@ -24,10 +24,10 @@ import kotlin.math.min */ object StringUtils { /** Line separator of the current platform. */ - val LINE_SEPARATOR: String = System.getProperty("line.separator") + private val LINE_SEPARATOR: String = System.lineSeparator() /** The empty string. */ - const val EMPTY_STRING: String = "" + private const val EMPTY_STRING: String = "" /** * Checks if a string is empty (after trimming). diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServiceGenerator.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServiceGenerator.kt index 57db9e256..6bc8bdaab 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServiceGenerator.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServiceGenerator.kt @@ -10,7 +10,7 @@ import java.time.Duration /** Helper class for generating a teamscale compatible service. */ object TeamscaleServiceGenerator { /** Custom user agent of the requests, used to monitor API traffic. */ - const val USER_AGENT: String = "Teamscale JaCoCo Agent" + const val USER_AGENT = "Teamscale JaCoCo Agent" /** * Generates a [Retrofit] instance for the given service, which uses basic auth to authenticate against the @@ -18,7 +18,6 @@ object TeamscaleServiceGenerator { */ @JvmStatic @JvmOverloads - // ToDo: Should use reified type parameter when all usages are in Kotlin fun createService( serviceClass: Class, baseUrl: HttpUrl, @@ -35,7 +34,6 @@ object TeamscaleServiceGenerator { * Generates a [Retrofit] instance for the given service, which uses basic auth to authenticate against the * server and which sets the accept-header to json. Logs requests and responses to the given logfile. */ - // ToDo: Should use reified type parameter when all usages are in Kotlin fun createServiceWithRequestLogging( serviceClass: Class, baseUrl: HttpUrl, diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/TestData.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/TestData.kt index 4c8058666..8e6d0a203 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/TestData.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/TestData.kt @@ -24,7 +24,7 @@ class TestData private constructor( * result in [OutOfMemoryError]s. */ class Builder { - private var digest: MessageDigest? = DigestUtils.getSha1Digest() + private var digest = DigestUtils.getSha1Digest() /** Adds the given bytes as additional test data. */ @Synchronized diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/TestWithClusterId.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/TestWithClusterId.kt index d280848d5..bf217be7f 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/TestWithClusterId.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/TestWithClusterId.kt @@ -10,7 +10,7 @@ class TestWithClusterId @JsonCreator constructor( /** * The uniform path of the test (unescaped and without -test-execution- prefix). */ - @param:JsonProperty("testName") val testName: String?, + @param:JsonProperty("testName") val testName: String, /** * The hashed content of the test. */ @@ -28,11 +28,10 @@ class TestWithClusterId @JsonCreator constructor( /** * Creates a #TestWithClusterId from a #ClusteredTestDetails object. */ - fun fromClusteredTestDetails(clusteredTestDetails: ClusteredTestDetails): TestWithClusterId { - return TestWithClusterId( + fun fromClusteredTestDetails(clusteredTestDetails: ClusteredTestDetails) = + TestWithClusterId( clusteredTestDetails.uniformPath, clusteredTestDetails.content, clusteredTestDetails.partition, clusteredTestDetails.clusterId ) - } } } diff --git a/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/config/Commit.kt b/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/config/Commit.kt index 8de6d36e1..29d1d30a7 100644 --- a/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/config/Commit.kt +++ b/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/config/Commit.kt @@ -10,68 +10,71 @@ import java.io.Serializable /** The commit object which holds the end commit for which we do Test Impact Analysis. */ class Commit : Serializable { - /** - * The branch to which the artifacts belong to. - * This field encapsulates the value set in the gradle config. - * Use [getOrResolveCommitDescriptor] to get a revision or branch and timestamp. - * It falls back to retrieving the values from the git repository, if not given manually. - */ - private var branchName: String? = null - set(value) { - field = value?.trim() - } + /** + * The branch to which the artifacts belong to. + * This field encapsulates the value set in the gradle config. + * Use [getOrResolveCommitDescriptor] to get a revision or branch and timestamp. + * It falls back to retrieving the values from the git repository, if not given manually. + */ + var branchName: String? = null + set(value) { + field = value?.trim() + } - /** - * The timestamp of the commit that has been used to generate the artifacts. - * This field encapsulates the value set in the gradle config. - * Use [getOrResolveCommitDescriptor] to get a revision or branch and timestamp. - * It falls back to retrieving the values from the git repository, if not given manually. - */ - private var timestamp: String? = null - set(value) { - field = value?.trim() - } + /** + * The timestamp of the commit that has been used to generate the artifacts. + * This field encapsulates the value set in the gradle config. + * Use [getOrResolveCommitDescriptor] to get a revision or branch and timestamp. + * It falls back to retrieving the values from the git repository, if not given manually. + */ + var timestamp: String? = null + set(value) { + field = value?.trim() + } - /** - * The revision of the commit that the artifacts should be uploaded to. - * This is e.g. the SHA1 hash of the commit in Git or the revision of the commit in SVN. - * This field encapsulates the value set in the gradle config. - * Use [getOrResolveCommitDescriptor] to get a revision or branch and timestamp. - * It falls back to retrieving the values from the git repository, if not given manually. - */ - private var revision: String? = null - set(value) { - field = value?.trim() - } + /** + * The revision of the commit that the artifacts should be uploaded to. + * This is e.g. the SHA1 hash of the commit in Git or the revision of the commit in SVN. + * This field encapsulates the value set in the gradle config. + * Use [getOrResolveCommitDescriptor] to get a revision or branch and timestamp. + * It falls back to retrieving the values from the git repository, if not given manually. + */ + var revision: String? = null + set(value) { + field = value?.trim() + } - /** Read automatically in [getOrResolveCommitDescriptor] if [revision] is not set */ - private var resolvedRevision: String? = null - /** Read automatically in [getOrResolveCommitDescriptor] if [branchName] and [timestamp] are not set */ - private var resolvedCommit: CommitDescriptor? = null + /** Read automatically in [getOrResolveCommitDescriptor] if [revision] is not set */ + private var resolvedRevision: String? = null - /** - * Checks that a branch name and timestamp are set or can be retrieved from the projects git and - * stores them for later use. - */ - fun getOrResolveCommitDescriptor(project: Project): Pair { - try { - // If timestamp and branch are set manually, prefer to use them - branchName?.let { branch -> timestamp?.let { time -> - return CommitDescriptor(branch, time) to null - }} - // If revision is set manually, use as 2nd option + /** Read automatically in [getOrResolveCommitDescriptor] if [branchName] and [timestamp] are not set */ + private var resolvedCommit: CommitDescriptor? = null + + /** + * Checks that a branch name and timestamp are set or can be retrieved from the projects git and + * stores them for later use. + */ + fun getOrResolveCommitDescriptor(project: Project): Pair { + try { + // If timestamp and branch are set manually, prefer to use them + branchName?.let { branch -> + timestamp?.let { time -> + return CommitDescriptor(branch, time) to null + } + } + // If revision is set manually, use as 2nd option revision?.let { rev -> return null to rev } - // Otherwise fall back to getting the information from the git repository - if (resolvedRevision == null && resolvedCommit == null) { - val (commit, ref) = GitRepositoryHelper.getHeadCommitDescriptor(project.rootDir) - resolvedRevision = ref - resolvedCommit = commit - } - return resolvedCommit to resolvedRevision - } catch (e: IOException) { - throw GradleException("Could not determine Teamscale upload commit", e) - } - } + // Otherwise fall back to getting the information from the git repository + if (resolvedRevision == null && resolvedCommit == null) { + val (commit, ref) = GitRepositoryHelper.getHeadCommitDescriptor(project.rootDir) + resolvedRevision = ref + resolvedCommit = commit + } + return resolvedCommit to resolvedRevision + } catch (e: IOException) { + throw GradleException("Could not determine Teamscale upload commit", e) + } + } } diff --git a/teamscale-maven-plugin/pom.xml b/teamscale-maven-plugin/pom.xml index 296199d0d..9c2c8bbad 100644 --- a/teamscale-maven-plugin/pom.xml +++ b/teamscale-maven-plugin/pom.xml @@ -53,12 +53,12 @@ 1.8 1.8 - 34.1.1-SNAPSHOT + 1.0.0-SNAPSHOT - 34.1.1-SNAPSHOT + 34.0.0 From b0b95dd34393af3e20722834568995d65bfe64be Mon Sep 17 00:00:00 2001 From: Constructor Date: Tue, 26 Nov 2024 02:05:56 +0100 Subject: [PATCH 4/9] TS-38628 Refactor --- .../com/teamscale/report/ReportUtils.kt | 13 +++++----- .../report/jacoco/JaCoCoXmlReportGenerator.kt | 3 +-- .../report/jacoco/TeamscaleCoverageBuilder.kt | 16 +++++++----- .../testwise/TestwiseCoverageReportWriter.kt | 2 -- .../jacoco/CachingExecutionDataReader.kt | 12 ++++++--- .../jacoco/JaCoCoTestwiseReportGenerator.kt | 4 ++- .../testwise/jacoco/cache/AnalyzerCache.kt | 2 -- .../jacoco/cache/ClassCoverageLookup.kt | 2 ++ .../builder/TestwiseCoverageReportBuilder.kt | 2 -- .../report/util/AntPatternIncludeFilter.kt | 1 - .../util/BashFileSkippingInputStream.kt | 2 +- .../client/FileLoggingInterceptor.kt | 2 +- .../com/teamscale/client/FileSystemUtils.kt | 7 +++-- .../kotlin/com/teamscale/client/HttpUtils.kt | 26 ++++++++----------- .../client/PrioritizableTestCluster.kt | 3 ++- 15 files changed, 47 insertions(+), 50 deletions(-) diff --git a/report-generator/src/main/kotlin/com/teamscale/report/ReportUtils.kt b/report-generator/src/main/kotlin/com/teamscale/report/ReportUtils.kt index 7e1ebc962..808e949a4 100644 --- a/report-generator/src/main/kotlin/com/teamscale/report/ReportUtils.kt +++ b/report-generator/src/main/kotlin/com/teamscale/report/ReportUtils.kt @@ -8,7 +8,6 @@ import com.teamscale.report.testwise.ETestArtifactFormat import com.teamscale.report.testwise.model.TestExecution import com.teamscale.report.testwise.model.TestwiseCoverageReport import java.io.File -import java.io.FileFilter import java.io.IOException import java.util.* @@ -39,7 +38,7 @@ object ReportUtils { @Throws(JsonProcessingException::class) fun getTestwiseCoverageReportAsString( report: TestwiseCoverageReport - ): String = JsonUtils.serialize(report) + ) = JsonUtils.serialize(report) /** Writes the report object to the given file as json. */ @Throws(IOException::class) @@ -59,7 +58,7 @@ object ReportUtils { clazz: Class>, directoriesOrFiles: List ) = listFiles(format, directoriesOrFiles) - .mapNotNull { JsonUtils.deserializeFile(it, clazz) } + .map { JsonUtils.deserializeFile(it, clazz) } .flatMap { listOf(*it) } /** Recursively lists all files of the given artifact type. */ @@ -70,14 +69,14 @@ object ReportUtils { ) = directoriesOrFiles.flatMap { directoryOrFile -> when { directoryOrFile.isDirectory() -> { - FileSystemUtils.listFilesRecursively(directoryOrFile) { - it.isOfArtifactFormat(format) - } + directoryOrFile.walkTopDown().filter { it.isOfArtifactFormat(format) }.toList() } + directoryOrFile.isOfArtifactFormat(format) -> { listOf(directoryOrFile) } - else -> emptyList() + + else -> emptyList() } } diff --git a/report-generator/src/main/kotlin/com/teamscale/report/jacoco/JaCoCoXmlReportGenerator.kt b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/JaCoCoXmlReportGenerator.kt index b8da73938..ea3406698 100644 --- a/report-generator/src/main/kotlin/com/teamscale/report/jacoco/JaCoCoXmlReportGenerator.kt +++ b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/JaCoCoXmlReportGenerator.kt @@ -47,8 +47,7 @@ class JaCoCoXmlReportGenerator( analyzeStructureAndAnnotateCoverage(mergedStore).apply { checkForEmptyReport() coverageFile.outputStream.use { outputStream -> - createReport( - outputStream, this, dump.info, mergedStore) + createReport(outputStream, this, dump.info, mergedStore) } } } diff --git a/report-generator/src/main/kotlin/com/teamscale/report/jacoco/TeamscaleCoverageBuilder.kt b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/TeamscaleCoverageBuilder.kt index d049df8dd..2f7c8304b 100644 --- a/report-generator/src/main/kotlin/com/teamscale/report/jacoco/TeamscaleCoverageBuilder.kt +++ b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/TeamscaleCoverageBuilder.kt @@ -45,13 +45,15 @@ internal class TeamscaleCoverageBuilder( // we do not log the exception here as it does not provide additional valuable information // and may confuse users into thinking there's a serious // problem with the agent due to the stack traces in the log - logger.warn("Ignoring duplicate, non-identical class file for class ${coverage.name} compiled " + - "from source file ${coverage.sourceFileName}. This happens when a class with the same " + - "fully-qualified name is loaded twice but the two loaded class files are not identical. " + - "A common reason for this is that the same library or shared code is included twice in " + - "your application but in two different versions. The produced coverage for this class " + - "may not be accurate or may even be unusable. To fix this problem, please resolve the " + - "conflict between both class files in your application.") + logger.warn( + "Ignoring duplicate, non-identical class file for class ${coverage.name} compiled " + + "from source file ${coverage.sourceFileName}. This happens when a class with the same " + + "fully-qualified name is loaded twice but the two loaded class files are not identical. " + + "A common reason for this is that the same library or shared code is included twice in " + + "your application but in two different versions. The produced coverage for this class " + + "may not be accurate or may even be unusable. To fix this problem, please resolve the " + + "conflict between both class files in your application." + ) return } diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/TestwiseCoverageReportWriter.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/TestwiseCoverageReportWriter.kt index b7eb6a329..181e0b668 100644 --- a/report-generator/src/main/kotlin/com/teamscale/report/testwise/TestwiseCoverageReportWriter.kt +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/TestwiseCoverageReportWriter.kt @@ -3,13 +3,11 @@ package com.teamscale.report.testwise import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.util.DefaultPrettyPrinter import com.teamscale.client.JsonUtils -import com.teamscale.client.StringUtils import com.teamscale.report.testwise.model.TestInfo import com.teamscale.report.testwise.model.builder.TestCoverageBuilder import com.teamscale.report.testwise.model.factory.TestInfoFactory import java.io.File import java.io.IOException -import java.io.OutputStream import java.nio.file.Files import java.util.function.Consumer diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/CachingExecutionDataReader.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/CachingExecutionDataReader.kt index 735ef1b1e..a4609b9d6 100644 --- a/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/CachingExecutionDataReader.kt +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/CachingExecutionDataReader.kt @@ -45,9 +45,13 @@ open class CachingExecutionDataReader( */ private fun analyzeDirectory(classDir: File, analyzer: AnalyzerCache) = runCatching { analyzer.analyzeAll(classDir) } - .onFailure { e -> logger.error("Failed to analyze class files in $classDir! " + - "Maybe the folder contains incompatible class files. Coverage for class files " + - "in this folder will be ignored.", e) } + .onFailure { e -> + logger.error( + "Failed to analyze class files in $classDir! " + + "Maybe the folder contains incompatible class files. Coverage for class files " + + "in this folder will be ignored.", e + ) + } .getOrDefault(0) /** @@ -73,7 +77,7 @@ open class CachingExecutionDataReader( /** * Consumer for processing [Dump] objects and passing them to [TestCoverageBuilder]. - * + * * @param logger The logger to use for logging. * @param locationIncludeFilter The filter to use for including locations. * @param nextConsumer The consumer to pass the generated [TestCoverageBuilder] to. diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/JaCoCoTestwiseReportGenerator.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/JaCoCoTestwiseReportGenerator.kt index bfa8df949..3b6b77844 100644 --- a/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/JaCoCoTestwiseReportGenerator.kt +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/JaCoCoTestwiseReportGenerator.kt @@ -87,7 +87,9 @@ open class JaCoCoTestwiseReportGenerator( } /** Collects execution information per session and passes it to the consumer . */ - private class DumpCallback(private val consumer: DumpConsumer) : IExecutionDataVisitor, ISessionInfoVisitor { + private class DumpCallback( + private val consumer: DumpConsumer + ) : IExecutionDataVisitor, ISessionInfoVisitor { /** The dump that is currently being read. */ private var currentDump: Dump? = null diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/AnalyzerCache.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/AnalyzerCache.kt index 6ad52eee1..bb718aaff 100644 --- a/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/AnalyzerCache.kt +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/AnalyzerCache.kt @@ -9,8 +9,6 @@ import org.jacoco.core.internal.analysis.StringPool import org.jacoco.core.internal.data.CRC64 import org.jacoco.core.internal.flow.ClassProbesAdapter import org.jacoco.core.internal.instr.InstrSupport -import org.objectweb.asm.ClassReader -import org.objectweb.asm.ClassVisitor import java.io.IOException import java.io.InputStream import java.nio.file.Files diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/ClassCoverageLookup.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/ClassCoverageLookup.kt index ac01714f2..034e78548 100644 --- a/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/ClassCoverageLookup.kt +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/ClassCoverageLookup.kt @@ -41,6 +41,7 @@ class ClassCoverageLookup internal constructor( probes.size > executedProbes.size -> throw CoverageGenerationException( "Probe lookup does not match with actual probe size for $sourceFileName $className (${probes.size} vs ${executedProbes.size})! This is a bug in the profiler tooling. Please report it back to CQSE." ) + sourceFileName == null -> { logger.warn("No source file name found for class $className! This class was probably not compiled with debug information enabled!") return null @@ -60,6 +61,7 @@ class ClassCoverageLookup internal constructor( coveredLines.isEmpty() -> logger.debug( "$sourceFileName $className contains a method with no line information. Does the class contain debug information?" ) + else -> fileCoverage.addLines(coveredLines) } } else { diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/TestwiseCoverageReportBuilder.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/TestwiseCoverageReportBuilder.kt index ca73fb3e6..072433ad4 100644 --- a/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/TestwiseCoverageReportBuilder.kt +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/TestwiseCoverageReportBuilder.kt @@ -2,9 +2,7 @@ package com.teamscale.report.testwise.model.builder import com.teamscale.client.TestDetails import com.teamscale.report.testwise.model.TestExecution -import com.teamscale.report.testwise.model.TestInfo import com.teamscale.report.testwise.model.TestwiseCoverageReport -import java.util.function.Function /** Container for coverage produced by multiple tests. */ class TestwiseCoverageReportBuilder { diff --git a/report-generator/src/main/kotlin/com/teamscale/report/util/AntPatternIncludeFilter.kt b/report-generator/src/main/kotlin/com/teamscale/report/util/AntPatternIncludeFilter.kt index 2ba781fd5..0d89e67e8 100644 --- a/report-generator/src/main/kotlin/com/teamscale/report/util/AntPatternIncludeFilter.kt +++ b/report-generator/src/main/kotlin/com/teamscale/report/util/AntPatternIncludeFilter.kt @@ -9,7 +9,6 @@ import com.teamscale.client.AntPatternUtils import com.teamscale.client.FileSystemUtils import java.util.function.Predicate import java.util.regex.Pattern -import java.util.stream.Collectors /** * Applies ANT include and exclude patterns to paths. diff --git a/report-generator/src/main/kotlin/com/teamscale/report/util/BashFileSkippingInputStream.kt b/report-generator/src/main/kotlin/com/teamscale/report/util/BashFileSkippingInputStream.kt index e2f6be389..ba9834456 100644 --- a/report-generator/src/main/kotlin/com/teamscale/report/util/BashFileSkippingInputStream.kt +++ b/report-generator/src/main/kotlin/com/teamscale/report/util/BashFileSkippingInputStream.kt @@ -57,7 +57,7 @@ class BashFileSkippingInputStream(input: InputStream) : FilterInputStream(Buffer * @return The index where the ZIP header starts, or -1 if not found. */ private fun findZipHeader(buffer: ByteArray, length: Int) = - (0 .. length - ZIP_HEADER.size) + (0..length - ZIP_HEADER.size) .firstOrNull { buffer[it] == ZIP_HEADER[0] && buffer[it + 1] == ZIP_HEADER[1] diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/FileLoggingInterceptor.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/FileLoggingInterceptor.kt index 9540bfa7c..27bd7fc02 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/FileLoggingInterceptor.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/FileLoggingInterceptor.kt @@ -12,7 +12,7 @@ import java.io.IOException import java.io.PrintWriter /** - * OkHttpInterceptor which prints out the full request and server response of requests to a file. + * [okhttp3.Interceptor] which prints out the full request and server response of requests to a file. */ class FileLoggingInterceptor( private val logfile: File diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/FileSystemUtils.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/FileSystemUtils.kt index 107158588..3bc6f1cc9 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/FileSystemUtils.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/FileSystemUtils.kt @@ -32,7 +32,7 @@ object FileSystemUtils { if (directory == null || !directory.isDirectory) { return emptyList() } - val result: MutableList = ArrayList() + val result = arrayListOf() listFilesRecursively(directory, result, filter) return result } @@ -88,9 +88,8 @@ object FileSystemUtils { * Replace platform dependent separator char with forward slashes to create system-independent paths. */ @JvmStatic - fun normalizeSeparators(path: String): String { - return path.replace(File.separatorChar, UNIX_SEPARATOR) - } + fun normalizeSeparators(path: String) = + path.replace(File.separatorChar, UNIX_SEPARATOR) /** * Copy an input stream to an output stream. This does *not* close the diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/HttpUtils.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/HttpUtils.kt index e2fe18450..2b27429d3 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/HttpUtils.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/HttpUtils.kt @@ -18,7 +18,7 @@ import java.util.function.Consumer import javax.net.ssl.* /** - * Utility functions to set up [Retrofit] and [OkHttpClient]. + * Utility functions to set up [Retrofit] and [okhttp3.OkHttpClient]. */ object HttpUtils { private val LOGGER = LoggerFactory.getLogger(HttpUtils::class.java) @@ -27,20 +27,20 @@ object HttpUtils { * Default read timeout in seconds. */ @JvmField - val DEFAULT_READ_TIMEOUT = Duration.ofSeconds(60) + val DEFAULT_READ_TIMEOUT: Duration = Duration.ofSeconds(60) /** * Default write timeout in seconds. */ @JvmField - val DEFAULT_WRITE_TIMEOUT = Duration.ofSeconds(60) + val DEFAULT_WRITE_TIMEOUT: Duration = Duration.ofSeconds(60) /** * HTTP header used for authenticating against a proxy server */ const val PROXY_AUTHORIZATION_HTTP_HEADER = "Proxy-Authorization" - /** Controls whether [OkHttpClient]s built with this class will validate SSL certificates. */ + /** Controls whether [okhttp3.OkHttpClient]s built with this class will validate SSL certificates. */ private var shouldValidateSsl = true /** @see .shouldValidateSsl @@ -51,11 +51,11 @@ object HttpUtils { } /** - * Creates a new [Retrofit] with proper defaults. The instance and the corresponding [OkHttpClient] can + * Creates a new [Retrofit] with proper defaults. The instance and the corresponding [okhttp3.OkHttpClient] can * be customized with the given action. Timeouts for reading and writing can be customized. */ /** - * Creates a new [Retrofit] with proper defaults. The instance and the corresponding [OkHttpClient] can + * Creates a new [Retrofit] with proper defaults. The instance and the corresponding [okhttp3.OkHttpClient] can * be customized with the given action. Read and write timeouts are set according to the default values. */ @JvmOverloads @@ -106,10 +106,8 @@ object HttpUtils { return false } - useProxyServer( - httpClientBuilder, proxySystemProperties.proxyHost!!, - proxySystemProperties.proxyPort - ) + val host = proxySystemProperties.proxyHost ?: return false + useProxyServer(httpClientBuilder, host, proxySystemProperties.proxyPort) } catch (e: ProxySystemProperties.IncorrectPortFormatException) { LOGGER.warn(e.message) return false @@ -130,17 +128,15 @@ object HttpUtils { private fun useProxyAuthenticator(httpClientBuilder: Builder, user: String, password: String) { val proxyAuthenticator = Authenticator { _, response -> - val credential = basic(user, password) response.request.newBuilder() - .header(PROXY_AUTHORIZATION_HTTP_HEADER, credential) + .header(PROXY_AUTHORIZATION_HTTP_HEADER, basic(user, password)) .build() } httpClientBuilder.proxyAuthenticator(proxyAuthenticator) } - /** - * Sets sensible defaults for the [OkHttpClient]. + * Sets sensible defaults for the [okhttp3.OkHttpClient]. */ private fun Builder.setTimeouts(readTimeout: Duration, writeTimeout: Duration) { connectTimeout(Duration.ofSeconds(60)) @@ -191,7 +187,7 @@ object HttpUtils { val credentials = "$username:$password" val basic = "Basic " + Base64.getEncoder().encodeToString(credentials.toByteArray()) - return Interceptor { chain: Interceptor.Chain -> + return Interceptor { chain -> val newRequest = chain.request().newBuilder().header("Authorization", basic).build() chain.proceed(newRequest) } diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/PrioritizableTestCluster.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/PrioritizableTestCluster.kt index 102fd0de0..2f36c7602 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/PrioritizableTestCluster.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/PrioritizableTestCluster.kt @@ -40,7 +40,8 @@ class PrioritizableTestCluster @JsonCreator constructor( var rank: Int = 0 override fun toString() = - StringJoiner(", ", PrioritizableTestCluster::class.java.simpleName + "[", "]").add("clusterId='$clusterId'") + StringJoiner(", ", PrioritizableTestCluster::class.java.simpleName + "[", "]") + .add("clusterId='$clusterId'") .add("score=$score") .add("rank=$rank") .add("tests=$tests") From a5cff102c0f6809c94c54bf5892d604ac4e3235b Mon Sep 17 00:00:00 2001 From: Constructor Date: Tue, 26 Nov 2024 02:07:24 +0100 Subject: [PATCH 5/9] TS-38628 Improved space complexity of levenshtein distance --- .../engine/executor/AvailableTests.java | 2 +- .../com/teamscale/client/StringUtils.kt | 84 ++++++++++--------- 2 files changed, 44 insertions(+), 42 deletions(-) diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/executor/AvailableTests.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/executor/AvailableTests.java index f80bef0f4..abaf9074f 100644 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/executor/AvailableTests.java +++ b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/executor/AvailableTests.java @@ -53,7 +53,7 @@ public Optional convertToUniqueId(PrioritizableTest test) { LOGGER.severe(() -> "Retrieved invalid test '" + test.testName + "' from Teamscale server!"); LOGGER.severe(() -> "The following seem related:"); uniformPathToUniqueIdMapping.keySet().stream().sorted(Comparator - .comparing(testPath -> StringUtils.editDistance(test.testName, testPath))).limit(5) + .comparing(testPath -> StringUtils.levenshteinDistance(test.testName, testPath))).limit(5) .forEach(testAlternative -> LOGGER.severe(() -> " - " + testAlternative)); } return Optional.ofNullable(clusterUniqueId); diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/StringUtils.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/StringUtils.kt index f4955989d..4b5533b0d 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/StringUtils.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/StringUtils.kt @@ -52,10 +52,7 @@ object StringUtils { * @return `true` if the string is blank */ @JvmStatic - fun isBlank(str: String?): Boolean { - return (str == null || str.trim { it <= ' ' }.isEmpty()) - } - + fun isBlank(str: String?) = (str == null || str.trim { it <= ' ' }.isEmpty()) /** * Returns the beginning of a String, cutting off the last part which is separated by the given character. @@ -146,51 +143,56 @@ object StringUtils { } /** - * Calculates the edit distance (aka Levenshtein distance) for two strings, i.e. the number of insert, delete or - * replace operations required to transform one string into the other. The running time is O(n*m) and the space - * complexity is O(n+m), where n/m are the lengths of the strings. Note that due to the high running time, for long - * strings the Diff class should be used, that has a more efficient algorithm, but only for insert/delete (not - * replace operation). + * Calculates the Levenshtein distance between this CharSequence and another CharSequence. + * The Levenshtein distance is a measure of the number of single-character edits (insertions, deletions, or substitutions) + * required to change one string into the other. + * + * This implementation has a time complexity of O(n * m) and a space complexity of O(n), where n and m are the lengths + * of the two strings. * + * For more information, see [Levenshtein distance](https://en.wikipedia.org/wiki/Levenshtein_distance). * - * Although this is a clean reimplementation, the basic algorithm is explained here: - * http://en.wikipedia.org/wiki/Levenshtein_distance# Iterative_with_two_matrix_rows + * @receiver The string to compare. + * @param rhs The string to compare against. + * @return The Levenshtein distance between the two strings. */ @JvmStatic - fun editDistance(s: String, t: String): Int { - val sChars = s.toCharArray() - val tChars = t.toCharArray() - val m = s.length - val n = t.length - - var distance = IntArray(m + 1) - for (i in 0..m) { - distance[i] = i + fun CharSequence.levenshteinDistance(rhs: CharSequence): Int { + if (this == rhs) { + return 0 + } + + if (isEmpty()) { + return rhs.length + } + + if (rhs.isEmpty()) { + return length } - var oldDistance = IntArray(m + 1) - for (j in 1..n) { - // swap distance and oldDistance - - val tmp = oldDistance - oldDistance = distance - distance = tmp - - distance[0] = j - for (i in 1..m) { - var cost = (1 + min( - distance[i - 1].toDouble(), - oldDistance[i].toDouble() - )).toInt() - cost = if (sChars[i - 1] == tChars[j - 1]) { - min(cost.toDouble(), oldDistance[i - 1].toDouble()).toInt() - } else { - min(cost.toDouble(), (1 + oldDistance[i - 1]).toDouble()).toInt() - } - distance[i] = cost + val len0 = length + 1 + val len1 = rhs.length + 1 + + var cost = IntArray(len0) { it } + var newCost = IntArray(len0) { 0 } + + (1.. + newCost[0] = i + + (1.. + val match = if (this[j - 1] == rhs[i - 1]) 0 else 1 + val costReplace = cost[j - 1] + match + val costInsert = cost[j] + 1 + val costDelete = newCost[j - 1] + 1 + + newCost[j] = minOf(costInsert, costDelete, costReplace) } + + val swap = cost + cost = newCost + newCost = swap } - return distance[m] + return cost[len0 - 1] } } From 3e0bb4c940676d3842b26e94efaed81d8e7f0449 Mon Sep 17 00:00:00 2001 From: Constructor Date: Tue, 26 Nov 2024 02:27:32 +0100 Subject: [PATCH 6/9] TS-38628 Use string builder DSL --- .../com/teamscale/client/TeamscaleClient.kt | 4 +- .../com/teamscale/client/TeamscaleServer.kt | 135 ++++++++---------- .../client/TeamscaleServiceGenerator.kt | 11 +- .../kotlin/com/teamscale/client/TestData.kt | 6 +- .../com/teamscale/client/TestDetails.kt | 4 +- 5 files changed, 74 insertions(+), 86 deletions(-) diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleClient.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleClient.kt index 1def61fb8..5b7462729 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleClient.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleClient.kt @@ -159,9 +159,7 @@ open class TeamscaleClient { ) } else { val availableTestsMap = availableTests.map { clusteredTestDetails -> - TestWithClusterId.fromClusteredTestDetails( - clusteredTestDetails - ) + TestWithClusterId.fromClusteredTestDetails(clusteredTestDetails) } service.getImpactedTests( projectId, baseline, baselineRevision, endCommit, endRevision, repository, partitions, diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServer.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServer.kt index 43a271399..7e1bbe62b 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServer.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServer.kt @@ -2,7 +2,6 @@ package com.teamscale.client import okhttp3.HttpUrl import java.net.InetAddress -import java.net.UnknownHostException import java.time.ZonedDateTime import java.time.format.DateTimeFormatter @@ -16,7 +15,7 @@ class TeamscaleServer { @JvmField var project: String? = null - /** The user name used to authenticate against Teamscale. */ + /** The username used to authenticate against Teamscale. */ @JvmField var userName: String? = null @@ -63,40 +62,41 @@ class TeamscaleServer { */ get() { if (field == null) { - return createDefaultMessage() + return buildDefaultMessage() } return field } - private fun createDefaultMessage(): String { - // we do not include the IP address here as one host may have - // - multiple network interfaces - // - each with multiple IP addresses - // - in either IPv4 or IPv6 format - // - and it is unclear which of those is "the right one" or even just which is useful (e.g. loopback or virtual - // adapters are not useful and might even confuse readers) - var hostnamePart = "uploaded from " - hostnamePart += try { - "hostname: " + InetAddress.getLocalHost().hostName - } catch (e: UnknownHostException) { - "an unknown computer" - } + /** + * We do not include the IP address here as one host may have + * - multiple network interfaces + * - each with multiple IP addresses + * - in either IPv4 or IPv6 format + * - and it is unclear which of those is "the right one" or even just which is useful (e.g. loopback or virtual + * adapters are not useful and might even confuse readers) + */ + private fun buildDefaultMessage() = + buildString { + append("$partition coverage uploaded at ") + append(DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now())) + append("\n\nuploaded from ") + + val hostname = runCatching { + "hostname: " + InetAddress.getLocalHost().hostName + }.getOrElse { + "an unknown computer" + } + append(hostname) - var revisionPart = "" - if (revision != null) { - revisionPart = "\nfor revision: $revision" - } + if (revision != null) { + append("\nfor revision: $revision") + } - var configIdPart = "" - if (configId != null) { - configIdPart = "\nprofiler configuration ID: $configId" + if (configId != null) { + append("\nprofiler configuration ID: $configId") + } } - return """$partition coverage uploaded at ${DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now())} - -$hostnamePart$revisionPart$configIdPart""" - } - val isConfiguredForSingleProjectTeamscaleUpload: Boolean /** Checks if all fields required for a single-project Teamscale upload are non-null. */ get() = isConfiguredForServerConnection && partition != null && project != null @@ -110,65 +110,56 @@ $hostnamePart$revisionPart$configIdPart""" get() = url != null && userName != null && userAccessToken != null /** Whether a URL, user and access token were provided. */ - fun canConnectToTeamscale(): Boolean { - return url != null && userName != null && userAccessToken != null - } + fun canConnectToTeamscale() = + url != null && userName != null && userAccessToken != null /** Returns whether all fields are null. */ - fun hasAllFieldsNull(): Boolean { - return url == null && project == null && userName == null && userAccessToken == null && partition == null && commit == null && revision == null - } + fun hasAllFieldsNull() = + url == null + && project == null + && userName == null + && userAccessToken == null + && partition == null + && commit == null + && revision == null /** Returns whether either a commit or revision has been set. */ - fun hasCommitOrRevision(): Boolean { - return commit != null || revision != null - } + fun hasCommitOrRevision() = + commit != null || revision != null /** Checks if another TeamscaleServer has the same project and revision/commit as this TeamscaleServer instance. */ fun hasSameProjectAndCommit(other: TeamscaleServer): Boolean { - if (this.project != other.project) { + if (project != other.project) { return false } - if (this.revision != null) { - return this.revision == other.revision + if (revision != null) { + return revision == other.revision } - return this.commit == other.commit + return commit == other.commit } - override fun toString(): String { - var at: String - if (revision != null) { - at = "revision $revision" - if (repository != null) { - at += "in repository $repository" + override fun toString() = + buildString { + append("Teamscale $url as user $userName for $project to $partition at ") + if (revision != null) { + append("revision $revision") + if (repository != null) { + append(" in repository $repository") + } + } else { + append("commit $commit") } - } else { - at = "commit $commit" } - return "Teamscale $url as user $userName for $project to $partition at $at" - } - - /** Creates a copy of the [TeamscaleServer] configuration, but with the given project and commit set. */ - fun withProjectAndCommit(teamscaleProject: String?, commitDescriptor: CommitDescriptor?): TeamscaleServer { - val teamscaleServer = TeamscaleServer() - teamscaleServer.url = url - teamscaleServer.userName = userName - teamscaleServer.userAccessToken = userAccessToken - teamscaleServer.partition = partition - teamscaleServer.project = teamscaleProject - teamscaleServer.commit = commitDescriptor - return teamscaleServer - } /** Creates a copy of the [TeamscaleServer] configuration, but with the given project and revision set. */ - fun withProjectAndRevision(teamscaleProject: String?, revision: String?): TeamscaleServer { - val teamscaleServer = TeamscaleServer() - teamscaleServer.url = url - teamscaleServer.userName = userName - teamscaleServer.userAccessToken = userAccessToken - teamscaleServer.partition = partition - teamscaleServer.project = teamscaleProject - teamscaleServer.revision = revision - return teamscaleServer + fun withProjectAndRevision(project: String, revision: String): TeamscaleServer { + val server = TeamscaleServer() + server.url = url + server.userName = userName + server.userAccessToken = userAccessToken + server.partition = partition + server.project = project + server.revision = revision + return server } } diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServiceGenerator.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServiceGenerator.kt index 6bc8bdaab..cab9e6316 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServiceGenerator.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServiceGenerator.kt @@ -49,7 +49,7 @@ object TeamscaleServiceGenerator { .addConverterFactory(JacksonConverterFactory.create(JsonUtils.OBJECT_MAPPER)) }, { okHttpBuilder -> - addInterceptors(okHttpBuilder, *interceptors) + okHttpBuilder.addInterceptors(*interceptors) .addInterceptor(HttpUtils.getBasicAuthInterceptor(username, accessToken)) .addInterceptor(AcceptJsonInterceptor()) .addNetworkInterceptor(CustomUserAgentInterceptor()) @@ -58,14 +58,15 @@ object TeamscaleServiceGenerator { readTimeout, writeTimeout ).create(serviceClass) - private fun addInterceptors(builder: OkHttpClient.Builder, vararg interceptors: Interceptor): OkHttpClient.Builder { + private fun OkHttpClient.Builder.addInterceptors( + vararg interceptors: Interceptor + ): OkHttpClient.Builder { interceptors.forEach { interceptor -> - builder.addInterceptor(interceptor) + addInterceptor(interceptor) } - return builder + return this } - /** * Sets an `Accept: application/json` header on all requests. */ diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/TestData.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/TestData.kt index 8e6d0a203..166ce9092 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/TestData.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/TestData.kt @@ -11,12 +11,12 @@ import java.security.MessageDigest * Represents additional test data to attach to [ClusteredTestDetails]. Use the [Builder] to create new * [TestData] objects. * - * - * Internally, the data you pass to the builder is hashed and only the hash is transferred as [ ][ClusteredTestDetails.content] to Teamscale to save network bandwidth and RAM. Whenever a test case's hash changes, + * Internally, the data you pass to the builder is hashed and only the hash is transferred as [ClusteredTestDetails.content] + * to Teamscale to save network bandwidth and RAM. Whenever a test case's hash changes, * Teamscale will select it for the next TIA test run. */ class TestData private constructor( - /** The hash of the test data which will be sent to Teamscale as the [ClusteredTestDetails.content]. */ /*package*/ + /** The hash of the test data which will be sent to Teamscale as the [ClusteredTestDetails.content]. */ val hash: String ) { /** diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/TestDetails.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/TestDetails.kt index 1293efa5d..c24a8ada5 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/TestDetails.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/TestDetails.kt @@ -36,7 +36,5 @@ open class TestDetails @JsonCreator constructor( content == that.content } - override fun hashCode(): Int { - return Objects.hash(uniformPath, sourcePath, content) - } + override fun hashCode() = Objects.hash(uniformPath, sourcePath, content) } From f55fbcce43afa1e48175a2d476fd828c5ebbce2a Mon Sep 17 00:00:00 2001 From: Constructor Date: Tue, 26 Nov 2024 02:30:58 +0100 Subject: [PATCH 7/9] TS-38628 Formatting --- .../com/teamscale/client/CommitDescriptor.kt | 1 - .../com/teamscale/client/FileSystemUtils.kt | 1 - .../teamscale/client/ProxySystemProperties.kt | 2 +- .../com/teamscale/client/StringUtils.kt | 1 - .../com/teamscale/client/TeamscaleClient.kt | 12 +++++---- .../client/TeamscaleServiceGenerator.kt | 26 +++++++++---------- .../kotlin/com/teamscale/client/TestData.kt | 1 - 7 files changed, 21 insertions(+), 23 deletions(-) diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/CommitDescriptor.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/CommitDescriptor.kt index d9829883a..cb470558b 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/CommitDescriptor.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/CommitDescriptor.kt @@ -1,7 +1,6 @@ package com.teamscale.client import java.io.Serializable -import java.util.* /** Holds the branch and timestamp of a commit. */ data class CommitDescriptor( diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/FileSystemUtils.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/FileSystemUtils.kt index 3bc6f1cc9..10d48702b 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/FileSystemUtils.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/FileSystemUtils.kt @@ -1,7 +1,6 @@ package com.teamscale.client import java.io.* -import java.nio.charset.StandardCharsets /** * File system utilities. diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/ProxySystemProperties.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/ProxySystemProperties.kt index e7b1f233f..acfd4f29b 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/ProxySystemProperties.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/ProxySystemProperties.kt @@ -49,7 +49,7 @@ open class ProxySystemProperties(private val protocol: Protocol) { var proxyPassword: String? get() = getProperty(PROXY_PASSWORD_SYSTEM_PROPERTY) - set(value) { + set(value) { setProperty(PROXY_PASSWORD_SYSTEM_PROPERTY, value) } diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/StringUtils.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/StringUtils.kt index 4b5533b0d..9eb7d1a20 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/StringUtils.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/StringUtils.kt @@ -17,7 +17,6 @@ package com.teamscale.client import java.text.NumberFormat -import kotlin.math.min /** * A utility class providing some advanced string functionality. diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleClient.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleClient.kt index 5b7462729..4d95d8772 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleClient.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleClient.kt @@ -128,7 +128,7 @@ open class TeamscaleClient { * @param partitions The partitions that should be considered for retrieving impacted tests. Can be * `null` to indicate that tests from all partitions should be returned. * @param options A list of options (See [ETestImpactOptions] for more details) - * @return A list of test clusters to execute. If availableTests is null, a single dummy cluster is returned with + * @return A list of test clusters to execute. If [availableTests] is null, a single dummy cluster is returned with * all prioritized tests. */ @Throws(IOException::class) @@ -236,10 +236,12 @@ open class TeamscaleClient { ): Response?> { return if (testListResponse.isSuccessful) { Response.success( - listOf(PrioritizableTestCluster( - "dummy", - testListResponse.body() - )), + listOf( + PrioritizableTestCluster( + "dummy", + testListResponse.body() + ) + ), testListResponse.raw() ) } else { diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServiceGenerator.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServiceGenerator.kt index cab9e6316..c8c450cbf 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServiceGenerator.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServiceGenerator.kt @@ -44,19 +44,19 @@ object TeamscaleServiceGenerator { writeTimeout: Duration, vararg interceptors: Interceptor ): S = HttpUtils.createRetrofit( - { retrofitBuilder -> - retrofitBuilder.baseUrl(baseUrl) - .addConverterFactory(JacksonConverterFactory.create(JsonUtils.OBJECT_MAPPER)) - }, - { okHttpBuilder -> - okHttpBuilder.addInterceptors(*interceptors) - .addInterceptor(HttpUtils.getBasicAuthInterceptor(username, accessToken)) - .addInterceptor(AcceptJsonInterceptor()) - .addNetworkInterceptor(CustomUserAgentInterceptor()) - logfile?.let { okHttpBuilder.addInterceptor(FileLoggingInterceptor(it)) } - }, - readTimeout, writeTimeout - ).create(serviceClass) + { retrofitBuilder -> + retrofitBuilder.baseUrl(baseUrl) + .addConverterFactory(JacksonConverterFactory.create(JsonUtils.OBJECT_MAPPER)) + }, + { okHttpBuilder -> + okHttpBuilder.addInterceptors(*interceptors) + .addInterceptor(HttpUtils.getBasicAuthInterceptor(username, accessToken)) + .addInterceptor(AcceptJsonInterceptor()) + .addNetworkInterceptor(CustomUserAgentInterceptor()) + logfile?.let { okHttpBuilder.addInterceptor(FileLoggingInterceptor(it)) } + }, + readTimeout, writeTimeout + ).create(serviceClass) private fun OkHttpClient.Builder.addInterceptors( vararg interceptors: Interceptor diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/TestData.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/TestData.kt index 166ce9092..767ef949d 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/TestData.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/TestData.kt @@ -5,7 +5,6 @@ import org.apache.commons.codec.binary.Hex import org.apache.commons.codec.digest.DigestUtils import java.io.IOException import java.nio.file.Path -import java.security.MessageDigest /** * Represents additional test data to attach to [ClusteredTestDetails]. Use the [Builder] to create new From 3ce14a35aaeff4756a4cde7000fbcd6c8d1d0fe4 Mon Sep 17 00:00:00 2001 From: Constructor Date: Tue, 26 Nov 2024 03:08:55 +0100 Subject: [PATCH 8/9] TS-38628 Bring back function --- .../kotlin/com/teamscale/client/TeamscaleServer.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServer.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServer.kt index 7e1bbe62b..a79a6e99b 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServer.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServer.kt @@ -151,6 +151,18 @@ class TeamscaleServer { } } + /** Creates a copy of the [TeamscaleServer] configuration, but with the given project and commit set. */ + fun withProjectAndCommit(teamscaleProject: String, commitDescriptor: CommitDescriptor): TeamscaleServer { + val teamscaleServer = TeamscaleServer() + teamscaleServer.url = url + teamscaleServer.userName = userName + teamscaleServer.userAccessToken = userAccessToken + teamscaleServer.partition = partition + teamscaleServer.project = teamscaleProject + teamscaleServer.commit = commitDescriptor + return teamscaleServer + } + /** Creates a copy of the [TeamscaleServer] configuration, but with the given project and revision set. */ fun withProjectAndRevision(project: String, revision: String): TeamscaleServer { val server = TeamscaleServer() From ecc83b39958362cb935076b31415992b98c72746 Mon Sep 17 00:00:00 2001 From: Constructor Date: Thu, 28 Nov 2024 19:20:13 +0100 Subject: [PATCH 9/9] TS-38628 Resolve findings, clean up long function --- .../upload/teamscale/TeamscaleUploader.java | 1 - .../teamscale/client/ClusteredTestDetails.kt | 30 +++---- .../teamscale/client/ProxySystemProperties.kt | 56 ++++++++++++- .../com/teamscale/client/StringUtils.kt | 14 +--- .../client/TeamscaleProxySystemProperties.kt | 9 +- .../com/teamscale/client/TeamscaleServer.kt | 14 ++-- .../com/teamscale/TeamscaleUploadTask.kt | 83 +++++++++++-------- .../teamscale/config/ServerConfiguration.kt | 7 ++ .../com/teamscale/TeamscalePluginTest.kt | 2 +- 9 files changed, 137 insertions(+), 79 deletions(-) diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/upload/teamscale/TeamscaleUploader.java b/agent/src/main/java/com/teamscale/jacoco/agent/upload/teamscale/TeamscaleUploader.java index 25e9dc5d8..8f8549916 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/upload/teamscale/TeamscaleUploader.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/upload/teamscale/TeamscaleUploader.java @@ -3,7 +3,6 @@ import com.google.common.base.Strings; import com.teamscale.client.CommitDescriptor; import com.teamscale.client.EReportFormat; -import com.teamscale.client.HttpUtils; import com.teamscale.client.ITeamscaleService; import com.teamscale.client.ITeamscaleServiceKt; import com.teamscale.client.TeamscaleServer; diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/ClusteredTestDetails.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/ClusteredTestDetails.kt index e8c3fac0b..20680dc0f 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/ClusteredTestDetails.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/ClusteredTestDetails.kt @@ -8,8 +8,18 @@ import com.fasterxml.jackson.annotation.JsonProperty * prioritization. */ class ClusteredTestDetails @JsonCreator constructor( + /** + * The uniform path of the test case. + */ @JsonProperty("uniformPath") uniformPath: String, + + /** + * The source path of the test case, if available. + */ @JsonProperty("sourcePath") sourcePath: String?, + /** + * The content associated with the test case, if available. + */ @JsonProperty("content") content: String?, /** * A unique identifier for the cluster this test should be prioritized within. If null the test gets assigned its @@ -24,23 +34,5 @@ class ClusteredTestDetails @JsonCreator constructor( @param:JsonProperty( "partition" ) var partition: String? -) : TestDetails(uniformPath, sourcePath, content) { - companion object { - /** - * Creates clustered test details with the given additional [TestData]. - * - * - * Use this to easily mark additional files or data as belonging to that test case. Whenever the given - * [TestData] changes, this test will be selected to be run by the TIA. - * - * - * Example: For a test that reads test data from an XML file, you should pass the contents of that XML file as its - * test data. Then, whenever the XML is modified, the corresponding test will be run by the TIA. - */ - fun createWithTestData( - uniformPath: String, sourcePath: String, testData: TestData, - clusterId: String, partition: String - ) = ClusteredTestDetails(uniformPath, sourcePath, testData.hash, clusterId, partition) - } -} +) : TestDetails(uniformPath, sourcePath, content) diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/ProxySystemProperties.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/ProxySystemProperties.kt index acfd4f29b..d2f846ad9 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/ProxySystemProperties.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/ProxySystemProperties.kt @@ -19,20 +19,37 @@ open class ProxySystemProperties(private val protocol: Protocol) { private const val PROXY_PASSWORD_SYSTEM_PROPERTY = ".proxyPassword" } + /** + * Enum representing the supported protocols. + */ enum class Protocol { HTTP, HTTPS; + /** + * Returns the protocol name in lowercase. + */ override fun toString() = name.lowercase() } + /** + * Prefix for the system property keys. + * Can be overridden by subclasses to provide a different prefix. + */ protected open val propertyPrefix = "" + /** + * The proxy host system property. + */ var proxyHost: String? get() = getProperty(PROXY_HOST_SYSTEM_PROPERTY) set(value) { setProperty(PROXY_HOST_SYSTEM_PROPERTY, value) } + /** + * The proxy port system property. + * Must be a positive integer and less than or equal to 65535. + */ var proxyPort: Int get() = getProperty(PROXY_PORT_SYSTEM_PROPERTY)?.toIntOrNull() ?: -1 set(value) { @@ -41,21 +58,39 @@ open class ProxySystemProperties(private val protocol: Protocol) { setProperty(PROXY_PORT_SYSTEM_PROPERTY, value.toString()) } + /** + * The proxy user system property. + */ var proxyUser: String? get() = getProperty(PROXY_USER_SYSTEM_PROPERTY) set(value) { setProperty(PROXY_USER_SYSTEM_PROPERTY, value) } + /** + * The proxy password system property. + */ var proxyPassword: String? get() = getProperty(PROXY_PASSWORD_SYSTEM_PROPERTY) set(value) { setProperty(PROXY_PASSWORD_SYSTEM_PROPERTY, value) } + /** + * Retrieves the system property value for the given property key. + * + * @param property The property key. + * @return The property value or null if not set. + */ private fun getProperty(property: String) = System.getProperty("$propertyPrefix${protocol}.$property") + /** + * Sets the system property value for the given property key. + * + * @param property The property key. + * @param value The property value to set. + */ private fun setProperty(property: String, value: String?) { value?.let { check(it.isNotBlank()) { "Value must not be blank" } @@ -63,10 +98,23 @@ open class ProxySystemProperties(private val protocol: Protocol) { } } + /** + * Checks if the proxy server is set. + * + * @return True if the proxy host and port are set, false otherwise. + */ fun isProxyServerSet() = !proxyHost.isNullOrEmpty() && proxyPort > 0 + /** + * Checks if the proxy authentication is set. + * + * @return True if the proxy user and password are set, false otherwise. + */ fun isProxyAuthSet() = !proxyUser.isNullOrEmpty() && !proxyPassword.isNullOrEmpty() + /** + * Clears all proxy system properties. + */ fun clear() { System.clearProperty("$propertyPrefix${protocol}.$PROXY_HOST_SYSTEM_PROPERTY") System.clearProperty("$propertyPrefix${protocol}.$PROXY_PORT_SYSTEM_PROPERTY") @@ -74,5 +122,11 @@ open class ProxySystemProperties(private val protocol: Protocol) { System.clearProperty("$propertyPrefix${protocol}.$PROXY_PASSWORD_SYSTEM_PROPERTY") } + /** + * Exception thrown when the port format is incorrect. + * + * @param message The exception message. + * @param cause The cause of the exception. + */ class IncorrectPortFormatException(message: String, cause: Throwable) : IllegalArgumentException(message, cause) -} +} \ No newline at end of file diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/StringUtils.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/StringUtils.kt index 9eb7d1a20..3dc3e4456 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/StringUtils.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/StringUtils.kt @@ -157,17 +157,9 @@ object StringUtils { */ @JvmStatic fun CharSequence.levenshteinDistance(rhs: CharSequence): Int { - if (this == rhs) { - return 0 - } - - if (isEmpty()) { - return rhs.length - } - - if (rhs.isEmpty()) { - return length - } + if (this == rhs) return 0 + if (isEmpty()) return rhs.length + if (rhs.isEmpty()) return length val len0 = length + 1 val len1 = rhs.length + 1 diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleProxySystemProperties.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleProxySystemProperties.kt index 2eb813c10..18dd5cb0d 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleProxySystemProperties.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleProxySystemProperties.kt @@ -13,10 +13,9 @@ package com.teamscale.client * over the default proxy settings (see [ProxySystemProperties.ProxySystemProperties]). */ class TeamscaleProxySystemProperties(protocol: Protocol) : ProxySystemProperties(protocol) { + /** + * The prefix for Teamscale system properties. + */ override val propertyPrefix: String - get() = TEAMSCALE_PREFIX - - companion object { - const val TEAMSCALE_PREFIX = "teamscale." - } + get() = "teamscale." } \ No newline at end of file diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServer.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServer.kt index a79a6e99b..05097b1e8 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServer.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServer.kt @@ -55,11 +55,11 @@ class TeamscaleServer { @JvmField var configId: String? = null + /** + * The commit message shown in the Teamscale UI for the coverage upload. If the message is null, auto-generates a + * sensible message. + */ var message: String? = null - /** - * The commit message shown in the Teamscale UI for the coverage upload. If the message is null, auto-generates a - * sensible message. - */ get() { if (field == null) { return buildDefaultMessage() @@ -97,16 +97,16 @@ class TeamscaleServer { } } + /** Checks if all fields required for a single-project Teamscale upload are non-null. */ val isConfiguredForSingleProjectTeamscaleUpload: Boolean - /** Checks if all fields required for a single-project Teamscale upload are non-null. */ get() = isConfiguredForServerConnection && partition != null && project != null + /** Checks if all fields required for a Teamscale upload are non-null, except the project which must be null. */ val isConfiguredForMultiProjectUpload: Boolean - /** Checks if all fields required for a Teamscale upload are non-null, except the project which must be null. */ get() = isConfiguredForServerConnection && partition != null && project == null + /** Checks if all required fields to access a Teamscale server are non-null. */ val isConfiguredForServerConnection: Boolean - /** Checks if all required fields to access a Teamscale server are non-null. */ get() = url != null && userName != null && userAccessToken != null /** Whether a URL, user and access token were provided. */ diff --git a/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/TeamscaleUploadTask.kt b/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/TeamscaleUploadTask.kt index 9a610a494..8344e36dc 100755 --- a/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/TeamscaleUploadTask.kt +++ b/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/TeamscaleUploadTask.kt @@ -1,13 +1,14 @@ package com.teamscale +import com.teamscale.client.EReportFormat import com.teamscale.client.TeamscaleClient import com.teamscale.config.extension.TeamscalePluginExtension import org.gradle.api.DefaultTask import org.gradle.api.GradleException import org.gradle.api.provider.SetProperty import org.gradle.api.tasks.* -import java.net.ConnectException -import java.net.SocketTimeoutException +import java.io.File +import java.io.IOException /** Handles report uploads to Teamscale. */ abstract class TeamscaleUploadTask : DefaultTask() { @@ -88,42 +89,56 @@ abstract class TeamscaleUploadTask : DefaultTask() { } private fun uploadReports(enabledReports: List) { - // We want to upload e.g. all JUnit test reports that go to the same partition - // as one commit, so we group them before uploading them - for ((key, reports) in enabledReports.groupBy { Triple(it.format, it.partition.get(), it.message.get()) }) { + // Group reports by format, partition, and message to upload similar reports together + val groupedReports = enabledReports.groupBy { + ReportGroupKey(it.format, it.partition.get(), it.message.get()) + } + + val teamscaleClient = server.toClient() + + groupedReports.forEach { (key, reports) -> val (format, partition, message) = key - val reportFiles = reports.flatMap { it.reportFiles.files }.filter { it.exists() }.distinct() - logger.info("Uploading ${reportFiles.size} ${format.name} report(s) to partition $partition...") + val reportFiles = getExistingReportFiles(reports) + if (reportFiles.isEmpty()) { - logger.info("Skipped empty upload!") - continue + logger.info("Skipped empty upload for ${format.name} reports to partition $partition.") + return@forEach } - logger.debug("Uploading $reportFiles") - - try { - // Prefer to upload to revision and fallback to branch timestamp - val commitDescriptorOrNull = if (revision != null) null else commitDescriptor - retry(3) { - TeamscaleClient( - server.url, - server.userName!!, - server.userAccessToken!!, - server.project!! - ).uploadReports( - format, - reportFiles, - commitDescriptorOrNull, - revision, - repository, - partition, - message - ) - } - } catch (e: ConnectException) { - throw GradleException("Upload failed (${e.message})", e) - } catch (e: SocketTimeoutException) { - throw GradleException("Upload failed (${e.message})", e) + + logger.info("Uploading ${reportFiles.size} ${format.name} report(s) to partition $partition...") + logger.debug("Uploading {}", reportFiles) + + teamscaleClient.uploadReportFiles(format, reportFiles, partition, message) + } + } + + private data class ReportGroupKey( + val format: EReportFormat, + val partition: String, + val message: String + ) + + private fun getExistingReportFiles(reports: List) = + reports.flatMap { it.reportFiles.files } + .filter { it.exists() } + .distinct() + + private fun TeamscaleClient.uploadReportFiles( + format: EReportFormat, + reportFiles: List, + partition: String, + message: String + ) { + val commitDescriptorOrNull = if (revision == null) commitDescriptor else null + try { + retry(3) { + uploadReports( + format, reportFiles, commitDescriptorOrNull, + revision, repository, partition, message + ) } + } catch (e: IOException) { + throw GradleException("Upload failed (${e.message})", e) } } } diff --git a/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/config/ServerConfiguration.kt b/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/config/ServerConfiguration.kt index dfe2df485..34e437df0 100644 --- a/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/config/ServerConfiguration.kt +++ b/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/config/ServerConfiguration.kt @@ -1,5 +1,6 @@ package com.teamscale.config +import com.teamscale.client.TeamscaleClient import org.gradle.api.GradleException import java.io.Serializable @@ -27,4 +28,10 @@ data class ServerConfiguration( throw GradleException("Teamscale user access token must not be empty!") } } + fun toClient() = TeamscaleClient( + url, + project ?: throw GradleException("Teamscale project name must not be null!"), + userName ?: throw GradleException("Teamscale user name must not be null!"), + userAccessToken ?: throw GradleException("Teamscale user access token must not be null!") + ) } diff --git a/teamscale-gradle-plugin/src/test/kotlin/com/teamscale/TeamscalePluginTest.kt b/teamscale-gradle-plugin/src/test/kotlin/com/teamscale/TeamscalePluginTest.kt index ebc0bfafc..825feea1d 100644 --- a/teamscale-gradle-plugin/src/test/kotlin/com/teamscale/TeamscalePluginTest.kt +++ b/teamscale-gradle-plugin/src/test/kotlin/com/teamscale/TeamscalePluginTest.kt @@ -273,7 +273,7 @@ class TeamscalePluginTest { private fun assertPartialCoverage(source: String) { val testwiseCoverageReport = JsonUtils.deserialize(source, TestwiseCoverageReport::class.java) - assertThat(testwiseCoverageReport!!) + assertThat(testwiseCoverageReport) .hasPartial(true) .containsExecutionResult("com/example/project/JUnit4Test/systemTest", ETestExecutionResult.PASSED) .containsCoverage(