From becf7da5ff118ff8ca431d4a15592774be73b5ac Mon Sep 17 00:00:00 2001 From: Johannes Veihelmann Date: Thu, 25 Jul 2024 13:06:08 +0200 Subject: [PATCH 001/186] TS-38429 re-enable flickering test --- .../test/java/com/teamscale/tia/TiaMavenMultipleJobsTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/system-tests/tia-maven/src/test/java/com/teamscale/tia/TiaMavenMultipleJobsTest.java b/system-tests/tia-maven/src/test/java/com/teamscale/tia/TiaMavenMultipleJobsTest.java index 07f9f88f5..bc98d2d5a 100644 --- a/system-tests/tia-maven/src/test/java/com/teamscale/tia/TiaMavenMultipleJobsTest.java +++ b/system-tests/tia-maven/src/test/java/com/teamscale/tia/TiaMavenMultipleJobsTest.java @@ -2,7 +2,6 @@ import com.teamscale.test.commons.SystemTestUtils; import org.conqat.lib.commons.filesystem.FileSystemUtils; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.nio.file.Path; @@ -13,7 +12,6 @@ /** * Test class to check if multiple maven plugins can be started with dynamic port allocation. */ -@Disabled("https://cqse.atlassian.net/browse/TS-38429") public class TiaMavenMultipleJobsTest { /** From 1a80b1cc66a5a268aaa059281b14c24bc681ad89 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Jul 2024 09:26:23 +0000 Subject: [PATCH 002/186] Update jersey to v2.44 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ae8fa1131..c0a06d374 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] jetty = "9.4.55.v20240627" -jersey = "2.43" +jersey = "2.44" jackson = "2.17.2" # When upgrading JaCoCo to a newer version make sure to # check the comment in the OpenAnalyzer.java, JaCoCoPreMain.java and CachingInstructionsBuilder.java From 0b2c8155556ff75cd16f7e185dee2bad71355b6e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 7 Aug 2024 20:01:22 +0000 Subject: [PATCH 003/186] Update dependency org.apache.commons:commons-lang3 to v3.16.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c0a06d374..0d76006ed 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -50,7 +50,7 @@ spark = { module = "com.sparkjava:spark-core", version = "2.9.4" } jcommander = { module = "com.beust:jcommander", version = "1.82" } teamscaleLibCommons = { module = "com.teamscale:teamscale-lib-commons", version = "9.4.1" } commonsCodec = { module = "commons-codec:commons-codec", version = "1.17.1" } -commonsLang = { module = "org.apache.commons:commons-lang3", version = "3.15.0" } +commonsLang = { module = "org.apache.commons:commons-lang3", version = "3.16.0" } commonsIo = { module = "commons-io:commons-io", version = "2.16.1" } slf4j-api = { module = "org.slf4j:slf4j-api", version = "2.0.13" } jgit = { module = "org.eclipse.jgit:org.eclipse.jgit", version = "6.10.0.202406032230-r" } From 506dd37049ae7ba1b6ce4dd8ae666d8006de2376 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 8 Aug 2024 16:07:19 +0000 Subject: [PATCH 004/186] Update dependency org.slf4j:slf4j-api to v2.0.15 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0d76006ed..5014ad113 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -52,7 +52,7 @@ teamscaleLibCommons = { module = "com.teamscale:teamscale-lib-commons", version commonsCodec = { module = "commons-codec:commons-codec", version = "1.17.1" } commonsLang = { module = "org.apache.commons:commons-lang3", version = "3.16.0" } commonsIo = { module = "commons-io:commons-io", version = "2.16.1" } -slf4j-api = { module = "org.slf4j:slf4j-api", version = "2.0.13" } +slf4j-api = { module = "org.slf4j:slf4j-api", version = "2.0.15" } jgit = { module = "org.eclipse.jgit:org.eclipse.jgit", version = "6.10.0.202406032230-r" } okio = { module = "com.squareup.okio:okio", version = "3.9.0" } From 7879e8fb72902d71e5d312eb6c22d0ce53b396f3 Mon Sep 17 00:00:00 2001 From: Florian Dreier Date: Fri, 9 Aug 2024 07:48:21 +0200 Subject: [PATCH 005/186] Adapted output after slf4j 2.0.14 update --- .../src/test/java/com/teamscale/client/SutUsesLogbackTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system-tests/sut-uses-logback-test/src/test/java/com/teamscale/client/SutUsesLogbackTest.java b/system-tests/sut-uses-logback-test/src/test/java/com/teamscale/client/SutUsesLogbackTest.java index cd23dd858..418520d85 100644 --- a/system-tests/sut-uses-logback-test/src/test/java/com/teamscale/client/SutUsesLogbackTest.java +++ b/system-tests/sut-uses-logback-test/src/test/java/com/teamscale/client/SutUsesLogbackTest.java @@ -19,7 +19,7 @@ public void systemTest() throws Exception { assertThat(result.getStdout()).contains("This warning is to test logging in the SUT"); assertThat(result.getStdout()).doesNotContainIgnoringCase("error"); - assertThat(result.getStderr()).isEmpty(); + assertThat(result.getStderr()).isEqualToNormalizingNewlines("SLF4J(I): Connected with provider of type [shadow.ch.qos.logback.classic.spi.LogbackServiceProvider]\n"); Path appLogFile = Paths.get("logTest/app.log"); assertThat(appLogFile).exists(); From 7c9228289784dcf101f8cb06982fb16b563d892e Mon Sep 17 00:00:00 2001 From: Thomas Pettinger Date: Fri, 9 Aug 2024 10:02:07 +0200 Subject: [PATCH 006/186] TS-39915 Add more logging --- .../test_descriptor/CucumberPickleDescriptorResolver.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.java index bd76acca1..cc2dcf3c7 100644 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.java +++ b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.java @@ -27,12 +27,14 @@ public class CucumberPickleDescriptorResolver implements ITestDescriptorResolver @Override public Optional getUniformPath(TestDescriptor testDescriptor) { Optional featurePath = getFeaturePath(testDescriptor); + LOGGER.fine(() -> "Resolved feature" + featurePath); if (!featurePath.isPresent()) { LOGGER.severe(() -> "Cannot resolve the feature classpath for " + testDescriptor + ". This is probably a bug. Please report to CQSE"); return Optional.empty(); } Optional pickleName = getPickleName(testDescriptor); + LOGGER.fine(() -> "Resolved pickle name: " + pickleName); if (!pickleName.isPresent()) { LOGGER.severe(() -> "Cannot resolve the pickle name for " + testDescriptor + ". This is probably a bug. Please report to CQSE"); @@ -49,6 +51,7 @@ public Optional getUniformPath(TestDescriptor testDescriptor) { uniformPath += " #" + indexOfCurrentTest; } + LOGGER.fine(() -> "Resolved uniform path: " + finalUniformPath); return Optional.of(uniformPath); } @@ -152,7 +155,7 @@ private boolean isFeatureFileTestDescriptor(TestDescriptor cucumberTestDescripto } private List flatListOfAllTestDescriptorChildrenWithPickleName(TestDescriptor testDescriptor, - String pickleName) { + String pickleName) { if (testDescriptor.getChildren().isEmpty()) { Optional pickleId = getPickleName(testDescriptor); if (pickleId.isPresent() && pickleName.equals(pickleId.get())) { From 2598424bb1d247711eedd12fd098730d42e92edb Mon Sep 17 00:00:00 2001 From: Thomas Pettinger Date: Fri, 9 Aug 2024 10:02:28 +0200 Subject: [PATCH 007/186] TS-39915 Remove duplicated slashes from uniform paths and clusterIds --- .../CucumberPickleDescriptorResolver.java | 12 +++++++++- .../CucumberPickleDescriptorResolverTest.java | 22 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.java index cc2dcf3c7..39bc7b188 100644 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.java +++ b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.java @@ -51,13 +51,16 @@ public Optional getUniformPath(TestDescriptor testDescriptor) { uniformPath += " #" + indexOfCurrentTest; } + // IntelliJ complains without this that uniform path should be final when used in a lambda + uniformPath = removeDuplicatedSlashes(uniformPath); + final String finalUniformPath = uniformPath; LOGGER.fine(() -> "Resolved uniform path: " + finalUniformPath); return Optional.of(uniformPath); } @Override public Optional getClusterId(TestDescriptor testDescriptor) { - return getFeaturePath(testDescriptor); + return getFeaturePath(testDescriptor).map(this::removeDuplicatedSlashes); } @Override @@ -76,6 +79,13 @@ private Optional getFeaturePath(TestDescriptor testDescriptor) { return featureClasspath.map(featureClasspathString -> featureClasspathString.replaceAll("classpath:", "")); } + /** + * Remove duplicated "/" with one (due to TS-39915) + */ + String removeDuplicatedSlashes(String string) { + return string.replaceAll("(? getPickleName(TestDescriptor testDescriptor) { // The PickleDescriptor test descriptor class is not public, so we can't import and use it to get access to the pickle attribute containing the name => reflection // https://github.com/cucumber/cucumber-jvm/blob/main/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/NodeDescriptor.java#L90 diff --git a/impacted-test-engine/src/test/java/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolverTest.java b/impacted-test-engine/src/test/java/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolverTest.java index 7ea90f0a6..288c41963 100644 --- a/impacted-test-engine/src/test/java/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolverTest.java +++ b/impacted-test-engine/src/test/java/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolverTest.java @@ -27,4 +27,26 @@ void escapeSlashes() { expectedByInput.forEach((input, expected) -> Assertions.assertEquals(expected, CucumberPickleDescriptorResolver.escapeSlashes(input))); } + + @Test + void testNoDuplicatedSlashesInUniformPath() { + CucumberPickleDescriptorResolver cucumberPickleDescriptorResolver = new CucumberPickleDescriptorResolver(); + LinkedHashMap expectedByInput = new LinkedHashMap<>(); + expectedByInput.put("abc", "abc"); + expectedByInput.put("ab/c", "ab/c"); + expectedByInput.put("ab//c", "ab/c"); + expectedByInput.put("ab///c", "ab/c"); + expectedByInput.put("ab\\/\\//c", "ab\\/\\//c"); + expectedByInput.put("a/", "a/"); + expectedByInput.put("a//", "a/"); + expectedByInput.put("/a", "/a"); + expectedByInput.put("//a", "/a"); + expectedByInput.put("/", "/"); + expectedByInput.put("\\/", "\\/"); + expectedByInput.put("\\", "\\"); + expectedByInput.put("\\\\", "\\\\"); + + expectedByInput.forEach((input, expected) -> Assertions.assertEquals(expected, + cucumberPickleDescriptorResolver.removeDuplicatedSlashes(input))); + } } \ No newline at end of file From f40f0a7e0a9b48d6ff9c51ff1346848710902cf1 Mon Sep 17 00:00:00 2001 From: Thomas Pettinger Date: Fri, 9 Aug 2024 10:06:07 +0200 Subject: [PATCH 008/186] TS-39915 Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ef04cba4..e2f78668f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ We use [semantic versioning](http://semver.org/): # Next version - [feature] _agent_: Prevent uploading coverage to the same project + revision or branch@timestamp when doing multi project upload via git.properties +- [fix] _impacted-test-engine_: Remove duplicated slashes in uniform paths and cluster ids for Cucumber Tests # 34.0.1 - [fix] _agent_: Error was reported when the system under test used logback From 8dcbb145a0b7a47bc2be6df5b2207caffe4b5b3d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 12 Aug 2024 10:31:24 +0000 Subject: [PATCH 009/186] Update dependency org.apache.maven.plugins:maven-gpg-plugin to v3.2.5 --- teamscale-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teamscale-maven-plugin/pom.xml b/teamscale-maven-plugin/pom.xml index e6c362466..aabf5c454 100644 --- a/teamscale-maven-plugin/pom.xml +++ b/teamscale-maven-plugin/pom.xml @@ -184,7 +184,7 @@ org.apache.maven.plugins maven-gpg-plugin - 3.2.4 + 3.2.5 sign-artifacts From 7b380732cb02f24b80c1204ae44dab259bf5ac51 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 14 Aug 2024 10:59:31 +0000 Subject: [PATCH 010/186] Update dependency org.junit.jupiter:junit-jupiter-engine to v5.11.0 --- teamscale-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teamscale-maven-plugin/pom.xml b/teamscale-maven-plugin/pom.xml index e6c362466..9521db82c 100644 --- a/teamscale-maven-plugin/pom.xml +++ b/teamscale-maven-plugin/pom.xml @@ -65,7 +65,7 @@ org.junit.jupiter junit-jupiter-engine - 5.10.3 + 5.11.0 test From 787c4a24fd110a6dcae1ac198b61c30b8ad7cbc5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 14 Aug 2024 10:59:36 +0000 Subject: [PATCH 011/186] Update dependency org.junit.vintage:junit-vintage-engine to v5.11.0 --- .../src/test/resources/calculator_groovy/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teamscale-gradle-plugin/src/test/resources/calculator_groovy/build.gradle b/teamscale-gradle-plugin/src/test/resources/calculator_groovy/build.gradle index c98b8c4e7..a6aa55c38 100644 --- a/teamscale-gradle-plugin/src/test/resources/calculator_groovy/build.gradle +++ b/teamscale-gradle-plugin/src/test/resources/calculator_groovy/build.gradle @@ -14,7 +14,7 @@ plugins { import com.teamscale.TestImpacted ext.junit4Version = '4.13.2' -ext.junitVintageVersion = '5.10.3' +ext.junitVintageVersion = '5.11.0' ext.junitPlatformVersion = '1.4.0' ext.junitJupiterVersion = '5.10.3' From 09e855729dede59e02f02314697e285325f2fa86 Mon Sep 17 00:00:00 2001 From: Thomas Pettinger Date: Thu, 22 Aug 2024 14:30:59 +0200 Subject: [PATCH 012/186] TS-39915 Fix log message --- .../test_descriptor/CucumberPickleDescriptorResolver.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.java index 39bc7b188..27910d785 100644 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.java +++ b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.java @@ -27,7 +27,7 @@ public class CucumberPickleDescriptorResolver implements ITestDescriptorResolver @Override public Optional getUniformPath(TestDescriptor testDescriptor) { Optional featurePath = getFeaturePath(testDescriptor); - LOGGER.fine(() -> "Resolved feature" + featurePath); + LOGGER.fine(() -> "Resolved feature: " + featurePath); if (!featurePath.isPresent()) { LOGGER.severe(() -> "Cannot resolve the feature classpath for " + testDescriptor + ". This is probably a bug. Please report to CQSE"); From 2855dc8cad41b02668dd11c18d27336ba7ada2ec Mon Sep 17 00:00:00 2001 From: Thomas Pettinger Date: Thu, 22 Aug 2024 14:31:39 +0200 Subject: [PATCH 013/186] TS-39915 Ensure that we can make use of the java.util.logging.config.file system property --- .../test_impacted/commons/LoggerUtils.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/commons/LoggerUtils.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/commons/LoggerUtils.java index 94959f935..b1ce31a0d 100644 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/commons/LoggerUtils.java +++ b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/commons/LoggerUtils.java @@ -1,6 +1,10 @@ package com.teamscale.test_impacted.commons; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; import java.util.logging.ConsoleHandler; +import java.util.logging.LogManager; import java.util.logging.LogRecord; import java.util.logging.Logger; import java.util.logging.SimpleFormatter; @@ -12,8 +16,12 @@ public class LoggerUtils { private static final Logger MAIN_LOGGER; + public static final String JAVA_UTIL_LOGGING_CONFIG_FILE_SYSTEM_PROPERTY = "java.util.logging.config.file"; static { + // Needs to be at the very top so it also takes affect when setting the log level for Console handlers + useDefaultJULConfigFile(); + MAIN_LOGGER = Logger.getLogger("com.teamscale"); MAIN_LOGGER.setUseParentHandlers(false); ConsoleHandler handler = new ConsoleHandler(); @@ -24,9 +32,28 @@ public synchronized String format(LogRecord lr) { return String.format("[%1$s] %2$s%n", lr.getLevel().getLocalizedName(), lr.getMessage()); } }); + + MAIN_LOGGER.addHandler(handler); } + /** + * Normally, the java util logging framework picks up the config file specified via the system property + * {@value #JAVA_UTIL_LOGGING_CONFIG_FILE_SYSTEM_PROPERTY}. For some reason, this does not work here, so we need to + * teach the log manager to use it. + */ + private static void useDefaultJULConfigFile() { + String loggingPropertiesFilePath = System.getProperty(JAVA_UTIL_LOGGING_CONFIG_FILE_SYSTEM_PROPERTY); + if (loggingPropertiesFilePath != null) { + File loggingPropertiesFile = new File(loggingPropertiesFilePath); + try { + LogManager.getLogManager().readConfiguration(Files.newInputStream(loggingPropertiesFile.toPath())); + } catch (IOException e) { + // Ignore, we cant load the logging config so we just use the defaults + } + } + } + /** * Returns a logger for the given class. */ From 6226938d8ade8dc7ec6447de17ce3db0116e388f Mon Sep 17 00:00:00 2001 From: Thomas Pettinger Date: Thu, 22 Aug 2024 14:33:26 +0200 Subject: [PATCH 014/186] TS-39915 Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2f78668f..78ea5adb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ We use [semantic versioning](http://semver.org/): # Next version - [feature] _agent_: Prevent uploading coverage to the same project + revision or branch@timestamp when doing multi project upload via git.properties - [fix] _impacted-test-engine_: Remove duplicated slashes in uniform paths and cluster ids for Cucumber Tests +- [fix] _impacted-test-engine_: Ensure that the config file specified via `java.util.logging.config.file` is used when specified # 34.0.1 - [fix] _agent_: Error was reported when the system under test used logback From 4e046114eabcb865bb01178e466671fcd3ea0c1d Mon Sep 17 00:00:00 2001 From: Thomas Pettinger Date: Thu, 22 Aug 2024 14:35:22 +0200 Subject: [PATCH 015/186] TS-39915 Add another fine log statement --- .../test_descriptor/CucumberPickleDescriptorResolver.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.java index 27910d785..2b8e064f0 100644 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.java +++ b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.java @@ -74,9 +74,10 @@ public String getEngineId() { * hellocucumber/calculator.feature/11/16/21 */ private Optional getFeaturePath(TestDescriptor testDescriptor) { - Optional featureClasspath = TestDescriptorUtils.getUniqueIdSegment(testDescriptor, + Optional featureSegment = TestDescriptorUtils.getUniqueIdSegment(testDescriptor, FEATURE_SEGMENT_TYPE); - return featureClasspath.map(featureClasspathString -> featureClasspathString.replaceAll("classpath:", "")); + LOGGER.fine(() -> "Resolved feature segment: " + featureSegment); + return featureSegment.map(featureClasspathString -> featureClasspathString.replaceAll("classpath:", "")); } /** From dd887f15ae94134f5acff06cd1eb8abb3767ac8f Mon Sep 17 00:00:00 2001 From: Thomas Pettinger Date: Thu, 22 Aug 2024 14:56:38 +0200 Subject: [PATCH 016/186] TS-39915 Log warnings if the file specified via java.util.logging.config.file cannot be loaded --- .../test_impacted/commons/LoggerUtils.java | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/commons/LoggerUtils.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/commons/LoggerUtils.java index b1ce31a0d..165fbc7f2 100644 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/commons/LoggerUtils.java +++ b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/commons/LoggerUtils.java @@ -1,8 +1,9 @@ package com.teamscale.test_impacted.commons; -import java.io.File; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.logging.ConsoleHandler; import java.util.logging.LogManager; import java.util.logging.LogRecord; @@ -43,14 +44,23 @@ public synchronized String format(LogRecord lr) { * teach the log manager to use it. */ private static void useDefaultJULConfigFile() { - String loggingPropertiesFilePath = System.getProperty(JAVA_UTIL_LOGGING_CONFIG_FILE_SYSTEM_PROPERTY); - if (loggingPropertiesFilePath != null) { - File loggingPropertiesFile = new File(loggingPropertiesFilePath); - try { - LogManager.getLogManager().readConfiguration(Files.newInputStream(loggingPropertiesFile.toPath())); - } catch (IOException e) { - // Ignore, we cant load the logging config so we just use the defaults + String loggingPropertiesFilePathString = System.getProperty(JAVA_UTIL_LOGGING_CONFIG_FILE_SYSTEM_PROPERTY); + if (loggingPropertiesFilePathString == null) { + return; + } + + Logger logger = Logger.getLogger(LoggerUtils.class.getName()); + try { + Path loggingPropertiesFilePath = Paths.get(loggingPropertiesFilePathString); + if (!loggingPropertiesFilePath.toFile().exists()) { + logger.warning( + "Cannot find the file specified via " + JAVA_UTIL_LOGGING_CONFIG_FILE_SYSTEM_PROPERTY + ": " + loggingPropertiesFilePathString); + return; } + LogManager.getLogManager().readConfiguration(Files.newInputStream(loggingPropertiesFilePath)); + } catch (IOException e) { + logger.warning( + "Cannot load the file specified via " + JAVA_UTIL_LOGGING_CONFIG_FILE_SYSTEM_PROPERTY + ": " + loggingPropertiesFilePathString + ". " + e.getMessage()); } } From 9d995e7108b94e86a0664bce688ef1e7991ff6d3 Mon Sep 17 00:00:00 2001 From: Thomas Pettinger Date: Thu, 22 Aug 2024 14:58:18 +0200 Subject: [PATCH 017/186] TS-39915 Make java.util.logging.config.file constant private --- .../java/com/teamscale/test_impacted/commons/LoggerUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/commons/LoggerUtils.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/commons/LoggerUtils.java index 165fbc7f2..44242f215 100644 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/commons/LoggerUtils.java +++ b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/commons/LoggerUtils.java @@ -17,7 +17,7 @@ public class LoggerUtils { private static final Logger MAIN_LOGGER; - public static final String JAVA_UTIL_LOGGING_CONFIG_FILE_SYSTEM_PROPERTY = "java.util.logging.config.file"; + private static final String JAVA_UTIL_LOGGING_CONFIG_FILE_SYSTEM_PROPERTY = "java.util.logging.config.file"; static { // Needs to be at the very top so it also takes affect when setting the log level for Console handlers From 4843a793414d30f0e9c2cbe105fef8fbc94e6f37 Mon Sep 17 00:00:00 2001 From: Thomas Pettinger Date: Mon, 26 Aug 2024 13:17:42 +0200 Subject: [PATCH 018/186] TS-39915 Add one more log --- .../java/com/teamscale/test_impacted/commons/LoggerUtils.java | 1 - .../test_descriptor/CucumberPickleDescriptorResolver.java | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/commons/LoggerUtils.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/commons/LoggerUtils.java index 44242f215..edf438f52 100644 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/commons/LoggerUtils.java +++ b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/commons/LoggerUtils.java @@ -34,7 +34,6 @@ public synchronized String format(LogRecord lr) { } }); - MAIN_LOGGER.addHandler(handler); } diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.java index 2b8e064f0..1d8c3b2d6 100644 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.java +++ b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.java @@ -74,6 +74,7 @@ public String getEngineId() { * hellocucumber/calculator.feature/11/16/21 */ private Optional getFeaturePath(TestDescriptor testDescriptor) { + LOGGER.fine((() -> "Unique ID of cucumber test descriptor: " + testDescriptor.getUniqueId())); Optional featureSegment = TestDescriptorUtils.getUniqueIdSegment(testDescriptor, FEATURE_SEGMENT_TYPE); LOGGER.fine(() -> "Resolved feature segment: " + featureSegment); From caac88d31fcdd45f27bcc778cc494f632398ad4f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Sep 2024 04:04:35 +0000 Subject: [PATCH 019/186] Update dependency maven to v3.9.9 --- teamscale-maven-plugin/.mvn/wrapper/maven-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teamscale-maven-plugin/.mvn/wrapper/maven-wrapper.properties b/teamscale-maven-plugin/.mvn/wrapper/maven-wrapper.properties index 06477ea70..2b215e45d 100644 --- a/teamscale-maven-plugin/.mvn/wrapper/maven-wrapper.properties +++ b/teamscale-maven-plugin/.mvn/wrapper/maven-wrapper.properties @@ -15,5 +15,5 @@ # specific language governing permissions and limitations # under the License. wrapperVersion=3.3.1 -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.8/apache-maven-3.9.8-bin.zip +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.1/maven-wrapper-3.3.1.jar From 60bba15ba8cc49e51e698f8d311db64a0aa806fe Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Sep 2024 04:05:25 +0000 Subject: [PATCH 020/186] Update dependency org.apache.maven.plugins:maven-deploy-plugin to v3.1.3 --- teamscale-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teamscale-maven-plugin/pom.xml b/teamscale-maven-plugin/pom.xml index 9986f4086..387f14f38 100644 --- a/teamscale-maven-plugin/pom.xml +++ b/teamscale-maven-plugin/pom.xml @@ -206,7 +206,7 @@ org.apache.maven.plugins maven-deploy-plugin - 3.1.2 + 3.1.3 org.sonatype.plugins From 55f1792e8a3a68552d75bb00e72da4365a494a93 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Sep 2024 04:11:35 +0000 Subject: [PATCH 021/186] Update dependency gradle to v8.10 --- gradle/wrapper/gradle-wrapper.jar | Bin 43504 -> 43583 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 2c3521197d7c4586c843d1d3e9090525f1898cde..a4b76b9530d66f5e68d973ea569d8e19de379189 100644 GIT binary patch delta 3990 zcmV;H4{7l5(*nQL0Kr1kzC=_KMxQY0|W5(lc#i zH*M1^P4B}|{x<+fkObwl)u#`$GxKKV&3pg*-y6R6txw)0qU|Clf9Uds3x{_-**c=7 z&*)~RHPM>Rw#Hi1R({;bX|7?J@w}DMF>dQQU2}9yj%iLjJ*KD6IEB2^n#gK7M~}6R zkH+)bc--JU^pV~7W=3{E*4|ZFpDpBa7;wh4_%;?XM-5ZgZNnVJ=vm!%a2CdQb?oTa z70>8rTb~M$5Tp!Se+4_OKWOB1LF+7gv~$$fGC95ToUM(I>vrd$>9|@h=O?eARj0MH zT4zo(M>`LWoYvE>pXvqG=d96D-4?VySz~=tPVNyD$XMshoTX(1ZLB5OU!I2OI{kb) zS8$B8Qm>wLT6diNnyJZC?yp{Kn67S{TCOt-!OonOK7$K)e-13U9GlnQXPAb&SJ0#3 z+vs~+4Qovv(%i8g$I#FCpCG^C4DdyQw3phJ(f#y*pvNDQCRZ~MvW<}fUs~PL=4??j zmhPyg<*I4RbTz|NHFE-DC7lf2=}-sGkE5e!RM%3ohM7_I^IF=?O{m*uUPH(V?gqyc(Rp?-Qu(3bBIL4Fz(v?=_Sh?LbK{nqZMD>#9D_hNhaV$0ef3@9V90|0u#|PUNTO>$F=qRhg1duaE z0`v~X3G{8RVT@kOa-pU+z8{JWyP6GF*u2e8eKr7a2t1fuqQy)@d|Qn(%YLZ62TWtoX@$nL}9?atE#Yw`rd(>cr0gY;dT9~^oL;u)zgHUvxc2I*b&ZkGM-iq=&(?kyO(3}=P! zRp=rErEyMT5UE9GjPHZ#T<`cnD)jyIL!8P{H@IU#`e8cAG5jMK zVyKw7--dAC;?-qEu*rMr$5@y535qZ6p(R#+fLA_)G~!wnT~~)|s`}&fA(s6xXN`9j zP#Fd3GBa#HeS{5&8p?%DKUyN^X9cYUc6vq}D_3xJ&d@=6j(6BZKPl?!k1?!`f3z&a zR4ZF60Mx7oBxLSxGuzA*Dy5n-d2K=+)6VMZh_0KetK|{e;E{8NJJ!)=_E~1uu=A=r zrn&gh)h*SFhsQJo!f+wKMIE;-EOaMSMB@aXRU(UcnJhZW^B^mgs|M9@5WF@s6B0p& zm#CTz)yiQCgURE{%hjxHcJ6G&>G9i`7MyftL!QQd5 z@RflRs?7)99?X`kHNt>W3l7YqscBpi*R2+fsgABor>KVOu(i(`03aytf2UA!&SC9v z!E}whj#^9~=XHMinFZ;6UOJjo=mmNaWkv~nC=qH9$s-8roGeyaW-E~SzZ3Gg>j zZ8}<320rg4=$`M0nxN!w(PtHUjeeU?MvYgWKZ6kkzABK;vMN0|U;X9abJleJA(xy<}5h5P(5 z{RzAFPvMnX2m0yH0Jn2Uo-p`daE|(O`YQiC#jB8;6bVIUf?SY(k$#C0`d6qT`>Xe0+0}Oj0=F&*D;PVe=Z<=0AGI<6$gYLwa#r` zm449x*fU;_+J>Mz!wa;T-wldoBB%&OEMJgtm#oaI60TSYCy7;+$5?q!zi5K`u66Wq zvg)Fx$s`V3Em{=OEY{3lmh_7|08ykS&U9w!kp@Ctuzqe1JFOGz6%i5}Kmm9>^=gih z?kRxqLA<3@e=}G4R_?phW{4DVr?`tPfyZSN@R=^;P;?!2bh~F1I|fB7P=V=9a6XU5 z<#0f>RS0O&rhc&nTRFOW7&QhevP0#>j0eq<1@D5yAlgMl5n&O9X|Vq}%RX}iNyRFF z7sX&u#6?E~bm~N|z&YikXC=I0E*8Z$v7PtWfjy)$e_Ez25fnR1Q=q1`;U!~U>|&YS zaOS8y!^ORmr2L4ik!IYR8@Dcx8MTC=(b4P6iE5CnrbI~7j7DmM8em$!da&D!6Xu)!vKPdLG z9f#)se|6=5yOCe)N6xDhPI!m81*dNe7u985zi%IVfOfJh69+#ag4ELzGne?o`eA`42K4T)h3S+s)5IT97%O>du- z0U54L8m4}rkRQ?QBfJ%DLssy^+a7Ajw;0&`NOTY4o;0-ivm9 zBz1C%nr_hQ)X)^QM6T1?=yeLkuG9Lf50(eH}`tFye;01&(p?8i+6h};VV-2B~qdxeC#=X z(JLlzy&fHkyi9Ksbcs~&r^%lh^2COldLz^H@X!s~mr9Dr6z!j+4?zkD@Ls7F8(t(f z9`U?P$Lmn*Y{K}aR4N&1N=?xtQ1%jqf1~pJyQ4SgBrEtR`j4lQuh7cqP49Em5cO=I zB(He2`iPN5M=Y0}h(IU$37ANTGx&|b-u1BYA*#dE(L-lptoOpo&th~E)_)y-`6kSH z3vvyVrcBwW^_XYReJ=JYd9OBQrzv;f2AQdZH#$Y{Y+Oa33M70XFI((fs;mB4e`<<{ ze4dv2B0V_?Ytsi>>g%qs*}oDGd5d(RNZ*6?7qNbdp7wP4T72=F&r?Ud#kZr8Ze5tB z_oNb7{G+(o2ajL$!69FW@jjPQ2a5C)m!MKKRirC$_VYIuVQCpf9rIms0GRDf)8AH${I`q^~5rjot@#3$2#zT2f`(N^P7Z;6(@EK$q*Jgif00I6*^ZGV+XB5uw*1R-@23yTw&WKD{s1;HTL;dO)%5i#`dc6b7;5@^{KU%N|A-$zsYw4)7LA{3`Zp>1 z-?K9_IE&z)dayUM)wd8K^29m-l$lFhi$zj0l!u~4;VGR6Y!?MAfBC^?QD53hy6VdD z@eUZIui}~L%#SmajaRq1J|#> z4m=o$vZ*34=ZWK2!QMNEcp2Lbc5N1q!lEDq(bz0b;WI9;e>l=CG9^n#ro`w>_0F$Q zfZ={2QyTkfByC&gy;x!r*NyXXbk=a%~~(#K?< zTke0HuF5{Q+~?@!KDXR|g+43$+;ab`^flS%miup_0OUTm=nIc%d5nLP)i308PIjl_YMF6cpQ__6&$n6it8K- z8PIjl_YMF6cpQ_!r)L8IivW`WdK8mBs6PXdjR2DYdK8nCs73=4j{uVadK8oNjwX|E wpAeHLsTu^*Y>Trk?aBtSQ(D-o$(D8Px^?ZI-PUB? z*1fv!{YdHme3Fc8%cR@*@zc5A_nq&2=R47Hp@$-JF4Fz*;SLw5}K^y>s-s;V!}b2i=5=M- zComP?ju>8Fe@=H@rlwe1l`J*6BTTo`9b$zjQ@HxrAhp0D#u?M~TxGC_!?ccCHCjt| zF*PgJf@kJB`|Ml}cmsyrAjO#Kjr^E5p29w+#>$C`Q|54BoDv$fQ9D?3n32P9LPMIzu?LjNqggOH=1@T{9bMn*u8(GI z!;MLTtFPHal^S>VcJdiYqX0VU|Rn@A}C1xOlxCribxes0~+n2 z6qDaIA2$?e`opx3_KW!rAgbpzU)gFdjAKXh|5w``#F0R|c)Y)Du0_Ihhz^S?k^pk% zP>9|pIDx)xHH^_~+aA=^$M!<8K~Hy(71nJGf6`HnjtS=4X4=Hk^O71oNia2V{HUCC zoN3RSBS?mZCLw;l4W4a+D8qc)XJS`pUJ5X-f^1ytxwr`@si$lAE?{4G|o; zO0l>`rr?;~c;{ZEFJ!!3=7=FdGJ?Q^xfNQh4A?i;IJ4}B+A?4olTK(fN++3CRBP97 ze~lG9h%oegkn)lpW-4F8o2`*WW0mZHwHez`ko@>U1_;EC_6ig|Drn@=DMV9YEUSCa zIf$kHei3(u#zm9I!Jf(4t`Vm1lltJ&lVHy(eIXE8sy9sUpmz%I_gA#8x^Zv8%w?r2 z{GdkX1SkzRIr>prRK@rqn9j2wG|rUvf6PJbbin=yy-TAXrguvzN8jL$hUrIXzr^s5 zVM?H4;eM-QeRFr06@ifV(ocvk?_)~N@1c2ien56UjWXid6W%6ievIh)>dk|rIs##^kY67ib8Kw%#-oVFaXG7$ERyA9(NSJUvWiOA5H(!{uOpcW zg&-?iqPhds%3%tFspHDqqr;A!e@B#iPQjHd=c>N1LoOEGRehVoPOdxJ>b6>yc#o#+ zl8s8!(|NMeqjsy@0x{8^j0d00SqRZjp{Kj)&4UHYGxG+z9b-)72I*&J70?+8e?p_@ z=>-(>l6z5vYlP~<2%DU02b!mA{7mS)NS_eLe=t)sm&+Pmk?asOEKlkPQ)EUvvfC=;4M&*|I!w}(@V_)eUKLA_t^%`o z0PM9LV|UKTLnk|?M3u!|f2S0?UqZsEIH9*NJS-8lzu;A6-rr-ot=dg9SASoluZUkFH$7X; zP=?kYX!K?JL-b~<#7wU;b;eS)O;@?h%sPPk{4xEBxb{!sm0AY|f9cNvx6>$3F!*0c z75H=dy8JvTyO8}g1w{$9T$p~5en}AeSLoCF>_RT9YPMpChUjl310o*$QocjbH& zbnwg#gssR#jDVN{uEi3n(PZ%PFZ|6J2 z5_rBf0-u>e4sFe0*Km49ATi7>Kn0f9!uc|rRMR1Dtt6m1LW8^>qFlo}h$@br=Rmpi z;mI&>OF64Be{dVeHI8utrh)v^wsZ0jii%x8UgZ8TC%K~@I(4E};GFW&(;WVov}3%H zH;IhRkfD^(vt^DjZz(MyHLZxv8}qzPc(%itBkBwf_fC~sDBgh<3XAv5cxxfF3<2U! z03Xe&z`is!JDHbe;mNmfkH+_LFE*I2^mdL@7(@9DfAcP6O04V-ko;Rpgp<%Cj5r8Z zd0`sXoIjV$j)--;jA6Zy^D5&5v$o^>e%>Q?9GLm{i~p^lAn!%ZtF$I~>39XVZxk0b zROh^Bk9cE0AJBLozZIEmy7xG(yHWGztvfnr0(2ro1%>zsGMS^EMu+S$r=_;9 zWwZkgf7Q7`H9sLf2Go^Xy6&h~a&%s2_T@_Csf19MntF$aVFiFkvE3_hUg(B@&Xw@YJ zpL$wNYf78=0c@!QU6_a$>CPiXT7QAGDM}7Z(0z#_ZA=fmLUj{2z7@Ypo71UDy8GHr z-&TLKf6a5WCf@Adle3VglBt4>Z>;xF}}-S~B7<(%B;Y z0QR55{z-buw>8ilNM3u6I+D$S%?)(p>=eBx-HpvZj{7c*_?K=d()*7q?93us}1dq%FAFYLsW8ZTQ_XZLh`P2*6(NgS}qGcfGXVWpwsp#Rs}IuKbk*`2}&) zI^Vsk6S&Q4@oYS?dJ`NwMVBs6f57+RxdqVub#PvMu?$=^OJy5xEl0<5SLsSRy%%a0 zi}Y#1-F3m;Ieh#Y12UgW?-R)|eX>ZuF-2cc!1>~NS|XSF-6In>zBoZg+ml!6%fk7U zw0LHcz8VQk(jOJ+Yu)|^|15ufl$KQd_1eUZZzj`aC%umU6F1&D5XVWce_wAe(qCSZ zpX-QF4e{EmEVN9~6%bR5U*UT{eMHfcUo`jw*u?4r2s_$`}U{?NjvEm(u&<>B|%mq$Q3weshxk z76<``8vh{+nX`@9CB6IE&z)I%IFjR^LH{s1p|eppv=x za(g_jLU|xjWMAn-V7th$f({|LG8zzIE0g?cyW;%Dmtv%C+0@xVxPE^ zyZzi9P%JAD6ynwHptuzP`Kox7*9h7XSMonCalv;Md0i9Vb-c*!f0ubfk?&T&T}AHh z4m8Bz{JllKcdNg?D^%a5MFQ;#1z|*}H^qHLzW)L}wp?2tY7RejtSh8<;Zw)QGJYUm z|MbTxyj*McKlStlT9I5XlSWtQGN&-LTr2XyNU+`490rg?LYLMRnz-@oKqT1hpCGqP zyRXt4=_Woj$%n5ee<3zhLF>5>`?m9a#xQH+Jk_+|RM8Vi;2*XbK- zEL6sCpaGPzP>k8f4Kh|##_imt#zJMB;ir|JrMPGW`rityK1vHXMLy18%qmMQAm4WZ zP)i30KR&5vs15)C+8dM66&$k~i|ZT;KR&5vs15)C+8dJ(sAmGPijyIz6_bsqKLSFH zlOd=TljEpH0>h4zA*dCTK&emy#FCRCs1=i^sZ9bFmXjf<6_X39E(XY)00000#N437 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index dedd5d1e6..66cd5a0e4 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 0bedc6d839fe35676957f2ce6c1034b1def973bf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Sep 2024 04:12:53 +0000 Subject: [PATCH 022/186] Update junitJupiterVersion to v5.11.0 --- .../src/test/resources/calculator_groovy/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teamscale-gradle-plugin/src/test/resources/calculator_groovy/build.gradle b/teamscale-gradle-plugin/src/test/resources/calculator_groovy/build.gradle index c98b8c4e7..acb89d209 100644 --- a/teamscale-gradle-plugin/src/test/resources/calculator_groovy/build.gradle +++ b/teamscale-gradle-plugin/src/test/resources/calculator_groovy/build.gradle @@ -16,7 +16,7 @@ import com.teamscale.TestImpacted ext.junit4Version = '4.13.2' ext.junitVintageVersion = '5.10.3' ext.junitPlatformVersion = '1.4.0' -ext.junitJupiterVersion = '5.10.3' +ext.junitJupiterVersion = '5.11.0' if (!project.hasProperty("withoutServerConfig")) { teamscale { From 10737d4f2994fa121e747ddf3468b264409ca899 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Sep 2024 04:12:57 +0000 Subject: [PATCH 023/186] Update junitPlatform to v1.11.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5014ad113..34a20ac19 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ jacoco = "0.8.12" logback = "1.3.14" retrofit = "2.11.0" junit = "5.10.3" -junitPlatform = "1.10.3" +junitPlatform = "1.11.0" okhttp = "4.12.0" mockito = "4.11.0" picocli = "4.7.6" From 97805567e114de3614f1a468abbbc3ff1b7f3d47 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Sep 2024 04:44:17 +0000 Subject: [PATCH 024/186] Update junit to v5.11.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 34a20ac19..8fcaf58dc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ jacoco = "0.8.12" # We need to stay on the 1.3.x release line as 1.4.x requires Java 11 logback = "1.3.14" retrofit = "2.11.0" -junit = "5.10.3" +junit = "5.11.0" junitPlatform = "1.11.0" okhttp = "4.12.0" mockito = "4.11.0" From 452b0a4aed43e047a4e7b907ce951de950b6f1f0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Sep 2024 05:47:10 +0000 Subject: [PATCH 025/186] Update dependency org.apache.maven:maven-core to v3.9.9 --- teamscale-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teamscale-maven-plugin/pom.xml b/teamscale-maven-plugin/pom.xml index 387f14f38..582004820 100644 --- a/teamscale-maven-plugin/pom.xml +++ b/teamscale-maven-plugin/pom.xml @@ -88,7 +88,7 @@ org.apache.maven maven-core - 3.9.8 + 3.9.9 provided From ac481d6d83541c8336fb4522fcec971330762a77 Mon Sep 17 00:00:00 2001 From: Florian Dreier Date: Mon, 2 Sep 2024 07:46:13 +0200 Subject: [PATCH 026/186] Added logging --- .../testimpact/TestwiseCoverageAgentTest.java | 77 ++++++++++--------- .../jacoco/agent/util/TestUtils.java | 8 ++ 2 files changed, 47 insertions(+), 38 deletions(-) diff --git a/agent/src/test/java/com/teamscale/jacoco/agent/testimpact/TestwiseCoverageAgentTest.java b/agent/src/test/java/com/teamscale/jacoco/agent/testimpact/TestwiseCoverageAgentTest.java index 40df45d00..4eeb503d0 100644 --- a/agent/src/test/java/com/teamscale/jacoco/agent/testimpact/TestwiseCoverageAgentTest.java +++ b/agent/src/test/java/com/teamscale/jacoco/agent/testimpact/TestwiseCoverageAgentTest.java @@ -1,27 +1,5 @@ package com.teamscale.jacoco.agent.testimpact; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.matches; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.io.File; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.io.TempDir; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - import com.teamscale.client.ClusteredTestDetails; import com.teamscale.client.CommitDescriptor; import com.teamscale.client.EReportFormat; @@ -31,16 +9,21 @@ import com.teamscale.client.TeamscaleServer; import com.teamscale.jacoco.agent.options.AgentOptions; import com.teamscale.jacoco.agent.options.ETestwiseCoverageMode; +import com.teamscale.jacoco.agent.util.TestUtils; import com.teamscale.report.testwise.jacoco.JaCoCoTestwiseReportGenerator; import com.teamscale.report.testwise.model.ETestExecutionResult; import com.teamscale.tia.client.RunningTest; import com.teamscale.tia.client.TestRun; import com.teamscale.tia.client.TestRunWithClusteredSuggestions; import com.teamscale.tia.client.TiaAgent; - import okhttp3.HttpUrl; import okhttp3.MediaType; import okhttp3.ResponseBody; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import retrofit2.Call; import retrofit2.Response; import retrofit2.Retrofit; @@ -48,6 +31,21 @@ import retrofit2.http.POST; import retrofit2.http.Query; +import java.io.File; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.matches; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + @ExtendWith(MockitoExtension.class) public class TestwiseCoverageAgentTest { private static final String FORBIDDEN_MESSAGE_PREFIX = "HTTP Status Code: 403 Forbidden\nMessage: "; @@ -63,12 +61,6 @@ public class TestwiseCoverageAgentTest { @TempDir File tempDir; - /** - * Ensures that each test case gets it's own port number, so each tested instance of the agent runs it's REST API on - * a separate port. - */ - private static final AtomicInteger PORT_COUNTER = new AtomicInteger(54321); - @Test public void testAccessViaTiaClientAndReportUploadToTeamscale() throws Exception { List availableTests = Arrays @@ -83,10 +75,13 @@ public void testAccessViaTiaClientAndReportUploadToTeamscale() throws Exception when(reportGenerator.convert(any(File.class))) .thenReturn(CoverageToTeamscaleStrategyTest.getDummyTestwiseCoverage("test2")); - int port = PORT_COUNTER.incrementAndGet(); - AgentOptions options = mockOptions(port); - when(options.createNewFileInOutputDirectory(any(), any())).thenReturn(new File(tempDir, "test")); - new TestwiseCoverageAgent(options, null, reportGenerator); + int port; + synchronized (TestUtils.class) { + port = TestUtils.getFreePort(); + AgentOptions options = mockOptions(port); + when(options.createNewFileInOutputDirectory(any(), any())).thenReturn(new File(tempDir, "test")); + new TestwiseCoverageAgent(options, null, reportGenerator); + } TiaAgent agent = new TiaAgent(false, HttpUrl.get("http://localhost:" + port)); @@ -111,9 +106,12 @@ public void testErrorHandling() throws Exception { .thenReturn(Response.error(403, ResponseBody.create(FORBIDDEN_MESSAGE_PREFIX + MISSING_VIEW_PERMISSIONS, PLAIN_TEXT))); - int port = PORT_COUNTER.incrementAndGet(); - AgentOptions options = mockOptions(port); - new TestwiseCoverageAgent(options, null, reportGenerator); + int port; + synchronized (TestUtils.class) { + port = TestUtils.getFreePort(); + AgentOptions options = mockOptions(port); + new TestwiseCoverageAgent(options, null, reportGenerator); + } TiaAgent agent = new TiaAgent(false, HttpUrl.get("http://localhost:" + port)); assertThatCode(agent::startTestRunAssumingUnchangedTests).hasMessageContaining(MISSING_VIEW_PERMISSIONS); @@ -141,8 +139,11 @@ public void shouldHandleMissingRequestBodyForTestrunStartGracefully() throws Exc when(client.getImpactedTests(any(), any(), any(), any(), any(), any(), any(), anyBoolean(), anyBoolean(), anyBoolean())) .thenReturn(Response.success(impactedClusters)); - int port = PORT_COUNTER.incrementAndGet(); - new TestwiseCoverageAgent(mockOptions(port), null, reportGenerator); + int port; + synchronized (TestUtils.class) { + port = TestUtils.getFreePort(); + new TestwiseCoverageAgent(mockOptions(port), null, reportGenerator); + } ITestwiseCoverageAgentApiWithoutBody api = new Retrofit.Builder() .addConverterFactory(JacksonConverterFactory.create()) diff --git a/agent/src/test/java/com/teamscale/jacoco/agent/util/TestUtils.java b/agent/src/test/java/com/teamscale/jacoco/agent/util/TestUtils.java index 7c360b2c8..d20c47386 100644 --- a/agent/src/test/java/com/teamscale/jacoco/agent/util/TestUtils.java +++ b/agent/src/test/java/com/teamscale/jacoco/agent/util/TestUtils.java @@ -1,6 +1,7 @@ package com.teamscale.jacoco.agent.util; import java.io.IOException; +import java.net.ServerSocket; import java.nio.file.Files; import java.nio.file.Path; import java.util.stream.Stream; @@ -25,4 +26,11 @@ public static void cleanAgentCoverageDirectory() throws IOException { } } + /** Returns a new free TCP port number */ + public static int getFreePort() throws IOException { + try (ServerSocket socket = new ServerSocket(0)) { + socket.setReuseAddress(true); + return socket.getLocalPort(); + } + } } From 8c1851bb0d851b2ac1c0bd02f9fda80f1d39a830 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Sep 2024 07:20:14 +0000 Subject: [PATCH 027/186] Update dependency org.springframework.boot:spring-boot-loader to v3.3.3 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8fcaf58dc..8a3d42e2a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -76,7 +76,7 @@ jsonassert = { module = "org.skyscreamer:jsonassert", version = "1.5.3" } mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" } mockito-junit = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" } -springboot-loader = { module = "org.springframework.boot:spring-boot-loader", version = "3.3.2" } +springboot-loader = { module = "org.springframework.boot:spring-boot-loader", version = "3.3.3" } [plugins] versions = { id = "com.github.ben-manes.versions", version = "0.51.0" } From afc834c8ac8e937303a883d55f2500805fd58276 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Sep 2024 07:20:19 +0000 Subject: [PATCH 028/186] Update plugin pluginPublish to v1.2.2 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8fcaf58dc..840d6d23b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -82,5 +82,5 @@ springboot-loader = { module = "org.springframework.boot:spring-boot-loader", ve versions = { id = "com.github.ben-manes.versions", version = "0.51.0" } markdownToPdf = { id = "de.fntsoftware.gradle.markdown-to-pdf", version = "1.1.0" } nexusPublish = { id = "io.github.gradle-nexus.publish-plugin", version = "2.0.0" } -pluginPublish = { id = "com.gradle.plugin-publish", version = "1.2.1" } +pluginPublish = { id = "com.gradle.plugin-publish", version = "1.2.2" } gitProperties = { id = "com.gorylenko.gradle-git-properties", version = "2.4.2" } From ed9ac72c36bc58d4ef1da181fc095fa0dc5a852b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Sep 2024 07:23:59 +0000 Subject: [PATCH 029/186] Update dependency org.slf4j:slf4j-api to v2.0.16 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8fcaf58dc..7b037c346 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -52,7 +52,7 @@ teamscaleLibCommons = { module = "com.teamscale:teamscale-lib-commons", version commonsCodec = { module = "commons-codec:commons-codec", version = "1.17.1" } commonsLang = { module = "org.apache.commons:commons-lang3", version = "3.16.0" } commonsIo = { module = "commons-io:commons-io", version = "2.16.1" } -slf4j-api = { module = "org.slf4j:slf4j-api", version = "2.0.15" } +slf4j-api = { module = "org.slf4j:slf4j-api", version = "2.0.16" } jgit = { module = "org.eclipse.jgit:org.eclipse.jgit", version = "6.10.0.202406032230-r" } okio = { module = "com.squareup.okio:okio", version = "3.9.0" } From a9bc0ef748d098be76469b4b95796c3df6cb40ac Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Sep 2024 07:25:31 +0000 Subject: [PATCH 030/186] Update dependency org.apache.maven:maven-plugin-api to v3.9.9 --- teamscale-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teamscale-maven-plugin/pom.xml b/teamscale-maven-plugin/pom.xml index 582004820..f3e1ccd3e 100644 --- a/teamscale-maven-plugin/pom.xml +++ b/teamscale-maven-plugin/pom.xml @@ -76,7 +76,7 @@ org.apache.maven maven-plugin-api - 3.9.8 + 3.9.9 provided From 8078ceb7a6f39414a65988a88b4e3e80ff09e34e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Sep 2024 08:38:12 +0000 Subject: [PATCH 031/186] Update dependency org.apache.commons:commons-lang3 to v3.17.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 274d2df15..53abe336e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -50,7 +50,7 @@ spark = { module = "com.sparkjava:spark-core", version = "2.9.4" } jcommander = { module = "com.beust:jcommander", version = "1.82" } teamscaleLibCommons = { module = "com.teamscale:teamscale-lib-commons", version = "9.4.1" } commonsCodec = { module = "commons-codec:commons-codec", version = "1.17.1" } -commonsLang = { module = "org.apache.commons:commons-lang3", version = "3.16.0" } +commonsLang = { module = "org.apache.commons:commons-lang3", version = "3.17.0" } commonsIo = { module = "commons-io:commons-io", version = "2.16.1" } slf4j-api = { module = "org.slf4j:slf4j-api", version = "2.0.15" } jgit = { module = "org.eclipse.jgit:org.eclipse.jgit", version = "6.10.0.202406032230-r" } From 80458ad5edd78f9d3d873ae35ebdab03a8c37ab7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Sep 2024 08:38:17 +0000 Subject: [PATCH 032/186] Update dependency org.apache.maven.plugin-tools:maven-plugin-annotations to v3.15.0 --- teamscale-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teamscale-maven-plugin/pom.xml b/teamscale-maven-plugin/pom.xml index f3e1ccd3e..c35a1cacc 100644 --- a/teamscale-maven-plugin/pom.xml +++ b/teamscale-maven-plugin/pom.xml @@ -82,7 +82,7 @@ org.apache.maven.plugin-tools maven-plugin-annotations - 3.13.1 + 3.15.0 provided From 1a4da8b5a5ae47dd5f5aa2b3b7fec71982c6b795 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Sep 2024 11:36:53 +0000 Subject: [PATCH 033/186] Update dependency org.apache.maven.plugins:maven-javadoc-plugin to v3.10.0 --- teamscale-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teamscale-maven-plugin/pom.xml b/teamscale-maven-plugin/pom.xml index c35a1cacc..15137c43f 100644 --- a/teamscale-maven-plugin/pom.xml +++ b/teamscale-maven-plugin/pom.xml @@ -158,7 +158,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.8.0 + 3.10.0 attach-javadocs From e1510afa8e444441f080572b457a38ddac03dcca Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Sep 2024 11:36:58 +0000 Subject: [PATCH 034/186] Update dependency org.apache.maven.plugins:maven-plugin-plugin to v3.15.0 --- teamscale-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teamscale-maven-plugin/pom.xml b/teamscale-maven-plugin/pom.xml index c35a1cacc..bf8e8fa09 100644 --- a/teamscale-maven-plugin/pom.xml +++ b/teamscale-maven-plugin/pom.xml @@ -127,7 +127,7 @@ org.apache.maven.plugins maven-plugin-plugin - 3.13.1 + 3.15.0 generate-helpmojo From 95f316daf9de3d91ab730a731ebd1a17f33e395a Mon Sep 17 00:00:00 2001 From: Florian Dreier Date: Mon, 2 Sep 2024 13:47:52 +0200 Subject: [PATCH 035/186] Adjust Test to reverted log4j behavior --- .../src/test/java/com/teamscale/client/SutUsesLogbackTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system-tests/sut-uses-logback-test/src/test/java/com/teamscale/client/SutUsesLogbackTest.java b/system-tests/sut-uses-logback-test/src/test/java/com/teamscale/client/SutUsesLogbackTest.java index 418520d85..cd23dd858 100644 --- a/system-tests/sut-uses-logback-test/src/test/java/com/teamscale/client/SutUsesLogbackTest.java +++ b/system-tests/sut-uses-logback-test/src/test/java/com/teamscale/client/SutUsesLogbackTest.java @@ -19,7 +19,7 @@ public void systemTest() throws Exception { assertThat(result.getStdout()).contains("This warning is to test logging in the SUT"); assertThat(result.getStdout()).doesNotContainIgnoringCase("error"); - assertThat(result.getStderr()).isEqualToNormalizingNewlines("SLF4J(I): Connected with provider of type [shadow.ch.qos.logback.classic.spi.LogbackServiceProvider]\n"); + assertThat(result.getStderr()).isEmpty(); Path appLogFile = Paths.get("logTest/app.log"); assertThat(appLogFile).exists(); From a0f1680f500db89a611f4275b96256ba41c3ab6c Mon Sep 17 00:00:00 2001 From: Florian Dreier Date: Mon, 2 Sep 2024 13:48:20 +0200 Subject: [PATCH 036/186] Switch from io.github.goooler.shadow to the new official com.gradleup.shadow --- buildSrc/build.gradle.kts | 2 +- .../kotlin/com.teamscale.shadow-convention.gradle.kts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index d6904def8..5bc5ef10f 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -11,7 +11,7 @@ repositories { } dependencies { - implementation("io.github.goooler.shadow:shadow-gradle-plugin:8.1.8") + implementation("com.gradleup.shadow:shadow-gradle-plugin:8.3.0") implementation("com.xpdustry.ksr:com.xpdustry.ksr.gradle.plugin:1.0.0") { exclude(group = "com.github.johnrengelman") } diff --git a/buildSrc/src/main/kotlin/com.teamscale.shadow-convention.gradle.kts b/buildSrc/src/main/kotlin/com.teamscale.shadow-convention.gradle.kts index 7370e8465..3ebff6747 100644 --- a/buildSrc/src/main/kotlin/com.teamscale.shadow-convention.gradle.kts +++ b/buildSrc/src/main/kotlin/com.teamscale.shadow-convention.gradle.kts @@ -3,21 +3,21 @@ import com.xpdustry.ksr.kotlinRelocate plugins { java - // https://github.com/Goooler/shadow - id("io.github.goooler.shadow") + // https://github.com/GradleUp/shadow + id("com.gradleup.shadow") // https://github.com/xpdustry/kotlin-shadow-relocator id("com.xpdustry.ksr") } tasks.named("shadowJar") { isEnableRelocation = project.properties["debug"] !== "true" - // Needed as a workaround for https://github.com/johnrengelman/shadow/issues/521 + // Needed as a workaround for https://github.com/GradleUp/shadow/issues/521 inputs.property("relocation-enabled", isEnableRelocation) archiveClassifier.set(null as String?) mergeServiceFiles() manifest { // The jaxb library, which we are shading is a multi release jar, so we have to explicitly "inherit" this attribute - // https://github.com/johnrengelman/shadow/issues/449 + // https://github.com/GradleUp/shadow/issues/449 attributes["Multi-Release"] = "true" } // Relocates the .kotlin_metadata files to ensure reflection in Kotlin does not break @@ -31,7 +31,7 @@ tasks.named("shadowJar") { // Defer the resolution of 'runtimeClasspath'. This is an issue in the shadow // plugin that it automatically accesses the files in 'runtimeClasspath' while // Gradle is building the task graph. The lines below work around that. -// https://github.com/johnrengelman/shadow/issues/882 +// https://github.com/GradleUp/shadow/issues/882 tasks.withType { dependsOn(tasks.jar) inputs.files(project.configurations.runtimeClasspath) From fff179edc11a694093ea6b366b09efb56a9bd80f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Sep 2024 12:09:13 +0000 Subject: [PATCH 037/186] Update dependency org.apache.maven.plugins:maven-surefire-plugin to v3.5.0 --- teamscale-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teamscale-maven-plugin/pom.xml b/teamscale-maven-plugin/pom.xml index 15137c43f..fc5022114 100644 --- a/teamscale-maven-plugin/pom.xml +++ b/teamscale-maven-plugin/pom.xml @@ -140,7 +140,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.3.1 + 3.5.0 org.apache.maven.plugins From 66e5f7229eeebeacfc65adca3f9c16056223a7ae Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Sep 2024 12:09:17 +0000 Subject: [PATCH 038/186] Update jersey to v2.45 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8a23fb3e4..a2b7165d1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] jetty = "9.4.55.v20240627" -jersey = "2.44" +jersey = "2.45" jackson = "2.17.2" # When upgrading JaCoCo to a newer version make sure to # check the comment in the OpenAnalyzer.java, JaCoCoPreMain.java and CachingInstructionsBuilder.java From 07089ad94694628125d9bd67930407255764e194 Mon Sep 17 00:00:00 2001 From: Max Schallermayer Date: Tue, 3 Sep 2024 13:58:37 +0200 Subject: [PATCH 039/186] TS-40158 Add missing agent-jar plugin --- sample-debugging-app/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/sample-debugging-app/build.gradle.kts b/sample-debugging-app/build.gradle.kts index d4752cbbb..c59e727da 100644 --- a/sample-debugging-app/build.gradle.kts +++ b/sample-debugging-app/build.gradle.kts @@ -1,6 +1,7 @@ plugins { com.teamscale.`java-convention` application + com.teamscale.`agent-jar` } application { From bc9772ae4707160017638889ea0fc6b048c87131 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 3 Sep 2024 19:39:34 +0000 Subject: [PATCH 040/186] Update jetty to v9.4.56.v20240826 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a2b7165d1..306b79a6c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -jetty = "9.4.55.v20240627" +jetty = "9.4.56.v20240826" jersey = "2.45" jackson = "2.17.2" # When upgrading JaCoCo to a newer version make sure to From 5b74f3e7c863b323dcbea1d5ce19df90f2cfb968 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 11 Sep 2024 01:15:48 +0000 Subject: [PATCH 041/186] Update dependency org.eclipse.jgit:org.eclipse.jgit to v7 --- gradle/libs.versions.toml | 2 +- teamscale-maven-plugin/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 306b79a6c..0d9d8ed12 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -53,7 +53,7 @@ commonsCodec = { module = "commons-codec:commons-codec", version = "1.17.1" } commonsLang = { module = "org.apache.commons:commons-lang3", version = "3.17.0" } commonsIo = { module = "commons-io:commons-io", version = "2.16.1" } slf4j-api = { module = "org.slf4j:slf4j-api", version = "2.0.16" } -jgit = { module = "org.eclipse.jgit:org.eclipse.jgit", version = "6.10.0.202406032230-r" } +jgit = { module = "org.eclipse.jgit:org.eclipse.jgit", version = "7.0.0.202409031743-r" } okio = { module = "com.squareup.okio:okio", version = "3.9.0" } picocli-core = { module = "info.picocli:picocli", version.ref = "picocli" } diff --git a/teamscale-maven-plugin/pom.xml b/teamscale-maven-plugin/pom.xml index 199f7fc2d..308cbe81c 100644 --- a/teamscale-maven-plugin/pom.xml +++ b/teamscale-maven-plugin/pom.xml @@ -94,7 +94,7 @@ org.eclipse.jgit org.eclipse.jgit - 6.10.0.202406032230-r + 7.0.0.202409031743-r com.teamscale From a1cafc82617c2071e6747d1aec17be1e8c43b115 Mon Sep 17 00:00:00 2001 From: Florian Dreier Date: Wed, 11 Sep 2024 07:17:57 +0200 Subject: [PATCH 042/186] Deflicker testNoMultipleUploadsToSameProjectAndRevision --- .../GitMultiProjectPropertiesLocatorTest.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/agent/src/test/java/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitMultiProjectPropertiesLocatorTest.java b/agent/src/test/java/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitMultiProjectPropertiesLocatorTest.java index 5a880e9a1..271a47948 100644 --- a/agent/src/test/java/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitMultiProjectPropertiesLocatorTest.java +++ b/agent/src/test/java/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitMultiProjectPropertiesLocatorTest.java @@ -47,13 +47,11 @@ void testNoMultipleUploadsToSameProjectAndRevision() { locator.searchFile(jarFile, false); List teamscaleServers = delayedTeamscaleMultiProjectUploader.getTeamscaleUploaders().stream() .map(TeamscaleUploader::getTeamscaleServer).collect(Collectors.toList()); - assertThat(teamscaleServers.size()).isEqualTo(2); - assertThat(teamscaleServers.get(0).project).isEqualTo("demo2"); - assertThat(teamscaleServers.get(0).commit).isEqualTo( - new CommitDescriptor("master", "1645713803000")); - assertThat(teamscaleServers.get(1).project).isEqualTo("demolib"); - assertThat(teamscaleServers.get(1).revision).isEqualTo( - "05b9d066a0c0762be622987de403b5752fa01cc0"); + assertThat(teamscaleServers).hasSize(2); + assertThat(teamscaleServers).anyMatch(server -> server.project.equals("demo2") && server.commit.equals( + new CommitDescriptor("master", "1645713803000"))); + assertThat(teamscaleServers).anyMatch(server -> server.project.equals("demolib") && server.revision.equals( + "05b9d066a0c0762be622987de403b5752fa01cc0")); } -} \ No newline at end of file +} From fde1daf6026bc7254991de58d5fc94ae28079bb3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Sep 2024 13:40:32 +0000 Subject: [PATCH 043/186] Update dependency com.gradleup.shadow:shadow-gradle-plugin to v8.3.1 --- buildSrc/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 5bc5ef10f..5a8fd8776 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -11,7 +11,7 @@ repositories { } dependencies { - implementation("com.gradleup.shadow:shadow-gradle-plugin:8.3.0") + implementation("com.gradleup.shadow:shadow-gradle-plugin:8.3.1") implementation("com.xpdustry.ksr:com.xpdustry.ksr.gradle.plugin:1.0.0") { exclude(group = "com.github.johnrengelman") } From 21a93ed69c73ea4e1a9346133091c30ea7d20fcc Mon Sep 17 00:00:00 2001 From: Florian Dreier Date: Tue, 10 Sep 2024 16:10:30 +0200 Subject: [PATCH 044/186] Use correct import --- teamscale-maven-plugin/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teamscale-maven-plugin/build.gradle.kts b/teamscale-maven-plugin/build.gradle.kts index f4f2f2fbb..e24a2d461 100644 --- a/teamscale-maven-plugin/build.gradle.kts +++ b/teamscale-maven-plugin/build.gradle.kts @@ -1,4 +1,4 @@ -import org.codehaus.plexus.util.Os +import org.apache.tools.ant.taskdefs.condition.Os abstract class MavenExec : Exec() { @TaskAction From b2a772700d6c6a4fa67e499b3b39b8ff9e1fe3bf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 11 Sep 2024 05:40:45 +0000 Subject: [PATCH 045/186] Update dependency org.apache.logging.log4j:log4j-core to v2.24.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0d9d8ed12..c14b40e36 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -60,7 +60,7 @@ picocli-core = { module = "info.picocli:picocli", version.ref = "picocli" } picocli-codegen = { module = "info.picocli:picocli-codegen", version.ref = "picocli" } jna-platform = { module = "net.java.dev.jna:jna-platform", version = "5.14.0" } -log4j-core = { module = "org.apache.logging.log4j:log4j-core", version = "2.23.1" } +log4j-core = { module = "org.apache.logging.log4j:log4j-core", version = "2.24.0" } junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } From 594482e4779bd7708bc12ac4235b53800da7825e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 11 Sep 2024 10:14:19 +0000 Subject: [PATCH 046/186] Update plugin pluginPublish to v1.3.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c14b40e36..1a5eb8360 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -82,5 +82,5 @@ springboot-loader = { module = "org.springframework.boot:spring-boot-loader", ve versions = { id = "com.github.ben-manes.versions", version = "0.51.0" } markdownToPdf = { id = "de.fntsoftware.gradle.markdown-to-pdf", version = "1.1.0" } nexusPublish = { id = "io.github.gradle-nexus.publish-plugin", version = "2.0.0" } -pluginPublish = { id = "com.gradle.plugin-publish", version = "1.2.2" } +pluginPublish = { id = "com.gradle.plugin-publish", version = "1.3.0" } gitProperties = { id = "com.gorylenko.gradle-git-properties", version = "2.4.2" } From fcebe97fa91011944d8264d5554481713472dd91 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Sep 2024 20:31:45 +0000 Subject: [PATCH 047/186] Update dependency com.squareup.okio:okio to v3.9.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1a5eb8360..2713e0572 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -54,7 +54,7 @@ commonsLang = { module = "org.apache.commons:commons-lang3", version = "3.17.0" commonsIo = { module = "commons-io:commons-io", version = "2.16.1" } slf4j-api = { module = "org.slf4j:slf4j-api", version = "2.0.16" } jgit = { module = "org.eclipse.jgit:org.eclipse.jgit", version = "7.0.0.202409031743-r" } -okio = { module = "com.squareup.okio:okio", version = "3.9.0" } +okio = { module = "com.squareup.okio:okio", version = "3.9.1" } picocli-core = { module = "info.picocli:picocli", version.ref = "picocli" } picocli-codegen = { module = "info.picocli:picocli-codegen", version.ref = "picocli" } From 4df55d41646a814e629a8702a6d80af49978f94e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 14 Sep 2024 18:12:46 +0000 Subject: [PATCH 048/186] Update dependency org.apache.maven.plugins:maven-gpg-plugin to v3.2.6 --- teamscale-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teamscale-maven-plugin/pom.xml b/teamscale-maven-plugin/pom.xml index 308cbe81c..b5bf203c2 100644 --- a/teamscale-maven-plugin/pom.xml +++ b/teamscale-maven-plugin/pom.xml @@ -184,7 +184,7 @@ org.apache.maven.plugins maven-gpg-plugin - 3.2.5 + 3.2.6 sign-artifacts From beb98e8cb27b91a8a2c743c42ebcc21223530315 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 15 Sep 2024 18:09:55 +0000 Subject: [PATCH 049/186] Update dependency net.java.dev.jna:jna-platform to v5.15.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2713e0572..e621e69d5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -58,7 +58,7 @@ okio = { module = "com.squareup.okio:okio", version = "3.9.1" } picocli-core = { module = "info.picocli:picocli", version.ref = "picocli" } picocli-codegen = { module = "info.picocli:picocli-codegen", version.ref = "picocli" } -jna-platform = { module = "net.java.dev.jna:jna-platform", version = "5.14.0" } +jna-platform = { module = "net.java.dev.jna:jna-platform", version = "5.15.0" } log4j-core = { module = "org.apache.logging.log4j:log4j-core", version = "2.24.0" } From ef09ca8222e2a51964e156b954c7e836b0065c2b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 19 Sep 2024 01:27:02 +0000 Subject: [PATCH 050/186] Update dependency commons-io:commons-io to v2.17.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e621e69d5..1d8f006e2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -51,7 +51,7 @@ jcommander = { module = "com.beust:jcommander", version = "1.82" } teamscaleLibCommons = { module = "com.teamscale:teamscale-lib-commons", version = "9.4.1" } commonsCodec = { module = "commons-codec:commons-codec", version = "1.17.1" } commonsLang = { module = "org.apache.commons:commons-lang3", version = "3.17.0" } -commonsIo = { module = "commons-io:commons-io", version = "2.16.1" } +commonsIo = { module = "commons-io:commons-io", version = "2.17.0" } slf4j-api = { module = "org.slf4j:slf4j-api", version = "2.0.16" } jgit = { module = "org.eclipse.jgit:org.eclipse.jgit", version = "7.0.0.202409031743-r" } okio = { module = "com.squareup.okio:okio", version = "3.9.1" } From 16af504fb49c856730f3ecf447719209ff650665 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 19 Sep 2024 04:35:32 +0000 Subject: [PATCH 051/186] Update dependency com.gradleup.shadow:shadow-gradle-plugin to v8.3.2 --- buildSrc/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 5a8fd8776..45b5d4c52 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -11,7 +11,7 @@ repositories { } dependencies { - implementation("com.gradleup.shadow:shadow-gradle-plugin:8.3.1") + implementation("com.gradleup.shadow:shadow-gradle-plugin:8.3.2") implementation("com.xpdustry.ksr:com.xpdustry.ksr.gradle.plugin:1.0.0") { exclude(group = "com.github.johnrengelman") } From c5872a5f5d989a47befb020bffefd17a6742e46d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 19 Sep 2024 11:37:06 +0000 Subject: [PATCH 052/186] Update dependency org.springframework.boot:spring-boot-loader to v3.3.4 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1d8f006e2..37a8dd518 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -76,7 +76,7 @@ jsonassert = { module = "org.skyscreamer:jsonassert", version = "1.5.3" } mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" } mockito-junit = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" } -springboot-loader = { module = "org.springframework.boot:spring-boot-loader", version = "3.3.3" } +springboot-loader = { module = "org.springframework.boot:spring-boot-loader", version = "3.3.4" } [plugins] versions = { id = "com.github.ben-manes.versions", version = "0.51.0" } From 7ad3bb1047ce19093c80b02346aff02f945aafd2 Mon Sep 17 00:00:00 2001 From: Jonas Bogenberger Date: Mon, 23 Sep 2024 13:30:02 +0200 Subject: [PATCH 053/186] TS-40412 add TeamscaleProxyOptions --- .../jacoco/agent/options/AgentOptions.java | 10 ++-- .../agent/options/AgentOptionsParser.java | 6 +-- .../agent/options/TeamscaleProxyOptions.java | 47 +++++++++++++++++++ 3 files changed, 55 insertions(+), 8 deletions(-) create mode 100644 agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptions.java b/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptions.java index f2855083b..cd397cbf8 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptions.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptions.java @@ -107,10 +107,10 @@ public class AgentOptions { * The directory to write the XML traces to. */ private Path outputDirectory; - /** - * A path to the file that contains the password for the proxy authentication. - */ - /* package */ Path proxyPasswordPath; + + /** Contains the options related to teamscale-specific proxy settings. */ + /* package */ TeamscaleProxyOptions teamscaleProxyOptions = new TeamscaleProxyOptions(); + /** * Additional metadata files to upload together with the coverage XML. */ @@ -230,7 +230,7 @@ public String getOriginalOptionsString() { } public Path getProxyPasswordPath() { - return proxyPasswordPath; + return teamscaleProxyOptions.proxyPasswordPath; } /** diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptionsParser.java b/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptionsParser.java index a91a626ad..9661df96c 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptionsParser.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptionsParser.java @@ -156,6 +156,9 @@ private void handleOption(AgentOptions options, value)) { return; } + if (key.startsWith("proxy-") && TeamscaleProxyOptions.handleTeamscaleProxyOptions(options.teamscaleProxyOptions, key, value, filePatternResolver)) { + return; + } if (handleAgentOptions(options, key, value)) { return; } @@ -207,9 +210,6 @@ private boolean handleAgentOptions(AgentOptions options, String key, String valu case LOGGING_CONFIG_OPTION: options.loggingConfig = filePatternResolver.parsePath(key, value); return true; - case "proxy-password-file": - options.proxyPasswordPath = filePatternResolver.parsePath(key, value); - return true; case "interval": options.dumpIntervalInMinutes = parseInt(key, value); return true; diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java b/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java new file mode 100644 index 000000000..50b148181 --- /dev/null +++ b/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java @@ -0,0 +1,47 @@ +package com.teamscale.jacoco.agent.options; + +import java.nio.file.Path; + +/** + * Parses agent command line options related to the proxy settings. + */ +public class TeamscaleProxyOptions { + + /** The host of the proxy server. */ + /* package */ String proxyHost; + + /** The port of the proxy server. */ + /* package */ int proxyPort; + + /** The password for the proxy user. */ + /* package */ String proxyPassword; + + /** A path to the file that contains the password for the proxy authentication. */ + /* package */ Path proxyPasswordPath; + + /** The username of the proxy user. */ + /* package */ String proxyUser; + + + public static boolean handleTeamscaleProxyOptions(TeamscaleProxyOptions options, String key, String value, FilePatternResolver filePatternResolver) throws AgentOptionParseException { + switch (key) { + case "proxy-password-file": + options.proxyPasswordPath = filePatternResolver.parsePath(key, value); + return true; + case "proxy-host": + options.proxyHost = value; + return true; + case "proxy-port": + options.proxyPort = Integer.parseInt(value); + return true; + case "proxy-user": + options.proxyUser = value; + return true; + case "proxy-password": + options.proxyPassword = value; + return true; + default: + return false; + } + } +} From ee3a9c024dde6de93f452a33fa131e1a3bf35f9b Mon Sep 17 00:00:00 2001 From: Jonas Bogenberger Date: Mon, 23 Sep 2024 15:25:21 +0200 Subject: [PATCH 054/186] TS-40412 put TeamscaleProxyOptions into system properties and use them. --- .../com/teamscale/jacoco/agent/AgentBase.java | 4 ++ .../jacoco/agent/options/AgentOptions.java | 5 ++ .../agent/options/TeamscaleProxyOptions.java | 17 +++++++ .../java/com/teamscale/client/HttpUtils.java | 36 +++++++++----- .../client/ProxySystemProperties.java | 30 ++++++++---- .../TeamscaleProxySystemProperties.java | 49 +++++++++++++++++++ 6 files changed, 119 insertions(+), 22 deletions(-) create mode 100644 teamscale-client/src/main/java/com/teamscale/client/TeamscaleProxySystemProperties.java diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java b/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java index 5568649cb..1a5b44f18 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java @@ -1,6 +1,7 @@ package com.teamscale.jacoco.agent; import com.teamscale.client.ProxySystemProperties; +import com.teamscale.client.TeamscaleProxySystemProperties; import com.teamscale.jacoco.agent.options.AgentOptions; import com.teamscale.jacoco.agent.util.LoggingUtils; import org.conqat.lib.commons.filesystem.FileSystemUtils; @@ -40,6 +41,7 @@ public abstract class AgentBase { /** Constructor. */ public AgentBase(AgentOptions options) throws IllegalStateException { this.options = options; + options.getTeamscaleProxyOptions().putTeamscaleProxyOptionsIntoSystemProperties(); setProxyPasswordFromFile(options.getProxyPasswordPath()); try { controller = new JacocoRuntimeController(RT.getAgent()); @@ -69,6 +71,8 @@ private void setProxyPasswordFromFile(Path proxyPasswordFilePath) { String proxyPassword = FileSystemUtils.readFileUTF8(proxyPasswordFilePath.toFile()).trim(); new ProxySystemProperties(ProxySystemProperties.Protocol.HTTP).setProxyPassword(proxyPassword); new ProxySystemProperties(ProxySystemProperties.Protocol.HTTPS).setProxyPassword(proxyPassword); + new TeamscaleProxySystemProperties(ProxySystemProperties.Protocol.HTTP).setProxyPassword(proxyPassword); + new TeamscaleProxySystemProperties(ProxySystemProperties.Protocol.HTTPS).setProxyPassword(proxyPassword); } catch (IOException e) { logger.error( "Unable to open file containing proxy password. Please make sure the file exists and the user has the permissions to read the file.", diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptions.java b/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptions.java index cd397cbf8..ec5c84b2c 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptions.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptions.java @@ -724,4 +724,9 @@ public ETestwiseCoverageMode getTestwiseCoverageMode() { public boolean shouldIgnoreUncoveredClasses() { return ignoreUncoveredClasses; } + + /** @see #teamscaleProxyOptions */ + public TeamscaleProxyOptions getTeamscaleProxyOptions() { + return teamscaleProxyOptions; + } } diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java b/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java index 50b148181..1765509ec 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java @@ -1,5 +1,8 @@ package com.teamscale.jacoco.agent.options; +import com.teamscale.client.ProxySystemProperties; +import com.teamscale.client.TeamscaleProxySystemProperties; + import java.nio.file.Path; /** @@ -44,4 +47,18 @@ public static boolean handleTeamscaleProxyOptions(TeamscaleProxyOptions options, return false; } } + + /** Stores the teamscale-specific proxy settings as system properties to make them always available. */ + public void putTeamscaleProxyOptionsIntoSystemProperties() { + putTeamscaleProxyOptionsIntoSystemPropertiesForProtocol(ProxySystemProperties.Protocol.HTTP); + putTeamscaleProxyOptionsIntoSystemPropertiesForProtocol(ProxySystemProperties.Protocol.HTTPS); + } + + private void putTeamscaleProxyOptionsIntoSystemPropertiesForProtocol(ProxySystemProperties.Protocol protocol) { + new TeamscaleProxySystemProperties(protocol).setProxyHost(proxyHost); + new TeamscaleProxySystemProperties(protocol).setProxyPort(proxyPort); + new TeamscaleProxySystemProperties(protocol).setProxyUser(proxyUser); + new TeamscaleProxySystemProperties(protocol).setProxyPassword(proxyPassword); + + } } diff --git a/teamscale-client/src/main/java/com/teamscale/client/HttpUtils.java b/teamscale-client/src/main/java/com/teamscale/client/HttpUtils.java index fbe0f1a50..68e1e6a80 100644 --- a/teamscale-client/src/main/java/com/teamscale/client/HttpUtils.java +++ b/teamscale-client/src/main/java/com/teamscale/client/HttpUtils.java @@ -102,24 +102,34 @@ private static void setUpProxyServer(OkHttpClient.Builder httpClientBuilder) { private static boolean setUpProxyServerForProtocol(ProxySystemProperties.Protocol protocol, OkHttpClient.Builder httpClientBuilder) { + TeamscaleProxySystemProperties teamscaleProxySystemProperties = new TeamscaleProxySystemProperties(protocol); + ProxySystemProperties defaultProxySystemProperties = new ProxySystemProperties(protocol); - ProxySystemProperties proxySystemProperties = new ProxySystemProperties(protocol); - String proxyHost = proxySystemProperties.getProxyHost(); - int proxyPort = proxySystemProperties.getProxyPort(); - String proxyUser = proxySystemProperties.getProxyUser(); - String proxyPassword = proxySystemProperties.getProxyPassword(); - - if (proxySystemProperties.proxyServerIsSet()) { - useProxyServer(httpClientBuilder, proxyHost, proxyPort); + return setUpProxyServerAndAuthentication(teamscaleProxySystemProperties, defaultProxySystemProperties, httpClientBuilder); + } - if (proxySystemProperties.proxyAuthIsSet()) { - useProxyAuthenticator(httpClientBuilder, proxyUser, proxyPassword); - } + private static boolean setUpProxyServerAndAuthentication(TeamscaleProxySystemProperties teamscaleProxySystemProperties, ProxySystemProperties defaultProxySystemProperties, OkHttpClient.Builder httpClientBuilder) { + // It is allowed to use for example global server settings, but teamscale-specific user settings. + // It is not allowed to mix global and teamscale-specific settings inside those categories, so for example + // user -> global setting + // password teamscale-specific + // is forbidden. + if (teamscaleProxySystemProperties.proxyServerIsSet()) { + useProxyServer(httpClientBuilder, teamscaleProxySystemProperties.getProxyHost(), + teamscaleProxySystemProperties.getProxyPort()); + } else if (defaultProxySystemProperties.proxyAuthIsSet()) { + useProxyServer(httpClientBuilder, defaultProxySystemProperties.getProxyHost(), defaultProxySystemProperties.getProxyPort()); + } else { + return false; + } - return true; + if (teamscaleProxySystemProperties.proxyAuthIsSet()) { + useProxyAuthenticator(httpClientBuilder, teamscaleProxySystemProperties.getProxyUser(), teamscaleProxySystemProperties.getProxyPassword()); + } else if (defaultProxySystemProperties.proxyAuthIsSet()) { + useProxyAuthenticator(httpClientBuilder, defaultProxySystemProperties.getProxyUser(), defaultProxySystemProperties.getProxyPassword()); } - return false; + return true; } private static void useProxyServer(OkHttpClient.Builder httpClientBuilder, String proxyHost, int proxyPort) { diff --git a/teamscale-client/src/main/java/com/teamscale/client/ProxySystemProperties.java b/teamscale-client/src/main/java/com/teamscale/client/ProxySystemProperties.java index 2a862112f..f7c919af7 100644 --- a/teamscale-client/src/main/java/com/teamscale/client/ProxySystemProperties.java +++ b/teamscale-client/src/main/java/com/teamscale/client/ProxySystemProperties.java @@ -19,12 +19,20 @@ public class ProxySystemProperties { private static final Logger LOGGER = LoggerFactory.getLogger(ProxySystemProperties.class); - 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"; + /** The property suffix specifying the proxy host. */ + protected static final String PROXY_HOST_SYSTEM_PROPERTY = ".proxyHost"; - private final Protocol protocol; + /** The property suffix specifying the proxy port. */ + protected static final String PROXY_PORT_SYSTEM_PROPERTY = ".proxyPort"; + + /** The property suffix specifying the proxy user. */ + protected static final String PROXY_USER_SYSTEM_PROPERTY = ".proxyUser"; + + /** The property suffix specifying the proxy password. */ + protected static final String PROXY_PASSWORD_SYSTEM_PROPERTY = ".proxyPassword"; + + /** The protocol used by the proxy server addressed by these settings. */ + protected final Protocol protocol; /** * Indicates, whether the {@link ProxySystemProperties} should return values for the http.proxy* system properties @@ -84,8 +92,9 @@ public void setProxyHost(String proxyHost) { System.setProperty(getProxyHostSystemPropertyName(), proxyHost); } + /** @return the name of the system property specifying the proxy host. */ @NotNull - private String getProxyHostSystemPropertyName() { + protected String getProxyHostSystemPropertyName() { return protocol + PROXY_HOST_SYSTEM_PROPERTY; } @@ -111,8 +120,9 @@ public void setProxyPort(String proxyPort) { System.clearProperty(getProxyPortSystemPropertyName()); } + /** @return the name of the system property specifying the proxy port. */ @NotNull - private String getProxyPortSystemPropertyName() { + protected String getProxyPortSystemPropertyName() { return protocol + PROXY_PORT_SYSTEM_PROPERTY; } @@ -130,8 +140,9 @@ public void setProxyUser(String proxyUser) { System.setProperty(getProxyUserSystemPropertyName(), proxyUser); } + /** @return the name of the system property specifying the proxy user. */ @NotNull - private String getProxyUserSystemPropertyName() { + protected String getProxyUserSystemPropertyName() { return protocol + PROXY_USER_SYSTEM_PROPERTY; } @@ -150,8 +161,9 @@ public void setProxyPassword(String proxyPassword) { System.setProperty(getProxyPasswordSystemPropertyName(), proxyPassword); } + /** @return the name of the system property specifying the proxy password. */ @NotNull - private String getProxyPasswordSystemPropertyName() { + protected String getProxyPasswordSystemPropertyName() { return protocol + PROXY_PASSWORD_SYSTEM_PROPERTY; } diff --git a/teamscale-client/src/main/java/com/teamscale/client/TeamscaleProxySystemProperties.java b/teamscale-client/src/main/java/com/teamscale/client/TeamscaleProxySystemProperties.java new file mode 100644 index 000000000..022b2134d --- /dev/null +++ b/teamscale-client/src/main/java/com/teamscale/client/TeamscaleProxySystemProperties.java @@ -0,0 +1,49 @@ +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 getProxyHostSystemPropertyName() { + return TEAMSCALE_PREFIX + super.getProxyHostSystemPropertyName(); + } + + @Override + @NotNull + protected String getProxyPortSystemPropertyName() { + return TEAMSCALE_PREFIX + super.getProxyPortSystemPropertyName(); + } + + @Override + @NotNull + protected String getProxyUserSystemPropertyName() { + return TEAMSCALE_PREFIX + super.getProxyUserSystemPropertyName(); + } + + @Override + @NotNull + protected String getProxyPasswordSystemPropertyName() { + return TEAMSCALE_PREFIX + super.getProxyPasswordSystemPropertyName(); + } +} \ No newline at end of file From 6a97fa7ee1106cde96c2791e4871d43dbf20e7f1 Mon Sep 17 00:00:00 2001 From: Jonas Bogenberger Date: Mon, 23 Sep 2024 15:55:11 +0200 Subject: [PATCH 055/186] TS-40412 add null-checks for TeamscaleProxyOptions --- .../agent/options/TeamscaleProxyOptions.java | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java b/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java index 1765509ec..475351a81 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java @@ -55,10 +55,17 @@ public void putTeamscaleProxyOptionsIntoSystemProperties() { } private void putTeamscaleProxyOptionsIntoSystemPropertiesForProtocol(ProxySystemProperties.Protocol protocol) { - new TeamscaleProxySystemProperties(protocol).setProxyHost(proxyHost); - new TeamscaleProxySystemProperties(protocol).setProxyPort(proxyPort); - new TeamscaleProxySystemProperties(protocol).setProxyUser(proxyUser); - new TeamscaleProxySystemProperties(protocol).setProxyPassword(proxyPassword); - + if (proxyHost != null) { + new TeamscaleProxySystemProperties(protocol).setProxyHost(proxyHost); + } + if (proxyPort > 0) { + new TeamscaleProxySystemProperties(protocol).setProxyPort(proxyPort); + } + if(proxyUser != null) { + new TeamscaleProxySystemProperties(protocol).setProxyUser(proxyUser); + } + if(proxyPassword != null) { + new TeamscaleProxySystemProperties(protocol).setProxyPassword(proxyPassword); + } } } From 0530182d01578b524cf1acfb05ffba9f1bd386db Mon Sep 17 00:00:00 2001 From: Jonas Bogenberger Date: Mon, 23 Sep 2024 16:12:20 +0200 Subject: [PATCH 056/186] TS-40412 add another null-check for TeamscaleProxyOptions --- agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java b/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java index 1a5b44f18..80e07b046 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java @@ -41,7 +41,9 @@ public abstract class AgentBase { /** Constructor. */ public AgentBase(AgentOptions options) throws IllegalStateException { this.options = options; - options.getTeamscaleProxyOptions().putTeamscaleProxyOptionsIntoSystemProperties(); + if (options.getTeamscaleProxyOptions() != null) { + options.getTeamscaleProxyOptions().putTeamscaleProxyOptionsIntoSystemProperties(); + } setProxyPasswordFromFile(options.getProxyPasswordPath()); try { controller = new JacocoRuntimeController(RT.getAgent()); From 03a7afd8c71bc58baa4cb716a97e4da065676212 Mon Sep 17 00:00:00 2001 From: Stefan Brand Date: Mon, 23 Sep 2024 16:26:21 +0200 Subject: [PATCH 057/186] TS-39915 Refactoring: Concatenate parts instead of alternating uniformPath --- .../CucumberPickleDescriptorResolver.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.java index 1d8c3b2d6..cb75e6c1b 100644 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.java +++ b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/test_descriptor/CucumberPickleDescriptorResolver.java @@ -40,21 +40,22 @@ public Optional getUniformPath(TestDescriptor testDescriptor) { testDescriptor + ". This is probably a bug. Please report to CQSE"); return Optional.empty(); } - String uniformPath = featurePath.get() + "/" + pickleName.get(); + String picklePath = featurePath.get() + "/" + pickleName.get(); // Add an index to the end of the name in case multiple tests have the same name in the same feature file Optional featureFileTestDescriptor = getFeatureFileTestDescriptor(testDescriptor); - if (featureFileTestDescriptor.isPresent()) { + String indexSuffix; + if (!featureFileTestDescriptor.isPresent()) { + indexSuffix = ""; + } else { List siblingTestsWithTheSameName = flatListOfAllTestDescriptorChildrenWithPickleName( featureFileTestDescriptor.get(), pickleName.get()); int indexOfCurrentTest = siblingTestsWithTheSameName.indexOf(testDescriptor) + 1; - uniformPath += " #" + indexOfCurrentTest; + indexSuffix = " #" + indexOfCurrentTest; } - // IntelliJ complains without this that uniform path should be final when used in a lambda - uniformPath = removeDuplicatedSlashes(uniformPath); - final String finalUniformPath = uniformPath; - LOGGER.fine(() -> "Resolved uniform path: " + finalUniformPath); + String uniformPath = removeDuplicatedSlashes(picklePath + indexSuffix); + LOGGER.fine(() -> "Resolved uniform path: " + uniformPath); return Optional.of(uniformPath); } From a9f9b1611df7aaeeb30508f0a78de67e420d70d6 Mon Sep 17 00:00:00 2001 From: Jonas Bogenberger Date: Mon, 23 Sep 2024 16:39:14 +0200 Subject: [PATCH 058/186] TS-40412 ad interface comment and fix typo --- .../jacoco/agent/options/TeamscaleProxyOptions.java | 5 +++++ .../jacoco/agent/upload/artifactory/ArtifactoryConfig.java | 2 +- .../jacoco/agent/upload/azure/AzureFileStorageConfig.java | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java b/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java index 475351a81..f05b58ec5 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java @@ -26,6 +26,11 @@ public class TeamscaleProxyOptions { /* package */ String proxyUser; + /** + * Handles all command-line options prefixed with 'proxy-' + * + * @return true if it has successfully processed the given option. + */ public static boolean handleTeamscaleProxyOptions(TeamscaleProxyOptions options, String key, String value, FilePatternResolver filePatternResolver) throws AgentOptionParseException { switch (key) { case "proxy-password-file": diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/upload/artifactory/ArtifactoryConfig.java b/agent/src/main/java/com/teamscale/jacoco/agent/upload/artifactory/ArtifactoryConfig.java index d8da410dd..2ac54d058 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/upload/artifactory/ArtifactoryConfig.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/upload/artifactory/ArtifactoryConfig.java @@ -112,7 +112,7 @@ public class ArtifactoryConfig { /** * Handles all command-line options prefixed with 'artifactory-' * - * @return true if it has successfully process the given option. + * @return true if it has successfully processed the given option. */ public static boolean handleArtifactoryOptions(ArtifactoryConfig options, FilePatternResolver filePatternResolver, String key, String value) throws AgentOptionParseException { diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/upload/azure/AzureFileStorageConfig.java b/agent/src/main/java/com/teamscale/jacoco/agent/upload/azure/AzureFileStorageConfig.java index c83119b3e..5ae61e326 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/upload/azure/AzureFileStorageConfig.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/upload/azure/AzureFileStorageConfig.java @@ -25,7 +25,7 @@ public boolean hasAllRequiredFieldsNull() { /** * Handles all command-line options prefixed with 'azure-' * - * @return true if it has successfully process the given option. + * @return true if it has successfully processed the given option. */ public static boolean handleAzureFileStorageOptions(AzureFileStorageConfig azureFileStorageConfig, String key, String value) From 8379019758737549fad16b46fa9a37370a2377ac Mon Sep 17 00:00:00 2001 From: Stefan Brand Date: Mon, 23 Sep 2024 17:05:59 +0200 Subject: [PATCH 059/186] Release 34.0.2 --- CHANGELOG.md | 4 +++- build.gradle.kts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78ea5adb8..2399c9138 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,11 @@ We use [semantic versioning](http://semver.org/): - PATCH version when you make backwards compatible bug fixes. # Next version + +# 34.0.2 - [feature] _agent_: Prevent uploading coverage to the same project + revision or branch@timestamp when doing multi project upload via git.properties - [fix] _impacted-test-engine_: Remove duplicated slashes in uniform paths and cluster ids for Cucumber Tests -- [fix] _impacted-test-engine_: Ensure that the config file specified via `java.util.logging.config.file` is used when specified +- [fix] _impacted-test-engine_: Ensure that the config file specified via `java.util.logging.config.file` is used when specified # 34.0.1 - [fix] _agent_: Error was reported when the system under test used logback diff --git a/build.gradle.kts b/build.gradle.kts index fba208333..c1a5048f2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,7 @@ plugins { group = "com.teamscale" -val appVersion by extra("34.0.1") +val appVersion by extra("34.0.2") val snapshotVersion = appVersion + if (VersionUtils.isTaggedRelease()) "" else "-SNAPSHOT" From 3a6d332861da83204f5e595027f86a870a76787f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Sep 2024 22:35:31 +0000 Subject: [PATCH 060/186] Update dependency gradle to v8.10.2 --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 66cd5a0e4..79eb9d003 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 0b7708c829b4ff817a366bef2e233cc54df11d70 Mon Sep 17 00:00:00 2001 From: Jonas Bogenberger Date: Tue, 24 Sep 2024 10:02:02 +0200 Subject: [PATCH 061/186] TS-40412 add tests whether the teamscale-specific system properties are used correctly --- .../java/com/teamscale/client/HttpUtils.java | 2 +- ...mscaleServiceGeneratorProxyServerTest.java | 104 ++++++++++++++++-- 2 files changed, 94 insertions(+), 12 deletions(-) diff --git a/teamscale-client/src/main/java/com/teamscale/client/HttpUtils.java b/teamscale-client/src/main/java/com/teamscale/client/HttpUtils.java index 68e1e6a80..7bb5d765f 100644 --- a/teamscale-client/src/main/java/com/teamscale/client/HttpUtils.java +++ b/teamscale-client/src/main/java/com/teamscale/client/HttpUtils.java @@ -117,7 +117,7 @@ private static boolean setUpProxyServerAndAuthentication(TeamscaleProxySystemPro if (teamscaleProxySystemProperties.proxyServerIsSet()) { useProxyServer(httpClientBuilder, teamscaleProxySystemProperties.getProxyHost(), teamscaleProxySystemProperties.getProxyPort()); - } else if (defaultProxySystemProperties.proxyAuthIsSet()) { + } else if (defaultProxySystemProperties.proxyServerIsSet()) { useProxyServer(httpClientBuilder, defaultProxySystemProperties.getProxyHost(), defaultProxySystemProperties.getProxyPort()); } else { return false; diff --git a/teamscale-client/src/test/java/com/teamscale/client/TeamscaleServiceGeneratorProxyServerTest.java b/teamscale-client/src/test/java/com/teamscale/client/TeamscaleServiceGeneratorProxyServerTest.java index 07024df05..b78a7b579 100644 --- a/teamscale-client/src/test/java/com/teamscale/client/TeamscaleServiceGeneratorProxyServerTest.java +++ b/teamscale-client/src/test/java/com/teamscale/client/TeamscaleServiceGeneratorProxyServerTest.java @@ -24,6 +24,14 @@ class TeamscaleServiceGeneratorProxyServerTest { private final ProxySystemProperties proxySystemProperties = new ProxySystemProperties( ProxySystemProperties.Protocol.HTTP); + private final TeamscaleProxySystemProperties teamscaleProxySystemProperties = new TeamscaleProxySystemProperties( + ProxySystemProperties.Protocol.HTTP); + + private final static String PROXY_USER = "myProxyUser"; + private final static String PROXY_PASSWORD = "myProxyPassword"; + private final static String BASE_64_ENCODED_BASIC_AUTH = Base64.getEncoder().encodeToString((PROXY_USER + ":" + PROXY_PASSWORD).getBytes( + StandardCharsets.UTF_8)); + @BeforeEach void setUp() throws IOException { mockProxyServer = new MockWebServer(); @@ -31,15 +39,84 @@ void setUp() throws IOException { } @Test - void testProxyAuthentication() throws IOException, InterruptedException { - String proxyUser = "myProxyUser"; - String proxyPassword = "myProxyPassword"; - String base64EncodedBasicAuth = Base64.getEncoder().encodeToString((proxyUser + ":" + proxyPassword).getBytes( - StandardCharsets.UTF_8)); + void testProxyAuthentication() throws Exception { proxySystemProperties.setProxyHost(mockProxyServer.getHostName()); proxySystemProperties.setProxyPort(mockProxyServer.getPort()); - proxySystemProperties.setProxyUser(proxyUser); - proxySystemProperties.setProxyPassword(proxyPassword); + proxySystemProperties.setProxyUser(PROXY_USER); + proxySystemProperties.setProxyPassword(PROXY_PASSWORD); + + assertProxyAuthenticationIsUsed(); + } + + @Test + void testTeamscaleProxyAuthentication() throws Exception { + // test that the teamscale-specific options take precedence over the global ones + proxySystemProperties.setProxyHost("incorrect"); + proxySystemProperties.setProxyPort("incorrect"); + proxySystemProperties.setProxyUser("incorrect"); + proxySystemProperties.setProxyPassword("incorrect"); + + teamscaleProxySystemProperties.setProxyHost(mockProxyServer.getHostName()); + teamscaleProxySystemProperties.setProxyPort(mockProxyServer.getPort()); + teamscaleProxySystemProperties.setProxyUser(PROXY_USER); + teamscaleProxySystemProperties.setProxyPassword(PROXY_PASSWORD); + + assertProxyAuthenticationIsUsed(); + } + + @Test + void testMixingTeamscaleSpecificAndGlobalProxySettingsIsPossible() throws Exception { + proxySystemProperties.setProxyHost(mockProxyServer.getHostName()); + proxySystemProperties.setProxyPort(mockProxyServer.getPort()); + proxySystemProperties.setProxyUser("incorrect"); + proxySystemProperties.setProxyPassword("incorrect"); + + teamscaleProxySystemProperties.setProxyUser(PROXY_USER); + teamscaleProxySystemProperties.setProxyPassword(PROXY_PASSWORD); + + assertProxyAuthenticationIsUsed(); + } + + @Test + void testMixingTeamscaleSpecificAndGlobalProxySettingsIsPossibleTheOtherWayAround() throws Exception { + proxySystemProperties.setProxyHost("incorrect"); + proxySystemProperties.setProxyPort("incorrect"); + proxySystemProperties.setProxyUser(PROXY_USER); + proxySystemProperties.setProxyPassword(PROXY_PASSWORD); + + teamscaleProxySystemProperties.setProxyHost(mockProxyServer.getHostName()); + teamscaleProxySystemProperties.setProxyPort(mockProxyServer.getPort()); + + assertProxyAuthenticationIsUsed(); + } + + @Test + void testPartiallyMixingTeamscaleSpecificAndGlobalProxyServerSettingsIsImpossible() throws Exception { + proxySystemProperties.setProxyHost(mockProxyServer.getHostName()); + proxySystemProperties.setProxyPort(mockProxyServer.getPort()); + proxySystemProperties.setProxyUser(PROXY_USER); + proxySystemProperties.setProxyPassword(PROXY_PASSWORD); + + // if mixing the server settings works, reaching the host would be impossible + teamscaleProxySystemProperties.setProxyHost("incorrect"); + + assertProxyAuthenticationIsUsed(); + } + + @Test + void testPartiallyMixingTeamscaleSpecificAndGlobalProxyAuthenticationSettingsIsImpossible() throws Exception { + proxySystemProperties.setProxyHost(mockProxyServer.getHostName()); + proxySystemProperties.setProxyPort(mockProxyServer.getPort()); + proxySystemProperties.setProxyUser(PROXY_USER); + proxySystemProperties.setProxyPassword(PROXY_PASSWORD); + + // if mixing the authentication settings works, authentication would not work + teamscaleProxySystemProperties.setProxyUser("incorrect"); + + assertProxyAuthenticationIsUsed(); + } + + private void assertProxyAuthenticationIsUsed() throws InterruptedException, IOException { ITeamscaleService service = TeamscaleServiceGenerator.createService(ITeamscaleService.class, HttpUrl.parse("http://localhost:1337"), @@ -58,17 +135,22 @@ void testProxyAuthentication() throws IOException, InterruptedException { RecordedRequest requestWithProxyAuth = mockProxyServer.takeRequest();// Request we are actually interested in assertThat(requestWithProxyAuth.getHeader(PROXY_AUTHORIZATION_HTTP_HEADER)).isEqualTo( - "Basic " + base64EncodedBasicAuth); + "Basic " + BASE_64_ENCODED_BASIC_AUTH); } @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(""); - - mockProxyServer.shutdown(); - mockProxyServer.close(); } } \ No newline at end of file From 827d3e802cdf7b47be8924fc1a4c846d6016f7a0 Mon Sep 17 00:00:00 2001 From: Jonas Bogenberger Date: Tue, 24 Sep 2024 10:04:33 +0200 Subject: [PATCH 062/186] TS-40412 fix findings --- .../client/TeamscaleServiceGeneratorProxyServerTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/teamscale-client/src/test/java/com/teamscale/client/TeamscaleServiceGeneratorProxyServerTest.java b/teamscale-client/src/test/java/com/teamscale/client/TeamscaleServiceGeneratorProxyServerTest.java index b78a7b579..15741b0f9 100644 --- a/teamscale-client/src/test/java/com/teamscale/client/TeamscaleServiceGeneratorProxyServerTest.java +++ b/teamscale-client/src/test/java/com/teamscale/client/TeamscaleServiceGeneratorProxyServerTest.java @@ -27,9 +27,9 @@ class TeamscaleServiceGeneratorProxyServerTest { private final TeamscaleProxySystemProperties teamscaleProxySystemProperties = new TeamscaleProxySystemProperties( ProxySystemProperties.Protocol.HTTP); - private final static String PROXY_USER = "myProxyUser"; - private final static String PROXY_PASSWORD = "myProxyPassword"; - private final static String BASE_64_ENCODED_BASIC_AUTH = Base64.getEncoder().encodeToString((PROXY_USER + ":" + PROXY_PASSWORD).getBytes( + private static final String PROXY_USER = "myProxyUser"; + private static final String PROXY_PASSWORD = "myProxyPassword"; + private static final String BASE_64_ENCODED_BASIC_AUTH = Base64.getEncoder().encodeToString((PROXY_USER + ":" + PROXY_PASSWORD).getBytes( StandardCharsets.UTF_8)); @BeforeEach From 3f0d500985e928d784e955c194f31fb0e43c41c2 Mon Sep 17 00:00:00 2001 From: Jonas Bogenberger Date: Tue, 24 Sep 2024 10:06:39 +0200 Subject: [PATCH 063/186] TS-40412 constant --- ...mscaleServiceGeneratorProxyServerTest.java | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/teamscale-client/src/test/java/com/teamscale/client/TeamscaleServiceGeneratorProxyServerTest.java b/teamscale-client/src/test/java/com/teamscale/client/TeamscaleServiceGeneratorProxyServerTest.java index 15741b0f9..394a2b5df 100644 --- a/teamscale-client/src/test/java/com/teamscale/client/TeamscaleServiceGeneratorProxyServerTest.java +++ b/teamscale-client/src/test/java/com/teamscale/client/TeamscaleServiceGeneratorProxyServerTest.java @@ -27,6 +27,7 @@ class TeamscaleServiceGeneratorProxyServerTest { private final TeamscaleProxySystemProperties teamscaleProxySystemProperties = new TeamscaleProxySystemProperties( ProxySystemProperties.Protocol.HTTP); + private static final String INCORRECT_VALUE = "incorrect"; private static final String PROXY_USER = "myProxyUser"; private static final String PROXY_PASSWORD = "myProxyPassword"; private static final String BASE_64_ENCODED_BASIC_AUTH = Base64.getEncoder().encodeToString((PROXY_USER + ":" + PROXY_PASSWORD).getBytes( @@ -51,10 +52,10 @@ void testProxyAuthentication() throws Exception { @Test void testTeamscaleProxyAuthentication() throws Exception { // test that the teamscale-specific options take precedence over the global ones - proxySystemProperties.setProxyHost("incorrect"); - proxySystemProperties.setProxyPort("incorrect"); - proxySystemProperties.setProxyUser("incorrect"); - proxySystemProperties.setProxyPassword("incorrect"); + proxySystemProperties.setProxyHost(INCORRECT_VALUE); + proxySystemProperties.setProxyPort(INCORRECT_VALUE); + proxySystemProperties.setProxyUser(INCORRECT_VALUE); + proxySystemProperties.setProxyPassword(INCORRECT_VALUE); teamscaleProxySystemProperties.setProxyHost(mockProxyServer.getHostName()); teamscaleProxySystemProperties.setProxyPort(mockProxyServer.getPort()); @@ -68,8 +69,8 @@ void testTeamscaleProxyAuthentication() throws Exception { void testMixingTeamscaleSpecificAndGlobalProxySettingsIsPossible() throws Exception { proxySystemProperties.setProxyHost(mockProxyServer.getHostName()); proxySystemProperties.setProxyPort(mockProxyServer.getPort()); - proxySystemProperties.setProxyUser("incorrect"); - proxySystemProperties.setProxyPassword("incorrect"); + proxySystemProperties.setProxyUser(INCORRECT_VALUE); + proxySystemProperties.setProxyPassword(INCORRECT_VALUE); teamscaleProxySystemProperties.setProxyUser(PROXY_USER); teamscaleProxySystemProperties.setProxyPassword(PROXY_PASSWORD); @@ -79,8 +80,8 @@ void testMixingTeamscaleSpecificAndGlobalProxySettingsIsPossible() throws Except @Test void testMixingTeamscaleSpecificAndGlobalProxySettingsIsPossibleTheOtherWayAround() throws Exception { - proxySystemProperties.setProxyHost("incorrect"); - proxySystemProperties.setProxyPort("incorrect"); + proxySystemProperties.setProxyHost(INCORRECT_VALUE); + proxySystemProperties.setProxyPort(INCORRECT_VALUE); proxySystemProperties.setProxyUser(PROXY_USER); proxySystemProperties.setProxyPassword(PROXY_PASSWORD); @@ -98,7 +99,7 @@ void testPartiallyMixingTeamscaleSpecificAndGlobalProxyServerSettingsIsImpossibl proxySystemProperties.setProxyPassword(PROXY_PASSWORD); // if mixing the server settings works, reaching the host would be impossible - teamscaleProxySystemProperties.setProxyHost("incorrect"); + teamscaleProxySystemProperties.setProxyHost(INCORRECT_VALUE); assertProxyAuthenticationIsUsed(); } @@ -111,7 +112,7 @@ void testPartiallyMixingTeamscaleSpecificAndGlobalProxyAuthenticationSettingsIsI proxySystemProperties.setProxyPassword(PROXY_PASSWORD); // if mixing the authentication settings works, authentication would not work - teamscaleProxySystemProperties.setProxyUser("incorrect"); + teamscaleProxySystemProperties.setProxyUser(INCORRECT_VALUE); assertProxyAuthenticationIsUsed(); } From df7f2a3a58dbbb4242884ecbb8eabd2c22ad7cff Mon Sep 17 00:00:00 2001 From: Florian Dreier Date: Mon, 23 Sep 2024 12:03:43 +0200 Subject: [PATCH 064/186] TS-40425 Fixe NPE in Maven plugin --- CHANGELOG.md | 1 + agent/build.gradle.kts | 3 +- ...eamscale.system-test-convention.gradle.kts | 4 + installer/build.gradle.kts | 2 +- .../cucumber-maven-tia/build.gradle.kts | 2 - system-tests/gradle-cucumber/build.gradle.kts | 2 - .../gradle-multi-module/build.gradle.kts | 2 - .../junit-run-listener-test/build.gradle.kts | 4 - .../build.gradle.kts | 1 - .../build.gradle.kts | 2 - .../missing-commit-project/.gitignore | 2 + .../.mvn/wrapper/maven-wrapper.properties | 18 + .../missing-commit-project/mvnw | 316 ++++++++++++++++++ .../missing-commit-project/mvnw.cmd | 188 +++++++++++ .../missing-commit-project/pom.xml | 82 +++++ .../src/main/java/org/example/SUTF.java | 16 + .../src/test/java/org/example/UnitTest.java | 17 + .../upload/MavenExternalUploadSystemTest.java | 20 ++ system-tests/tia-maven/build.gradle.kts | 2 - .../java/com/teamscale/maven/GitCommit.java | 19 +- 20 files changed, 679 insertions(+), 24 deletions(-) create mode 100644 system-tests/maven-external-upload-test/missing-commit-project/.gitignore create mode 100644 system-tests/maven-external-upload-test/missing-commit-project/.mvn/wrapper/maven-wrapper.properties create mode 100755 system-tests/maven-external-upload-test/missing-commit-project/mvnw create mode 100644 system-tests/maven-external-upload-test/missing-commit-project/mvnw.cmd create mode 100644 system-tests/maven-external-upload-test/missing-commit-project/pom.xml create mode 100644 system-tests/maven-external-upload-test/missing-commit-project/src/main/java/org/example/SUTF.java create mode 100644 system-tests/maven-external-upload-test/missing-commit-project/src/test/java/org/example/UnitTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 2399c9138..067515b73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ We use [semantic versioning](http://semver.org/): - PATCH version when you make backwards compatible bug fixes. # Next version +- [fix] _teamscale-maven-plugin_: NPE when no commit, nor revision was configured # 34.0.2 - [feature] _agent_: Prevent uploading coverage to the same project + revision or branch@timestamp when doing multi project upload via git.properties diff --git a/agent/build.gradle.kts b/agent/build.gradle.kts index a3b4d7fb6..3b59d9593 100644 --- a/agent/build.gradle.kts +++ b/agent/build.gradle.kts @@ -107,9 +107,10 @@ tasks.shadowDistZip { } tasks.processResources { + inputs.property("version", project.version) filesMatching("**/app.properties") { filter { - it.replace("%APP_VERSION_TOKEN_REPLACED_DURING_BUILD%", appVersion) + it.replace("%APP_VERSION_TOKEN_REPLACED_DURING_BUILD%", version.toString()) } } } diff --git a/buildSrc/src/main/kotlin/com.teamscale.system-test-convention.gradle.kts b/buildSrc/src/main/kotlin/com.teamscale.system-test-convention.gradle.kts index c22dcd9e2..7688d057c 100644 --- a/buildSrc/src/main/kotlin/com.teamscale.system-test-convention.gradle.kts +++ b/buildSrc/src/main/kotlin/com.teamscale.system-test-convention.gradle.kts @@ -12,6 +12,10 @@ tasks.test { teamscalePort = provider.get().pickFreePort() agentPort = provider.get().pickFreePort() systemProperties("agentPort" to agentPort, "teamscalePort" to teamscalePort) + environment("AGENT_VERSION", version) + environment("AGENT_PATH", agentJar) + environment("TEAMSCALE_PORT", teamscalePort) + environment("AGENT_PORT", agentPort) } dependencies { diff --git a/installer/build.gradle.kts b/installer/build.gradle.kts index ccbdc35a8..cd33e39a7 100644 --- a/installer/build.gradle.kts +++ b/installer/build.gradle.kts @@ -81,7 +81,7 @@ dependencies { tasks.processResources { filesMatching("**/app.properties") { filter { - it.replace("%APP_VERSION_TOKEN_REPLACED_DURING_BUILD%", rootProject.ext["appVersion"].toString()) + it.replace("%APP_VERSION_TOKEN_REPLACED_DURING_BUILD%", version.toString()) } } } diff --git a/system-tests/cucumber-maven-tia/build.gradle.kts b/system-tests/cucumber-maven-tia/build.gradle.kts index 969326618..10a3c21f0 100644 --- a/system-tests/cucumber-maven-tia/build.gradle.kts +++ b/system-tests/cucumber-maven-tia/build.gradle.kts @@ -4,8 +4,6 @@ plugins { } tasks.test { - environment("AGENT_VERSION", version) - environment("TEAMSCALE_PORT", teamscalePort) // install dependencies needed by the Maven test project dependsOn(rootProject.tasks["publishToMavenLocal"]) } diff --git a/system-tests/gradle-cucumber/build.gradle.kts b/system-tests/gradle-cucumber/build.gradle.kts index 1fe8e1c00..394ee070b 100644 --- a/system-tests/gradle-cucumber/build.gradle.kts +++ b/system-tests/gradle-cucumber/build.gradle.kts @@ -4,8 +4,6 @@ plugins { } tasks.test { - environment("AGENT_VERSION", version) - environment("TEAMSCALE_PORT", teamscalePort) // install dependencies needed by the Gradle test project dependsOn(rootProject.tasks["publishToMavenLocal"]) } diff --git a/system-tests/gradle-multi-module/build.gradle.kts b/system-tests/gradle-multi-module/build.gradle.kts index 1fe8e1c00..394ee070b 100644 --- a/system-tests/gradle-multi-module/build.gradle.kts +++ b/system-tests/gradle-multi-module/build.gradle.kts @@ -4,8 +4,6 @@ plugins { } tasks.test { - environment("AGENT_VERSION", version) - environment("TEAMSCALE_PORT", teamscalePort) // install dependencies needed by the Gradle test project dependsOn(rootProject.tasks["publishToMavenLocal"]) } diff --git a/system-tests/junit-run-listener-test/build.gradle.kts b/system-tests/junit-run-listener-test/build.gradle.kts index 3a326a754..a983093a7 100644 --- a/system-tests/junit-run-listener-test/build.gradle.kts +++ b/system-tests/junit-run-listener-test/build.gradle.kts @@ -4,10 +4,6 @@ plugins { } tasks.test { - environment("AGENT_PATH", agentJar) - environment("AGENT_VERSION", version) - environment("AGENT_PORT", agentPort) - environment("TEAMSCALE_PORT", teamscalePort) // install dependencies needed by the Maven test projects dependsOn(rootProject.tasks["publishToMavenLocal"]) } diff --git a/system-tests/log-version-on-startup-test/build.gradle.kts b/system-tests/log-version-on-startup-test/build.gradle.kts index 5770e2287..a1473cb5e 100644 --- a/system-tests/log-version-on-startup-test/build.gradle.kts +++ b/system-tests/log-version-on-startup-test/build.gradle.kts @@ -4,6 +4,5 @@ plugins { tasks.test { val logFilePath = "logTest" - environment("AGENT_VERSION", rootProject.extra["appVersion"].toString()) teamscaleAgent(mapOf("debug" to logFilePath)) } diff --git a/system-tests/maven-external-upload-test/build.gradle.kts b/system-tests/maven-external-upload-test/build.gradle.kts index 8acf7590f..1eaa62be2 100644 --- a/system-tests/maven-external-upload-test/build.gradle.kts +++ b/system-tests/maven-external-upload-test/build.gradle.kts @@ -3,8 +3,6 @@ plugins { } tasks.test { - environment("AGENT_VERSION", rootProject.extra["appVersion"].toString()) - environment("TEAMSCALE_PORT", teamscalePort) // install dependencies needed by the Maven test project dependsOn(":publishToMavenLocal") } diff --git a/system-tests/maven-external-upload-test/missing-commit-project/.gitignore b/system-tests/maven-external-upload-test/missing-commit-project/.gitignore new file mode 100644 index 000000000..c5078494e --- /dev/null +++ b/system-tests/maven-external-upload-test/missing-commit-project/.gitignore @@ -0,0 +1,2 @@ +target +.idea diff --git a/system-tests/maven-external-upload-test/missing-commit-project/.mvn/wrapper/maven-wrapper.properties b/system-tests/maven-external-upload-test/missing-commit-project/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..e83fa6959 --- /dev/null +++ b/system-tests/maven-external-upload-test/missing-commit-project/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar diff --git a/system-tests/maven-external-upload-test/missing-commit-project/mvnw b/system-tests/maven-external-upload-test/missing-commit-project/mvnw new file mode 100755 index 000000000..5643201c7 --- /dev/null +++ b/system-tests/maven-external-upload-test/missing-commit-project/mvnw @@ -0,0 +1,316 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`\\unset -f command; \\command -v java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/system-tests/maven-external-upload-test/missing-commit-project/mvnw.cmd b/system-tests/maven-external-upload-test/missing-commit-project/mvnw.cmd new file mode 100644 index 000000000..8a15b7f31 --- /dev/null +++ b/system-tests/maven-external-upload-test/missing-commit-project/mvnw.cmd @@ -0,0 +1,188 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/system-tests/maven-external-upload-test/missing-commit-project/pom.xml b/system-tests/maven-external-upload-test/missing-commit-project/pom.xml new file mode 100644 index 000000000..b04911945 --- /dev/null +++ b/system-tests/maven-external-upload-test/missing-commit-project/pom.xml @@ -0,0 +1,82 @@ + + + + + 4.0.0 + org.example + missing-commit-project + 1.0-SNAPSHOT + missing-commit-project + + + UTF-8 + 1.8 + 1.8 + ${env.TEAMSCALE_PORT} + ${env.AGENT_VERSION} + + + + + org.junit.jupiter + junit-jupiter-engine + 5.8.2 + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + + org.jacoco + jacoco-maven-plugin + 0.8.9 + + + default-prepare-agent + + prepare-agent + + + + default-report + + report + + + + XML + + + + + + + com.teamscale + teamscale-maven-plugin + ${tia.agent.version} + + + + upload-coverage + + + + + http://localhost:${tia.teamscale.fake.port} + m + build + 6lJKEvNHeTxGPhMAi4D84DWqzoSFL1p4 + My Custom Unit Tests Partition + + + + + + diff --git a/system-tests/maven-external-upload-test/missing-commit-project/src/main/java/org/example/SUTF.java b/system-tests/maven-external-upload-test/missing-commit-project/src/main/java/org/example/SUTF.java new file mode 100644 index 000000000..af1d55e71 --- /dev/null +++ b/system-tests/maven-external-upload-test/missing-commit-project/src/main/java/org/example/SUTF.java @@ -0,0 +1,16 @@ +package org.example; + +/** + * Hello world! + * + */ +public class SUTF +{ + public void bla() { + System.out.println("bla"); + } + + public void foo() { + System.out.println("foo"); + } +} \ No newline at end of file diff --git a/system-tests/maven-external-upload-test/missing-commit-project/src/test/java/org/example/UnitTest.java b/system-tests/maven-external-upload-test/missing-commit-project/src/test/java/org/example/UnitTest.java new file mode 100644 index 000000000..55cee8d6e --- /dev/null +++ b/system-tests/maven-external-upload-test/missing-commit-project/src/test/java/org/example/UnitTest.java @@ -0,0 +1,17 @@ +package bar; + +import org.junit.jupiter.api.Test; +import org.example.SUTF; + +public class UnitTest { + + @Test + public void utBla() { + new SUTF().bla(); + } + + @Test + public void utFoo() { + new SUTF().foo(); + } +} \ No newline at end of file diff --git a/system-tests/maven-external-upload-test/src/test/java/com/teamscale/upload/MavenExternalUploadSystemTest.java b/system-tests/maven-external-upload-test/src/test/java/com/teamscale/upload/MavenExternalUploadSystemTest.java index 0389c85d1..e4b004570 100644 --- a/system-tests/maven-external-upload-test/src/test/java/com/teamscale/upload/MavenExternalUploadSystemTest.java +++ b/system-tests/maven-external-upload-test/src/test/java/com/teamscale/upload/MavenExternalUploadSystemTest.java @@ -4,13 +4,16 @@ import com.teamscale.test.commons.SystemTestUtils; import com.teamscale.test.commons.TeamscaleMockServer; import org.apache.commons.lang3.SystemUtils; +import org.conqat.lib.commons.filesystem.FileSystemUtils; import org.conqat.lib.commons.io.ProcessUtils; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import java.io.File; import java.io.IOException; +import java.nio.file.Path; import java.nio.file.Paths; import static org.assertj.core.api.Assertions.assertThat; @@ -75,6 +78,23 @@ public void testIncorrectJaCoCoConfiguration() throws IOException { FAILING_MAVEN_PROJECT_NAME, "org.jacoco:jacoco-maven-plugin")); } + /** + * When no commit is given and no git repo is available, which is the usual fallback, a helpful error message should + * be shown (TS-40425). + */ + @Test + public void testErrorMessageOnMissingCommit(@TempDir Path tmpDir) throws IOException { + FileSystemUtils.copyFiles(new File("missing-commit-project"), tmpDir.toFile(), file -> true); + tmpDir.resolve("mvnw").toFile().setExecutable(true); + String projectPath = tmpDir.toAbsolutePath().toString(); + SystemTestUtils.runMavenTests(projectPath); + ProcessUtils.ExecutionResult result = runCoverageUploadGoal(projectPath); + assertThat(result).isNotNull(); + assertThat(result.getReturnCode()).isNotEqualTo(0); + assertThat(teamscaleMockServer.uploadedReports).isEmpty(); + assertThat(result.getStdout()).contains("There is no configured in the pom.xml and it was not possible to determine the checked out commit"); + } + @AfterAll public static void stopFakeTeamscaleServer() { teamscaleMockServer.shutdown(); diff --git a/system-tests/tia-maven/build.gradle.kts b/system-tests/tia-maven/build.gradle.kts index 43532996d..60c105c0d 100644 --- a/system-tests/tia-maven/build.gradle.kts +++ b/system-tests/tia-maven/build.gradle.kts @@ -4,8 +4,6 @@ plugins { } tasks.test { - environment("AGENT_VERSION", version) - environment("TEAMSCALE_PORT", teamscalePort) // install dependencies needed by the Maven test project dependsOn(rootProject.tasks["publishToMavenLocal"]) } diff --git a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/GitCommit.java b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/GitCommit.java index 8767e68d7..1fc558cb8 100644 --- a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/GitCommit.java +++ b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/GitCommit.java @@ -36,13 +36,18 @@ private GitCommit(String sha1, long timestamp, String branch) { */ public static GitCommit getGitHeadCommitDescriptor(Path searchDirectory) throws IOException { Path gitDirectory = findGitBaseDirectory(searchDirectory); - Git git = Git.open(gitDirectory.toFile()); - Repository repository = git.getRepository(); - String branch = repository.getBranch(); - RevCommit commit = getCommit(repository, branch); - long commitTimeSeconds = commit.getCommitTime(); - String ref = repository.getRefDatabase().findRef("HEAD").getObjectId().getName(); - return new GitCommit(ref, commitTimeSeconds * 1000L, branch); + if (gitDirectory == null) { + throw new IOException("Could not find git directory in " + searchDirectory); + } + Repository repository; + try (Git git = Git.open(gitDirectory.toFile())) { + repository = git.getRepository(); + String branch = repository.getBranch(); + RevCommit commit = getCommit(repository, branch); + long commitTimeSeconds = commit.getCommitTime(); + String ref = repository.getRefDatabase().findRef("HEAD").getObjectId().getName(); + return new GitCommit(ref, commitTimeSeconds * 1000L, branch); + } } /** From 61d44c45135f2e5590f61c44711aa019884a6097 Mon Sep 17 00:00:00 2001 From: Florian Dreier Date: Tue, 24 Sep 2024 08:49:00 +0200 Subject: [PATCH 065/186] TS-40425 Fixed commit being ignored in end commit resolution --- CHANGELOG.md | 1 + .../upload/MavenExternalUploadSystemTest.java | 2 +- .../java/com/teamscale/maven/GitCommit.java | 77 ------------------- .../com/teamscale/maven/GitCommitUtils.java | 45 +++++++++++ .../teamscale/maven/TeamscaleMojoBase.java | 72 +++++++---------- .../com/teamscale/maven/tia/TiaMojoBase.java | 26 +++---- .../maven/upload/CoverageUploadMojo.java | 33 ++++---- 7 files changed, 104 insertions(+), 152 deletions(-) delete mode 100644 teamscale-maven-plugin/src/main/java/com/teamscale/maven/GitCommit.java create mode 100644 teamscale-maven-plugin/src/main/java/com/teamscale/maven/GitCommitUtils.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 067515b73..bc0253ad4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ We use [semantic versioning](http://semver.org/): # Next version - [fix] _teamscale-maven-plugin_: NPE when no commit, nor revision was configured +- [fix] _teamscale-maven-plugin_: Fixed commit was ignored in commit resolution # 34.0.2 - [feature] _agent_: Prevent uploading coverage to the same project + revision or branch@timestamp when doing multi project upload via git.properties diff --git a/system-tests/maven-external-upload-test/src/test/java/com/teamscale/upload/MavenExternalUploadSystemTest.java b/system-tests/maven-external-upload-test/src/test/java/com/teamscale/upload/MavenExternalUploadSystemTest.java index e4b004570..c2e5d50d6 100644 --- a/system-tests/maven-external-upload-test/src/test/java/com/teamscale/upload/MavenExternalUploadSystemTest.java +++ b/system-tests/maven-external-upload-test/src/test/java/com/teamscale/upload/MavenExternalUploadSystemTest.java @@ -92,7 +92,7 @@ public void testErrorMessageOnMissingCommit(@TempDir Path tmpDir) throws IOExcep assertThat(result).isNotNull(); assertThat(result.getReturnCode()).isNotEqualTo(0); assertThat(teamscaleMockServer.uploadedReports).isEmpty(); - assertThat(result.getStdout()).contains("There is no configured in the pom.xml and it was not possible to determine the checked out commit"); + assertThat(result.getStdout()).contains("There is no or configured in the pom.xml and it was not possible to determine the current revision"); } @AfterAll diff --git a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/GitCommit.java b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/GitCommit.java deleted file mode 100644 index 1fc558cb8..000000000 --- a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/GitCommit.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.teamscale.maven; - -import org.eclipse.jgit.api.Git; -import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.lib.Ref; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.revwalk.RevCommit; -import org.eclipse.jgit.revwalk.RevWalk; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; - -/** - * Represents a single commit in a Git repository. - */ -public class GitCommit { - - /** The SHA1 of this commit. */ - public final String sha1; - /** The timestamp of this commit (Unix epoch in milliseconds). */ - public final long timestamp; - /** The branch of this commit. */ - public final String branch; - - private GitCommit(String sha1, long timestamp, String branch) { - this.sha1 = sha1; - this.timestamp = timestamp; - this.branch = branch; - } - - /** - * Determines the current HEAD commit in the Git repository located in the or above the given search directory. - * - * @throws IOException if reading from the Git repository fails or the current directory is not a Git repository. - */ - public static GitCommit getGitHeadCommitDescriptor(Path searchDirectory) throws IOException { - Path gitDirectory = findGitBaseDirectory(searchDirectory); - if (gitDirectory == null) { - throw new IOException("Could not find git directory in " + searchDirectory); - } - Repository repository; - try (Git git = Git.open(gitDirectory.toFile())) { - repository = git.getRepository(); - String branch = repository.getBranch(); - RevCommit commit = getCommit(repository, branch); - long commitTimeSeconds = commit.getCommitTime(); - String ref = repository.getRefDatabase().findRef("HEAD").getObjectId().getName(); - return new GitCommit(ref, commitTimeSeconds * 1000L, branch); - } - } - - /** - * Traverses the directory tree upwards until it finds a .git directory. Returns null if no .git directory is - * found. - */ - private static Path findGitBaseDirectory(Path searchDirectory) { - while (searchDirectory != null) { - if (Files.exists(searchDirectory.resolve(".git"))) { - return searchDirectory; - } - searchDirectory = searchDirectory.getParent(); - } - return null; - } - - private static RevCommit getCommit(Repository repository, String revisionBranchOrTag) throws IOException { - try (RevWalk revWalk = new RevWalk(repository)) { - Ref head = repository.getRefDatabase().findRef(revisionBranchOrTag); - if (head != null) { - return revWalk.parseCommit(head.getLeaf().getObjectId()); - } else { - return revWalk.parseCommit(ObjectId.fromString(revisionBranchOrTag)); - } - } - } -} diff --git a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/GitCommitUtils.java b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/GitCommitUtils.java new file mode 100644 index 000000000..67cfd0c16 --- /dev/null +++ b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/GitCommitUtils.java @@ -0,0 +1,45 @@ +package com.teamscale.maven; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.lib.Repository; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * Represents a single commit in a Git repository. + */ +public class GitCommitUtils { + + /** + * Determines the current HEAD commit in the Git repository located in the or above the given search directory. + * + * @throws IOException if reading from the Git repository fails or the current directory is not a Git repository. + */ + public static String getGitHeadRevision(Path searchDirectory) throws IOException { + Path gitDirectory = findGitBaseDirectory(searchDirectory); + if (gitDirectory == null) { + throw new IOException("Could not find git directory in " + searchDirectory); + } + Repository repository; + try (Git git = Git.open(gitDirectory.toFile())) { + repository = git.getRepository(); + return repository.getRefDatabase().findRef("HEAD").getObjectId().getName(); + } + } + + /** + * Traverses the directory tree upwards until it finds a .git directory. Returns null if no .git directory is + * found. + */ + private static Path findGitBaseDirectory(Path searchDirectory) { + while (searchDirectory != null) { + if (Files.exists(searchDirectory.resolve(".git"))) { + return searchDirectory; + } + searchDirectory = searchDirectory.getParent(); + } + return null; + } +} diff --git a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/TeamscaleMojoBase.java b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/TeamscaleMojoBase.java index 973f5f047..ae016af6b 100644 --- a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/TeamscaleMojoBase.java +++ b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/TeamscaleMojoBase.java @@ -1,8 +1,5 @@ package com.teamscale.maven; -import java.io.IOException; -import java.nio.file.Path; - import org.apache.commons.lang3.StringUtils; import org.apache.maven.execution.MavenSession; import org.apache.maven.model.Plugin; @@ -14,9 +11,12 @@ import org.apache.maven.project.MavenProject; import org.codehaus.plexus.util.xml.Xpp3Dom; +import java.io.IOException; +import java.nio.file.Path; + /** - * A base class for all Teamscale related maven Mojos. - * Offers basic attributes and functionality related to Teamscale and Maven. + * A base class for all Teamscale related maven Mojos. Offers basic attributes and functionality related to Teamscale + * and Maven. */ public abstract class TeamscaleMojoBase extends AbstractMojo { @@ -40,8 +40,8 @@ public abstract class TeamscaleMojoBase extends AbstractMojo { public String username; /** - * Teamscale access token of the {@link #username}. Can also be specified via the Maven property {@code - * teamscale.accessToken}. + * Teamscale access token of the {@link #username}. Can also be specified via the Maven property + * {@code teamscale.accessToken}. */ @Parameter(property = "teamscale.accessToken") public String accessToken; @@ -56,15 +56,15 @@ public abstract class TeamscaleMojoBase extends AbstractMojo { public String commit; /** - * You can optionally use this property to override the revision to which the coverage will be uploaded. - * If no revision is manually specified, the plugin will try to determine the current git revision. + * You can optionally use this property to override the revision to which the coverage will be uploaded. If no + * revision is manually specified, the plugin will try to determine the current git revision. */ @Parameter(property = "teamscale.revision") 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. + * 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. */ @Parameter(property = "teamscale.repository") public String repository; @@ -91,25 +91,6 @@ public abstract class TeamscaleMojoBase extends AbstractMojo { */ protected String resolvedRevision; - /** - * Sets the resolvedCommit and resolvedRevision, if not provided, via the GitCommit class - * @see GitCommit - */ - protected void resolveCommit() throws MojoFailureException { - if (StringUtils.isNotBlank(commit)) { - resolvedCommit = commit; - return; - } - Path basedir = session.getCurrentProject().getBasedir().toPath(); - try { - GitCommit commit = GitCommit.getGitHeadCommitDescriptor(basedir); - resolvedCommit = commit.branch + ":" + commit.timestamp; - } catch (IOException e) { - throw new MojoFailureException("There is no configured in the pom.xml" + - " and it was not possible to determine the checked out commit in " + basedir + " from Git", e); - } - } - @Override public void execute() throws MojoExecutionException, MojoFailureException { if (StringUtils.isNotEmpty(revision) && StringUtils.isNotBlank(commit)) { @@ -119,29 +100,34 @@ public void execute() throws MojoExecutionException, MojoFailureException { } /** - * Sets the resolvedRevision, if not provided, via the GitCommit class + * Sets the resolvedRevision or resolvedCommit. If not provided, try to determine the + * revision via the GitCommit class. * - * @see GitCommit + * @see GitCommitUtils */ - protected void resolveRevision() throws MojoFailureException { + protected void resolveCommitOrRevision() throws MojoFailureException { if (StringUtils.isNotBlank(revision)) { resolvedRevision = revision; - } else { - Path basedir = session.getCurrentProject().getBasedir().toPath(); - try { - GitCommit commit = GitCommit.getGitHeadCommitDescriptor(basedir); - resolvedRevision = commit.sha1; - } catch (IOException e) { - throw new MojoFailureException("There is no configured in the pom.xml" + - " and it was not possible to determine the current revision in " + basedir + " from Git", e); - } + return; + } + if (StringUtils.isNotBlank(commit)) { + resolvedCommit = commit; + return; + } + Path basedir = session.getCurrentProject().getBasedir().toPath(); + try { + resolvedRevision = GitCommitUtils.getGitHeadRevision(basedir); + } catch (IOException e) { + throw new MojoFailureException("There is no or configured in the pom.xml" + + " and it was not possible to determine the current revision in " + basedir + " from Git", e); } } /** * Retrieves the configuration of a goal execution for the given plugin + * * @param pluginArtifact The id of the plugin - * @param pluginGoal The name of the goal + * @param pluginGoal The name of the goal * @return The configuration DOM if present, otherwise null */ protected Xpp3Dom getExecutionConfigurationDom(MavenProject project, String pluginArtifact, String pluginGoal) { diff --git a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TiaMojoBase.java b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TiaMojoBase.java index 8f27150c9..0f045e85a 100644 --- a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TiaMojoBase.java +++ b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TiaMojoBase.java @@ -1,16 +1,6 @@ package com.teamscale.maven.tia; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.ServerSocket; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Collections; -import java.util.Map; -import java.util.Properties; - +import com.teamscale.maven.TeamscaleMojoBase; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.util.Strings; @@ -25,7 +15,16 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import com.teamscale.maven.TeamscaleMojoBase; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.Map; +import java.util.Properties; /** * Base class for TIA Mojos. Provides all necessary functionality but can be subclassed to change the partition. @@ -189,8 +188,7 @@ public void execute() throws MojoFailureException, MojoExecutionException { targetDirectory = Paths.get(projectBuildDir, "tia").toAbsolutePath(); createTargetDirectory(); - resolveCommit(); - resolveRevision(); + resolveCommitOrRevision(); setTiaProperties(); diff --git a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/upload/CoverageUploadMojo.java b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/upload/CoverageUploadMojo.java index 4ac1828ed..915e411a5 100644 --- a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/upload/CoverageUploadMojo.java +++ b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/upload/CoverageUploadMojo.java @@ -1,15 +1,8 @@ package com.teamscale.maven.upload; -import java.io.File; -import java.io.IOException; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - +import com.google.common.base.Strings; +import com.teamscale.maven.TeamscaleMojoBase; +import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; @@ -17,14 +10,20 @@ import org.apache.maven.plugins.annotations.ResolutionScope; import org.apache.maven.project.MavenProject; import org.codehaus.plexus.util.xml.Xpp3Dom; - -import com.google.common.base.Strings; -import com.teamscale.maven.TeamscaleMojoBase; - import shadow.com.teamscale.client.CommitDescriptor; import shadow.com.teamscale.client.EReportFormat; import shadow.com.teamscale.client.TeamscaleClient; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + /** * Run this goal after the Jacoco report generation to upload them to a * configured Teamscale instance. The configuration can be specified in the root @@ -116,7 +115,8 @@ public class CoverageUploadMojo extends TeamscaleMojoBase { private TeamscaleClient teamscaleClient; @Override - public void execute() throws MojoFailureException { + public void execute() throws MojoFailureException, MojoExecutionException { + super.execute(); if (skip) { getLog().debug("Skipping since skip is set to true"); return; @@ -127,8 +127,7 @@ public void execute() throws MojoFailureException { } teamscaleClient = new TeamscaleClient(teamscaleUrl, username, accessToken, projectId); getLog().debug("Resolving end commit"); - resolveCommit(); - resolveRevision(); + resolveCommitOrRevision(); getLog().debug("Parsing Jacoco plugin configurations"); parseJacocoConfiguration(); try { From db3289abb1fae2c6ec46520c6ab7a537e2135df4 Mon Sep 17 00:00:00 2001 From: Florian Dreier Date: Tue, 24 Sep 2024 10:26:48 +0200 Subject: [PATCH 066/186] TS-40425 Fixed Gradle deprecation warnings --- build.gradle.kts | 1 + buildSrc/src/main/kotlin/com.teamscale.publish.gradle.kts | 5 +---- settings.gradle.kts | 6 ++++++ system-tests/kotlin-inline-function-test/build.gradle.kts | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index c1a5048f2..6a2fcdc09 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(libs.plugins.versions) alias(libs.plugins.nexusPublish) + kotlin("jvm") apply false } group = "com.teamscale" diff --git a/buildSrc/src/main/kotlin/com.teamscale.publish.gradle.kts b/buildSrc/src/main/kotlin/com.teamscale.publish.gradle.kts index aa7a9333b..751f9e65b 100644 --- a/buildSrc/src/main/kotlin/com.teamscale.publish.gradle.kts +++ b/buildSrc/src/main/kotlin/com.teamscale.publish.gradle.kts @@ -1,5 +1,3 @@ -import com.github.jengelman.gradle.plugins.shadow.ShadowExtension - plugins { java `maven-publish` @@ -42,8 +40,7 @@ fun PublicationContainer.configureMavenPublication() { val publication = this var hasShadow = false pluginManager.withPlugin("com.teamscale.shadow-convention") { - val shadowExtension = extensions.getByName("shadow") - shadowExtension.component(publication) + publication.from(components.findByName("shadow")) setArtifacts(listOf(tasks["shadowJar"])) artifact(tasks["sourcesJar"]) artifact(tasks["javadocJar"]) diff --git a/settings.gradle.kts b/settings.gradle.kts index ff40862d7..bd9c9c7fd 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,9 @@ +pluginManagement { + plugins { + kotlin("jvm") version "1.9.24" + } +} + plugins { id("org.gradle.toolchains.foojay-resolver-convention") version("0.8.0") } diff --git a/system-tests/kotlin-inline-function-test/build.gradle.kts b/system-tests/kotlin-inline-function-test/build.gradle.kts index df45d8731..fefee92b2 100644 --- a/system-tests/kotlin-inline-function-test/build.gradle.kts +++ b/system-tests/kotlin-inline-function-test/build.gradle.kts @@ -1,6 +1,6 @@ plugins { com.teamscale.`system-test-convention` - kotlin("jvm") version "1.9.24" + kotlin("jvm") } tasks.test { From b1448b0b1f4b582345a93dff8ec6ae7ad97fcb75 Mon Sep 17 00:00:00 2001 From: Florian Dreier Date: Tue, 24 Sep 2024 11:15:17 +0200 Subject: [PATCH 067/186] TS-40425 Always prefer revision over commit if explicitly given --- .../com/teamscale/tia/TiaMavenSystemTest.java | 25 +++++++++---------- .../teamscale/client/CommitDescriptor.java | 3 +++ .../com/teamscale/maven/tia/TiaMojoBase.java | 10 +++----- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/system-tests/tia-maven/src/test/java/com/teamscale/tia/TiaMavenSystemTest.java b/system-tests/tia-maven/src/test/java/com/teamscale/tia/TiaMavenSystemTest.java index 337df1d59..78b55976d 100644 --- a/system-tests/tia-maven/src/test/java/com/teamscale/tia/TiaMavenSystemTest.java +++ b/system-tests/tia-maven/src/test/java/com/teamscale/tia/TiaMavenSystemTest.java @@ -1,18 +1,17 @@ package com.teamscale.tia; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; - -import java.io.IOException; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - import com.teamscale.report.testwise.model.ETestExecutionResult; import com.teamscale.report.testwise.model.TestwiseCoverageReport; import com.teamscale.test.commons.SystemTestUtils; import com.teamscale.test.commons.TeamscaleMockServer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; /** * Runs several Maven projects' Surefire tests that have the agent attached and one of our JUnit run listeners enabled. @@ -78,10 +77,10 @@ public void testMavenTia() throws Exception { public void testPreferBranchAndTimestampOverRevisionWhenProvidedManually() throws IOException { SystemTestUtils.runMavenTests("maven-project", "-DteamscaleRevision=abcd1337", "-DteamscaleTimestamp=master:HEAD"); - assertThat(teamscaleMockServer.impactedTestCommits.get(0)).matches("null, master:HEAD"); - assertThat(teamscaleMockServer.impactedTestCommits.get(1)).matches("null, master:HEAD"); - assertThat(teamscaleMockServer.uploadCommits.get(0)).matches("null, master:HEAD"); - assertThat(teamscaleMockServer.uploadCommits.get(1)).matches("null, master:HEAD"); + assertThat(teamscaleMockServer.impactedTestCommits.get(0)).matches("abcd1337, null"); + assertThat(teamscaleMockServer.impactedTestCommits.get(1)).matches("abcd1337, null"); + assertThat(teamscaleMockServer.uploadCommits.get(0)).matches("abcd1337, null"); + assertThat(teamscaleMockServer.uploadCommits.get(1)).matches("abcd1337, null"); } @Test diff --git a/teamscale-client/src/main/java/com/teamscale/client/CommitDescriptor.java b/teamscale-client/src/main/java/com/teamscale/client/CommitDescriptor.java index 08a5e4ed0..fee858ff7 100644 --- a/teamscale-client/src/main/java/com/teamscale/client/CommitDescriptor.java +++ b/teamscale-client/src/main/java/com/teamscale/client/CommitDescriptor.java @@ -28,6 +28,9 @@ public CommitDescriptor(String branchName, long 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]); diff --git a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TiaMojoBase.java b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TiaMojoBase.java index 0f045e85a..23be7bbf0 100644 --- a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TiaMojoBase.java +++ b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TiaMojoBase.java @@ -204,10 +204,10 @@ private void setTiaProperties() { setTiaProperty("server.userName", username); setTiaProperty("server.userAccessToken", accessToken); - if (StringUtils.isNotEmpty(commit)) { - setTiaProperty("endCommit", resolvedCommit); - } else { + if (StringUtils.isNotEmpty(resolvedRevision)) { setTiaProperty("endRevision", resolvedRevision); + } else { + setTiaProperty("endCommit", resolvedCommit); } if (StringUtils.isNotEmpty(baselineRevision)) { @@ -416,9 +416,7 @@ private String createAgentConfig(Path loggingConfigPath, Path agentOutputDirecto config += "\nteamscale-repository=" + repository; } - // "commit" (in contrast to "resolvedCommit") is only set via the config option in the pom. - // If the user sets it, prefer it over the revision. If not, prefer the revision - if (StringUtils.isNotEmpty(resolvedRevision) && StringUtils.isEmpty(commit)) { + if (StringUtils.isNotEmpty(resolvedRevision)) { config += "\nteamscale-revision=" + resolvedRevision; } else { config += "\nteamscale-commit=" + resolvedCommit; From 64855a233371921e031d410183caa9f025fff8ee Mon Sep 17 00:00:00 2001 From: Jonas Bogenberger Date: Tue, 24 Sep 2024 12:29:56 +0200 Subject: [PATCH 068/186] TS-40412 Test for TeamscaleProxyOptions --- .../agent/options/AgentOptionsTest.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) 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 d0020f8fa..d97d2eb7c 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 @@ -1,6 +1,8 @@ package com.teamscale.jacoco.agent.options; import com.teamscale.client.CommitDescriptor; +import com.teamscale.client.ProxySystemProperties; +import com.teamscale.client.TeamscaleProxySystemProperties; import com.teamscale.client.TeamscaleServer; import com.teamscale.jacoco.agent.upload.artifactory.ArtifactoryConfig; import com.teamscale.jacoco.agent.util.TestUtils; @@ -324,6 +326,43 @@ public void testArtifactoryApiKeySetPassesValidityCheck() throws Exception { assertThat(agentOptions.getValidator().isValid()).isTrue(); } + /** + * Tests that the {@link TeamscaleProxyOptions} are parsed correctly and correctly put into + * system properties that can be read using {@link TeamscaleProxySystemProperties}. + * */ + @Test + public void testTeamscaleProxyOptionsCorrectlySetSystemProperties() throws Exception { + String expectedHost = "host"; + int expectedPort = 9999; + String expectedUser = "user"; + String expectedPassword = "password"; + String optionsString = String.format("proxy-host=%s,proxy-port=%d,proxy-user=%s,proxy-password=%s", expectedHost, expectedPort, expectedUser, expectedPassword); + AgentOptions agentOptions = getAgentOptionsParserWithDummyLogger().parse(optionsString); + agentOptions.getTeamscaleProxyOptions().putTeamscaleProxyOptionsIntoSystemProperties(); + + assertTeamscaleProxySystemPropertiesAreCorrect(ProxySystemProperties.Protocol.HTTP, expectedHost, expectedPort, expectedUser, expectedPassword); + assertTeamscaleProxySystemPropertiesAreCorrect(ProxySystemProperties.Protocol.HTTPS, expectedHost, expectedPort, expectedUser, expectedPassword); + + clearTeamscaleProxySystemProperties(ProxySystemProperties.Protocol.HTTP); + clearTeamscaleProxySystemProperties(ProxySystemProperties.Protocol.HTTPS); + } + + private void clearTeamscaleProxySystemProperties(ProxySystemProperties.Protocol protocol) { + TeamscaleProxySystemProperties teamscaleProxySystemProperties = new TeamscaleProxySystemProperties(protocol); + teamscaleProxySystemProperties.setProxyHost(""); + teamscaleProxySystemProperties.setProxyPort(""); + teamscaleProxySystemProperties.setProxyUser(""); + teamscaleProxySystemProperties.setProxyPassword(""); + } + + private void assertTeamscaleProxySystemPropertiesAreCorrect(ProxySystemProperties.Protocol protocol, String expectedHost, int expectedPort, String expectedUser, String expectedPassword) { + TeamscaleProxySystemProperties teamscaleProxySystemProperties = new TeamscaleProxySystemProperties(protocol); + assertThat(teamscaleProxySystemProperties.getProxyHost()).isEqualTo(expectedHost); + assertThat(teamscaleProxySystemProperties.getProxyPort()).isEqualTo(expectedPort); + assertThat(teamscaleProxySystemProperties.getProxyUser()).isEqualTo(expectedUser); + assertThat(teamscaleProxySystemProperties.getProxyPassword()).isEqualTo(expectedPassword); + } + /** Returns the include filter predicate for the given filter expression. */ private static Predicate includeFilter(String filterString) throws Exception { AgentOptions agentOptions = getAgentOptionsParserWithDummyLogger() From 4b52e32106f7840fb9d4aca6b1bbbbf390b6bde9 Mon Sep 17 00:00:00 2001 From: Jonas Bogenberger Date: Tue, 24 Sep 2024 12:41:08 +0200 Subject: [PATCH 069/186] TS-40412 small fix and typo --- .../teamscale/jacoco/agent/options/AgentOptionsTest.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 d97d2eb7c..852723e86 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 @@ -303,7 +303,7 @@ public void testArtifactoryApiKeyOptionIsCorrectlyParsed() throws Exception { * {@link ArtifactoryConfig#ARTIFACTORY_URL_OPTION}) passes the AgentOptions' validity check. */ @Test - public void testArtifactoryBasicAuthSetPassesValiditiyCheck() throws Exception { + public void testArtifactoryBasicAuthSetPassesValidityCheck() throws Exception { AgentOptions agentOptions = getAgentOptionsParserWithDummyLogger().parse(""); agentOptions.artifactoryConfig.url = HttpUrl.get("http://some_url"); agentOptions.artifactoryConfig.user = "user"; @@ -338,6 +338,11 @@ public void testTeamscaleProxyOptionsCorrectlySetSystemProperties() throws Excep String expectedPassword = "password"; String optionsString = String.format("proxy-host=%s,proxy-port=%d,proxy-user=%s,proxy-password=%s", expectedHost, expectedPort, expectedUser, expectedPassword); AgentOptions agentOptions = getAgentOptionsParserWithDummyLogger().parse(optionsString); + + // clear to be sure the system properties are empty + clearTeamscaleProxySystemProperties(ProxySystemProperties.Protocol.HTTP); + clearTeamscaleProxySystemProperties(ProxySystemProperties.Protocol.HTTPS); + agentOptions.getTeamscaleProxyOptions().putTeamscaleProxyOptionsIntoSystemProperties(); assertTeamscaleProxySystemPropertiesAreCorrect(ProxySystemProperties.Protocol.HTTP, expectedHost, expectedPort, expectedUser, expectedPassword); From 2ae672096a1bc224f1d809da15c58ad56d40f418 Mon Sep 17 00:00:00 2001 From: Jonas Bogenberger Date: Tue, 24 Sep 2024 13:27:29 +0200 Subject: [PATCH 070/186] TS-40412 adjust changelog and documentation --- CHANGELOG.md | 1 + agent/README.md | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ef04cba4..f8c192ef5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ We use [semantic versioning](http://semver.org/): # Next version - [feature] _agent_: Prevent uploading coverage to the same project + revision or branch@timestamp when doing multi project upload via git.properties +- [feature] _agent_: New options `proxy-host`/`-port`/`-user`/`-password` allows user to specify teamscale-specific proxy settings. # 34.0.1 - [fix] _agent_: Error was reported when the system under test used logback diff --git a/agent/README.md b/agent/README.md index e3aff930c..9759f1bb4 100644 --- a/agent/README.md +++ b/agent/README.md @@ -63,6 +63,7 @@ The following options are available: `logging` folder, e.g. to enable debug logging or log directly to the console. (For details see path format section below) - `proxy-password-file` (optional): path to a file that contains the password for a proxy server authentication. This file may only contain the password and nothing else. +- `proxy-` (optional): For other options regarding teamscale-specific proxy settings see [Teamscale-Specific-Proxy Section](#teamscale-specific-proxy-settings). - `mode` (optional): which coverage collection mode to use. Can be either `normal` or `testwise` (Default is `normal`) - `debug` (optional): `true`, `false` or a path to which the logs should be written to. `true` if no explicit value given. This option turns on debug mode. The logs will be written to console and the given file path. If no file path is given, @@ -87,6 +88,26 @@ Note that defining `-Djava.io.tmpdir` will change the temp directory that is bei If there is no log file at that location, it means the agent didn't even start and you have not configured it correctly. Check your applications console output for error messages. +#### Teamscale-Specific Proxy Settings +It is possible to specify teamscale-specific proxy settings that take prevalence over the system properties of the JVM. +If only teamscale-specific proxy server settings are provided and no teamscale-specific proxy authentication settings, +the agent will fall back to the system properties of the JVM, and the other way around. + +It is also possible to specify these options by prefixing the JVM flags for proxies with `teamscale.`, +for example `-Dteamscale.https.proxyHost`. + +The following options are available: + +##### Server Settings +If only one of the two options is provided, the teamscale specific proxy server settings are ignored. +- `proxy-host`: The host name of the proxy server. +- `proxy-port`: The port of the proxy server. + +##### Authentication Settings +If only one of the two options is provided, the teamscale specific proxy authentication settings are ignored. +- `proxy-user`: The username for the proxy server. +- `proxy-password`: The password for the proxy user. + #### Testwise coverage If you want to collect testwise coverage, please have a look below in the [Testwise mode section](#testwise-coverage-modes). From d1b2c4f7066d55645cc3710cf396644615e0a136 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 25 Sep 2024 11:39:09 +0000 Subject: [PATCH 071/186] Update dependency org.junit.jupiter:junit-jupiter-engine to v5.11.1 --- teamscale-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teamscale-maven-plugin/pom.xml b/teamscale-maven-plugin/pom.xml index b5bf203c2..f058f3974 100644 --- a/teamscale-maven-plugin/pom.xml +++ b/teamscale-maven-plugin/pom.xml @@ -65,7 +65,7 @@ org.junit.jupiter junit-jupiter-engine - 5.11.0 + 5.11.1 test From ce8ce1257460745949faea44e66b0dfbb7f8d3d6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 25 Sep 2024 11:39:14 +0000 Subject: [PATCH 072/186] Update dependency org.junit.vintage:junit-vintage-engine to v5.11.1 --- .../src/test/resources/calculator_groovy/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teamscale-gradle-plugin/src/test/resources/calculator_groovy/build.gradle b/teamscale-gradle-plugin/src/test/resources/calculator_groovy/build.gradle index cde58c34b..d1f52c3af 100644 --- a/teamscale-gradle-plugin/src/test/resources/calculator_groovy/build.gradle +++ b/teamscale-gradle-plugin/src/test/resources/calculator_groovy/build.gradle @@ -14,7 +14,7 @@ plugins { import com.teamscale.TestImpacted ext.junit4Version = '4.13.2' -ext.junitVintageVersion = '5.11.0' +ext.junitVintageVersion = '5.11.1' ext.junitPlatformVersion = '1.4.0' ext.junitJupiterVersion = '5.11.0' From e692b1380323b302db362304132f46ae08f96de2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 25 Sep 2024 14:04:17 +0000 Subject: [PATCH 073/186] Update junit to v5.11.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 37a8dd518..692b0e112 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ jacoco = "0.8.12" # We need to stay on the 1.3.x release line as 1.4.x requires Java 11 logback = "1.3.14" retrofit = "2.11.0" -junit = "5.11.0" +junit = "5.11.1" junitPlatform = "1.11.0" okhttp = "4.12.0" mockito = "4.11.0" From b86a6be2471a68ec6d06bcc2e1374df3187a8715 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 25 Sep 2024 14:04:21 +0000 Subject: [PATCH 074/186] Update junitJupiterVersion to v5.11.1 --- .../src/test/resources/calculator_groovy/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teamscale-gradle-plugin/src/test/resources/calculator_groovy/build.gradle b/teamscale-gradle-plugin/src/test/resources/calculator_groovy/build.gradle index cde58c34b..edc240388 100644 --- a/teamscale-gradle-plugin/src/test/resources/calculator_groovy/build.gradle +++ b/teamscale-gradle-plugin/src/test/resources/calculator_groovy/build.gradle @@ -16,7 +16,7 @@ import com.teamscale.TestImpacted ext.junit4Version = '4.13.2' ext.junitVintageVersion = '5.11.0' ext.junitPlatformVersion = '1.4.0' -ext.junitJupiterVersion = '5.11.0' +ext.junitJupiterVersion = '5.11.1' if (!project.hasProperty("withoutServerConfig")) { teamscale { From c95744354fea831643a26562c3c6d2c9c5b0d29d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 25 Sep 2024 17:09:31 +0000 Subject: [PATCH 075/186] Update junitPlatform to v1.11.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 692b0e112..84457b4f4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ jacoco = "0.8.12" logback = "1.3.14" retrofit = "2.11.0" junit = "5.11.1" -junitPlatform = "1.11.0" +junitPlatform = "1.11.1" okhttp = "4.12.0" mockito = "4.11.0" picocli = "4.7.6" From 5e3fa474943c7d3ae2f5e28a808b227d544f0585 Mon Sep 17 00:00:00 2001 From: Florian Dreier Date: Thu, 26 Sep 2024 15:17:22 +0200 Subject: [PATCH 076/186] TS-40425 Rework --- .../src/main/java/com/teamscale/maven/GitCommitUtils.java | 2 +- .../main/java/com/teamscale/maven/TeamscaleMojoBase.java | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/GitCommitUtils.java b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/GitCommitUtils.java index 67cfd0c16..092ba3a49 100644 --- a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/GitCommitUtils.java +++ b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/GitCommitUtils.java @@ -8,7 +8,7 @@ import java.nio.file.Path; /** - * Represents a single commit in a Git repository. + * Utils for working with a Git repository. */ public class GitCommitUtils { diff --git a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/TeamscaleMojoBase.java b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/TeamscaleMojoBase.java index ae016af6b..1996d1a3b 100644 --- a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/TeamscaleMojoBase.java +++ b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/TeamscaleMojoBase.java @@ -50,14 +50,18 @@ public abstract class TeamscaleMojoBase extends AbstractMojo { * You can optionally use this property to override the code commit to which the coverage will be uploaded. Format: * {@code BRANCH:UNIX_EPOCH_TIMESTAMP_IN_MILLISECONDS} *

- * If no end commit is manually specified, the plugin will try to determine the currently checked out Git commit. + * If no commit and revision is manually specified, the plugin will try to determine the currently checked-out Git + * commit. You should specify either commit or revision, not both. If both are specified, a warning is logged and + * the revision takes precedence. */ @Parameter(property = "teamscale.commit") public String commit; /** * You can optionally use this property to override the revision to which the coverage will be uploaded. If no - * revision is manually specified, the plugin will try to determine the current git revision. + * commit and revision is manually specified, the plugin will try to determine the current git revision. You should + * specify either commit or revision, not both. If both are specified, a warning is logged and the revision takes + * precedence. */ @Parameter(property = "teamscale.revision") public String revision; From 307eaa0437349ca52fd2cf0538b2b8624bc792f3 Mon Sep 17 00:00:00 2001 From: Jonas Bogenberger Date: Thu, 26 Sep 2024 15:34:50 +0200 Subject: [PATCH 077/186] TS-40412 rework --- .../com/teamscale/jacoco/agent/AgentBase.java | 35 +++----- .../jacoco/agent/options/AgentOptions.java | 23 ++++-- .../agent/options/AgentOptionsParser.java | 20 ++++- .../agent/options/TeamscaleProxyOptions.java | 82 +++++++++++++------ .../java/com/teamscale/client/HttpUtils.java | 23 +----- .../client/ProxySystemProperties.java | 81 +++++++----------- .../TeamscaleProxySystemProperties.java | 22 +---- 7 files changed, 133 insertions(+), 153 deletions(-) diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java b/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java index 80e07b046..c42a3817a 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java @@ -4,7 +4,6 @@ import com.teamscale.client.TeamscaleProxySystemProperties; import com.teamscale.jacoco.agent.options.AgentOptions; import com.teamscale.jacoco.agent.util.LoggingUtils; -import org.conqat.lib.commons.filesystem.FileSystemUtils; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.servlet.ServletContextHandler; @@ -15,9 +14,7 @@ import org.jacoco.agent.rt.RT; import org.slf4j.Logger; -import java.io.IOException; import java.lang.management.ManagementFactory; -import java.nio.file.Path; /** * Base class for agent implementations. Handles logger shutdown, store creation and instantiation of the @@ -41,10 +38,9 @@ public abstract class AgentBase { /** Constructor. */ public AgentBase(AgentOptions options) throws IllegalStateException { this.options = options; - if (options.getTeamscaleProxyOptions() != null) { - options.getTeamscaleProxyOptions().putTeamscaleProxyOptionsIntoSystemProperties(); - } - setProxyPasswordFromFile(options.getProxyPasswordPath()); + + putTeamscaleProxyOptionsIntoSystemProperties(options); + try { controller = new JacocoRuntimeController(RT.getAgent()); } catch (IllegalStateException e) { @@ -64,23 +60,14 @@ public AgentBase(AgentOptions options) throws IllegalStateException { } } - /** Sets the proxy password JVM property from a file for both http and https. */ - private void setProxyPasswordFromFile(Path proxyPasswordFilePath) { - if (proxyPasswordFilePath == null) { - return; - } - try { - String proxyPassword = FileSystemUtils.readFileUTF8(proxyPasswordFilePath.toFile()).trim(); - new ProxySystemProperties(ProxySystemProperties.Protocol.HTTP).setProxyPassword(proxyPassword); - new ProxySystemProperties(ProxySystemProperties.Protocol.HTTPS).setProxyPassword(proxyPassword); - new TeamscaleProxySystemProperties(ProxySystemProperties.Protocol.HTTP).setProxyPassword(proxyPassword); - new TeamscaleProxySystemProperties(ProxySystemProperties.Protocol.HTTPS).setProxyPassword(proxyPassword); - } catch (IOException e) { - logger.error( - "Unable to open file containing proxy password. Please make sure the file exists and the user has the permissions to read the file.", - e); - } - + /** + * Stores the agent options for proxies in the {@link TeamscaleProxySystemProperties} and overwrites + * the password with the password found in the proxy-password-file if necessary. + * public for testing. + */ + public static void putTeamscaleProxyOptionsIntoSystemProperties(AgentOptions options) { + options.getTeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTP).putTeamscaleProxyOptionsIntoSystemProperties(); + options.getTeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTPS).putTeamscaleProxyOptionsIntoSystemProperties(); } /** diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptions.java b/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptions.java index ec5c84b2c..995de5058 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptions.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptions.java @@ -7,6 +7,7 @@ import com.teamscale.client.EReportFormat; import com.teamscale.client.FileSystemUtils; +import com.teamscale.client.ProxySystemProperties; import com.teamscale.client.StringUtils; import com.teamscale.client.TeamscaleClient; import com.teamscale.client.TeamscaleServer; @@ -108,8 +109,13 @@ public class AgentOptions { */ private Path outputDirectory; - /** Contains the options related to teamscale-specific proxy settings. */ - /* package */ TeamscaleProxyOptions teamscaleProxyOptions = new TeamscaleProxyOptions(); + /** Contains the options related to teamscale-specific proxy settings for http. */ + /* package */ TeamscaleProxyOptions teamscaleProxyOptionsForHttp = new TeamscaleProxyOptions( + ProxySystemProperties.Protocol.HTTP); + + /** Contains the options related to teamscale-specific proxy settings for https. */ + /* package */ TeamscaleProxyOptions teamscaleProxyOptionsForHttps = new TeamscaleProxyOptions( + ProxySystemProperties.Protocol.HTTPS); /** * Additional metadata files to upload together with the coverage XML. @@ -229,10 +235,6 @@ public String getOriginalOptionsString() { return originalOptionsString; } - public Path getProxyPasswordPath() { - return teamscaleProxyOptions.proxyPasswordPath; - } - /** * Remove parts of the API key for security reasons from the options string. String is used for logging purposes. *

@@ -725,8 +727,11 @@ public boolean shouldIgnoreUncoveredClasses() { return ignoreUncoveredClasses; } - /** @see #teamscaleProxyOptions */ - public TeamscaleProxyOptions getTeamscaleProxyOptions() { - return teamscaleProxyOptions; + /** @return the {@link TeamscaleProxyOptions} for the given protocol. */ + public TeamscaleProxyOptions getTeamscaleProxyOptions(ProxySystemProperties.Protocol protocol) { + if(protocol == ProxySystemProperties.Protocol.HTTP) { + return teamscaleProxyOptionsForHttp; + } + return teamscaleProxyOptionsForHttps; } } diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptionsParser.java b/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptionsParser.java index 9661df96c..60a4eaa46 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptionsParser.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptionsParser.java @@ -6,6 +6,7 @@ package com.teamscale.jacoco.agent.options; import com.google.common.annotations.VisibleForTesting; +import com.teamscale.client.ProxySystemProperties; import com.teamscale.client.StringUtils; import com.teamscale.jacoco.agent.commandline.Validator; import com.teamscale.jacoco.agent.configuration.AgentOptionReceiveException; @@ -25,6 +26,7 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.InvalidPathException; +import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; import java.util.List; @@ -156,8 +158,22 @@ private void handleOption(AgentOptions options, value)) { return; } - if (key.startsWith("proxy-") && TeamscaleProxyOptions.handleTeamscaleProxyOptions(options.teamscaleProxyOptions, key, value, filePatternResolver)) { - return; + String proxyKeyword = "proxy-"; + if (key.startsWith(proxyKeyword)) { + if (key.startsWith(proxyKeyword + ProxySystemProperties.Protocol.HTTPS) + && options.getTeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTPS).handleTeamscaleProxyOptions(key, value)) { + return; + } + if (key.startsWith(proxyKeyword + ProxySystemProperties.Protocol.HTTP) + && options.getTeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTP).handleTeamscaleProxyOptions(key, value)) { + return; + } + if(key.equals("proxy-password-file")) { + Path proxyPasswordPath = filePatternResolver.parsePath(key, value); + options.getTeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTPS).proxyPasswordPath=proxyPasswordPath; + options.getTeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTP).proxyPasswordPath=proxyPasswordPath; + return; + } } if (handleAgentOptions(options, key, value)) { return; diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java b/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java index f05b58ec5..4020f47c3 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java @@ -2,7 +2,11 @@ import com.teamscale.client.ProxySystemProperties; import com.teamscale.client.TeamscaleProxySystemProperties; +import com.teamscale.jacoco.agent.util.LoggingUtils; +import org.conqat.lib.commons.filesystem.FileSystemUtils; +import org.slf4j.Logger; +import java.io.IOException; import java.nio.file.Path; /** @@ -10,6 +14,8 @@ */ public class TeamscaleProxyOptions { + private final Logger logger = LoggingUtils.getLogger(this); + /** The host of the proxy server. */ /* package */ String proxyHost; @@ -25,52 +31,74 @@ public class TeamscaleProxyOptions { /** The username of the proxy user. */ /* package */ String proxyUser; + private final ProxySystemProperties.Protocol protocol; + + /** Constructor. */ + public TeamscaleProxyOptions(ProxySystemProperties.Protocol protocol) { + this.protocol = protocol; + ProxySystemProperties proxySystemProperties = new ProxySystemProperties(protocol); + proxyHost = proxySystemProperties.getProxyHost(); + proxyPort = proxySystemProperties.getProxyPort(); + proxyUser = proxySystemProperties.getProxyUser(); + proxyPassword = proxySystemProperties.getProxyPassword(); + } /** * Handles all command-line options prefixed with 'proxy-' * * @return true if it has successfully processed the given option. */ - public static boolean handleTeamscaleProxyOptions(TeamscaleProxyOptions options, String key, String value, FilePatternResolver filePatternResolver) throws AgentOptionParseException { - switch (key) { - case "proxy-password-file": - options.proxyPasswordPath = filePatternResolver.parsePath(key, value); + public boolean handleTeamscaleProxyOptions(String key, String value) { + if (String.format("proxy-%s-host", protocol).equals(key)){ + proxyHost = value; return true; - case "proxy-host": - options.proxyHost = value; + } + if (String.format("proxy-%s-port", protocol).equals(key)) { + proxyPort = Integer.parseInt(value); return true; - case "proxy-port": - options.proxyPort = Integer.parseInt(value); + } + if (String.format("proxy-%s-user", protocol).equals(key)) { + proxyUser = value; return true; - case "proxy-user": - options.proxyUser = value; + } + if (String.format("proxy-%s-password", protocol).equals(key)) { + proxyPassword = value; return true; - case "proxy-password": - options.proxyPassword = value; - return true; - default: - return false; - } + } + return false; } /** Stores the teamscale-specific proxy settings as system properties to make them always available. */ public void putTeamscaleProxyOptionsIntoSystemProperties() { - putTeamscaleProxyOptionsIntoSystemPropertiesForProtocol(ProxySystemProperties.Protocol.HTTP); - putTeamscaleProxyOptionsIntoSystemPropertiesForProtocol(ProxySystemProperties.Protocol.HTTPS); - } - - private void putTeamscaleProxyOptionsIntoSystemPropertiesForProtocol(ProxySystemProperties.Protocol protocol) { - if (proxyHost != null) { - new TeamscaleProxySystemProperties(protocol).setProxyHost(proxyHost); + TeamscaleProxySystemProperties teamscaleProxySystemProperties = new TeamscaleProxySystemProperties(protocol); + if (proxyHost != null && !proxyHost.isEmpty()) { + teamscaleProxySystemProperties.setProxyHost(proxyHost); } if (proxyPort > 0) { - new TeamscaleProxySystemProperties(protocol).setProxyPort(proxyPort); + teamscaleProxySystemProperties.setProxyPort(proxyPort); + } + if(proxyUser != null && !proxyUser.isEmpty()) { + teamscaleProxySystemProperties.setProxyUser(proxyUser); + } + if(proxyPassword != null && !proxyPassword.isEmpty()) { + teamscaleProxySystemProperties.setProxyPassword(proxyPassword); } - if(proxyUser != null) { - new TeamscaleProxySystemProperties(protocol).setProxyUser(proxyUser); + + setProxyPasswordFromFile(proxyPasswordPath); + } + + /** Sets the proxy password JVM property from a file for the protocol in this instance of {@link TeamscaleProxyOptions}. */ + private void setProxyPasswordFromFile(Path proxyPasswordFilePath) { + if (proxyPasswordFilePath == null) { + return; } - if(proxyPassword != null) { + try { + String proxyPassword = FileSystemUtils.readFileUTF8(proxyPasswordFilePath.toFile()).trim(); new TeamscaleProxySystemProperties(protocol).setProxyPassword(proxyPassword); + } catch (IOException e) { + logger.error( + "Unable to open file containing proxy password. Please make sure the file exists and the user has the permissions to read the file.", + 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 index 7bb5d765f..a710be367 100644 --- a/teamscale-client/src/main/java/com/teamscale/client/HttpUtils.java +++ b/teamscale-client/src/main/java/com/teamscale/client/HttpUtils.java @@ -103,30 +103,15 @@ private static void setUpProxyServer(OkHttpClient.Builder httpClientBuilder) { private static boolean setUpProxyServerForProtocol(ProxySystemProperties.Protocol protocol, OkHttpClient.Builder httpClientBuilder) { TeamscaleProxySystemProperties teamscaleProxySystemProperties = new TeamscaleProxySystemProperties(protocol); - ProxySystemProperties defaultProxySystemProperties = new ProxySystemProperties(protocol); - - return setUpProxyServerAndAuthentication(teamscaleProxySystemProperties, defaultProxySystemProperties, httpClientBuilder); - } - - private static boolean setUpProxyServerAndAuthentication(TeamscaleProxySystemProperties teamscaleProxySystemProperties, ProxySystemProperties defaultProxySystemProperties, OkHttpClient.Builder httpClientBuilder) { - // It is allowed to use for example global server settings, but teamscale-specific user settings. - // It is not allowed to mix global and teamscale-specific settings inside those categories, so for example - // user -> global setting - // password teamscale-specific - // is forbidden. - if (teamscaleProxySystemProperties.proxyServerIsSet()) { - useProxyServer(httpClientBuilder, teamscaleProxySystemProperties.getProxyHost(), - teamscaleProxySystemProperties.getProxyPort()); - } else if (defaultProxySystemProperties.proxyServerIsSet()) { - useProxyServer(httpClientBuilder, defaultProxySystemProperties.getProxyHost(), defaultProxySystemProperties.getProxyPort()); - } else { + if (!teamscaleProxySystemProperties.proxyServerIsSet()) { return false; } + useProxyServer(httpClientBuilder, teamscaleProxySystemProperties.getProxyHost(), + teamscaleProxySystemProperties.getProxyPort()); + if (teamscaleProxySystemProperties.proxyAuthIsSet()) { useProxyAuthenticator(httpClientBuilder, teamscaleProxySystemProperties.getProxyUser(), teamscaleProxySystemProperties.getProxyPassword()); - } else if (defaultProxySystemProperties.proxyAuthIsSet()) { - useProxyAuthenticator(httpClientBuilder, defaultProxySystemProperties.getProxyUser(), defaultProxySystemProperties.getProxyPassword()); } return true; diff --git a/teamscale-client/src/main/java/com/teamscale/client/ProxySystemProperties.java b/teamscale-client/src/main/java/com/teamscale/client/ProxySystemProperties.java index f7c919af7..96ac45696 100644 --- a/teamscale-client/src/main/java/com/teamscale/client/ProxySystemProperties.java +++ b/teamscale-client/src/main/java/com/teamscale/client/ProxySystemProperties.java @@ -19,20 +19,12 @@ public class ProxySystemProperties { private static final Logger LOGGER = LoggerFactory.getLogger(ProxySystemProperties.class); - /** The property suffix specifying the proxy host. */ - protected static final String PROXY_HOST_SYSTEM_PROPERTY = ".proxyHost"; + 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"; - /** The property suffix specifying the proxy port. */ - protected static final String PROXY_PORT_SYSTEM_PROPERTY = ".proxyPort"; - - /** The property suffix specifying the proxy user. */ - protected static final String PROXY_USER_SYSTEM_PROPERTY = ".proxyUser"; - - /** The property suffix specifying the proxy password. */ - protected static final String PROXY_PASSWORD_SYSTEM_PROPERTY = ".proxyPassword"; - - /** The protocol used by the proxy server addressed by these settings. */ - protected final Protocol protocol; + private final Protocol protocol; /** * Indicates, whether the {@link ProxySystemProperties} should return values for the http.proxy* system properties @@ -48,6 +40,15 @@ public String toString() { } } + /** + * @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 @@ -63,31 +64,22 @@ public boolean proxyServerIsSet() { return !StringUtils.isEmpty(getProxyHost()) && getProxyPort() > 0; } - /** - * Checks whether proxyUser and proxyPassword are set - */ + /** Checks whether proxyUser and proxyPassword are set */ public boolean proxyAuthIsSet() { return !StringUtils.isEmpty(getProxyUser()) && !StringUtils.isEmpty(getProxyPassword()); } - /** - * Read the http(s).proxyHost system variable - */ + /** Read the http(s).proxyHost system variable */ public String getProxyHost() { return System.getProperty(getProxyHostSystemPropertyName()); } - /** - * Read the http(s).proxyPort system variable. - * Returns -1 if no or an invalid port was set. - */ + /** Read the http(s).proxyPort system variable. Returns -1 if no or an invalid port was set. */ public int getProxyPort() { return parsePort(System.getProperty(getProxyPortSystemPropertyName())); } - /** - * Set the http(s).proxyHost system variable - */ + /** Set the http(s).proxyHost system variable. */ public void setProxyHost(String proxyHost) { System.setProperty(getProxyHostSystemPropertyName(), proxyHost); } @@ -95,27 +87,20 @@ public void setProxyHost(String proxyHost) { /** @return the name of the system property specifying the proxy host. */ @NotNull protected String getProxyHostSystemPropertyName() { - return protocol + PROXY_HOST_SYSTEM_PROPERTY; + return getPropertyPrefix() + protocol + PROXY_HOST_SYSTEM_PROPERTY; } - /** - * Set the http(s).proxyPort system variable - */ + /** Set the http(s).proxyPort system variable. */ public void setProxyPort(int proxyPort) { setProxyPort(proxyPort + ""); } - /** - * Set the http(s).proxyPort system variable - */ + /** 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. - */ + /** Removes the http(s).proxyPort system variable. For testing. */ /*package*/ void removeProxyPort() { System.clearProperty(getProxyPortSystemPropertyName()); } @@ -123,19 +108,15 @@ public void setProxyPort(String proxyPort) { /** @return the name of the system property specifying the proxy port. */ @NotNull protected String getProxyPortSystemPropertyName() { - return protocol + PROXY_PORT_SYSTEM_PROPERTY; + return getPropertyPrefix() + protocol + PROXY_PORT_SYSTEM_PROPERTY; } - /** - * Get the http(s).proxyUser system variable - */ + /** Get the http(s).proxyUser system variable. */ public String getProxyUser() { return System.getProperty(getProxyUserSystemPropertyName()); } - /** - * Set the http(s).proxyUser system variable - */ + /** Set the http(s).proxyUser system variable. */ public void setProxyUser(String proxyUser) { System.setProperty(getProxyUserSystemPropertyName(), proxyUser); } @@ -143,20 +124,16 @@ public void setProxyUser(String proxyUser) { /** @return the name of the system property specifying the proxy user. */ @NotNull protected String getProxyUserSystemPropertyName() { - return protocol + PROXY_USER_SYSTEM_PROPERTY; + return getPropertyPrefix() + protocol + PROXY_USER_SYSTEM_PROPERTY; } - /** - * Get the http(s).proxyPassword system variable - */ + /** Get the http(s).proxyPassword system variable. */ public String getProxyPassword() { return System.getProperty(getProxyPasswordSystemPropertyName()); } - /** - * Set the http(s).proxyPassword system variable - */ + /** Set the http(s).proxyPassword system variable. */ public void setProxyPassword(String proxyPassword) { System.setProperty(getProxyPasswordSystemPropertyName(), proxyPassword); } @@ -164,7 +141,7 @@ public void setProxyPassword(String proxyPassword) { /** @return the name of the system property specifying the proxy password. */ @NotNull protected String getProxyPasswordSystemPropertyName() { - return protocol + PROXY_PASSWORD_SYSTEM_PROPERTY; + return getPropertyPrefix() + protocol + PROXY_PASSWORD_SYSTEM_PROPERTY; } /** Parses the given port string. Returns -1 if the string is null or not a valid number. */ diff --git a/teamscale-client/src/main/java/com/teamscale/client/TeamscaleProxySystemProperties.java b/teamscale-client/src/main/java/com/teamscale/client/TeamscaleProxySystemProperties.java index 022b2134d..94245b9b7 100644 --- a/teamscale-client/src/main/java/com/teamscale/client/TeamscaleProxySystemProperties.java +++ b/teamscale-client/src/main/java/com/teamscale/client/TeamscaleProxySystemProperties.java @@ -25,25 +25,7 @@ public TeamscaleProxySystemProperties(Protocol protocol) { @Override @NotNull - protected String getProxyHostSystemPropertyName() { - return TEAMSCALE_PREFIX + super.getProxyHostSystemPropertyName(); - } - - @Override - @NotNull - protected String getProxyPortSystemPropertyName() { - return TEAMSCALE_PREFIX + super.getProxyPortSystemPropertyName(); - } - - @Override - @NotNull - protected String getProxyUserSystemPropertyName() { - return TEAMSCALE_PREFIX + super.getProxyUserSystemPropertyName(); - } - - @Override - @NotNull - protected String getProxyPasswordSystemPropertyName() { - return TEAMSCALE_PREFIX + super.getProxyPasswordSystemPropertyName(); + protected String getPropertyPrefix() { + return TEAMSCALE_PREFIX; } } \ No newline at end of file From e3d9b8a7d2cf3fc873452a1f98a1fb015b2f0cd6 Mon Sep 17 00:00:00 2001 From: Jonas Bogenberger Date: Thu, 26 Sep 2024 15:35:36 +0200 Subject: [PATCH 078/186] TS-40412 adjust Tests --- .../agent/options/AgentOptionsTest.java | 52 +++++++---- .../testimpact/TestwiseCoverageAgentTest.java | 11 ++- ...mscaleServiceGeneratorProxyServerTest.java | 92 ++++--------------- 3 files changed, 59 insertions(+), 96 deletions(-) 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 852723e86..cebdf4859 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 @@ -4,6 +4,7 @@ import com.teamscale.client.ProxySystemProperties; import com.teamscale.client.TeamscaleProxySystemProperties; import com.teamscale.client.TeamscaleServer; +import com.teamscale.jacoco.agent.AgentBase; import com.teamscale.jacoco.agent.upload.artifactory.ArtifactoryConfig; import com.teamscale.jacoco.agent.util.TestUtils; import com.teamscale.report.util.CommandLineLogger; @@ -327,37 +328,43 @@ public void testArtifactoryApiKeySetPassesValidityCheck() throws Exception { } /** - * Tests that the {@link TeamscaleProxyOptions} are parsed correctly and correctly put into + * Tests that the {@link TeamscaleProxyOptions} for HTTP are parsed correctly and correctly put into * system properties that can be read using {@link TeamscaleProxySystemProperties}. - * */ + */ + @Test + public void testTeamscaleProxyOptionsCorrectlySetSystemPropertiesForHttp() throws Exception { + testTeamscaleProxyOptionsCorrectlySetSystemProperties(ProxySystemProperties.Protocol.HTTP); + } + + /** + * Tests that the {@link TeamscaleProxyOptions} for HTTPS are parsed correctly and correctly put into + * system properties that can be read using {@link TeamscaleProxySystemProperties}. + */ @Test - public void testTeamscaleProxyOptionsCorrectlySetSystemProperties() throws Exception { + public void testTeamscaleProxyOptionsCorrectlySetSystemPropertiesForHttps() throws Exception { + testTeamscaleProxyOptionsCorrectlySetSystemProperties(ProxySystemProperties.Protocol.HTTPS); + } + + private void testTeamscaleProxyOptionsCorrectlySetSystemProperties(ProxySystemProperties.Protocol protocol) throws Exception { String expectedHost = "host"; int expectedPort = 9999; String expectedUser = "user"; String expectedPassword = "password"; - String optionsString = String.format("proxy-host=%s,proxy-port=%d,proxy-user=%s,proxy-password=%s", expectedHost, expectedPort, expectedUser, expectedPassword); + String proxyHostOption = String.format("proxy-%s-host=%s", protocol.name(), expectedHost); + String proxyPortOption = String.format("proxy-%s-port=%d", protocol.name(), expectedPort); + String proxyUserOption = String.format("proxy-%s-user=%s", protocol.name(), expectedUser); + String proxyPasswordOption = String.format("proxy-%s-password=%s", protocol.name(), expectedPassword); + String optionsString = String.format("%s,%s,%s,%s", proxyHostOption, proxyPortOption, proxyUserOption, proxyPasswordOption); AgentOptions agentOptions = getAgentOptionsParserWithDummyLogger().parse(optionsString); // clear to be sure the system properties are empty - clearTeamscaleProxySystemProperties(ProxySystemProperties.Protocol.HTTP); - clearTeamscaleProxySystemProperties(ProxySystemProperties.Protocol.HTTPS); + clearTeamscaleProxySystemProperties(protocol); - agentOptions.getTeamscaleProxyOptions().putTeamscaleProxyOptionsIntoSystemProperties(); + AgentBase.putTeamscaleProxyOptionsIntoSystemProperties(agentOptions); - assertTeamscaleProxySystemPropertiesAreCorrect(ProxySystemProperties.Protocol.HTTP, expectedHost, expectedPort, expectedUser, expectedPassword); - assertTeamscaleProxySystemPropertiesAreCorrect(ProxySystemProperties.Protocol.HTTPS, expectedHost, expectedPort, expectedUser, expectedPassword); + assertTeamscaleProxySystemPropertiesAreCorrect(protocol, expectedHost, expectedPort, expectedUser, expectedPassword); - clearTeamscaleProxySystemProperties(ProxySystemProperties.Protocol.HTTP); - clearTeamscaleProxySystemProperties(ProxySystemProperties.Protocol.HTTPS); - } - - private void clearTeamscaleProxySystemProperties(ProxySystemProperties.Protocol protocol) { - TeamscaleProxySystemProperties teamscaleProxySystemProperties = new TeamscaleProxySystemProperties(protocol); - teamscaleProxySystemProperties.setProxyHost(""); - teamscaleProxySystemProperties.setProxyPort(""); - teamscaleProxySystemProperties.setProxyUser(""); - teamscaleProxySystemProperties.setProxyPassword(""); + clearTeamscaleProxySystemProperties(protocol); } private void assertTeamscaleProxySystemPropertiesAreCorrect(ProxySystemProperties.Protocol protocol, String expectedHost, int expectedPort, String expectedUser, String expectedPassword) { @@ -368,6 +375,13 @@ private void assertTeamscaleProxySystemPropertiesAreCorrect(ProxySystemPropertie assertThat(teamscaleProxySystemProperties.getProxyPassword()).isEqualTo(expectedPassword); } + private void clearTeamscaleProxySystemProperties(ProxySystemProperties.Protocol protocol) { + TeamscaleProxySystemProperties teamscaleProxySystemProperties = new TeamscaleProxySystemProperties(protocol); + teamscaleProxySystemProperties.setProxyHost(""); + teamscaleProxySystemProperties.setProxyPort(""); + teamscaleProxySystemProperties.setProxyUser(""); + teamscaleProxySystemProperties.setProxyPassword(""); + } /** Returns the include filter predicate for the given filter expression. */ private static Predicate includeFilter(String filterString) throws Exception { AgentOptions agentOptions = getAgentOptionsParserWithDummyLogger() diff --git a/agent/src/test/java/com/teamscale/jacoco/agent/testimpact/TestwiseCoverageAgentTest.java b/agent/src/test/java/com/teamscale/jacoco/agent/testimpact/TestwiseCoverageAgentTest.java index 4eeb503d0..d37eadcd8 100644 --- a/agent/src/test/java/com/teamscale/jacoco/agent/testimpact/TestwiseCoverageAgentTest.java +++ b/agent/src/test/java/com/teamscale/jacoco/agent/testimpact/TestwiseCoverageAgentTest.java @@ -5,10 +5,12 @@ import com.teamscale.client.EReportFormat; import com.teamscale.client.PrioritizableTest; import com.teamscale.client.PrioritizableTestCluster; +import com.teamscale.client.ProxySystemProperties; import com.teamscale.client.TeamscaleClient; import com.teamscale.client.TeamscaleServer; import com.teamscale.jacoco.agent.options.AgentOptions; import com.teamscale.jacoco.agent.options.ETestwiseCoverageMode; +import com.teamscale.jacoco.agent.options.TeamscaleProxyOptions; import com.teamscale.jacoco.agent.util.TestUtils; import com.teamscale.report.testwise.jacoco.JaCoCoTestwiseReportGenerator; import com.teamscale.report.testwise.model.ETestExecutionResult; @@ -35,6 +37,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Objects; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; @@ -160,6 +163,13 @@ public void shouldHandleMissingRequestBodyForTestrunStartGracefully() throws Exc private AgentOptions mockOptions(int port) { AgentOptions options = mock(AgentOptions.class); when(options.createTeamscaleClient()).thenReturn(client); + when(options.getTeamscaleProxyOptions(any(ProxySystemProperties.Protocol.class))).thenAnswer(invocation -> { + if (Objects.requireNonNull( + (ProxySystemProperties.Protocol) invocation.getArguments()[0]) == ProxySystemProperties.Protocol.HTTP) { + return new TeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTP); + } + return new TeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTPS); + }); TeamscaleServer server = new TeamscaleServer(); server.commit = new CommitDescriptor("branch", "12345"); @@ -174,5 +184,4 @@ private AgentOptions mockOptions(int port) { when(options.createTeamscaleClient()).thenReturn(client); return options; } - } diff --git a/teamscale-client/src/test/java/com/teamscale/client/TeamscaleServiceGeneratorProxyServerTest.java b/teamscale-client/src/test/java/com/teamscale/client/TeamscaleServiceGeneratorProxyServerTest.java index 394a2b5df..158f1095b 100644 --- a/teamscale-client/src/test/java/com/teamscale/client/TeamscaleServiceGeneratorProxyServerTest.java +++ b/teamscale-client/src/test/java/com/teamscale/client/TeamscaleServiceGeneratorProxyServerTest.java @@ -27,98 +27,38 @@ class TeamscaleServiceGeneratorProxyServerTest { private final TeamscaleProxySystemProperties teamscaleProxySystemProperties = new TeamscaleProxySystemProperties( ProxySystemProperties.Protocol.HTTP); - private static final String INCORRECT_VALUE = "incorrect"; - private static final String PROXY_USER = "myProxyUser"; - private static final String PROXY_PASSWORD = "myProxyPassword"; - private static final String BASE_64_ENCODED_BASIC_AUTH = Base64.getEncoder().encodeToString((PROXY_USER + ":" + PROXY_PASSWORD).getBytes( - StandardCharsets.UTF_8)); - @BeforeEach void setUp() throws IOException { mockProxyServer = new MockWebServer(); mockProxyServer.start(); } - @Test - void testProxyAuthentication() throws Exception { - proxySystemProperties.setProxyHost(mockProxyServer.getHostName()); - proxySystemProperties.setProxyPort(mockProxyServer.getPort()); - proxySystemProperties.setProxyUser(PROXY_USER); - proxySystemProperties.setProxyPassword(PROXY_PASSWORD); - - assertProxyAuthenticationIsUsed(); - } @Test void testTeamscaleProxyAuthentication() throws Exception { - // test that the teamscale-specific options take precedence over the global ones - proxySystemProperties.setProxyHost(INCORRECT_VALUE); - proxySystemProperties.setProxyPort(INCORRECT_VALUE); - proxySystemProperties.setProxyUser(INCORRECT_VALUE); - proxySystemProperties.setProxyPassword(INCORRECT_VALUE); + 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()); - teamscaleProxySystemProperties.setProxyUser(PROXY_USER); - teamscaleProxySystemProperties.setProxyPassword(PROXY_PASSWORD); - - assertProxyAuthenticationIsUsed(); - } - - @Test - void testMixingTeamscaleSpecificAndGlobalProxySettingsIsPossible() throws Exception { - proxySystemProperties.setProxyHost(mockProxyServer.getHostName()); - proxySystemProperties.setProxyPort(mockProxyServer.getPort()); - proxySystemProperties.setProxyUser(INCORRECT_VALUE); - proxySystemProperties.setProxyPassword(INCORRECT_VALUE); - - teamscaleProxySystemProperties.setProxyUser(PROXY_USER); - teamscaleProxySystemProperties.setProxyPassword(PROXY_PASSWORD); - - assertProxyAuthenticationIsUsed(); - } - - @Test - void testMixingTeamscaleSpecificAndGlobalProxySettingsIsPossibleTheOtherWayAround() throws Exception { - proxySystemProperties.setProxyHost(INCORRECT_VALUE); - proxySystemProperties.setProxyPort(INCORRECT_VALUE); - proxySystemProperties.setProxyUser(PROXY_USER); - proxySystemProperties.setProxyPassword(PROXY_PASSWORD); - - teamscaleProxySystemProperties.setProxyHost(mockProxyServer.getHostName()); - teamscaleProxySystemProperties.setProxyPort(mockProxyServer.getPort()); - - assertProxyAuthenticationIsUsed(); - } - - @Test - void testPartiallyMixingTeamscaleSpecificAndGlobalProxyServerSettingsIsImpossible() throws Exception { - proxySystemProperties.setProxyHost(mockProxyServer.getHostName()); - proxySystemProperties.setProxyPort(mockProxyServer.getPort()); - proxySystemProperties.setProxyUser(PROXY_USER); - proxySystemProperties.setProxyPassword(PROXY_PASSWORD); - - // if mixing the server settings works, reaching the host would be impossible - teamscaleProxySystemProperties.setProxyHost(INCORRECT_VALUE); - - assertProxyAuthenticationIsUsed(); - } - - @Test - void testPartiallyMixingTeamscaleSpecificAndGlobalProxyAuthenticationSettingsIsImpossible() throws Exception { - proxySystemProperties.setProxyHost(mockProxyServer.getHostName()); - proxySystemProperties.setProxyPort(mockProxyServer.getPort()); - proxySystemProperties.setProxyUser(PROXY_USER); - proxySystemProperties.setProxyPassword(PROXY_PASSWORD); - // if mixing the authentication settings works, authentication would not work - teamscaleProxySystemProperties.setProxyUser(INCORRECT_VALUE); + String proxyUser = "myProxyUser"; + String proxyPassword = "myProxyPassword"; + String base64EncodedBasicAuth = Base64.getEncoder().encodeToString((proxyUser + ":" + proxyPassword).getBytes( + StandardCharsets.UTF_8)); + teamscaleProxySystemProperties.setProxyUser(proxyUser); + teamscaleProxySystemProperties.setProxyPassword(proxyPassword); - assertProxyAuthenticationIsUsed(); + assertProxyAuthenticationIsUsed(base64EncodedBasicAuth); } - private void assertProxyAuthenticationIsUsed() throws InterruptedException, IOException { + // TODO write a test that verifies the AgentBase correctly mixes teamscale-specific options and JVM-system properties for proxies + 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, @@ -136,7 +76,7 @@ private void assertProxyAuthenticationIsUsed() throws InterruptedException, IOEx RecordedRequest requestWithProxyAuth = mockProxyServer.takeRequest();// Request we are actually interested in assertThat(requestWithProxyAuth.getHeader(PROXY_AUTHORIZATION_HTTP_HEADER)).isEqualTo( - "Basic " + BASE_64_ENCODED_BASIC_AUTH); + "Basic " + base64EncodedBasicAuth); } @AfterEach From 7f453fd6bb293f4c43d69a908c73161f9691b868 Mon Sep 17 00:00:00 2001 From: Jonas Bogenberger Date: Thu, 26 Sep 2024 15:49:27 +0200 Subject: [PATCH 079/186] TS-40412 another test for TeamscaleProxyOptions --- .../options/TeamscaleProxyOptionsTest.java | 33 +++++++++++++++++++ ...mscaleServiceGeneratorProxyServerTest.java | 2 -- 2 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 agent/src/test/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptionsTest.java 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 new file mode 100644 index 000000000..7bba07980 --- /dev/null +++ b/agent/src/test/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptionsTest.java @@ -0,0 +1,33 @@ +package com.teamscale.jacoco.agent.options; + +import com.teamscale.client.ProxySystemProperties; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class TeamscaleProxyOptionsTest { + + @Test + void testTeamscaleProxyOptionsFilledWithJVMOptionsOnInit() { + ProxySystemProperties proxySystemProperties =new ProxySystemProperties(ProxySystemProperties.Protocol.HTTP); + String expectedHost = "testHost"; + proxySystemProperties.setProxyHost(expectedHost); + int expectedPort = 1234; + proxySystemProperties.setProxyPort(expectedPort); + String expectedUser = "testUser"; + proxySystemProperties.setProxyUser(expectedUser); + String expectedPassword = "testPassword"; + proxySystemProperties.setProxyPassword(expectedPassword); + + TeamscaleProxyOptions teamscaleProxyOptions = new TeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTP); + assertThat(teamscaleProxyOptions.proxyHost).isEqualTo(expectedHost); + assertThat(teamscaleProxyOptions.proxyPort).isEqualTo(expectedPort); + assertThat(teamscaleProxyOptions.proxyUser).isEqualTo(expectedUser); + assertThat(teamscaleProxyOptions.proxyPassword).isEqualTo(expectedPassword); + + 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/TeamscaleServiceGeneratorProxyServerTest.java b/teamscale-client/src/test/java/com/teamscale/client/TeamscaleServiceGeneratorProxyServerTest.java index 158f1095b..e4e19aa3a 100644 --- a/teamscale-client/src/test/java/com/teamscale/client/TeamscaleServiceGeneratorProxyServerTest.java +++ b/teamscale-client/src/test/java/com/teamscale/client/TeamscaleServiceGeneratorProxyServerTest.java @@ -56,8 +56,6 @@ void testTeamscaleProxyAuthentication() throws Exception { assertProxyAuthenticationIsUsed(base64EncodedBasicAuth); } - // TODO write a test that verifies the AgentBase correctly mixes teamscale-specific options and JVM-system properties for proxies - private void assertProxyAuthenticationIsUsed(String base64EncodedBasicAuth) throws InterruptedException, IOException { ITeamscaleService service = TeamscaleServiceGenerator.createService(ITeamscaleService.class, HttpUrl.parse("http://localhost:1337"), From 174e258b2ad2c32dfbe8cd75dc14e2c97c1bec31 Mon Sep 17 00:00:00 2001 From: Jonas Bogenberger Date: Thu, 26 Sep 2024 15:52:44 +0200 Subject: [PATCH 080/186] TS-40412 adjust Readme to rework --- agent/README.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/agent/README.md b/agent/README.md index 9759f1bb4..03b665198 100644 --- a/agent/README.md +++ b/agent/README.md @@ -90,21 +90,15 @@ Check your applications console output for error messages. #### Teamscale-Specific Proxy Settings It is possible to specify teamscale-specific proxy settings that take prevalence over the system properties of the JVM. -If only teamscale-specific proxy server settings are provided and no teamscale-specific proxy authentication settings, -the agent will fall back to the system properties of the JVM, and the other way around. +If no or not all teamscale-specific proxy settings are provided the agent will fall back to the system properties of the +JVM. It is also possible to specify these options by prefixing the JVM flags for proxies with `teamscale.`, for example `-Dteamscale.https.proxyHost`. The following options are available: - -##### Server Settings -If only one of the two options is provided, the teamscale specific proxy server settings are ignored. - `proxy-host`: The host name of the proxy server. - `proxy-port`: The port of the proxy server. - -##### Authentication Settings -If only one of the two options is provided, the teamscale specific proxy authentication settings are ignored. - `proxy-user`: The username for the proxy server. - `proxy-password`: The password for the proxy user. From 81c68dd9da7ea6f6936b94b6a0bd128426e680ce Mon Sep 17 00:00:00 2001 From: Jonas Bogenberger Date: Thu, 26 Sep 2024 15:53:09 +0200 Subject: [PATCH 081/186] TS-40412 Typo --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8c192ef5..3829a0cbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ We use [semantic versioning](http://semver.org/): # Next version - [feature] _agent_: Prevent uploading coverage to the same project + revision or branch@timestamp when doing multi project upload via git.properties -- [feature] _agent_: New options `proxy-host`/`-port`/`-user`/`-password` allows user to specify teamscale-specific proxy settings. +- [feature] _agent_: New options `proxy-host`/`-port`/`-user`/`-password` allow user to specify teamscale-specific proxy settings. # 34.0.1 - [fix] _agent_: Error was reported when the system under test used logback From 3cdc10231d774b226bcc829c727fd51ba9ddbbdd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Sep 2024 14:07:15 +0000 Subject: [PATCH 082/186] Update plugin org.jetbrains.kotlin.jvm to v2 --- settings.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index bd9c9c7fd..84e91165d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,6 +1,6 @@ pluginManagement { plugins { - kotlin("jvm") version "1.9.24" + kotlin("jvm") version "2.0.20" } } From cc3cf26963b4943c72ca35ed53e77411620e6c55 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Sep 2024 03:50:51 +0000 Subject: [PATCH 083/186] Update jackson to v2.18.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 84457b4f4..af1b1c588 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] jetty = "9.4.56.v20240826" jersey = "2.45" -jackson = "2.17.2" +jackson = "2.18.0" # When upgrading JaCoCo to a newer version make sure to # check the comment in the OpenAnalyzer.java, JaCoCoPreMain.java and CachingInstructionsBuilder.java # and update the internal_xxxxxx hash included in the imports in LenientCoverageTransformer.java and JaCoCoPreMain.java. From 390109ad9c01c9f5634e6000cc6dded30122c48b Mon Sep 17 00:00:00 2001 From: Jonas Bogenberger Date: Fri, 27 Sep 2024 11:14:40 +0200 Subject: [PATCH 084/186] TS-40412 use delayedLogger in TeamscaleProxyOptions --- .../teamscale/jacoco/agent/options/AgentOptions.java | 10 ++++++---- .../jacoco/agent/options/TeamscaleProxyOptions.java | 6 ++++-- .../agent/options/TeamscaleProxyOptionsTest.java | 3 ++- .../agent/testimpact/TestwiseCoverageAgentTest.java | 5 +++-- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptions.java b/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptions.java index 995de5058..a89e21a19 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptions.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptions.java @@ -110,12 +110,10 @@ public class AgentOptions { private Path outputDirectory; /** Contains the options related to teamscale-specific proxy settings for http. */ - /* package */ TeamscaleProxyOptions teamscaleProxyOptionsForHttp = new TeamscaleProxyOptions( - ProxySystemProperties.Protocol.HTTP); + /* package */ TeamscaleProxyOptions teamscaleProxyOptionsForHttp; /** Contains the options related to teamscale-specific proxy settings for https. */ - /* package */ TeamscaleProxyOptions teamscaleProxyOptionsForHttps = new TeamscaleProxyOptions( - ProxySystemProperties.Protocol.HTTPS); + /* package */ TeamscaleProxyOptions teamscaleProxyOptionsForHttps; /** * Additional metadata files to upload together with the coverage XML. @@ -216,6 +214,10 @@ public class AgentOptions { public AgentOptions(ILogger logger) { this.logger = logger; setParentOutputDirectory(AgentUtils.getMainTempDirectory().resolve("coverage")); + teamscaleProxyOptionsForHttp = new TeamscaleProxyOptions( + ProxySystemProperties.Protocol.HTTP, logger); + teamscaleProxyOptionsForHttps = new TeamscaleProxyOptions( + ProxySystemProperties.Protocol.HTTPS, logger); } /** @see #debugLogging */ diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java b/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java index 4020f47c3..f00f31dc5 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java @@ -3,6 +3,7 @@ import com.teamscale.client.ProxySystemProperties; import com.teamscale.client.TeamscaleProxySystemProperties; import com.teamscale.jacoco.agent.util.LoggingUtils; +import com.teamscale.report.util.ILogger; import org.conqat.lib.commons.filesystem.FileSystemUtils; import org.slf4j.Logger; @@ -14,7 +15,7 @@ */ public class TeamscaleProxyOptions { - private final Logger logger = LoggingUtils.getLogger(this); + private final ILogger logger; /** The host of the proxy server. */ /* package */ String proxyHost; @@ -34,8 +35,9 @@ public class TeamscaleProxyOptions { private final ProxySystemProperties.Protocol protocol; /** Constructor. */ - public TeamscaleProxyOptions(ProxySystemProperties.Protocol protocol) { + public TeamscaleProxyOptions(ProxySystemProperties.Protocol protocol, ILogger logger) { this.protocol = protocol; + this.logger = logger; ProxySystemProperties proxySystemProperties = new ProxySystemProperties(protocol); proxyHost = proxySystemProperties.getProxyHost(); proxyPort = proxySystemProperties.getProxyPort(); 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 7bba07980..44cf2e411 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 @@ -1,6 +1,7 @@ package com.teamscale.jacoco.agent.options; import com.teamscale.client.ProxySystemProperties; +import com.teamscale.report.util.CommandLineLogger; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -19,7 +20,7 @@ void testTeamscaleProxyOptionsFilledWithJVMOptionsOnInit() { String expectedPassword = "testPassword"; proxySystemProperties.setProxyPassword(expectedPassword); - TeamscaleProxyOptions teamscaleProxyOptions = new TeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTP); + TeamscaleProxyOptions teamscaleProxyOptions = new TeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTP, new CommandLineLogger()); assertThat(teamscaleProxyOptions.proxyHost).isEqualTo(expectedHost); assertThat(teamscaleProxyOptions.proxyPort).isEqualTo(expectedPort); assertThat(teamscaleProxyOptions.proxyUser).isEqualTo(expectedUser); diff --git a/agent/src/test/java/com/teamscale/jacoco/agent/testimpact/TestwiseCoverageAgentTest.java b/agent/src/test/java/com/teamscale/jacoco/agent/testimpact/TestwiseCoverageAgentTest.java index d37eadcd8..801c95c71 100644 --- a/agent/src/test/java/com/teamscale/jacoco/agent/testimpact/TestwiseCoverageAgentTest.java +++ b/agent/src/test/java/com/teamscale/jacoco/agent/testimpact/TestwiseCoverageAgentTest.java @@ -14,6 +14,7 @@ import com.teamscale.jacoco.agent.util.TestUtils; import com.teamscale.report.testwise.jacoco.JaCoCoTestwiseReportGenerator; import com.teamscale.report.testwise.model.ETestExecutionResult; +import com.teamscale.report.util.CommandLineLogger; import com.teamscale.tia.client.RunningTest; import com.teamscale.tia.client.TestRun; import com.teamscale.tia.client.TestRunWithClusteredSuggestions; @@ -166,9 +167,9 @@ private AgentOptions mockOptions(int port) { when(options.getTeamscaleProxyOptions(any(ProxySystemProperties.Protocol.class))).thenAnswer(invocation -> { if (Objects.requireNonNull( (ProxySystemProperties.Protocol) invocation.getArguments()[0]) == ProxySystemProperties.Protocol.HTTP) { - return new TeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTP); + return new TeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTP, new CommandLineLogger()); } - return new TeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTPS); + return new TeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTPS, new CommandLineLogger()); }); TeamscaleServer server = new TeamscaleServer(); From 20c242df4e6f05dd57f2d0c2ad5e3b71dd61225d Mon Sep 17 00:00:00 2001 From: Jonas Bogenberger Date: Fri, 27 Sep 2024 14:28:40 +0200 Subject: [PATCH 085/186] TS-40412 remove logger from ProxySystemProperties --- .../agent/options/TeamscaleProxyOptions.java | 15 ++++++++++----- .../jacoco/agent/options/AgentOptionsTest.java | 2 +- .../java/com/teamscale/client/HttpUtils.java | 5 +++-- .../client/ProxySystemProperties.java | 18 ++++++++---------- .../client/ProxySystemPropertiesTest.java | 11 +++++++---- 5 files changed, 29 insertions(+), 22 deletions(-) diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java b/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java index f00f31dc5..fbdbc755c 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java @@ -2,10 +2,8 @@ import com.teamscale.client.ProxySystemProperties; import com.teamscale.client.TeamscaleProxySystemProperties; -import com.teamscale.jacoco.agent.util.LoggingUtils; import com.teamscale.report.util.ILogger; import org.conqat.lib.commons.filesystem.FileSystemUtils; -import org.slf4j.Logger; import java.io.IOException; import java.nio.file.Path; @@ -40,7 +38,7 @@ public TeamscaleProxyOptions(ProxySystemProperties.Protocol protocol, ILogger lo this.logger = logger; ProxySystemProperties proxySystemProperties = new ProxySystemProperties(protocol); proxyHost = proxySystemProperties.getProxyHost(); - proxyPort = proxySystemProperties.getProxyPort(); + proxyPort = proxySystemProperties.getProxyPort(logger::warn); proxyUser = proxySystemProperties.getProxyUser(); proxyPassword = proxySystemProperties.getProxyPassword(); } @@ -55,8 +53,15 @@ public boolean handleTeamscaleProxyOptions(String key, String value) { proxyHost = value; return true; } - if (String.format("proxy-%s-port", protocol).equals(key)) { - proxyPort = Integer.parseInt(value); + String proxyPortOption = "proxy-%s-port"; + if (String.format(proxyPortOption, protocol).equals(key)) { + try { + proxyPort = Integer.parseInt(value); + } catch (NumberFormatException e) { + logger.warn(String.format("Could not parse proxy port \"" + value + + "\" set via \"" + proxyPortOption + "\"")); + return false; + } return true; } if (String.format("proxy-%s-user", protocol).equals(key)) { 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 cebdf4859..e330c2243 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 @@ -370,7 +370,7 @@ private void testTeamscaleProxyOptionsCorrectlySetSystemProperties(ProxySystemPr private void assertTeamscaleProxySystemPropertiesAreCorrect(ProxySystemProperties.Protocol protocol, String expectedHost, int expectedPort, String expectedUser, String expectedPassword) { TeamscaleProxySystemProperties teamscaleProxySystemProperties = new TeamscaleProxySystemProperties(protocol); assertThat(teamscaleProxySystemProperties.getProxyHost()).isEqualTo(expectedHost); - assertThat(teamscaleProxySystemProperties.getProxyPort()).isEqualTo(expectedPort); + assertThat(teamscaleProxySystemProperties.getProxyPort(logMessage -> new CommandLineLogger().warn(logMessage))).isEqualTo(expectedPort); assertThat(teamscaleProxySystemProperties.getProxyUser()).isEqualTo(expectedUser); assertThat(teamscaleProxySystemProperties.getProxyPassword()).isEqualTo(expectedPassword); } diff --git a/teamscale-client/src/main/java/com/teamscale/client/HttpUtils.java b/teamscale-client/src/main/java/com/teamscale/client/HttpUtils.java index a710be367..d9f71ac1d 100644 --- a/teamscale-client/src/main/java/com/teamscale/client/HttpUtils.java +++ b/teamscale-client/src/main/java/com/teamscale/client/HttpUtils.java @@ -102,13 +102,14 @@ private static void setUpProxyServer(OkHttpClient.Builder httpClientBuilder) { private static boolean setUpProxyServerForProtocol(ProxySystemProperties.Protocol protocol, OkHttpClient.Builder httpClientBuilder) { + Consumer logFunctionForTeamscaleProxySystemProperties = logMessage -> LoggerFactory.getLogger(ProxySystemProperties.class).warn(logMessage); TeamscaleProxySystemProperties teamscaleProxySystemProperties = new TeamscaleProxySystemProperties(protocol); - if (!teamscaleProxySystemProperties.proxyServerIsSet()) { + if (!teamscaleProxySystemProperties.proxyServerIsSet(logFunctionForTeamscaleProxySystemProperties)) { return false; } useProxyServer(httpClientBuilder, teamscaleProxySystemProperties.getProxyHost(), - teamscaleProxySystemProperties.getProxyPort()); + teamscaleProxySystemProperties.getProxyPort(logFunctionForTeamscaleProxySystemProperties)); if (teamscaleProxySystemProperties.proxyAuthIsSet()) { useProxyAuthenticator(httpClientBuilder, teamscaleProxySystemProperties.getProxyUser(), teamscaleProxySystemProperties.getProxyPassword()); diff --git a/teamscale-client/src/main/java/com/teamscale/client/ProxySystemProperties.java b/teamscale-client/src/main/java/com/teamscale/client/ProxySystemProperties.java index 96ac45696..b640fdaae 100644 --- a/teamscale-client/src/main/java/com/teamscale/client/ProxySystemProperties.java +++ b/teamscale-client/src/main/java/com/teamscale/client/ProxySystemProperties.java @@ -1,8 +1,8 @@ package com.teamscale.client; import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; + +import java.util.function.Consumer; /** * Reads and writes Java system properties values for @@ -17,8 +17,6 @@ */ public class ProxySystemProperties { - private static final Logger LOGGER = LoggerFactory.getLogger(ProxySystemProperties.class); - 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"; @@ -60,8 +58,8 @@ public ProxySystemProperties(Protocol protocol) { /** * Checks whether proxyHost and proxyPort are set */ - public boolean proxyServerIsSet() { - return !StringUtils.isEmpty(getProxyHost()) && getProxyPort() > 0; + public boolean proxyServerIsSet(Consumer logFunction) { + return !StringUtils.isEmpty(getProxyHost()) && getProxyPort(logFunction) > 0; } /** Checks whether proxyUser and proxyPassword are set */ @@ -75,8 +73,8 @@ public String getProxyHost() { } /** Read the http(s).proxyPort system variable. Returns -1 if no or an invalid port was set. */ - public int getProxyPort() { - return parsePort(System.getProperty(getProxyPortSystemPropertyName())); + public int getProxyPort(Consumer logFunction) { + return parsePort(System.getProperty(getProxyPortSystemPropertyName()), logFunction); } /** Set the http(s).proxyHost system variable. */ @@ -145,7 +143,7 @@ protected String getProxyPasswordSystemPropertyName() { } /** Parses the given port string. Returns -1 if the string is null or not a valid number. */ - private int parsePort(String portString) { + private int parsePort(String portString, Consumer logFunction) { if (portString == null) { return -1; } @@ -153,7 +151,7 @@ private int parsePort(String portString) { try { return Integer.parseInt(portString); } catch (NumberFormatException e) { - LOGGER.warn("Could not parse proxy port \"" + portString + + logFunction.accept("Could not parse proxy port \"" + portString + "\" set via \"" + getProxyPortSystemPropertyName() + "\""); return -1; } diff --git a/teamscale-client/src/test/java/com/teamscale/client/ProxySystemPropertiesTest.java b/teamscale-client/src/test/java/com/teamscale/client/ProxySystemPropertiesTest.java index 84ce8d74a..67f1907d3 100644 --- a/teamscale-client/src/test/java/com/teamscale/client/ProxySystemPropertiesTest.java +++ b/teamscale-client/src/test/java/com/teamscale/client/ProxySystemPropertiesTest.java @@ -4,6 +4,8 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; +import java.util.function.Consumer; + class ProxySystemPropertiesTest { private static ProxySystemProperties properties = new ProxySystemProperties(ProxySystemProperties.Protocol.HTTP); @@ -16,13 +18,14 @@ static void teardown() { @Test void testPortParsing() { properties.setProxyPort(9876); - Assertions.assertThat(properties.getProxyPort()).isEqualTo(9876); + Consumer logFunction = logMessage -> {}; + Assertions.assertThat(properties.getProxyPort(logFunction)).isEqualTo(9876); properties.setProxyPort(""); - Assertions.assertThat(properties.getProxyPort()).isEqualTo(-1); + Assertions.assertThat(properties.getProxyPort(logFunction)).isEqualTo(-1); properties.setProxyPort("nonsense"); - Assertions.assertThat(properties.getProxyPort()).isEqualTo(-1); + Assertions.assertThat(properties.getProxyPort(logFunction)).isEqualTo(-1); properties.removeProxyPort(); - Assertions.assertThat(properties.getProxyPort()).isEqualTo(-1); + Assertions.assertThat(properties.getProxyPort(logFunction)).isEqualTo(-1); } } \ No newline at end of file From 39cc7dc3a39826b3c6a436718f03dae2b6dffd11 Mon Sep 17 00:00:00 2001 From: Jonas Bogenberger Date: Fri, 27 Sep 2024 16:22:04 +0200 Subject: [PATCH 086/186] TS-40412 throw IncorrectPortFormatException --- .../agent/options/TeamscaleProxyOptions.java | 14 +++++++---- .../agent/options/AgentOptionsTest.java | 4 ++-- .../java/com/teamscale/client/HttpUtils.java | 16 +++++++++---- .../client/ProxySystemProperties.java | 24 +++++++++++-------- .../client/ProxySystemPropertiesTest.java | 8 +++---- 5 files changed, 40 insertions(+), 26 deletions(-) diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java b/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java index fbdbc755c..edba10291 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java @@ -38,7 +38,12 @@ public TeamscaleProxyOptions(ProxySystemProperties.Protocol protocol, ILogger lo this.logger = logger; ProxySystemProperties proxySystemProperties = new ProxySystemProperties(protocol); proxyHost = proxySystemProperties.getProxyHost(); - proxyPort = proxySystemProperties.getProxyPort(logger::warn); + try { + proxyPort = proxySystemProperties.getProxyPort(); + } catch (ProxySystemProperties.IncorrectPortFormatException e) { + proxyPort = -1; + logger.warn(e.getMessage()); + } proxyUser = proxySystemProperties.getProxyUser(); proxyPassword = proxySystemProperties.getProxyPassword(); } @@ -48,7 +53,7 @@ public TeamscaleProxyOptions(ProxySystemProperties.Protocol protocol, ILogger lo * * @return true if it has successfully processed the given option. */ - public boolean handleTeamscaleProxyOptions(String key, String value) { + public boolean handleTeamscaleProxyOptions(String key, String value) throws AgentOptionParseException { if (String.format("proxy-%s-host", protocol).equals(key)){ proxyHost = value; return true; @@ -58,9 +63,8 @@ public boolean handleTeamscaleProxyOptions(String key, String value) { try { proxyPort = Integer.parseInt(value); } catch (NumberFormatException e) { - logger.warn(String.format("Could not parse proxy port \"" + value + - "\" set via \"" + proxyPortOption + "\"")); - return false; + throw new AgentOptionParseException("Could not parse proxy port \"" + value + + "\" set via \"" + proxyPortOption + "\"", e); } 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 e330c2243..aa47b9187 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 @@ -367,10 +367,10 @@ private void testTeamscaleProxyOptionsCorrectlySetSystemProperties(ProxySystemPr clearTeamscaleProxySystemProperties(protocol); } - private void assertTeamscaleProxySystemPropertiesAreCorrect(ProxySystemProperties.Protocol protocol, String expectedHost, int expectedPort, String expectedUser, String expectedPassword) { + private void assertTeamscaleProxySystemPropertiesAreCorrect(ProxySystemProperties.Protocol protocol, String expectedHost, int expectedPort, String expectedUser, String expectedPassword) throws ProxySystemProperties.IncorrectPortFormatException { TeamscaleProxySystemProperties teamscaleProxySystemProperties = new TeamscaleProxySystemProperties(protocol); assertThat(teamscaleProxySystemProperties.getProxyHost()).isEqualTo(expectedHost); - assertThat(teamscaleProxySystemProperties.getProxyPort(logMessage -> new CommandLineLogger().warn(logMessage))).isEqualTo(expectedPort); + assertThat(teamscaleProxySystemProperties.getProxyPort()).isEqualTo(expectedPort); assertThat(teamscaleProxySystemProperties.getProxyUser()).isEqualTo(expectedUser); assertThat(teamscaleProxySystemProperties.getProxyPassword()).isEqualTo(expectedPassword); } diff --git a/teamscale-client/src/main/java/com/teamscale/client/HttpUtils.java b/teamscale-client/src/main/java/com/teamscale/client/HttpUtils.java index d9f71ac1d..1e4ab08bc 100644 --- a/teamscale-client/src/main/java/com/teamscale/client/HttpUtils.java +++ b/teamscale-client/src/main/java/com/teamscale/client/HttpUtils.java @@ -102,15 +102,21 @@ private static void setUpProxyServer(OkHttpClient.Builder httpClientBuilder) { private static boolean setUpProxyServerForProtocol(ProxySystemProperties.Protocol protocol, OkHttpClient.Builder httpClientBuilder) { - Consumer logFunctionForTeamscaleProxySystemProperties = logMessage -> LoggerFactory.getLogger(ProxySystemProperties.class).warn(logMessage); TeamscaleProxySystemProperties teamscaleProxySystemProperties = new TeamscaleProxySystemProperties(protocol); - if (!teamscaleProxySystemProperties.proxyServerIsSet(logFunctionForTeamscaleProxySystemProperties)) { + try { + if (!teamscaleProxySystemProperties.proxyServerIsSet()) { + return false; + } + + useProxyServer(httpClientBuilder, teamscaleProxySystemProperties.getProxyHost(), + teamscaleProxySystemProperties.getProxyPort()); + + } catch (ProxySystemProperties.IncorrectPortFormatException e) + { + LOGGER.warn(e.getMessage()); return false; } - useProxyServer(httpClientBuilder, teamscaleProxySystemProperties.getProxyHost(), - teamscaleProxySystemProperties.getProxyPort(logFunctionForTeamscaleProxySystemProperties)); - if (teamscaleProxySystemProperties.proxyAuthIsSet()) { useProxyAuthenticator(httpClientBuilder, teamscaleProxySystemProperties.getProxyUser(), teamscaleProxySystemProperties.getProxyPassword()); } diff --git a/teamscale-client/src/main/java/com/teamscale/client/ProxySystemProperties.java b/teamscale-client/src/main/java/com/teamscale/client/ProxySystemProperties.java index b640fdaae..3da96661a 100644 --- a/teamscale-client/src/main/java/com/teamscale/client/ProxySystemProperties.java +++ b/teamscale-client/src/main/java/com/teamscale/client/ProxySystemProperties.java @@ -2,8 +2,6 @@ import org.jetbrains.annotations.NotNull; -import java.util.function.Consumer; - /** * Reads and writes Java system properties values for *

    @@ -58,8 +56,8 @@ public ProxySystemProperties(Protocol protocol) { /** * Checks whether proxyHost and proxyPort are set */ - public boolean proxyServerIsSet(Consumer logFunction) { - return !StringUtils.isEmpty(getProxyHost()) && getProxyPort(logFunction) > 0; + public boolean proxyServerIsSet() throws IncorrectPortFormatException { + return !StringUtils.isEmpty(getProxyHost()) && getProxyPort() > 0; } /** Checks whether proxyUser and proxyPassword are set */ @@ -73,8 +71,8 @@ public String getProxyHost() { } /** Read the http(s).proxyPort system variable. Returns -1 if no or an invalid port was set. */ - public int getProxyPort(Consumer logFunction) { - return parsePort(System.getProperty(getProxyPortSystemPropertyName()), logFunction); + public int getProxyPort() throws IncorrectPortFormatException { + return parsePort(System.getProperty(getProxyPortSystemPropertyName())); } /** Set the http(s).proxyHost system variable. */ @@ -142,8 +140,15 @@ protected String getProxyPasswordSystemPropertyName() { return getPropertyPrefix() + protocol + PROXY_PASSWORD_SYSTEM_PROPERTY; } + 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, Consumer logFunction) { + private int parsePort(String portString) throws IncorrectPortFormatException { if (portString == null) { return -1; } @@ -151,9 +156,8 @@ private int parsePort(String portString, Consumer logFunction) { try { return Integer.parseInt(portString); } catch (NumberFormatException e) { - logFunction.accept("Could not parse proxy port \"" + portString + - "\" set via \"" + getProxyPortSystemPropertyName() + "\""); - return -1; + throw new IncorrectPortFormatException("Could not parse proxy port \"" + portString + + "\" set via \"" + getProxyPortSystemPropertyName() + "\"", e); } } } diff --git a/teamscale-client/src/test/java/com/teamscale/client/ProxySystemPropertiesTest.java b/teamscale-client/src/test/java/com/teamscale/client/ProxySystemPropertiesTest.java index 67f1907d3..9ca3439df 100644 --- a/teamscale-client/src/test/java/com/teamscale/client/ProxySystemPropertiesTest.java +++ b/teamscale-client/src/test/java/com/teamscale/client/ProxySystemPropertiesTest.java @@ -19,13 +19,13 @@ static void teardown() { void testPortParsing() { properties.setProxyPort(9876); Consumer logFunction = logMessage -> {}; - Assertions.assertThat(properties.getProxyPort(logFunction)).isEqualTo(9876); + Assertions.assertThat(properties.getProxyPort()).isEqualTo(9876); properties.setProxyPort(""); - Assertions.assertThat(properties.getProxyPort(logFunction)).isEqualTo(-1); + Assertions.assertThat(properties.getProxyPort()).isEqualTo(-1); properties.setProxyPort("nonsense"); - Assertions.assertThat(properties.getProxyPort(logFunction)).isEqualTo(-1); + Assertions.assertThat(properties.getProxyPort()).isEqualTo(-1); properties.removeProxyPort(); - Assertions.assertThat(properties.getProxyPort(logFunction)).isEqualTo(-1); + Assertions.assertThat(properties.getProxyPort()).isEqualTo(-1); } } \ No newline at end of file From 69f5a579c42da01e0a1a67708ff757b9a6960fcb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Sep 2024 16:37:29 +0000 Subject: [PATCH 087/186] Update dependency org.apache.maven.plugins:maven-gpg-plugin to v3.2.7 --- teamscale-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teamscale-maven-plugin/pom.xml b/teamscale-maven-plugin/pom.xml index f058f3974..c8cb06abb 100644 --- a/teamscale-maven-plugin/pom.xml +++ b/teamscale-maven-plugin/pom.xml @@ -184,7 +184,7 @@ org.apache.maven.plugins maven-gpg-plugin - 3.2.6 + 3.2.7 sign-artifacts From 4f513d67a6e12df0a070b54a0aff7cd81aa68b07 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 29 Sep 2024 15:22:29 +0000 Subject: [PATCH 088/186] Update dependency org.apache.logging.log4j:log4j-core to v2.24.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index af1b1c588..da1afa441 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -60,7 +60,7 @@ picocli-core = { module = "info.picocli:picocli", version.ref = "picocli" } picocli-codegen = { module = "info.picocli:picocli-codegen", version.ref = "picocli" } jna-platform = { module = "net.java.dev.jna:jna-platform", version = "5.15.0" } -log4j-core = { module = "org.apache.logging.log4j:log4j-core", version = "2.24.0" } +log4j-core = { module = "org.apache.logging.log4j:log4j-core", version = "2.24.1" } junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } From e47d8cca6f27c02e1dab9cd9d28b2c9e1458f627 Mon Sep 17 00:00:00 2001 From: Jonas Bogenberger Date: Mon, 30 Sep 2024 11:08:21 +0200 Subject: [PATCH 089/186] TS-40412 Rework --- .../com/teamscale/jacoco/agent/AgentBase.java | 3 +- .../agent/options/AgentOptionsParser.java | 55 +++++++++++-------- .../agent/options/TeamscaleProxyOptions.java | 7 ++- .../client/ProxySystemProperties.java | 11 ++-- .../client/ProxySystemPropertiesTest.java | 18 +++--- 5 files changed, 54 insertions(+), 40 deletions(-) diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java b/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java index c42a3817a..d38424368 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java @@ -1,5 +1,6 @@ package com.teamscale.jacoco.agent; +import com.google.common.annotations.VisibleForTesting; import com.teamscale.client.ProxySystemProperties; import com.teamscale.client.TeamscaleProxySystemProperties; import com.teamscale.jacoco.agent.options.AgentOptions; @@ -63,8 +64,8 @@ public AgentBase(AgentOptions options) throws IllegalStateException { /** * Stores the agent options for proxies in the {@link TeamscaleProxySystemProperties} and overwrites * the password with the password found in the proxy-password-file if necessary. - * public for testing. */ + @VisibleForTesting public static void putTeamscaleProxyOptionsIntoSystemProperties(AgentOptions options) { options.getTeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTP).putTeamscaleProxyOptionsIntoSystemProperties(); options.getTeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTPS).putTeamscaleProxyOptionsIntoSystemProperties(); diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptionsParser.java b/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptionsParser.java index 60a4eaa46..2bd29690e 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptionsParser.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptionsParser.java @@ -99,7 +99,7 @@ public static AgentOptions parse(String optionsString, String environmentConfigI if (!StringUtils.isEmpty(optionsString)) { String[] optionParts = optionsString.split(","); for (String optionPart : optionParts) { - handleOption(options, optionPart); + handleOptionPart(options, optionPart); } } @@ -115,11 +115,11 @@ public static AgentOptions parse(String optionsString, String environmentConfigI private void handleConfigFromEnvironment( AgentOptions options) throws AgentOptionParseException, AgentOptionReceiveException { if (environmentConfigId != null) { - handleOption(options, "config-id=" + environmentConfigId); + handleOptionPart(options, "config-id=" + environmentConfigId); } if (environmentConfigFile != null) { - handleOption(options, "config-file=" + environmentConfigFile); + handleOptionPart(options, "config-file=" + environmentConfigFile); } if (environmentConfigId != null && environmentConfigFile != null) { @@ -132,11 +132,16 @@ private void handleConfigFromEnvironment( /** * Parses and stores the given option in the format key=value. */ - private void handleOption(AgentOptions options, - String optionPart) throws AgentOptionParseException, AgentOptionReceiveException { + private void handleOptionPart(AgentOptions options, String optionPart) throws AgentOptionParseException, AgentOptionReceiveException { Pair keyAndValue = parseOption(optionPart); - String key = keyAndValue.getFirst(); - String value = keyAndValue.getSecond(); + handleOption(options, keyAndValue.getFirst(), keyAndValue.getSecond()); + } + + /** + * Parses and stores the option with the given key and value. + */ + private void handleOption(AgentOptions options, + String key, String value) throws AgentOptionParseException, AgentOptionReceiveException { if (key.startsWith("debug")) { handleDebugOption(options, value); return; @@ -158,29 +163,33 @@ private void handleOption(AgentOptions options, value)) { return; } - String proxyKeyword = "proxy-"; - if (key.startsWith(proxyKeyword)) { - if (key.startsWith(proxyKeyword + ProxySystemProperties.Protocol.HTTPS) - && options.getTeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTPS).handleTeamscaleProxyOptions(key, value)) { - return; - } - if (key.startsWith(proxyKeyword + ProxySystemProperties.Protocol.HTTP) - && options.getTeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTP).handleTeamscaleProxyOptions(key, value)) { + if (key.startsWith("proxy-") && handleProxyOptions(options, StringUtils.stripPrefix(key, "proxy-"), key, filePatternResolver)){ return; } - if(key.equals("proxy-password-file")) { - Path proxyPasswordPath = filePatternResolver.parsePath(key, value); - options.getTeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTPS).proxyPasswordPath=proxyPasswordPath; - options.getTeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTP).proxyPasswordPath=proxyPasswordPath; - return; - } - } if (handleAgentOptions(options, key, value)) { return; } throw new AgentOptionParseException("Unknown option: " + key); } + private boolean handleProxyOptions(AgentOptions options, String key, String value, FilePatternResolver filePatternResolver) throws AgentOptionParseException { + if (key.startsWith(ProxySystemProperties.Protocol.HTTPS.toString()) + && options.getTeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTPS).handleTeamscaleProxyOptions(key, value)) { + return true; + } + if (key.startsWith(ProxySystemProperties.Protocol.HTTP.toString()) + && options.getTeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTP).handleTeamscaleProxyOptions(key, value)) { + return true; + } + if(key.equals("password-file")) { + Path proxyPasswordPath = filePatternResolver.parsePath(key, value); + options.getTeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTPS).proxyPasswordPath=proxyPasswordPath; + options.getTeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTP).proxyPasswordPath=proxyPasswordPath; + return true; + } + return false; + } + /** Parses and stores the debug logging file path if given. */ private void handleDebugOption(AgentOptions options, String value) { if (value.equalsIgnoreCase("false")) { @@ -363,7 +372,7 @@ private void readConfigFromString(AgentOptions options, if (trimmedOption.isEmpty() || trimmedOption.startsWith(COMMENT_PREFIX)) { continue; } - handleOption(options, optionKeyValue); + handleOptionPart(options, optionKeyValue); } } diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java b/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java index edba10291..352c2e14b 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java @@ -1,6 +1,7 @@ package com.teamscale.jacoco.agent.options; import com.teamscale.client.ProxySystemProperties; +import com.teamscale.client.StringUtils; import com.teamscale.client.TeamscaleProxySystemProperties; import com.teamscale.report.util.ILogger; import org.conqat.lib.commons.filesystem.FileSystemUtils; @@ -82,16 +83,16 @@ public boolean handleTeamscaleProxyOptions(String key, String value) throws Agen /** Stores the teamscale-specific proxy settings as system properties to make them always available. */ public void putTeamscaleProxyOptionsIntoSystemProperties() { TeamscaleProxySystemProperties teamscaleProxySystemProperties = new TeamscaleProxySystemProperties(protocol); - if (proxyHost != null && !proxyHost.isEmpty()) { + if (StringUtils.isEmpty(proxyHost)) { teamscaleProxySystemProperties.setProxyHost(proxyHost); } if (proxyPort > 0) { teamscaleProxySystemProperties.setProxyPort(proxyPort); } - if(proxyUser != null && !proxyUser.isEmpty()) { + if(!StringUtils.isEmpty(proxyUser)) { teamscaleProxySystemProperties.setProxyUser(proxyUser); } - if(proxyPassword != null && !proxyPassword.isEmpty()) { + if(StringUtils.isEmpty(proxyPassword)) { teamscaleProxySystemProperties.setProxyPassword(proxyPassword); } diff --git a/teamscale-client/src/main/java/com/teamscale/client/ProxySystemProperties.java b/teamscale-client/src/main/java/com/teamscale/client/ProxySystemProperties.java index 3da96661a..a327069ec 100644 --- a/teamscale-client/src/main/java/com/teamscale/client/ProxySystemProperties.java +++ b/teamscale-client/src/main/java/com/teamscale/client/ProxySystemProperties.java @@ -65,12 +65,12 @@ public boolean proxyAuthIsSet() { return !StringUtils.isEmpty(getProxyUser()) && !StringUtils.isEmpty(getProxyPassword()); } - /** Read the http(s).proxyHost system variable */ + /** @return the http(s).proxyHost system variable */ public String getProxyHost() { return System.getProperty(getProxyHostSystemPropertyName()); } - /** Read the http(s).proxyPort system variable. Returns -1 if no or an invalid port was set. */ + /** @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())); } @@ -107,7 +107,7 @@ protected String getProxyPortSystemPropertyName() { return getPropertyPrefix() + protocol + PROXY_PORT_SYSTEM_PROPERTY; } - /** Get the http(s).proxyUser system variable. */ + /** @return the http(s).proxyUser system variable. */ public String getProxyUser() { return System.getProperty(getProxyUserSystemPropertyName()); } @@ -123,7 +123,7 @@ protected String getProxyUserSystemPropertyName() { return getPropertyPrefix() + protocol + PROXY_USER_SYSTEM_PROPERTY; } - /** Get the http(s).proxyPassword system variable. */ + /** @return the http(s).proxyPassword system variable. */ public String getProxyPassword() { return System.getProperty(getProxyPasswordSystemPropertyName()); } @@ -140,6 +140,7 @@ 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) { @@ -149,7 +150,7 @@ public static class IncorrectPortFormatException extends IllegalArgumentExceptio /** 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 (portString == null) { + if (StringUtils.isEmpty(portString)) { return -1; } diff --git a/teamscale-client/src/test/java/com/teamscale/client/ProxySystemPropertiesTest.java b/teamscale-client/src/test/java/com/teamscale/client/ProxySystemPropertiesTest.java index 9ca3439df..13b250145 100644 --- a/teamscale-client/src/test/java/com/teamscale/client/ProxySystemPropertiesTest.java +++ b/teamscale-client/src/test/java/com/teamscale/client/ProxySystemPropertiesTest.java @@ -1,10 +1,10 @@ package com.teamscale.client; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; -import java.util.function.Consumer; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; class ProxySystemPropertiesTest { @@ -18,14 +18,16 @@ static void teardown() { @Test void testPortParsing() { properties.setProxyPort(9876); - Consumer logFunction = logMessage -> {}; - Assertions.assertThat(properties.getProxyPort()).isEqualTo(9876); + assertThat(properties.getProxyPort()).isEqualTo(9876); properties.setProxyPort(""); - Assertions.assertThat(properties.getProxyPort()).isEqualTo(-1); - properties.setProxyPort("nonsense"); - Assertions.assertThat(properties.getProxyPort()).isEqualTo(-1); + 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(); - Assertions.assertThat(properties.getProxyPort()).isEqualTo(-1); + assertThat(properties.getProxyPort()).isEqualTo(-1); } } \ No newline at end of file From e64818903ec8d565c7246897cec73f3c58d68ec6 Mon Sep 17 00:00:00 2001 From: Jonas Bogenberger Date: Mon, 30 Sep 2024 11:08:56 +0200 Subject: [PATCH 090/186] TS-40412 Rework --- .../java/com/teamscale/client/ProxySystemPropertiesTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teamscale-client/src/test/java/com/teamscale/client/ProxySystemPropertiesTest.java b/teamscale-client/src/test/java/com/teamscale/client/ProxySystemPropertiesTest.java index 13b250145..29eb83cd3 100644 --- a/teamscale-client/src/test/java/com/teamscale/client/ProxySystemPropertiesTest.java +++ b/teamscale-client/src/test/java/com/teamscale/client/ProxySystemPropertiesTest.java @@ -8,7 +8,7 @@ class ProxySystemPropertiesTest { - private static ProxySystemProperties properties = new ProxySystemProperties(ProxySystemProperties.Protocol.HTTP); + private static final ProxySystemProperties properties = new ProxySystemProperties(ProxySystemProperties.Protocol.HTTP); @AfterAll static void teardown() { From 67994c57e94634b874e797f02f89204b00b58ba9 Mon Sep 17 00:00:00 2001 From: Jonas Bogenberger Date: Mon, 30 Sep 2024 11:22:31 +0200 Subject: [PATCH 091/186] TS-40412 Rework --- .../jacoco/agent/options/TeamscaleProxyOptions.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java b/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java index 352c2e14b..f131463bf 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java @@ -83,7 +83,7 @@ public boolean handleTeamscaleProxyOptions(String key, String value) throws Agen /** Stores the teamscale-specific proxy settings as system properties to make them always available. */ public void putTeamscaleProxyOptionsIntoSystemProperties() { TeamscaleProxySystemProperties teamscaleProxySystemProperties = new TeamscaleProxySystemProperties(protocol); - if (StringUtils.isEmpty(proxyHost)) { + if (!StringUtils.isEmpty(proxyHost)) { teamscaleProxySystemProperties.setProxyHost(proxyHost); } if (proxyPort > 0) { @@ -92,7 +92,7 @@ public void putTeamscaleProxyOptionsIntoSystemProperties() { if(!StringUtils.isEmpty(proxyUser)) { teamscaleProxySystemProperties.setProxyUser(proxyUser); } - if(StringUtils.isEmpty(proxyPassword)) { + if(!StringUtils.isEmpty(proxyPassword)) { teamscaleProxySystemProperties.setProxyPassword(proxyPassword); } @@ -101,7 +101,7 @@ public void putTeamscaleProxyOptionsIntoSystemProperties() { /** Sets the proxy password JVM property from a file for the protocol in this instance of {@link TeamscaleProxyOptions}. */ private void setProxyPasswordFromFile(Path proxyPasswordFilePath) { - if (proxyPasswordFilePath == null) { + if (StringUtils.isEmpty(proxyPassword)) { return; } try { From c6e1899b1c2339cb3a03636bb999352c4d9ec5b3 Mon Sep 17 00:00:00 2001 From: Jonas Bogenberger Date: Mon, 30 Sep 2024 13:10:25 +0200 Subject: [PATCH 092/186] TS-40412 Rework --- .../jacoco/agent/options/AgentOptionsParser.java | 16 +++++++++++----- .../agent/options/TeamscaleProxyOptions.java | 14 +++++++------- .../jacoco/agent/options/AgentOptionsTest.java | 8 ++++---- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptionsParser.java b/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptionsParser.java index 2bd29690e..09cbe2594 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptionsParser.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptionsParser.java @@ -163,7 +163,7 @@ private void handleOption(AgentOptions options, value)) { return; } - if (key.startsWith("proxy-") && handleProxyOptions(options, StringUtils.stripPrefix(key, "proxy-"), key, filePatternResolver)){ + if (key.startsWith("proxy-") && handleProxyOptions(options, StringUtils.stripPrefix(key, "proxy-"), value, filePatternResolver)){ return; } if (handleAgentOptions(options, key, value)) { @@ -173,14 +173,20 @@ private void handleOption(AgentOptions options, } private boolean handleProxyOptions(AgentOptions options, String key, String value, FilePatternResolver filePatternResolver) throws AgentOptionParseException { - if (key.startsWith(ProxySystemProperties.Protocol.HTTPS.toString()) - && options.getTeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTPS).handleTeamscaleProxyOptions(key, value)) { + String httpsPrefix = String.format("%s-", ProxySystemProperties.Protocol.HTTPS); + if (key.startsWith(httpsPrefix) + && options.getTeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTPS).handleTeamscaleProxyOptions(StringUtils.stripPrefix( + key, httpsPrefix), value)) { return true; } - if (key.startsWith(ProxySystemProperties.Protocol.HTTP.toString()) - && options.getTeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTP).handleTeamscaleProxyOptions(key, value)) { + + String httpPrefix = String.format("%s-", ProxySystemProperties.Protocol.HTTP); + if (key.startsWith(httpPrefix) + && options.getTeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTP).handleTeamscaleProxyOptions(StringUtils.stripPrefix( + key, httpPrefix), value)) { return true; } + if(key.equals("password-file")) { Path proxyPasswordPath = filePatternResolver.parsePath(key, value); options.getTeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTPS).proxyPasswordPath=proxyPasswordPath; diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java b/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java index f131463bf..50af955d9 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java @@ -50,17 +50,17 @@ public TeamscaleProxyOptions(ProxySystemProperties.Protocol protocol, ILogger lo } /** - * Handles all command-line options prefixed with 'proxy-' + * Processes the command-line options for proxies. * * @return true if it has successfully processed the given option. */ public boolean handleTeamscaleProxyOptions(String key, String value) throws AgentOptionParseException { - if (String.format("proxy-%s-host", protocol).equals(key)){ + if ("host".equals(key)){ proxyHost = value; return true; } - String proxyPortOption = "proxy-%s-port"; - if (String.format(proxyPortOption, protocol).equals(key)) { + String proxyPortOption = "port"; + if (proxyPortOption.equals(key)) { try { proxyPort = Integer.parseInt(value); } catch (NumberFormatException e) { @@ -69,11 +69,11 @@ public boolean handleTeamscaleProxyOptions(String key, String value) throws Agen } return true; } - if (String.format("proxy-%s-user", protocol).equals(key)) { + if ("user".equals(key)) { proxyUser = value; return true; } - if (String.format("proxy-%s-password", protocol).equals(key)) { + if ("password".equals(key)) { proxyPassword = value; return true; } @@ -101,7 +101,7 @@ public void putTeamscaleProxyOptionsIntoSystemProperties() { /** Sets the proxy password JVM property from a file for the protocol in this instance of {@link TeamscaleProxyOptions}. */ private void setProxyPasswordFromFile(Path proxyPasswordFilePath) { - if (StringUtils.isEmpty(proxyPassword)) { + if (proxyPasswordFilePath == null) { return; } try { 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 aa47b9187..73f26751c 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 @@ -350,10 +350,10 @@ private void testTeamscaleProxyOptionsCorrectlySetSystemProperties(ProxySystemPr int expectedPort = 9999; String expectedUser = "user"; String expectedPassword = "password"; - String proxyHostOption = String.format("proxy-%s-host=%s", protocol.name(), expectedHost); - String proxyPortOption = String.format("proxy-%s-port=%d", protocol.name(), expectedPort); - String proxyUserOption = String.format("proxy-%s-user=%s", protocol.name(), expectedUser); - String proxyPasswordOption = String.format("proxy-%s-password=%s", protocol.name(), expectedPassword); + String proxyHostOption = String.format("proxy-%s-host=%s", protocol, expectedHost); + String proxyPortOption = String.format("proxy-%s-port=%d", protocol, expectedPort); + String proxyUserOption = String.format("proxy-%s-user=%s", protocol, expectedUser); + String proxyPasswordOption = String.format("proxy-%s-password=%s", protocol, expectedPassword); String optionsString = String.format("%s,%s,%s,%s", proxyHostOption, proxyPortOption, proxyUserOption, proxyPasswordOption); AgentOptions agentOptions = getAgentOptionsParserWithDummyLogger().parse(optionsString); From 27c52a32d8a5244488178287f7889fa2b9904a57 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 09:38:10 +0000 Subject: [PATCH 093/186] Update dependency org.apache.maven.plugins:maven-javadoc-plugin to v3.10.1 --- teamscale-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teamscale-maven-plugin/pom.xml b/teamscale-maven-plugin/pom.xml index c8cb06abb..3e95af00a 100644 --- a/teamscale-maven-plugin/pom.xml +++ b/teamscale-maven-plugin/pom.xml @@ -158,7 +158,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.10.0 + 3.10.1 attach-javadocs From 3e60f81384e1e7f4d0dc76ea106cf9f8b71df0ca Mon Sep 17 00:00:00 2001 From: Jonas Bogenberger Date: Tue, 1 Oct 2024 16:27:30 +0200 Subject: [PATCH 094/186] TS-40412 Rework --- agent/README.md | 10 +++++----- .../jacoco/agent/options/AgentOptionsParser.java | 4 ++-- .../jacoco/agent/options/TeamscaleProxyOptions.java | 4 ++++ 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/agent/README.md b/agent/README.md index 03b665198..22f396681 100644 --- a/agent/README.md +++ b/agent/README.md @@ -96,11 +96,11 @@ JVM. It is also possible to specify these options by prefixing the JVM flags for proxies with `teamscale.`, for example `-Dteamscale.https.proxyHost`. -The following options are available: -- `proxy-host`: The host name of the proxy server. -- `proxy-port`: The port of the proxy server. -- `proxy-user`: The username for the proxy server. -- `proxy-password`: The password for the proxy user. +The following options are available both for `https` and `http`: +- `proxy-https-host`/`proxy-http-host`: The host name of the proxy server. +- `proxy-https-port`/`proxy-http-port`: The port of the proxy server. +- `proxy-https-user`/`proxy-http-user`: The username for the proxy server. +- `proxy-https-password`/`proxy-http-password`: The password for the proxy user. #### Testwise coverage diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptionsParser.java b/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptionsParser.java index 09cbe2594..411c01f6e 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptionsParser.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptionsParser.java @@ -189,8 +189,8 @@ private boolean handleProxyOptions(AgentOptions options, String key, String valu if(key.equals("password-file")) { Path proxyPasswordPath = filePatternResolver.parsePath(key, value); - options.getTeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTPS).proxyPasswordPath=proxyPasswordPath; - options.getTeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTP).proxyPasswordPath=proxyPasswordPath; + options.getTeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTPS).setProxyPasswordPath(proxyPasswordPath); + options.getTeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTP).setProxyPasswordPath(proxyPasswordPath); return true; } return false; diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java b/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java index 50af955d9..96a01d999 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/options/TeamscaleProxyOptions.java @@ -25,6 +25,10 @@ public class TeamscaleProxyOptions { /** The password for the proxy user. */ /* package */ String proxyPassword; + public void setProxyPasswordPath(Path proxyPasswordPath) { + this.proxyPasswordPath = proxyPasswordPath; + } + /** A path to the file that contains the password for the proxy authentication. */ /* package */ Path proxyPasswordPath; From fa071375fa06d6569780b9f67b4ac6d2a176ee62 Mon Sep 17 00:00:00 2001 From: Jonas Bogenberger Date: Tue, 1 Oct 2024 16:55:51 +0200 Subject: [PATCH 095/186] TS-40412 Rework --- .../teamscale/jacoco/agent/options/AgentOptionsParser.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptionsParser.java b/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptionsParser.java index 411c01f6e..add97543a 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptionsParser.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptionsParser.java @@ -173,14 +173,14 @@ private void handleOption(AgentOptions options, } private boolean handleProxyOptions(AgentOptions options, String key, String value, FilePatternResolver filePatternResolver) throws AgentOptionParseException { - String httpsPrefix = String.format("%s-", ProxySystemProperties.Protocol.HTTPS); + String httpsPrefix = ProxySystemProperties.Protocol.HTTPS + "-"; if (key.startsWith(httpsPrefix) && options.getTeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTPS).handleTeamscaleProxyOptions(StringUtils.stripPrefix( key, httpsPrefix), value)) { return true; } - String httpPrefix = String.format("%s-", ProxySystemProperties.Protocol.HTTP); + String httpPrefix = ProxySystemProperties.Protocol.HTTP + "-"; if (key.startsWith(httpPrefix) && options.getTeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTP).handleTeamscaleProxyOptions(StringUtils.stripPrefix( key, httpPrefix), value)) { From 2ab02fef1e5d56b1b1c17c50ecbcda6927c006de Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 2 Oct 2024 06:59:11 +0000 Subject: [PATCH 096/186] Update dependency com.gradleup.shadow:shadow-gradle-plugin to v8.3.3 --- buildSrc/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 45b5d4c52..f41dd3273 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -11,7 +11,7 @@ repositories { } dependencies { - implementation("com.gradleup.shadow:shadow-gradle-plugin:8.3.2") + implementation("com.gradleup.shadow:shadow-gradle-plugin:8.3.3") implementation("com.xpdustry.ksr:com.xpdustry.ksr.gradle.plugin:1.0.0") { exclude(group = "com.github.johnrengelman") } From 84ff03675f942d6ed18f176d7300884ee66596e1 Mon Sep 17 00:00:00 2001 From: Jonas Bogenberger Date: Wed, 2 Oct 2024 09:41:41 +0200 Subject: [PATCH 097/186] Changes for release version 34.1.0 --- CHANGELOG.md | 4 +++- build.gradle.kts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ecd3cef41..b96d1f3f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,9 @@ We use [semantic versioning](http://semver.org/): - PATCH version when you make backwards compatible bug fixes. # Next version -- [feature] _agent_: New options `proxy-host`/`-port`/`-user`/`-password` allow user to specify teamscale-specific proxy settings. + +# 34.1.0 +- [feature] _agent_: New options `proxy-http(s)-host`/`-port`/`-user`/`-password` allow user to specify teamscale-specific proxy settings. - [fix] _teamscale-maven-plugin_: NPE when no commit, nor revision was configured - [fix] _teamscale-maven-plugin_: Fixed commit was ignored in commit resolution diff --git a/build.gradle.kts b/build.gradle.kts index 6a2fcdc09..854136d44 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ plugins { group = "com.teamscale" -val appVersion by extra("34.0.2") +val appVersion by extra("34.1.0") val snapshotVersion = appVersion + if (VersionUtils.isTaggedRelease()) "" else "-SNAPSHOT" From 9405bf2fb1f52de4bb56432ea3984cb2be851da5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 4 Oct 2024 16:26:36 +0000 Subject: [PATCH 098/186] Update dependency org.junit.jupiter:junit-jupiter-engine to v5.11.2 --- teamscale-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teamscale-maven-plugin/pom.xml b/teamscale-maven-plugin/pom.xml index 3e95af00a..5c516d674 100644 --- a/teamscale-maven-plugin/pom.xml +++ b/teamscale-maven-plugin/pom.xml @@ -65,7 +65,7 @@ org.junit.jupiter junit-jupiter-engine - 5.11.1 + 5.11.2 test From 8ac3980b553ab949c3912491f981dc2a08af9d36 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 4 Oct 2024 16:26:40 +0000 Subject: [PATCH 099/186] Update dependency org.junit.vintage:junit-vintage-engine to v5.11.2 --- .../src/test/resources/calculator_groovy/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teamscale-gradle-plugin/src/test/resources/calculator_groovy/build.gradle b/teamscale-gradle-plugin/src/test/resources/calculator_groovy/build.gradle index f3b3669b3..ae3afef55 100644 --- a/teamscale-gradle-plugin/src/test/resources/calculator_groovy/build.gradle +++ b/teamscale-gradle-plugin/src/test/resources/calculator_groovy/build.gradle @@ -14,7 +14,7 @@ plugins { import com.teamscale.TestImpacted ext.junit4Version = '4.13.2' -ext.junitVintageVersion = '5.11.1' +ext.junitVintageVersion = '5.11.2' ext.junitPlatformVersion = '1.4.0' ext.junitJupiterVersion = '5.11.1' From cc6c319fb0e247b4c05c6aac125b96af79451cfe Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 4 Oct 2024 18:07:50 +0000 Subject: [PATCH 100/186] Update junitJupiterVersion to v5.11.2 --- .../src/test/resources/calculator_groovy/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teamscale-gradle-plugin/src/test/resources/calculator_groovy/build.gradle b/teamscale-gradle-plugin/src/test/resources/calculator_groovy/build.gradle index f3b3669b3..ef4a69e07 100644 --- a/teamscale-gradle-plugin/src/test/resources/calculator_groovy/build.gradle +++ b/teamscale-gradle-plugin/src/test/resources/calculator_groovy/build.gradle @@ -16,7 +16,7 @@ import com.teamscale.TestImpacted ext.junit4Version = '4.13.2' ext.junitVintageVersion = '5.11.1' ext.junitPlatformVersion = '1.4.0' -ext.junitJupiterVersion = '5.11.1' +ext.junitJupiterVersion = '5.11.2' if (!project.hasProperty("withoutServerConfig")) { teamscale { From c613a340afed7fbe12e1d78f44499adbf40b5642 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 4 Oct 2024 22:55:32 +0000 Subject: [PATCH 101/186] Update junitPlatform to v1.11.2 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index da1afa441..494c81377 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ jacoco = "0.8.12" logback = "1.3.14" retrofit = "2.11.0" junit = "5.11.1" -junitPlatform = "1.11.1" +junitPlatform = "1.11.2" okhttp = "4.12.0" mockito = "4.11.0" picocli = "4.7.6" From 50bd357b90bf567618abdf792c67fdf21661cefe Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 5 Oct 2024 03:03:47 +0000 Subject: [PATCH 102/186] Update junit to v5.11.2 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 494c81377..f785be12f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ jacoco = "0.8.12" # We need to stay on the 1.3.x release line as 1.4.x requires Java 11 logback = "1.3.14" retrofit = "2.11.0" -junit = "5.11.1" +junit = "5.11.2" junitPlatform = "1.11.2" okhttp = "4.12.0" mockito = "4.11.0" From e97ee4695611af0d6367638c0bc9e31d2732adfd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 6 Oct 2024 10:52:58 +0000 Subject: [PATCH 103/186] Update dependency org.apache.maven.plugins:maven-surefire-plugin to v3.5.1 --- teamscale-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teamscale-maven-plugin/pom.xml b/teamscale-maven-plugin/pom.xml index 5c516d674..49f5a8992 100644 --- a/teamscale-maven-plugin/pom.xml +++ b/teamscale-maven-plugin/pom.xml @@ -140,7 +140,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.5.0 + 3.5.1 org.apache.maven.plugins From fcf4b58ae466ea585ea9df1ecd63b8bae883c110 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 6 Oct 2024 10:53:02 +0000 Subject: [PATCH 104/186] Update dependency org.ow2.asm:asm to v9.7.1 --- buildSrc/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index f41dd3273..8bd034c89 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -16,6 +16,6 @@ dependencies { exclude(group = "com.github.johnrengelman") } - implementation("org.ow2.asm:asm:9.7") + implementation("org.ow2.asm:asm:9.7.1") implementation("org.ow2.asm:asm-commons:9.7") } From e54686cf2b0c7fef19061ee4145b8a1d1671b9a5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 6 Oct 2024 12:44:11 +0000 Subject: [PATCH 105/186] Update dependency org.ow2.asm:asm-commons to v9.7.1 --- buildSrc/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 8bd034c89..2e0b48a2a 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -17,5 +17,5 @@ dependencies { } implementation("org.ow2.asm:asm:9.7.1") - implementation("org.ow2.asm:asm-commons:9.7") + implementation("org.ow2.asm:asm-commons:9.7.1") } From afb609901588ba5fbc1739c9f53983275e5a6e41 Mon Sep 17 00:00:00 2001 From: Jonas Bogenberger Date: Thu, 10 Oct 2024 12:02:11 +0200 Subject: [PATCH 106/186] TS-40621 load config from Teamscale after everything else, including setting the proxy options --- .../com/teamscale/jacoco/agent/AgentBase.java | 12 +- .../agent/options/AgentOptionsParser.java | 41 +++++-- .../agent/options/AgentOptionsTest.java | 112 ++++++++++++++++-- 3 files changed, 139 insertions(+), 26 deletions(-) diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java b/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java index d38424368..84017a245 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java @@ -40,8 +40,6 @@ public abstract class AgentBase { public AgentBase(AgentOptions options) throws IllegalStateException { this.options = options; - putTeamscaleProxyOptionsIntoSystemProperties(options); - try { controller = new JacocoRuntimeController(RT.getAgent()); } catch (IllegalStateException e) { @@ -61,15 +59,7 @@ public AgentBase(AgentOptions options) throws IllegalStateException { } } - /** - * Stores the agent options for proxies in the {@link TeamscaleProxySystemProperties} and overwrites - * the password with the password found in the proxy-password-file if necessary. - */ - @VisibleForTesting - public static void putTeamscaleProxyOptionsIntoSystemProperties(AgentOptions options) { - options.getTeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTP).putTeamscaleProxyOptionsIntoSystemProperties(); - options.getTeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTPS).putTeamscaleProxyOptionsIntoSystemProperties(); - } + /** * Lazily generated string representation of the command line arguments to print to the log. diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptionsParser.java b/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptionsParser.java index add97543a..2bdf85ec3 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptionsParser.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptionsParser.java @@ -8,6 +8,7 @@ import com.google.common.annotations.VisibleForTesting; import com.teamscale.client.ProxySystemProperties; import com.teamscale.client.StringUtils; +import com.teamscale.client.TeamscaleProxySystemProperties; import com.teamscale.jacoco.agent.commandline.Validator; import com.teamscale.jacoco.agent.configuration.AgentOptionReceiveException; import com.teamscale.jacoco.agent.configuration.ConfigurationViaTeamscale; @@ -103,7 +104,11 @@ public static AgentOptions parse(String optionsString, String environmentConfigI } } - handleConfigFromEnvironment(options); + // we have to put the proxy options into system properties before reading the configuration from Teamscale as we + // might need them to connect to Teamscale + putTeamscaleProxyOptionsIntoSystemProperties(options); + + handleConfigId(options); Validator validator = options.getValidator(); if (!validator.isValid()) { @@ -112,12 +117,28 @@ public static AgentOptions parse(String optionsString, String environmentConfigI return options; } - private void handleConfigFromEnvironment( - AgentOptions options) throws AgentOptionParseException, AgentOptionReceiveException { + /** + * Stores the agent options for proxies in the {@link TeamscaleProxySystemProperties} and overwrites + * the password with the password found in the proxy-password-file if necessary. + */ + @VisibleForTesting + public static void putTeamscaleProxyOptionsIntoSystemProperties(AgentOptions options) { + options.getTeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTP).putTeamscaleProxyOptionsIntoSystemProperties(); + options.getTeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTPS).putTeamscaleProxyOptionsIntoSystemProperties(); + } + + private void handleConfigId(AgentOptions options) throws AgentOptionReceiveException, AgentOptionParseException { if (environmentConfigId != null) { + if (options.teamscaleServer.configId != null) { + logger.warn("You specified an ID for a profiler configuration in Teamscale both in the agent options and using an environment variable." + + " The environment variable will override the ID specified using the agent options." + + " Please use one or the other."); + } handleOptionPart(options, "config-id=" + environmentConfigId); } + readConfigFromTeamscale(options); + if (environmentConfigFile != null) { handleOptionPart(options, "config-file=" + environmentConfigFile); } @@ -233,7 +254,7 @@ private boolean handleAgentOptions(AgentOptions options, String key, String valu throws AgentOptionParseException, AgentOptionReceiveException { switch (key) { case "config-id": - readConfigFromTeamscale(options, value); + storeConfigIdForLaterUse(options, value); return true; case CONFIG_FILE_OPTION: readConfigFromFile(options, filePatternResolver.parsePath(key, value).toFile()); @@ -304,14 +325,20 @@ private boolean handleAgentOptions(AgentOptions options, String key, String valu } } - private void readConfigFromTeamscale(AgentOptions options, - String configId) throws AgentOptionParseException, AgentOptionReceiveException { + private void storeConfigIdForLaterUse(AgentOptions options, String configId) throws AgentOptionParseException { if (!options.teamscaleServer.isConfiguredForServerConnection()) { throw new AgentOptionParseException( "Has specified config-id '" + configId + "' without teamscale url/user/accessKey! The options need to be defined in teamscale.properties."); } options.teamscaleServer.configId = configId; - ConfigurationViaTeamscale configuration = ConfigurationViaTeamscale.retrieve(logger, configId, + } + + private void readConfigFromTeamscale(AgentOptions options) throws AgentOptionParseException, AgentOptionReceiveException { + if(options.teamscaleServer.configId == null) { + return; + } + + ConfigurationViaTeamscale configuration = ConfigurationViaTeamscale.retrieve(logger, options.teamscaleServer.configId, options.teamscaleServer.url, options.teamscaleServer.userName, options.teamscaleServer.userAccessToken); 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 73f26751c..60bd34310 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 @@ -1,24 +1,36 @@ package com.teamscale.jacoco.agent.options; import com.teamscale.client.CommitDescriptor; +import com.teamscale.client.JsonUtils; +import com.teamscale.client.ProfilerConfiguration; +import com.teamscale.client.ProfilerRegistration; import com.teamscale.client.ProxySystemProperties; import com.teamscale.client.TeamscaleProxySystemProperties; import com.teamscale.client.TeamscaleServer; -import com.teamscale.jacoco.agent.AgentBase; import com.teamscale.jacoco.agent.upload.artifactory.ArtifactoryConfig; import com.teamscale.jacoco.agent.util.TestUtils; import com.teamscale.report.util.CommandLineLogger; import okhttp3.HttpUrl; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.Rule; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.junit.rules.TemporaryFolder; +import java.io.BufferedWriter; import java.io.File; +import java.io.FileWriter; import java.io.IOException; import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.function.Predicate; +import static com.teamscale.client.HttpUtils.PROXY_AUTHORIZATION_HTTP_HEADER; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -345,28 +357,108 @@ public void testTeamscaleProxyOptionsCorrectlySetSystemPropertiesForHttps() thro testTeamscaleProxyOptionsCorrectlySetSystemProperties(ProxySystemProperties.Protocol.HTTPS); } + /** + * Temporary folder to create the password file for + * {@link AgentOptionsTest#testTeamscaleProxyOptionsAreUsedWhileFetchingConfigFromTeamscale()}. + */ + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + /** + * Test that the proxy settings are put into system properties and used for fetching a profiler configuration from + * Teamscale. Also tests that it is possible to specify the proxy password in a file and that this overwrites the + * password specified as agent option. + */ + @Test + public void testTeamscaleProxyOptionsAreUsedWhileFetchingConfigFromTeamscale() throws Exception { + String expectedUser = "user"; + // this is the password passed as agent property, it should be overwritten by the password file + String unexpectedPassword = "not-my-password"; + + String expectedPassword = "password"; + File passwordFile = writePasswortToPasswordFile(expectedPassword); + + try (MockWebServer mockProxyServer = new MockWebServer()) { + String expectedHost = mockProxyServer.getHostName(); + int expectedPort = mockProxyServer.getPort(); + + + ProfilerConfiguration expectedProfilerConfiguration = new ProfilerConfiguration(); + expectedProfilerConfiguration.configurationId = "bla"; + expectedProfilerConfiguration.configurationOptions = ""; + ProfilerRegistration profilerRegistration = new ProfilerRegistration(); + profilerRegistration.profilerId = "blub"; + profilerRegistration.profilerConfiguration = expectedProfilerConfiguration; + + mockProxyServer.enqueue(new MockResponse().setResponseCode(407)); + mockProxyServer.enqueue(new MockResponse().setResponseCode(200).setBody(JsonUtils.serialize(profilerRegistration))); + + AgentOptions agentOptions= parseProxyOptions("config-id=config,", ProxySystemProperties.Protocol.HTTP, expectedHost, expectedPort, expectedUser, unexpectedPassword, passwordFile); + + assertThat(agentOptions.configurationViaTeamscale.getProfilerConfiguration().configurationId).isEqualTo(expectedProfilerConfiguration.configurationId); + + // 2 requests: one without proxy authentication, which failed (407), one with proxy authentication + assertThat(mockProxyServer.getRequestCount()).isEqualTo(2); + + mockProxyServer.takeRequest(); + RecordedRequest requestWithProxyAuth = mockProxyServer.takeRequest(); // this is the interesting request + + // check that the correct password was used + String base64EncodedBasicAuth = Base64.getEncoder().encodeToString((expectedUser + ":" + expectedPassword).getBytes( + StandardCharsets.UTF_8)); + assertThat(requestWithProxyAuth.getHeader(PROXY_AUTHORIZATION_HTTP_HEADER)).isEqualTo("Basic " + base64EncodedBasicAuth); + } + + } + + private File writePasswortToPasswordFile(String expectedPassword) throws IOException { + temporaryFolder.create(); + File passwordFile = temporaryFolder.newFile(); + + BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(passwordFile)); + bufferedWriter.write(expectedPassword); + bufferedWriter.close(); + + return passwordFile; + } + private void testTeamscaleProxyOptionsCorrectlySetSystemProperties(ProxySystemProperties.Protocol protocol) throws Exception { String expectedHost = "host"; int expectedPort = 9999; String expectedUser = "user"; String expectedPassword = "password"; - String proxyHostOption = String.format("proxy-%s-host=%s", protocol, expectedHost); - String proxyPortOption = String.format("proxy-%s-port=%d", protocol, expectedPort); - String proxyUserOption = String.format("proxy-%s-user=%s", protocol, expectedUser); - String proxyPasswordOption = String.format("proxy-%s-password=%s", protocol, expectedPassword); - String optionsString = String.format("%s,%s,%s,%s", proxyHostOption, proxyPortOption, proxyUserOption, proxyPasswordOption); - AgentOptions agentOptions = getAgentOptionsParserWithDummyLogger().parse(optionsString); + AgentOptions agentOptions = parseProxyOptions("", protocol, + expectedHost, expectedPort, expectedUser, expectedPassword, null); // clear to be sure the system properties are empty clearTeamscaleProxySystemProperties(protocol); - AgentBase.putTeamscaleProxyOptionsIntoSystemProperties(agentOptions); + AgentOptionsParser.putTeamscaleProxyOptionsIntoSystemProperties(agentOptions); assertTeamscaleProxySystemPropertiesAreCorrect(protocol, expectedHost, expectedPort, expectedUser, expectedPassword); clearTeamscaleProxySystemProperties(protocol); } + private static AgentOptions parseProxyOptions(String otherOptionsString, ProxySystemProperties.Protocol protocol, + String expectedHost, int expectedPort, String expectedUser, + String expectedPassword, File passwordFile) throws Exception { + String proxyHostOption = String.format("proxy-%s-host=%s", protocol, expectedHost); + String proxyPortOption = String.format("proxy-%s-port=%d", protocol, expectedPort); + String proxyUserOption = String.format("proxy-%s-user=%s", protocol, expectedUser); + 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) { + String proxyPasswordFileOption = String.format("proxy-password-file=%s", passwordFile.getAbsoluteFile()); + optionsString += "," + proxyPasswordFileOption; + } + + TeamscaleCredentials credentials = new TeamscaleCredentials(HttpUrl.parse("http://localhost:80"), "unused", "unused"); + AgentOptions agentOptions = getAgentOptionsParserWithDummyLoggerAndCredentials(credentials).parse(optionsString); + return agentOptions; + } + private void assertTeamscaleProxySystemPropertiesAreCorrect(ProxySystemProperties.Protocol protocol, String expectedHost, int expectedPort, String expectedUser, String expectedPassword) throws ProxySystemProperties.IncorrectPortFormatException { TeamscaleProxySystemProperties teamscaleProxySystemProperties = new TeamscaleProxySystemProperties(protocol); assertThat(teamscaleProxySystemProperties.getProxyHost()).isEqualTo(expectedHost); @@ -400,6 +492,10 @@ private static AgentOptionsParser getAgentOptionsParserWithDummyLogger() { return new AgentOptionsParser(new CommandLineLogger(), null, null, null); } + private static AgentOptionsParser getAgentOptionsParserWithDummyLoggerAndCredentials(TeamscaleCredentials credentials) { + return new AgentOptionsParser(new CommandLineLogger(), null, null, credentials); + } + /** * Delete created coverage folders */ From 188db0a999bf87bae5ab0f2a533b4d0d73c70145 Mon Sep 17 00:00:00 2001 From: Jonas Bogenberger Date: Thu, 10 Oct 2024 12:03:49 +0200 Subject: [PATCH 107/186] TS-40621 clean up --- agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java | 3 --- .../com/teamscale/jacoco/agent/options/AgentOptionsTest.java | 3 +-- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java b/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java index 84017a245..7351144b1 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java @@ -1,8 +1,5 @@ package com.teamscale.jacoco.agent; -import com.google.common.annotations.VisibleForTesting; -import com.teamscale.client.ProxySystemProperties; -import com.teamscale.client.TeamscaleProxySystemProperties; import com.teamscale.jacoco.agent.options.AgentOptions; import com.teamscale.jacoco.agent.util.LoggingUtils; import org.eclipse.jetty.server.Server; 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 60bd34310..c6ed951b4 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 @@ -455,8 +455,7 @@ private static AgentOptions parseProxyOptions(String otherOptionsString, ProxySy } TeamscaleCredentials credentials = new TeamscaleCredentials(HttpUrl.parse("http://localhost:80"), "unused", "unused"); - AgentOptions agentOptions = getAgentOptionsParserWithDummyLoggerAndCredentials(credentials).parse(optionsString); - return agentOptions; + return getAgentOptionsParserWithDummyLoggerAndCredentials(credentials).parse(optionsString); } private void assertTeamscaleProxySystemPropertiesAreCorrect(ProxySystemProperties.Protocol protocol, String expectedHost, int expectedPort, String expectedUser, String expectedPassword) throws ProxySystemProperties.IncorrectPortFormatException { From 9cafd1ec41c019ddf46b0b0a562606b171b4b333 Mon Sep 17 00:00:00 2001 From: Jonas Bogenberger Date: Thu, 10 Oct 2024 15:51:12 +0200 Subject: [PATCH 108/186] TS-40621 fix tests --- .../jacoco/agent/options/AgentOptionsTest.java | 16 +++++++--------- .../testimpact/TestwiseCoverageAgentTest.java | 8 +------- 2 files changed, 8 insertions(+), 16 deletions(-) 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 c6ed951b4..e35e053f5 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 @@ -14,12 +14,10 @@ import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; -import org.junit.Rule; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import org.junit.rules.TemporaryFolder; import java.io.BufferedWriter; import java.io.File; @@ -361,8 +359,8 @@ public void testTeamscaleProxyOptionsCorrectlySetSystemPropertiesForHttps() thro * Temporary folder to create the password file for * {@link AgentOptionsTest#testTeamscaleProxyOptionsAreUsedWhileFetchingConfigFromTeamscale()}. */ - @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @TempDir + public File temporaryDirectory; /** * Test that the proxy settings are put into system properties and used for fetching a profiler configuration from @@ -384,10 +382,10 @@ public void testTeamscaleProxyOptionsAreUsedWhileFetchingConfigFromTeamscale() t ProfilerConfiguration expectedProfilerConfiguration = new ProfilerConfiguration(); - expectedProfilerConfiguration.configurationId = "bla"; - expectedProfilerConfiguration.configurationOptions = ""; + expectedProfilerConfiguration.configurationId = "config-id"; + expectedProfilerConfiguration.configurationOptions = "mode=testwise\ntia-mode=disk"; ProfilerRegistration profilerRegistration = new ProfilerRegistration(); - profilerRegistration.profilerId = "blub"; + profilerRegistration.profilerId = "profiler-id"; profilerRegistration.profilerConfiguration = expectedProfilerConfiguration; mockProxyServer.enqueue(new MockResponse().setResponseCode(407)); @@ -396,6 +394,7 @@ public void testTeamscaleProxyOptionsAreUsedWhileFetchingConfigFromTeamscale() t AgentOptions agentOptions= parseProxyOptions("config-id=config,", ProxySystemProperties.Protocol.HTTP, expectedHost, expectedPort, expectedUser, unexpectedPassword, passwordFile); assertThat(agentOptions.configurationViaTeamscale.getProfilerConfiguration().configurationId).isEqualTo(expectedProfilerConfiguration.configurationId); + assertThat(agentOptions.mode).isEqualTo(EMode.TESTWISE); // 2 requests: one without proxy authentication, which failed (407), one with proxy authentication assertThat(mockProxyServer.getRequestCount()).isEqualTo(2); @@ -412,8 +411,7 @@ public void testTeamscaleProxyOptionsAreUsedWhileFetchingConfigFromTeamscale() t } private File writePasswortToPasswordFile(String expectedPassword) throws IOException { - temporaryFolder.create(); - File passwordFile = temporaryFolder.newFile(); + File passwordFile = new File(temporaryDirectory, "password.txt"); BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(passwordFile)); bufferedWriter.write(expectedPassword); diff --git a/agent/src/test/java/com/teamscale/jacoco/agent/testimpact/TestwiseCoverageAgentTest.java b/agent/src/test/java/com/teamscale/jacoco/agent/testimpact/TestwiseCoverageAgentTest.java index 801c95c71..1e81cdbe8 100644 --- a/agent/src/test/java/com/teamscale/jacoco/agent/testimpact/TestwiseCoverageAgentTest.java +++ b/agent/src/test/java/com/teamscale/jacoco/agent/testimpact/TestwiseCoverageAgentTest.java @@ -164,13 +164,7 @@ public void shouldHandleMissingRequestBodyForTestrunStartGracefully() throws Exc private AgentOptions mockOptions(int port) { AgentOptions options = mock(AgentOptions.class); when(options.createTeamscaleClient()).thenReturn(client); - when(options.getTeamscaleProxyOptions(any(ProxySystemProperties.Protocol.class))).thenAnswer(invocation -> { - if (Objects.requireNonNull( - (ProxySystemProperties.Protocol) invocation.getArguments()[0]) == ProxySystemProperties.Protocol.HTTP) { - return new TeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTP, new CommandLineLogger()); - } - return new TeamscaleProxyOptions(ProxySystemProperties.Protocol.HTTPS, new CommandLineLogger()); - }); + TeamscaleServer server = new TeamscaleServer(); server.commit = new CommitDescriptor("branch", "12345"); From 8c7ee918b5fd06c05d9982576f2ae6a2ffd335c0 Mon Sep 17 00:00:00 2001 From: Jonas Bogenberger Date: Thu, 10 Oct 2024 15:53:28 +0200 Subject: [PATCH 109/186] TS-40621 adapt CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b96d1f3f7..54b0ee661 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ We use [semantic versioning](http://semver.org/): - PATCH version when you make backwards compatible bug fixes. # Next version +- [fix] _agent_: Loading a profiler configuration from Teamscale was not possible if the potentially necessary proxy settings were not set yet. # 34.1.0 - [feature] _agent_: New options `proxy-http(s)-host`/`-port`/`-user`/`-password` allow user to specify teamscale-specific proxy settings. From e38b6e52f1591298f76c3cbc8f14f70e084e7aac Mon Sep 17 00:00:00 2001 From: Jonas Bogenberger Date: Thu, 10 Oct 2024 15:54:12 +0200 Subject: [PATCH 110/186] TS-40621 clean-up --- .../jacoco/agent/testimpact/TestwiseCoverageAgentTest.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/agent/src/test/java/com/teamscale/jacoco/agent/testimpact/TestwiseCoverageAgentTest.java b/agent/src/test/java/com/teamscale/jacoco/agent/testimpact/TestwiseCoverageAgentTest.java index 1e81cdbe8..395adbec9 100644 --- a/agent/src/test/java/com/teamscale/jacoco/agent/testimpact/TestwiseCoverageAgentTest.java +++ b/agent/src/test/java/com/teamscale/jacoco/agent/testimpact/TestwiseCoverageAgentTest.java @@ -5,16 +5,13 @@ import com.teamscale.client.EReportFormat; import com.teamscale.client.PrioritizableTest; import com.teamscale.client.PrioritizableTestCluster; -import com.teamscale.client.ProxySystemProperties; import com.teamscale.client.TeamscaleClient; import com.teamscale.client.TeamscaleServer; import com.teamscale.jacoco.agent.options.AgentOptions; import com.teamscale.jacoco.agent.options.ETestwiseCoverageMode; -import com.teamscale.jacoco.agent.options.TeamscaleProxyOptions; import com.teamscale.jacoco.agent.util.TestUtils; import com.teamscale.report.testwise.jacoco.JaCoCoTestwiseReportGenerator; import com.teamscale.report.testwise.model.ETestExecutionResult; -import com.teamscale.report.util.CommandLineLogger; import com.teamscale.tia.client.RunningTest; import com.teamscale.tia.client.TestRun; import com.teamscale.tia.client.TestRunWithClusteredSuggestions; @@ -38,7 +35,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Objects; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; From 48e3d5b1e04d1293f6062207f246577a88923eea Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 10 Oct 2024 14:28:33 +0000 Subject: [PATCH 111/186] Update plugin org.jetbrains.kotlin.jvm to v2.0.21 --- settings.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index 84e91165d..c9295ad7e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,6 +1,6 @@ pluginManagement { plugins { - kotlin("jvm") version "2.0.20" + kotlin("jvm") version "2.0.21" } } From 3d89736109695149c281ba380e776c79eaca7300 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 09:26:23 +0000 Subject: [PATCH 112/186] Update dependency maven-wrapper to v3.3.2 --- .../.mvn/wrapper/maven-wrapper.properties | 5 +++-- teamscale-maven-plugin/mvnw | 6 +++--- teamscale-maven-plugin/mvnw.cmd | 6 +++--- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/teamscale-maven-plugin/.mvn/wrapper/maven-wrapper.properties b/teamscale-maven-plugin/.mvn/wrapper/maven-wrapper.properties index 2b215e45d..23241dc39 100644 --- a/teamscale-maven-plugin/.mvn/wrapper/maven-wrapper.properties +++ b/teamscale-maven-plugin/.mvn/wrapper/maven-wrapper.properties @@ -14,6 +14,7 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -wrapperVersion=3.3.1 +wrapperVersion=3.3.2 +distributionType=script distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip -wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.1/maven-wrapper-3.3.1.jar +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar diff --git a/teamscale-maven-plugin/mvnw b/teamscale-maven-plugin/mvnw index aa09908be..5e9618cac 100755 --- a/teamscale-maven-plugin/mvnw +++ b/teamscale-maven-plugin/mvnw @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.3.1 +# Apache Maven Wrapper startup batch script, version 3.3.2 # # Required ENV vars: # ------------------ @@ -212,9 +212,9 @@ else log "Couldn't find $wrapperJarPath, downloading it ..." if [ -n "$MVNW_REPOURL" ]; then - wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.3.1/maven-wrapper-3.3.1.jar" + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar" else - wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.1/maven-wrapper-3.3.1.jar" + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar" fi while IFS="=" read -r key value; do # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) diff --git a/teamscale-maven-plugin/mvnw.cmd b/teamscale-maven-plugin/mvnw.cmd index ef8be0400..1204076a9 100644 --- a/teamscale-maven-plugin/mvnw.cmd +++ b/teamscale-maven-plugin/mvnw.cmd @@ -18,7 +18,7 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.1 +@REM Apache Maven Wrapper startup batch script, version 3.3.2 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @@ -119,7 +119,7 @@ SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.1/maven-wrapper-3.3.1.jar" +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar" FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B @@ -133,7 +133,7 @@ if exist %WRAPPER_JAR% ( ) ) else ( if not "%MVNW_REPOURL%" == "" ( - SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.3.1/maven-wrapper-3.3.1.jar" + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar" ) if "%MVNW_VERBOSE%" == "true" ( echo Couldn't find %WRAPPER_JAR%, downloading it ... From b8d5f437ae27c6e7a16dd4ff76f04631ead89145 Mon Sep 17 00:00:00 2001 From: Jonas Bogenberger Date: Mon, 14 Oct 2024 14:36:55 +0200 Subject: [PATCH 113/186] TS-40621 rework --- .../teamscale/jacoco/agent/options/AgentOptionsParser.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptionsParser.java b/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptionsParser.java index 2bdf85ec3..fa0de8279 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptionsParser.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptionsParser.java @@ -109,6 +109,7 @@ public static AgentOptions parse(String optionsString, String environmentConfigI putTeamscaleProxyOptionsIntoSystemProperties(options); handleConfigId(options); + handleConfigFile(options); Validator validator = options.getValidator(); if (!validator.isValid()) { @@ -138,7 +139,9 @@ private void handleConfigId(AgentOptions options) throws AgentOptionReceiveExcep } readConfigFromTeamscale(options); + } + private void handleConfigFile(AgentOptions options) throws AgentOptionParseException, AgentOptionReceiveException { if (environmentConfigFile != null) { handleOptionPart(options, "config-file=" + environmentConfigFile); } @@ -254,7 +257,7 @@ private boolean handleAgentOptions(AgentOptions options, String key, String valu throws AgentOptionParseException, AgentOptionReceiveException { switch (key) { case "config-id": - storeConfigIdForLaterUse(options, value); + storeConfigId(options, value); return true; case CONFIG_FILE_OPTION: readConfigFromFile(options, filePatternResolver.parsePath(key, value).toFile()); @@ -325,7 +328,7 @@ private boolean handleAgentOptions(AgentOptions options, String key, String valu } } - private void storeConfigIdForLaterUse(AgentOptions options, String configId) throws AgentOptionParseException { + private void storeConfigId(AgentOptions options, String configId) throws AgentOptionParseException { if (!options.teamscaleServer.isConfiguredForServerConnection()) { throw new AgentOptionParseException( "Has specified config-id '" + configId + "' without teamscale url/user/accessKey! The options need to be defined in teamscale.properties."); From 2a9a54b1e8f9bb2c096c5af292669495ced4dcdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Raphael=20N=C3=B6mmer?= Date: Mon, 14 Oct 2024 17:53:07 +0200 Subject: [PATCH 114/186] Changes for release version 34.1.1 --- CHANGELOG.md | 2 ++ build.gradle.kts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54b0ee661..2d92dc469 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ We use [semantic versioning](http://semver.org/): - PATCH version when you make backwards compatible bug fixes. # Next version + +# 34.1.1 - [fix] _agent_: Loading a profiler configuration from Teamscale was not possible if the potentially necessary proxy settings were not set yet. # 34.1.0 diff --git a/build.gradle.kts b/build.gradle.kts index 854136d44..6358d0a57 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ plugins { group = "com.teamscale" -val appVersion by extra("34.1.0") +val appVersion by extra("34.1.1") val snapshotVersion = appVersion + if (VersionUtils.isTaggedRelease()) "" else "-SNAPSHOT" From 9911c7b846a31a670bee09b5ab8f1985d902e419 Mon Sep 17 00:00:00 2001 From: Andreas Stahlbauer Date: Wed, 16 Oct 2024 09:48:25 +0200 Subject: [PATCH 115/186] TS-31571 Uncommited changes and rework --- .../com/teamscale/jacoco/agent/AgentBase.java | 1 + .../com/teamscale/jacoco/agent/PreMain.java | 1 + .../agent/logging/LogToTeamscaleAppender.java | 71 +++++++++++++------ .../jacoco/agent/options/AgentOptions.java | 6 +- .../TestEventHandlerStrategyBase.java | 2 +- .../CoverageToTeamscaleStrategyTest.java | 4 +- .../testimpact/TestwiseCoverageAgentTest.java | 4 +- ...mscaleProfilerConfigurationSystemTest.java | 4 +- 8 files changed, 62 insertions(+), 31 deletions(-) diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java b/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java index d9876bc53..8ff012ec1 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java @@ -133,6 +133,7 @@ private ServletContextHandler buildUsingResourceConfig() { */ void registerShutdownHook() { Runtime.getRuntime().addShutdownHook(new Thread(() -> { + logger.info("CQSE JaCoCo agent is shutting down..."); stopServer(); prepareShutdown(); logger.info("CQSE JaCoCo agent successfully shut down."); diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/PreMain.java b/agent/src/main/java/com/teamscale/jacoco/agent/PreMain.java index f9d7931ed..e72adc301 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/PreMain.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/PreMain.java @@ -157,6 +157,7 @@ private static void initializeLogging(AgentOptions agentOptions, DelayedLogger l if (agentOptions.getTeamscaleServerOptions().isConfiguredForServerConnection()) { LogToTeamscaleAppender.addTeamscaleAppenderTo(getLoggerContext(), agentOptions); + logger.info("Logs are being forwarded to Teamscale at " + agentOptions.getTeamscaleServerOptions().url); } } diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java b/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java index 0d6ae3b84..5e88fc757 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java @@ -7,6 +7,7 @@ import com.teamscale.client.ProfilerLogEntry; import com.teamscale.client.TeamscaleClient; import com.teamscale.jacoco.agent.options.AgentOptions; +import org.checkerframework.checker.nullness.qual.Nullable; import retrofit2.Call; import java.time.Duration; @@ -16,11 +17,22 @@ public class LogToTeamscaleAppender extends AppenderBase { + /** Flush the logs after N elements are in the queue */ + private static final int BATCH_SIZE = 50; + + /** Flush the logs in the given time interval */ + private static final Duration FLUSH_INTERVAL = Duration.ofSeconds(3); + + /** The unique ID of the profiler */ private String profilerId; + + /** The service client for sending logs to Teamscale */ private TeamscaleClient teamscaleClient; - private int batchSize = 10; - private Duration flushInterval = Duration.ofSeconds(3); + + /** Buffer for unsent logs */ private final List logBuffer = new ArrayList<>(); + + /** Scheduler for sending logs after the configured time interval */ private final ScheduledExecutorService scheduler; public LogToTeamscaleAppender() { @@ -32,33 +44,17 @@ public LogToTeamscaleAppender() { }); } - public void setTeamscaleClient(TeamscaleClient teamscaleClient) { - this.teamscaleClient = teamscaleClient; - } - - public void setProfilerId(String profilerId) { - this.profilerId = profilerId; - } - - public void setBatchSize(int batchSize) { - this.batchSize = batchSize; - } - - public void setFlushInterval(Duration flushInterval) { - this.flushInterval = flushInterval; - } - @Override public void start() { super.start(); - scheduler.scheduleAtFixedRate(this::flush, flushInterval.toMillis(), flushInterval.toMillis(), TimeUnit.MILLISECONDS); + scheduler.scheduleAtFixedRate(this::flush, FLUSH_INTERVAL.toMillis(), FLUSH_INTERVAL.toMillis(), TimeUnit.MILLISECONDS); } @Override protected void append(ILoggingEvent eventObject) { synchronized (logBuffer) { logBuffer.add(formatLog(eventObject)); - if (logBuffer.size() >= batchSize) { + if (logBuffer.size() >= BATCH_SIZE) { flush(); } } @@ -83,22 +79,31 @@ private void flush() { sendLogs(logsToSend); } + /** Send logs in a separate thread */ private void sendLogs(List logs) { CompletableFuture.runAsync(() -> { try { + if (teamscaleClient == null) { + // There might be no connection configured. + return; + } + Call call = teamscaleClient.service.postProfilerLog(profilerId, logs); retrofit2.Response response = call.execute(); if (!response.isSuccessful()) { throw new RuntimeException("Failed to send log: HTTP error code : " + response.code()); } } catch (Exception e) { - e.printStackTrace(); // Handle exceptions appropriately in production code + System.err.println("Sending logs to Teamscale failed: " + e.getMessage()); } }); } @Override public void stop() { + // Already flush here once to make sure that we do not miss too much. + flush(); + scheduler.shutdown(); try { if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) { @@ -107,16 +112,36 @@ public void stop() { } catch (InterruptedException e) { scheduler.shutdownNow(); } - flush(); // Ensure remaining logs are sent + + // A final flush after the scheduler has been shut down. + flush(); + super.stop(); } + public void setTeamscaleClient(TeamscaleClient teamscaleClient) { + this.teamscaleClient = teamscaleClient; + } + public void setProfilerId(String profilerId) { + this.profilerId = profilerId; + } + + /** + * Add the {@link com.teamscale.jacoco.agent.logging.LogToTeamscaleAppender} to the logging configuration + * and enable/start it. + */ public static void addTeamscaleAppenderTo(LoggerContext context, AgentOptions agentOptions) { + @Nullable TeamscaleClient client = agentOptions.createTeamscaleClient( + false); + if (client == null) { + return; + } + LogToTeamscaleAppender logToTeamscaleAppender = new LogToTeamscaleAppender(); logToTeamscaleAppender.setContext(context); logToTeamscaleAppender.setProfilerId(agentOptions.configurationViaTeamscale.getProfilerId()); - logToTeamscaleAppender.setTeamscaleClient(agentOptions.createTeamscaleClient()); + logToTeamscaleAppender.setTeamscaleClient(client); logToTeamscaleAppender.start(); Logger rootLogger = context.getLogger(Logger.ROOT_LOGGER_NAME); diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptions.java b/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptions.java index f2855083b..32e7ceb7e 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptions.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptions.java @@ -35,6 +35,7 @@ import com.teamscale.report.EDuplicateClassFileBehavior; import com.teamscale.report.util.ClasspathWildcardIncludeFilter; import com.teamscale.report.util.ILogger; +import org.checkerframework.checker.nullness.qual.Nullable; import org.conqat.lib.commons.assertion.CCSMAssert; import org.conqat.lib.commons.collections.PairList; import org.jacoco.core.runtime.WildcardMatcher; @@ -378,8 +379,9 @@ private void validateTestwiseCoverageConfig(Validator validator) { * Creates a {@link TeamscaleClient} based on the agent options. Returns null if the user did not fully configure a * Teamscale connection. */ - public TeamscaleClient createTeamscaleClient() { - if (teamscaleServer.isConfiguredForSingleProjectTeamscaleUpload()) { + public @Nullable TeamscaleClient createTeamscaleClient(boolean requireSingleProjectUploadConfig) { + if (teamscaleServer.isConfiguredForSingleProjectTeamscaleUpload() || + !requireSingleProjectUploadConfig && teamscaleServer.isConfiguredForServerConnection()) { return new TeamscaleClient(teamscaleServer.url.toString(), teamscaleServer.userName, teamscaleServer.userAccessToken, teamscaleServer.project); } diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/testimpact/TestEventHandlerStrategyBase.java b/agent/src/main/java/com/teamscale/jacoco/agent/testimpact/TestEventHandlerStrategyBase.java index 621280867..3efe3adb9 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/testimpact/TestEventHandlerStrategyBase.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/testimpact/TestEventHandlerStrategyBase.java @@ -41,7 +41,7 @@ public abstract class TestEventHandlerStrategyBase { protected TestEventHandlerStrategyBase(AgentOptions agentOptions, JacocoRuntimeController controller) { this.controller = controller; this.agentOptions = agentOptions; - this.teamscaleClient = agentOptions.createTeamscaleClient(); + this.teamscaleClient = agentOptions.createTeamscaleClient(true); } /** Called when test test with the given name is about to start. */ diff --git a/agent/src/test/java/com/teamscale/jacoco/agent/testimpact/CoverageToTeamscaleStrategyTest.java b/agent/src/test/java/com/teamscale/jacoco/agent/testimpact/CoverageToTeamscaleStrategyTest.java index da3e24bb9..14233b1f7 100644 --- a/agent/src/test/java/com/teamscale/jacoco/agent/testimpact/CoverageToTeamscaleStrategyTest.java +++ b/agent/src/test/java/com/teamscale/jacoco/agent/testimpact/CoverageToTeamscaleStrategyTest.java @@ -113,7 +113,7 @@ protected static TestwiseCoverage getDummyTestwiseCoverage(String test) { private AgentOptions mockOptions() throws IOException { AgentOptions options = mock(AgentOptions.class); - when(options.createTeamscaleClient()).thenReturn(client); + when(options.createTeamscaleClient(true)).thenReturn(client); when(options.createNewFileInOutputDirectory(any(), any())).thenReturn(new File(tempDir, "test")); TeamscaleServer server = new TeamscaleServer(); @@ -124,7 +124,7 @@ private AgentOptions mockOptions() throws IOException { server.partition = "partition"; when(options.getTeamscaleServerOptions()).thenReturn(server); - when(options.createTeamscaleClient()).thenReturn(client); + when(options.createTeamscaleClient(true)).thenReturn(client); return options; } diff --git a/agent/src/test/java/com/teamscale/jacoco/agent/testimpact/TestwiseCoverageAgentTest.java b/agent/src/test/java/com/teamscale/jacoco/agent/testimpact/TestwiseCoverageAgentTest.java index 40df45d00..62e7ef186 100644 --- a/agent/src/test/java/com/teamscale/jacoco/agent/testimpact/TestwiseCoverageAgentTest.java +++ b/agent/src/test/java/com/teamscale/jacoco/agent/testimpact/TestwiseCoverageAgentTest.java @@ -158,7 +158,7 @@ public void shouldHandleMissingRequestBodyForTestrunStartGracefully() throws Exc private AgentOptions mockOptions(int port) { AgentOptions options = mock(AgentOptions.class); - when(options.createTeamscaleClient()).thenReturn(client); + when(options.createTeamscaleClient(true)).thenReturn(client); TeamscaleServer server = new TeamscaleServer(); server.commit = new CommitDescriptor("branch", "12345"); @@ -170,7 +170,7 @@ private AgentOptions mockOptions(int port) { when(options.getHttpServerPort()).thenReturn(port); when(options.getTestwiseCoverageMode()).thenReturn(ETestwiseCoverageMode.TEAMSCALE_UPLOAD); - when(options.createTeamscaleClient()).thenReturn(client); + when(options.createTeamscaleClient(true)).thenReturn(client); return options; } diff --git a/system-tests/teamscale-profiler-configuration-test/src/test/java/com/teamscale/client/TeamscaleProfilerConfigurationSystemTest.java b/system-tests/teamscale-profiler-configuration-test/src/test/java/com/teamscale/client/TeamscaleProfilerConfigurationSystemTest.java index 87ed40b71..ba68a3858 100644 --- a/system-tests/teamscale-profiler-configuration-test/src/test/java/com/teamscale/client/TeamscaleProfilerConfigurationSystemTest.java +++ b/system-tests/teamscale-profiler-configuration-test/src/test/java/com/teamscale/client/TeamscaleProfilerConfigurationSystemTest.java @@ -38,7 +38,9 @@ public void systemTestRetrieveConfig() throws Exception { assertThat(teamscaleMockServer.getProfilerEvents()).as("We expect a sequence of interactions with the mock. " + "Note that unexpected interactions can be caused by old agent instances that have not been killed properly.") // - .containsExactly("Profiler registered and requested configuration my-config", "Profiler 123 sent heartbeat", + .containsExactly("Profiler registered and requested configuration my-config", + "Profiler 123 sent logs", + "Profiler 123 sent heartbeat", "Profiler 123 unregistered"); } From 23a836206d8130728ac15131b35dac644a1b64c5 Mon Sep 17 00:00:00 2001 From: Andreas Stahlbauer Date: Wed, 16 Oct 2024 09:57:12 +0200 Subject: [PATCH 116/186] TS-31571 Findings fixed --- .../teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java b/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java index 5e88fc757..53bf34e24 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java @@ -91,7 +91,7 @@ private void sendLogs(List logs) { Call call = teamscaleClient.service.postProfilerLog(profilerId, logs); retrofit2.Response response = call.execute(); if (!response.isSuccessful()) { - throw new RuntimeException("Failed to send log: HTTP error code : " + response.code()); + throw new IllegalStateException("Failed to send log: HTTP error code : " + response.code()); } } catch (Exception e) { System.err.println("Sending logs to Teamscale failed: " + e.getMessage()); From 98662a49c3276272f8c6d19967eacb8675d5fdb6 Mon Sep 17 00:00:00 2001 From: Andreas Stahlbauer Date: Wed, 16 Oct 2024 11:18:53 +0200 Subject: [PATCH 117/186] TS-31571 Wait for flush on termination --- .../java/com/teamscale/jacoco/agent/PreMain.java | 2 -- .../agent/logging/LogToTeamscaleAppender.java | 14 ++++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/PreMain.java b/agent/src/main/java/com/teamscale/jacoco/agent/PreMain.java index e72adc301..f1154b1d3 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/PreMain.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/PreMain.java @@ -1,7 +1,6 @@ package com.teamscale.jacoco.agent; import com.teamscale.client.HttpUtils; -import com.teamscale.client.TeamscaleServer; import com.teamscale.jacoco.agent.configuration.AgentOptionReceiveException; import com.teamscale.jacoco.agent.logging.LogToTeamscaleAppender; import com.teamscale.jacoco.agent.options.AgentOptionParseException; @@ -13,7 +12,6 @@ import com.teamscale.jacoco.agent.options.TeamscalePropertiesUtils; import com.teamscale.jacoco.agent.testimpact.TestwiseCoverageAgent; import com.teamscale.jacoco.agent.upload.UploaderException; -import com.teamscale.jacoco.agent.upload.teamscale.TeamscaleConfig; import com.teamscale.jacoco.agent.util.AgentUtils; import com.teamscale.jacoco.agent.logging.DebugLogDirectoryPropertyDefiner; import com.teamscale.jacoco.agent.logging.LogDirectoryPropertyDefiner; diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java b/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java index 53bf34e24..5b9ce0ca5 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java @@ -8,11 +8,13 @@ import com.teamscale.client.TeamscaleClient; import com.teamscale.jacoco.agent.options.AgentOptions; import org.checkerframework.checker.nullness.qual.Nullable; +import org.conqat.lib.commons.collections.IdentityHashSet; import retrofit2.Call; import java.time.Duration; import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.concurrent.*; public class LogToTeamscaleAppender extends AppenderBase { @@ -35,6 +37,9 @@ public class LogToTeamscaleAppender extends AppenderBase { /** Scheduler for sending logs after the configured time interval */ private final ScheduledExecutorService scheduler; + /** Active log flushing threads */ + private final Set> activeLogFlushes = new IdentityHashSet<>(); + public LogToTeamscaleAppender() { this.scheduler = Executors.newScheduledThreadPool(1, r -> { // Make the thread a daemon so that it does not prevent the JVM from terminating. @@ -81,7 +86,7 @@ private void flush() { /** Send logs in a separate thread */ private void sendLogs(List logs) { - CompletableFuture.runAsync(() -> { + activeLogFlushes.add(CompletableFuture.runAsync(() -> { try { if (teamscaleClient == null) { // There might be no connection configured. @@ -96,7 +101,9 @@ private void sendLogs(List logs) { } catch (Exception e) { System.err.println("Sending logs to Teamscale failed: " + e.getMessage()); } - }); + }).whenComplete((result, throwable) -> { + activeLogFlushes.removeIf(CompletableFuture::isDone); + })); } @Override @@ -116,6 +123,9 @@ public void stop() { // A final flush after the scheduler has been shut down. flush(); + // Block until all flushes are done + CompletableFuture.allOf(activeLogFlushes.toArray(new CompletableFuture[0])).join(); + super.stop(); } From 218ef77fae86f104b9c0be4da568040378116c8f Mon Sep 17 00:00:00 2001 From: Andreas Stahlbauer Date: Wed, 16 Oct 2024 11:23:49 +0200 Subject: [PATCH 118/186] TS-31571 Ensure logging of shutdown errors --- .../java/com/teamscale/jacoco/agent/AgentBase.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java b/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java index 8ff012ec1..de23a5ecc 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java @@ -133,11 +133,15 @@ private ServletContextHandler buildUsingResourceConfig() { */ void registerShutdownHook() { Runtime.getRuntime().addShutdownHook(new Thread(() -> { - logger.info("CQSE JaCoCo agent is shutting down..."); - stopServer(); - prepareShutdown(); - logger.info("CQSE JaCoCo agent successfully shut down."); - PreMain.closeLoggingResources(); + try { + logger.info("CQSE JaCoCo agent is shutting down..."); + stopServer(); + prepareShutdown(); + logger.info("CQSE JaCoCo agent successfully shut down."); + } finally { + // Try to flush logging resources also in case of an exception during shutdown + PreMain.closeLoggingResources(); + } })); } From c7b1f3ee656a72e0482432e3e02ecc15efe446d7 Mon Sep 17 00:00:00 2001 From: Andreas Stahlbauer Date: Thu, 17 Oct 2024 10:24:51 +0200 Subject: [PATCH 119/186] TS-31571 Enhanced backwards compatibility --- .../ConfigurationViaTeamscale.java | 10 +++++++++ .../teamscale/client/ITeamscaleService.java | 22 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/configuration/ConfigurationViaTeamscale.java b/agent/src/main/java/com/teamscale/jacoco/agent/configuration/ConfigurationViaTeamscale.java index 48ee78655..8f64e4829 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/configuration/ConfigurationViaTeamscale.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/configuration/ConfigurationViaTeamscale.java @@ -60,6 +60,10 @@ public static ConfigurationViaTeamscale retrieve(ILogger logger, String configur ProcessInformation processInformation = new ProcessInformationRetriever(logger).getProcessInformation(); Response response = teamscaleClient.registerProfiler(configurationId, processInformation).execute(); + if (response.code() == 405) { + response = teamscaleClient.registerProfilerLegacy(configurationId, + processInformation).execute(); + } if (!response.isSuccessful()) { if (response.code() >= 400 && response.code() < 500) { throw new AgentOptionParseException( @@ -109,6 +113,9 @@ public void startHeartbeatThreadAndRegisterShutdownHook() { private void sendHeartbeat() { try { Response response = teamscaleClient.sendHeartbeat(profilerId, profilerInfo).execute(); + if (response.code() == 405) { + response = teamscaleClient.sendHeartbeatLegacy(profilerId, profilerInfo).execute(); + } if (!response.isSuccessful()) { LoggingUtils.getLogger(this) .error("Failed to send heartbeat. Teamscale responded with: " + response.errorBody().string()); @@ -121,6 +128,9 @@ private void sendHeartbeat() { private void unregisterProfiler() { try { Response response = teamscaleClient.unregisterProfiler(profilerId).execute(); + if (response.code() == 405) { + response = teamscaleClient.unregisterProfilerLegacy(profilerId).execute(); + } if (!response.isSuccessful()) { LoggingUtils.getLogger(this) .error("Failed to unregister profiler. Teamscale responded with: " + response.errorBody() diff --git a/teamscale-client/src/main/java/com/teamscale/client/ITeamscaleService.java b/teamscale-client/src/main/java/com/teamscale/client/ITeamscaleService.java index 50f3ab861..d2983b54e 100644 --- a/teamscale-client/src/main/java/com/teamscale/client/ITeamscaleService.java +++ b/teamscale-client/src/main/java/com/teamscale/client/ITeamscaleService.java @@ -145,6 +145,28 @@ Call> getImpactedTests( @Query("include-added-tests") boolean includeAddedTests ); + /** Registers a profiler to Teamscale and returns the profiler configuration it should be started with. */ + @Deprecated + @POST("api/v9.4.0/running-profilers") + Call registerProfilerLegacy( + @Query("configuration-id") String configurationId, + @Body ProcessInformation processInformation + ); + + /** Updates the profiler infos and sets the profiler to still alive. */ + @Deprecated + @PUT("api/v9.4.0/running-profilers/{profilerId}") + Call sendHeartbeatLegacy( + @Path("profilerId") String profilerId, + @Body ProfilerInfo profilerInfo + ); + + /** Removes the profiler identified by given ID. */ + @Deprecated + @DELETE("api/v9.4.0/running-profilers/{profilerId}") + Call unregisterProfilerLegacy(@Path("profilerId") String profilerId); + + /** Registers a profiler to Teamscale and returns the profiler configuration it should be started with. */ @POST("api/v2024.7.0/profilers") Call registerProfiler( From fa4b88d40d007b28e0dd1f8f5a43a31b48ecda12 Mon Sep 17 00:00:00 2001 From: Andreas Stahlbauer Date: Thu, 17 Oct 2024 11:07:28 +0200 Subject: [PATCH 120/186] TS-31571 Keep logs to flush is Teamscale is not yet available. --- .../agent/logging/LogToTeamscaleAppender.java | 60 ++++++++++++------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java b/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java index 5b9ce0ca5..f70f77c99 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java @@ -13,6 +13,8 @@ import java.time.Duration; import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.concurrent.*; @@ -31,8 +33,9 @@ public class LogToTeamscaleAppender extends AppenderBase { /** The service client for sending logs to Teamscale */ private TeamscaleClient teamscaleClient; - /** Buffer for unsent logs */ - private final List logBuffer = new ArrayList<>(); + /** Buffer for unsent logs. We use a set here to allow for removing + * entries fast after sending them to Teamscale was successful. */ + private final LinkedHashSet logBuffer = new LinkedHashSet<>(); /** Scheduler for sending logs after the configured time interval */ private final ScheduledExecutorService scheduler; @@ -52,7 +55,13 @@ public LogToTeamscaleAppender() { @Override public void start() { super.start(); - scheduler.scheduleAtFixedRate(this::flush, FLUSH_INTERVAL.toMillis(), FLUSH_INTERVAL.toMillis(), TimeUnit.MILLISECONDS); + scheduler.scheduleAtFixedRate(() -> { + synchronized (activeLogFlushes) { + if (this.activeLogFlushes.isEmpty()) { + flush(); + } + } + }, FLUSH_INTERVAL.toMillis(), FLUSH_INTERVAL.toMillis(), TimeUnit.MILLISECONDS); } @Override @@ -79,31 +88,38 @@ private void flush() { return; } logsToSend = new ArrayList<>(logBuffer); - logBuffer.clear(); } sendLogs(logsToSend); } /** Send logs in a separate thread */ - private void sendLogs(List logs) { - activeLogFlushes.add(CompletableFuture.runAsync(() -> { - try { - if (teamscaleClient == null) { - // There might be no connection configured. - return; - } - - Call call = teamscaleClient.service.postProfilerLog(profilerId, logs); - retrofit2.Response response = call.execute(); - if (!response.isSuccessful()) { - throw new IllegalStateException("Failed to send log: HTTP error code : " + response.code()); + private void sendLogs(List logsToSend) { + synchronized (activeLogFlushes) { + activeLogFlushes.add(CompletableFuture.runAsync(() -> { + try { + if (teamscaleClient == null) { + // There might be no connection configured. + return; + } + + Call call = teamscaleClient.service.postProfilerLog(profilerId, logsToSend); + retrofit2.Response response = call.execute(); + if (!response.isSuccessful()) { + throw new IllegalStateException("Failed to send log: HTTP error code : " + response.code()); + } + + synchronized (logBuffer) { + // Removing the logs that have been sent after the fact. + // This handles problems with lost network connections. + logsToSend.forEach(logBuffer::remove); + } + } catch (Exception e) { + System.err.println("Sending logs to Teamscale failed: " + e.getMessage()); } - } catch (Exception e) { - System.err.println("Sending logs to Teamscale failed: " + e.getMessage()); - } - }).whenComplete((result, throwable) -> { - activeLogFlushes.removeIf(CompletableFuture::isDone); - })); + }).whenComplete((result, throwable) -> { + activeLogFlushes.removeIf(CompletableFuture::isDone); + })); + } } @Override From 55f347c583146cc6d4d3e39c8fc8edf9b0515d8c Mon Sep 17 00:00:00 2001 From: Andreas Stahlbauer Date: Thu, 17 Oct 2024 13:46:16 +0200 Subject: [PATCH 121/186] TS-31571 Some rework --- .../jacoco/agent/logging/LogToTeamscaleAppender.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java b/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java index f70f77c99..e7b3bc681 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java @@ -11,6 +11,7 @@ import org.conqat.lib.commons.collections.IdentityHashSet; import retrofit2.Call; +import java.net.ConnectException; import java.time.Duration; import java.util.ArrayList; import java.util.Collection; @@ -57,6 +58,7 @@ public void start() { super.start(); scheduler.scheduleAtFixedRate(() -> { synchronized (activeLogFlushes) { + activeLogFlushes.removeIf(CompletableFuture::isDone); if (this.activeLogFlushes.isEmpty()) { flush(); } @@ -114,10 +116,15 @@ private void sendLogs(List logsToSend) { logsToSend.forEach(logBuffer::remove); } } catch (Exception e) { - System.err.println("Sending logs to Teamscale failed: " + e.getMessage()); + // We do not report on connection exceptions here. + if (!(e instanceof ConnectException)) { + System.err.println("Sending logs to Teamscale failed: " + e.getMessage()); + } } }).whenComplete((result, throwable) -> { - activeLogFlushes.removeIf(CompletableFuture::isDone); + synchronized (activeLogFlushes) { + activeLogFlushes.removeIf(CompletableFuture::isDone); + } })); } } From e61b2e16c3ad00dc562a66ba4824646297562390 Mon Sep 17 00:00:00 2001 From: Andreas Stahlbauer Date: Mon, 21 Oct 2024 08:25:00 +0200 Subject: [PATCH 122/186] TS-31571 Rework: Also send stack traces --- .../com/teamscale/jacoco/agent/PreMain.java | 39 ++++++++-- .../ConfigurationViaTeamscale.java | 3 +- .../agent/logging/LogToTeamscaleAppender.java | 74 +++++++++++-------- .../jacoco/agent/logging/LoggingUtils.java | 21 ++++++ .../agent/options/AgentOptionsParser.java | 40 +++++++--- .../teamscale/client/ProfilerLogEntry.java | 10 ++- 6 files changed, 136 insertions(+), 51 deletions(-) diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/PreMain.java b/agent/src/main/java/com/teamscale/jacoco/agent/PreMain.java index f1154b1d3..fbff0d1ae 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/PreMain.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/PreMain.java @@ -17,9 +17,9 @@ import com.teamscale.jacoco.agent.logging.LogDirectoryPropertyDefiner; import com.teamscale.jacoco.agent.logging.LoggingUtils; import org.conqat.lib.commons.collections.CollectionUtils; +import org.conqat.lib.commons.collections.Pair; import org.conqat.lib.commons.filesystem.FileSystemUtils; import org.conqat.lib.commons.string.StringUtils; -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import java.io.File; @@ -69,9 +69,27 @@ public static void premain(String options, Instrumentation instrumentation) thro return; } - AgentOptions agentOptions; + AgentOptions agentOptions = null; try { - agentOptions = getAndApplyAgentOptions(options, environmentConfigId, environmentConfigFile); + Pair> parseResult = getAndApplyAgentOptions(options, environmentConfigId, + environmentConfigFile); + agentOptions = parseResult.getFirst(); + + for (Exception exception : parseResult.getSecond()) { + throw new AgentOptionParseException("Failed to parse options: " + exception.getMessage(), exception); + } + } catch (AgentOptionParseException e) { + getLoggerContext().getLogger(PreMain.class).error(e.getMessage()); + + // Flush logs to Teamscale, if configured. + closeLoggingResources(); + + // Unregister the profiler from Teamscale. + if (agentOptions != null && agentOptions.configurationViaTeamscale != null) { + agentOptions.configurationViaTeamscale.unregisterProfiler(); + } + + throw e; } catch (AgentOptionReceiveException e) { // When Teamscale is not available, we don't want to fail hard to still allow for testing even if no // coverage is collected (see TS-33237) @@ -92,8 +110,7 @@ public static void premain(String options, Instrumentation instrumentation) thro agent.registerShutdownHook(); } - @NotNull - private static AgentOptions getAndApplyAgentOptions(String options, String environmentConfigId, + private static Pair> getAndApplyAgentOptions(String options, String environmentConfigId, String environmentConfigFile) throws AgentOptionParseException, IOException, AgentOptionReceiveException { DelayedLogger delayedLogger = new DelayedLogger(); @@ -110,9 +127,13 @@ private static AgentOptions getAndApplyAgentOptions(String options, String envir if (credentials == null) { delayedLogger.warn("Did not find a teamscale.properties file!"); } + + Pair> parseResult; AgentOptions agentOptions; try { - agentOptions = AgentOptionsParser.parse(options, environmentConfigId, environmentConfigFile, credentials, delayedLogger); + parseResult = AgentOptionsParser.parse( + options, environmentConfigId, environmentConfigFile, credentials, delayedLogger); + agentOptions = parseResult.getFirst(); } catch (AgentOptionParseException e) { try (LoggingUtils.LoggingResources ignored = initializeFallbackLogging(options, delayedLogger)) { delayedLogger.errorAndStdErr("Failed to parse agent options: " + e.getMessage(), e); @@ -133,7 +154,9 @@ private static AgentOptions getAndApplyAgentOptions(String options, String envir Logger logger = LoggingUtils.getLogger(Agent.class); delayedLogger.logTo(logger); HttpUtils.setShouldValidateSsl(agentOptions.shouldValidateSsl()); - return agentOptions; + + + return parseResult; } private static void attemptLogAndThrow(DelayedLogger delayedLogger) { @@ -197,7 +220,7 @@ private static void initializeDebugLogging(AgentOptions agentOptions, DelayedLog * this and falls back to the default logger. */ private static LoggingUtils.LoggingResources initializeFallbackLogging(String premainOptions, - DelayedLogger delayedLogger) { + DelayedLogger delayedLogger) { if (premainOptions == null) { return LoggingUtils.initializeDefaultLogging(); } diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/configuration/ConfigurationViaTeamscale.java b/agent/src/main/java/com/teamscale/jacoco/agent/configuration/ConfigurationViaTeamscale.java index 8f64e4829..3bccd6193 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/configuration/ConfigurationViaTeamscale.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/configuration/ConfigurationViaTeamscale.java @@ -125,7 +125,8 @@ private void sendHeartbeat() { } } - private void unregisterProfiler() { + /** Unregisters the profiler in Teamscale (marks it as shut down). */ + public void unregisterProfiler() { try { Response response = teamscaleClient.unregisterProfiler(profilerId).execute(); if (response.code() == 405) { diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java b/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java index e7b3bc681..6c9eccac8 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java @@ -4,6 +4,7 @@ import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.AppenderBase; +import ch.qos.logback.core.status.ErrorStatus; import com.teamscale.client.ProfilerLogEntry; import com.teamscale.client.TeamscaleClient; import com.teamscale.jacoco.agent.options.AgentOptions; @@ -14,11 +15,13 @@ import java.net.ConnectException; import java.time.Duration; import java.util.ArrayList; -import java.util.Collection; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.teamscale.jacoco.agent.logging.LoggingUtils.getStackTraceFromEvent; public class LogToTeamscaleAppender extends AppenderBase { @@ -44,6 +47,10 @@ public class LogToTeamscaleAppender extends AppenderBase { /** Active log flushing threads */ private final Set> activeLogFlushes = new IdentityHashSet<>(); + /** Is performing a flush right now? */ + private final AtomicBoolean isFlusing = new AtomicBoolean(false); + + public LogToTeamscaleAppender() { this.scheduler = Executors.newScheduledThreadPool(1, r -> { // Make the thread a daemon so that it does not prevent the JVM from terminating. @@ -80,45 +87,48 @@ private ProfilerLogEntry formatLog(ILoggingEvent eventObject) { long timestamp = eventObject.getTimeStamp(); String message = eventObject.getFormattedMessage(); String severity = eventObject.getLevel().toString(); - return new ProfilerLogEntry(timestamp, message, severity); + String details = getStackTraceFromEvent(eventObject); + return new ProfilerLogEntry(timestamp, message, details, severity); } private void flush() { - List logsToSend; - synchronized (logBuffer) { - if (logBuffer.isEmpty()) { - return; - } - logsToSend = new ArrayList<>(logBuffer); - } - sendLogs(logsToSend); + sendLogs(); } /** Send logs in a separate thread */ - private void sendLogs(List logsToSend) { + private void sendLogs() { synchronized (activeLogFlushes) { activeLogFlushes.add(CompletableFuture.runAsync(() -> { - try { - if (teamscaleClient == null) { - // There might be no connection configured. - return; - } - - Call call = teamscaleClient.service.postProfilerLog(profilerId, logsToSend); - retrofit2.Response response = call.execute(); - if (!response.isSuccessful()) { - throw new IllegalStateException("Failed to send log: HTTP error code : " + response.code()); - } - - synchronized (logBuffer) { - // Removing the logs that have been sent after the fact. - // This handles problems with lost network connections. - logsToSend.forEach(logBuffer::remove); - } - } catch (Exception e) { - // We do not report on connection exceptions here. - if (!(e instanceof ConnectException)) { - System.err.println("Sending logs to Teamscale failed: " + e.getMessage()); + if (isFlusing.compareAndSet(false, true)) { + try { + if (teamscaleClient == null) { + // There might be no connection configured. + return; + } + + List logsToSend; + synchronized (logBuffer) { + logsToSend = new ArrayList<>(logBuffer); + } + + Call call = teamscaleClient.service.postProfilerLog(profilerId, logsToSend); + retrofit2.Response response = call.execute(); + if (!response.isSuccessful()) { + throw new IllegalStateException("Failed to send log: HTTP error code : " + response.code()); + } + + synchronized (logBuffer) { + // Removing the logs that have been sent after the fact. + // This handles problems with lost network connections. + logsToSend.forEach(logBuffer::remove); + } + } catch (Exception e) { + // We do not report on exceptions here. + if (!(e instanceof ConnectException)) { + addStatus(new ErrorStatus("Sending logs to Teamscale failed: " + e.getMessage(), this, e)); + } + } finally { + isFlusing.set(false); } } }).whenComplete((result, throwable) -> { diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/logging/LoggingUtils.java b/agent/src/main/java/com/teamscale/jacoco/agent/logging/LoggingUtils.java index 5f90300e0..6e0bcd87b 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/logging/LoggingUtils.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/logging/LoggingUtils.java @@ -7,6 +7,10 @@ import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.joran.JoranConfigurator; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.IThrowableProxy; +import ch.qos.logback.classic.spi.StackTraceElementProxy; +import ch.qos.logback.classic.spi.ThrowableProxyUtil; import ch.qos.logback.core.joran.spi.JoranException; import ch.qos.logback.core.util.StatusPrinter; import com.teamscale.jacoco.agent.Agent; @@ -59,6 +63,23 @@ public static LoggerContext getLoggerContext() { return (LoggerContext) LoggerFactory.getILoggerFactory(); } + /** + * Extracts the stack trace from an ILoggingEvent using ThrowableProxyUtil. + * + * @param event the logging event containing the exception + * @return the stack trace as a String, or null if no exception is associated + */ + public static String getStackTraceFromEvent(ILoggingEvent event) { + IThrowableProxy throwableProxy = event.getThrowableProxy(); + + if (throwableProxy != null) { + // Use ThrowableProxyUtil to convert the IThrowableProxy to a String + return ThrowableProxyUtil.asString(throwableProxy); + } + + return null; + } + /** * Reconfigures the logger context to use the configuration XML from the given input stream. Cf. https://logback.qos.ch/manual/configuration.html diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptionsParser.java b/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptionsParser.java index a91a626ad..6f6f6fba2 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptionsParser.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptionsParser.java @@ -6,6 +6,7 @@ package com.teamscale.jacoco.agent.options; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Maps; import com.teamscale.client.StringUtils; import com.teamscale.jacoco.agent.commandline.Validator; import com.teamscale.jacoco.agent.configuration.AgentOptionReceiveException; @@ -17,6 +18,7 @@ import com.teamscale.report.EDuplicateClassFileBehavior; import com.teamscale.report.util.ILogger; import okhttp3.HttpUrl; +import org.apache.commons.compress.utils.Lists; import org.conqat.lib.commons.collections.CollectionUtils; import org.conqat.lib.commons.collections.Pair; import org.conqat.lib.commons.filesystem.FileSystemUtils; @@ -27,7 +29,9 @@ import java.nio.file.InvalidPathException; import java.nio.file.Paths; import java.util.Arrays; +import java.util.Collection; import java.util.List; +import java.util.Map; import static java.util.stream.Collectors.joining; @@ -51,6 +55,7 @@ public class AgentOptionsParser { private final String environmentConfigId; private final String environmentConfigFile; private final TeamscaleCredentials credentials; + private final List collectedErrors; /** * Parses the given command-line options. @@ -58,11 +63,12 @@ public class AgentOptionsParser { * @param environmentConfigId The Profiler configuration ID given via an environment variable. * @param environmentConfigFile The Profiler configuration file given via an environment variable. */ - public static AgentOptions parse(String optionsString, String environmentConfigId, String environmentConfigFile, + public static Pair> parse(String optionsString, String environmentConfigId, String environmentConfigFile, TeamscaleCredentials credentials, ILogger logger) throws AgentOptionParseException, AgentOptionReceiveException { - return new AgentOptionsParser(logger, environmentConfigId, environmentConfigFile, credentials).parse( - optionsString); + AgentOptionsParser parser = new AgentOptionsParser(logger, environmentConfigId, environmentConfigFile, credentials); + AgentOptions options = parser.parse(optionsString); + return Pair.createPair(options, parser.getCollectedErrors()); } @VisibleForTesting @@ -74,6 +80,11 @@ public static AgentOptions parse(String optionsString, String environmentConfigI this.environmentConfigId = environmentConfigId; this.environmentConfigFile = environmentConfigFile; this.credentials = credentials; + this.collectedErrors = Lists.newArrayList(); + } + + private List getCollectedErrors() { + return collectedErrors; } /** @@ -81,10 +92,12 @@ public static AgentOptions parse(String optionsString, String environmentConfigI */ /* package */ AgentOptions parse( String optionsString) throws AgentOptionParseException, AgentOptionReceiveException { + if (optionsString == null) { optionsString = ""; } logger.debug("Parsing options: " + optionsString); + AgentOptions options = new AgentOptions(logger); options.originalOptionsString = optionsString; @@ -97,7 +110,11 @@ public static AgentOptions parse(String optionsString, String environmentConfigI if (!StringUtils.isEmpty(optionsString)) { String[] optionParts = optionsString.split(","); for (String optionPart : optionParts) { - handleOption(options, optionPart); + try { + handleOption(options, optionPart); + } catch (Exception e) { + collectedErrors.add(e); + } } } @@ -105,8 +122,9 @@ public static AgentOptions parse(String optionsString, String environmentConfigI Validator validator = options.getValidator(); if (!validator.isValid()) { - throw new AgentOptionParseException("Invalid options given: " + validator.getErrorMessage()); + collectedErrors.add(new AgentOptionParseException("Invalid options given: " + validator.getErrorMessage())); } + return options; } @@ -343,11 +361,15 @@ private void readConfigFromString(AgentOptions options, List configFileKeyValues = org.conqat.lib.commons.string.StringUtils.splitLinesAsList( content); for (String optionKeyValue : configFileKeyValues) { - String trimmedOption = optionKeyValue.trim(); - if (trimmedOption.isEmpty() || trimmedOption.startsWith(COMMENT_PREFIX)) { - continue; + try { + String trimmedOption = optionKeyValue.trim(); + if (trimmedOption.isEmpty() || trimmedOption.startsWith(COMMENT_PREFIX)) { + continue; + } + handleOption(options, optionKeyValue); + } catch (Exception e) { + collectedErrors.add(e); } - handleOption(options, optionKeyValue); } } diff --git a/teamscale-client/src/main/java/com/teamscale/client/ProfilerLogEntry.java b/teamscale-client/src/main/java/com/teamscale/client/ProfilerLogEntry.java index 7ef2e5665..5ef6f541e 100644 --- a/teamscale-client/src/main/java/com/teamscale/client/ProfilerLogEntry.java +++ b/teamscale-client/src/main/java/com/teamscale/client/ProfilerLogEntry.java @@ -1,17 +1,25 @@ package com.teamscale.client; +/** A log entry to be sent to Teamscale */ public class ProfilerLogEntry { + /** The time of the event */ private final long timestamp; + /** Log message */ private final String message; + /** Event severity */ private final String severity; - public ProfilerLogEntry(long timestamp, String message, String severity) { + /** Details, for example, the stack trace */ + private final String details; + + public ProfilerLogEntry(long timestamp, String message, String details, String severity) { this.timestamp = timestamp; this.message = message; this.severity = severity; + this.details = details; } } From ee68f414ab357a4e688c36b72e2bf32d4c8b8ac7 Mon Sep 17 00:00:00 2001 From: Andreas Stahlbauer Date: Mon, 21 Oct 2024 14:44:15 +0200 Subject: [PATCH 123/186] TS-31571 Ensure to send the stack trace, too --- agent/src/main/java/com/teamscale/jacoco/agent/PreMain.java | 2 +- .../jacoco/agent/logging/LogToTeamscaleAppender.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/PreMain.java b/agent/src/main/java/com/teamscale/jacoco/agent/PreMain.java index fbff0d1ae..9466e8637 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/PreMain.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/PreMain.java @@ -79,7 +79,7 @@ public static void premain(String options, Instrumentation instrumentation) thro throw new AgentOptionParseException("Failed to parse options: " + exception.getMessage(), exception); } } catch (AgentOptionParseException e) { - getLoggerContext().getLogger(PreMain.class).error(e.getMessage()); + getLoggerContext().getLogger(PreMain.class).error(e.getMessage(), e); // Flush logs to Teamscale, if configured. closeLoggingResources(); diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java b/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java index 6c9eccac8..ac2492fe2 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java @@ -84,11 +84,11 @@ protected void append(ILoggingEvent eventObject) { } private ProfilerLogEntry formatLog(ILoggingEvent eventObject) { + String trace = getStackTraceFromEvent(eventObject); long timestamp = eventObject.getTimeStamp(); String message = eventObject.getFormattedMessage(); String severity = eventObject.getLevel().toString(); - String details = getStackTraceFromEvent(eventObject); - return new ProfilerLogEntry(timestamp, message, details, severity); + return new ProfilerLogEntry(timestamp, message, trace, severity); } private void flush() { From cd9cd30f30e8ac2222e5d1f2d46337af0b045f85 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2024 15:07:31 +0000 Subject: [PATCH 124/186] Update dependency org.junit.jupiter:junit-jupiter-engine to v5.11.3 --- teamscale-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teamscale-maven-plugin/pom.xml b/teamscale-maven-plugin/pom.xml index 49f5a8992..d1755c968 100644 --- a/teamscale-maven-plugin/pom.xml +++ b/teamscale-maven-plugin/pom.xml @@ -65,7 +65,7 @@ org.junit.jupiter junit-jupiter-engine - 5.11.2 + 5.11.3 test From 66be7a95299e5945f54c75a96c784f4d02d7d072 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2024 15:07:36 +0000 Subject: [PATCH 125/186] Update dependency org.junit.vintage:junit-vintage-engine to v5.11.3 --- .../src/test/resources/calculator_groovy/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teamscale-gradle-plugin/src/test/resources/calculator_groovy/build.gradle b/teamscale-gradle-plugin/src/test/resources/calculator_groovy/build.gradle index a13263c4f..25f99a780 100644 --- a/teamscale-gradle-plugin/src/test/resources/calculator_groovy/build.gradle +++ b/teamscale-gradle-plugin/src/test/resources/calculator_groovy/build.gradle @@ -14,7 +14,7 @@ plugins { import com.teamscale.TestImpacted ext.junit4Version = '4.13.2' -ext.junitVintageVersion = '5.11.2' +ext.junitVintageVersion = '5.11.3' ext.junitPlatformVersion = '1.4.0' ext.junitJupiterVersion = '5.11.2' From 4c1d0831b27fec6b3a1966f4ef2e7323575c87a8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2024 18:28:57 +0000 Subject: [PATCH 126/186] Update junit to v5.11.3 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f785be12f..d784c42dc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ jacoco = "0.8.12" # We need to stay on the 1.3.x release line as 1.4.x requires Java 11 logback = "1.3.14" retrofit = "2.11.0" -junit = "5.11.2" +junit = "5.11.3" junitPlatform = "1.11.2" okhttp = "4.12.0" mockito = "4.11.0" From 1311c558d90b55ee37f3a99fa0d4c910559d4293 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2024 18:29:02 +0000 Subject: [PATCH 127/186] Update junitJupiterVersion to v5.11.3 --- .../src/test/resources/calculator_groovy/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teamscale-gradle-plugin/src/test/resources/calculator_groovy/build.gradle b/teamscale-gradle-plugin/src/test/resources/calculator_groovy/build.gradle index 25f99a780..c6d42395f 100644 --- a/teamscale-gradle-plugin/src/test/resources/calculator_groovy/build.gradle +++ b/teamscale-gradle-plugin/src/test/resources/calculator_groovy/build.gradle @@ -16,7 +16,7 @@ import com.teamscale.TestImpacted ext.junit4Version = '4.13.2' ext.junitVintageVersion = '5.11.3' ext.junitPlatformVersion = '1.4.0' -ext.junitJupiterVersion = '5.11.2' +ext.junitJupiterVersion = '5.11.3' if (!project.hasProperty("withoutServerConfig")) { teamscale { From db9f4af33e0a05059b8aa766ae7a5146995b529f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2024 19:43:43 +0000 Subject: [PATCH 128/186] Update junitPlatform to v1.11.3 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d784c42dc..edb3881b3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ jacoco = "0.8.12" logback = "1.3.14" retrofit = "2.11.0" junit = "5.11.3" -junitPlatform = "1.11.2" +junitPlatform = "1.11.3" okhttp = "4.12.0" mockito = "4.11.0" picocli = "4.7.6" From adc7e6cf6d402d72802f54fcf38e6d7123642f0b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 24 Oct 2024 13:38:45 +0000 Subject: [PATCH 129/186] Update dependency org.springframework.boot:spring-boot-loader to v3.3.5 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index edb3881b3..616d81a4a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -76,7 +76,7 @@ jsonassert = { module = "org.skyscreamer:jsonassert", version = "1.5.3" } mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" } mockito-junit = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" } -springboot-loader = { module = "org.springframework.boot:spring-boot-loader", version = "3.3.4" } +springboot-loader = { module = "org.springframework.boot:spring-boot-loader", version = "3.3.5" } [plugins] versions = { id = "com.github.ben-manes.versions", version = "0.51.0" } From e3bccf91babef7ff716828587d824531b7b22a14 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 25 Oct 2024 16:13:01 +0000 Subject: [PATCH 130/186] Update dependency org.apache.maven.plugin-tools:maven-plugin-annotations to v3.15.1 --- teamscale-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teamscale-maven-plugin/pom.xml b/teamscale-maven-plugin/pom.xml index d1755c968..c34c2b75a 100644 --- a/teamscale-maven-plugin/pom.xml +++ b/teamscale-maven-plugin/pom.xml @@ -82,7 +82,7 @@ org.apache.maven.plugin-tools maven-plugin-annotations - 3.15.0 + 3.15.1 provided From e77642c5a217576dc1e3517f74829a9bec2a3a4f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 25 Oct 2024 17:06:30 +0000 Subject: [PATCH 131/186] Update dependency org.apache.maven.plugins:maven-plugin-plugin to v3.15.1 --- teamscale-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teamscale-maven-plugin/pom.xml b/teamscale-maven-plugin/pom.xml index c34c2b75a..5ea71f7e6 100644 --- a/teamscale-maven-plugin/pom.xml +++ b/teamscale-maven-plugin/pom.xml @@ -127,7 +127,7 @@ org.apache.maven.plugins maven-plugin-plugin - 3.15.0 + 3.15.1 generate-helpmojo From 84909971d1ac8eb3c18d488f2251a3f81ddffc5b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 29 Oct 2024 02:01:59 +0000 Subject: [PATCH 132/186] Update jackson to v2.18.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 616d81a4a..1b1af657e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] jetty = "9.4.56.v20240826" jersey = "2.45" -jackson = "2.18.0" +jackson = "2.18.1" # When upgrading JaCoCo to a newer version make sure to # check the comment in the OpenAnalyzer.java, JaCoCoPreMain.java and CachingInstructionsBuilder.java # and update the internal_xxxxxx hash included in the imports in LenientCoverageTransformer.java and JaCoCoPreMain.java. From 3f32260983547d93764e45f80b5267028aad8e56 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 29 Oct 2024 04:32:13 +0000 Subject: [PATCH 133/186] Update dependency com.gradleup.shadow:shadow-gradle-plugin to v8.3.4 --- buildSrc/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 2e0b48a2a..539304beb 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -11,7 +11,7 @@ repositories { } dependencies { - implementation("com.gradleup.shadow:shadow-gradle-plugin:8.3.3") + implementation("com.gradleup.shadow:shadow-gradle-plugin:8.3.4") implementation("com.xpdustry.ksr:com.xpdustry.ksr.gradle.plugin:1.0.0") { exclude(group = "com.github.johnrengelman") } From 411c92337e5caf60524354be44ecbd99fc4391b9 Mon Sep 17 00:00:00 2001 From: Constructor Date: Thu, 31 Oct 2024 00:48:10 +0100 Subject: [PATCH 134/186] TS-38628 Agent generator raw migration --- .../teamscale/jacoco/agent/DelayedLogger.java | 1 + .../CoverageToJsonStrategyBase.java | 7 +- .../TestEventHandlerStrategyBase.java | 2 +- .../test/commons/SystemTestUtils.java | 2 +- ...verageCollectingExecutionListenerTest.java | 6 +- report-generator/build.gradle.kts | 1 + .../report/EDuplicateClassFileBehavior.java | 15 - .../com/teamscale/report/ReportUtils.java | 83 ----- .../teamscale/report/jacoco/CoverageFile.java | 133 -------- .../report/jacoco/EmptyReportException.java | 11 - .../report/jacoco/FilteringAnalyzer.java | 97 ------ .../jacoco/JaCoCoXmlReportGenerator.java | 119 ------- .../teamscale/report/jacoco/OpenAnalyzer.java | 315 ------------------ .../jacoco/TeamscaleCoverageBuilder.java | 76 ----- .../teamscale/report/jacoco/dump/Dump.java | 26 -- .../report/testwise/ETestArtifactFormat.java | 32 -- .../TestwiseCoverageReportWriter.java | 99 ------ .../jacoco/CachingExecutionDataReader.java | 134 -------- .../jacoco/JaCoCoTestwiseReportGenerator.java | 142 -------- .../testwise/jacoco/cache/AnalyzerCache.java | 82 ----- .../jacoco/cache/ClassCoverageLookup.java | 132 -------- .../jacoco/cache/ClassNotFoundLogger.java | 53 --- .../cache/CoverageGenerationException.java | 13 - .../testwise/jacoco/cache/ProbesCache.java | 113 ------- .../report/testwise/model/ERevisionType.java | 11 - .../report/testwise/model/FileCoverage.java | 20 -- .../report/testwise/model/LineRange.java | 44 --- .../report/testwise/model/PathCoverage.java | 30 -- .../report/testwise/model/RevisionInfo.java | 59 ---- .../report/testwise/model/TestInfo.java | 55 --- .../testwise/model/TestwiseCoverage.java | 46 --- .../model/TestwiseCoverageReport.java | 26 -- .../model/builder/FileCoverageBuilder.java | 111 ------ .../model/builder/PathCoverageBuilder.java | 58 ---- .../model/builder/TestCoverageBuilder.java | 67 ---- .../model/builder/TestInfoBuilder.java | 87 ----- .../TestwiseCoverageReportBuilder.java | 88 ----- .../model/factory/TestInfoFactory.java | 100 ------ .../report/util/AntPatternIncludeFilter.java | 57 ---- .../util/BashFileSkippingInputStream.java | 47 --- .../util/ClasspathWildcardIncludeFilter.java | 71 ---- .../report/util/CommandLineLogger.java | 41 --- .../com/teamscale/report/util/ILogger.java | 32 -- .../teamscale/report/util/SortedIntList.java | 80 ----- .../analysis/CachingClassAnalyzer.java | 57 ---- .../analysis/CachingInstructionsBuilder.java | 234 ------------- .../report/EDuplicateClassFileBehavior.kt | 15 + .../com/teamscale/report/ReportUtils.kt | 96 ++++++ .../teamscale/report/jacoco/CoverageFile.kt | 121 +++++++ .../report/jacoco/EmptyReportException.kt | 6 + .../report/jacoco/FilteringAnalyzer.kt | 88 +++++ .../report/jacoco/JaCoCoXmlReportGenerator.kt | 116 +++++++ .../teamscale/report/jacoco/OpenAnalyzer.kt | 309 +++++++++++++++++ .../report/jacoco/TeamscaleCoverageBuilder.kt | 68 ++++ .../com/teamscale/report/jacoco/dump/Dump.kt | 15 + .../report/testwise/ETestArtifactFormat.kt | 23 ++ .../testwise/TestwiseCoverageReportWriter.kt | 93 ++++++ .../jacoco/CachingExecutionDataReader.kt | 130 ++++++++ .../jacoco/JaCoCoTestwiseReportGenerator.kt | 113 +++++++ .../testwise/jacoco/cache/AnalyzerCache.kt | 75 +++++ .../jacoco/cache/ClassCoverageLookup.kt | 136 ++++++++ .../jacoco/cache/ClassNotFoundLogger.kt | 46 +++ .../cache/CoverageGenerationException.kt | 8 + .../testwise/jacoco/cache/ProbesCache.kt | 107 ++++++ .../report/testwise/model/ERevisionType.kt | 10 + .../testwise/model/ETestExecutionResult.kt} | 17 +- .../report/testwise/model/FileCoverage.kt | 12 + .../report/testwise/model/LineRange.kt | 31 ++ .../report/testwise/model/PathCoverage.kt | 12 + .../report/testwise/model/RevisionInfo.kt | 51 +++ .../report/testwise/model/TestExecution.kt} | 135 ++++---- .../report/testwise/model/TestInfo.kt | 33 ++ .../report/testwise/model/TestwiseCoverage.kt | 37 ++ .../testwise/model/TestwiseCoverageReport.kt | 18 + .../model/builder/FileCoverageBuilder.kt | 93 ++++++ .../model/builder/PathCoverageBuilder.kt | 46 +++ .../model/builder/TestCoverageBuilder.kt | 54 +++ .../testwise/model/builder/TestInfoBuilder.kt | 81 +++++ .../builder/TestwiseCoverageReportBuilder.kt | 73 ++++ .../testwise/model/factory/TestInfoFactory.kt | 93 ++++++ .../report/util/AntPatternIncludeFilter.kt | 53 +++ .../util/BashFileSkippingInputStream.kt | 46 +++ .../util/ClasspathWildcardIncludeFilter.kt | 77 +++++ .../report/util/CommandLineLogger.kt | 30 ++ .../com/teamscale/report/util/ILogger.kt | 27 ++ .../teamscale/report/util/SortedIntList.kt | 79 +++++ .../internal/analysis/CachingClassAnalyzer.kt | 53 +++ .../analysis/CachingInstructionsBuilder.kt | 210 ++++++++++++ .../JaCoCoTestwiseReportGeneratorTest.java | 10 +- .../teamscale/TestwiseCoverageReportTask.kt | 14 +- 90 files changed, 2872 insertions(+), 3203 deletions(-) delete mode 100644 report-generator/src/main/java/com/teamscale/report/EDuplicateClassFileBehavior.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/ReportUtils.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/jacoco/CoverageFile.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/jacoco/EmptyReportException.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/jacoco/FilteringAnalyzer.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/jacoco/JaCoCoXmlReportGenerator.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/jacoco/OpenAnalyzer.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/jacoco/TeamscaleCoverageBuilder.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/jacoco/dump/Dump.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/testwise/ETestArtifactFormat.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/testwise/TestwiseCoverageReportWriter.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/testwise/jacoco/CachingExecutionDataReader.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/testwise/jacoco/JaCoCoTestwiseReportGenerator.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/testwise/jacoco/cache/AnalyzerCache.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/testwise/jacoco/cache/ClassCoverageLookup.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/testwise/jacoco/cache/ClassNotFoundLogger.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/testwise/jacoco/cache/CoverageGenerationException.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/testwise/jacoco/cache/ProbesCache.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/testwise/model/ERevisionType.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/testwise/model/FileCoverage.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/testwise/model/LineRange.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/testwise/model/PathCoverage.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/testwise/model/RevisionInfo.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/testwise/model/TestInfo.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/testwise/model/TestwiseCoverage.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/testwise/model/TestwiseCoverageReport.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/testwise/model/builder/FileCoverageBuilder.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/testwise/model/builder/PathCoverageBuilder.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/testwise/model/builder/TestCoverageBuilder.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/testwise/model/builder/TestInfoBuilder.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/testwise/model/builder/TestwiseCoverageReportBuilder.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/testwise/model/factory/TestInfoFactory.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/util/AntPatternIncludeFilter.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/util/BashFileSkippingInputStream.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/util/ClasspathWildcardIncludeFilter.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/util/CommandLineLogger.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/util/ILogger.java delete mode 100644 report-generator/src/main/java/com/teamscale/report/util/SortedIntList.java delete mode 100644 report-generator/src/main/java/org/jacoco/core/internal/analysis/CachingClassAnalyzer.java delete mode 100644 report-generator/src/main/java/org/jacoco/core/internal/analysis/CachingInstructionsBuilder.java create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/EDuplicateClassFileBehavior.kt create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/ReportUtils.kt create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/jacoco/CoverageFile.kt create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/jacoco/EmptyReportException.kt create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/jacoco/FilteringAnalyzer.kt create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/jacoco/JaCoCoXmlReportGenerator.kt create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/jacoco/OpenAnalyzer.kt create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/jacoco/TeamscaleCoverageBuilder.kt create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/jacoco/dump/Dump.kt create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/testwise/ETestArtifactFormat.kt create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/testwise/TestwiseCoverageReportWriter.kt create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/CachingExecutionDataReader.kt create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/JaCoCoTestwiseReportGenerator.kt create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/AnalyzerCache.kt create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/ClassCoverageLookup.kt create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/ClassNotFoundLogger.kt create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/CoverageGenerationException.kt create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/ProbesCache.kt create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/testwise/model/ERevisionType.kt rename report-generator/src/main/{java/com/teamscale/report/testwise/model/ETestExecutionResult.java => kotlin/com/teamscale/report/testwise/model/ETestExecutionResult.kt} (82%) create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/testwise/model/FileCoverage.kt create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/testwise/model/LineRange.kt create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/testwise/model/PathCoverage.kt create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/testwise/model/RevisionInfo.kt rename report-generator/src/main/{java/com/teamscale/report/testwise/model/TestExecution.java => kotlin/com/teamscale/report/testwise/model/TestExecution.kt} (51%) create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/testwise/model/TestInfo.kt create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/testwise/model/TestwiseCoverage.kt create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/testwise/model/TestwiseCoverageReport.kt create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/FileCoverageBuilder.kt create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/PathCoverageBuilder.kt create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/TestCoverageBuilder.kt create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/TestInfoBuilder.kt create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/TestwiseCoverageReportBuilder.kt create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/testwise/model/factory/TestInfoFactory.kt create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/util/AntPatternIncludeFilter.kt create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/util/BashFileSkippingInputStream.kt create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/util/ClasspathWildcardIncludeFilter.kt create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/util/CommandLineLogger.kt create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/util/ILogger.kt create mode 100644 report-generator/src/main/kotlin/com/teamscale/report/util/SortedIntList.kt create mode 100644 report-generator/src/main/kotlin/org/jacoco/core/internal/analysis/CachingClassAnalyzer.kt create mode 100644 report-generator/src/main/kotlin/org/jacoco/core/internal/analysis/CachingInstructionsBuilder.kt diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/DelayedLogger.java b/agent/src/main/java/com/teamscale/jacoco/agent/DelayedLogger.java index 574389c58..4be356c4c 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/DelayedLogger.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/DelayedLogger.java @@ -1,6 +1,7 @@ package com.teamscale.jacoco.agent; import com.teamscale.report.util.ILogger; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import java.util.ArrayList; diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/testimpact/CoverageToJsonStrategyBase.java b/agent/src/main/java/com/teamscale/jacoco/agent/testimpact/CoverageToJsonStrategyBase.java index 96280f102..e0944a7c3 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/testimpact/CoverageToJsonStrategyBase.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/testimpact/CoverageToJsonStrategyBase.java @@ -121,7 +121,7 @@ private String createTestwiseCoverageReport(boolean partial) throws IOException, if (execution == null) { return null; } else { - return execution.getUniformPath(); + return execution.uniformPath; } }).collect(toList()); @@ -132,10 +132,9 @@ private String createTestwiseCoverageReport(boolean partial) throws IOException, reportGenerator.updateClassDirCache(); TestwiseCoverage testwiseCoverage = reportGenerator.convert(testExecFile); logger.debug("Created testwise coverage report (containing coverage for tests `{}`)", - testwiseCoverage.getTests().stream().map(TestCoverageBuilder::getUniformPath).collect(toList())); + testwiseCoverage.getTests().values().stream().map(test -> test.uniformPath).collect(toList())); - TestwiseCoverageReport report = TestwiseCoverageReportBuilder - .createFrom(availableTests, testwiseCoverage.getTests(), testExecutions, partial); + TestwiseCoverageReport report = TestwiseCoverageReportBuilder.createFrom(availableTests, testwiseCoverage.getTests().values(), testExecutions, partial); testExecFile.delete(); testExecFile = null; diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/testimpact/TestEventHandlerStrategyBase.java b/agent/src/main/java/com/teamscale/jacoco/agent/testimpact/TestEventHandlerStrategyBase.java index 840f5f7d6..cb8bad766 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/testimpact/TestEventHandlerStrategyBase.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/testimpact/TestEventHandlerStrategyBase.java @@ -65,7 +65,7 @@ public void testStart(String test) { public TestInfo testEnd(String test, TestExecution testExecution) throws JacocoRuntimeController.DumpException, CoverageGenerationException { if (testExecution != null) { - testExecution.setUniformPath(test); + testExecution.uniformPath = test; if (startTimestamp != -1) { long endTimestamp = System.currentTimeMillis(); testExecution.setDurationMillis(endTimestamp - startTimestamp); diff --git a/common-system-test/src/main/java/com/teamscale/test/commons/SystemTestUtils.java b/common-system-test/src/main/java/com/teamscale/test/commons/SystemTestUtils.java index a83f2316a..eebc26d52 100644 --- a/common-system-test/src/main/java/com/teamscale/test/commons/SystemTestUtils.java +++ b/common-system-test/src/main/java/com/teamscale/test/commons/SystemTestUtils.java @@ -49,7 +49,7 @@ public class SystemTestUtils { * Example: {@code file1.java:1,7-12;file2.java:9-22,33} */ public static String getCoverageString(TestInfo info) { - return info.paths.stream().flatMap(path -> path.getFiles().stream()) + return info.paths.stream().flatMap(path -> path.files.stream()) .map(file -> file.fileName + ":" + file.coveredLines).collect( Collectors.joining(";")); } diff --git a/impacted-test-engine/src/test/java/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListenerTest.java b/impacted-test-engine/src/test/java/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListenerTest.java index 9e4f78c63..e03b4a784 100644 --- a/impacted-test-engine/src/test/java/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListenerTest.java +++ b/impacted-test-engine/src/test/java/com/teamscale/test_impacted/engine/executor/TestwiseCoverageCollectingExecutionListenerTest.java @@ -85,9 +85,9 @@ void testInteractionWithListenersAndCoverageApi() { assertThat(testExecutions).hasSize(2); assertThat(testExecutions).anySatisfy(testExecution -> - assertThat(testExecution.getUniformPath()).isEqualTo("MyClass/impactedTestCase()")); + assertThat(testExecution.uniformPath).isEqualTo("MyClass/impactedTestCase()")); assertThat(testExecutions).anySatisfy(testExecution -> - assertThat(testExecution.getUniformPath()).isEqualTo("MyClass/regularSkippedTestCase()")); + assertThat(testExecution.uniformPath).isEqualTo("MyClass/regularSkippedTestCase()")); } @Test @@ -125,6 +125,6 @@ void testSkipOfTestClass() { assertThat(testExecutions).hasSize(2); assertThat(testExecutions) - .allMatch(testExecution -> testExecution.getResult().equals(ETestExecutionResult.SKIPPED)); + .allMatch(testExecution -> testExecution.result.equals(ETestExecutionResult.SKIPPED)); } } \ No newline at end of file diff --git a/report-generator/build.gradle.kts b/report-generator/build.gradle.kts index 882e8782b..e9d3e4b12 100644 --- a/report-generator/build.gradle.kts +++ b/report-generator/build.gradle.kts @@ -3,6 +3,7 @@ plugins { com.teamscale.`java-convention` com.teamscale.coverage com.teamscale.publish + kotlin("jvm") } publishAs { diff --git a/report-generator/src/main/java/com/teamscale/report/EDuplicateClassFileBehavior.java b/report-generator/src/main/java/com/teamscale/report/EDuplicateClassFileBehavior.java deleted file mode 100644 index 9fb811ca8..000000000 --- a/report-generator/src/main/java/com/teamscale/report/EDuplicateClassFileBehavior.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.teamscale.report; - -/** - * Behavior when two non-identical class files with the same package name are found. - */ -public enum EDuplicateClassFileBehavior { - /** Completely ignores it. */ - IGNORE, - - /** Prints a warning to the logger. */ - WARN, - - /** Fails and stops further processing. */ - FAIL -} diff --git a/report-generator/src/main/java/com/teamscale/report/ReportUtils.java b/report-generator/src/main/java/com/teamscale/report/ReportUtils.java deleted file mode 100644 index c5513dd6f..000000000 --- a/report-generator/src/main/java/com/teamscale/report/ReportUtils.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.teamscale.report; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.teamscale.client.FileSystemUtils; -import com.teamscale.client.JsonUtils; -import com.teamscale.client.TestDetails; -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.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -/** Utilities for generating reports. */ -public class ReportUtils { - - /** Converts to given test list to a json report and writes it to the given file. */ - public static void writeTestListReport(File reportFile, List report) throws IOException { - writeReportToFile(reportFile, report); - } - - /** Converts to given test execution report to a json report and writes it to the given file. */ - public static void writeTestExecutionReport(File reportFile, List report) throws IOException { - writeReportToFile(reportFile, report); - } - - /** Converts to given testwise coverage report to a json report and writes it to the given file. */ - public static void writeTestwiseCoverageReport(File reportFile, TestwiseCoverageReport report) throws IOException { - writeReportToFile(reportFile, report); - } - - /** Converts to given report to a json string. For testing only. */ - public static String getTestwiseCoverageReportAsString( - TestwiseCoverageReport report) throws JsonProcessingException { - return JsonUtils.serialize(report); - } - - /** Writes the report object to the given file as json. */ - private static void writeReportToFile(File reportFile, T report) throws IOException { - File directory = reportFile.getParentFile(); - if (!directory.isDirectory() && !directory.mkdirs()) { - throw new IOException("Failed to create directory " + directory.getAbsolutePath()); - } - JsonUtils.serializeToFile(reportFile, report); - } - - /** Recursively lists all files in the given directory that match the specified extension. */ - public static List readObjects(ETestArtifactFormat format, Class clazz, - List directoriesOrFiles) throws IOException { - List files = listFiles(format, directoriesOrFiles); - ArrayList result = new ArrayList<>(); - for (File file : files) { - T[] t = JsonUtils.deserializeFile(file, clazz); - if (t != null) { - result.addAll(Arrays.asList(t)); - } - } - return result; - } - - /** Recursively lists all files of the given artifact type. */ - public static List listFiles(ETestArtifactFormat format, List directoriesOrFiles) { - List filesWithSpecifiedArtifactType = new ArrayList<>(); - for (File directoryOrFile : directoriesOrFiles) { - if (directoryOrFile.isDirectory()) { - filesWithSpecifiedArtifactType.addAll(FileSystemUtils - .listFilesRecursively(directoryOrFile, file -> fileIsOfArtifactFormat(file, format))); - } else if (fileIsOfArtifactFormat(directoryOrFile, format)) { - filesWithSpecifiedArtifactType.add(directoryOrFile); - } - } - return filesWithSpecifiedArtifactType; - } - - private static boolean fileIsOfArtifactFormat(File file, ETestArtifactFormat format) { - return file.isFile() && - file.getName().startsWith(format.filePrefix) && - FileSystemUtils.getFileExtension(file).equalsIgnoreCase(format.extension); - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/jacoco/CoverageFile.java b/report-generator/src/main/java/com/teamscale/report/jacoco/CoverageFile.java deleted file mode 100644 index c121a013d..000000000 --- a/report-generator/src/main/java/com/teamscale/report/jacoco/CoverageFile.java +++ /dev/null @@ -1,133 +0,0 @@ -package com.teamscale.report.jacoco; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.file.Files; -import java.util.Objects; - -import com.teamscale.client.FileSystemUtils; - -import okhttp3.MultipartBody; -import okhttp3.RequestBody; - -/** - * Represents a coverage file on disk. The main purpose is to avoid reading the - * entire file into memory as this dramatically increases the memory footprint - * of the JVM which might run out of memory because of this. - * - * The object internally holds a counter of how many references to the file are - * currently held. This allows to share the same file for multiple uploads and - * deleting it once all uploads have succeeded. Use {@link #acquireReference()} - * to make the object aware that it was passed to another uploader and - * {@link #delete()} to signal that you no longer intend to access the file. - */ -public class CoverageFile { - - private final File coverageFile; - private int referenceCounter = 0; - - public CoverageFile(File coverageFile) { - this.coverageFile = coverageFile; - } - - /** - * Marks the file as being used by an additional uploader. This ensures that the - * file is not deleted until all users have signed via {@link #delete()} that - * they no longer intend to access the file. - */ - public CoverageFile acquireReference() { - referenceCounter++; - return this; - } - - /** - * Copies the coverage File in blocks from the disk to the output stream to - * avoid having to read the entire file into memory. - */ - public void copy(OutputStream outputStream) throws IOException { - FileInputStream inputStream = new FileInputStream(coverageFile); - FileSystemUtils.copy(inputStream, outputStream); - inputStream.close(); - } - - /** - * Get the filename of the coverage file on disk without its extension - */ - public String getNameWithoutExtension() { - return FileSystemUtils.getFilenameWithoutExtension(coverageFile); - } - - /** Get the filename of the coverage file. */ - public String getName() { - return coverageFile.getName(); - } - - /** - * Delete the coverage file from disk - */ - public void delete() throws IOException { - referenceCounter--; - if (referenceCounter <= 0) { - Files.delete(coverageFile.toPath()); - } - } - - /** - * Create a {@link okhttp3.MultipartBody} form body with the contents of the - * coverage file. - */ - public RequestBody createFormRequestBody() { - return RequestBody.create(MultipartBody.FORM, new File(coverageFile.getAbsolutePath())); - } - - /** - * Get the {@link java.io.OutputStream} in order to write to the coverage file. - * - * @throws IOException - * If the file did not exist yet and could not be created - */ - public OutputStream getOutputStream() throws IOException { - try { - return new FileOutputStream(coverageFile); - } catch (IOException e) { - throw new IOException("Could not create temporary coverage file" + this + ". " - + "This is used to cache the coverage file on disk before uploading it to its final destination. " - + "This coverage is lost. Please fix the underlying issue to avoid losing coverage.", e); - } - - } - - /** - * {@inheritDoc} - */ - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - CoverageFile that = (CoverageFile) o; - return coverageFile.equals(that.coverageFile); - } - - /** - * {@inheritDoc} - */ - @Override - public int hashCode() { - return Objects.hash(coverageFile); - } - - /** - * {@inheritDoc} - */ - @Override - public String toString() { - return coverageFile.getAbsolutePath(); - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/jacoco/EmptyReportException.java b/report-generator/src/main/java/com/teamscale/report/jacoco/EmptyReportException.java deleted file mode 100644 index 80f873def..000000000 --- a/report-generator/src/main/java/com/teamscale/report/jacoco/EmptyReportException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.teamscale.report.jacoco; - -/** - * Exception indicating that the generated report was empty and no {@link CoverageFile} was written to disk. - */ -public class EmptyReportException extends Exception { - - public EmptyReportException(String message) { - super(message); - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/jacoco/FilteringAnalyzer.java b/report-generator/src/main/java/com/teamscale/report/jacoco/FilteringAnalyzer.java deleted file mode 100644 index ab6607b58..000000000 --- a/report-generator/src/main/java/com/teamscale/report/jacoco/FilteringAnalyzer.java +++ /dev/null @@ -1,97 +0,0 @@ -/*-------------------------------------------------------------------------+ -| | -| Copyright (c) 2009-2018 CQSE GmbH | -| | -+-------------------------------------------------------------------------*/ -package com.teamscale.report.jacoco; - -import com.teamscale.report.util.BashFileSkippingInputStream; -import com.teamscale.report.util.ClasspathWildcardIncludeFilter; -import com.teamscale.report.util.ILogger; -import org.jacoco.core.analysis.Analyzer; -import org.jacoco.core.analysis.ICoverageVisitor; -import org.jacoco.core.data.ExecutionDataStore; - -import java.io.IOException; -import java.io.InputStream; -import java.util.function.Predicate; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; - -/** - * {@link Analyzer} that filters the analyzed class files based on a {@link Predicate}. - */ -/* package */ public class FilteringAnalyzer extends OpenAnalyzer { - - /** The filter for the analyzed class files. */ - private final ClasspathWildcardIncludeFilter locationIncludeFilter; - private final ILogger logger; - - public FilteringAnalyzer(ExecutionDataStore executionData, ICoverageVisitor coverageVisitor, - ClasspathWildcardIncludeFilter locationIncludeFilter, ILogger logger) { - super(executionData, coverageVisitor); - this.locationIncludeFilter = locationIncludeFilter; - this.logger = logger; - } - - /** {@inheritDoc} */ - @Override - public int analyzeAll(InputStream input, String location) throws IOException { - if (location.endsWith(".class") && !locationIncludeFilter.isIncluded(location)) { - logger.debug("Excluding class file " + location); - return 1; - } - if (location.endsWith(".jar")) { - return analyzeJar(input, location); - } - return super.analyzeAll(input, location); - } - - @Override - public void analyzeClass(final byte[] buffer, final String location) - throws IOException { - try { - analyzeClass(buffer); - } catch (final RuntimeException cause) { - if (isUnsupportedClassFile(cause)) { - logger.error(cause.getMessage() + " in " + location); - } else { - throw analyzerError(location, cause); - } - } - } - - /** - * Checks if the error indicates that the class file might be newer than what is currently supported by - * JaCoCo. The concrete error message seems to depend on the used JVM, so we only check for "Unsupported" which seems - * to be common amongst all of them. - */ - private boolean isUnsupportedClassFile(RuntimeException cause) { - return cause instanceof IllegalArgumentException && cause.getMessage() - .startsWith("Unsupported"); - } - - /** - * Copied from Analyzer.analyzeZip renamed to analyzeJar and added wrapping BashFileSkippingInputStream. - */ - protected int analyzeJar(final InputStream input, final String location) - throws IOException { - ZipInputStream zip = new ZipInputStream(new BashFileSkippingInputStream(input)); - ZipEntry entry; - int count = 0; - while ((entry = nextEntry(zip, location)) != null) { - count += analyzeAll(zip, location + "@" + entry.getName()); - } - return count; - } - - /** Copied from Analyzer.nextEntry. */ - private ZipEntry nextEntry(final ZipInputStream input, - final String location) throws IOException { - try { - return input.getNextEntry(); - } catch (final IOException e) { - throw analyzerError(location, e); - } - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/jacoco/JaCoCoXmlReportGenerator.java b/report-generator/src/main/java/com/teamscale/report/jacoco/JaCoCoXmlReportGenerator.java deleted file mode 100644 index 73ca6abb3..000000000 --- a/report-generator/src/main/java/com/teamscale/report/jacoco/JaCoCoXmlReportGenerator.java +++ /dev/null @@ -1,119 +0,0 @@ -package com.teamscale.report.jacoco; - -import com.teamscale.report.EDuplicateClassFileBehavior; -import com.teamscale.report.jacoco.dump.Dump; -import com.teamscale.report.util.ClasspathWildcardIncludeFilter; -import com.teamscale.report.util.ILogger; -import org.jacoco.core.analysis.CoverageBuilder; -import org.jacoco.core.analysis.IBundleCoverage; -import org.jacoco.core.data.ExecutionDataStore; -import org.jacoco.core.data.SessionInfo; -import org.jacoco.report.IReportVisitor; -import org.jacoco.report.xml.XMLFormatter; - -import java.io.File; -import java.io.IOException; -import java.io.OutputStream; -import java.util.Collections; -import java.util.List; - -/** Creates an XML report from binary execution data. */ -public class JaCoCoXmlReportGenerator { - - /** The logger. */ - private final ILogger logger; - - /** Directories and zip files that contain class files. */ - private final List codeDirectoriesOrArchives; - - /** - * Include filter to apply to all locations during class file traversal. - */ - private final ClasspathWildcardIncludeFilter locationIncludeFilter; - - /** Whether to ignore non-identical duplicates of class files. */ - private final EDuplicateClassFileBehavior duplicateClassFileBehavior; - - /** Whether to remove uncovered classes from the report. */ - private final boolean ignoreUncoveredClasses; - - /** Part of the error message logged when validating the coverage report fails. */ - private static final String MOST_LIKELY_CAUSE_MESSAGE = "Most likely you did not configure the agent correctly." + - " Please check that the includes and excludes options are set correctly so the relevant code is included." + - " If in doubt, first include more code and then iteratively narrow the patterns down to just the relevant code." + - " If you have specified the class-dir option, please make sure it points to a directory containing the" + - " class files/jars/wars/ears/etc. for which you are trying to measure code coverage."; - - public JaCoCoXmlReportGenerator(List codeDirectoriesOrArchives, - ClasspathWildcardIncludeFilter locationIncludeFilter, - EDuplicateClassFileBehavior duplicateClassFileBehavior, - boolean ignoreUncoveredClasses, ILogger logger) { - this.codeDirectoriesOrArchives = codeDirectoriesOrArchives; - this.duplicateClassFileBehavior = duplicateClassFileBehavior; - this.locationIncludeFilter = locationIncludeFilter; - this.ignoreUncoveredClasses = ignoreUncoveredClasses; - this.logger = logger; - } - - - /** - * Creates the report and writes it to a file. - * - * @return The file object of for the converted report or null if it could not be created - */ - public CoverageFile convert(Dump dump, File filePath) throws IOException, EmptyReportException { - CoverageFile coverageFile = new CoverageFile(filePath); - convertToReport(coverageFile, dump); - return coverageFile; - } - - /** Creates the report. */ - private void convertToReport(CoverageFile coverageFile, Dump dump) throws IOException, EmptyReportException { - ExecutionDataStore mergedStore = dump.store; - IBundleCoverage bundleCoverage = analyzeStructureAndAnnotateCoverage(mergedStore); - checkForEmptyReport(bundleCoverage); - try (OutputStream outputStream = coverageFile.getOutputStream()) { - createReport(outputStream, bundleCoverage, dump.info, mergedStore); - } - } - - private void checkForEmptyReport(IBundleCoverage coverage) throws EmptyReportException { - if (coverage.getPackages().size() == 0 || coverage.getLineCounter().getTotalCount() == 0) { - throw new EmptyReportException("The generated coverage report is empty. " + MOST_LIKELY_CAUSE_MESSAGE); - } - if (coverage.getLineCounter().getCoveredCount() == 0) { - throw new EmptyReportException( - "The generated coverage report does not contain any covered source code lines. " + - MOST_LIKELY_CAUSE_MESSAGE); - } - } - - /** Creates an XML report based on the given session and coverage data. */ - private static void createReport(OutputStream output, IBundleCoverage bundleCoverage, SessionInfo sessionInfo, - ExecutionDataStore store) throws IOException { - XMLFormatter xmlFormatter = new XMLFormatter(); - IReportVisitor visitor = xmlFormatter.createVisitor(output); - - visitor.visitInfo(Collections.singletonList(sessionInfo), store.getContents()); - visitor.visitBundle(bundleCoverage, null); - visitor.visitEnd(); - } - - /** - * Analyzes the structure of the class files in {@link #codeDirectoriesOrArchives} and builds an in-memory coverage - * report with the coverage in the given store. - */ - private IBundleCoverage analyzeStructureAndAnnotateCoverage(ExecutionDataStore store) throws IOException { - CoverageBuilder coverageBuilder = new TeamscaleCoverageBuilder(this.logger, - duplicateClassFileBehavior, ignoreUncoveredClasses); - - FilteringAnalyzer analyzer = new FilteringAnalyzer(store, coverageBuilder, locationIncludeFilter, logger); - - for (File file : codeDirectoriesOrArchives) { - analyzer.analyzeAll(file); - } - - return coverageBuilder.getBundle("dummybundle"); - } - -} 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 deleted file mode 100644 index 97556dca8..000000000 --- a/report-generator/src/main/java/com/teamscale/report/jacoco/OpenAnalyzer.java +++ /dev/null @@ -1,315 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2009, 2023 Mountainminds GmbH & Co. KG and Contributors - * This program and the accompanying materials are made available under - * the terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - * - * Contributors: - * Marc R. Hoffmann - initial API and implementation - * - *******************************************************************************/ - -package com.teamscale.report.jacoco; - -import org.jacoco.core.JaCoCo; -import org.jacoco.core.analysis.Analyzer; -import org.jacoco.core.analysis.ICoverageVisitor; -import org.jacoco.core.data.ExecutionData; -import org.jacoco.core.data.ExecutionDataStore; -import org.jacoco.core.internal.ContentTypeDetector; -import org.jacoco.core.internal.InputStreams; -import org.jacoco.core.internal.Pack200Streams; -import org.jacoco.core.internal.analysis.ClassAnalyzer; -import org.jacoco.core.internal.analysis.ClassCoverageImpl; -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 org.objectweb.asm.Opcodes; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.StringTokenizer; -import java.util.zip.GZIPInputStream; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; - -/** - * This is a copy of the {@link Analyzer} class from JaCoCo. - * The only changes are that the following methods are protected instead of private: - * - {@link #analyzeClass(byte[])} - * - {@link #analyzerError(String, Exception)} - *

    - * When performing an update of JaCoCo we need to check that this file is still up-to-date. - *

    - * An {@link Analyzer} instance processes a set of Java class files and - * calculates coverage data for them. For each class file the result is reported - * to a given {@link ICoverageVisitor} instance. In addition the - * {@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. - */ -public class OpenAnalyzer { - - private final ExecutionDataStore executionData; - - private final ICoverageVisitor coverageVisitor; - - private final StringPool stringPool; - - /** - * Creates a new analyzer reporting to the given output. - * - * @param executionData - * execution data - * @param coverageVisitor - * the output instance that will coverage data for every analyzed - * class - */ - public OpenAnalyzer(final ExecutionDataStore executionData, - final ICoverageVisitor coverageVisitor) { - this.executionData = executionData; - this.coverageVisitor = coverageVisitor; - this.stringPool = new StringPool(); - } - - /** - * Creates an ASM class visitor for analysis. - * - * @param classId - * id of the class calculated with {@link CRC64} - * @param className - * VM name of the class - * @return ASM visitor to write class definition to - */ - private ClassVisitor createAnalyzingVisitor(final long classId, - final String className) { - final ExecutionData data = executionData.get(classId); - final boolean[] probes; - final boolean noMatch; - if (data == null) { - probes = null; - noMatch = executionData.contains(className); - } else { - probes = data.getProbes(); - noMatch = false; - } - final ClassCoverageImpl coverage = new ClassCoverageImpl(className, - classId, noMatch); - final ClassAnalyzer analyzer = new ClassAnalyzer(coverage, probes, - stringPool) { - @Override - public void visitEnd() { - super.visitEnd(); - coverageVisitor.visitCoverage(coverage); - } - }; - return new ClassProbesAdapter(analyzer, false); - } - - /** Analyzes the given class in binary form. */ - protected void analyzeClass(final byte[] source) { - final long classId = CRC64.classId(source); - final ClassReader reader = InstrSupport.classReaderFor(source); - if ((reader.getAccess() & Opcodes.ACC_MODULE) != 0) { - return; - } - if ((reader.getAccess() & Opcodes.ACC_SYNTHETIC) != 0) { - return; - } - final ClassVisitor visitor = createAnalyzingVisitor(classId, - reader.getClassName()); - reader.accept(visitor, 0); - } - - /** - * Analyzes the class definition from a given in-memory buffer. - * - * @param buffer - * class definitions - * @param location - * a location description used for exception messages - * @throws IOException - * if the class can't be analyzed - */ - public void analyzeClass(final byte[] buffer, final String location) - throws IOException { - try { - analyzeClass(buffer); - } catch (final RuntimeException cause) { - throw analyzerError(location, cause); - } - } - - /** - * Analyzes the class definition from a given input stream. The provided - * {@link InputStream} is not closed by this method. - * - * @param input - * stream to read class definition from - * @param location - * a location description used for exception messages - * @throws IOException - * if the stream can't be read or the class can't be analyzed - */ - public void analyzeClass(final InputStream input, final String location) - throws IOException { - final byte[] buffer; - try { - buffer = InputStreams.readFully(input); - } catch (final IOException e) { - throw analyzerError(location, e); - } - analyzeClass(buffer, location); - } - - /** Creates an {@link IOException} which includes the affected file location and JaCoCo version. */ - protected IOException analyzerError(final String location, - final Exception cause) { - final IOException ex = new IOException( - String.format("Error while analyzing %s with JaCoCo %s/%s.", - location, JaCoCo.VERSION, JaCoCo.COMMITID_SHORT)); - ex.initCause(cause); - return ex; - } - - /** - * Analyzes all classes found in the given input stream. The input stream - * may either represent a single class file, a ZIP archive, a Pack200 - * archive or a gzip stream that is searched recursively for class files. - * All other content types are ignored. The provided {@link InputStream} is - * not closed by this method. - * - * @param input - * input data - * @param location - * a location description used for exception messages - * @return number of class files found - * @throws IOException - * if the stream can't be read or a class can't be analyzed - */ - public int analyzeAll(final InputStream input, final String location) - throws IOException { - final ContentTypeDetector detector; - try { - detector = new ContentTypeDetector(input); - } catch (final IOException e) { - throw analyzerError(location, e); - } - switch (detector.getType()) { - case ContentTypeDetector.CLASSFILE: - analyzeClass(detector.getInputStream(), location); - return 1; - case ContentTypeDetector.ZIPFILE: - return analyzeZip(detector.getInputStream(), location); - case ContentTypeDetector.GZFILE: - return analyzeGzip(detector.getInputStream(), location); - case ContentTypeDetector.PACK200FILE: - return analyzePack200(detector.getInputStream(), location); - default: - return 0; - } - } - - /** - * Analyzes all class files contained in the given file or folder. Class - * files as well as ZIP files are considered. Folders are searched - * recursively. - * - * @param file - * file or folder to look for class files - * @return number of class files found - * @throws IOException - * if the file can't be read or a class can't be analyzed - */ - public int analyzeAll(final File file) throws IOException { - int count = 0; - if (file.isDirectory()) { - for (final File f : file.listFiles()) { - count += analyzeAll(f); - } - } else { - final InputStream in = new FileInputStream(file); - try { - count += analyzeAll(in, file.getPath()); - } finally { - in.close(); - } - } - return count; - } - - /** - * Analyzes all classes from the given class path. Directories containing - * class files as well as archive files are considered. - * - * @param path - * path definition - * @param basedir - * optional base directory, if null the current - * working directory is used as the base for relative path - * entries - * @return number of class files found - * @throws IOException - * if a file can't be read or a class can't be analyzed - */ - public int analyzeAll(final String path, final File basedir) - throws IOException { - int count = 0; - final StringTokenizer st = new StringTokenizer(path, - File.pathSeparator); - while (st.hasMoreTokens()) { - count += analyzeAll(new File(basedir, st.nextToken())); - } - return count; - } - - private int analyzeZip(final InputStream input, final String location) - throws IOException { - final ZipInputStream zip = new ZipInputStream(input); - ZipEntry entry; - int count = 0; - while ((entry = nextEntry(zip, location)) != null) { - count += analyzeAll(zip, location + "@" + entry.getName()); - } - return count; - } - - private ZipEntry nextEntry(final ZipInputStream input, - final String location) throws IOException { - try { - return input.getNextEntry(); - } catch (final IOException e) { - throw analyzerError(location, e); - } - } - - private int analyzeGzip(final InputStream input, final String location) - throws IOException { - GZIPInputStream gzipInputStream; - try { - gzipInputStream = new GZIPInputStream(input); - } catch (final IOException e) { - throw analyzerError(location, e); - } - return analyzeAll(gzipInputStream, location); - } - - private int analyzePack200(final InputStream input, final String location) - throws IOException { - InputStream unpackedInput; - try { - unpackedInput = Pack200Streams.unpack(input); - } catch (final IOException e) { - throw analyzerError(location, e); - } - return analyzeAll(unpackedInput, location); - } - -} diff --git a/report-generator/src/main/java/com/teamscale/report/jacoco/TeamscaleCoverageBuilder.java b/report-generator/src/main/java/com/teamscale/report/jacoco/TeamscaleCoverageBuilder.java deleted file mode 100644 index 6cf802f5a..000000000 --- a/report-generator/src/main/java/com/teamscale/report/jacoco/TeamscaleCoverageBuilder.java +++ /dev/null @@ -1,76 +0,0 @@ -/*-------------------------------------------------------------------------+ -| | -| Copyright (c) 2009-2018 CQSE GmbH | -| | -+-------------------------------------------------------------------------*/ -package com.teamscale.report.jacoco; - -import com.teamscale.report.EDuplicateClassFileBehavior; -import com.teamscale.report.util.ILogger; -import org.jacoco.core.analysis.CoverageBuilder; -import org.jacoco.core.analysis.IBundleCoverage; -import org.jacoco.core.analysis.IClassCoverage; -import org.jacoco.core.analysis.ICounter; -import org.jacoco.core.internal.analysis.BundleCoverageImpl; - -import java.util.Collections; - -/** - * Modified {@link CoverageBuilder} can ignore non-identical duplicate classes or classes without coverage. In addition, - * coverage returned via {@link #getBundle(String)} will only return source file coverage because Teamscale does not - * need class coverage anyway. This reduces XML size by approximately half. - */ -/* package */class TeamscaleCoverageBuilder extends CoverageBuilder { - - /** The logger. */ - private final ILogger logger; - - /** How to behave if duplicate class files are encountered. */ - private final EDuplicateClassFileBehavior duplicateClassFileBehavior; - - /** Whether to ignore uncovered classes (i.e. leave them out of the report). */ - private final boolean ignoreUncoveredClasses; - - TeamscaleCoverageBuilder(ILogger logger, EDuplicateClassFileBehavior duplicateClassFileBehavior, - boolean removeUncoveredClasses) { - this.logger = logger; - this.duplicateClassFileBehavior = duplicateClassFileBehavior; - this.ignoreUncoveredClasses = removeUncoveredClasses; - } - - /** Just returns source file coverage, because Teamscale does not need class coverage. */ - @Override - public IBundleCoverage getBundle(final String name) { - return new BundleCoverageImpl(name, Collections.emptyList(), getSourceFiles()); - } - - /** {@inheritDoc} */ - @Override - public void visitCoverage(IClassCoverage coverage) { - if (ignoreUncoveredClasses && (coverage.getClassCounter().getStatus() & ICounter.FULLY_COVERED) == 0) { - return; - } - - try { - super.visitCoverage(coverage); - } catch (IllegalStateException e) { - switch (duplicateClassFileBehavior) { - case IGNORE: - return; - case WARN: - // we deliberately do not log the exception in this case as it does not provide any additional - // valuable information but confuses users into thinking there's a serious problem with the agent - // as they only see that there are stack traces in the log - logger.warn("Ignoring duplicate, non-identical class file for class " + coverage - .getName() + " compiled from source file " + coverage.getSourceFileName() + "." - + " 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; - default: - throw e; - } - } - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/jacoco/dump/Dump.java b/report-generator/src/main/java/com/teamscale/report/jacoco/dump/Dump.java deleted file mode 100644 index 09ad436f6..000000000 --- a/report-generator/src/main/java/com/teamscale/report/jacoco/dump/Dump.java +++ /dev/null @@ -1,26 +0,0 @@ -/*-------------------------------------------------------------------------+ -| | -| Copyright (c) 2009-2018 CQSE GmbH | -| | -+-------------------------------------------------------------------------*/ -package com.teamscale.report.jacoco.dump; - -import org.jacoco.core.data.ExecutionDataStore; -import org.jacoco.core.data.SessionInfo; - -/** All data received in one dump. */ -public class Dump { - - /** The session info. */ - public final SessionInfo info; - - /** The execution data store. */ - public final ExecutionDataStore store; - - /** Constructor. */ - public Dump(SessionInfo info, ExecutionDataStore store) { - this.info = info; - this.store = store; - } - -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/ETestArtifactFormat.java b/report-generator/src/main/java/com/teamscale/report/testwise/ETestArtifactFormat.java deleted file mode 100644 index 3ef64b3e2..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/ETestArtifactFormat.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.teamscale.report.testwise; - -/** Enum of test artifacts that can be converted to a full testwise coverage report later on. */ -public enum ETestArtifactFormat { - - /** A json list of tests ({@link com.teamscale.client.TestDetails}). */ - TEST_LIST("Test List", "test-list", "json"), - - /** A json list of test executions ({@link com.teamscale.report.testwise.model.TestExecution}). */ - TEST_EXECUTION("Test Execution", "test-execution", "json"), - - /** Binary jacoco test coverage (.exec file). */ - JACOCO("Jacoco", "", "exec"), - - /** Google closure coverage files with additional uniformPath entries. */ - CLOSURE("Closure Coverage", "closure-coverage", "json"); - - /** A readable name for the report type. */ - public final String readableName; - - /** Prefix to use when writing the report to the file system. */ - public final String filePrefix; - - /** File extension of the report. */ - public final String extension; - - ETestArtifactFormat(String readableName, String filePrefix, String extension) { - this.readableName = readableName; - this.filePrefix = filePrefix; - this.extension = extension; - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/TestwiseCoverageReportWriter.java b/report-generator/src/main/java/com/teamscale/report/testwise/TestwiseCoverageReportWriter.java deleted file mode 100644 index d43af754b..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/TestwiseCoverageReportWriter.java +++ /dev/null @@ -1,99 +0,0 @@ -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; - -/** - * Writes out a {@link com.teamscale.report.testwise.model.TestwiseCoverageReport} one {@link TestInfo} after the other - * so that we do not need to keep them all in memory during the conversion. - */ -public class TestwiseCoverageReportWriter implements Consumer, AutoCloseable { - - /** Factory for converting {@link TestCoverageBuilder} objects to {@link TestInfo}s. */ - private final TestInfoFactory testInfoFactory; - - private final File outputFile; - /** After how many written tests a new file should be started. */ - private final int splitAfter; - - /** Writer instance to where the {@link com.teamscale.report.testwise.model.TestwiseCoverageReport} is written to. */ - private JsonGenerator jsonGenerator; - - /** Number of tests written to the file. */ - private int testsWritten = 0; - - /** Number of test files that have been written. */ - private int testFileCounter = 0; - - public TestwiseCoverageReportWriter(TestInfoFactory testInfoFactory, File outputFile, - int splitAfter) throws IOException { - this.testInfoFactory = testInfoFactory; - this.outputFile = outputFile; - this.splitAfter = splitAfter; - - startReport(); - } - - @Override - public void accept(TestCoverageBuilder testCoverageBuilder) { - TestInfo testInfo = testInfoFactory.createFor(testCoverageBuilder); - try { - writeTestInfo(testInfo); - } catch (IOException e) { - // Need to be wrapped in RuntimeException as Consumer does not allow to throw a checked Exception - throw new RuntimeException("Writing test info to report failed.", e); - } - } - - @Override - public void close() throws IOException { - for (TestInfo testInfo : testInfoFactory.createTestInfosWithoutCoverage()) { - writeTestInfo(testInfo); - } - endReport(); - } - - private void startReport() throws IOException { - testFileCounter++; - OutputStream outputStream = Files.newOutputStream(getOutputFile(testFileCounter).toPath()); - jsonGenerator = JsonUtils.createFactory().createGenerator(outputStream); - jsonGenerator.setPrettyPrinter(new DefaultPrettyPrinter()); - jsonGenerator.writeStartObject(); - jsonGenerator.writeFieldName("tests"); - jsonGenerator.writeStartArray(); - } - - private File getOutputFile(int testFileCounter) { - String name = this.outputFile.getName(); - name = StringUtils.stripSuffix(name, ".json"); - name = name + "-" + testFileCounter + ".json"; - return new File(this.outputFile.getParent(), name); - } - - private void writeTestInfo(TestInfo testInfo) throws IOException { - if (testsWritten >= splitAfter) { - endReport(); - testsWritten = 0; - startReport(); - } - jsonGenerator.writeObject(testInfo); - testsWritten++; - } - - private void endReport() throws IOException { - jsonGenerator.writeEndArray(); - jsonGenerator.writeEndObject(); - jsonGenerator.close(); - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/CachingExecutionDataReader.java b/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/CachingExecutionDataReader.java deleted file mode 100644 index e57d5b02b..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/CachingExecutionDataReader.java +++ /dev/null @@ -1,134 +0,0 @@ -package com.teamscale.report.testwise.jacoco; - -import com.teamscale.report.EDuplicateClassFileBehavior; -import com.teamscale.report.jacoco.dump.Dump; -import com.teamscale.report.testwise.jacoco.cache.AnalyzerCache; -import com.teamscale.report.testwise.jacoco.cache.CoverageGenerationException; -import com.teamscale.report.testwise.jacoco.cache.ProbesCache; -import com.teamscale.report.testwise.model.builder.TestCoverageBuilder; -import com.teamscale.report.util.ClasspathWildcardIncludeFilter; -import com.teamscale.report.util.ILogger; -import org.jacoco.core.data.ExecutionData; -import org.jacoco.core.data.ExecutionDataStore; - -import java.io.File; -import java.io.IOException; -import java.util.Collection; -import java.util.function.Consumer; -import java.util.stream.Collectors; - -/** - * Helper class for analyzing class files, reading execution data and converting them to coverage data. - */ -class CachingExecutionDataReader { - - private final ILogger logger; - private final Collection classesDirectories; - private final ClasspathWildcardIncludeFilter locationIncludeFilter; - private final EDuplicateClassFileBehavior duplicateClassFileBehavior; - private ProbesCache probesCache; - - public CachingExecutionDataReader(ILogger logger, Collection classesDirectories, - ClasspathWildcardIncludeFilter locationIncludeFilter, - EDuplicateClassFileBehavior duplicateClassFileBehavior) { - this.logger = logger; - this.classesDirectories = classesDirectories; - this.locationIncludeFilter = locationIncludeFilter; - this.duplicateClassFileBehavior = duplicateClassFileBehavior; - } - - /** - * Analyzes the class/jar/war/... files and creates a lookup of which probes belong to which method. - */ - public void analyzeClassDirs() { - if (probesCache == null) { - probesCache = new ProbesCache(logger, duplicateClassFileBehavior); - } - if (classesDirectories.isEmpty()) { - logger.warn("No class directories found for caching."); - return; - } - AnalyzerCache analyzer = new AnalyzerCache(probesCache, locationIncludeFilter, logger); - int classCount = 0; - for (File classDir : classesDirectories) { - if (classDir.exists()) { - try { - classCount += analyzer.analyzeAll(classDir); - } catch (IOException 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); - } - } - } - if (classCount == 0) { - String directoryList = classesDirectories.stream().map(File::getPath).collect(Collectors.joining(",")); - logger.error("No class files found in the given directories! " + directoryList); - } else if (probesCache.isEmpty()) { - String directoryList = classesDirectories.stream().map(File::getPath).collect(Collectors.joining(",")); - logger.error( - "None of the " + classCount + " class files found in the given directories match the configured include/exclude patterns! " + directoryList); - } - } - - /** - * Converts the given store to coverage data. The coverage will only contain line range coverage information. - */ - public DumpConsumer buildCoverageConsumer(ClasspathWildcardIncludeFilter locationIncludeFilter, - Consumer nextConsumer) { - return new DumpConsumer(logger, locationIncludeFilter, nextConsumer); - } - - /** - * Consumer of {@link Dump} objects. Converts them to {@link TestCoverageBuilder} and passes them to the - * nextConsumer. - */ - public class DumpConsumer implements Consumer { - - /** The logger. */ - private final ILogger logger; - - /** The location include filter to be applied on the profiled classes. */ - private final ClasspathWildcardIncludeFilter locationIncludeFilter; - - /** Consumer that should be called with the newly built TestCoverageBuilder. */ - private final Consumer nextConsumer; - - private DumpConsumer(ILogger logger, ClasspathWildcardIncludeFilter locationIncludeFilter, - Consumer nextConsumer) { - this.logger = logger; - this.locationIncludeFilter = locationIncludeFilter; - this.nextConsumer = nextConsumer; - } - - @Override - public void accept(Dump dump) { - String testId = dump.info.getId(); - if (testId.isEmpty()) { - // Ignore intermediate coverage that does not belong to any specific test - logger.debug("Found a session with empty name! This could indicate that coverage is dumped also for " + - "coverage in between tests or that the given test name was empty!"); - return; - } - try { - TestCoverageBuilder testCoverage = buildCoverage(testId, dump.store, locationIncludeFilter); - nextConsumer.accept(testCoverage); - } catch (CoverageGenerationException e) { - logger.error("Failed to generate coverage for test " + testId + "! Skipping to the next test.", e); - } - } - - /** - * Converts the given store to coverage data. The coverage will only contain line range coverage information. - */ - private TestCoverageBuilder buildCoverage(String testId, ExecutionDataStore executionDataStore, - ClasspathWildcardIncludeFilter locationIncludeFilter) throws CoverageGenerationException { - TestCoverageBuilder testCoverage = new TestCoverageBuilder(testId); - for (ExecutionData executionData : executionDataStore.getContents()) { - testCoverage.add(probesCache.getCoverage(executionData, locationIncludeFilter)); - } - probesCache.flushLogger(); - return testCoverage; - } - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/JaCoCoTestwiseReportGenerator.java b/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/JaCoCoTestwiseReportGenerator.java deleted file mode 100644 index 971557ca4..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/JaCoCoTestwiseReportGenerator.java +++ /dev/null @@ -1,142 +0,0 @@ -package com.teamscale.report.testwise.jacoco; - -import com.teamscale.report.EDuplicateClassFileBehavior; -import com.teamscale.report.jacoco.dump.Dump; -import com.teamscale.report.testwise.jacoco.cache.CoverageGenerationException; -import com.teamscale.report.testwise.model.TestwiseCoverage; -import com.teamscale.report.testwise.model.builder.TestCoverageBuilder; -import com.teamscale.report.util.ClasspathWildcardIncludeFilter; -import com.teamscale.report.util.ILogger; -import org.jacoco.core.data.ExecutionData; -import org.jacoco.core.data.ExecutionDataReader; -import org.jacoco.core.data.ExecutionDataStore; -import org.jacoco.core.data.IExecutionDataVisitor; -import org.jacoco.core.data.ISessionInfoVisitor; -import org.jacoco.core.data.SessionInfo; - -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.function.Consumer; - -/** - * Creates an XML report for an execution data store. The report is grouped by session. - *

    - * The class files under test must be compiled with debug information otherwise no coverage will be collected. - */ -public class JaCoCoTestwiseReportGenerator { - - /** The execution data reader and converter. */ - private final CachingExecutionDataReader executionDataReader; - - /** The filter for the analyzed class files. */ - private final ClasspathWildcardIncludeFilter locationIncludeFilter; - - /** - * Create a new generator with a collection of class directories. - * - * @param codeDirectoriesOrArchives Root directory that contains the projects class files. - * @param locationIncludeFilter Filter for class files - * @param logger The logger - */ - public JaCoCoTestwiseReportGenerator(Collection codeDirectoriesOrArchives, - ClasspathWildcardIncludeFilter locationIncludeFilter, - EDuplicateClassFileBehavior duplicateClassFileBehavior, - ILogger logger) { - this.locationIncludeFilter = locationIncludeFilter; - this.executionDataReader = new CachingExecutionDataReader(logger, codeDirectoriesOrArchives, - locationIncludeFilter, duplicateClassFileBehavior); - updateClassDirCache(); - } - - /** - * Updates the probe cache of the {@link ExecutionDataReader}. - */ - public void updateClassDirCache() { - executionDataReader.analyzeClassDirs(); - } - - /** Converts the given dumps to a report. */ - public TestwiseCoverage convert(File executionDataFile) throws IOException, CoverageGenerationException { - TestwiseCoverage testwiseCoverage = new TestwiseCoverage(); - CachingExecutionDataReader.DumpConsumer dumpConsumer = executionDataReader - .buildCoverageConsumer(locationIncludeFilter, testwiseCoverage::add); - readAndConsumeDumps(executionDataFile, dumpConsumer); - return testwiseCoverage; - } - - /** Converts the given dump to a report. */ - public TestCoverageBuilder convert(Dump dump) throws CoverageGenerationException { - List list = new ArrayList<>(); - CachingExecutionDataReader.DumpConsumer dumpConsumer = executionDataReader - .buildCoverageConsumer(locationIncludeFilter, list::add); - dumpConsumer.accept(dump); - if (list.size() == 1) { - return list.get(0); - } else { - return null; - } - } - - /** Converts the given dumps to a report. */ - public void convertAndConsume(File executionDataFile, - Consumer consumer) throws IOException { - CachingExecutionDataReader.DumpConsumer dumpConsumer = executionDataReader - .buildCoverageConsumer(locationIncludeFilter, consumer); - readAndConsumeDumps(executionDataFile, dumpConsumer); - } - - /** Reads the dumps from the given *.exec file. */ - private void readAndConsumeDumps(File executionDataFile, Consumer dumpConsumer) throws IOException { - try (InputStream input = new BufferedInputStream(new FileInputStream(executionDataFile))) { - ExecutionDataReader executionDataReader = new ExecutionDataReader(input); - DumpCallback dumpCallback = new DumpCallback(dumpConsumer); - executionDataReader.setExecutionDataVisitor(dumpCallback); - executionDataReader.setSessionInfoVisitor(dumpCallback); - executionDataReader.read(); - // Ensure that the last read dump is also consumed - dumpCallback.processDump(); - } - } - - /** Collects execution information per session and passes it to the consumer . */ - private static class DumpCallback implements IExecutionDataVisitor, ISessionInfoVisitor { - - /** The dump that is currently being read. */ - private Dump currentDump = null; - - /** The store to which coverage is currently written to. */ - private ExecutionDataStore store; - - /** The consumer to pass {@link Dump}s to. */ - private final Consumer dumpConsumer; - - private DumpCallback(Consumer dumpConsumer) { - this.dumpConsumer = dumpConsumer; - } - - @Override - public void visitSessionInfo(SessionInfo info) { - processDump(); - store = new ExecutionDataStore(); - currentDump = new Dump(info, store); - } - - @Override - public void visitClassExecution(ExecutionData data) { - store.put(data); - } - - private void processDump() { - if (currentDump != null) { - dumpConsumer.accept(currentDump); - currentDump = null; - } - } - } -} \ No newline at end of file diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/cache/AnalyzerCache.java b/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/cache/AnalyzerCache.java deleted file mode 100644 index 6e67f2597..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/cache/AnalyzerCache.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.teamscale.report.testwise.jacoco.cache; - -import com.teamscale.report.jacoco.FilteringAnalyzer; -import com.teamscale.report.util.ClasspathWildcardIncludeFilter; -import com.teamscale.report.util.ILogger; -import org.jacoco.core.analysis.Analyzer; -import org.jacoco.core.internal.analysis.CachingClassAnalyzer; -import org.jacoco.core.internal.analysis.ClassCoverageImpl; -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; -import java.nio.file.Paths; - -/** - * An {@link AnalyzerCache} instance processes a set of Java class/jar/war/... files and builds a {@link - * ClassCoverageLookup} for each of the classes. - *

    - * For every class that gets found {@link #analyzeClass(byte[])} is called. A class is identified by its class ID which - * is a CRC64 checksum of the classfile. We process each class with {@link CachingClassAnalyzer} to fill a {@link - * ClassCoverageLookup}. - */ -public class AnalyzerCache extends FilteringAnalyzer { - - /** The probes cache. */ - private final ProbesCache probesCache; - - - private final StringPool stringPool = new StringPool(); - - /** Creates a new analyzer filling the given cache. */ - public AnalyzerCache(ProbesCache probesCache, ClasspathWildcardIncludeFilter locationIncludeFilter, - ILogger logger) { - super(null, null, locationIncludeFilter, logger); - this.probesCache = probesCache; - } - - /** - * Analyses the given class. Instead of the original implementation in {@link Analyzer#analyzeClass(byte[])} we - * don't use concrete execution data, but instead build a probe cache to speed up repeated lookups. - */ - @Override - protected void analyzeClass(final byte[] source) { - long classId = CRC64.classId(source); - if (probesCache.containsClassId(classId)) { - return; - } - final ClassReader reader = InstrSupport.classReaderFor(source); - ClassCoverageLookup classCoverageLookup = probesCache.createClass(classId, reader.getClassName()); - - // Dummy class coverage object that allows us to subclass ClassAnalyzer with CachingClassAnalyzer and reuse its - // IFilterContext implementation - final ClassCoverageImpl dummyClassCoverage = new ClassCoverageImpl(reader.getClassName(), - classId, false); - - CachingClassAnalyzer classAnalyzer = new CachingClassAnalyzer(classCoverageLookup, dummyClassCoverage, - stringPool); - final ClassVisitor visitor = new ClassProbesAdapter(classAnalyzer, false); - reader.accept(visitor, 0); - } - - /** - * Adds caching for jar files to the analyze jar functionality. - */ - @Override - protected int analyzeJar(final InputStream input, final String location) throws IOException { - long jarId = CRC64.classId(Files.readAllBytes(Paths.get(location))); - int probesCountForJarId = probesCache.countForJarId(jarId); - if (probesCountForJarId != 0) { - return probesCountForJarId; - } - int count = super.analyzeJar(input, location); - probesCache.addJarId(jarId, count); - return count; - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/cache/ClassCoverageLookup.java b/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/cache/ClassCoverageLookup.java deleted file mode 100644 index 2675b3309..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/cache/ClassCoverageLookup.java +++ /dev/null @@ -1,132 +0,0 @@ -package com.teamscale.report.testwise.jacoco.cache; - -import com.teamscale.client.StringUtils; -import com.teamscale.report.testwise.model.builder.FileCoverageBuilder; -import com.teamscale.report.util.ILogger; -import com.teamscale.report.util.SortedIntList; -import org.jacoco.core.data.ExecutionData; - -import java.util.ArrayList; -import java.util.List; - -/** - * Holds information about a class' probes and to which line ranges they refer. - *

    - *

      - *
    • Create an instance of this class for every analyzed java class. - *
    • Set the file name of the java source file from which the class has been created. - *
    • Then call {@link #addProbe(int, SortedIntList)} for all probes and lines that belong to that probe. - *
    • Afterwards call {@link #getFileCoverage(ExecutionData, ILogger)} to transform probes ({@link - * ExecutionData}) for this class into covered lines ({@link FileCoverageBuilder}). - *
    - */ -public class ClassCoverageLookup { - - /** Fully qualified name of the class (with / as separators). */ - private String className; - - /** Name of the java source file. */ - private String sourceFileName; - - /** - * Mapping from probe IDs to sets of covered lines. The index in this list corresponds to the probe ID. - */ - private final List probes = new ArrayList<>(); - - /** - * Constructor. - * - * @param className Classname as stored in the bytecode e.g. com/company/Example - */ - ClassCoverageLookup(String className) { - this.className = className; - } - - /** Sets the file name of the currently analyzed class (without path). */ - public void setSourceFileName(String sourceFileName) { - this.sourceFileName = sourceFileName; - } - - /** Adjusts the size of the probes list to the total probes count. */ - public void setTotalProbeCount(int count) { - ensureArraySize(count - 1); - } - - /** Adds the probe with the given id to the method. */ - public void addProbe(int probeId, SortedIntList lines) { - ensureArraySize(probeId); - probes.set(probeId, lines); - } - - /** - * Ensures that the probes list is big enough to allow access to the given index. Intermediate list entries are - * filled with null. - */ - private void ensureArraySize(int index) { - while (index >= probes.size()) { - probes.add(null); - } - } - - /** - * Generates {@link FileCoverageBuilder} from an {@link ExecutionData}. {@link ExecutionData} holds coverage of - * exactly one class (whereby inner classes are a separate class). This method returns a {@link FileCoverageBuilder} - * object which is later merged with the {@link FileCoverageBuilder} of other classes that reside in the same file. - */ - public FileCoverageBuilder getFileCoverage(ExecutionData executionData, - ILogger logger) throws CoverageGenerationException { - boolean[] executedProbes = executionData.getProbes(); - - if (checkProbeInvariant(executedProbes)) { - throw new CoverageGenerationException("Probe lookup does not match with actual probe size for " + - sourceFileName + " " + className + " (" + probes.size() + " vs " + executedProbes.length + ")! " + - "This is a bug in the profiler tooling. Please report it back to CQSE."); - } - if (sourceFileName == null) { - logger.warn( - "No source file name found for class " + className + "! This class was probably not compiled with " + - "debug information enabled!"); - return null; - } - - // we model the default package as the empty string - String packageName = ""; - if (className.contains("/")) { - packageName = StringUtils.removeLastPart(className, '/'); - } - final FileCoverageBuilder fileCoverage = new FileCoverageBuilder(packageName, sourceFileName); - fillFileCoverage(fileCoverage, executedProbes, logger); - - return fileCoverage; - } - - private void fillFileCoverage(FileCoverageBuilder fileCoverage, boolean[] executedProbes, ILogger logger) { - for (int i = 0; i < probes.size(); i++) { - SortedIntList coveredLines = probes.get(i); - if (!executedProbes[i]) { - continue; - } - // coveredLines is null if the probe is outside of a method - // Happens e.g. for methods generated by Lombok - if (coveredLines == null) { - logger.info(sourceFileName + " " + className + " did contain a covered probe " + i + "(of " + - executedProbes.length + ") that could not be " + - "matched to any method. This could be a bug in the profiler tooling. Please report it back " + - "to CQSE."); - continue; - } - if (coveredLines.isEmpty()) { - logger.debug( - sourceFileName + " " + className + " did contain a method with no line information. " + - "Does the class contain debug information?"); - continue; - } - fileCoverage.addLines(coveredLines); - } - } - - /** Checks that the executed probes is not smaller than the cached probes. */ - private boolean checkProbeInvariant(boolean[] executedProbes) { - return probes.size() > executedProbes.length; - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/cache/ClassNotFoundLogger.java b/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/cache/ClassNotFoundLogger.java deleted file mode 100644 index b8fe6ecc1..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/cache/ClassNotFoundLogger.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.teamscale.report.testwise.jacoco.cache; - -import com.teamscale.report.util.ILogger; - -import java.util.HashSet; -import java.util.Set; - -/** - * Coordinates logging of missing class files to ensure the warnings are only emitted once and not for every individual - * test. - */ -/* package */ class ClassNotFoundLogger { - - /** The logger. */ - private final ILogger logger; - - /** Missing classes that will be logged when {@link #flush()} is called. */ - private final Set classesToBeLogged = new HashSet<>(); - - /** Classes that have already been reported as missing. */ - private final Set alreadyLoggedClasses = new HashSet<>(); - - /** Constructor */ - /* package */ ClassNotFoundLogger(ILogger logger) { - this.logger = logger; - } - - /** Saves the given class to be logged later on. Ensures that the class is only logged once. */ - /* package */ void log(String fullyQualifiedClassName) { - if (!alreadyLoggedClasses.contains(fullyQualifiedClassName)) { - classesToBeLogged.add(fullyQualifiedClassName); - } - } - - /** Writes a summary of the missing class files to the logger. */ - /* package */ void flush() { - if (classesToBeLogged.isEmpty()) { - return; - } - - logger.warn( - "Found coverage for " + classesToBeLogged - .size() + " classes that were not provided. Either you did not provide " + - "all relevant class files or you did not adjust the include/exclude filters on the agent to exclude " + - "coverage from irrelevant code. The classes are:" - ); - for (String fullyQualifiedClassName : classesToBeLogged) { - logger.warn(" - " + fullyQualifiedClassName); - } - alreadyLoggedClasses.addAll(classesToBeLogged); - classesToBeLogged.clear(); - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/cache/CoverageGenerationException.java b/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/cache/CoverageGenerationException.java deleted file mode 100644 index fa975c191..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/cache/CoverageGenerationException.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.teamscale.report.testwise.jacoco.cache; - -/** - * Exception thrown during coverage generation. - */ -public class CoverageGenerationException extends Exception { - - /** Constructor. */ - public CoverageGenerationException(String message) { - super(message); - } - -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/cache/ProbesCache.java b/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/cache/ProbesCache.java deleted file mode 100644 index 603fc1337..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/jacoco/cache/ProbesCache.java +++ /dev/null @@ -1,113 +0,0 @@ -package com.teamscale.report.testwise.jacoco.cache; - -import com.teamscale.report.EDuplicateClassFileBehavior; -import com.teamscale.report.testwise.model.builder.FileCoverageBuilder; -import com.teamscale.report.util.ClasspathWildcardIncludeFilter; -import com.teamscale.report.util.ILogger; -import org.jacoco.core.data.ExecutionData; -import org.jacoco.report.JavaNames; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -/** - * Holds {@link ClassCoverageLookup}s for all analyzed classes. - */ -public class ProbesCache { - - /** The logger. */ - private final ILogger logger; - - /** A mapping from class ID (CRC64 of the class file) to {@link ClassCoverageLookup}. */ - private final HashMap classCoverageLookups = new HashMap<>(); - - /** Holds all fully-qualified class names that are already contained in the cache. */ - private final Set containedClasses = new HashSet<>(); - - private final Map containedJars = new HashMap<>(); - - /** Whether to ignore non-identical duplicates of class files. */ - private final EDuplicateClassFileBehavior duplicateClassFileBehavior; - - private final ClassNotFoundLogger classNotFoundLogger; - - /** Constructor. */ - public ProbesCache(ILogger logger, EDuplicateClassFileBehavior duplicateClassFileBehavior) { - this.logger = logger; - this.classNotFoundLogger = new ClassNotFoundLogger(logger); - this.duplicateClassFileBehavior = duplicateClassFileBehavior; - } - - /** Adds a new class entry to the cache and returns its {@link ClassCoverageLookup}. */ - public ClassCoverageLookup createClass(long classId, String className) { - if (containedClasses.contains(className)) { - if (duplicateClassFileBehavior != EDuplicateClassFileBehavior.IGNORE) { - logger.warn("Non-identical class file for class " + className + "." - + " 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."); - } - if (duplicateClassFileBehavior == EDuplicateClassFileBehavior.FAIL) { - throw new IllegalStateException( - "Found non-identical class file for class " + className + ". See logs for more details."); - } - } - containedClasses.add(className); - ClassCoverageLookup classCoverageLookup = new ClassCoverageLookup(className); - classCoverageLookups.put(classId, classCoverageLookup); - return classCoverageLookup; - } - - /** Returns whether a class with the given class ID has already been analyzed. */ - public boolean containsClassId(long classId) { - return classCoverageLookups.containsKey(classId); - } - - /** - * Returns the number of found class files in a cached jar file. Otherwise 0. - */ - public int countForJarId(long jarId) { - return containedJars.getOrDefault(jarId, 0); - } - - /** - * Adds a jar id along with the count of class files found in the jar. - */ - public void addJarId(long jarId, int count) { - containedJars.put(jarId, count); - } - - /** - * Converts the given {@link ExecutionData} to {@link FileCoverageBuilder} using the cached lookups or null if the - * class file of this class has not been included in the analysis or was not covered. - */ - public FileCoverageBuilder getCoverage(ExecutionData executionData, - ClasspathWildcardIncludeFilter locationIncludeFilter) throws CoverageGenerationException { - long classId = executionData.getId(); - if (!containsClassId(classId)) { - String fullyQualifiedClassName = new JavaNames().getQualifiedClassName(executionData.getName()); - if (locationIncludeFilter.isIncluded(fullyQualifiedClassName + ".class")) { - classNotFoundLogger.log(fullyQualifiedClassName); - } - return null; - } - if (!executionData.hasHits()) { - return null; - } - - return classCoverageLookups.get(classId).getFileCoverage(executionData, logger); - } - - /** Returns true if the cache does not contain coverage for any class. */ - public boolean isEmpty() { - return classCoverageLookups.isEmpty(); - } - - /** Prints a the collected class not found messages. */ - public void flushLogger() { - classNotFoundLogger.flush(); - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/model/ERevisionType.java b/report-generator/src/main/java/com/teamscale/report/testwise/model/ERevisionType.java deleted file mode 100644 index 01417d1b5..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/model/ERevisionType.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.teamscale.report.testwise.model; - -/** Type of revision information. */ -public enum ERevisionType { - - /** Commit descriptor in the format branch:timestamp. */ - COMMIT, - - /** Source control revision, e.g. SVN revision or Git hash. */ - REVISION -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/model/FileCoverage.java b/report-generator/src/main/java/com/teamscale/report/testwise/model/FileCoverage.java deleted file mode 100644 index 40ba4c93b..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/model/FileCoverage.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.teamscale.report.testwise.model; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -/** Holds coverage of a single file. */ -public class FileCoverage { - - /** The name of the file. */ - public final String fileName; - - /** A list of line ranges that have been covered. */ - public final String coveredLines; - - @JsonCreator - public FileCoverage(@JsonProperty("fileName") String fileName, @JsonProperty("coveredLines") String coveredLines) { - this.fileName = fileName; - this.coveredLines = coveredLines; - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/model/LineRange.java b/report-generator/src/main/java/com/teamscale/report/testwise/model/LineRange.java deleted file mode 100644 index b280100b3..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/model/LineRange.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.teamscale.report.testwise.model; - -/** Holds a line range with start and end (both inclusive and 1-based). */ -public class LineRange { - - /** The start line (1-based). */ - private int start; - - /** The end line (1-based). */ - private int end; - - /** Constructor. */ - public LineRange(int start, int end) { - this.start = start; - this.end = end; - } - - /** @see #end */ - public int getEnd() { - return end; - } - - /** @see #end */ - public void setEnd(int end) { - this.end = end; - } - - /** - * Returns the line range as used in the XML report. - * A range is returned as e.g. 2-5 or simply 3 if the start and end are equal. - */ - public String toReportString() { - if (start == end) { - return String.valueOf(start); - } else { - return start + "-" + end; - } - } - - @Override - public String toString() { - return toReportString(); - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/model/PathCoverage.java b/report-generator/src/main/java/com/teamscale/report/testwise/model/PathCoverage.java deleted file mode 100644 index 1886c7c9b..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/model/PathCoverage.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.teamscale.report.testwise.model; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.List; - -/** Container for {@link FileCoverage}s of the same path. */ -public class PathCoverage { - - /** File system path. */ - private final String path; - - /** Files with coverage. */ - private final List files; - - @JsonCreator - public PathCoverage(@JsonProperty("path") String path, @JsonProperty("files") List files) { - this.path = path; - this.files = files; - } - - public String getPath() { - return path; - } - - public List getFiles() { - return files; - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/model/RevisionInfo.java b/report-generator/src/main/java/com/teamscale/report/testwise/model/RevisionInfo.java deleted file mode 100644 index b272ec1b6..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/model/RevisionInfo.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.teamscale.report.testwise.model; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.teamscale.client.CommitDescriptor; - -import java.io.Serializable; - -/** Revision information necessary for uploading reports to Teamscale. */ -public class RevisionInfo implements Serializable { - - private static final long serialVersionUID = 1L; - - /** The type of revision information. */ - private final ERevisionType type; - - /** The value. Either a commit descriptor or a source control revision, depending on {@link #type}. */ - private final String value; - - @JsonCreator - public RevisionInfo(@JsonProperty("type") ERevisionType type, @JsonProperty("value") String value) { - this.type = type; - this.value = value; - } - - /** Constructor for Commit. */ - public RevisionInfo(CommitDescriptor commit) { - type = ERevisionType.COMMIT; - value = commit.toString(); - } - - /** Constructor for Revision. */ - public RevisionInfo(String revision) { - type = ERevisionType.REVISION; - value = revision; - } - - /** - * Constructor in case you have both fields, and either may be null. If both are set, the commit wins. If both are - * null, {@link #type} will be {@link ERevisionType#REVISION} and {@link #value} will be null. - */ - public RevisionInfo(CommitDescriptor commit, String revision) { - if (commit == null) { - type = ERevisionType.REVISION; - value = revision; - } else { - type = ERevisionType.COMMIT; - value = commit.toString(); - } - } - - public ERevisionType getType() { - return type; - } - - public String getValue() { - return value; - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/model/TestInfo.java b/report-generator/src/main/java/com/teamscale/report/testwise/model/TestInfo.java deleted file mode 100644 index 8648662ec..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/model/TestInfo.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.teamscale.report.testwise.model; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.ArrayList; -import java.util.List; - -/** Generic container of all information about a specific test as written to the report. */ -@SuppressWarnings({"FieldCanBeLocal", "WeakerAccess"}) -public class TestInfo { - - /** Unique name of the test case by using a path like hierarchical description, which can be shown in the UI. */ - public final 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 class name of the most specific subclass, from where it was actually executed. - */ - public final 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. - */ - public final String content; - - /** Duration of the execution in seconds. */ - public final Double duration; - - /** The actual execution result state. */ - public final ETestExecutionResult result; - - /** - * Optional message given for test failures (normally contains a stack trace). May be {@code null}. - */ - public final String message; - - /** All paths that the test did cover. */ - public final List paths = new ArrayList<>(); - - @JsonCreator - public TestInfo(@JsonProperty("uniformPath") String uniformPath, @JsonProperty("sourcePath") String sourcePath, - @JsonProperty("content") String content, @JsonProperty("duration") Double duration, - @JsonProperty("result") ETestExecutionResult result, - @JsonProperty("message") String message) { - this.uniformPath = uniformPath; - this.sourcePath = sourcePath; - this.content = content; - this.duration = duration; - this.result = result; - this.message = message; - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/model/TestwiseCoverage.java b/report-generator/src/main/java/com/teamscale/report/testwise/model/TestwiseCoverage.java deleted file mode 100644 index 94e786a44..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/model/TestwiseCoverage.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.teamscale.report.testwise.model; - -import com.teamscale.report.testwise.model.builder.TestCoverageBuilder; - -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; - -/** Container for coverage produced by multiple tests. */ -public class TestwiseCoverage { - - /** A mapping from test ID to {@link TestCoverageBuilder}. */ - private final Map tests = new HashMap<>(); - - /** - * Adds the {@link TestCoverageBuilder} to the map. - * If there is already a test with the same ID the coverage is merged. - */ - public void add(TestCoverageBuilder coverage) { - if (coverage == null || coverage.isEmpty()) { - return; - } - if (tests.containsKey(coverage.getUniformPath())) { - TestCoverageBuilder testCoverage = tests.get(coverage.getUniformPath()); - testCoverage.addAll(coverage.getFiles()); - } else { - tests.put(coverage.getUniformPath(), coverage); - } - } - - /** - * Merges the given {@link TestwiseCoverage} with this one. - */ - public void add(TestwiseCoverage testwiseCoverage) { - if (testwiseCoverage == null) { - return; - } - for (TestCoverageBuilder value : testwiseCoverage.tests.values()) { - this.add(value); - } - } - - public Collection getTests() { - return tests.values(); - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/model/TestwiseCoverageReport.java b/report-generator/src/main/java/com/teamscale/report/testwise/model/TestwiseCoverageReport.java deleted file mode 100644 index 1ec2f2e73..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/model/TestwiseCoverageReport.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.teamscale.report.testwise.model; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.ArrayList; -import java.util.List; - -/** Container for coverage produced by multiple tests. */ -public class TestwiseCoverageReport { - - /** - * If set to `true` the set of tests contained in the report don't represent the full set of tests within a - * partition. These tests are added or updated in Teamscale, but no tests or executable units that are missing in - * the report will be deleted. - */ - public final boolean partial; - - /** The tests contained in the report. */ - public final List tests = new ArrayList<>(); - - @JsonCreator - public TestwiseCoverageReport(@JsonProperty("partial") boolean partial) { - this.partial = partial; - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/model/builder/FileCoverageBuilder.java b/report-generator/src/main/java/com/teamscale/report/testwise/model/builder/FileCoverageBuilder.java deleted file mode 100644 index 0782f89d4..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/model/builder/FileCoverageBuilder.java +++ /dev/null @@ -1,111 +0,0 @@ -package com.teamscale.report.testwise.model.builder; - -import com.teamscale.report.testwise.model.FileCoverage; -import com.teamscale.report.testwise.model.LineRange; -import com.teamscale.report.util.SortedIntList; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -/** Holds coverage of a single file. */ -public class FileCoverageBuilder { - - /** The file system path of the file not including the file itself. */ - private final String path; - - /** The name of the file. */ - private final String fileName; - - /** - * A list of line numbers that have been covered. Using a set here is too memory intensive. - */ - private final SortedIntList coveredLines = new SortedIntList(); - - /** Constructor. */ - public FileCoverageBuilder(String path, String fileName) { - this.path = path; - this.fileName = fileName; - } - - /** @see #fileName */ - public String getFileName() { - return fileName; - } - - /** @see #path */ - public String getPath() { - return path; - } - - /** Adds a line as covered. */ - public void addLine(int line) { - coveredLines.add(line); - } - - /** Adds a line range as covered. */ - public void addLineRange(int start, int end) { - for (int i = start; i <= end; i++) { - coveredLines.add(i); - } - } - - /** Adds set of lines as covered. */ - public void addLines(SortedIntList range) { - coveredLines.addAll(range); - } - - /** Merges the list of ranges into the current list. */ - public void merge(FileCoverageBuilder other) { - if (!other.fileName.equals(fileName) || !other.path.equals(path)) { - throw new AssertionError("Cannot merge coverage of two different files! This is a bug!"); - } - coveredLines.addAll(other.coveredLines); - } - - /** - * Merges all neighboring line numbers to ranges. E.g. a list of [[1-5],[3-7],[8-10],[12-14]] becomes - * [[1-10],[12-14]] - */ - public static List compactifyToRanges(SortedIntList lines) { - if (lines.size() == 0) { - return new ArrayList<>(); - } - - int firstLine = lines.get(0); - LineRange currentRange = new LineRange(firstLine, firstLine); - - List compactifiedRanges = new ArrayList<>(); - compactifiedRanges.add(currentRange); - - for (int i = 0; i < lines.size(); i++) { - int currentLine = lines.get(i); - if (currentRange.getEnd() == currentLine || currentRange.getEnd() == currentLine - 1) { - currentRange.setEnd(currentLine); - } else { - currentRange = new LineRange(currentLine, currentLine); - compactifiedRanges.add(currentRange); - } - } - return compactifiedRanges; - } - - /** - * Returns a compact string representation of the covered lines. Continuous line ranges are merged to ranges and - * sorted. Individual ranges are separated by commas. E.g. 1-5,7,9-11. - */ - public String computeCompactifiedRangesAsString() { - List coveredRanges = compactifyToRanges(coveredLines); - return coveredRanges.stream().map(LineRange::toReportString).collect(Collectors.joining(",")); - } - - /** Returns true if there is no coverage for the file yet. */ - public boolean isEmpty() { - return coveredLines.size() == 0; - } - - /** Builds the {@link FileCoverage} object, which is serialized into the report. */ - public FileCoverage build() { - return new FileCoverage(fileName, computeCompactifiedRangesAsString()); - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/model/builder/PathCoverageBuilder.java b/report-generator/src/main/java/com/teamscale/report/testwise/model/builder/PathCoverageBuilder.java deleted file mode 100644 index 8da3d78c3..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/model/builder/PathCoverageBuilder.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.teamscale.report.testwise.model.builder; - -import com.teamscale.report.testwise.model.FileCoverage; -import com.teamscale.report.testwise.model.PathCoverage; - -import java.util.Collection; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static java.util.stream.Collectors.toList; - -/** Container for {@link FileCoverageBuilder}s of the same path. */ -public class PathCoverageBuilder { - - /** File system path. */ - private final String path; - - /** Mapping from file names to {@link FileCoverageBuilder}. */ - private final Map fileCoverageList = new HashMap<>(); - - /** Constructor. */ - public PathCoverageBuilder(String path) { - this.path = path; - } - - /** @see #path */ - public String getPath() { - return path; - } - - /** - * Adds the given {@link FileCoverageBuilder} to the container. - * If coverage for the same file already exists it gets merged. - */ - public void add(FileCoverageBuilder fileCoverage) { - if (fileCoverageList.containsKey(fileCoverage.getFileName())) { - FileCoverageBuilder existingFile = fileCoverageList.get(fileCoverage.getFileName()); - existingFile.merge(fileCoverage); - } else { - fileCoverageList.put(fileCoverage.getFileName(), fileCoverage); - } - } - - /** Returns a collection of {@link FileCoverageBuilder}s associated with this path. */ - public Collection getFiles() { - return fileCoverageList.values(); - } - - /** Builds a {@link PathCoverage} object. */ - public PathCoverage build() { - List files = fileCoverageList.values().stream() - .sorted(Comparator.comparing(FileCoverageBuilder::getFileName)) - .map(FileCoverageBuilder::build).collect(toList()); - return new PathCoverage(path, files); - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/model/builder/TestCoverageBuilder.java b/report-generator/src/main/java/com/teamscale/report/testwise/model/builder/TestCoverageBuilder.java deleted file mode 100644 index b8eedce72..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/model/builder/TestCoverageBuilder.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.teamscale.report.testwise.model.builder; - -import com.teamscale.report.testwise.model.PathCoverage; - -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static java.util.stream.Collectors.toList; - -/** Generic holder of test coverage of a single test based on line-ranges. */ -public class TestCoverageBuilder { - - /** The uniformPath of the test (see TEST_IMPACT_ANALYSIS_DOC.md for more information). */ - private final String uniformPath; - - /** Mapping from path names to all files on this path. */ - private final Map pathCoverageList = new HashMap<>(); - - /** Constructor. */ - public TestCoverageBuilder(String uniformPath) { - this.uniformPath = uniformPath; - } - - /** @see #uniformPath */ - public String getUniformPath() { - return uniformPath; - } - - /** Returns a collection of {@link PathCoverageBuilder}s associated with the test. */ - public List getPaths() { - return pathCoverageList.values().stream().sorted(Comparator.comparing(PathCoverageBuilder::getPath)) - .map(PathCoverageBuilder::build).collect(toList()); - } - - /** Adds the {@link FileCoverageBuilder} to into the map, but filters out file coverage that is null or empty. */ - public void add(FileCoverageBuilder fileCoverage) { - if (fileCoverage == null || fileCoverage.isEmpty() - || fileCoverage.getFileName() == null || fileCoverage.getPath() == null) { - return; - } - PathCoverageBuilder pathCoverage = pathCoverageList - .computeIfAbsent(fileCoverage.getPath(), PathCoverageBuilder::new); - pathCoverage.add(fileCoverage); - } - - /** Adds the {@link FileCoverageBuilder}s into the map, but filters out empty ones. */ - public void addAll(List fileCoverageList) { - for (FileCoverageBuilder fileCoverage : fileCoverageList) { - add(fileCoverage); - } - } - - /** Returns all {@link FileCoverageBuilder}s stored for the test. */ - public List getFiles() { - return pathCoverageList.values().stream() - .flatMap(path -> path.getFiles().stream()) - .collect(toList()); - } - - /** Returns true if there is no coverage for the test yet. */ - public boolean isEmpty() { - return pathCoverageList.isEmpty(); - } - -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/model/builder/TestInfoBuilder.java b/report-generator/src/main/java/com/teamscale/report/testwise/model/builder/TestInfoBuilder.java deleted file mode 100644 index 6a4bd08c0..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/model/builder/TestInfoBuilder.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.teamscale.report.testwise.model.builder; - -import com.teamscale.client.TestDetails; -import com.teamscale.report.testwise.model.ETestExecutionResult; -import com.teamscale.report.testwise.model.TestExecution; -import com.teamscale.report.testwise.model.TestInfo; - -/** Generic container of all information about a specific test including details, execution info and coverage. */ -public class TestInfoBuilder { - - /** Unique name of the test case by using a path like hierarchical description, which can be shown in the UI. */ - public final 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. - */ - private String sourcePath = null; - - /** - * Some kind of content to tell whether the test specification has changed. Can be revision number or hash over the - * specification or similar. - */ - private String content = null; - - /** Duration of the execution in milliseconds. */ - private Double durationSeconds = null; - - /** The actual execution result state. */ - private ETestExecutionResult result; - - /** - * Optional message given for test failures (normally contains a stack trace). May be {@code null}. - */ - private String message; - - /** Coverage generated by this test. */ - private TestCoverageBuilder coverage; - - /** Constructor. */ - /* package */ - public TestInfoBuilder(String uniformPath) { - this.uniformPath = uniformPath; - } - - /** @see #uniformPath */ - public String getUniformPath() { - return uniformPath; - } - - /** Returns true if there is no coverage for the test yet. */ - public boolean isEmpty() { - return coverage.isEmpty(); - } - - /** Sets the test details fields. */ - public void setDetails(TestDetails details) { - if (details != null) { - sourcePath = details.sourcePath; - content = details.content; - } - } - - /** Sets the test execution fields. */ - public void setExecution(TestExecution execution) { - if (execution != null) { - durationSeconds = execution.getDurationSeconds(); - result = execution.getResult(); - message = execution.getMessage(); - } - } - - /** @see #coverage */ - public void setCoverage(TestCoverageBuilder coverage) { - this.coverage = coverage; - } - - /** Builds a {@link TestInfo} object of the data in this container. */ - public TestInfo build() { - TestInfo testInfo = new TestInfo(uniformPath, sourcePath, content, durationSeconds, result, message); - if (coverage != null) { - testInfo.paths.addAll(coverage.getPaths()); - } - return testInfo; - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/model/builder/TestwiseCoverageReportBuilder.java b/report-generator/src/main/java/com/teamscale/report/testwise/model/builder/TestwiseCoverageReportBuilder.java deleted file mode 100644 index ae221bc7d..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/model/builder/TestwiseCoverageReportBuilder.java +++ /dev/null @@ -1,88 +0,0 @@ -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.ArrayList; -import java.util.Collection; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** Container for coverage produced by multiple tests. */ -public class TestwiseCoverageReportBuilder { - - /** A mapping from test ID to {@link TestCoverageBuilder}. */ - private final Map tests = new HashMap<>(); - - /** - * Adds the {@link TestCoverageBuilder} to the map. If there is already a test with the same ID the coverage is - * merged. - */ - public static TestwiseCoverageReport createFrom( - Collection testDetailsList, - Collection testCoverage, - Collection testExecutions, - boolean partial - ) { - TestwiseCoverageReportBuilder report = new TestwiseCoverageReportBuilder(); - for (TestDetails testDetails : testDetailsList) { - TestInfoBuilder container = new TestInfoBuilder(testDetails.uniformPath); - container.setDetails(testDetails); - report.tests.put(testDetails.uniformPath, container); - } - for (TestCoverageBuilder coverage : testCoverage) { - TestInfoBuilder container = resolveUniformPath(report, coverage.getUniformPath()); - if (container == null) { - continue; - } - container.setCoverage(coverage); - } - for (TestExecution testExecution : testExecutions) { - TestInfoBuilder container = resolveUniformPath(report, testExecution.getUniformPath()); - if (container == null) { - continue; - } - container.setExecution(testExecution); - } - return report.build(partial); - } - - private static TestInfoBuilder resolveUniformPath(TestwiseCoverageReportBuilder report, String uniformPath) { - TestInfoBuilder container = report.tests.get(uniformPath); - if (container != null) { - return container; - } - String shortenedUniformPath = stripParameterizedTestArguments(uniformPath); - TestInfoBuilder testInfoBuilder = report.tests.get(shortenedUniformPath); - if (testInfoBuilder == null) { - System.err.println("No container found for test '" + uniformPath + "'!"); - } - return testInfoBuilder; - } - - /** - * Removes parameterized test arguments from the given uniform path. - */ - public static String stripParameterizedTestArguments(String uniformPath) { - return uniformPath.replaceFirst("(.*\\))\\[.*]", "$1"); - } - - private TestwiseCoverageReport build(boolean partial) { - TestwiseCoverageReport report = new TestwiseCoverageReport(partial); - List testInfoBuilders = new ArrayList<>(tests.values()); - testInfoBuilders.sort(Comparator.comparing(TestInfoBuilder::getUniformPath)); - for (TestInfoBuilder testInfoBuilder : testInfoBuilders) { - TestInfo testInfo = testInfoBuilder.build(); - if (testInfo == null) { - System.err.println("No coverage for test '" + testInfoBuilder.getUniformPath() + "'"); - continue; - } - report.tests.add(testInfo); - } - return report; - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/model/factory/TestInfoFactory.java b/report-generator/src/main/java/com/teamscale/report/testwise/model/factory/TestInfoFactory.java deleted file mode 100644 index 0895630a9..000000000 --- a/report-generator/src/main/java/com/teamscale/report/testwise/model/factory/TestInfoFactory.java +++ /dev/null @@ -1,100 +0,0 @@ -package com.teamscale.report.testwise.model.factory; - -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.builder.TestCoverageBuilder; -import com.teamscale.report.testwise.model.builder.TestInfoBuilder; -import com.teamscale.report.testwise.model.builder.TestwiseCoverageReportBuilder; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Factory class for converting {@link TestCoverageBuilder} to {@link TestInfo}s while augmenting them with information - * from test details and test executions. - */ -public class TestInfoFactory { - - /** Maps uniform paths to test details. */ - private Map testDetailsMap = new HashMap<>(); - - /** Maps uniform paths to test executions. */ - private Map testExecutionsMap = new HashMap<>(); - - /** Holds all uniform paths for tests that have been written to the outputFile. */ - private final Set processedTestUniformPaths = new HashSet<>(); - - public TestInfoFactory(List testDetails, List testExecutions) { - for (TestDetails testDetail : testDetails) { - testDetailsMap.put(testDetail.uniformPath, testDetail); - } - for (TestExecution testExecution : testExecutions) { - testExecutionsMap.put(testExecution.getUniformPath(), testExecution); - } - } - - /** - * Converts the given {@link TestCoverageBuilder} to a {@link TestInfo} using the internally stored test details and - * test executions. - */ - public TestInfo createFor(TestCoverageBuilder testCoverageBuilder) { - String resolvedUniformPath = resolveUniformPath(testCoverageBuilder.getUniformPath()); - processedTestUniformPaths.add(resolvedUniformPath); - - TestInfoBuilder container = new TestInfoBuilder(resolvedUniformPath); - container.setCoverage(testCoverageBuilder); - TestDetails testDetails = testDetailsMap.get(resolvedUniformPath); - if (testDetails == null) { - System.err.println("No test details found for " + resolvedUniformPath); - } - container.setDetails(testDetails); - TestExecution execution = testExecutionsMap.get(resolvedUniformPath); - if (execution == null) { - System.err.println("No test execution found for " + resolvedUniformPath); - } - container.setExecution(execution); - return container.build(); - } - - /** Returns {@link TestInfo}s for all tests that have not been used yet in {@link #createFor(TestCoverageBuilder)}. */ - public List createTestInfosWithoutCoverage() { - ArrayList results = new ArrayList<>(); - for (TestDetails testDetails : testDetailsMap.values()) { - if (!processedTestUniformPaths.contains(testDetails.uniformPath)) { - TestInfoBuilder testInfo = new TestInfoBuilder(testDetails.uniformPath); - testInfo.setDetails(testDetails); - testInfo.setExecution(testExecutionsMap.get(testDetails.uniformPath)); - results.add(testInfo.build()); - processedTestUniformPaths.add(testDetails.uniformPath); - } - } - for (TestExecution testExecution : testExecutionsMap.values()) { - if (!processedTestUniformPaths.contains(testExecution.getUniformPath())) { - System.err.println("Test " + testExecution.getUniformPath() + " was executed but no coverage was found. " + - "Please make sure that you did provide all relevant exec files and that the test IDs passed to " + - "the agent match the ones from the provided test execution list."); - processedTestUniformPaths.add(testExecution.getUniformPath()); - } - } - return results; - } - - /** - * Strips parameterized test arguments when the full path given in the coverage file cannot be found in the test - * details. - */ - private String resolveUniformPath(String originalUniformPath) { - String uniformPath = originalUniformPath; - TestDetails testDetails = testDetailsMap.get(uniformPath); - if (testDetails == null) { - uniformPath = TestwiseCoverageReportBuilder - .stripParameterizedTestArguments(uniformPath); - } - return uniformPath; - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/util/AntPatternIncludeFilter.java b/report-generator/src/main/java/com/teamscale/report/util/AntPatternIncludeFilter.java deleted file mode 100644 index 309e22750..000000000 --- a/report-generator/src/main/java/com/teamscale/report/util/AntPatternIncludeFilter.java +++ /dev/null @@ -1,57 +0,0 @@ -/*-------------------------------------------------------------------------+ -| | -| Copyright (c) 2009-2018 CQSE GmbH | -| | -+-------------------------------------------------------------------------*/ -package com.teamscale.report.util; - -import com.teamscale.client.AntPatternUtils; -import com.teamscale.client.FileSystemUtils; - -import java.util.List; -import java.util.function.Predicate; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -/** - * Applies ANT include and exclude patterns to paths. - */ -public class AntPatternIncludeFilter implements Predicate { - - /** The include filters. Empty means include everything. */ - private final List locationIncludeFilters; - - /** The exclude filters. Empty means exclude nothing. */ - private final List locationExcludeFilters; - - /** Constructor. */ - public AntPatternIncludeFilter(List locationIncludeFilters, List locationExcludeFilters) { - this.locationIncludeFilters = locationIncludeFilters.stream().map( - filter -> AntPatternUtils.convertPattern(filter, false)).collect(Collectors.toList()); - this.locationExcludeFilters = locationExcludeFilters.stream().map( - filter -> AntPatternUtils.convertPattern(filter, false)).collect(Collectors.toList()); - } - - /** {@inheritDoc} */ - @Override - public boolean test(String path) { - return !isFiltered(FileSystemUtils.normalizeSeparators(path)); - } - - /** - * Returns true if the given class file location (normalized to forward slashes as path separators) - * should not be analyzed. - *

    - * Exclude filters overrule include filters. - */ - private boolean isFiltered(String location) { - // first check includes - if (!locationIncludeFilters.isEmpty() - && locationIncludeFilters.stream().noneMatch(filter -> filter.matcher(location).matches())) { - return true; - } - // only if they match, check excludes - return locationExcludeFilters.stream().anyMatch(filter -> filter.matcher(location).matches()); - } - -} diff --git a/report-generator/src/main/java/com/teamscale/report/util/BashFileSkippingInputStream.java b/report-generator/src/main/java/com/teamscale/report/util/BashFileSkippingInputStream.java deleted file mode 100644 index 59c9102d4..000000000 --- a/report-generator/src/main/java/com/teamscale/report/util/BashFileSkippingInputStream.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.teamscale.report.util; - -import java.io.BufferedInputStream; -import java.io.FilterInputStream; -import java.io.IOException; -import java.io.InputStream; - -/** - * Handles executable spring-boot jar files that prepend a bash file to the beginning of the ZIP file to make it - * directly executable without "java -jar my.jar". We just skip the bash file until we find the zip file header. - */ -public class BashFileSkippingInputStream extends FilterInputStream { - - /** - * Wraps the given input stream in a BufferedInputStream and consumes all bytes until a zip file header is found. - */ - public BashFileSkippingInputStream(InputStream in) throws IOException { - super(new BufferedInputStream(in)); - consumeUntilZipHeader(); - } - - /** - * Reads the stream until the zip file header "50 4B 03 04" is found or EOF is reached. After calling the method the - * read pointer points to the first byte of the zip file header. - */ - private void consumeUntilZipHeader() throws IOException { - byte[] buffer = new byte[8192]; - in.mark(buffer.length); - int count = in.read(buffer, 0, buffer.length); - while (count > 0) { - for (int i = 0; i < count - 3; i++) { - if (buffer[i] == 0x50 && buffer[i + 1] == 0x4B && buffer[i + 2] == 0x03 && buffer[i + 3] == 0x04) { - in.reset(); - in.skip(i); - return; - } - } - - // Reset mark to 3 bytes before the end of the previously read buffer end to - // also detect a zip header when it spans over two buffers - in.reset(); - in.skip(buffer.length - 3); - in.mark(buffer.length); - count = in.read(buffer, 0, buffer.length); - } - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/util/ClasspathWildcardIncludeFilter.java b/report-generator/src/main/java/com/teamscale/report/util/ClasspathWildcardIncludeFilter.java deleted file mode 100644 index 56d9480c5..000000000 --- a/report-generator/src/main/java/com/teamscale/report/util/ClasspathWildcardIncludeFilter.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.teamscale.report.util; - -import com.teamscale.client.FileSystemUtils; -import com.teamscale.client.StringUtils; -import org.jacoco.core.runtime.WildcardMatcher; -import org.jacoco.report.JavaNames; - -/*** - * Tests given class file paths against call name patterns. - * E.g. "/some/file/path/test.jar@my/package/Test.class" matches "my/package/*" or "my/package/Test" - */ -public class ClasspathWildcardIncludeFilter { - - /** - * Include patterns to apply during JaCoCo's traversal of class files. If null then everything is included. - */ - private WildcardMatcher locationIncludeFilters = null; - - /** - * Exclude patterns to apply during JaCoCo's traversal of class files. If null then nothing is excluded. - */ - private WildcardMatcher locationExcludeFilters = null; - - /** - * Constructor. - * - * @param locationIncludeFilters Colon separated list of wildcard include patterns for fully qualified class names - * or null for no includes. See {@link WildcardMatcher} for the pattern syntax. - * @param locationExcludeFilters Colon separated list of wildcard exclude patterns for fully qualified class names - * or null for no excludes.See {@link WildcardMatcher} for the pattern syntax. - */ - public ClasspathWildcardIncludeFilter(String locationIncludeFilters, String locationExcludeFilters) { - if (locationIncludeFilters != null && !locationIncludeFilters.isEmpty()) { - this.locationIncludeFilters = new WildcardMatcher(locationIncludeFilters); - } - if (locationExcludeFilters != null && !locationExcludeFilters.isEmpty()) { - this.locationExcludeFilters = new WildcardMatcher(locationExcludeFilters); - } - } - - /** - * Tests if the given file path (e.g. "/some/file/path/test.jar@my/package/Test.class" or "org/mypackage/MyClass" - */ - public boolean isIncluded(String path) { - String className = getClassName(path); - // first check includes - if (locationIncludeFilters != null && !locationIncludeFilters.matches(className)) { - return false; - } - // if they match, check excludes - return locationExcludeFilters == null || !locationExcludeFilters.matches(className); - } - - /** - * Returns the normalized class name of the given class file's path. I.e. turns something like - * "/opt/deploy/some.jar@com/teamscale/Class.class" into something like "com.teamscale.Class". - */ - /* package */ - static String getClassName(String path) { - String[] parts = FileSystemUtils.normalizeSeparators(path).split("@"); - if (parts.length == 0) { - return ""; - } - - String pathInsideJar = parts[parts.length - 1]; - if (path.toLowerCase().endsWith(".class")) { - pathInsideJar = StringUtils.removeLastPart(pathInsideJar, '.'); - } - return new JavaNames().getQualifiedClassName(pathInsideJar); - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/util/CommandLineLogger.java b/report-generator/src/main/java/com/teamscale/report/util/CommandLineLogger.java deleted file mode 100644 index fe112bf8b..000000000 --- a/report-generator/src/main/java/com/teamscale/report/util/CommandLineLogger.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.teamscale.report.util; - -/** Logger that prints all output to the console. */ -public class CommandLineLogger implements ILogger { - - @Override - public void debug(String message) { - System.out.println(message); - } - - @Override - public void info(String message) { - System.out.println(message); - } - - @Override - public void warn(String message) { - System.err.println(message); - } - - @Override - public void warn(String message, Throwable throwable) { - System.err.println(message); - if (throwable != null) { - throwable.printStackTrace(); - } - } - - @Override - public void error(Throwable throwable) { - throwable.printStackTrace(); - } - - @Override - public void error(String message, Throwable throwable) { - System.err.println(message); - if (throwable != null) { - throwable.printStackTrace(); - } - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/util/ILogger.java b/report-generator/src/main/java/com/teamscale/report/util/ILogger.java deleted file mode 100644 index b4d23c3c3..000000000 --- a/report-generator/src/main/java/com/teamscale/report/util/ILogger.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.teamscale.report.util; - -/** - * Minimal logging interface. - *

    - * We use this to work around some strange problems when using log4j from the Teamscale Gradle plugin. - */ -public interface ILogger { - - /** Logs at debug level. */ - void debug(String message); - - /** Logs at info level. */ - void info(String message); - - /** Logs at warning level. */ - void warn(String message); - - /** Logs at warning level. The given {@link Throwable} may be null. */ - void warn(String message, Throwable throwable); - - /** Logs at error level. */ - void error(Throwable throwable); - - /** Logs at error level. The given {@link Throwable} may be null. */ - void error(String message, Throwable throwable); - - /** Logs at error level. */ - default void error(String message) { - error(message, null); - } -} diff --git a/report-generator/src/main/java/com/teamscale/report/util/SortedIntList.java b/report-generator/src/main/java/com/teamscale/report/util/SortedIntList.java deleted file mode 100644 index afa4a7ef8..000000000 --- a/report-generator/src/main/java/com/teamscale/report/util/SortedIntList.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.teamscale.report.util; - -/** - * Performant implementation of a deduplicated sorted integer list that assumes that insertions mainly happen at the end - * and that input is already sorted. - */ -public class SortedIntList { - - /** - * The list of values in sorted order and without duplicates. The list might be bigger than the number of elements. - */ - protected int[] list; - - /** The number of actual elements in the list. */ - private int count; - - public SortedIntList() { - list = new int[64]; - } - - /** Adds the given value to the list at the correct location, ignoring duplicates. */ - public boolean add(int value) { - int high = count; - int low = 0; - - if (isEmpty()) { - list[0] = value; - count = 1; - return true; - } - - // Perform binary search to find target location - do { - int p = (low + high) >>> 1; - if (value < list[p]) { - high = p; - } else if (value == list[p]) { - // Element already exists in the list - return false; - } else { - low = p + 1; - } - } while (low < high); - - if (count == list.length) { - int[] n = new int[list.length * 2]; - System.arraycopy(list, 0, n, 0, count); - list = n; - } - - if (low < count) { - System.arraycopy(list, low, list, low + 1, count - low); - } - list[low] = value; - count++; - return true; - } - - /** Inserts all values from the given list, ignoring duplicates. */ - public void addAll(SortedIntList input) { - for (int i = 0; i < input.size(); i++) { - add(input.get(i)); - } - } - - /** Returns the size of the list. */ - public int size() { - return count; - } - - /** Returns whether the list is empty. */ - public boolean isEmpty() { - return count == 0; - } - - /** Returns the i-th element of the list. */ - public int get(int i) { - return list[i]; - } -} \ No newline at end of file diff --git a/report-generator/src/main/java/org/jacoco/core/internal/analysis/CachingClassAnalyzer.java b/report-generator/src/main/java/org/jacoco/core/internal/analysis/CachingClassAnalyzer.java deleted file mode 100644 index 406b0877b..000000000 --- a/report-generator/src/main/java/org/jacoco/core/internal/analysis/CachingClassAnalyzer.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.jacoco.core.internal.analysis; - -import com.teamscale.report.testwise.jacoco.cache.ClassCoverageLookup; -import org.jacoco.core.internal.flow.MethodProbesVisitor; -import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.tree.MethodNode; - -/** - * Analyzes a class to reconstruct probe information. - *

    - * A probe lookup holds for a single class which probe belongs to which lines. The actual filling of the - * {@link ClassCoverageLookup} happens in {@link CachingInstructionsBuilder}. - */ -public class CachingClassAnalyzer extends ClassAnalyzer { - - /** The cache, which contains a probe lookups for the current class. */ - private final ClassCoverageLookup classCoverageLookup; - - /** - * Creates a new analyzer that builds coverage data for a class. - * - * @param classCoverageLookup cache for the class' probes - * @param coverage coverage node for the analyzed class data - * @param stringPool shared pool to minimize the number of {@link String} instances - */ - public CachingClassAnalyzer(ClassCoverageLookup classCoverageLookup, ClassCoverageImpl coverage, StringPool stringPool) { - super(coverage, null, stringPool); - this.classCoverageLookup = classCoverageLookup; - } - - @Override - public void visitSource(String source, String debug) { - super.visitSource(source, debug); - classCoverageLookup.setSourceFileName(source); - } - - @Override - public MethodProbesVisitor visitMethod(final int access, final String name, - final String desc, final String signature, final String[] exceptions) { - final CachingInstructionsBuilder builder = new CachingInstructionsBuilder(classCoverageLookup); - - return new MethodAnalyzer(builder) { - - @Override - public void accept(final MethodNode methodNode, - final MethodVisitor methodVisitor) { - super.accept(methodNode, methodVisitor); - builder.fillCache(); - } - }; - } - - @Override - public void visitTotalProbeCount(final int count) { - classCoverageLookup.setTotalProbeCount(count); - } -} 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 deleted file mode 100644 index 4701cbadd..000000000 --- a/report-generator/src/main/java/org/jacoco/core/internal/analysis/CachingInstructionsBuilder.java +++ /dev/null @@ -1,234 +0,0 @@ -package org.jacoco.core.internal.analysis; - -import com.teamscale.report.testwise.jacoco.cache.ClassCoverageLookup; -import com.teamscale.report.util.SortedIntList; -import org.jacoco.core.analysis.ISourceNode; -import org.jacoco.core.internal.flow.LabelInfo; -import org.objectweb.asm.Label; -import org.objectweb.asm.tree.AbstractInsnNode; - -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Stateful builder for the {@link Instruction}s of a method. All instructions of a method must be added in their - * original sequence along with additional information like line numbers. Afterwards the instructions can be obtained - * with the getInstructions() method. - *

    - * It's core is a copy of {@link org.jacoco.core.internal.analysis.InstructionsBuilder} that has been extended with - * caching functionality to speed up report generation. - *

    - * This class contains callbacks for stepping through a method at bytecode level which has been decorated with probes by - * JaCoCo in a depth-first-search like way. - *

    - * Changes that have been applied to the original class are marked with ADDED and REMOVED comments to make it as easy as - * possible to adjust the implementation to new versions of JaCoCo. - *

    - * 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. - */ -public class CachingInstructionsBuilder extends InstructionsBuilder { - - /** Probe array of the class the analyzed method belongs to. */ - // REMOVED private final boolean[] probes; - - // ADDED field to hold a reference to our coverage lookup - private final ClassCoverageLookup classCoverageLookup; - private final List coveredProbes = new ArrayList<>(); - - /** The line which belong to subsequently added instructions. */ - private int currentLine; - - /** The last instruction which has been added. */ - private Instruction currentInsn; - - /** - * All instructions of a method mapped from the ASM node to the corresponding {@link Instruction} instance. - */ - private final Map instructions; - - /** - * The labels which mark the subsequent instructions. - *

    - * Due to ASM issue #315745 there can be more than one label per instruction - */ - private final List

    - * ADDED ClassCoverageLookup classCoverageLookup parameter REMOVED final boolean[] probes - * - * @param classCoverageLookup cache of the class' probes - */ - public CachingInstructionsBuilder(ClassCoverageLookup classCoverageLookup) { - super(null); - this.classCoverageLookup = classCoverageLookup; - this.currentLine = ISourceNode.UNKNOWN_LINE; - this.currentInsn = null; - this.instructions = new HashMap<>(); - this.currentLabel = new ArrayList<>(2); - this.jumps = new ArrayList<>(); - } - - /** - * Sets the current source line. All subsequently added instructions will be assigned to this line. If no line is - * set (e.g. for classes compiled without debug information) {@link ISourceNode#UNKNOWN_LINE} is assigned to the - * instructions. - */ - void setCurrentLine(final int line) { - currentLine = line; - } - - /** - * Adds a label which applies to the subsequently added instruction. Due to ASM internals multiple {@link Label}s - * can be added to an instruction. - */ - void addLabel(final Label label) { - currentLabel.add(label); - if (!LabelInfo.isSuccessor(label)) { - noSuccessor(); - } - } - - /** - * Adds a new instruction. Instructions are by default linked with the previous instruction unless specified - * otherwise. - */ - void addInstruction(final AbstractInsnNode node) { - final Instruction insn = new Instruction(currentLine); - final int labelCount = currentLabel.size(); - if (labelCount > 0) { - for (int i = labelCount; --i >= 0; ) { - LabelInfo.setInstruction(currentLabel.get(i), insn); - } - currentLabel.clear(); - } - if (currentInsn != null) { - currentInsn.addBranch(insn, 0); - } - currentInsn = insn; - instructions.put(node, insn); - } - - /** - * Declares that the next instruction will not be a successor of the current instruction. This is the case with an - * unconditional jump or technically when a probe was inserted before. - */ - void noSuccessor() { - currentInsn = null; - } - - /** - * Adds a jump from the last added instruction. - * - * @param target jump target - * @param branch unique branch number - */ - void addJump(final Label target, final int branch) { - jumps.add(new Jump(currentInsn, target, branch)); - } - - /** - * Adds a new probe for the last instruction. - * - * @param probeId index in the probe array - * @param branch unique branch number for the last instruction - */ - void addProbe(final int probeId, final int branch) { - // REMOVED check of probes array and instead add the probes unconditionally - // final boolean executed = probes != null && probes[probeId]; - // currentInsn.addBranch(executed, branch); - - // ADDED - currentInsn.addBranch(true, branch); - coveredProbes.add(new CoveredProbe(probeId, currentInsn, branch)); - } - - /** - * Returns the status for all instructions of this method. This method must be called exactly once after the - * instructions have been added. - */ - public void fillCache() { - // Wire jumps: - for (final Jump j : jumps) { - j.wire(); - } - - // ADDED - // Traces back all instructions that are executed before reaching a probe - // and stores the mapping from probe to lines in #classCoverageLookup - // We need this because JaCoCo does not insert a probe after every line. - for (CoveredProbe coveredProbe : coveredProbes) { - Instruction instruction = coveredProbe.instruction; - SortedIntList coveredLines = new SortedIntList(); - while (instruction != null) { - if (instruction.getLine() != -1) { - // Only add the line number if one is associated with the instruction. - // This is not the case for e.g. Lombok generated code. - coveredLines.add(instruction.getLine()); - } - instruction = getPredecessor(instruction); - } - classCoverageLookup.addProbe(coveredProbe.probeId, coveredLines); - } - } - - /** - * ADDED Helper to get the private field predecessor from an instruction. The predecessor of an instruction is the - * preceding node according to the control flow graph of the method. - */ - private Instruction getPredecessor(Instruction instruction) { - try { - Field predecessorField = instruction.getClass().getDeclaredField("predecessor"); - predecessorField.setAccessible(true); - instruction = (Instruction) predecessorField.get(instruction); - } catch (NoSuchFieldException | IllegalAccessException e) { - // This means we have a serious coding mistake here there is no way to recover from this anyway - throw new RuntimeException("Instruction has no field named predecessor! This is a programming error!", e); - } - return instruction; - } - - // ADDED - private static class CoveredProbe { - - final int probeId; - final Instruction instruction; - final int branch; - - private CoveredProbe(int probeId, final Instruction instruction, final int branch) { - this.probeId = probeId; - this.instruction = instruction; - this.branch = branch; - } - } - - private static class Jump { - - private final Instruction source; - private final Label target; - private final int branch; - - Jump(final Instruction source, final Label target, final int branch) { - this.source = source; - this.target = target; - this.branch = branch; - } - - void wire() { - source.addBranch(LabelInfo.getInstruction(target), branch); - } - - } - -} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/EDuplicateClassFileBehavior.kt b/report-generator/src/main/kotlin/com/teamscale/report/EDuplicateClassFileBehavior.kt new file mode 100644 index 000000000..9b21854b0 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/EDuplicateClassFileBehavior.kt @@ -0,0 +1,15 @@ +package com.teamscale.report + +/** + * Behavior when two non-identical class files with the same package name are found. + */ +enum class EDuplicateClassFileBehavior { + /** Completely ignores it. */ + IGNORE, + + /** Prints a warning to the logger. */ + WARN, + + /** Fails and stops further processing. */ + FAIL +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/ReportUtils.kt b/report-generator/src/main/kotlin/com/teamscale/report/ReportUtils.kt new file mode 100644 index 000000000..e3010f447 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/ReportUtils.kt @@ -0,0 +1,96 @@ +package com.teamscale.report + +import com.fasterxml.jackson.core.JsonProcessingException +import com.teamscale.client.FileSystemUtils +import com.teamscale.client.JsonUtils +import com.teamscale.client.TestDetails +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.* + +/** Utilities for generating reports. */ +object ReportUtils { + /** Converts to given test list to a json report and writes it to the given file. */ + @Throws(IOException::class) + @JvmStatic + fun writeTestListReport(reportFile: File, report: List) { + writeReportToFile(reportFile, report) + } + + /** Converts to given test execution report to a json report and writes it to the given file. */ + @Throws(IOException::class) + @JvmStatic + fun writeTestExecutionReport(reportFile: File, report: List) { + writeReportToFile(reportFile, report) + } + + /** Converts to given testwise coverage report to a json report and writes it to the given file. */ + @Throws(IOException::class) + fun writeTestwiseCoverageReport(reportFile: File, report: TestwiseCoverageReport) { + writeReportToFile(reportFile, report) + } + + /** Converts to given report to a json string. For testing only. */ + @JvmStatic + @Throws(JsonProcessingException::class) + fun getTestwiseCoverageReportAsString( + report: TestwiseCoverageReport? + ): String { + return JsonUtils.serialize(report) + } + + /** Writes the report object to the given file as json. */ + @Throws(IOException::class) + private fun writeReportToFile(reportFile: File, report: T) { + val directory: File = reportFile.getParentFile() + if (!directory.isDirectory() && !directory.mkdirs()) { + throw IOException("Failed to create directory " + directory.absolutePath) + } + JsonUtils.serializeToFile(reportFile, report) + } + + /** Recursively lists all files in the given directory that match the specified extension. */ + @Throws(IOException::class) + @JvmStatic + fun readObjects( + format: ETestArtifactFormat, clazz: Class>?, + directoriesOrFiles: List + ): List { + val files: List = listFiles(format, directoriesOrFiles) + val result: ArrayList = ArrayList() + for (file: File? in files) { + val t: Array? = JsonUtils.deserializeFile(file, clazz) + if (t != null) { + result.addAll(Arrays.asList(*t)) + } + } + return result + } + + /** Recursively lists all files of the given artifact type. */ + @JvmStatic + fun listFiles(format: ETestArtifactFormat, directoriesOrFiles: List): List { + val filesWithSpecifiedArtifactType: MutableList = ArrayList() + for (directoryOrFile: File in directoriesOrFiles) { + if (directoryOrFile.isDirectory()) { + filesWithSpecifiedArtifactType.addAll( + FileSystemUtils.listFilesRecursively(directoryOrFile) { + it.isOfArtifactFormat(format) + } + ) + } else if (directoryOrFile.isOfArtifactFormat(format)) { + filesWithSpecifiedArtifactType.add(directoryOrFile) + } + } + return filesWithSpecifiedArtifactType + } + + private fun File.isOfArtifactFormat(format: ETestArtifactFormat) = + isFile() && + getName().startsWith(format.filePrefix) && + FileSystemUtils.getFileExtension(this).equals(format.extension, ignoreCase = true) +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/jacoco/CoverageFile.kt b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/CoverageFile.kt new file mode 100644 index 000000000..530017739 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/CoverageFile.kt @@ -0,0 +1,121 @@ +package com.teamscale.report.jacoco + +import com.teamscale.client.FileSystemUtils +import okhttp3.MultipartBody +import okhttp3.RequestBody +import java.io.* +import java.nio.file.Files +import java.util.* + +/** + * Represents a coverage file on disk. The main purpose is to avoid reading the + * entire file into memory as this dramatically increases the memory footprint + * of the JVM which might run out of memory because of this. + * + * The object internally holds a counter of how many references to the file are + * currently held. This allows to share the same file for multiple uploads and + * deleting it once all uploads have succeeded. Use [.acquireReference] + * to make the object aware that it was passed to another uploader and + * [.delete] to signal that you no longer intend to access the file. + */ +class CoverageFile(private val coverageFile: File) { + private var referenceCounter = 0 + + /** + * Marks the file as being used by an additional uploader. This ensures that the + * file is not deleted until all users have signed via [.delete] that + * they no longer intend to access the file. + */ + fun acquireReference(): CoverageFile { + referenceCounter++ + return this + } + + /** + * Copies the coverage File in blocks from the disk to the output stream to + * avoid having to read the entire file into memory. + */ + @Throws(IOException::class) + fun copy(outputStream: OutputStream?) { + val inputStream = FileInputStream(coverageFile) + FileSystemUtils.copy(inputStream, outputStream) + inputStream.close() + } + + val nameWithoutExtension: String + /** + * Get the filename of the coverage file on disk without its extension + */ + get() = FileSystemUtils.getFilenameWithoutExtension(coverageFile) + + val name: String + /** Get the filename of the coverage file. */ + get() = coverageFile.name + + /** + * Delete the coverage file from disk + */ + @Throws(IOException::class) + fun delete() { + referenceCounter-- + if (referenceCounter <= 0) { + Files.delete(coverageFile.toPath()) + } + } + + /** + * Create a [okhttp3.MultipartBody] form body with the contents of the + * coverage file. + */ + fun createFormRequestBody(): RequestBody { + return RequestBody.create(MultipartBody.FORM, File(coverageFile.absolutePath)) + } + + @get:Throws(IOException::class) + val outputStream: OutputStream + /** + * Get the [java.io.OutputStream] in order to write to the coverage file. + * + * @throws IOException + * If the file did not exist yet and could not be created + */ + get() { + try { + return FileOutputStream(coverageFile) + } catch (e: IOException) { + throw IOException( + ("Could not create temporary coverage file" + this + ". " + + "This is used to cache the coverage file on disk before uploading it to its final destination. " + + "This coverage is lost. Please fix the underlying issue to avoid losing coverage."), e + ) + } + } + + /** + * {@inheritDoc} + */ + override fun equals(o: Any?): Boolean { + if (this === o) { + return true + } + if (o == null || javaClass != o.javaClass) { + return false + } + val that = o as CoverageFile + return coverageFile == that.coverageFile + } + + /** + * {@inheritDoc} + */ + override fun hashCode(): Int { + return Objects.hash(coverageFile) + } + + /** + * {@inheritDoc} + */ + override fun toString(): String { + return coverageFile.absolutePath + } +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/jacoco/EmptyReportException.kt b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/EmptyReportException.kt new file mode 100644 index 000000000..aa035ff54 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/EmptyReportException.kt @@ -0,0 +1,6 @@ +package com.teamscale.report.jacoco + +/** + * Exception indicating that the generated report was empty and no [CoverageFile] was written to disk. + */ +class EmptyReportException(message: String?) : Exception(message) diff --git a/report-generator/src/main/kotlin/com/teamscale/report/jacoco/FilteringAnalyzer.kt b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/FilteringAnalyzer.kt new file mode 100644 index 000000000..49e9a7cbb --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/FilteringAnalyzer.kt @@ -0,0 +1,88 @@ +/*-------------------------------------------------------------------------+ +| | +| Copyright (c) 2009-2018 CQSE GmbH | +| | ++-------------------------------------------------------------------------*/ +package com.teamscale.report.jacoco + +import com.teamscale.report.util.BashFileSkippingInputStream +import com.teamscale.report.util.ClasspathWildcardIncludeFilter +import com.teamscale.report.util.ILogger +import org.jacoco.core.analysis.ICoverageVisitor +import org.jacoco.core.data.ExecutionDataStore +import java.io.IOException +import java.io.InputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream + +/** + * [Analyzer] that filters the analyzed class files based on a [Predicate]. + */ +/* package */ +open class FilteringAnalyzer( + executionData: ExecutionDataStore?, coverageVisitor: ICoverageVisitor?, + /** The filter for the analyzed class files. */ + private val locationIncludeFilter: ClasspathWildcardIncludeFilter, private val logger: ILogger +) : OpenAnalyzer(executionData, coverageVisitor) { + /** {@inheritDoc} */ + @Throws(IOException::class) + override fun analyzeAll(input: InputStream, location: String): Int { + if (location.endsWith(".class") && !locationIncludeFilter.isIncluded(location)) { + logger.debug("Excluding class file $location") + return 1 + } + if (location.endsWith(".jar")) { + return analyzeJar(input, location) + } + return super.analyzeAll(input, location) + } + + @Throws(IOException::class) + override fun analyzeClass(buffer: ByteArray, location: String) { + try { + analyzeClass(buffer) + } catch (cause: RuntimeException) { + if (isUnsupportedClassFile(cause)) { + logger.error(cause.message + " in " + location) + } else { + throw analyzerError(location, cause) + } + } + } + + /** + * Checks if the error indicates that the class file might be newer than what is currently supported by + * JaCoCo. The concrete error message seems to depend on the used JVM, so we only check for "Unsupported" which seems + * to be common amongst all of them. + */ + private fun isUnsupportedClassFile(cause: RuntimeException): Boolean { + return cause is IllegalArgumentException && cause.message?.startsWith("Unsupported") == true + } + + /** + * Copied from Analyzer.analyzeZip renamed to analyzeJar and added wrapping BashFileSkippingInputStream. + */ + @Throws(IOException::class) + protected open fun analyzeJar(input: InputStream, location: String): Int { + val zip = ZipInputStream(BashFileSkippingInputStream(input)) + var entry: ZipEntry? + var count = 0 + while ((nextEntry(zip, location).also { entry = it }) != null) { + count += analyzeAll(zip, location + "@" + entry!!.name) + } + return count + } + + /** Copied from Analyzer.nextEntry. */ + @Throws(IOException::class) + private fun nextEntry( + input: ZipInputStream, + location: String + ): ZipEntry? { + try { + return input.nextEntry + } catch (e: IOException) { + throw analyzerError(location, e) + } + } +} 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 new file mode 100644 index 000000000..2512bae9e --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/JaCoCoXmlReportGenerator.kt @@ -0,0 +1,116 @@ +package com.teamscale.report.jacoco + +import com.teamscale.report.EDuplicateClassFileBehavior +import com.teamscale.report.jacoco.dump.Dump +import com.teamscale.report.util.ClasspathWildcardIncludeFilter +import com.teamscale.report.util.ILogger +import org.jacoco.core.analysis.CoverageBuilder +import org.jacoco.core.analysis.IBundleCoverage +import org.jacoco.core.data.ExecutionDataStore +import org.jacoco.core.data.SessionInfo +import org.jacoco.report.xml.XMLFormatter +import java.io.File +import java.io.IOException +import java.io.OutputStream + +/** Creates an XML report from binary execution data. */ +class JaCoCoXmlReportGenerator( + /** Directories and zip files that contain class files. */ + private val codeDirectoriesOrArchives: List, + /** + * Include filter to apply to all locations during class file traversal. + */ + private val locationIncludeFilter: ClasspathWildcardIncludeFilter, + /** Whether to ignore non-identical duplicates of class files. */ + private val duplicateClassFileBehavior: EDuplicateClassFileBehavior, + /** Whether to remove uncovered classes from the report. */ + private val ignoreUncoveredClasses: Boolean, + /** The logger. */ + private val logger: ILogger +) { + /** + * Creates the report and writes it to a file. + * + * @return The file object of for the converted report or null if it could not be created + */ + @Throws(IOException::class, EmptyReportException::class) + fun convert(dump: Dump, filePath: File): CoverageFile { + val coverageFile = CoverageFile(filePath) + convertToReport(coverageFile, dump) + return coverageFile + } + + /** Creates the report. */ + @Throws(IOException::class, EmptyReportException::class) + private fun convertToReport(coverageFile: CoverageFile, dump: Dump) { + val mergedStore = dump.store + val bundleCoverage = analyzeStructureAndAnnotateCoverage(mergedStore) + checkForEmptyReport(bundleCoverage) + coverageFile.outputStream.use { outputStream -> + createReport( + outputStream, bundleCoverage, dump.info, + mergedStore!! + ) + } + } + + @Throws(EmptyReportException::class) + private fun checkForEmptyReport(coverage: IBundleCoverage) { + if (coverage.packages.size == 0 || coverage.lineCounter.totalCount == 0) { + throw EmptyReportException("The generated coverage report is empty. " + MOST_LIKELY_CAUSE_MESSAGE) + } + if (coverage.lineCounter.coveredCount == 0) { + throw EmptyReportException( + "The generated coverage report does not contain any covered source code lines. " + + MOST_LIKELY_CAUSE_MESSAGE + ) + } + } + + /** + * Analyzes the structure of the class files in [.codeDirectoriesOrArchives] and builds an in-memory coverage + * report with the coverage in the given store. + */ + @Throws(IOException::class) + private fun analyzeStructureAndAnnotateCoverage(store: ExecutionDataStore?): IBundleCoverage { + val coverageBuilder: CoverageBuilder = TeamscaleCoverageBuilder( + this.logger, + duplicateClassFileBehavior, ignoreUncoveredClasses + ) + + val analyzer = FilteringAnalyzer( + store, coverageBuilder, + locationIncludeFilter, + logger + ) + + for (file in codeDirectoriesOrArchives) { + analyzer.analyzeAll(file) + } + + return coverageBuilder.getBundle("dummybundle") + } + + companion object { + /** Part of the error message logged when validating the coverage report fails. */ + private const val MOST_LIKELY_CAUSE_MESSAGE = "Most likely you did not configure the agent correctly." + + " Please check that the includes and excludes options are set correctly so the relevant code is included." + + " If in doubt, first include more code and then iteratively narrow the patterns down to just the relevant code." + + " If you have specified the class-dir option, please make sure it points to a directory containing the" + + " class files/jars/wars/ears/etc. for which you are trying to measure code coverage." + + /** Creates an XML report based on the given session and coverage data. */ + @Throws(IOException::class) + private fun createReport( + output: OutputStream, bundleCoverage: IBundleCoverage, sessionInfo: SessionInfo?, + store: ExecutionDataStore + ) { + val xmlFormatter = XMLFormatter() + val visitor = xmlFormatter.createVisitor(output) + + visitor.visitInfo(listOf(sessionInfo), store.contents) + visitor.visitBundle(bundleCoverage, null) + visitor.visitEnd() + } + } +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/jacoco/OpenAnalyzer.kt b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/OpenAnalyzer.kt new file mode 100644 index 000000000..c771c4df1 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/OpenAnalyzer.kt @@ -0,0 +1,309 @@ +/******************************************************************************* + * Copyright (c) 2009, 2023 Mountainminds GmbH & Co. KG and Contributors + * This program and the accompanying materials are made available under + * the terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Marc R. Hoffmann - initial API and implementation + * + */ +package com.teamscale.report.jacoco + +import org.jacoco.core.JaCoCo +import org.jacoco.core.analysis.ICoverageVisitor +import org.jacoco.core.data.ExecutionDataStore +import org.jacoco.core.internal.ContentTypeDetector +import org.jacoco.core.internal.InputStreams +import org.jacoco.core.internal.Pack200Streams +import org.jacoco.core.internal.analysis.ClassAnalyzer +import org.jacoco.core.internal.analysis.ClassCoverageImpl +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.ClassVisitor +import org.objectweb.asm.Opcodes +import java.io.File +import java.io.FileInputStream +import java.io.IOException +import java.io.InputStream +import java.util.* +import java.util.zip.GZIPInputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream + +/** + * This is a copy of the [Analyzer] class from JaCoCo. + * The only changes are that the following methods are protected instead of private: + * - [.analyzeClass] + * - [.analyzerError] + * + * + * When performing an update of JaCoCo we need to check that this file is still up-to-date. + * + * + * An [Analyzer] instance processes a set of Java class files and + * calculates coverage data for them. For each class file the result is reported + * to a given [ICoverageVisitor] instance. In addition the + * [Analyzer] requires a [ExecutionDataStore] instance that holds + * the execution data for the classes to analyze. The [Analyzer] offers + * several methods to analyze classes from a variety of sources. + */ +open class OpenAnalyzer( + private val executionData: ExecutionDataStore?, + private val coverageVisitor: ICoverageVisitor? +) { + private val stringPool = StringPool() + + /** + * Creates an ASM class visitor for analysis. + * + * @param classId + * id of the class calculated with [CRC64] + * @param className + * VM name of the class + * @return ASM visitor to write class definition to + */ + private fun createAnalyzingVisitor( + classId: Long, + className: String + ): ClassVisitor { + val data = executionData!![classId] + val probes: BooleanArray? + val noMatch: Boolean + if (data == null) { + probes = null + noMatch = executionData.contains(className) + } else { + probes = data.probes + noMatch = false + } + val coverage = ClassCoverageImpl( + className, + classId, noMatch + ) + val analyzer: ClassAnalyzer = object : ClassAnalyzer( + coverage, probes, + stringPool + ) { + override fun visitEnd() { + super.visitEnd() + coverageVisitor!!.visitCoverage(coverage) + } + } + return ClassProbesAdapter(analyzer, false) + } + + /** Analyzes the given class in binary form. */ + protected open fun analyzeClass(source: ByteArray) { + val classId = CRC64.classId(source) + val reader = InstrSupport.classReaderFor(source) + if ((reader.access and Opcodes.ACC_MODULE) != 0) { + return + } + if ((reader.access and Opcodes.ACC_SYNTHETIC) != 0) { + return + } + val visitor = createAnalyzingVisitor( + classId, + reader.className + ) + reader.accept(visitor, 0) + } + + /** + * Analyzes the class definition from a given in-memory buffer. + * + * @param buffer + * class definitions + * @param location + * a location description used for exception messages + * @throws IOException + * if the class can't be analyzed + */ + @Throws(IOException::class) + open fun analyzeClass(buffer: ByteArray, location: String) { + try { + analyzeClass(buffer) + } catch (cause: RuntimeException) { + throw analyzerError(location, cause) + } + } + + /** + * Analyzes the class definition from a given input stream. The provided + * [InputStream] is not closed by this method. + * + * @param input + * stream to read class definition from + * @param location + * a location description used for exception messages + * @throws IOException + * if the stream can't be read or the class can't be analyzed + */ + @Throws(IOException::class) + fun analyzeClass(input: InputStream, location: String) { + val buffer: ByteArray + try { + buffer = InputStreams.readFully(input) + } catch (e: IOException) { + throw analyzerError(location, e) + } + analyzeClass(buffer, location) + } + + /** Creates an [IOException] which includes the affected file location and JaCoCo version. */ + protected fun analyzerError( + location: String?, + cause: Exception? + ): IOException { + val ex = IOException( + String.format( + "Error while analyzing %s with JaCoCo %s/%s.", + location, JaCoCo.VERSION, JaCoCo.COMMITID_SHORT + ) + ) + ex.initCause(cause) + return ex + } + + /** + * Analyzes all classes found in the given input stream. The input stream + * may either represent a single class file, a ZIP archive, a Pack200 + * archive or a gzip stream that is searched recursively for class files. + * All other content types are ignored. The provided [InputStream] is + * not closed by this method. + * + * @param input + * input data + * @param location + * a location description used for exception messages + * @return number of class files found + * @throws IOException + * if the stream can't be read or a class can't be analyzed + */ + @Throws(IOException::class) + open fun analyzeAll(input: InputStream, location: String): Int { + val detector: ContentTypeDetector + try { + detector = ContentTypeDetector(input) + } catch (e: IOException) { + throw analyzerError(location, e) + } + when (detector.type) { + ContentTypeDetector.CLASSFILE -> { + analyzeClass(detector.inputStream, location) + return 1 + } + + ContentTypeDetector.ZIPFILE -> return analyzeZip(detector.inputStream, location) + ContentTypeDetector.GZFILE -> return analyzeGzip(detector.inputStream, location) + ContentTypeDetector.PACK200FILE -> return analyzePack200(detector.inputStream, location) + else -> return 0 + } + } + + /** + * Analyzes all class files contained in the given file or folder. Class + * files as well as ZIP files are considered. Folders are searched + * recursively. + * + * @param file + * file or folder to look for class files + * @return number of class files found + * @throws IOException + * if the file can't be read or a class can't be analyzed + */ + @Throws(IOException::class) + fun analyzeAll(file: File): Int { + var count = 0 + if (file.isDirectory) { + for (f in file.listFiles()) { + count += analyzeAll(f) + } + } else { + val `in`: InputStream = FileInputStream(file) + try { + count += analyzeAll(`in`, file.path) + } finally { + `in`.close() + } + } + return count + } + + /** + * Analyzes all classes from the given class path. Directories containing + * class files as well as archive files are considered. + * + * @param path + * path definition + * @param basedir + * optional base directory, if `null` the current + * working directory is used as the base for relative path + * entries + * @return number of class files found + * @throws IOException + * if a file can't be read or a class can't be analyzed + */ + @Throws(IOException::class) + fun analyzeAll(path: String, basedir: File?): Int { + var count = 0 + val st = StringTokenizer( + path, + File.pathSeparator + ) + while (st.hasMoreTokens()) { + count += analyzeAll(File(basedir, st.nextToken())) + } + return count + } + + @Throws(IOException::class) + private fun analyzeZip(input: InputStream, location: String): Int { + val zip = ZipInputStream(input) + var entry: ZipEntry? + var count = 0 + while ((nextEntry(zip, location).also { entry = it }) != null) { + count += analyzeAll(zip, location + "@" + entry!!.name) + } + return count + } + + @Throws(IOException::class) + private fun nextEntry( + input: ZipInputStream, + location: String + ): ZipEntry? { + try { + return input.nextEntry + } catch (e: IOException) { + throw analyzerError(location, e) + } + } + + @Throws(IOException::class) + private fun analyzeGzip(input: InputStream, location: String): Int { + val gzipInputStream: GZIPInputStream + try { + gzipInputStream = GZIPInputStream(input) + } catch (e: IOException) { + throw analyzerError(location, e) + } + return analyzeAll(gzipInputStream, location) + } + + @Throws(IOException::class) + private fun analyzePack200(input: InputStream, location: String): Int { + val unpackedInput: InputStream + try { + unpackedInput = Pack200Streams.unpack(input) + } catch (e: IOException) { + throw analyzerError(location, e) + } + return analyzeAll(unpackedInput, location) + } +} 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 new file mode 100644 index 000000000..e24a95956 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/TeamscaleCoverageBuilder.kt @@ -0,0 +1,68 @@ +/*-------------------------------------------------------------------------+ +| | +| Copyright (c) 2009-2018 CQSE GmbH | +| | ++-------------------------------------------------------------------------*/ +package com.teamscale.report.jacoco + +import com.teamscale.report.EDuplicateClassFileBehavior +import com.teamscale.report.util.ILogger +import org.jacoco.core.analysis.CoverageBuilder +import org.jacoco.core.analysis.IBundleCoverage +import org.jacoco.core.analysis.IClassCoverage +import org.jacoco.core.analysis.ICounter +import org.jacoco.core.internal.analysis.BundleCoverageImpl + +/** + * Modified [CoverageBuilder] can ignore non-identical duplicate classes or classes without coverage. In addition, + * coverage returned via [.getBundle] will only return source file coverage because Teamscale does not + * need class coverage anyway. This reduces XML size by approximately half. + */ +/* package */ +internal class TeamscaleCoverageBuilder( + /** The logger. */ + private val logger: ILogger, + /** How to behave if duplicate class files are encountered. */ + private val duplicateClassFileBehavior: EDuplicateClassFileBehavior, + /** Whether to ignore uncovered classes (i.e. leave them out of the report). */ + private val ignoreUncoveredClasses: Boolean +) : CoverageBuilder() { + /** Just returns source file coverage, because Teamscale does not need class coverage. */ + override fun getBundle(name: String): IBundleCoverage { + return BundleCoverageImpl( + name, emptyList(), + sourceFiles + ) + } + + /** {@inheritDoc} */ + override fun visitCoverage(coverage: IClassCoverage) { + if (ignoreUncoveredClasses && (coverage.classCounter.status and ICounter.FULLY_COVERED) == 0) { + return + } + + try { + super.visitCoverage(coverage) + } catch (e: IllegalStateException) { + when (duplicateClassFileBehavior) { + EDuplicateClassFileBehavior.IGNORE -> return + EDuplicateClassFileBehavior.WARN -> { + // we deliberately do not log the exception in this case as it does not provide any additional + // valuable information but confuses users into thinking there's a serious problem with the agent + // as they only see that there are 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.") + ) + return + } + + else -> throw e + } + } + } +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/jacoco/dump/Dump.kt b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/dump/Dump.kt new file mode 100644 index 000000000..dda5f8024 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/dump/Dump.kt @@ -0,0 +1,15 @@ +/*-------------------------------------------------------------------------+ +| | +| Copyright (c) 2009-2018 CQSE GmbH | +| | ++-------------------------------------------------------------------------*/ +package com.teamscale.report.jacoco.dump + +import org.jacoco.core.data.ExecutionDataStore +import org.jacoco.core.data.SessionInfo + +/** All data received in one dump. */ +class Dump( + val info: SessionInfo, + val store: ExecutionDataStore +) diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/ETestArtifactFormat.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/ETestArtifactFormat.kt new file mode 100644 index 000000000..3e9b15c73 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/ETestArtifactFormat.kt @@ -0,0 +1,23 @@ +package com.teamscale.report.testwise + +/** Enum of test artifacts that can be converted to a full testwise coverage report later on. */ +enum class ETestArtifactFormat( + /** A readable name for the report type. */ + val readableName: String, + /** Prefix to use when writing the report to the file system. */ + val filePrefix: String, + /** File extension of the report. */ + val extension: String +) { + /** A json list of tests ([com.teamscale.client.TestDetails]). */ + TEST_LIST("Test List", "test-list", "json"), + + /** A json list of test executions ([com.teamscale.report.testwise.model.TestExecution]). */ + TEST_EXECUTION("Test Execution", "test-execution", "json"), + + /** Binary jacoco test coverage (.exec file). */ + JACOCO("Jacoco", "", "exec"), + + /** Google closure coverage files with additional uniformPath entries. */ + CLOSURE("Closure Coverage", "closure-coverage", "json") +} 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 new file mode 100644 index 000000000..2f1973034 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/TestwiseCoverageReportWriter.kt @@ -0,0 +1,93 @@ +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 + +/** + * Writes out a [com.teamscale.report.testwise.model.TestwiseCoverageReport] one [TestInfo] after the other + * so that we do not need to keep them all in memory during the conversion. + */ +class TestwiseCoverageReportWriter( + /** Factory for converting [TestCoverageBuilder] objects to [TestInfo]s. */ + private val testInfoFactory: TestInfoFactory, private val outputFile: File, + /** After how many written tests a new file should be started. */ + private val splitAfter: Int +) : Consumer, + AutoCloseable { + /** Writer instance to where the [com.teamscale.report.testwise.model.TestwiseCoverageReport] is written to. */ + private var jsonGenerator: JsonGenerator? = null + + /** Number of tests written to the file. */ + private var testsWritten: Int = 0 + + /** Number of test files that have been written. */ + private var testFileCounter: Int = 0 + + init { + startReport() + } + + override fun accept(testCoverageBuilder: TestCoverageBuilder) { + val testInfo: TestInfo? = testInfoFactory.createFor(testCoverageBuilder) + try { + writeTestInfo(testInfo) + } catch (e: IOException) { + // Need to be wrapped in RuntimeException as Consumer does not allow to throw a checked Exception + throw RuntimeException("Writing test info to report failed.", e) + } + } + + @Throws(IOException::class) + override fun close() { + for (testInfo: TestInfo? in testInfoFactory.createTestInfosWithoutCoverage()) { + writeTestInfo(testInfo) + } + endReport() + } + + @Throws(IOException::class) + private fun startReport() { + testFileCounter++ + val outputStream: OutputStream = Files.newOutputStream(getOutputFile(testFileCounter).toPath()) + jsonGenerator = JsonUtils.createFactory().createGenerator(outputStream) + jsonGenerator?.setPrettyPrinter(DefaultPrettyPrinter()) + jsonGenerator?.writeStartObject() + jsonGenerator?.writeFieldName("tests") + jsonGenerator?.writeStartArray() + } + + private fun getOutputFile(testFileCounter: Int): File { + var name: String = outputFile.getName() + name = StringUtils.stripSuffix(name, ".json") + name = name + "-" + testFileCounter + ".json" + return File(outputFile.getParent(), name) + } + + @Throws(IOException::class) + private fun writeTestInfo(testInfo: TestInfo?) { + if (testsWritten >= splitAfter) { + endReport() + testsWritten = 0 + startReport() + } + jsonGenerator!!.writeObject(testInfo) + testsWritten++ + } + + @Throws(IOException::class) + private fun endReport() { + jsonGenerator!!.writeEndArray() + jsonGenerator!!.writeEndObject() + jsonGenerator!!.close() + } +} 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 new file mode 100644 index 000000000..8b762aee2 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/CachingExecutionDataReader.kt @@ -0,0 +1,130 @@ +package com.teamscale.report.testwise.jacoco + +import com.teamscale.report.EDuplicateClassFileBehavior +import com.teamscale.report.jacoco.dump.Dump +import com.teamscale.report.testwise.jacoco.cache.AnalyzerCache +import com.teamscale.report.testwise.jacoco.cache.CoverageGenerationException +import com.teamscale.report.testwise.jacoco.cache.ProbesCache +import com.teamscale.report.testwise.model.builder.TestCoverageBuilder +import com.teamscale.report.util.ClasspathWildcardIncludeFilter +import com.teamscale.report.util.ILogger +import org.jacoco.core.data.ExecutionData +import org.jacoco.core.data.ExecutionDataStore +import java.io.File +import java.io.IOException +import java.util.function.Consumer +import java.util.stream.Collectors + +/** + * Helper class for analyzing class files, reading execution data and converting them to coverage data. + */ +open class CachingExecutionDataReader( + private val logger: ILogger, private val classesDirectories: Collection, + private val locationIncludeFilter: ClasspathWildcardIncludeFilter, + private val duplicateClassFileBehavior: EDuplicateClassFileBehavior +) { + private var probesCache: ProbesCache? = null + + /** + * Analyzes the class/jar/war/... files and creates a lookup of which probes belong to which method. + */ + fun analyzeClassDirs() { + if (probesCache == null) { + probesCache = ProbesCache(logger, duplicateClassFileBehavior) + } + if (classesDirectories.isEmpty()) { + logger.warn("No class directories found for caching.") + return + } + val analyzer = AnalyzerCache( + probesCache!!, + locationIncludeFilter, + logger + ) + var classCount = 0 + for (classDir in classesDirectories) { + if (classDir.exists()) { + try { + classCount += analyzer.analyzeAll(classDir) + } catch (e: IOException) { + 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 + ) + } + } + } + if (classCount == 0) { + val directoryList: String = classesDirectories.stream().map { obj: File -> obj.path }.collect( + Collectors.joining(",") + ) + logger.error("No class files found in the given directories! $directoryList") + } else if (probesCache?.isEmpty == true) { + val directoryList: String = classesDirectories.stream().map { obj: File -> obj.path }.collect( + Collectors.joining(",") + ) + logger.error( + "None of the $classCount class files found in the given directories match the configured include/exclude patterns! $directoryList" + ) + } + } + + /** + * Converts the given store to coverage data. The coverage will only contain line range coverage information. + */ + fun buildCoverageConsumer( + locationIncludeFilter: ClasspathWildcardIncludeFilter, + nextConsumer: Consumer + ) = DumpConsumer(logger, locationIncludeFilter, nextConsumer) + + /** + * Consumer of [Dump] objects. Converts them to [TestCoverageBuilder] and passes them to the + * nextConsumer. + */ + inner class DumpConsumer( + /** The logger. */ + private val logger: ILogger, + /** The location include filter to be applied on the profiled classes. */ + private val locationIncludeFilter: ClasspathWildcardIncludeFilter, + /** Consumer that should be called with the newly built TestCoverageBuilder. */ + private val nextConsumer: Consumer + ) : Consumer { + override fun accept(dump: Dump) { + val testId = dump.info.id + if (testId.isEmpty()) { + // Ignore intermediate coverage that does not belong to any specific test + logger.debug( + "Found a session with empty name! This could indicate that coverage is dumped also for " + + "coverage in between tests or that the given test name was empty!" + ) + return + } + try { + val testCoverage: TestCoverageBuilder = buildCoverage( + testId, dump.store, + locationIncludeFilter + ) + nextConsumer.accept(testCoverage) + } catch (e: CoverageGenerationException) { + logger.error("Failed to generate coverage for test $testId! Skipping to the next test.", e) + } + } + + /** + * Converts the given store to coverage data. The coverage will only contain line range coverage information. + */ + @Throws(CoverageGenerationException::class) + private fun buildCoverage( + testId: String, executionDataStore: ExecutionDataStore, + locationIncludeFilter: ClasspathWildcardIncludeFilter + ): TestCoverageBuilder { + val testCoverage = TestCoverageBuilder(testId) + for (executionData in executionDataStore.contents) { + testCoverage.add(probesCache!!.getCoverage(executionData, locationIncludeFilter)) + } + probesCache!!.flushLogger() + return testCoverage + } + } +} 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 new file mode 100644 index 000000000..5119bfa2e --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/JaCoCoTestwiseReportGenerator.kt @@ -0,0 +1,113 @@ +package com.teamscale.report.testwise.jacoco + +import com.teamscale.report.EDuplicateClassFileBehavior +import com.teamscale.report.jacoco.dump.Dump +import com.teamscale.report.testwise.jacoco.cache.CoverageGenerationException +import com.teamscale.report.testwise.model.TestwiseCoverage +import com.teamscale.report.testwise.model.builder.TestCoverageBuilder +import com.teamscale.report.util.ClasspathWildcardIncludeFilter +import com.teamscale.report.util.ILogger +import org.jacoco.core.data.ExecutionData +import org.jacoco.core.data.ExecutionDataReader +import org.jacoco.core.data.ExecutionDataStore +import org.jacoco.core.data.IExecutionDataVisitor +import org.jacoco.core.data.ISessionInfoVisitor +import org.jacoco.core.data.SessionInfo +import java.io.BufferedInputStream +import java.io.File +import java.io.FileInputStream +import java.io.IOException +import java.io.InputStream +import java.util.function.Consumer + +/** + * Creates an XML report for an execution data store. The report is grouped by session. + * + * The class files under test must be compiled with debug information otherwise no coverage will be collected. + */ +open class JaCoCoTestwiseReportGenerator( + codeDirectoriesOrArchives: Collection, + private val locationIncludeFilter: ClasspathWildcardIncludeFilter, + duplicateClassFileBehavior: EDuplicateClassFileBehavior, + logger: ILogger +) { + + /** The execution data reader and converter. */ + private val executionDataReader: CachingExecutionDataReader = CachingExecutionDataReader( + logger, codeDirectoriesOrArchives, locationIncludeFilter, duplicateClassFileBehavior + ) + + init { + updateClassDirCache() + } + + /** Updates the probe cache of the [ExecutionDataReader]. */ + open fun updateClassDirCache() { + executionDataReader.analyzeClassDirs() + } + + /** Converts the given dumps to a report. */ + @Throws(IOException::class, CoverageGenerationException::class) + open fun convert(executionDataFile: File): TestwiseCoverage { + val testwiseCoverage = TestwiseCoverage() + val dumpConsumer = executionDataReader.buildCoverageConsumer(locationIncludeFilter, testwiseCoverage::add) + readAndConsumeDumps(executionDataFile, dumpConsumer) + return testwiseCoverage + } + + /** Converts the given dump to a report. */ + @Throws(CoverageGenerationException::class) + open fun convert(dump: Dump): TestCoverageBuilder? { + val list = mutableListOf() + val dumpConsumer = executionDataReader.buildCoverageConsumer(locationIncludeFilter, list::add) + dumpConsumer.accept(dump) + return if (list.size == 1) list[0] else null + } + + /** Converts the given dumps to a report. */ + @Throws(IOException::class) + open fun convertAndConsume(executionDataFile: File, consumer: Consumer) { + val dumpConsumer = executionDataReader.buildCoverageConsumer(locationIncludeFilter, consumer) + readAndConsumeDumps(executionDataFile, dumpConsumer) + } + + /** Reads the dumps from the given *.exec file. */ + @Throws(IOException::class) + private fun readAndConsumeDumps(executionDataFile: File, dumpConsumer: Consumer) { + BufferedInputStream(FileInputStream(executionDataFile)).use { input -> + val executionDataReader = ExecutionDataReader(input) + val dumpCallback = DumpCallback(dumpConsumer) + executionDataReader.setExecutionDataVisitor(dumpCallback) + executionDataReader.setSessionInfoVisitor(dumpCallback) + executionDataReader.read() + dumpCallback.processDump() // Ensure that the last read dump is also consumed + } + } + + /** Collects execution information per session and passes it to the consumer . */ + private class DumpCallback(private val dumpConsumer: Consumer) : IExecutionDataVisitor, ISessionInfoVisitor { + + /** The dump that is currently being read. */ + private var currentDump: Dump? = null + + /** The store to which coverage is currently written to. */ + private var store: ExecutionDataStore? = null + + override fun visitSessionInfo(info: SessionInfo) { + processDump() + store = ExecutionDataStore() + currentDump = Dump(info, store!!) + } + + override fun visitClassExecution(data: ExecutionData) { + store?.put(data) + } + + fun processDump() { + currentDump?.let { + dumpConsumer.accept(it) + currentDump = 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 new file mode 100644 index 000000000..1fb4bffcb --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/AnalyzerCache.kt @@ -0,0 +1,75 @@ +package com.teamscale.report.testwise.jacoco.cache + +import com.teamscale.report.jacoco.FilteringAnalyzer +import com.teamscale.report.util.ClasspathWildcardIncludeFilter +import com.teamscale.report.util.ILogger +import org.jacoco.core.internal.analysis.CachingClassAnalyzer +import org.jacoco.core.internal.analysis.ClassCoverageImpl +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 +import java.nio.file.Paths + +/** + * An [AnalyzerCache] instance processes a set of Java class/jar/war/... files and builds a [ ] for each of the classes. + * + * + * For every class that gets found [.analyzeClass] is called. A class is identified by its class ID which + * is a CRC64 checksum of the classfile. We process each class with [CachingClassAnalyzer] to fill a [ ]. + */ +class AnalyzerCache +/** Creates a new analyzer filling the given cache. */( + /** The probes cache. */ + private val probesCache: ProbesCache, locationIncludeFilter: ClasspathWildcardIncludeFilter, + logger: ILogger +) : FilteringAnalyzer(null, null, locationIncludeFilter, logger) { + private val stringPool: StringPool = StringPool() + + /** + * Analyses the given class. Instead of the original implementation in [Analyzer.analyzeClass] we + * don't use concrete execution data, but instead build a probe cache to speed up repeated lookups. + */ + override fun analyzeClass(source: ByteArray) { + val classId: Long = CRC64.classId(source) + if (probesCache.containsClassId(classId)) { + return + } + val reader: ClassReader = InstrSupport.classReaderFor(source) + val classCoverageLookup: ClassCoverageLookup = probesCache.createClass(classId, reader.getClassName()) + + // Dummy class coverage object that allows us to subclass ClassAnalyzer with CachingClassAnalyzer and reuse its + // IFilterContext implementation + val dummyClassCoverage: ClassCoverageImpl = ClassCoverageImpl( + reader.getClassName(), + classId, false + ) + + val classAnalyzer: CachingClassAnalyzer = CachingClassAnalyzer( + classCoverageLookup, dummyClassCoverage, + stringPool + ) + val visitor: ClassVisitor = ClassProbesAdapter(classAnalyzer, false) + reader.accept(visitor, 0) + } + + /** + * Adds caching for jar files to the analyze jar functionality. + */ + @Throws(IOException::class) + override fun analyzeJar(input: InputStream, location: String): Int { + val jarId: Long = CRC64.classId(Files.readAllBytes(Paths.get(location))) + val probesCountForJarId: Int = probesCache.countForJarId(jarId) + if (probesCountForJarId != 0) { + return probesCountForJarId + } + val count: Int = super.analyzeJar(input, location) + probesCache.addJarId(jarId, count) + return count + } +} 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 new file mode 100644 index 000000000..4c8dcc5a8 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/ClassCoverageLookup.kt @@ -0,0 +1,136 @@ +package com.teamscale.report.testwise.jacoco.cache + +import com.teamscale.client.StringUtils +import com.teamscale.report.testwise.model.builder.FileCoverageBuilder +import com.teamscale.report.util.ILogger +import com.teamscale.report.util.SortedIntList +import org.jacoco.core.data.ExecutionData + +/** + * Holds information about a class' probes and to which line ranges they refer. + * + * + * + * * Create an instance of this class for every analyzed java class. + * * Set the file name of the java source file from which the class has been created. + * * Then call [.addProbe] for all probes and lines that belong to that probe. + * * Afterwards call [.getFileCoverage] to transform probes ([ ]) for this class into covered lines ([FileCoverageBuilder]). + * + */ +class ClassCoverageLookup +/** + * Constructor. + * + * @param className Classname as stored in the bytecode e.g. com/company/Example + */ internal constructor( + /** Fully qualified name of the class (with / as separators). */ + private val className: String +) { + /** Name of the java source file. */ + private var sourceFileName: String? = null + + /** + * Mapping from probe IDs to sets of covered lines. The index in this list corresponds to the probe ID. + */ + private val probes: MutableList = ArrayList() + + /** Sets the file name of the currently analyzed class (without path). */ + fun setSourceFileName(sourceFileName: String?) { + this.sourceFileName = sourceFileName + } + + /** Adjusts the size of the probes list to the total probes count. */ + fun setTotalProbeCount(count: Int) { + ensureArraySize(count - 1) + } + + /** Adds the probe with the given id to the method. */ + fun addProbe(probeId: Int, lines: SortedIntList?) { + ensureArraySize(probeId) + probes.set(probeId, lines) + } + + /** + * Ensures that the probes list is big enough to allow access to the given index. Intermediate list entries are + * filled with null. + */ + private fun ensureArraySize(index: Int) { + while (index >= probes.size) { + probes.add(null) + } + } + + /** + * Generates [FileCoverageBuilder] from an [ExecutionData]. [ExecutionData] holds coverage of + * exactly one class (whereby inner classes are a separate class). This method returns a [FileCoverageBuilder] + * object which is later merged with the [FileCoverageBuilder] of other classes that reside in the same file. + */ + @Throws(CoverageGenerationException::class) + fun getFileCoverage( + executionData: ExecutionData, + logger: ILogger + ): FileCoverageBuilder? { + val executedProbes: BooleanArray = executionData.getProbes() + + if (checkProbeInvariant(executedProbes)) { + 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." + ) + } + if (sourceFileName == null) { + logger.warn( + "No source file name found for class " + className + "! This class was probably not compiled with " + + "debug information enabled!" + ) + return null + } + + // we model the default package as the empty string + var packageName: String = "" + if (className.contains("/")) { + packageName = StringUtils.removeLastPart(className, '/') + } + val fileCoverage: FileCoverageBuilder = FileCoverageBuilder( + packageName, + sourceFileName!! + ) + fillFileCoverage(fileCoverage, executedProbes, logger) + + return fileCoverage + } + + private fun fillFileCoverage(fileCoverage: FileCoverageBuilder, executedProbes: BooleanArray, logger: ILogger) { + for (i in probes.indices) { + val coveredLines: SortedIntList? = probes.get(i) + if (!executedProbes.get(i)) { + continue + } + // coveredLines is null if the probe is outside of a method + // Happens e.g. for methods generated by Lombok + if (coveredLines == null) { + logger.info( + sourceFileName + " " + className + " did contain a covered probe " + i + "(of " + + executedProbes.size + ") that could not be " + + "matched to any method. This could be a bug in the profiler tooling. Please report it back " + + "to CQSE." + ) + continue + } + if (coveredLines.isEmpty) { + logger.debug( + sourceFileName + " " + className + " did contain a method with no line information. " + + "Does the class contain debug information?" + ) + continue + } + fileCoverage.addLines(coveredLines) + } + } + + /** Checks that the executed probes is not smaller than the cached probes. */ + private fun checkProbeInvariant(executedProbes: BooleanArray): Boolean { + return probes.size > executedProbes.size + } +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/ClassNotFoundLogger.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/ClassNotFoundLogger.kt new file mode 100644 index 000000000..468363bd9 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/ClassNotFoundLogger.kt @@ -0,0 +1,46 @@ +package com.teamscale.report.testwise.jacoco.cache + +import com.teamscale.report.util.ILogger + +/** + * Coordinates logging of missing class files to ensure the warnings are only emitted once and not for every individual + * test. + */ +/* package */ +internal class ClassNotFoundLogger +/** Constructor */ /* package */( + /** The logger. */ + private val logger: ILogger +) { + /** Missing classes that will be logged when [.flush] is called. */ + private val classesToBeLogged: MutableSet = HashSet() + + /** Classes that have already been reported as missing. */ + private val alreadyLoggedClasses: MutableSet = HashSet() + + /** Saves the given class to be logged later on. Ensures that the class is only logged once. */ /* package */ + fun log(fullyQualifiedClassName: String) { + if (!alreadyLoggedClasses.contains(fullyQualifiedClassName)) { + classesToBeLogged.add(fullyQualifiedClassName) + } + } + + /** Writes a summary of the missing class files to the logger. */ /* package */ + fun flush() { + if (classesToBeLogged.isEmpty()) { + return + } + + logger.warn( + "Found coverage for " + classesToBeLogged + .size + " classes that were not provided. Either you did not provide " + + "all relevant class files or you did not adjust the include/exclude filters on the agent to exclude " + + "coverage from irrelevant code. The classes are:" + ) + for (fullyQualifiedClassName: String in classesToBeLogged) { + logger.warn(" - " + fullyQualifiedClassName) + } + alreadyLoggedClasses.addAll(classesToBeLogged) + classesToBeLogged.clear() + } +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/CoverageGenerationException.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/CoverageGenerationException.kt new file mode 100644 index 000000000..052064996 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/CoverageGenerationException.kt @@ -0,0 +1,8 @@ +package com.teamscale.report.testwise.jacoco.cache + +/** + * Exception thrown during coverage generation. + */ +class CoverageGenerationException +/** Constructor. */ + (message: String?) : Exception(message) diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/ProbesCache.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/ProbesCache.kt new file mode 100644 index 000000000..930a31f57 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/jacoco/cache/ProbesCache.kt @@ -0,0 +1,107 @@ +package com.teamscale.report.testwise.jacoco.cache + +import com.teamscale.report.EDuplicateClassFileBehavior +import com.teamscale.report.testwise.model.builder.FileCoverageBuilder +import com.teamscale.report.util.ClasspathWildcardIncludeFilter +import com.teamscale.report.util.ILogger +import org.jacoco.core.data.ExecutionData +import org.jacoco.report.JavaNames + +/** + * Holds [ClassCoverageLookup]s for all analyzed classes. + */ +class ProbesCache( + /** The logger. */ + private val logger: ILogger, + /** Whether to ignore non-identical duplicates of class files. */ + private val duplicateClassFileBehavior: EDuplicateClassFileBehavior +) { + /** A mapping from class ID (CRC64 of the class file) to [ClassCoverageLookup]. */ + private val classCoverageLookups: HashMap = HashMap() + + /** Holds all fully-qualified class names that are already contained in the cache. */ + private val containedClasses: MutableSet = HashSet() + + private val containedJars: MutableMap = HashMap() + + private val classNotFoundLogger: ClassNotFoundLogger + + /** Constructor. */ + init { + this.classNotFoundLogger = ClassNotFoundLogger(logger) + } + + /** Adds a new class entry to the cache and returns its [ClassCoverageLookup]. */ + fun createClass(classId: Long, className: String): ClassCoverageLookup { + if (containedClasses.contains(className)) { + if (duplicateClassFileBehavior != EDuplicateClassFileBehavior.IGNORE) { + logger.warn( + ("Non-identical class file for class " + className + "." + + " 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.") + ) + } + check(duplicateClassFileBehavior != EDuplicateClassFileBehavior.FAIL) { "Found non-identical class file for class " + className + ". See logs for more details." } + } + containedClasses.add(className) + val classCoverageLookup: ClassCoverageLookup = ClassCoverageLookup(className) + classCoverageLookups.put(classId, classCoverageLookup) + return classCoverageLookup + } + + /** Returns whether a class with the given class ID has already been analyzed. */ + fun containsClassId(classId: Long): Boolean { + return classCoverageLookups.containsKey(classId) + } + + /** + * Returns the number of found class files in a cached jar file. Otherwise 0. + */ + fun countForJarId(jarId: Long): Int { + return containedJars.getOrDefault(jarId, 0) + } + + /** + * Adds a jar id along with the count of class files found in the jar. + */ + fun addJarId(jarId: Long, count: Int) { + containedJars.put(jarId, count) + } + + /** + * Converts the given [ExecutionData] to [FileCoverageBuilder] using the cached lookups or null if the + * class file of this class has not been included in the analysis or was not covered. + */ + @Throws(CoverageGenerationException::class) + fun getCoverage( + executionData: ExecutionData, + locationIncludeFilter: ClasspathWildcardIncludeFilter + ): FileCoverageBuilder? { + val classId: Long = executionData.getId() + if (!containsClassId(classId)) { + val fullyQualifiedClassName: String = JavaNames().getQualifiedClassName(executionData.getName()) + if (locationIncludeFilter.isIncluded(fullyQualifiedClassName + ".class")) { + classNotFoundLogger.log(fullyQualifiedClassName) + } + return null + } + if (!executionData.hasHits()) { + return null + } + + return classCoverageLookups.get(classId)!!.getFileCoverage(executionData, logger) + } + + val isEmpty: Boolean + /** Returns true if the cache does not contain coverage for any class. */ + get() { + return classCoverageLookups.isEmpty() + } + + /** Prints a the collected class not found messages. */ + fun flushLogger() { + classNotFoundLogger.flush() + } +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/ERevisionType.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/ERevisionType.kt new file mode 100644 index 000000000..566aa148a --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/ERevisionType.kt @@ -0,0 +1,10 @@ +package com.teamscale.report.testwise.model + +/** Type of revision information. */ +enum class ERevisionType { + /** Commit descriptor in the format branch:timestamp. */ + COMMIT, + + /** Source control revision, e.g. SVN revision or Git hash. */ + REVISION +} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/model/ETestExecutionResult.java b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/ETestExecutionResult.kt similarity index 82% rename from report-generator/src/main/java/com/teamscale/report/testwise/model/ETestExecutionResult.java rename to report-generator/src/main/kotlin/com/teamscale/report/testwise/model/ETestExecutionResult.kt index 034082081..1c9497ccf 100644 --- a/report-generator/src/main/java/com/teamscale/report/testwise/model/ETestExecutionResult.java +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/ETestExecutionResult.kt @@ -15,23 +15,22 @@ | limitations under the License. | | | +-------------------------------------------------------------------------*/ -package com.teamscale.report.testwise.model; +package com.teamscale.report.testwise.model -/** The result of a test execution. */ -public enum ETestExecutionResult { - - /** Test execution was successful. */ +/** The result of a test execution. */ +enum class ETestExecutionResult { + /** Test execution was successful. */ PASSED, - /** The test is currently marked as "do not execute" (e.g. JUnit @Ignore). */ + /** The test is currently marked as "do not execute" (e.g. JUnit @Ignore). */ IGNORED, - /** Caused by a failing assumption. */ + /** Caused by a failing assumption. */ SKIPPED, - /** Caused by a failing assertion. */ + /** Caused by a failing assertion. */ FAILURE, - /** Caused by an error during test execution (e.g. exception thrown in the test runner code, not the test itself). */ + /** Caused by an error during test execution (e.g. exception thrown in the test runner code, not the test itself). */ ERROR } diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/FileCoverage.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/FileCoverage.kt new file mode 100644 index 000000000..68d8ddd1d --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/FileCoverage.kt @@ -0,0 +1,12 @@ +package com.teamscale.report.testwise.model + +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty + +/** Holds coverage of a single file. */ +class FileCoverage @JsonCreator constructor( + /** The name of the file. */ + @JvmField @param:JsonProperty("fileName") val fileName: String, + /** A list of line ranges that have been covered. */ + @JvmField @param:JsonProperty("coveredLines") val coveredLines: String +) diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/LineRange.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/LineRange.kt new file mode 100644 index 000000000..6e95ac2e8 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/LineRange.kt @@ -0,0 +1,31 @@ +package com.teamscale.report.testwise.model + +/** Holds a line range with start and end (both inclusive and 1-based). */ +class LineRange +/** Constructor. */( + /** The start line (1-based). */ + private val start: Int, + /** The end line (1-based). */ + var end: Int +) { + /** @see .end + */ + /** @see .end + */ + + /** + * Returns the line range as used in the XML report. + * A range is returned as e.g. 2-5 or simply 3 if the start and end are equal. + */ + fun toReportString(): String { + if (start == end) { + return start.toString() + } else { + return start.toString() + "-" + end + } + } + + override fun toString(): String { + return toReportString() + } +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/PathCoverage.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/PathCoverage.kt new file mode 100644 index 000000000..0f87fdeb7 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/PathCoverage.kt @@ -0,0 +1,12 @@ +package com.teamscale.report.testwise.model + +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty + +/** Container for [FileCoverage]s of the same path. */ +class PathCoverage @JsonCreator constructor( + /** File system path. */ + @param:JsonProperty("path") val path: String?, + /** Files with coverage. */ + @JvmField @param:JsonProperty("files") val files: List +) diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/RevisionInfo.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/RevisionInfo.kt new file mode 100644 index 000000000..6e760e6dc --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/RevisionInfo.kt @@ -0,0 +1,51 @@ +package com.teamscale.report.testwise.model + +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty +import com.teamscale.client.CommitDescriptor +import java.io.Serializable + +/** Revision information necessary for uploading reports to Teamscale. */ +class RevisionInfo : Serializable { + /** The type of revision information. */ + val type: ERevisionType + + /** The value. Either a commit descriptor or a source control revision, depending on [.type]. */ + val value: String? + + @JsonCreator + constructor(@JsonProperty("type") type: ERevisionType, @JsonProperty("value") value: String) { + this.type = type + this.value = value + } + + /** Constructor for Commit. */ + constructor(commit: CommitDescriptor) { + type = ERevisionType.COMMIT + value = commit.toString() + } + + /** Constructor for Revision. */ + constructor(revision: String) { + type = ERevisionType.REVISION + value = revision + } + + /** + * Constructor in case you have both fields, and either may be null. If both are set, the commit wins. If both are + * null, [.type] will be [ERevisionType.REVISION] and [.value] will be null. + */ + constructor(commit: CommitDescriptor?, revision: String?) { + if (commit == null) { + type = ERevisionType.REVISION + value = revision + } else { + type = ERevisionType.COMMIT + value = commit.toString() + } + } + + companion object { + private const val serialVersionUID: Long = 1L + } +} diff --git a/report-generator/src/main/java/com/teamscale/report/testwise/model/TestExecution.java b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/TestExecution.kt similarity index 51% rename from report-generator/src/main/java/com/teamscale/report/testwise/model/TestExecution.java rename to report-generator/src/main/kotlin/com/teamscale/report/testwise/model/TestExecution.kt index 6f6409177..f69c90ae7 100644 --- a/report-generator/src/main/java/com/teamscale/report/testwise/model/TestExecution.java +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/TestExecution.kt @@ -15,115 +15,96 @@ | limitations under the License. | | | +-------------------------------------------------------------------------*/ -package com.teamscale.report.testwise.model; +package com.teamscale.report.testwise.model -import com.fasterxml.jackson.annotation.JsonAlias; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.io.Serializable; - -/** Representation of a single test (method) execution. */ -public class TestExecution implements Serializable { - - private static final long serialVersionUID = 1L; +import com.fasterxml.jackson.annotation.JsonAlias +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty +import java.io.Serializable +/** Representation of a single test (method) execution. */ +class TestExecution : Serializable { + /** @see .uniformPath + */ + /** @see .uniformPath + */ /** * The uniform path of the test (method) that was executed. This is an absolute (i.e. hierarchical) reference which * identifies the test uniquely in the scope of the Teamscale project. It may (but is not required to) correspond to * the path of some automated test case source code known to Teamscale. If the test was parameterized, this path is * expected to reflect the parameter in some manner. */ - private String uniformPath; + @JvmField + var uniformPath: String? = null - /** Duration of the execution in milliseconds. */ - @Deprecated - private long durationMillis; + /** Duration of the execution in milliseconds. */ + @Deprecated("") + private var durationMillis: Long = 0 - /** Duration of the execution in seconds. */ + /** Duration of the execution in seconds. */ @JsonProperty("duration") @JsonAlias("durationSeconds") - private Double duration; + private val duration: Double? = null - /** The actual execution result state. */ - private ETestExecutionResult result; + /** @see .result + */ + /** @see .result + */ + /** The actual execution result state. */ + @JvmField + var result: ETestExecutionResult? = null + /** @see .message + */ + /** @see .message + */ /** - * Optional message given for test failures (normally contains a stack trace). May be {@code null}. + * Optional message given for test failures (normally contains a stack trace). May be `null`. */ - private String message; + var message: String? = null /** * Needed for Jackson deserialization. */ @JsonCreator - public TestExecution() { - // Needed for Jackson - } - - public TestExecution(String name, long durationMillis, ETestExecutionResult result) { - this(name, durationMillis, result, null); + constructor() + + @JvmOverloads + constructor(name: String?, durationMillis: Long, result: ETestExecutionResult?, message: String? = null) { + this.uniformPath = name + this.durationMillis = durationMillis + this.result = result + this.message = message } - public TestExecution(String name, long durationMillis, ETestExecutionResult result, String message) { - this.uniformPath = name; - this.durationMillis = durationMillis; - this.result = result; - this.message = message; - } - - /** @see #durationMillis */ - public double getDurationSeconds() { - if (duration != null) { - return duration; - } else { - return durationMillis / 1000.0; + val durationSeconds: Double + /** @see .durationMillis + */ + get() { + if (duration != null) { + return duration + } else { + return durationMillis / 1000.0 + } } - } - /** @see #result */ - public ETestExecutionResult getResult() { - return result; - } - - /** @see #message */ - public String getMessage() { - return message; - } - - /** @see #uniformPath */ - public String getUniformPath() { - return uniformPath; - } - - /** @see #uniformPath */ - public void setUniformPath(String uniformPath) { - this.uniformPath = uniformPath; - } - - /** @see #durationMillis */ - public void setDurationMillis(long durationMillis) { - this.durationMillis = durationMillis; - } - - /** @see #result */ - public void setResult(ETestExecutionResult result) { - this.result = result; - } - - /** @see #message */ - public void setMessage(String message) { - this.message = message; + /** @see .durationMillis + */ + fun setDurationMillis(durationMillis: Long) { + this.durationMillis = durationMillis } - @Override - public String toString() { + override fun toString(): String { return "TestExecution{" + "uniformPath='" + uniformPath + '\'' + ", durationMillis=" + durationMillis + ", duration=" + duration + ", result=" + result + ", message='" + message + '\'' + - '}'; + '}' + } + + companion object { + private const val serialVersionUID: Long = 1L } } diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/TestInfo.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/TestInfo.kt new file mode 100644 index 000000000..922736f0f --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/TestInfo.kt @@ -0,0 +1,33 @@ +package com.teamscale.report.testwise.model + +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty + +/** Generic container of all information about a specific test as written to the report. */ +class TestInfo @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") val 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 class name of the most specific subclass, from where it was actually executed. + */ + @param:JsonProperty("sourcePath") val 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. + */ + @param:JsonProperty("content") val content: String?, + /** Duration of the execution in seconds. */ + @param:JsonProperty("duration") val duration: Double?, + /** The actual execution result state. */ + @JvmField @param:JsonProperty("result") val result: ETestExecutionResult?, + /** + * Optional message given for test failures (normally contains a stack trace). May be `null`. + */ + @param:JsonProperty("message") val message: String? +) { + /** All paths that the test did cover. */ + @JvmField + val paths: MutableList = ArrayList() +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/TestwiseCoverage.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/TestwiseCoverage.kt new file mode 100644 index 000000000..94833f65d --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/TestwiseCoverage.kt @@ -0,0 +1,37 @@ +package com.teamscale.report.testwise.model + +import com.teamscale.report.testwise.model.builder.TestCoverageBuilder + +/** Container for coverage produced by multiple tests. */ +class TestwiseCoverage { + /** A mapping from test ID to [TestCoverageBuilder]. */ + val tests: MutableMap = HashMap() + + /** + * Adds the [TestCoverageBuilder] to the map. + * If there is already a test with the same ID the coverage is merged. + */ + fun add(coverage: TestCoverageBuilder?) { + if (coverage == null || coverage.isEmpty) { + return + } + if (tests.containsKey(coverage.uniformPath)) { + val testCoverage: TestCoverageBuilder? = tests.get(coverage.uniformPath) + testCoverage!!.addAll(coverage.files) + } else { + tests.put(coverage.uniformPath, coverage) + } + } + + /** + * Merges the given [TestwiseCoverage] with this one. + */ + fun add(testwiseCoverage: TestwiseCoverage?) { + if (testwiseCoverage == null) { + return + } + for (value: TestCoverageBuilder? in testwiseCoverage.tests.values) { + this.add(value) + } + } +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/TestwiseCoverageReport.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/TestwiseCoverageReport.kt new file mode 100644 index 000000000..3f98b7cdf --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/TestwiseCoverageReport.kt @@ -0,0 +1,18 @@ +package com.teamscale.report.testwise.model + +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty + +/** Container for coverage produced by multiple tests. */ +class TestwiseCoverageReport @JsonCreator constructor( + /** + * If set to `true` the set of tests contained in the report don't represent the full set of tests within a + * partition. These tests are added or updated in Teamscale, but no tests or executable units that are missing in + * the report will be deleted. + */ + @JvmField @param:JsonProperty("partial") val partial: Boolean +) { + /** The tests contained in the report. */ + @JvmField + val tests: MutableList = ArrayList() +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/FileCoverageBuilder.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/FileCoverageBuilder.kt new file mode 100644 index 000000000..b11ef738c --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/FileCoverageBuilder.kt @@ -0,0 +1,93 @@ +package com.teamscale.report.testwise.model.builder + +import com.teamscale.report.testwise.model.FileCoverage +import com.teamscale.report.testwise.model.LineRange +import com.teamscale.report.util.SortedIntList +import java.util.stream.Collectors + +/** Holds coverage of a single file. */ +class FileCoverageBuilder +/** Constructor. */( + /** The file system path of the file not including the file itself. */ + val path: String, + /** The name of the file. */ + val fileName: String +) { + /** + * A list of line numbers that have been covered. Using a set here is too memory intensive. + */ + private val coveredLines = SortedIntList() + + /** Adds a line as covered. */ + fun addLine(line: Int) { + coveredLines.add(line) + } + + /** Adds a line range as covered. */ + fun addLineRange(start: Int, end: Int) { + for (i in start..end) { + coveredLines.add(i) + } + } + + /** Adds set of lines as covered. */ + fun addLines(range: SortedIntList) { + coveredLines.addAll(range) + } + + /** Merges the list of ranges into the current list. */ + fun merge(other: FileCoverageBuilder) { + if (other.fileName != fileName || other.path != path) { + throw AssertionError("Cannot merge coverage of two different files! This is a bug!") + } + coveredLines.addAll(other.coveredLines) + } + + /** + * Returns a compact string representation of the covered lines. Continuous line ranges are merged to ranges and + * sorted. Individual ranges are separated by commas. E.g. 1-5,7,9-11. + */ + fun computeCompactifiedRangesAsString(): String { + val coveredRanges = compactifyToRanges(coveredLines) + return coveredRanges.stream().map { obj: LineRange -> obj.toReportString() }.collect(Collectors.joining(",")) + } + + val isEmpty: Boolean + /** Returns true if there is no coverage for the file yet. */ + get() = coveredLines.size() == 0 + + /** Builds the [FileCoverage] object, which is serialized into the report. */ + fun build(): FileCoverage { + return FileCoverage(fileName, computeCompactifiedRangesAsString()) + } + + companion object { + /** + * Merges all neighboring line numbers to ranges. E.g. a list of [[1-5],[3-7],[8-10],[12-14]] becomes + * [[1-10],[12-14]] + */ + @JvmStatic + fun compactifyToRanges(lines: SortedIntList): List { + if (lines.size() == 0) { + return ArrayList() + } + + val firstLine = lines[0] + var currentRange = LineRange(firstLine, firstLine) + + val compactifiedRanges: MutableList = ArrayList() + compactifiedRanges.add(currentRange) + + for (i in 0 until lines.size()) { + val currentLine = lines[i] + if (currentRange.end == currentLine || currentRange.end == currentLine - 1) { + currentRange.end = currentLine + } else { + currentRange = LineRange(currentLine, currentLine) + compactifiedRanges.add(currentRange) + } + } + return compactifiedRanges + } + } +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/PathCoverageBuilder.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/PathCoverageBuilder.kt new file mode 100644 index 000000000..9aac516b4 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/PathCoverageBuilder.kt @@ -0,0 +1,46 @@ +package com.teamscale.report.testwise.model.builder + +import com.teamscale.report.testwise.model.PathCoverage +import java.util.function.Function +import java.util.stream.Collectors + +/** Container for [FileCoverageBuilder]s of the same path. */ +class PathCoverageBuilder +/** Constructor. */( + /** File system path. */ + val path: String +) { + /** @see .path + */ + + /** Mapping from file names to [FileCoverageBuilder]. */ + private val fileCoverageList: MutableMap = HashMap() + + /** + * Adds the given [FileCoverageBuilder] to the container. + * If coverage for the same file already exists it gets merged. + */ + fun add(fileCoverage: FileCoverageBuilder) { + if (fileCoverageList.containsKey(fileCoverage.fileName)) { + val existingFile = fileCoverageList[fileCoverage.fileName] + existingFile!!.merge(fileCoverage) + } else { + fileCoverageList[fileCoverage.fileName] = fileCoverage + } + } + + val files: Collection + /** Returns a collection of [FileCoverageBuilder]s associated with this path. */ + get() = fileCoverageList.values + + /** Builds a [PathCoverage] object. */ + fun build(): PathCoverage { + val files = fileCoverageList.values.stream() + .sorted( + Comparator.comparing( + Function { obj: FileCoverageBuilder? -> obj!!.fileName }) + ) + .map { obj: FileCoverageBuilder? -> obj!!.build() }.collect(Collectors.toList()) + return PathCoverage(path, files) + } +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/TestCoverageBuilder.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/TestCoverageBuilder.kt new file mode 100644 index 000000000..f00f47966 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/TestCoverageBuilder.kt @@ -0,0 +1,54 @@ +package com.teamscale.report.testwise.model.builder + +import com.teamscale.report.testwise.model.PathCoverage +import java.util.function.Function +import java.util.stream.Collectors + +/** Generic holder of test coverage of a single test based on line-ranges. */ +class TestCoverageBuilder +/** Constructor. */( + /** The uniformPath of the test (see TEST_IMPACT_ANALYSIS_DOC.md for more information). */ + @JvmField val uniformPath: String +) { + /** @see .uniformPath + */ + + /** Mapping from path names to all files on this path. */ + private val pathCoverageList: MutableMap = HashMap() + + val paths: List + /** Returns a collection of [PathCoverageBuilder]s associated with the test. */ + get() = pathCoverageList.values.stream().sorted( + Comparator.comparing { obj -> obj.path } + ).map { obj: PathCoverageBuilder -> obj.build() }.collect(Collectors.toList()) + + /** Adds the [FileCoverageBuilder] to into the map, but filters out file coverage that is null or empty. */ + fun add(fileCoverage: FileCoverageBuilder?) { + if (fileCoverage == null || fileCoverage.isEmpty) { + return + } + val pathCoverage = pathCoverageList.computeIfAbsent(fileCoverage.path) { path -> PathCoverageBuilder(path) } + pathCoverage.add(fileCoverage) + } + + /** Adds the [FileCoverageBuilder]s into the map, but filters out empty ones. */ + fun addAll(fileCoverageList: List) { + for (fileCoverage: FileCoverageBuilder? in fileCoverageList) { + add(fileCoverage) + } + } + + val files: List + /** Returns all [FileCoverageBuilder]s stored for the test. */ + get() { + return pathCoverageList.values.stream() + .flatMap { path: PathCoverageBuilder -> path.files.stream() } + .collect(Collectors.toList()) + } + + val isEmpty: Boolean + /** Returns true if there is no coverage for the test yet. */ + get() { + return pathCoverageList.isEmpty() + } +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/TestInfoBuilder.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/TestInfoBuilder.kt new file mode 100644 index 000000000..0b73e6007 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/TestInfoBuilder.kt @@ -0,0 +1,81 @@ +package com.teamscale.report.testwise.model.builder + +import com.teamscale.client.TestDetails +import com.teamscale.report.testwise.model.ETestExecutionResult +import com.teamscale.report.testwise.model.TestExecution +import com.teamscale.report.testwise.model.TestInfo + +/** Generic container of all information about a specific test including details, execution info and coverage. */ +class TestInfoBuilder +/** Constructor. */ /* package */( + /** Unique name of the test case by using a path like hierarchical description, which can be shown in the UI. */ + val uniformPath: String +) { + /** @see .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. + */ + private var sourcePath: String? = null + + /** + * Some kind of content to tell whether the test specification has changed. Can be revision number or hash over the + * specification or similar. + */ + private var content: String? = null + + /** Duration of the execution in milliseconds. */ + private var durationSeconds: Double? = null + + /** The actual execution result state. */ + private var result: ETestExecutionResult? = null + + /** + * Optional message given for test failures (normally contains a stack trace). May be `null`. + */ + private var message: String? = null + + /** Coverage generated by this test. */ + private var coverage: TestCoverageBuilder? = null + + val isEmpty: Boolean + /** Returns true if there is no coverage for the test yet. */ + get() = coverage?.isEmpty == true + + /** Sets the test details fields. */ + fun setDetails(details: TestDetails?) { + if (details != null) { + sourcePath = details.sourcePath + content = details.content + } + } + + /** Sets the test execution fields. */ + fun setExecution(execution: TestExecution?) { + if (execution != null) { + durationSeconds = execution.durationSeconds + result = execution.result + message = execution.message + } + } + + /** @see .coverage + */ + fun setCoverage(coverage: TestCoverageBuilder?) { + this.coverage = coverage + } + + /** Builds a [TestInfo] object of the data in this container. */ + fun build(): TestInfo { + val testInfo = TestInfo( + uniformPath, sourcePath, content, durationSeconds, result, message + ) + coverage?.let { + testInfo.paths.addAll(it.paths) + } + return testInfo + } +} 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 new file mode 100644 index 000000000..cb37da52f --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/TestwiseCoverageReportBuilder.kt @@ -0,0 +1,73 @@ +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 { + /** A mapping from test ID to [TestCoverageBuilder]. */ + private val tests = mutableMapOf() + + private fun build(partial: Boolean): TestwiseCoverageReport { + val report = TestwiseCoverageReport(partial) + tests.values + .sortedBy { it.uniformPath } + .map { it.build() } + .forEach { report.tests.add(it) } + return report + } + + companion object { + /** + * Adds the [TestCoverageBuilder] to the map. If there is already a test with the same ID the coverage is + * merged. + */ + @JvmStatic + fun createFrom( + testDetailsList: Collection, + testCoverage: Collection, + testExecutions: Collection, + partial: Boolean + ): TestwiseCoverageReport { + val report = TestwiseCoverageReportBuilder() + for (testDetails: TestDetails in testDetailsList) { + val container = TestInfoBuilder(testDetails.uniformPath) + container.setDetails(testDetails) + report.tests[testDetails.uniformPath] = container + } + for (coverage: TestCoverageBuilder in testCoverage) { + val container = resolveUniformPath(report, coverage.uniformPath) ?: continue + container.setCoverage(coverage) + } + for (testExecution: TestExecution in testExecutions) { + val path = testExecution.uniformPath ?: continue + val container = resolveUniformPath(report, path) ?: continue + container.setExecution(testExecution) + } + return report.build(partial) + } + + private fun resolveUniformPath(report: TestwiseCoverageReportBuilder, uniformPath: String): TestInfoBuilder? { + val container = report.tests[uniformPath] + if (container != null) { + return container + } + val shortenedUniformPath: String = stripParameterizedTestArguments(uniformPath) + val testInfoBuilder = report.tests[shortenedUniformPath] + if (testInfoBuilder == null) { + System.err.println("No container found for test '$uniformPath'!") + } + return testInfoBuilder + } + + /** + * Removes parameterized test arguments from the given uniform path. + */ + fun stripParameterizedTestArguments(uniformPath: String): String { + return uniformPath.replaceFirst("(.*\\))\\[.*]".toRegex(), "$1") + } + } +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/factory/TestInfoFactory.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/factory/TestInfoFactory.kt new file mode 100644 index 000000000..531e47883 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/factory/TestInfoFactory.kt @@ -0,0 +1,93 @@ +package com.teamscale.report.testwise.model.factory + +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.builder.TestCoverageBuilder +import com.teamscale.report.testwise.model.builder.TestInfoBuilder +import com.teamscale.report.testwise.model.builder.TestwiseCoverageReportBuilder + +/** + * Factory class for converting [TestCoverageBuilder] to [TestInfo]s while augmenting them with information + * from test details and test executions. + */ +class TestInfoFactory(testDetails: List, testExecutions: List) { + /** Maps uniform paths to test details. */ + private val testDetailsMap: MutableMap = HashMap() + + /** Maps uniform paths to test executions. */ + private val testExecutionsMap: MutableMap = HashMap() + + /** Holds all uniform paths for tests that have been written to the outputFile. */ + private val processedTestUniformPaths: MutableSet = HashSet() + + init { + for (testDetail: TestDetails in testDetails) { + testDetailsMap.put(testDetail.uniformPath, testDetail) + } + for (testExecution: TestExecution in testExecutions) { + testExecutionsMap.put(testExecution.uniformPath, testExecution) + } + } + + /** + * Converts the given [TestCoverageBuilder] to a [TestInfo] using the internally stored test details and + * test executions. + */ + fun createFor(testCoverageBuilder: TestCoverageBuilder): TestInfo { + val resolvedUniformPath: String = resolveUniformPath(testCoverageBuilder.uniformPath) + processedTestUniformPaths.add(resolvedUniformPath) + + val container: TestInfoBuilder = TestInfoBuilder(resolvedUniformPath) + container.setCoverage(testCoverageBuilder) + val testDetails: TestDetails? = testDetailsMap.get(resolvedUniformPath) + if (testDetails == null) { + System.err.println("No test details found for " + resolvedUniformPath) + } + container.setDetails(testDetails) + val execution: TestExecution? = testExecutionsMap.get(resolvedUniformPath) + if (execution == null) { + System.err.println("No test execution found for " + resolvedUniformPath) + } + container.setExecution(execution) + return container.build() + } + + /** Returns [TestInfo]s for all tests that have not been used yet in [.createFor]. */ + fun createTestInfosWithoutCoverage(): List { + val results: ArrayList = ArrayList() + for (testDetails: TestDetails in testDetailsMap.values) { + if (!processedTestUniformPaths.contains(testDetails.uniformPath)) { + val testInfo: TestInfoBuilder = TestInfoBuilder(testDetails.uniformPath) + testInfo.setDetails(testDetails) + testInfo.setExecution(testExecutionsMap.get(testDetails.uniformPath)) + results.add(testInfo.build()) + processedTestUniformPaths.add(testDetails.uniformPath) + } + } + for (testExecution: TestExecution in testExecutionsMap.values) { + if (!processedTestUniformPaths.contains(testExecution.uniformPath)) { + System.err.println( + "Test " + testExecution.uniformPath + " was executed but no coverage was found. " + + "Please make sure that you did provide all relevant exec files and that the test IDs passed to " + + "the agent match the ones from the provided test execution list." + ) + processedTestUniformPaths.add(testExecution.uniformPath) + } + } + return results + } + + /** + * Strips parameterized test arguments when the full path given in the coverage file cannot be found in the test + * details. + */ + private fun resolveUniformPath(originalUniformPath: String?): String { + var uniformPath: String = originalUniformPath!! + val testDetails: TestDetails? = testDetailsMap.get(uniformPath) + if (testDetails == null) { + uniformPath = TestwiseCoverageReportBuilder.Companion.stripParameterizedTestArguments(uniformPath) + } + return uniformPath + } +} 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 new file mode 100644 index 000000000..bc2ff41fe --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/util/AntPatternIncludeFilter.kt @@ -0,0 +1,53 @@ +/*-------------------------------------------------------------------------+ +| | +| Copyright (c) 2009-2018 CQSE GmbH | +| | ++-------------------------------------------------------------------------*/ +package com.teamscale.report.util + +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. + */ +class AntPatternIncludeFilter(locationIncludeFilters: List, locationExcludeFilters: List) : + Predicate { + /** The include filters. Empty means include everything. */ + private val locationIncludeFilters: List = + locationIncludeFilters.stream().map { filter: String? -> AntPatternUtils.convertPattern(filter, false) } + .collect(Collectors.toList()) + + /** The exclude filters. Empty means exclude nothing. */ + private val locationExcludeFilters: List = + locationExcludeFilters.stream().map { filter: String? -> AntPatternUtils.convertPattern(filter, false) } + .collect( + Collectors.toList() + ) + + /** {@inheritDoc} */ + override fun test(path: String): Boolean { + return !isFiltered(FileSystemUtils.normalizeSeparators(path)) + } + + /** + * Returns `true` if the given class file location (normalized to forward slashes as path separators) + * should not be analyzed. + * + * + * Exclude filters overrule include filters. + */ + private fun isFiltered(location: String): Boolean { + // first check includes + if (!locationIncludeFilters.isEmpty() + && locationIncludeFilters.stream().noneMatch { filter: Pattern -> filter.matcher(location).matches() } + ) { + return true + } + // only if they match, check excludes + return locationExcludeFilters.stream().anyMatch { filter: Pattern -> filter.matcher(location).matches() } + } +} 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 new file mode 100644 index 000000000..1be8aa30c --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/util/BashFileSkippingInputStream.kt @@ -0,0 +1,46 @@ +package com.teamscale.report.util + +import java.io.BufferedInputStream +import java.io.FilterInputStream +import java.io.IOException +import java.io.InputStream + +/** + * Handles executable spring-boot jar files that prepend a bash file to the beginning of the ZIP file to make it + * directly executable without "java -jar my.jar". We just skip the bash file until we find the zip file header. + */ +class BashFileSkippingInputStream(`in`: InputStream) : FilterInputStream(BufferedInputStream(`in`)) { + /** + * Wraps the given input stream in a BufferedInputStream and consumes all bytes until a zip file header is found. + */ + init { + consumeUntilZipHeader() + } + + /** + * Reads the stream until the zip file header "50 4B 03 04" is found or EOF is reached. After calling the method the + * read pointer points to the first byte of the zip file header. + */ + @Throws(IOException::class) + private fun consumeUntilZipHeader() { + val buffer = ByteArray(8192) + `in`.mark(buffer.size) + var count = `in`.read(buffer, 0, buffer.size) + while (count > 0) { + for (i in 0 until count - 3) { + if (buffer[i].toInt() == 0x50 && buffer[i + 1].toInt() == 0x4B && buffer[i + 2].toInt() == 0x03 && buffer[i + 3].toInt() == 0x04) { + `in`.reset() + `in`.skip(i.toLong()) + return + } + } + + // Reset mark to 3 bytes before the end of the previously read buffer end to + // also detect a zip header when it spans over two buffers + `in`.reset() + `in`.skip((buffer.size - 3).toLong()) + `in`.mark(buffer.size) + count = `in`.read(buffer, 0, buffer.size) + } + } +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/util/ClasspathWildcardIncludeFilter.kt b/report-generator/src/main/kotlin/com/teamscale/report/util/ClasspathWildcardIncludeFilter.kt new file mode 100644 index 000000000..b97ba3679 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/util/ClasspathWildcardIncludeFilter.kt @@ -0,0 +1,77 @@ +package com.teamscale.report.util + +import com.teamscale.client.FileSystemUtils +import com.teamscale.client.StringUtils +import org.jacoco.core.runtime.WildcardMatcher +import org.jacoco.report.JavaNames +import sun.management.MonitorInfoCompositeData +import java.util.* + + +/*** + * Tests given class file paths against call name patterns. + * E.g. "/some/file/path/test.jar@my/package/Test.class" matches "my/package/ *" or "my/package/Test" + */ +open class ClasspathWildcardIncludeFilter(locationIncludeFilters: String?, locationExcludeFilters: String?) { + /** + * Include patterns to apply during JaCoCo's traversal of class files. If null then everything is included. + */ + private var locationIncludeFilters: WildcardMatcher? = null + + /** + * Exclude patterns to apply during JaCoCo's traversal of class files. If null then nothing is excluded. + */ + private var locationExcludeFilters: WildcardMatcher? = null + + /** + * Constructor. + * + * @param locationIncludeFilters Colon separated list of wildcard include patterns for fully qualified class names + * or null for no includes. See [WildcardMatcher] for the pattern syntax. + * @param locationExcludeFilters Colon separated list of wildcard exclude patterns for fully qualified class names + * or null for no excludes.See [WildcardMatcher] for the pattern syntax. + */ + init { + if (!locationIncludeFilters.isNullOrEmpty()) { + this.locationIncludeFilters = WildcardMatcher(locationIncludeFilters) + } + if (!locationExcludeFilters.isNullOrEmpty()) { + this.locationExcludeFilters = WildcardMatcher(locationExcludeFilters) + } + } + + /** + * Tests if the given file path (e.g. "/some/file/path/test.jar@my/package/Test.class" or "org/mypackage/MyClass" + */ + fun isIncluded(path: String?): Boolean { + val className = getClassName(path ?: "") + // first check includes + if (locationIncludeFilters != null && !locationIncludeFilters!!.matches(className)) { + return false + } + // if they match, check excludes + return locationExcludeFilters == null || !locationExcludeFilters!!.matches(className) + } + + + companion object { + /** + * Returns the normalized class name of the given class file's path. I.e. turns something like + * "/opt/deploy/some.jar@com/teamscale/Class.class" into something like "com.teamscale.Class". + */ + @JvmStatic + fun getClassName(path: String): String { + val parts = FileSystemUtils.normalizeSeparators(path).split("@".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() + if (parts.isEmpty()) { + return "" + } + + var pathInsideJar = parts[parts.size - 1] + if (path.lowercase(Locale.getDefault()).endsWith(".class")) { + pathInsideJar = StringUtils.removeLastPart(pathInsideJar, '.') + } + return JavaNames().getQualifiedClassName(pathInsideJar) + } + } +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/util/CommandLineLogger.kt b/report-generator/src/main/kotlin/com/teamscale/report/util/CommandLineLogger.kt new file mode 100644 index 000000000..84a13084d --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/util/CommandLineLogger.kt @@ -0,0 +1,30 @@ +package com.teamscale.report.util + +/** Logger that prints all output to the console. */ +class CommandLineLogger : ILogger { + override fun debug(message: String?) { + println(message) + } + + override fun info(message: String?) { + println(message) + } + + override fun warn(message: String?) { + System.err.println(message) + } + + override fun warn(message: String?, throwable: Throwable?) { + System.err.println(message) + throwable?.printStackTrace() + } + + override fun error(throwable: Throwable) { + throwable.printStackTrace() + } + + override fun error(message: String?, throwable: Throwable?) { + System.err.println(message) + throwable?.printStackTrace() + } +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/util/ILogger.kt b/report-generator/src/main/kotlin/com/teamscale/report/util/ILogger.kt new file mode 100644 index 000000000..657c0cab6 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/util/ILogger.kt @@ -0,0 +1,27 @@ +package com.teamscale.report.util + +/** + * Minimal logging interface. + * + * + * We use this to work around some strange problems when using log4j from the Teamscale Gradle plugin. + */ +interface ILogger { + /** Logs at debug level. */ + fun debug(message: String?) + + /** Logs at info level. */ + fun info(message: String?) + + /** Logs at warning level. */ + fun warn(message: String?) + + /** Logs at warning level. The given [Throwable] may be null. */ + fun warn(message: String?, throwable: Throwable?) + + /** Logs at error level. */ + fun error(throwable: Throwable) + + /** Logs at error level. The given [Throwable] may be null. */ + fun error(message: String?, throwable: Throwable? = null) +} diff --git a/report-generator/src/main/kotlin/com/teamscale/report/util/SortedIntList.kt b/report-generator/src/main/kotlin/com/teamscale/report/util/SortedIntList.kt new file mode 100644 index 000000000..ce81f9ded --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/util/SortedIntList.kt @@ -0,0 +1,79 @@ +package com.teamscale.report.util + +/** + * Performant implementation of a deduplicated sorted integer list that assumes that insertions mainly happen at the end + * and that input is already sorted. + */ +class SortedIntList { + /** + * The list of values in sorted order and without duplicates. The list might be bigger than the number of elements. + */ + @JvmField + var list: IntArray + + /** The number of actual elements in the list. */ + private var count = 0 + + init { + list = IntArray(64) + } + + /** Adds the given value to the list at the correct location, ignoring duplicates. */ + fun add(value: Int): Boolean { + var high = count + var low = 0 + + if (isEmpty) { + list[0] = value + count = 1 + return true + } + + // Perform binary search to find target location + do { + val p = (low + high) ushr 1 + if (value < list[p]) { + high = p + } else if (value == list[p]) { + // Element already exists in the list + return false + } else { + low = p + 1 + } + } while (low < high) + + if (count == list.size) { + val n = IntArray(list.size * 2) + System.arraycopy(list, 0, n, 0, count) + list = n + } + + if (low < count) { + System.arraycopy(list, low, list, low + 1, count - low) + } + list[low] = value + count++ + return true + } + + /** Inserts all values from the given list, ignoring duplicates. */ + fun addAll(input: SortedIntList) { + for (i in 0 until input.size()) { + add(input.get(i)) + } + } + + /** Returns the size of the list. */ + fun size(): Int { + return count + } + + val isEmpty: Boolean + /** Returns whether the list is empty. */ + get() = count == 0 + + /** Returns the i-th element of the list. */ + operator fun get(i: Int): Int { + return list[i] + } +} \ No newline at end of file diff --git a/report-generator/src/main/kotlin/org/jacoco/core/internal/analysis/CachingClassAnalyzer.kt b/report-generator/src/main/kotlin/org/jacoco/core/internal/analysis/CachingClassAnalyzer.kt new file mode 100644 index 000000000..7a5b09ef1 --- /dev/null +++ b/report-generator/src/main/kotlin/org/jacoco/core/internal/analysis/CachingClassAnalyzer.kt @@ -0,0 +1,53 @@ +package org.jacoco.core.internal.analysis + +import com.teamscale.report.testwise.jacoco.cache.ClassCoverageLookup +import org.jacoco.core.internal.flow.MethodProbesVisitor +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.tree.MethodNode + +/** + * Analyzes a class to reconstruct probe information. + * + * + * A probe lookup holds for a single class which probe belongs to which lines. The actual filling of the + * [ClassCoverageLookup] happens in [CachingInstructionsBuilder]. + */ +class CachingClassAnalyzer +/** + * Creates a new analyzer that builds coverage data for a class. + * + * @param classCoverageLookup cache for the class' probes + * @param coverage coverage node for the analyzed class data + * @param stringPool shared pool to minimize the number of [String] instances + */( + /** The cache, which contains a probe lookups for the current class. */ + private val classCoverageLookup: ClassCoverageLookup, + coverage: ClassCoverageImpl?, + stringPool: StringPool? +) : ClassAnalyzer(coverage, null, stringPool) { + override fun visitSource(source: String?, debug: String?) { + super.visitSource(source, debug) + classCoverageLookup.setSourceFileName(source) + } + + override fun visitMethod( + access: Int, name: String?, + desc: String?, signature: String?, exceptions: Array? + ): MethodProbesVisitor { + val builder = CachingInstructionsBuilder(classCoverageLookup) + + return object : MethodAnalyzer(builder) { + override fun accept( + methodNode: MethodNode, + methodVisitor: MethodVisitor + ) { + super.accept(methodNode, methodVisitor) + builder.fillCache() + } + } + } + + override fun visitTotalProbeCount(count: Int) { + classCoverageLookup.setTotalProbeCount(count) + } +} diff --git a/report-generator/src/main/kotlin/org/jacoco/core/internal/analysis/CachingInstructionsBuilder.kt b/report-generator/src/main/kotlin/org/jacoco/core/internal/analysis/CachingInstructionsBuilder.kt new file mode 100644 index 000000000..596476d72 --- /dev/null +++ b/report-generator/src/main/kotlin/org/jacoco/core/internal/analysis/CachingInstructionsBuilder.kt @@ -0,0 +1,210 @@ +package org.jacoco.core.internal.analysis + +import com.teamscale.report.testwise.jacoco.cache.ClassCoverageLookup +import com.teamscale.report.util.SortedIntList +import org.jacoco.core.analysis.ISourceNode +import org.jacoco.core.internal.flow.LabelInfo +import org.objectweb.asm.Label +import org.objectweb.asm.tree.AbstractInsnNode +import java.lang.reflect.Field + +/** + * Stateful builder for the [Instruction]s of a method. All instructions of a method must be added in their + * original sequence along with additional information like line numbers. Afterwards the instructions can be obtained + * with the `getInstructions()` method. + * + * + * It's core is a copy of [org.jacoco.core.internal.analysis.InstructionsBuilder] that has been extended with + * caching functionality to speed up report generation. + * + * + * This class contains callbacks for stepping through a method at bytecode level which has been decorated with probes by + * JaCoCo in a depth-first-search like way. + * + * + * Changes that have been applied to the original class are marked with ADDED and REMOVED comments to make it as easy as + * possible to adjust the implementation to new versions of JaCoCo. + * + * + * When updating JaCoCo make a diff of the previous [org.jacoco.core.internal.analysis.InstructionsBuilder] + * implementation and the new implementation and update this class accordingly. + */ +internal class CachingInstructionsBuilder( + /** Probe array of the class the analyzed method belongs to. */ // REMOVED private final boolean[] probes; + // ADDED field to hold a reference to our coverage lookup + private val classCoverageLookup: ClassCoverageLookup +) : InstructionsBuilder(null) { + private val coveredProbes: MutableList = ArrayList() + + /** The line which belong to subsequently added instructions. */ + private var currentLine: Int + + /** The last instruction which has been added. */ + private var currentInsn: Instruction? = null + + /** + * All instructions of a method mapped from the ASM node to the corresponding [Instruction] instance. + */ + private val instructions: MutableMap + + /** + * The labels which mark the subsequent instructions. + * + * + * Due to ASM issue #315745 there can be more than one label per instruction + */ + private val currentLabel: MutableList

    + * When performing an update of JaCoCo we need to check that this file is still up to date. + *

    + * An {@link Analyzer} instance processes a set of Java class files and + * calculates coverage data for them. For each class file the result is reported + * to a given {@link ICoverageVisitor} instance. In addition, the + * {@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. + */ +public class OpenAnalyzer { + + private final ExecutionDataStore executionData; + + private final ICoverageVisitor coverageVisitor; + + private final StringPool stringPool; + + /** + * Creates a new analyzer reporting to the given output. + * + * @param executionData + * execution data + * @param coverageVisitor + * the output instance that will coverage data for every analyzed + * class + */ + public OpenAnalyzer(final ExecutionDataStore executionData, + final ICoverageVisitor coverageVisitor) { + this.executionData = executionData; + this.coverageVisitor = coverageVisitor; + this.stringPool = new StringPool(); + } + + /** + * Creates an ASM class visitor for analysis. + * + * @param classId + * id of the class calculated with {@link CRC64} + * @param className + * VM name of the class + * @return ASM visitor to write class definition to + */ + private ClassVisitor createAnalyzingVisitor(final long classId, + final String className) { + final ExecutionData data = executionData.get(classId); + final boolean[] probes; + final boolean noMatch; + if (data == null) { + probes = null; + noMatch = executionData.contains(className); + } else { + probes = data.getProbes(); + noMatch = false; + } + final ClassCoverageImpl coverage = new ClassCoverageImpl(className, + classId, noMatch); + final ClassAnalyzer analyzer = new ClassAnalyzer(coverage, probes, + stringPool) { + @Override + public void visitEnd() { + super.visitEnd(); + coverageVisitor.visitCoverage(coverage); + } + }; + return new ClassProbesAdapter(analyzer, false); + } + + /** Analyzes the given class in binary form. */ + protected void analyzeClass(final byte[] source) { + final long classId = CRC64.classId(source); + final ClassReader reader = InstrSupport.classReaderFor(source); + if ((reader.getAccess() & Opcodes.ACC_MODULE) != 0) { + return; + } + if ((reader.getAccess() & Opcodes.ACC_SYNTHETIC) != 0) { + return; + } + final ClassVisitor visitor = createAnalyzingVisitor(classId, + reader.getClassName()); + reader.accept(visitor, 0); + } + + /** + * Analyzes the class definition from a given in-memory buffer. + * + * @param buffer + * class definitions + * @param location + * a location description used for exception messages + * @throws IOException + * if the class can't be analyzed + */ + public void analyzeClass(final byte[] buffer, final String location) + throws IOException { + try { + analyzeClass(buffer); + } catch (final RuntimeException cause) { + throw analyzerError(location, cause); + } + } + + /** + * Analyzes the class definition from a given input stream. The provided + * {@link InputStream} is not closed by this method. + * + * @param input + * stream to read class definition from + * @param location + * a location description used for exception messages + * @throws IOException + * if the stream can't be read or the class can't be analyzed + */ + public void analyzeClass(final InputStream input, final String location) + throws IOException { + final byte[] buffer; + try { + buffer = InputStreams.readFully(input); + } catch (final IOException e) { + throw analyzerError(location, e); + } + analyzeClass(buffer, location); + } + + /** Creates an {@link IOException} which includes the affected file location and JaCoCo version. */ + protected IOException analyzerError(final String location, + final Exception cause) { + final IOException ex = new IOException( + String.format("Error while analyzing %s with JaCoCo %s/%s.", + location, JaCoCo.VERSION, JaCoCo.COMMITID_SHORT)); + ex.initCause(cause); + return ex; + } + + /** + * Analyzes all classes found in the given input stream. The input stream + * may either represent a single class file, a ZIP archive, a Pack200 + * archive or a gzip stream that is searched recursively for class files. + * All other content types are ignored. The provided {@link InputStream} is + * not closed by this method. + * + * @param input + * input data + * @param location + * a location description used for exception messages + * @return number of class files found + * @throws IOException + * if the stream can't be read or a class can't be analyzed + */ + public int analyzeAll(final InputStream input, final String location) + throws IOException { + final ContentTypeDetector detector; + try { + detector = new ContentTypeDetector(input); + } catch (final IOException e) { + throw analyzerError(location, e); + } + switch (detector.getType()) { + case ContentTypeDetector.CLASSFILE: + analyzeClass(detector.getInputStream(), location); + return 1; + case ContentTypeDetector.ZIPFILE: + return analyzeZip(detector.getInputStream(), location); + case ContentTypeDetector.GZFILE: + return analyzeGzip(detector.getInputStream(), location); + case ContentTypeDetector.PACK200FILE: + return analyzePack200(detector.getInputStream(), location); + default: + return 0; + } + } + + /** + * Analyzes all class files contained in the given file or folder. Class + * files as well as ZIP files are considered. Folders are searched + * recursively. + * + * @param file + * file or folder to look for class files + * @return number of class files found + * @throws IOException + * if the file can't be read or a class can't be analyzed + */ + public int analyzeAll(final File file) throws IOException { + int count = 0; + if (file.isDirectory()) { + for (final File f : file.listFiles()) { + count += analyzeAll(f); + } + } else { + final InputStream in = new FileInputStream(file); + try { + count += analyzeAll(in, file.getPath()); + } finally { + in.close(); + } + } + return count; + } + + /** + * Analyzes all classes from the given class path. Directories containing + * class files as well as archive files are considered. + * + * @param path + * path definition + * @param basedir + * optional base directory, if null the current + * working directory is used as the base for relative path + * entries + * @return number of class files found + * @throws IOException + * if a file can't be read or a class can't be analyzed + */ + public int analyzeAll(final String path, final File basedir) + throws IOException { + int count = 0; + final StringTokenizer st = new StringTokenizer(path, + File.pathSeparator); + while (st.hasMoreTokens()) { + count += analyzeAll(new File(basedir, st.nextToken())); + } + return count; + } + + private int analyzeZip(final InputStream input, final String location) + throws IOException { + final ZipInputStream zip = new ZipInputStream(input); + ZipEntry entry; + int count = 0; + while ((entry = nextEntry(zip, location)) != null) { + count += analyzeAll(zip, location + "@" + entry.getName()); + } + return count; + } + + private ZipEntry nextEntry(final ZipInputStream input, + final String location) throws IOException { + try { + return input.getNextEntry(); + } catch (final IOException e) { + throw analyzerError(location, e); + } + } + + private int analyzeGzip(final InputStream input, final String location) + throws IOException { + GZIPInputStream gzipInputStream; + try { + gzipInputStream = new GZIPInputStream(input); + } catch (final IOException e) { + throw analyzerError(location, e); + } + return analyzeAll(gzipInputStream, location); + } + + private int analyzePack200(final InputStream input, final String location) + throws IOException { + InputStream unpackedInput; + try { + unpackedInput = Pack200Streams.unpack(input); + } catch (final IOException e) { + throw analyzerError(location, e); + } + return analyzeAll(unpackedInput, location); + } + +} \ No newline at end of file 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 new file mode 100644 index 000000000..f91e52202 --- /dev/null +++ b/report-generator/src/main/java/org/jacoco/core/internal/analysis/CachingInstructionsBuilder.java @@ -0,0 +1,234 @@ +package org.jacoco.core.internal.analysis; + +import com.teamscale.report.testwise.jacoco.cache.ClassCoverageLookup; +import com.teamscale.report.util.CompactLines; +import org.jacoco.core.analysis.ISourceNode; +import org.jacoco.core.internal.flow.LabelInfo; +import org.objectweb.asm.Label; +import org.objectweb.asm.tree.AbstractInsnNode; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Stateful builder for the {@link Instruction}s of a method. All instructions of a method must be added in their + * original sequence along with additional information like line numbers. Afterwards the instructions can be obtained + * with the getInstructions() method. + *

    + * It's core is a copy of {@link org.jacoco.core.internal.analysis.InstructionsBuilder} that has been extended with + * caching functionality to speed up report generation. + *

    + * This class contains callbacks for stepping through a method at bytecode level which has been decorated with probes by + * JaCoCo in a depth-first-search like way. + *

    + * Changes that have been applied to the original class are marked with ADDED and REMOVED comments to make it as easy as + * possible to adjust the implementation to new versions of JaCoCo. + *

    + * 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. + */ +public class CachingInstructionsBuilder extends InstructionsBuilder { + + /** Probe array of the class the analyzed method belongs to. */ + // REMOVED private final boolean[] probes; + + // ADDED field to hold a reference to our coverage lookup + private final ClassCoverageLookup classCoverageLookup; + private final List coveredProbes = new ArrayList<>(); + + /** The line which belong to subsequently added instructions. */ + private int currentLine; + + /** The last instruction which has been added. */ + private Instruction currentInsn; + + /** + * All instructions of a method mapped from the ASM node to the corresponding {@link Instruction} instance. + */ + private final Map instructions; + + /** + * The labels which mark the subsequent instructions. + *

    + * Due to ASM issue #315745 there can be more than one label per instruction + */ + private final List

    + * ADDED ClassCoverageLookup classCoverageLookup parameter REMOVED final boolean[] probes + * + * @param classCoverageLookup cache of the class' probes + */ + public CachingInstructionsBuilder(ClassCoverageLookup classCoverageLookup) { + super(null); + this.classCoverageLookup = classCoverageLookup; + this.currentLine = ISourceNode.UNKNOWN_LINE; + this.currentInsn = null; + this.instructions = new HashMap<>(); + this.currentLabel = new ArrayList<>(2); + this.jumps = new ArrayList<>(); + } + + /** + * Sets the current source line. All subsequently added instructions will be assigned to this line. If no line is + * set (e.g. for classes compiled without debug information) {@link ISourceNode#UNKNOWN_LINE} is assigned to the + * instructions. + */ + void setCurrentLine(final int line) { + currentLine = line; + } + + /** + * Adds a label which applies to the subsequently added instruction. Due to ASM internals multiple {@link Label}s + * can be added to an instruction. + */ + void addLabel(final Label label) { + currentLabel.add(label); + if (!LabelInfo.isSuccessor(label)) { + noSuccessor(); + } + } + + /** + * Adds a new instruction. Instructions are by default linked with the previous instruction unless specified + * otherwise. + */ + void addInstruction(final AbstractInsnNode node) { + final Instruction insn = new Instruction(currentLine); + final int labelCount = currentLabel.size(); + if (labelCount > 0) { + for (int i = labelCount; --i >= 0; ) { + LabelInfo.setInstruction(currentLabel.get(i), insn); + } + currentLabel.clear(); + } + if (currentInsn != null) { + currentInsn.addBranch(insn, 0); + } + currentInsn = insn; + instructions.put(node, insn); + } + + /** + * Declares that the next instruction will not be a successor of the current instruction. This is the case with an + * unconditional jump or technically when a probe was inserted before. + */ + void noSuccessor() { + currentInsn = null; + } + + /** + * Adds a jump from the last added instruction. + * + * @param target jump target + * @param branch unique branch number + */ + void addJump(final Label target, final int branch) { + jumps.add(new Jump(currentInsn, target, branch)); + } + + /** + * Adds a new probe for the last instruction. + * + * @param probeId index in the probe array + * @param branch unique branch number for the last instruction + */ + void addProbe(final int probeId, final int branch) { + // REMOVED check of probes array and instead add the probes unconditionally + // final boolean executed = probes != null && probes[probeId]; + // currentInsn.addBranch(executed, branch); + + // ADDED + currentInsn.addBranch(true, branch); + coveredProbes.add(new CoveredProbe(probeId, currentInsn, branch)); + } + + /** + * Returns the status for all instructions of this method. This method must be called exactly once after the + * instructions have been added. + */ + public void fillCache() { + // Wire jumps: + for (final Jump j : jumps) { + j.wire(); + } + + // ADDED + // Traces back all instructions that are executed before reaching a probe + // and stores the mapping from probe to lines in #classCoverageLookup + // We need this because JaCoCo does not insert a probe after every line. + for (CoveredProbe coveredProbe : coveredProbes) { + Instruction instruction = coveredProbe.instruction; + CompactLines coveredLines = new CompactLines(); + while (instruction != null) { + if (instruction.getLine() != -1) { + // Only add the line number if one is associated with the instruction. + // This is not the case for e.g. Lombok generated code. + coveredLines.add(instruction.getLine()); + } + instruction = getPredecessor(instruction); + } + classCoverageLookup.addProbe(coveredProbe.probeId, coveredLines); + } + } + + /** + * ADDED Helper to get the private field predecessor from an instruction. The predecessor of an instruction is the + * preceding node according to the control flow graph of the method. + */ + private Instruction getPredecessor(Instruction instruction) { + try { + Field predecessorField = instruction.getClass().getDeclaredField("predecessor"); + predecessorField.setAccessible(true); + instruction = (Instruction) predecessorField.get(instruction); + } catch (NoSuchFieldException | IllegalAccessException e) { + // This means we have a serious coding mistake here there is no way to recover from this anyway + throw new RuntimeException("Instruction has no field named predecessor! This is a programming error!", e); + } + return instruction; + } + + // ADDED + private static class CoveredProbe { + + final int probeId; + final Instruction instruction; + final int branch; + + private CoveredProbe(int probeId, final Instruction instruction, final int branch) { + this.probeId = probeId; + this.instruction = instruction; + this.branch = branch; + } + } + + private static class Jump { + + private final Instruction source; + private final Label target; + private final int branch; + + Jump(final Instruction source, final Label target, final int branch) { + this.source = source; + this.target = target; + this.branch = branch; + } + + void wire() { + source.addBranch(LabelInfo.getInstruction(target), branch); + } + + } + +} \ No newline at end of file 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 91b06831c..7e1ebc962 100644 --- a/report-generator/src/main/kotlin/com/teamscale/report/ReportUtils.kt +++ b/report-generator/src/main/kotlin/com/teamscale/report/ReportUtils.kt @@ -54,38 +54,31 @@ object ReportUtils { /** Recursively lists all files in the given directory that match the specified extension. */ @Throws(IOException::class) @JvmStatic - // ToDo: Should use reified type parameter when Converter is in kotlin fun readObjects( format: ETestArtifactFormat, clazz: Class>, directoriesOrFiles: List - ): List { - val files = listFiles(format, directoriesOrFiles) - val result = mutableListOf() - files.mapNotNull { JsonUtils.deserializeFile(it, clazz) } - .forEach { result.addAll(listOf(*it)) } - return result - } + ) = listFiles(format, directoriesOrFiles) + .mapNotNull { JsonUtils.deserializeFile(it, clazz) } + .flatMap { listOf(*it) } /** Recursively lists all files of the given artifact type. */ @JvmStatic fun listFiles( format: ETestArtifactFormat, directoriesOrFiles: List - ): List { - val filesWithSpecifiedArtifactType = mutableListOf() - directoriesOrFiles.forEach { directoryOrFile -> - if (directoryOrFile.isDirectory()) { - filesWithSpecifiedArtifactType.addAll( - FileSystemUtils.listFilesRecursively(directoryOrFile) { - it.isOfArtifactFormat(format) - } - ) - } else if (directoryOrFile.isOfArtifactFormat(format)) { - filesWithSpecifiedArtifactType.add(directoryOrFile) + ) = directoriesOrFiles.flatMap { directoryOrFile -> + when { + directoryOrFile.isDirectory() -> { + FileSystemUtils.listFilesRecursively(directoryOrFile) { + it.isOfArtifactFormat(format) + } + } + directoryOrFile.isOfArtifactFormat(format) -> { + listOf(directoryOrFile) } + else -> emptyList() } - return filesWithSpecifiedArtifactType } private fun File.isOfArtifactFormat(format: ETestArtifactFormat) = diff --git a/report-generator/src/main/kotlin/com/teamscale/report/jacoco/CoverageFile.kt b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/CoverageFile.kt index 9ad9ec8cc..1e8754175 100644 --- a/report-generator/src/main/kotlin/com/teamscale/report/jacoco/CoverageFile.kt +++ b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/CoverageFile.kt @@ -34,7 +34,7 @@ data class CoverageFile(private val coverageFile: File) { * avoid having to read the entire file into memory. */ @Throws(IOException::class) - fun copy(outputStream: OutputStream) { + fun copyStream(outputStream: OutputStream) { coverageFile.inputStream().use { input -> input.copyTo(outputStream) } @@ -44,7 +44,7 @@ data class CoverageFile(private val coverageFile: File) { * Get the filename of the coverage file on disk without its extension */ val nameWithoutExtension: String - get() = coverageFile.name.substringBeforeLast('.') + get() = coverageFile.nameWithoutExtension /** Get the filename of the coverage file. */ val name: String diff --git a/report-generator/src/main/kotlin/com/teamscale/report/jacoco/FilteringAnalyzer.kt b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/FilteringAnalyzer.kt index 159b670ee..7a12e7dbe 100644 --- a/report-generator/src/main/kotlin/com/teamscale/report/jacoco/FilteringAnalyzer.kt +++ b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/FilteringAnalyzer.kt @@ -16,7 +16,7 @@ import java.util.zip.ZipEntry import java.util.zip.ZipInputStream /** - * Analyzer that filters the analyzed class files based on a given predicate. + * [org.jacoco.core.analysis.Analyzer] that filters the analyzed class files based on a given predicate. * * @param executionData The execution data store. * @param coverageVisitor The coverage visitor. @@ -63,6 +63,10 @@ open class FilteringAnalyzer( private fun RuntimeException.isUnsupportedClassFile() = this is IllegalArgumentException && message?.startsWith("Unsupported") == true + /** + * Copied from [org.jacoco.core.analysis.Analyzer.analyzeZip] renamed to analyzeJar + * and added wrapping [BashFileSkippingInputStream]. + */ @Throws(IOException::class) protected open fun analyzeJar(input: InputStream, location: String): Int { ZipInputStream(BashFileSkippingInputStream(input)).use { zip -> @@ -72,6 +76,7 @@ open class FilteringAnalyzer( } } + /** Copied from [org.jacoco.core.analysis.Analyzer.nextEntry]. */ @Throws(IOException::class) private fun ZipInputStream.nextEntry(location: String): ZipEntry? { try { diff --git a/report-generator/src/main/kotlin/com/teamscale/report/jacoco/OpenAnalyzer.kt b/report-generator/src/main/kotlin/com/teamscale/report/jacoco/OpenAnalyzer.kt deleted file mode 100644 index 6e9e7eb23..000000000 --- a/report-generator/src/main/kotlin/com/teamscale/report/jacoco/OpenAnalyzer.kt +++ /dev/null @@ -1,309 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2009, 2023 Mountainminds GmbH & Co. KG and Contributors - * This program and the accompanying materials are made available under - * the terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - * - * Contributors: - * Marc R. Hoffmann - initial API and implementation - * - */ -package com.teamscale.report.jacoco - -import org.jacoco.core.JaCoCo -import org.jacoco.core.analysis.ICoverageVisitor -import org.jacoco.core.data.ExecutionDataStore -import org.jacoco.core.internal.ContentTypeDetector -import org.jacoco.core.internal.InputStreams -import org.jacoco.core.internal.Pack200Streams -import org.jacoco.core.internal.analysis.ClassAnalyzer -import org.jacoco.core.internal.analysis.ClassCoverageImpl -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.ClassVisitor -import org.objectweb.asm.Opcodes -import java.io.File -import java.io.FileInputStream -import java.io.IOException -import java.io.InputStream -import java.util.* -import java.util.zip.GZIPInputStream -import java.util.zip.ZipEntry -import java.util.zip.ZipInputStream - -/** - * This is a copy of the Analyzer class from JaCoCo. - * The only changes are that the following methods are protected instead of private: - * - [.analyzeClass] - * - [.analyzerError] - * - * - * When performing an update of JaCoCo we need to check that this file is still up-to-date. - * - * - * An Analyzer instance processes a set of Java class files and - * calculates coverage data for them. For each class file the result is reported - * to a given [ICoverageVisitor] instance. In addition, the - * Analyzer requires a [ExecutionDataStore] instance that holds - * the execution data for the classes to analyze. The Analyzer offers - * several methods to analyze classes from a variety of sources. - */ -open class OpenAnalyzer( - private val executionData: ExecutionDataStore?, - private val coverageVisitor: ICoverageVisitor? -) { - private val stringPool = StringPool() - - /** - * Creates an ASM class visitor for analysis. - * - * @param classId - * id of the class calculated with [CRC64] - * @param className - * VM name of the class - * @return ASM visitor to write class definition to - */ - private fun createAnalyzingVisitor( - classId: Long, - className: String - ): ClassVisitor { - val data = executionData!![classId] - val probes: BooleanArray? - val noMatch: Boolean - if (data == null) { - probes = null - noMatch = executionData.contains(className) - } else { - probes = data.probes - noMatch = false - } - val coverage = ClassCoverageImpl( - className, - classId, noMatch - ) - val analyzer: ClassAnalyzer = object : ClassAnalyzer( - coverage, probes, - stringPool - ) { - override fun visitEnd() { - super.visitEnd() - coverageVisitor!!.visitCoverage(coverage) - } - } - return ClassProbesAdapter(analyzer, false) - } - - /** Analyzes the given class in binary form. */ - protected open fun analyzeClass(source: ByteArray) { - val classId = CRC64.classId(source) - val reader = InstrSupport.classReaderFor(source) - if ((reader.access and Opcodes.ACC_MODULE) != 0) { - return - } - if ((reader.access and Opcodes.ACC_SYNTHETIC) != 0) { - return - } - val visitor = createAnalyzingVisitor( - classId, - reader.className - ) - reader.accept(visitor, 0) - } - - /** - * Analyzes the class definition from a given in-memory buffer. - * - * @param buffer - * class definitions - * @param location - * a location description used for exception messages - * @throws IOException - * if the class can't be analyzed - */ - @Throws(IOException::class) - open fun analyzeClass(buffer: ByteArray, location: String) { - try { - analyzeClass(buffer) - } catch (cause: RuntimeException) { - throw analyzerError(location, cause) - } - } - - /** - * Analyzes the class definition from a given input stream. The provided - * [InputStream] is not closed by this method. - * - * @param input - * stream to read class definition from - * @param location - * a location description used for exception messages - * @throws IOException - * if the stream can't be read or the class can't be analyzed - */ - @Throws(IOException::class) - fun analyzeClass(input: InputStream, location: String) { - val buffer: ByteArray - try { - buffer = InputStreams.readFully(input) - } catch (e: IOException) { - throw analyzerError(location, e) - } - analyzeClass(buffer, location) - } - - /** Creates an [IOException] which includes the affected file location and JaCoCo version. */ - protected fun analyzerError( - location: String, - cause: Exception? - ): IOException { - val ex = IOException( - String.format( - "Error while analyzing %s with JaCoCo %s/%s.", - location, JaCoCo.VERSION, JaCoCo.COMMITID_SHORT - ) - ) - ex.initCause(cause) - return ex - } - - /** - * Analyzes all classes found in the given input stream. The input stream - * may either represent a single class file, a ZIP archive, a Pack200 - * archive or a gzip stream that is searched recursively for class files. - * All other content types are ignored. The provided [InputStream] is - * not closed by this method. - * - * @param input - * input data - * @param location - * a location description used for exception messages - * @return number of class files found - * @throws IOException - * if the stream can't be read or a class can't be analyzed - */ - @Throws(IOException::class) - open fun analyzeAll(input: InputStream, location: String): Int { - val detector: ContentTypeDetector - try { - detector = ContentTypeDetector(input) - } catch (e: IOException) { - throw analyzerError(location, e) - } - when (detector.type) { - ContentTypeDetector.CLASSFILE -> { - analyzeClass(detector.inputStream, location) - return 1 - } - - ContentTypeDetector.ZIPFILE -> return analyzeZip(detector.inputStream, location) - ContentTypeDetector.GZFILE -> return analyzeGzip(detector.inputStream, location) - ContentTypeDetector.PACK200FILE -> return analyzePack200(detector.inputStream, location) - else -> return 0 - } - } - - /** - * Analyzes all class files contained in the given file or folder. Class - * files as well as ZIP files are considered. Folders are searched - * recursively. - * - * @param file - * file or folder to look for class files - * @return number of class files found - * @throws IOException - * if the file can't be read or a class can't be analyzed - */ - @Throws(IOException::class) - fun analyzeAll(file: File): Int { - var count = 0 - if (file.isDirectory) { - for (f in file.listFiles()) { - count += analyzeAll(f) - } - } else { - val `in`: InputStream = FileInputStream(file) - try { - count += analyzeAll(`in`, file.path) - } finally { - `in`.close() - } - } - return count - } - - /** - * Analyzes all classes from the given class path. Directories containing - * class files as well as archive files are considered. - * - * @param path - * path definition - * @param basedir - * optional base directory, if `null` the current - * working directory is used as the base for relative path - * entries - * @return number of class files found - * @throws IOException - * if a file can't be read or a class can't be analyzed - */ - @Throws(IOException::class) - fun analyzeAll(path: String, basedir: File?): Int { - var count = 0 - val st = StringTokenizer( - path, - File.pathSeparator - ) - while (st.hasMoreTokens()) { - count += analyzeAll(File(basedir, st.nextToken())) - } - return count - } - - @Throws(IOException::class) - private fun analyzeZip(input: InputStream, location: String): Int { - val zip = ZipInputStream(input) - var entry: ZipEntry? - var count = 0 - while ((nextEntry(zip, location).also { entry = it }) != null) { - count += analyzeAll(zip, location + "@" + entry!!.name) - } - return count - } - - @Throws(IOException::class) - private fun nextEntry( - input: ZipInputStream, - location: String - ): ZipEntry? { - try { - return input.nextEntry - } catch (e: IOException) { - throw analyzerError(location, e) - } - } - - @Throws(IOException::class) - private fun analyzeGzip(input: InputStream, location: String): Int { - val gzipInputStream: GZIPInputStream - try { - gzipInputStream = GZIPInputStream(input) - } catch (e: IOException) { - throw analyzerError(location, e) - } - return analyzeAll(gzipInputStream, location) - } - - @Throws(IOException::class) - private fun analyzePack200(input: InputStream, location: String): Int { - val unpackedInput: InputStream - try { - unpackedInput = Pack200Streams.unpack(input) - } catch (e: IOException) { - throw analyzerError(location, e) - } - return analyzeAll(unpackedInput, location) - } -} 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 44447480b..b7eb6a329 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 @@ -68,7 +68,8 @@ class TestwiseCoverageReportWriter( } private fun getOutputFile(testFileCounter: Int): File { - var name = StringUtils.stripSuffix(outputFile.getName(), ".json") + var name = outputFile.nameWithoutExtension + name = "$name-$testFileCounter.json" return File(outputFile.getParent(), name) } 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 c49bd911d..735ef1b1e 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 @@ -40,14 +40,6 @@ open class CachingExecutionDataReader( validateAnalysisResult(classCount) } - /** - * Builds a consumer for coverage data. - */ - fun buildCoverageConsumer( - locationIncludeFilter: ClasspathWildcardIncludeFilter, - nextConsumer: Consumer - ) = DumpConsumer(logger, locationIncludeFilter, nextConsumer) - /** * Analyzes the specified directory, logging errors if any occur. */ @@ -58,6 +50,14 @@ open class CachingExecutionDataReader( "in this folder will be ignored.", e) } .getOrDefault(0) + /** + * Builds a consumer for coverage data. + */ + fun buildCoverageConsumer( + locationIncludeFilter: ClasspathWildcardIncludeFilter, + nextConsumer: Consumer + ) = DumpConsumer(logger, locationIncludeFilter, nextConsumer) + /** * Logs errors if no classes were analyzed or if the filter excluded all files. */ 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 70f120d0a..6ad52eee1 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 @@ -17,7 +17,7 @@ import java.nio.file.Files import java.nio.file.Paths /** - * An `AnalyzerCache` instance processes a set of Java class/jar/war/... files and builds a cache for each of the classes. + * An [AnalyzerCache] instance processes a set of Java class/jar/war/... files and builds a cache for each of the classes. * * For every class that gets found, [analyzeClass] is called. A class is identified by its class ID, which * is a CRC64 checksum of the class file. We process each class with `CachingClassAnalyzer` to fill a cache. 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 498b245f1..ac01714f2 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 @@ -2,29 +2,30 @@ package com.teamscale.report.testwise.jacoco.cache import com.teamscale.client.StringUtils import com.teamscale.report.testwise.model.builder.FileCoverageBuilder +import com.teamscale.report.util.CompactLines import com.teamscale.report.util.ILogger import org.jacoco.core.data.ExecutionData /** * Holds information about a class' probes and to which line ranges they refer. * - * - * * Create an instance of this class for every analyzed java class. * Set the file name of the java source file from which the class has been created. - * Then call [.addProbe] for all probes and lines that belong to that probe. - * Afterwards call [.getFileCoverage] to transform probes ([ ]) for this class into covered lines ([FileCoverageBuilder]). + * Then call [addProbe] for all probes and lines that belong to that probe. + * Afterward call [getFileCoverage] to transform probes ([ExecutionData]) for this class into covered lines + * ([FileCoverageBuilder]). * - * @param className Classname as stored in the bytecode e.g. com/company/Example + * @param className Classname as stored in the bytecode e.g., com/company/Example */ class ClassCoverageLookup internal constructor( private val className: String ) { var sourceFileName: String? = null - private val probes = mutableMapOf>() + private val probes = mutableMapOf() - fun addProbe(probeId: Int, lines: Set) { - probes[probeId] = lines.toSortedSet() + /** Adds the probe with the given id to the method. */ + fun addProbe(probeId: Int, lines: CompactLines) { + probes[probeId] = lines } /** diff --git a/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/FileCoverageBuilder.kt b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/FileCoverageBuilder.kt index f8dc70780..8f3fd7b38 100644 --- a/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/FileCoverageBuilder.kt +++ b/report-generator/src/main/kotlin/com/teamscale/report/testwise/model/builder/FileCoverageBuilder.kt @@ -2,8 +2,8 @@ package com.teamscale.report.testwise.model.builder import com.teamscale.report.testwise.model.FileCoverage import com.teamscale.report.testwise.model.LineRange -import java.util.SortedSet -import java.util.TreeSet +import com.teamscale.report.util.CompactLines +import com.teamscale.report.util.CompactLines.Companion.compactLinesOf /** Holds coverage of a single file. */ class FileCoverageBuilder( @@ -15,7 +15,7 @@ class FileCoverageBuilder( /** * A set of line numbers that have been covered. Ensures order and uniqueness. */ - private val coveredLines = sortedSetOf() + private val coveredLines = compactLinesOf() /** Adds a line as covered. */ fun addLine(line: Int) = coveredLines.add(line) @@ -24,14 +24,14 @@ class FileCoverageBuilder( fun addLineRange(start: Int, end: Int) = (start..end).forEach { coveredLines.add(it) } /** Adds a set of lines as covered. */ - fun addLines(lines: Set) = coveredLines.addAll(lines) + fun addLines(lines: CompactLines) = coveredLines merge lines /** Merges the coverage of another [FileCoverageBuilder] into the current list. */ fun merge(other: FileCoverageBuilder) { require(other.fileName == fileName && other.path == path) { "Cannot merge coverage of two different files! This is a bug!" } - coveredLines.addAll(other.coveredLines) + coveredLines merge other.coveredLines } /** @@ -53,7 +53,7 @@ class FileCoverageBuilder( * [[1-10],[12-14]] */ @JvmStatic - fun compactifyToRanges(lines: SortedSet): List = + fun compactifyToRanges(lines: CompactLines): List = lines.fold(mutableListOf()) { ranges, line -> if (ranges.isNotEmpty() && ranges.last().end >= line - 1) { ranges.last().end = line diff --git a/report-generator/src/main/kotlin/com/teamscale/report/util/CompactLines.kt b/report-generator/src/main/kotlin/com/teamscale/report/util/CompactLines.kt new file mode 100644 index 000000000..9a8bfcf75 --- /dev/null +++ b/report-generator/src/main/kotlin/com/teamscale/report/util/CompactLines.kt @@ -0,0 +1,212 @@ +/* + * Copyright (c) CQSE GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.teamscale.report.util + +import java.io.IOException +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import java.io.Serializable +import java.util.BitSet +import java.util.NoSuchElementException + +/** + * A compact, serializable representation of line numbers using a BitSet. This class is designed to + * efficiently store and manipulate sets of line numbers, which is particularly useful for tracking + * coverage information, regions of a text, or any scenario where line-based data needs to be + * compactly managed. + * + * Instances of this class can be created empty, from a collection of integers, or from a string + * representation of line number ranges. It supports basic set operations such as addition, removal, + * intersection, and union, as well as specialized operations like checking if any line number + * within a range or specific region is present. + * + * This class also implements [Iterable], allowing for easy iteration over all stored line + * numbers. + * + * @see BitSet + */ +data class CompactLines( + private var bitSet: BitSet = BitSet() +) : Serializable, Iterable { + + companion object { + private const val serialVersionUID = 1L + + fun compactLinesOf(vararg lines: Int) = CompactLines(*lines) + fun compactLinesOf(lines: Iterable) = CompactLines(lines) + fun compactLinesOf() = CompactLines() + } + + constructor(lines: Iterable) : this() { + lines.forEach { line -> + bitSet.set(line) + } + } + + constructor(vararg lines: Int) : this() { + lines.forEach { line -> + bitSet.set(line) + } + } + + /** Returns the number of line numbers in this set. */ + fun size() = bitSet.cardinality() + + /** + * Checks if this set of line numbers is empty. + * + * @return `true` if there are no line numbers in this set, `false` otherwise. + */ + fun isEmpty() = bitSet.isEmpty + + /** + * Adds all line numbers from another [CompactLines] instance to this one. + */ + infix fun merge(lines: CompactLines) { + bitSet.or(lines.bitSet) + } + + /** + * Checks if a specific line number is present in this set. + * + * @param line The line number (1-based) + * @return `true` if the line number is present, `false` otherwise. + */ + fun contains(line: Int) = bitSet.get(line) + + /** + * Checks if any line number within a specified range is present in this set. + * + * @param start the start of the range (inclusive, 1-based). + * @param end the end of the range (inclusive, 1-based). + * @return `true` if any line number within the range is present, `false` otherwise. + */ + fun containsAny(start: Int, end: Int): Boolean { + val nextSetBit = bitSet.nextSetBit(start) + return nextSetBit != -1 && nextSetBit <= end + } + + /** + * Checks if this set contains all the line numbers specified in an iterable collection. + * + * @return `true` if every line number in the collection is contained in this set, + * `false` otherwise. + */ + fun containsAll(lines: Iterable) = + lines.all { line -> bitSet.get(line) } + + /** + * Adds a specific line number to this set. + * + * @param line The line number (1-based) + */ + fun add(line: Int) { + bitSet.set(line) + } + + /** + * Adds a range of line numbers to this set. + * + * @param startLine the starting line number of the range to add (inclusive, 1-based) + * @param endLine the ending line number of the range to add (inclusive, 1-based) + */ + fun addRange(startLine: Int, endLine: Int) { + bitSet.set(startLine, endLine + 1) + } + + /** Removes a specific line number from this set. */ + fun remove(line: Int) { + bitSet.clear(line) + } + + /** + * Removes all line numbers that are present in another [CompactLines] instance from this one. + */ + fun removeAll(lines: CompactLines) { + bitSet.andNot(lines.bitSet) + } + + /** Clears all line numbers from this set. */ + fun clear() { + bitSet.clear() + } + + /** + * Retains only the line numbers that are present in both this and another [CompactLines] + * instance. This basically builds the intersection set between both. + */ + fun retainAll(lines: CompactLines) { + bitSet.and(lines.bitSet) + } + + /** + * Creates a new [CompactLines] object with the intersection of this and the other lines. + */ + fun intersection(other: CompactLines) = + compactLinesOf(this).apply { + retainAll(other) + } + + /** + * Checks if there is any overlap between the line numbers in this and another [CompactLines] + * instance. + * + * @return `true` if there is at least one common line number, `false` otherwise. + */ + fun intersects(lines: CompactLines) = + bitSet.intersects(lines.bitSet) + + override fun toString() = joinToString(",") + + /** + * Gets the highest line number contained in this set or null if there are no line numbers + * contained. + */ + fun getHighestLineNumber() = + if (bitSet.isEmpty) null else bitSet.previousSetBit(bitSet.length() - 1) + + @Throws(IOException::class) + private fun writeObject(out: ObjectOutputStream) { + val bytes = bitSet.toByteArray() + out.write(bytes) + } + + @Throws(IOException::class, ClassNotFoundException::class) + private fun readObject(`in`: ObjectInputStream) { + bitSet = BitSet.valueOf(`in`.readBytes()) + } + + override fun iterator(): Iterator { + return object : Iterator { + private var currentIndex = -1 + + override fun hasNext(): Boolean { + val nextIndex = bitSet.nextSetBit(currentIndex + 1) + return nextIndex != -1 + } + + override fun next(): Int { + if (!hasNext()) { + throw NoSuchElementException() + } + currentIndex = bitSet.nextSetBit(currentIndex + 1) + return currentIndex + } + } + } +} diff --git a/report-generator/src/main/kotlin/org/jacoco/core/internal/analysis/CachingInstructionsBuilder.kt b/report-generator/src/main/kotlin/org/jacoco/core/internal/analysis/CachingInstructionsBuilder.kt deleted file mode 100644 index 6d92ae9d0..000000000 --- a/report-generator/src/main/kotlin/org/jacoco/core/internal/analysis/CachingInstructionsBuilder.kt +++ /dev/null @@ -1,160 +0,0 @@ -package org.jacoco.core.internal.analysis - -import com.teamscale.report.testwise.jacoco.cache.ClassCoverageLookup -import org.jacoco.core.analysis.ISourceNode -import org.jacoco.core.internal.flow.LabelInfo -import org.objectweb.asm.Label -import org.objectweb.asm.tree.AbstractInsnNode - -/** - * Stateful builder for the [Instruction]s of a method. All instructions of a method must be added in their - * original sequence along with additional information like line numbers. Afterwards the instructions can be obtained - * with the `getInstructions()` method. - * - * - * It's core is a copy of [org.jacoco.core.internal.analysis.InstructionsBuilder] that has been extended with - * caching functionality to speed up report generation. - * - * - * This class contains callbacks for stepping through a method at bytecode level which has been decorated with probes by - * JaCoCo in a depth-first-search like way. - * - * - * Changes that have been applied to the original class are marked with ADDED and REMOVED comments to make it as easy as - * possible to adjust the implementation to new versions of JaCoCo. - * - * - * When updating JaCoCo make a diff of the previous [org.jacoco.core.internal.analysis.InstructionsBuilder] - * implementation and the new implementation and update this class accordingly. - * - * @param classCoverageLookup probe array of the class the analyzed method belongs to. - */ -internal class CachingInstructionsBuilder( - private val classCoverageLookup: ClassCoverageLookup -) : InstructionsBuilder(null) { - private val coveredProbes = mutableListOf() - - /** The line which belong to subsequently added instructions. */ - private var currentLine: Int = ISourceNode.UNKNOWN_LINE - - /** The last instruction which has been added. */ - private var currentInstruction: Instruction? = null - - /** - * All instructions of a method mapped from the ASM node to the corresponding [Instruction] instance. - */ - private val instructions = mutableMapOf() - - /** - * The labels which mark the subsequent instructions. - * - * - * Due to ASM issue #315745 there can be more than one label per instruction - */ - private val currentLabel = mutableListOf

    - * 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 - *

      - *
    • http.proxyHost
    • - *
    • http.proxyPort
    • - *
    • http.proxyUser
    • - *
    • 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. - */ -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 167/186] 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 b3dcf96b792e0f002cf3ebbdf74b0c99a9714515 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 22 Nov 2024 15:50:39 +0000 Subject: [PATCH 168/186] Update plugin org.beryx.jlink to v3.1.1 --- installer/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installer/build.gradle.kts b/installer/build.gradle.kts index cd33e39a7..daa56e493 100644 --- a/installer/build.gradle.kts +++ b/installer/build.gradle.kts @@ -5,7 +5,7 @@ plugins { com.teamscale.`java-convention` com.teamscale.coverage com.teamscale.`system-test-convention` - id("org.beryx.jlink") version ("3.0.1") + id("org.beryx.jlink") version ("3.1.1") } tasks.jar { From 6266b79af5223ca980e6c02696db03bfe74ec86a Mon Sep 17 00:00:00 2001 From: Constructor Date: Tue, 26 Nov 2024 01:19:11 +0100 Subject: [PATCH 169/186] 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 170/186] 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 171/186] 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 172/186] 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 173/186] 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 174/186] 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 97f8d1567813b5bd874bee1ad9dc890393ff7bea Mon Sep 17 00:00:00 2001 From: Andreas Stahlbauer Date: Tue, 26 Nov 2024 11:29:52 +0100 Subject: [PATCH 175/186] TS-31571 Typing issue fixed (after merge) --- agent/src/main/java/com/teamscale/jacoco/agent/PreMain.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/PreMain.java b/agent/src/main/java/com/teamscale/jacoco/agent/PreMain.java index 596624721..4ab473707 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/PreMain.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/PreMain.java @@ -16,6 +16,7 @@ import com.teamscale.jacoco.agent.logging.DebugLogDirectoryPropertyDefiner; import com.teamscale.jacoco.agent.logging.LogDirectoryPropertyDefiner; import com.teamscale.jacoco.agent.logging.LoggingUtils; +import com.teamscale.report.util.ILogger; import org.conqat.lib.commons.collections.CollectionUtils; import org.conqat.lib.commons.collections.Pair; import org.conqat.lib.commons.filesystem.FileSystemUtils; @@ -259,7 +260,7 @@ private static LoggingUtils.LoggingResources initializeFallbackLogging(String pr /** Creates a fallback logger using the given config file. */ private static LoggingUtils.LoggingResources createFallbackLoggerFromConfig(String configLocation, - DelayedLogger delayedLogger) { + ILogger delayedLogger) { try { return LoggingUtils.initializeLogging( new FilePatternResolver(delayedLogger).parsePath(AgentOptionsParser.LOGGING_CONFIG_OPTION, From c05bfab4de8c427b90b5bc09e5eb5245cfb9a57a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 27 Nov 2024 16:59:47 +0000 Subject: [PATCH 176/186] Update plugin org.jetbrains.kotlin.jvm to v2.1.0 --- settings.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index c9295ad7e..c69ecb086 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,6 +1,6 @@ pluginManagement { plugins { - kotlin("jvm") version "2.0.21" + kotlin("jvm") version "2.1.0" } } From e8fcb756854eb914d7af7028f3dc5aebbf10eccf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 28 Nov 2024 01:04:44 +0000 Subject: [PATCH 177/186] Update dependency com.fasterxml.jackson.core:jackson-annotations to v2.18.2 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2526779e3..d8846fc74 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] jetty = "9.4.56.v20240826" jersey = "2.45" -jackson = "2.18.1" +jackson = "2.18.2" # When upgrading JaCoCo to a newer version make sure to # check the comment in the OpenAnalyzer.java, JaCoCoPreMain.java and CachingInstructionsBuilder.java # and update the internal_xxxxxx hash included in the imports in LenientCoverageTransformer.java and JaCoCoPreMain.java. From ecc83b39958362cb935076b31415992b98c72746 Mon Sep 17 00:00:00 2001 From: Constructor Date: Thu, 28 Nov 2024 19:20:13 +0100 Subject: [PATCH 178/186] 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( From f8a4c03cde0268e370818fda58e506bd32d13693 Mon Sep 17 00:00:00 2001 From: Constructor Date: Wed, 27 Nov 2024 08:23:05 +0100 Subject: [PATCH 179/186] TS-38628 Tia Runlistener migration --- tia-runlisteners/build.gradle.kts | 1 + .../JUnit4TestwiseCoverageRunListener.java | 61 ---------- ...nit5TestwiseCoverageExecutionListener.java | 85 ------------- .../runlistener/RunListenerAgentBridge.java | 112 ------------------ .../tia/runlistener/RunListenerLogger.java | 34 ------ .../JUnit4TestwiseCoverageRunListener.kt | 52 ++++++++ ...JUnit5TestwiseCoverageExecutionListener.kt | 66 +++++++++++ .../tia/runlistener/RunListenerAgentBridge.kt | 93 +++++++++++++++ .../tia/runlistener/RunListenerLogger.kt | 29 +++++ 9 files changed, 241 insertions(+), 292 deletions(-) delete mode 100644 tia-runlisteners/src/main/java/com/teamscale/tia/runlistener/JUnit4TestwiseCoverageRunListener.java delete mode 100644 tia-runlisteners/src/main/java/com/teamscale/tia/runlistener/JUnit5TestwiseCoverageExecutionListener.java delete mode 100644 tia-runlisteners/src/main/java/com/teamscale/tia/runlistener/RunListenerAgentBridge.java delete mode 100644 tia-runlisteners/src/main/java/com/teamscale/tia/runlistener/RunListenerLogger.java create mode 100644 tia-runlisteners/src/main/kotlin/com/teamscale/tia/runlistener/JUnit4TestwiseCoverageRunListener.kt create mode 100644 tia-runlisteners/src/main/kotlin/com/teamscale/tia/runlistener/JUnit5TestwiseCoverageExecutionListener.kt create mode 100644 tia-runlisteners/src/main/kotlin/com/teamscale/tia/runlistener/RunListenerAgentBridge.kt create mode 100644 tia-runlisteners/src/main/kotlin/com/teamscale/tia/runlistener/RunListenerLogger.kt diff --git a/tia-runlisteners/build.gradle.kts b/tia-runlisteners/build.gradle.kts index 89486d4e8..c8f590315 100644 --- a/tia-runlisteners/build.gradle.kts +++ b/tia-runlisteners/build.gradle.kts @@ -1,4 +1,5 @@ plugins { + kotlin("jvm") `java-library` com.teamscale.`java-convention` com.teamscale.coverage diff --git a/tia-runlisteners/src/main/java/com/teamscale/tia/runlistener/JUnit4TestwiseCoverageRunListener.java b/tia-runlisteners/src/main/java/com/teamscale/tia/runlistener/JUnit4TestwiseCoverageRunListener.java deleted file mode 100644 index 490cedda9..000000000 --- a/tia-runlisteners/src/main/java/com/teamscale/tia/runlistener/JUnit4TestwiseCoverageRunListener.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.teamscale.tia.runlistener; - -import com.teamscale.report.testwise.model.ETestExecutionResult; -import com.teamscale.tia.client.TiaAgent; -import org.junit.runner.Description; -import org.junit.runner.Result; -import org.junit.runner.notification.Failure; -import org.junit.runner.notification.RunListener; - -/** - * {@link RunListener} that uses the {@link TiaAgent} to record test-wise coverage. - */ -@SuppressWarnings("unused") -public class JUnit4TestwiseCoverageRunListener extends RunListener { - - private final RunListenerAgentBridge bridge = new RunListenerAgentBridge( - JUnit4TestwiseCoverageRunListener.class.getName()); - - @Override - public void testStarted(Description description) { - String uniformPath = getUniformPath(description); - bridge.testStarted(uniformPath); - } - - private String getUniformPath(Description description) { - String uniformPath = description.getClassName().replace('.', '/'); - if (description.getMethodName() != null) { - uniformPath += "/" + description.getMethodName(); - } - return uniformPath; - } - - @Override - public void testFinished(Description description) { - String uniformPath = getUniformPath(description); - bridge.testFinished(uniformPath, ETestExecutionResult.PASSED); - } - - @Override - public void testFailure(Failure failure) { - String uniformPath = getUniformPath(failure.getDescription()); - bridge.testFinished(uniformPath, ETestExecutionResult.FAILURE, failure.getMessage()); - } - - @Override - public void testAssumptionFailure(Failure failure) { - String uniformPath = getUniformPath(failure.getDescription()); - bridge.testFinished(uniformPath, ETestExecutionResult.FAILURE); - } - - @Override - public void testIgnored(Description description) { - String uniformPath = getUniformPath(description); - bridge.testSkipped(uniformPath, null); - } - - @Override - public void testRunFinished(Result result) { - bridge.testRunFinished(); - } -} diff --git a/tia-runlisteners/src/main/java/com/teamscale/tia/runlistener/JUnit5TestwiseCoverageExecutionListener.java b/tia-runlisteners/src/main/java/com/teamscale/tia/runlistener/JUnit5TestwiseCoverageExecutionListener.java deleted file mode 100644 index 7e4e36b23..000000000 --- a/tia-runlisteners/src/main/java/com/teamscale/tia/runlistener/JUnit5TestwiseCoverageExecutionListener.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.teamscale.tia.runlistener; - -import com.teamscale.report.testwise.model.ETestExecutionResult; -import com.teamscale.tia.client.TiaAgent; -import org.junit.platform.engine.TestExecutionResult; -import org.junit.platform.engine.TestSource; -import org.junit.platform.engine.support.descriptor.ClassSource; -import org.junit.platform.engine.support.descriptor.MethodSource; -import org.junit.platform.launcher.TestExecutionListener; -import org.junit.platform.launcher.TestIdentifier; -import org.junit.platform.launcher.TestPlan; - -import java.util.Optional; - -/** - * {@link TestExecutionListener} that uses the {@link TiaAgent} to record test-wise coverage. - */ -@SuppressWarnings("unused") -public class JUnit5TestwiseCoverageExecutionListener implements TestExecutionListener { - - private final RunListenerAgentBridge bridge = new RunListenerAgentBridge( - JUnit5TestwiseCoverageExecutionListener.class.getName()); - - @Override - public void executionStarted(TestIdentifier testIdentifier) { - if (!testIdentifier.isTest()) { - return; - } - String uniformPath = getUniformPath(testIdentifier); - bridge.testStarted(uniformPath); - } - - private String getUniformPath(TestIdentifier testIdentifier) { - return testIdentifier.getSource().flatMap(this::parseTestSource).orElse(testIdentifier.getDisplayName()); - } - - private Optional parseTestSource(TestSource source) { - if (source instanceof ClassSource) { - ClassSource classSource = (ClassSource) source; - return Optional.of(classSource.getClassName().replace('.', '/')); - } else if (source instanceof MethodSource) { - MethodSource methodSource = (MethodSource) source; - return Optional.of(methodSource.getClassName().replace('.', '/') + "/" + - methodSource.getMethodName() + "(" + methodSource.getMethodParameterTypes() + ")"); - } - return Optional.empty(); - } - - @Override - public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) { - if (!testIdentifier.isTest()) { - return; - } - String uniformPath = getUniformPath(testIdentifier); - ETestExecutionResult result; - switch (testExecutionResult.getStatus()) { - case SUCCESSFUL: - result = ETestExecutionResult.PASSED; - break; - case ABORTED: - result = ETestExecutionResult.ERROR; - break; - case FAILED: - default: - result = ETestExecutionResult.FAILURE; - break; - } - - bridge.testFinished(uniformPath, result); - } - - @Override - public void executionSkipped(TestIdentifier testIdentifier, String reason) { - if (testIdentifier.isContainer()) { - return; - } - String uniformPath = getUniformPath(testIdentifier); - bridge.testSkipped(uniformPath, reason); - } - - @Override - public void testPlanExecutionFinished(TestPlan testPlan) { - bridge.testRunFinished(); - } -} diff --git a/tia-runlisteners/src/main/java/com/teamscale/tia/runlistener/RunListenerAgentBridge.java b/tia-runlisteners/src/main/java/com/teamscale/tia/runlistener/RunListenerAgentBridge.java deleted file mode 100644 index 400af387a..000000000 --- a/tia-runlisteners/src/main/java/com/teamscale/tia/runlistener/RunListenerAgentBridge.java +++ /dev/null @@ -1,112 +0,0 @@ -package com.teamscale.tia.runlistener; - -import com.teamscale.report.testwise.model.ETestExecutionResult; -import com.teamscale.tia.client.RunningTest; -import com.teamscale.tia.client.TestRun; -import com.teamscale.tia.client.TiaAgent; -import okhttp3.HttpUrl; -import org.junit.runner.notification.RunListener; - -/** - * Handles communication with the {@link TiaAgent} and logging for any type of test run listener. This allows e.g. Junit - * 4 and Junit 5 listeners to share the same logic for these tasks. - */ -public class RunListenerAgentBridge { - - private final TestRun testRun; - private RunningTest runningTest; - private final RunListenerLogger logger = new RunListenerLogger(RunListenerAgentBridge.class); - - private static class RunListenerConfigurationException extends RuntimeException { - public RunListenerConfigurationException(String message) { - super(message); - } - } - - public RunListenerAgentBridge(String runListenerClassName) { - logger.debug(runListenerClassName + " instantiated"); - String agentUrl = System.getProperty("tia.agent"); - if (agentUrl == null) { - agentUrl = System.getenv("TIA_AGENT"); - } - if (agentUrl == null) { - RunListenerConfigurationException exception = new RunListenerConfigurationException( - "You did not provide the URL of a Teamscale JaCoCo agent that will record test-wise coverage." + - " You can configure the URL either as a system property with -Dtia.agent=URL" + - " or as an environment variable with TIA_AGENT=URL."); - logger.error("Failed to instantiate " + runListenerClassName, exception); - throw exception; - } - - TiaAgent agent = new TiaAgent(false, HttpUrl.get(agentUrl)); - testRun = agent.startTestRunWithoutTestSelection(); - } - - @FunctionalInterface - private interface Action { - /** Runs the action, throwing exceptions if it fails. */ - void run() throws Exception; - } - - /** - * We mustn't throw exceptions out of the {@link RunListener} interface methods or Maven will treat the test as - * failed. And we don't have access to the Maven logger, so we just log to stderr. - */ - private void handleErrors(Action action, String description) { - try { - action.run(); - } catch (Exception e) { - logger.error("Encountered an error while recording test-wise coverage in step: " + description, e); - } - } - - /** Notifies the {@link TiaAgent} that the given test was started. */ - public void testStarted(String uniformPath) { - logger.debug("Started test '" + uniformPath + "'"); - handleErrors(() -> runningTest = testRun.startTest(uniformPath), "Starting test '" + uniformPath + "'"); - } - - /** Notifies the {@link TiaAgent} that the given test was finished (both successfully and unsuccessfully). */ - public void testFinished(String uniformPath, ETestExecutionResult result) { - testFinished(uniformPath, result, null); - } - - /** - * Notifies the {@link TiaAgent} that the given test was finished (both successfully and unsuccessfully). - * - * @param message mey be null if no useful message can be provided. - */ - public void testFinished(String uniformPath, ETestExecutionResult result, String message) { - logger.debug("Finished test '" + uniformPath + "'"); - handleErrors(() -> { - if (runningTest != null) { - runningTest.endTest(new TestRun.TestResultWithMessage(result, message)); - runningTest = null; - } - }, "Finishing test '" + uniformPath + "'"); - } - - /** - * Notifies the {@link TiaAgent} that the given test was skipped. - * - * @param reason Optional reason. Pass null if no reason was provided by the test framework. - */ - public void testSkipped(String uniformPath, String reason) { - logger.debug("Skipped test '" + uniformPath + "'"); - handleErrors(() -> { - if (runningTest != null) { - runningTest.endTest(new TestRun.TestResultWithMessage(ETestExecutionResult.SKIPPED, reason)); - runningTest = null; - } - }, "Skipping test '" + uniformPath + "'"); - } - - /** - * Notifies the {@link TiaAgent} that the whole test run is finished and that test-wise coverage recording can end - * now. - */ - public void testRunFinished() { - logger.debug("Finished test run"); - handleErrors(() -> testRun.endTestRun(false), "Finishing the test run"); - } -} diff --git a/tia-runlisteners/src/main/java/com/teamscale/tia/runlistener/RunListenerLogger.java b/tia-runlisteners/src/main/java/com/teamscale/tia/runlistener/RunListenerLogger.java deleted file mode 100644 index c9160b229..000000000 --- a/tia-runlisteners/src/main/java/com/teamscale/tia/runlistener/RunListenerLogger.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.teamscale.tia.runlistener; - -/** - * Implements simple STDOUT logging for run listeners. We cannot use SLF4J, since the run listeners are executed in the - * context of the system under test, where we cannot rely on any SLF4J bindings to be present. - *

    - * To enable debug logging, specify {@code -Dtia.debug} for the JVM containing the run listener. - */ -public class RunListenerLogger { - - private static final boolean DEBUG_ENABLED = Boolean.getBoolean("tia.debug"); - - private final String callerClassName; - - public RunListenerLogger(Class callerClass) { - callerClassName = callerClass.getSimpleName(); - } - - /** Logs a debug message. */ - public void debug(String message) { - if (!DEBUG_ENABLED) { - return; - } - // we log to System.err instead of System.out as some runners will filter System.out, e.g. Maven - System.err.println("[DEBUG] " + callerClassName + " - " + message); - } - - /** Logs an error message and the stack trace of an optional throwable. */ - public void error(String message, Throwable throwable) { - System.err.println("[ERROR] " + callerClassName + " - " + message); - throwable.printStackTrace(); - } - -} diff --git a/tia-runlisteners/src/main/kotlin/com/teamscale/tia/runlistener/JUnit4TestwiseCoverageRunListener.kt b/tia-runlisteners/src/main/kotlin/com/teamscale/tia/runlistener/JUnit4TestwiseCoverageRunListener.kt new file mode 100644 index 000000000..aa3c6170a --- /dev/null +++ b/tia-runlisteners/src/main/kotlin/com/teamscale/tia/runlistener/JUnit4TestwiseCoverageRunListener.kt @@ -0,0 +1,52 @@ +package com.teamscale.tia.runlistener + +import com.teamscale.report.testwise.model.ETestExecutionResult +import com.teamscale.tia.client.TiaAgent +import org.junit.runner.Description +import org.junit.runner.Result +import org.junit.runner.notification.Failure +import org.junit.runner.notification.RunListener + +/** + * [RunListener] that uses the [TiaAgent] to record test-wise coverage. + */ +class JUnit4TestwiseCoverageRunListener : RunListener() { + private val bridge = RunListenerAgentBridge.create() + + override fun testStarted(description: Description) { + val uniformPath = getUniformPath(description) + bridge.testStarted(uniformPath) + } + + private fun getUniformPath(description: Description): String { + var uniformPath = description.className.replace('.', '/') + if (description.methodName != null) { + uniformPath += "/" + description.methodName + } + return uniformPath + } + + override fun testFinished(description: Description) { + val uniformPath = getUniformPath(description) + bridge.testFinished(uniformPath, ETestExecutionResult.PASSED) + } + + override fun testFailure(failure: Failure) { + val uniformPath = getUniformPath(failure.description) + bridge.testFinished(uniformPath, ETestExecutionResult.FAILURE, failure.message) + } + + override fun testAssumptionFailure(failure: Failure) { + val uniformPath = getUniformPath(failure.description) + bridge.testFinished(uniformPath, ETestExecutionResult.FAILURE) + } + + override fun testIgnored(description: Description) { + val uniformPath = getUniformPath(description) + bridge.testSkipped(uniformPath, null) + } + + override fun testRunFinished(result: Result) { + bridge.testRunFinished() + } +} diff --git a/tia-runlisteners/src/main/kotlin/com/teamscale/tia/runlistener/JUnit5TestwiseCoverageExecutionListener.kt b/tia-runlisteners/src/main/kotlin/com/teamscale/tia/runlistener/JUnit5TestwiseCoverageExecutionListener.kt new file mode 100644 index 000000000..0cc022695 --- /dev/null +++ b/tia-runlisteners/src/main/kotlin/com/teamscale/tia/runlistener/JUnit5TestwiseCoverageExecutionListener.kt @@ -0,0 +1,66 @@ +package com.teamscale.tia.runlistener + +import com.teamscale.report.testwise.model.ETestExecutionResult +import com.teamscale.tia.client.TiaAgent +import org.junit.platform.engine.TestExecutionResult +import org.junit.platform.engine.TestSource +import org.junit.platform.engine.support.descriptor.ClassSource +import org.junit.platform.engine.support.descriptor.MethodSource +import org.junit.platform.launcher.TestExecutionListener +import org.junit.platform.launcher.TestIdentifier +import org.junit.platform.launcher.TestPlan +import java.util.* +import java.util.function.Function + +/** + * [TestExecutionListener] that uses the [TiaAgent] to record test-wise coverage. + */ +class JUnit5TestwiseCoverageExecutionListener : TestExecutionListener { + private val bridge = RunListenerAgentBridge.create() + + override fun executionStarted(testIdentifier: TestIdentifier) { + if (!testIdentifier.isTest) return + val uniformPath = getUniformPath(testIdentifier) + bridge.testStarted(uniformPath) + } + + private fun getUniformPath(testIdentifier: TestIdentifier) = + testIdentifier.source.flatMap { source -> + parseTestSource(source) + }.orElse(testIdentifier.displayName) + + private fun parseTestSource(source: TestSource) = + when (source) { + is ClassSource -> Optional.of(source.className.replace('.', '/')) + is MethodSource -> Optional.of( + source.className.replace('.', '/') + "/" + + source.methodName + "(" + source.methodParameterTypes + ")" + ) + else -> Optional.empty() + } + + override fun executionFinished(testIdentifier: TestIdentifier, testExecutionResult: TestExecutionResult) { + if (!testIdentifier.isTest) { + return + } + val uniformPath = getUniformPath(testIdentifier) + val result = when (testExecutionResult.status) { + TestExecutionResult.Status.SUCCESSFUL -> ETestExecutionResult.PASSED + TestExecutionResult.Status.ABORTED -> ETestExecutionResult.ERROR + TestExecutionResult.Status.FAILED -> ETestExecutionResult.FAILURE + else -> ETestExecutionResult.FAILURE + } + + bridge.testFinished(uniformPath, result) + } + + override fun executionSkipped(testIdentifier: TestIdentifier, reason: String) { + if (testIdentifier.isContainer) return + val uniformPath = getUniformPath(testIdentifier) + bridge.testSkipped(uniformPath, reason) + } + + override fun testPlanExecutionFinished(testPlan: TestPlan) { + bridge.testRunFinished() + } +} diff --git a/tia-runlisteners/src/main/kotlin/com/teamscale/tia/runlistener/RunListenerAgentBridge.kt b/tia-runlisteners/src/main/kotlin/com/teamscale/tia/runlistener/RunListenerAgentBridge.kt new file mode 100644 index 000000000..9c48f6b6b --- /dev/null +++ b/tia-runlisteners/src/main/kotlin/com/teamscale/tia/runlistener/RunListenerAgentBridge.kt @@ -0,0 +1,93 @@ +package com.teamscale.tia.runlistener + +import com.teamscale.report.testwise.model.ETestExecutionResult +import com.teamscale.tia.client.RunningTest +import com.teamscale.tia.client.TestRun +import com.teamscale.tia.client.TestRun.TestResultWithMessage +import com.teamscale.tia.client.TiaAgent +import okhttp3.HttpUrl + +/** + * Handles communication with the [TiaAgent] and logging for any type of test run listener. + * This allows, e.g., Junit 4 and Junit 5 listeners to share the same logic for these tasks. + */ +class RunListenerAgentBridge(runListenerClassName: String) { + private val testRun: TestRun + private var runningTest: RunningTest? = null + private val logger = RunListenerLogger.create() + + private class RunListenerConfigurationException(message: String) : RuntimeException(message) + + init { + logger.debug("$runListenerClassName instantiated") + val agentUrl = System.getProperty("tia.agent") ?: System.getenv("TIA_AGENT") + if (agentUrl == null) { + val exception = RunListenerConfigurationException( + "You did not provide the URL of a Teamscale JaCoCo agent that will record test-wise coverage." + + " You can configure the URL either as a system property with -Dtia.agent=URL" + + " or as an environment variable with TIA_AGENT=URL." + ) + logger.error("Failed to instantiate $runListenerClassName", exception) + throw exception + } + + val agent = TiaAgent(false, HttpUrl.get(agentUrl)) + testRun = agent.startTestRunWithoutTestSelection() + } + + private fun handleErrors(description: String, action: () -> Unit) { + runCatching { action() }.onFailure { e -> + logger.error("Encountered an error while recording test-wise coverage in step: $description", e) + } + } + + /** Notifies the [TiaAgent] that the given test was started. */ + fun testStarted(uniformPath: String) { + logger.debug("Started test '$uniformPath'") + handleErrors("Starting test '$uniformPath'") { + runningTest = testRun.startTest(uniformPath) + } + } + + /** + * Notifies the [TiaAgent] that the given test was finished (both successfully and unsuccessfully). + * + * @param message may be null if no useful message can be provided. + */ + @JvmOverloads + fun testFinished(uniformPath: String, result: ETestExecutionResult, message: String? = null) { + logger.debug("Finished test '$uniformPath'") + handleErrors("Finishing test '$uniformPath'") { + runningTest?.endTest(TestResultWithMessage(result, message)) + runningTest = null + } + } + + /** + * Notifies the [TiaAgent] that the given test was skipped. + * + * @param reason Optional reason. Pass null if no reason was provided by the test framework. + */ + fun testSkipped(uniformPath: String, reason: String?) { + logger.debug("Skipped test '$uniformPath'") + handleErrors("Skipping test '$uniformPath'") { + runningTest?.endTest(TestResultWithMessage(ETestExecutionResult.SKIPPED, reason)) + runningTest = null + } + } + + /** + * Notifies the [TiaAgent] that the whole test run is finished and that test-wise coverage recording can end + * now. + */ + fun testRunFinished() { + logger.debug("Finished test run") + handleErrors("Finishing the test run") { + testRun.endTestRun(false) + } + } + + companion object { + inline fun create() = RunListenerAgentBridge(T::class.java.name) + } +} \ No newline at end of file diff --git a/tia-runlisteners/src/main/kotlin/com/teamscale/tia/runlistener/RunListenerLogger.kt b/tia-runlisteners/src/main/kotlin/com/teamscale/tia/runlistener/RunListenerLogger.kt new file mode 100644 index 000000000..a54fe3ba1 --- /dev/null +++ b/tia-runlisteners/src/main/kotlin/com/teamscale/tia/runlistener/RunListenerLogger.kt @@ -0,0 +1,29 @@ +package com.teamscale.tia.runlistener + +/** + * Implements simple STDOUT logging for run listeners. We cannot use SLF4J, since the run listeners are executed in the + * context of the system under test, where we cannot rely on any SLF4J bindings to be present. + * + * To enable debug logging, specify `-Dtia.debug` for the JVM containing the run listener. + */ +class RunListenerLogger(private val callerClassName: String) { + private val debugEnabled: Boolean get() = + java.lang.Boolean.getBoolean("tia.debug") + + /** Logs a debug message. */ + fun debug(message: String) { + if (!debugEnabled) return + // we log to System.err instead of System.out as some runners will filter System.out, e.g. Maven + System.err.println("[DEBUG] $callerClassName - $message") + } + + /** Logs an error message and the stack trace of an optional throwable. */ + fun error(message: String, throwable: Throwable) { + System.err.println("[ERROR] $callerClassName - $message") + throwable.printStackTrace() + } + + companion object { + inline fun create() = RunListenerLogger(T::class.java.simpleName) + } +} From 6346133f9b573c084ec0db5b782040cf4f49779e Mon Sep 17 00:00:00 2001 From: Constructor Date: Wed, 27 Nov 2024 08:24:49 +0100 Subject: [PATCH 180/186] TS-38628 Tia Client migration --- .../testimpact/TestwiseCoverageAgentTest.java | 6 +- .../executor/TeamscaleAgentNotifier.java | 6 +- .../testframework/CustomTestFramework.java | 2 +- teamscale-client/build.gradle.kts | 2 +- tia-client/build.gradle.kts | 1 + .../tia.client/AgentCommunicationUtils.java | 55 ------ .../AgentHttpRequestFailedException.java | 18 -- .../tia.client/CommandLineInterface.java | 173 ----------------- .../tia.client/ITestwiseCoverageAgentApi.java | 104 ----------- .../com/teamscale/tia.client/RunningTest.java | 114 ------------ .../com/teamscale/tia.client/TestRun.java | 70 ------- .../TestRunWithClusteredSuggestions.java | 30 --- .../TestRunWithFlatSuggestions.java | 30 --- .../com/teamscale/tia.client/TiaAgent.java | 156 ---------------- .../com/teamscale/tia.client/UrlUtils.java | 37 ---- .../tia/client/AgentCommunicationUtils.kt | 55 ++++++ .../client/AgentHttpRequestFailedException.kt | 12 ++ .../tia/client/CommandLineInterface.kt | 174 ++++++++++++++++++ .../tia/client/ITestwiseCoverageAgentApi.kt | 100 ++++++++++ .../com/teamscale/tia/client/RunningTest.kt | 114 ++++++++++++ .../com/teamscale/tia/client/TestRun.kt | 62 +++++++ .../client/TestRunWithClusteredSuggestions.kt | 20 ++ .../tia/client/TestRunWithFlatSuggestions.kt | 20 ++ .../com/teamscale/tia/client/TiaAgent.kt | 142 ++++++++++++++ .../com/teamscale/tia/client/UrlUtils.kt | 32 ++++ .../client/AgentCommunicationUtilsTest.java | 67 ------- .../tia/client/AgentCommunicationUtilsTest.kt | 73 ++++++++ 27 files changed, 813 insertions(+), 862 deletions(-) delete mode 100644 tia-client/src/main/java/com/teamscale/tia.client/AgentCommunicationUtils.java delete mode 100644 tia-client/src/main/java/com/teamscale/tia.client/AgentHttpRequestFailedException.java delete mode 100644 tia-client/src/main/java/com/teamscale/tia.client/CommandLineInterface.java delete mode 100644 tia-client/src/main/java/com/teamscale/tia.client/ITestwiseCoverageAgentApi.java delete mode 100644 tia-client/src/main/java/com/teamscale/tia.client/RunningTest.java delete mode 100644 tia-client/src/main/java/com/teamscale/tia.client/TestRun.java delete mode 100644 tia-client/src/main/java/com/teamscale/tia.client/TestRunWithClusteredSuggestions.java delete mode 100644 tia-client/src/main/java/com/teamscale/tia.client/TestRunWithFlatSuggestions.java delete mode 100644 tia-client/src/main/java/com/teamscale/tia.client/TiaAgent.java delete mode 100644 tia-client/src/main/java/com/teamscale/tia.client/UrlUtils.java create mode 100644 tia-client/src/main/kotlin/com/teamscale/tia/client/AgentCommunicationUtils.kt create mode 100644 tia-client/src/main/kotlin/com/teamscale/tia/client/AgentHttpRequestFailedException.kt create mode 100644 tia-client/src/main/kotlin/com/teamscale/tia/client/CommandLineInterface.kt create mode 100644 tia-client/src/main/kotlin/com/teamscale/tia/client/ITestwiseCoverageAgentApi.kt create mode 100644 tia-client/src/main/kotlin/com/teamscale/tia/client/RunningTest.kt create mode 100644 tia-client/src/main/kotlin/com/teamscale/tia/client/TestRun.kt create mode 100644 tia-client/src/main/kotlin/com/teamscale/tia/client/TestRunWithClusteredSuggestions.kt create mode 100644 tia-client/src/main/kotlin/com/teamscale/tia/client/TestRunWithFlatSuggestions.kt create mode 100644 tia-client/src/main/kotlin/com/teamscale/tia/client/TiaAgent.kt create mode 100644 tia-client/src/main/kotlin/com/teamscale/tia/client/UrlUtils.kt delete mode 100644 tia-client/src/test/java/com/teamscale/tia/client/AgentCommunicationUtilsTest.java create mode 100644 tia-client/src/test/kotlin/com/teamscale/tia/client/AgentCommunicationUtilsTest.kt diff --git a/agent/src/test/java/com/teamscale/jacoco/agent/testimpact/TestwiseCoverageAgentTest.java b/agent/src/test/java/com/teamscale/jacoco/agent/testimpact/TestwiseCoverageAgentTest.java index 395adbec9..2daccefaf 100644 --- a/agent/src/test/java/com/teamscale/jacoco/agent/testimpact/TestwiseCoverageAgentTest.java +++ b/agent/src/test/java/com/teamscale/jacoco/agent/testimpact/TestwiseCoverageAgentTest.java @@ -86,9 +86,9 @@ public void testAccessViaTiaClientAndReportUploadToTeamscale() throws Exception TiaAgent agent = new TiaAgent(false, HttpUrl.get("http://localhost:" + port)); TestRunWithClusteredSuggestions testRun = agent.startTestRun(availableTests); - assertThat(testRun.getPrioritizedClusters()).hasSize(1); - assertThat(testRun.getPrioritizedClusters().get(0).tests).hasSize(1); - PrioritizableTest test = testRun.getPrioritizedClusters().get(0).tests.get(0); + assertThat(testRun.prioritizedClusters).hasSize(1); + assertThat(testRun.prioritizedClusters.get(0).tests).hasSize(1); + PrioritizableTest test = testRun.prioritizedClusters.get(0).tests.get(0); assertThat(test.testName).isEqualTo("test2"); RunningTest runningTest = testRun.startTest(test.testName); diff --git a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/executor/TeamscaleAgentNotifier.java b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/executor/TeamscaleAgentNotifier.java index ebb9aef49..21e9710cb 100644 --- a/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/executor/TeamscaleAgentNotifier.java +++ b/impacted-test-engine/src/main/java/com/teamscale/test_impacted/engine/executor/TeamscaleAgentNotifier.java @@ -33,7 +33,7 @@ public TeamscaleAgentNotifier(List testwiseCoverageAg public void startTest(String testUniformPath) { try { for (ITestwiseCoverageAgentApi apiService : testwiseCoverageAgentApis) { - apiService.testStarted(UrlUtils.percentEncode(testUniformPath)).execute(); + apiService.testStarted(UrlUtils.encodeUrl(testUniformPath)).execute(); } } catch (IOException e) { LOGGER.log(Level.SEVERE, e, () -> "Error while calling service api."); @@ -45,9 +45,9 @@ public void endTest(String testUniformPath, TestExecution testExecution) { try { for (ITestwiseCoverageAgentApi apiService : testwiseCoverageAgentApis) { if (testExecution == null) { - apiService.testFinished(UrlUtils.percentEncode(testUniformPath)).execute(); + apiService.testFinished(UrlUtils.encodeUrl(testUniformPath)).execute(); } else { - apiService.testFinished(UrlUtils.percentEncode(testUniformPath), testExecution).execute(); + apiService.testFinished(UrlUtils.encodeUrl(testUniformPath), testExecution).execute(); } } } catch (IOException e) { diff --git a/system-tests/tia-client-test/src/main/java/testframework/CustomTestFramework.java b/system-tests/tia-client-test/src/main/java/testframework/CustomTestFramework.java index 84c6b7a6a..7e26ddcfe 100644 --- a/system-tests/tia-client-test/src/main/java/testframework/CustomTestFramework.java +++ b/system-tests/tia-client-test/src/main/java/testframework/CustomTestFramework.java @@ -44,7 +44,7 @@ public void runTestsWithTia() throws AgentHttpRequestFailedException { allTests.keySet().stream().map(name -> new ClusteredTestDetails(name, name, null, null, null)) .collect(toList())); - for (PrioritizableTestCluster cluster : testRun.getPrioritizedClusters()) { + for (PrioritizableTestCluster cluster : testRun.prioritizedClusters) { for (PrioritizableTest test : cluster.tests) { Runnable runnable = allTests.get(test.testName); RunningTest runningTest = testRun.startTest(test.testName); diff --git a/teamscale-client/build.gradle.kts b/teamscale-client/build.gradle.kts index 952098003..263410236 100644 --- a/teamscale-client/build.gradle.kts +++ b/teamscale-client/build.gradle.kts @@ -1,9 +1,9 @@ plugins { + kotlin("jvm") `java-library` com.teamscale.`java-convention` com.teamscale.coverage com.teamscale.publish - kotlin("jvm") } publishAs { diff --git a/tia-client/build.gradle.kts b/tia-client/build.gradle.kts index c0fa57733..d49504163 100644 --- a/tia-client/build.gradle.kts +++ b/tia-client/build.gradle.kts @@ -1,4 +1,5 @@ plugins { + kotlin("jvm") `java-library` com.teamscale.`java-convention` com.teamscale.coverage diff --git a/tia-client/src/main/java/com/teamscale/tia.client/AgentCommunicationUtils.java b/tia-client/src/main/java/com/teamscale/tia.client/AgentCommunicationUtils.java deleted file mode 100644 index c1ad5581b..000000000 --- a/tia-client/src/main/java/com/teamscale/tia.client/AgentCommunicationUtils.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.teamscale.tia.client; - -import com.teamscale.client.HttpUtils; -import retrofit2.Call; -import retrofit2.Response; - -import java.io.IOException; -import java.util.function.Supplier; - -/** - * Utilities for performing requests to the agent. - */ -class AgentCommunicationUtils { - - /** - * Performs the given request and handles common errors (e.g. network failures, internal exceptions in the agent). - * In case of network problems, retries the request once. - */ - static T handleRequestError(Supplier> requestFactory, String errorMessage) - throws AgentHttpRequestFailedException { - return handleRequestError(requestFactory, errorMessage, true); - } - - private static T handleRequestError(Supplier> requestFactory, String errorMessage, boolean retryOnce) - throws AgentHttpRequestFailedException { - - try { - Response response = requestFactory.get().execute(); - if (response.isSuccessful()) { - return response.body(); - } - - String bodyString = HttpUtils.getErrorBodyStringSafe(response); - throw new AgentHttpRequestFailedException( - errorMessage + ". The agent responded with HTTP status " + response.code() + " " + response - .message() + ". Response body: " + bodyString); - } catch (IOException e) { - if (!retryOnce) { - throw new AgentHttpRequestFailedException( - errorMessage + ". I already retried this request and it failed twice (see the suppressed" + - " exception for details of the first failure). This is probably a network problem" + - " that you should address.", e); - } - - // retry once on network problems - try { - return handleRequestError(requestFactory, errorMessage, false); - } catch (Throwable t) { - t.addSuppressed(e); - throw t; - } - } - } - -} diff --git a/tia-client/src/main/java/com/teamscale/tia.client/AgentHttpRequestFailedException.java b/tia-client/src/main/java/com/teamscale/tia.client/AgentHttpRequestFailedException.java deleted file mode 100644 index b4a3a21c3..000000000 --- a/tia-client/src/main/java/com/teamscale/tia.client/AgentHttpRequestFailedException.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.teamscale.tia.client; - -/** - * Thrown if communicating with the agent via HTTP fails. The underlying reason can be either a network problem or an - * internal error in the agent. Users of this library should report these exceptions appropriately so the underlying - * problems can be addressed. - */ -public class AgentHttpRequestFailedException extends Exception { - - public AgentHttpRequestFailedException(String message) { - super(message); - } - - public AgentHttpRequestFailedException(String message, Throwable cause) { - super(message, cause); - } - -} diff --git a/tia-client/src/main/java/com/teamscale/tia.client/CommandLineInterface.java b/tia-client/src/main/java/com/teamscale/tia.client/CommandLineInterface.java deleted file mode 100644 index d43209f7f..000000000 --- a/tia-client/src/main/java/com/teamscale/tia.client/CommandLineInterface.java +++ /dev/null @@ -1,173 +0,0 @@ -package com.teamscale.tia.client; - -import static java.util.stream.Collectors.joining; - -import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import com.teamscale.client.ClusteredTestDetails; -import com.teamscale.client.JsonUtils; -import com.teamscale.client.PrioritizableTestCluster; -import com.teamscale.client.StringUtils; -import com.teamscale.report.testwise.model.ETestExecutionResult; -import com.teamscale.report.testwise.model.TestExecution; - -import okhttp3.HttpUrl; - -/** - * Simple command-line interface to expose the {@link TiaAgent} to non-Java test runners. - */ -public class CommandLineInterface { - - private static class InvalidCommandLineException extends RuntimeException { - public InvalidCommandLineException(String message) { - super(message); - } - } - - private final List arguments; - private final String command; - private final ITestwiseCoverageAgentApi api; - - public CommandLineInterface(String[] arguments) { - this.arguments = new ArrayList<>(Arrays.asList(arguments)); - if (arguments.length < 2) { - throw new InvalidCommandLineException( - "You must provide at least two arguments: the agent's URL and the command to execute"); - } - - HttpUrl url = HttpUrl.parse(this.arguments.remove(0)); - api = ITestwiseCoverageAgentApi.createService(url); - - command = this.arguments.remove(0); - } - - /** Entry point. */ - public static void main(String[] arguments) throws Exception { - new CommandLineInterface(arguments).runCommand(); - } - - private void runCommand() throws Exception { - switch (command) { - case "startTestRun": - startTestRun(); - break; - case "startTest": - startTest(); - break; - case "endTest": - endTest(); - break; - case "endTestRun": - endTestRun(); - break; - default: - throw new InvalidCommandLineException( - "Unknown command '" + command + "'. Should be one of startTestRun, startTest, endTest," + - " endTestRun"); - } - } - - private void endTestRun() throws Exception { - boolean partial; - if (arguments.size() == 1) { - partial = Boolean.parseBoolean(arguments.remove(0)); - } else { - partial = false; - } - AgentCommunicationUtils.handleRequestError(() -> api.testRunFinished(partial), - "Failed to create a coverage report and upload it to Teamscale. The coverage is most likely lost"); - } - - private void endTest() throws Exception { - if (arguments.size() < 2) { - throw new InvalidCommandLineException( - "You must provide the uniform path of the test that is about to be started" + - " as the first argument of the endTest command and the test result as the second."); - } - String uniformPath = arguments.remove(0); - ETestExecutionResult result = ETestExecutionResult.valueOf(arguments.remove(0).toUpperCase()); - - String message = readStdin(); - - // the agent already records test duration, so we can simply provide a dummy value here - TestExecution execution = new TestExecution(uniformPath, 0L, result, message); - AgentCommunicationUtils.handleRequestError( - () -> api.testFinished(UrlUtils.percentEncode(uniformPath), execution), - "Failed to end coverage recording for test case " + uniformPath + - ". Coverage for that test case is most likely lost."); - } - - private void startTest() throws Exception { - if (arguments.size() < 1) { - throw new InvalidCommandLineException( - "You must provide the uniform path of the test that is about to be started" + - " as the first argument of the startTest command"); - } - String uniformPath = arguments.remove(0); - AgentCommunicationUtils.handleRequestError(() -> api.testStarted(UrlUtils.percentEncode(uniformPath)), - "Failed to start coverage recording for test case " + uniformPath + - ". Coverage for that test case is lost."); - } - - private void startTestRun() throws Exception { - boolean includeNonImpacted = parseAndRemoveBooleanSwitch("include-non-impacted"); - Long baseline = parseAndRemoveLongParameter("baseline"); - String baselineRevision = parseAndRemoveStringParameter("baseline-revision"); - List availableTests = parseAvailableTestsFromStdin(); - - List clusters = AgentCommunicationUtils.handleRequestError(() -> - api.testRunStarted(includeNonImpacted, baseline, baselineRevision, availableTests), "Failed to start the test run"); - System.out.println(JsonUtils.serialize(clusters)); - } - - private List parseAvailableTestsFromStdin() throws java.io.IOException { - String json = readStdin(); - List availableTests = Collections.emptyList(); - if (!StringUtils.isEmpty(json)) { - availableTests = JsonUtils.deserializeList(json, ClusteredTestDetails.class); - } - return availableTests; - } - - private String readStdin() { - return new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8)).lines() - .collect(joining("\n")); - } - - private Long parseAndRemoveLongParameter(String name) { - for (int i = 0; i < arguments.size(); i++) { - if (arguments.get(i).startsWith("--" + name + "=")) { - String argument = arguments.remove(i); - return Long.parseLong(argument.substring(name.length() + 3)); - } - } - return null; - } - - private boolean parseAndRemoveBooleanSwitch(String name) { - for (int i = 0; i < arguments.size(); i++) { - if (arguments.get(i).equals("--" + name)) { - arguments.remove(i); - return true; - } - } - return false; - } - - private String parseAndRemoveStringParameter(String name) { - for (int i = 0; i < arguments.size(); i++) { - if (arguments.get(i).startsWith("--" + name + "=")) { - String argument = arguments.remove(i); - return argument.substring(name.length() + 3); - } - } - return null; - } - -} diff --git a/tia-client/src/main/java/com/teamscale/tia.client/ITestwiseCoverageAgentApi.java b/tia-client/src/main/java/com/teamscale/tia.client/ITestwiseCoverageAgentApi.java deleted file mode 100644 index c62000d30..000000000 --- a/tia-client/src/main/java/com/teamscale/tia.client/ITestwiseCoverageAgentApi.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.teamscale.tia.client; - -import java.util.List; -import java.util.concurrent.TimeUnit; - -import com.teamscale.client.ClusteredTestDetails; -import com.teamscale.client.PrioritizableTestCluster; -import com.teamscale.report.testwise.model.TestExecution; - -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.ResponseBody; -import retrofit2.Call; -import retrofit2.Retrofit; -import retrofit2.converter.jackson.JacksonConverterFactory; -import retrofit2.http.Body; -import retrofit2.http.POST; -import retrofit2.http.PUT; -import retrofit2.http.Path; -import retrofit2.http.Query; - -/** {@link Retrofit} API specification for the JaCoCo agent in test-wise coverage mode. */ -public interface ITestwiseCoverageAgentApi { - - /** Set the partition name as shown in Teamscale. */ - @PUT("partition") - Call setPartition(@Body String partition); - - /** Set the revision as shown in Teamscale. */ - @PUT("revision") - Call setRevision(@Body String partition); - - /** Set the upload commit as shown in Teamscale. */ - @PUT("commit") - Call setCommit(@Body String commit); - - /** Set the commit message with which the upload is shown in Teamscale. */ - @PUT("message") - Call setMessage(@Body String message); - - /** Test start. */ - @POST("test/start/{testUniformPath}") - Call testStarted(@Path(value = "testUniformPath", encoded = true) String testUniformPath); - - /** Test finished. */ - @POST("test/end/{testUniformPath}") - Call testFinished( - @Path(value = "testUniformPath", encoded = true) String testUniformPath - ); - - /** Test finished. */ - @POST("test/end/{testUniformPath}") - Call testFinished( - @Path(value = "testUniformPath", encoded = true) String testUniformPath, - @Body TestExecution testExecution - ); - - /** - * Test run started. Returns a single dummy cluster of TIA-selected and -prioritized tests - * that Teamscale currently knows about. - */ - @POST("testrun/start") - Call> testRunStarted( - @Query("include-non-impacted") boolean includeNonImpacted, - @Query("baseline") Long baseline, - @Query("baseline-revision") String baselineRevision - ); - - /** - * Test run started. Returns the list of TIA-selected and -prioritized test clusters to execute. - */ - @POST("testrun/start") - Call> testRunStarted( - @Query("include-non-impacted") boolean includeNonImpacted, - @Query("baseline") Long baseline, - @Query("baseline-revision") String baselineRevision, - @Body List availableTests - ); - - /** - * Test run finished. Generate test-wise coverage report and upload to Teamscale. - * - * @param partial Whether the test recording only contains a subset of the available tests. - */ - @POST("testrun/end") - Call testRunFinished(@Query("partial") Boolean partial); - - /** - * Generates a {@link Retrofit} instance for this service, which uses basic auth to authenticate against the server - * and which sets the Accept header to JSON. - */ - static ITestwiseCoverageAgentApi createService(HttpUrl baseUrl) { - OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder(); - httpClientBuilder.connectTimeout(60, TimeUnit.SECONDS); - httpClientBuilder.readTimeout(120, TimeUnit.SECONDS); - httpClientBuilder.writeTimeout(60, TimeUnit.SECONDS); - Retrofit retrofit = new Retrofit.Builder() - .client(httpClientBuilder.build()) // - .baseUrl(baseUrl) // - .addConverterFactory(JacksonConverterFactory.create()) // - .build(); - return retrofit.create(ITestwiseCoverageAgentApi.class); - } -} diff --git a/tia-client/src/main/java/com/teamscale/tia.client/RunningTest.java b/tia-client/src/main/java/com/teamscale/tia.client/RunningTest.java deleted file mode 100644 index fe991f39a..000000000 --- a/tia-client/src/main/java/com/teamscale/tia.client/RunningTest.java +++ /dev/null @@ -1,114 +0,0 @@ -package com.teamscale.tia.client; - -import com.teamscale.client.JsonUtils; -import com.teamscale.client.StringUtils; -import com.teamscale.report.testwise.model.TestExecution; -import com.teamscale.report.testwise.model.TestInfo; -import okhttp3.ResponseBody; - -import java.io.IOException; - -/** - * Represents a single test that is currently being executed by the caller of this library. Use - * {@link #endTest(TestRun.TestResultWithMessage)} or {@link #endTestAndRetrieveCoverage(TestRun.TestResultWithMessage)} - * to signal that executing the test case has finished and test-wise coverage for this test should be stored. - */ -@SuppressWarnings("unused") -public class RunningTest { - - private static class AgentConfigurationMismatch extends RuntimeException { - private AgentConfigurationMismatch(String message) { - super(message); - } - } - - private final String uniformPath; - private final ITestwiseCoverageAgentApi api; - - public RunningTest(String uniformPath, ITestwiseCoverageAgentApi api) { - this.uniformPath = uniformPath; - this.api = api; - } - - /** - * Signals to the agent that the test runner has finished executing this test and the result of the test run. - * - * @throws AgentHttpRequestFailedException if communicating with the agent fails or in case of internal errors. This - * method already retries the request once, so this is likely a terminal - * failure. The caller should record this problem appropriately. Coverage - * for subsequent test cases could, however, potentially still be recorded. - * Thus, the caller should continue with test execution and continue - * informing the coverage agent about further test start and end events. - */ - public void endTest(TestRun.TestResultWithMessage result) throws AgentHttpRequestFailedException { - // the agent already records test duration, so we can simply provide a dummy value here - TestExecution execution = new TestExecution(uniformPath, 0L, result.result, - result.message); - ResponseBody body = AgentCommunicationUtils - .handleRequestError(() -> api.testFinished(UrlUtils.percentEncode(uniformPath), execution), - "Failed to end coverage recording for test case " + uniformPath + - ". Coverage for that test case is most likely lost."); - - if (!StringUtils.isBlank(readBodyStringNullSafe(body))) { - throw new AgentConfigurationMismatch("The agent seems to be configured to return test coverage via" + - " HTTP to the tia-client (agent option `tia-mode=http`) but you did not instruct the" + - " tia-client to handle this. Please either reconfigure the agent or call" + - " #endTestAndRetrieveCoverage() instead of this method and handle the returned coverage." + - " As it is currently configured, the agent will not store or process the recorded coverage" + - " in any way other than sending it to the tia-client via HTTP so it is lost permanently."); - } - } - - private String readBodyStringNullSafe(ResponseBody body) throws AgentHttpRequestFailedException { - if (body == null) { - return null; - } - - try { - return body.string(); - } catch (IOException e) { - throw new AgentHttpRequestFailedException("Unable to read agent HTTP response body string", e); - } - } - - /** - * Signals to the agent that the test runner has finished executing this test and the result of the test run. It - * will also parse the testwise coverage data returned by the agent for this test and return it so it can be - * manually processed by you. The agent will not store or otherwise process this coverage, so be sure to do so - * yourself. - *

    - * This method assumes that the agent is configured to return each test's coverage data via HTTP. It will receive - * and parse the data. If the agent is configured differently, this method will throw a terminal - * {@link RuntimeException}. In this case, you must reconfigure the agent with the `tia-mode=http` option enabled. - * - * @throws AgentHttpRequestFailedException if communicating with the agent fails or in case of internal errors. This - * method already retries the request once, so this is likely a terminal - * failure. The recorded coverage is likely lost. The caller should log this - * problem appropriately. - */ - public TestInfo endTestAndRetrieveCoverage( - TestRun.TestResultWithMessage result) throws AgentHttpRequestFailedException { - // the agent already records test duration, so we can simply provide a dummy value here - TestExecution execution = new TestExecution(uniformPath, 0L, result.result, - result.message); - ResponseBody body = AgentCommunicationUtils.handleRequestError( - () -> api.testFinished(UrlUtils.percentEncode(uniformPath), execution), - "Failed to end coverage recording for test case " + uniformPath + - ". Coverage for that test case is most likely lost."); - - String json = readBodyStringNullSafe(body); - if (StringUtils.isBlank(json)) { - throw new AgentConfigurationMismatch("You asked the tia-client to retrieve this test's coverage via HTTP" + - " but the agent is not configured for this. Please reconfigure the agent to use `tia-mode=http`."); - } - - try { - return JsonUtils.deserialize(json, TestInfo.class); - } catch (IOException e) { - throw new AgentHttpRequestFailedException("Unable to parse the JSON returned by the agent. Maybe you have" + - " a version mismatch between the tia-client and the agent?. Json returned by the agent: `" + json + - "`", e); - } - } - -} diff --git a/tia-client/src/main/java/com/teamscale/tia.client/TestRun.java b/tia-client/src/main/java/com/teamscale/tia.client/TestRun.java deleted file mode 100644 index 48a122922..000000000 --- a/tia-client/src/main/java/com/teamscale/tia.client/TestRun.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.teamscale.tia.client; - -import com.teamscale.report.testwise.model.ETestExecutionResult; - -/** - * Use this class to report test start and end events and upload testwise coverage to Teamscale. - *

    - * After having run all tests, call {@link #endTestRun(boolean)} to create a testwise coverage report and upload it to - * Teamscale. This requires that you configured the agent to upload coverage to Teamscale - * (`tia-mode=teamscale-upload`). - */ -public class TestRun { - - private final ITestwiseCoverageAgentApi api; - - TestRun(ITestwiseCoverageAgentApi api) { - this.api = api; - } - - /** - * Represents the result of running a single test. - */ - public static class TestResultWithMessage { - - /** Whether the test succeeded or failed. */ - public final ETestExecutionResult result; - - /** An optional message, e.g. a stack trace in case of test failures. */ - public final String message; - - public TestResultWithMessage(ETestExecutionResult result, String message) { - this.result = result; - this.message = message; - } - } - - /** - * Informs the testwise coverage agent that a new test is about to start. - * - * @throws AgentHttpRequestFailedException if communicating with the agent fails or in case of internal errors. In - * this case, the agent probably doesn't know that this test case was - * started, so its coverage is lost. This method already retries the request - * once, so this is likely a terminal failure. The caller should log this - * problem appropriately. Coverage for subsequent test cases could, however, - * potentially still be recorded. Thus, the caller should continue with test - * execution and continue informing the coverage agent about further test - * start and end events. - */ - public RunningTest startTest(String uniformPath) throws AgentHttpRequestFailedException { - AgentCommunicationUtils.handleRequestError(() -> api.testStarted(UrlUtils.percentEncode(uniformPath)), - "Failed to start coverage recording for test case " + uniformPath); - return new RunningTest(uniformPath, api); - } - - /** - * Informs the testwise coverage agent that the caller has finished running all tests and should upload coverage to - * Teamscale. Only call this if you configured the agent to upload coverage to Teamscale - * (`tia-mode=teamscale-upload`). Otherwise, this method will throw an exception. - * - * @throws AgentHttpRequestFailedException if communicating with the agent fails or in case of internal errors. This - * method already retries the request once, so this is likely a terminal - * failure. The recorded coverage is likely lost. The caller should log this - * problem appropriately. - */ - public void endTestRun(boolean partial) throws AgentHttpRequestFailedException { - AgentCommunicationUtils.handleRequestError(() -> api.testRunFinished(partial), - "Failed to create a coverage report and upload it to Teamscale. The coverage is most likely lost"); - } - -} diff --git a/tia-client/src/main/java/com/teamscale/tia.client/TestRunWithClusteredSuggestions.java b/tia-client/src/main/java/com/teamscale/tia.client/TestRunWithClusteredSuggestions.java deleted file mode 100644 index 31e189432..000000000 --- a/tia-client/src/main/java/com/teamscale/tia.client/TestRunWithClusteredSuggestions.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.teamscale.tia.client; - -import com.teamscale.client.PrioritizableTestCluster; - -import java.util.List; - -/** - * Represents a run of prioritized and selected test clusters as reported by the TIA. Use this class to report test - * start and end events and upload testwise coverage to Teamscale. - *

    - * The caller of this class should retrieve the test clusters to execute from {@link #getPrioritizedClusters()}, run - * them (in the given order if possible) and report test start and end events via {@link #startTest(String)} )}. - *

    - * After having run all tests, call {@link #endTestRun(boolean)} to create a testwise coverage report and upload it to - * Teamscale. - */ -public class TestRunWithClusteredSuggestions extends TestRun { - - private final List prioritizedClusters; - - TestRunWithClusteredSuggestions(ITestwiseCoverageAgentApi api, - List prioritizedClusters) { - super(api); - this.prioritizedClusters = prioritizedClusters; - } - - public List getPrioritizedClusters() { - return prioritizedClusters; - } -} diff --git a/tia-client/src/main/java/com/teamscale/tia.client/TestRunWithFlatSuggestions.java b/tia-client/src/main/java/com/teamscale/tia.client/TestRunWithFlatSuggestions.java deleted file mode 100644 index 3e57845d9..000000000 --- a/tia-client/src/main/java/com/teamscale/tia.client/TestRunWithFlatSuggestions.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.teamscale.tia.client; - -import com.teamscale.client.PrioritizableTest; - -import java.util.List; - -/** - * Represents a run of a flat list of prioritized and selected tests as reported by the TIA. Use this class to report - * test start and end events and upload testwise coverage to Teamscale. - *

    - * The caller of this class should retrieve the tests to execute from {@link #getPrioritizedTests()}, run them (in the - * given order if possible) and report test start and end events via {@link #startTest(String)} )}. - *

    - * After having run all tests, call {@link #endTestRun(boolean)} to create a testwise coverage report and upload it to - * Teamscale. - */ -public class TestRunWithFlatSuggestions extends TestRun { - - private final List prioritizedTests; - - TestRunWithFlatSuggestions(ITestwiseCoverageAgentApi api, - List prioritizedTests) { - super(api); - this.prioritizedTests = prioritizedTests; - } - - public List getPrioritizedTests() { - return prioritizedTests; - } -} diff --git a/tia-client/src/main/java/com/teamscale/tia.client/TiaAgent.java b/tia-client/src/main/java/com/teamscale/tia.client/TiaAgent.java deleted file mode 100644 index ac165df1d..000000000 --- a/tia-client/src/main/java/com/teamscale/tia.client/TiaAgent.java +++ /dev/null @@ -1,156 +0,0 @@ -package com.teamscale.tia.client; - -import java.time.Instant; -import java.util.List; - -import com.teamscale.client.ClusteredTestDetails; -import com.teamscale.client.PrioritizableTestCluster; - -import okhttp3.HttpUrl; - -/** - * Communicates with one Teamscale JaCoCo agent in testwise coverage mode to facilitate the Test Impact analysis. - *

    - * Use this class to retrieve impacted tests from Teamscale, start a {@link TestRun} based on these selected and - * prioritized tests and upload test-wise coverage after having executed the tests. - *

    - * The caller of this class is responsible for actually executing the tests. - * - *

    Available Tests

    - * This API allows you to pass a list of available tests to start a test run. This list is used for multiple purposes: - * - *
      - *
    • To determine if any tests changed, were added or removed: Teamscale will not suggest deleted tests and - * will always suggest changed or added tests. - *
    • To cluster tests: The list of available tests includes information about test clusters, which are logical - * groups of tests. Teamscale will only prioritize tests against each other if they are in the same cluster. - *
    - */ -public class TiaAgent { - - private final boolean includeNonImpactedTests; - private final ITestwiseCoverageAgentApi api; - - /** - * @param includeNonImpactedTests if this is true, only prioritization is performed, no test selection. - * @param url URL under which the agent is reachable. - */ - public TiaAgent(boolean includeNonImpactedTests, HttpUrl url) { - this.includeNonImpactedTests = includeNonImpactedTests; - api = ITestwiseCoverageAgentApi.createService(url); - } - - /** - * Starts a test run but does not ask Teamscale to prioritize and select any test cases. Use this when you only want - * to record test-wise coverage and don't care about TIA's test selection and prioritization. - */ - public TestRun startTestRunWithoutTestSelection() { - return new TestRun(api); - } - - /** - * Runs the TIA to determine which of the given available tests should be run and in which order. This method - * considers all changes since the last time that test-wise coverage was uploaded. In most situations this is the - * optimal behaviour. - * - * @param availableTests A list of all available tests. This is used to determine which tests need to be run, e.g. - * because they are completely new or changed since the last run. If you provide an empty - * list, no tests will be selected. The clustering information in this list is used to - * construct the test clusters in the returned {@link TestRunWithClusteredSuggestions}. - * @throws AgentHttpRequestFailedException e.g. if the agent or Teamscale is not reachable or an internal error - * occurs. This method already retries the request once, so this is likely a - * terminal failure. You should simply fall back to running all tests in - * this case and not communicate further with the agent. You should visibly - * report this problem so it can be fixed. - */ - public TestRunWithClusteredSuggestions startTestRun( - List availableTests) throws AgentHttpRequestFailedException { - return startTestRun(availableTests, null, null); - } - - /** - * Runs the TIA to determine which of the given available tests should be run and in which order. This method - * considers all changes since the last time that test-wise coverage was uploaded. In most situations this is the - * optimal behaviour. - *

    - * Using this method, Teamscale will perform the selection and prioritization based on the tests it currently knows - * about. In this case, it will not automatically include changed or new tests in the selection (since it doesn't - * know about these changes) and it may return deleted tests (since it doesn't know about the deletions). It will - * also not cluster the tests as {@link #startTestRun(List)} would. - *

    - * Thus, we recommend that, if possible, you use {@link #startTestRun(List)} instead. - * - * @throws AgentHttpRequestFailedException e.g. if the agent or Teamscale is not reachable or an internal error - * occurs. This method already retries the request once, so this is likely a - * terminal failure. You should simply fall back to running all tests in - * this case and not communicate further with the agent. You should visibly - * report this problem so it can be fixed. - */ - public TestRunWithFlatSuggestions startTestRunAssumingUnchangedTests() throws AgentHttpRequestFailedException { - return startTestRunAssumingUnchangedTests(null, null); - } - - /** - * Runs the TIA to determine which of the given available tests should be run and in which order. This method - * considers all changes since the given baseline timestamp. - * - * @param availableTests A list of all available tests. This is used to determine which tests need to be run, e.g. - * because they are completely new or changed since the last run. If you provide an empty - * list, no tests will be selected.The clustering information in this list is used to - * construct the test clusters in the returned {@link TestRunWithClusteredSuggestions}. - * @param baseline Consider all code changes since this date when calculating the impacted tests. - * @param baselineRevision Same as baseline but accepts a revision (e.g. git SHA1) instead of a branch and timestamp - * @throws AgentHttpRequestFailedException e.g. if the agent or Teamscale is not reachable or an internal error - * occurs. You should simply fall back to running all tests in this case. - */ - public TestRunWithClusteredSuggestions startTestRun(List availableTests, - Instant baseline, String baselineRevision) throws AgentHttpRequestFailedException { - if (availableTests == null) { - throw new IllegalArgumentException("availableTests must not be null. If you cannot provide a list of" + - " available tests, please use startTestRunAssumingUnchangedTests instead - but please be aware" + - " that this method of using the TIA cannot take into account changes in the tests themselves."); - } - Long baselineTimestamp = calculateBaselineTimestamp(baseline); - List clusters = AgentCommunicationUtils.handleRequestError( - () -> api.testRunStarted(includeNonImpactedTests, baselineTimestamp, baselineRevision, availableTests), - "Failed to start the test run"); - return new TestRunWithClusteredSuggestions(api, clusters); - } - - /** - * Runs the TIA to determine which of the given available tests should be run and in which order. This method - * considers all changes since the given baseline timestamp. - *

    - * Using this method, Teamscale will perform the selection and prioritization based on the tests it currently knows - * about. In this case, it will not automatically include changed or new tests in the selection (since it doesn't - * know about these changes) and it may return deleted tests (since it doesn't know about the deletions). It will * - * also not cluster the tests as {@link #startTestRun(List, Instant, String)} would. - *

    - * Thus, we recommend that, if possible, you use {@link #startTestRun(List, Instant, String)} - * instead. - * - * @throws AgentHttpRequestFailedException e.g. if the agent or Teamscale is not reachable or an internal error - * occurs. This method already retries the request once, so this is likely a - * terminal failure. You should simply fall back to running all tests in - * this case and not communicate further with the agent. You should visibly - * report this problem so it can be fixed. - */ - public TestRunWithFlatSuggestions startTestRunAssumingUnchangedTests( - Instant baseline, String baselineRevision) throws AgentHttpRequestFailedException { - Long baselineTimestamp = calculateBaselineTimestamp(baseline); - List clusters = AgentCommunicationUtils.handleRequestError( - () -> api.testRunStarted(includeNonImpactedTests, baselineTimestamp, baselineRevision), - "Failed to start the test run"); - return new TestRunWithFlatSuggestions(api, clusters.get(0).tests); - } - - private Long calculateBaselineTimestamp(Instant baseline) { - Long baselineTimestamp = null; - if (baseline != null) { - baselineTimestamp = baseline.toEpochMilli(); - } - return baselineTimestamp; - } - - -} diff --git a/tia-client/src/main/java/com/teamscale/tia.client/UrlUtils.java b/tia-client/src/main/java/com/teamscale/tia.client/UrlUtils.java deleted file mode 100644 index 84669da51..000000000 --- a/tia-client/src/main/java/com/teamscale/tia.client/UrlUtils.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.teamscale.tia.client; - -/** Utils for URL encoding as the Java internal URLEncoder does not handle all escape-worthy symbols for path segments. */ -public class UrlUtils { - - /** - * Percent-decodes a string, such as used in a URL Path (not a query string / form encode, which uses + for spaces, - * etc). - * Source: https://stackoverflow.com/a/44076794 - */ - public static String percentEncode(String encodeMe) { - if (encodeMe == null) { - return ""; - } - String encoded = encodeMe.replace("%", "%25"); - encoded = encoded.replace(" ", "%20"); - encoded = encoded.replace("!", "%21"); - encoded = encoded.replace("#", "%23"); - encoded = encoded.replace("$", "%24"); - encoded = encoded.replace("&", "%26"); - encoded = encoded.replace("'", "%27"); - encoded = encoded.replace("(", "%28"); - encoded = encoded.replace(")", "%29"); - encoded = encoded.replace("*", "%2A"); - encoded = encoded.replace("+", "%2B"); - encoded = encoded.replace(",", "%2C"); - encoded = encoded.replace("/", "%2F"); - encoded = encoded.replace(":", "%3A"); - encoded = encoded.replace(";", "%3B"); - encoded = encoded.replace("=", "%3D"); - encoded = encoded.replace("?", "%3F"); - encoded = encoded.replace("@", "%40"); - encoded = encoded.replace("[", "%5B"); - encoded = encoded.replace("]", "%5D"); - return encoded; - } -} diff --git a/tia-client/src/main/kotlin/com/teamscale/tia/client/AgentCommunicationUtils.kt b/tia-client/src/main/kotlin/com/teamscale/tia/client/AgentCommunicationUtils.kt new file mode 100644 index 000000000..a6e46c314 --- /dev/null +++ b/tia-client/src/main/kotlin/com/teamscale/tia/client/AgentCommunicationUtils.kt @@ -0,0 +1,55 @@ +package com.teamscale.tia.client + +import com.teamscale.client.HttpUtils.getErrorBodyStringSafe +import retrofit2.Call +import java.io.IOException +import java.util.function.Supplier + +/** + * Utilities for performing requests to the agent. + */ +internal object AgentCommunicationUtils { + /** + * Performs the given request and handles common errors (e.g., network failures, internal exceptions in the agent). + * In case of network problems, retries the request once. + */ + @Throws(AgentHttpRequestFailedException::class) + fun handleRequestError(errorMessage: String, requestFactory: () -> Call) = + handleRequestError(requestFactory, errorMessage, true) + + @Throws(AgentHttpRequestFailedException::class) + private fun handleRequestError( + requestFactory: () -> Call, + errorMessage: String, + retryOnce: Boolean + ): T? { + try { + val response = requestFactory().execute() + if (response.isSuccessful) { + return response.body() + } + + val bodyString = getErrorBodyStringSafe(response) + throw AgentHttpRequestFailedException( + errorMessage + ". The agent responded with HTTP status " + response.code() + " " + response + .message() + ". Response body: " + bodyString + ) + } catch (e: IOException) { + if (!retryOnce) { + throw AgentHttpRequestFailedException( + errorMessage + ". I already retried this request and it failed twice (see the suppressed" + + " exception for details of the first failure). This is probably a network problem" + + " that you should address.", e + ) + } + + // retry once on network problems + try { + return handleRequestError(requestFactory, errorMessage, false) + } catch (t: Throwable) { + t.addSuppressed(e) + throw t + } + } + } +} diff --git a/tia-client/src/main/kotlin/com/teamscale/tia/client/AgentHttpRequestFailedException.kt b/tia-client/src/main/kotlin/com/teamscale/tia/client/AgentHttpRequestFailedException.kt new file mode 100644 index 000000000..40fabbf79 --- /dev/null +++ b/tia-client/src/main/kotlin/com/teamscale/tia/client/AgentHttpRequestFailedException.kt @@ -0,0 +1,12 @@ +package com.teamscale.tia.client + +/** + * Thrown if communicating with the agent via HTTP fails. The underlying reason can be either a network problem or an + * internal error in the agent. Users of this library should report these exceptions appropriately so the underlying + * problems can be addressed. + */ +class AgentHttpRequestFailedException : Exception { + constructor(message: String?) : super(message) + + constructor(message: String?, cause: Throwable?) : super(message, cause) +} diff --git a/tia-client/src/main/kotlin/com/teamscale/tia/client/CommandLineInterface.kt b/tia-client/src/main/kotlin/com/teamscale/tia/client/CommandLineInterface.kt new file mode 100644 index 000000000..5648736a5 --- /dev/null +++ b/tia-client/src/main/kotlin/com/teamscale/tia/client/CommandLineInterface.kt @@ -0,0 +1,174 @@ +package com.teamscale.tia.client + +import com.teamscale.client.ClusteredTestDetails +import com.teamscale.client.JsonUtils.deserializeList +import com.teamscale.client.JsonUtils.serialize +import com.teamscale.client.PrioritizableTestCluster +import com.teamscale.client.StringUtils.isEmpty +import com.teamscale.report.testwise.model.ETestExecutionResult +import com.teamscale.report.testwise.model.TestExecution +import com.teamscale.tia.client.AgentCommunicationUtils.handleRequestError +import com.teamscale.tia.client.UrlUtils.encodeUrl +import okhttp3.HttpUrl +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStreamReader +import java.nio.charset.StandardCharsets +import java.util.* +import java.util.function.Supplier +import java.util.stream.Collectors + +/** + * Simple command-line interface to expose the [TiaAgent] to non-Java test runners. + */ +class CommandLineInterface(arguments: Array) { + private class InvalidCommandLineException(message: String?) : RuntimeException(message) + + private val arguments: MutableList = + ArrayList(listOf(*arguments)) + private val command: String + private val api: ITestwiseCoverageAgentApi + + init { + if (arguments.size < 2) { + throw InvalidCommandLineException( + "You must provide at least two arguments: the agent's URL and the command to execute" + ) + } + + val url = HttpUrl.parse(this.arguments.removeAt(0)) + api = ITestwiseCoverageAgentApi.createService(url!!) + + command = this.arguments.removeAt(0) + } + + @Throws(Exception::class) + private fun runCommand() { + when (command) { + "startTestRun" -> startTestRun() + "startTest" -> startTest() + "endTest" -> endTest() + "endTestRun" -> endTestRun() + else -> throw InvalidCommandLineException( + "Unknown command '" + command + "'. Should be one of startTestRun, startTest, endTest," + + " endTestRun" + ) + } + } + + @Throws(Exception::class) + private fun endTestRun() { + val partial = if (arguments.size == 1) { + arguments.removeAt(0).toBoolean() + } else { + false + } + handleRequestError( + "Failed to create a coverage report and upload it to Teamscale. The coverage is most likely lost" + ) { api.testRunFinished(partial) } + } + + @Throws(Exception::class) + private fun endTest() { + if (arguments.size < 2) { + throw InvalidCommandLineException( + "You must provide the uniform path of the test that is about to be started" + + " as the first argument of the endTest command and the test result as the second." + ) + } + val uniformPath = arguments.removeAt(0) + val result = ETestExecutionResult.valueOf(arguments.removeAt(0).uppercase(Locale.getDefault())) + + val message = readStdin() + + // the agent already records test duration, so we can simply provide a dummy value here + val execution = TestExecution(uniformPath, 0L, result, message) + handleRequestError( + "Failed to end coverage recording for test case $uniformPath. Coverage for that test case is most likely lost." + ) { api.testFinished(uniformPath.encodeUrl(), execution) } + } + + @Throws(Exception::class) + private fun startTest() { + if (arguments.size < 1) { + throw InvalidCommandLineException( + "You must provide the uniform path of the test that is about to be started" + + " as the first argument of the startTest command" + ) + } + val uniformPath = arguments.removeAt(0) + handleRequestError( + "Failed to start coverage recording for test case $uniformPath. Coverage for that test case is lost." + ) { api.testStarted(uniformPath.encodeUrl()) } + } + + @Throws(Exception::class) + private fun startTestRun() { + val includeNonImpacted = parseAndRemoveBooleanSwitch("include-non-impacted") + val baseline = parseAndRemoveLongParameter("baseline") + val baselineRevision = parseAndRemoveStringParameter("baseline-revision") + val availableTests = parseAvailableTestsFromStdin() + + val clusters = handleRequestError( + "Failed to start the test run" + ) { api.testRunStarted(includeNonImpacted, baseline, baselineRevision, availableTests) } + println(serialize(clusters!!)) + } + + @Throws(IOException::class) + private fun parseAvailableTestsFromStdin(): List { + val json = readStdin() + var availableTests = emptyList() + if (!isEmpty(json)) { + availableTests = deserializeList( + json, + ClusteredTestDetails::class.java + ) + } + return availableTests + } + + private fun readStdin(): String { + return BufferedReader(InputStreamReader(System.`in`, StandardCharsets.UTF_8)).lines() + .collect(Collectors.joining("\n")) + } + + private fun parseAndRemoveLongParameter(name: String): Long? { + for (i in arguments.indices) { + if (arguments[i].startsWith("--$name=")) { + val argument = arguments.removeAt(i) + return argument.substring(name.length + 3).toLong() + } + } + return null + } + + private fun parseAndRemoveBooleanSwitch(name: String): Boolean { + for (i in arguments.indices) { + if (arguments[i] == "--$name") { + arguments.removeAt(i) + return true + } + } + return false + } + + private fun parseAndRemoveStringParameter(name: String): String? { + for (i in arguments.indices) { + if (arguments[i].startsWith("--$name=")) { + val argument = arguments.removeAt(i) + return argument.substring(name.length + 3) + } + } + return null + } + + companion object { + /** Entry point. */ + @Throws(Exception::class) + @JvmStatic + fun main(arguments: Array) { + CommandLineInterface(arguments).runCommand() + } + } +} diff --git a/tia-client/src/main/kotlin/com/teamscale/tia/client/ITestwiseCoverageAgentApi.kt b/tia-client/src/main/kotlin/com/teamscale/tia/client/ITestwiseCoverageAgentApi.kt new file mode 100644 index 000000000..b0d67e109 --- /dev/null +++ b/tia-client/src/main/kotlin/com/teamscale/tia/client/ITestwiseCoverageAgentApi.kt @@ -0,0 +1,100 @@ +package com.teamscale.tia.client + +import com.teamscale.client.ClusteredTestDetails +import com.teamscale.client.PrioritizableTestCluster +import com.teamscale.report.testwise.model.TestExecution +import okhttp3.HttpUrl +import okhttp3.OkHttpClient +import okhttp3.ResponseBody +import retrofit2.Call +import retrofit2.Retrofit +import retrofit2.converter.jackson.JacksonConverterFactory +import retrofit2.http.* +import java.util.concurrent.TimeUnit + +/** [Retrofit] API specification for the JaCoCo agent in test-wise coverage mode. */ +interface ITestwiseCoverageAgentApi { + /** Set the partition name as shown in Teamscale. */ + @PUT("partition") + fun setPartition(@Body partition: String): Call + + /** Set the revision as shown in Teamscale. */ + @PUT("revision") + fun setRevision(@Body partition: String): Call + + /** Set the upload commit as shown in Teamscale. */ + @PUT("commit") + fun setCommit(@Body commit: String): Call + + /** Set the commit message with which the upload is shown in Teamscale. */ + @PUT("message") + fun setMessage(@Body message: String): Call + + /** Test start. */ + @POST("test/start/{testUniformPath}") + fun testStarted(@Path(value = "testUniformPath", encoded = true) testUniformPath: String): Call + + /** Test finished. */ + @POST("test/end/{testUniformPath}") + fun testFinished( + @Path(value = "testUniformPath", encoded = true) testUniformPath: String + ): Call + + /** Test finished. */ + @POST("test/end/{testUniformPath}") + fun testFinished( + @Path(value = "testUniformPath", encoded = true) testUniformPath: String, + @Body testExecution: TestExecution + ): Call + + /** + * Test run started. Returns a single dummy cluster of TIA-selected and -prioritized tests + * that Teamscale currently knows about. + */ + @POST("testrun/start") + fun testRunStarted( + @Query("include-non-impacted") includeNonImpacted: Boolean, + @Query("baseline") baseline: Long?, + @Query("baseline-revision") baselineRevision: String? + ): Call> + + /** + * Test run started. Returns the list of TIA-selected and -prioritized test clusters to execute. + */ + @POST("testrun/start") + fun testRunStarted( + @Query("include-non-impacted") includeNonImpacted: Boolean, + @Query("baseline") baseline: Long?, + @Query("baseline-revision") baselineRevision: String?, + @Body availableTests: List + ): Call> + + /** + * Test run finished. Generate test-wise coverage report and upload to Teamscale. + * + * @param partial Whether the test recording only contains a subset of the available tests. + */ + @POST("testrun/end") + fun testRunFinished(@Query("partial") partial: Boolean): Call + + companion object { + /** + * Generates a [Retrofit] instance for this service, which uses basic auth to authenticate against the server + * and which sets the Accept header to JSON. + */ + @JvmStatic + fun createService(baseUrl: HttpUrl): ITestwiseCoverageAgentApi { + val httpClientBuilder = OkHttpClient.Builder().apply { + connectTimeout(60, TimeUnit.SECONDS) + readTimeout(120, TimeUnit.SECONDS) + writeTimeout(60, TimeUnit.SECONDS) + } + return Retrofit.Builder() + .client(httpClientBuilder.build()) + .baseUrl(baseUrl) + .addConverterFactory(JacksonConverterFactory.create()) + .build() + .create(ITestwiseCoverageAgentApi::class.java) + } + } +} diff --git a/tia-client/src/main/kotlin/com/teamscale/tia/client/RunningTest.kt b/tia-client/src/main/kotlin/com/teamscale/tia/client/RunningTest.kt new file mode 100644 index 000000000..444883be7 --- /dev/null +++ b/tia-client/src/main/kotlin/com/teamscale/tia/client/RunningTest.kt @@ -0,0 +1,114 @@ +package com.teamscale.tia.client + +import com.teamscale.client.JsonUtils.deserialize +import com.teamscale.client.StringUtils.isBlank +import com.teamscale.report.testwise.model.TestExecution +import com.teamscale.report.testwise.model.TestInfo +import com.teamscale.tia.client.AgentCommunicationUtils.handleRequestError +import com.teamscale.tia.client.TestRun.TestResultWithMessage +import com.teamscale.tia.client.UrlUtils.encodeUrl +import okhttp3.ResponseBody +import java.io.IOException + +/** + * Represents a single test that is currently being executed by the caller of this library. Use + * [.endTest] or [.endTestAndRetrieveCoverage] + * to signal that executing the test case has finished and test-wise coverage for this test should be stored. + */ +@Suppress("unused") +class RunningTest(private val uniformPath: String, private val api: ITestwiseCoverageAgentApi) { + private class AgentConfigurationMismatch(message: String?) : RuntimeException(message) + + /** + * Signals to the agent that the test runner has finished executing this test and the result of the test run. + * + * @throws AgentHttpRequestFailedException if communicating with the agent fails or in case of internal errors. This + * method already retries the request once, so this is likely a terminal + * failure. The caller should record this problem appropriately. Coverage + * for subsequent test cases could, however, potentially still be recorded. + * Thus, the caller should continue with test execution and continue + * informing the coverage agent about further test start and end events. + */ + @Throws(AgentHttpRequestFailedException::class) + fun endTest(result: TestResultWithMessage) { + // the agent already records test duration, so we can simply provide a dummy value here + val execution = TestExecution( + uniformPath, 0L, result.result, + result.message + ) + val body = handleRequestError( + "Failed to end coverage recording for test case $uniformPath. Coverage for that test case is most likely lost." + ) { api.testFinished(uniformPath.encodeUrl(), execution) } + + if (!readBodyStringNullSafe(body).isNullOrBlank()) { + throw AgentConfigurationMismatch( + "The agent seems to be configured to return test coverage via" + + " HTTP to the tia-client (agent option `tia-mode=http`) but you did not instruct the" + + " tia-client to handle this. Please either reconfigure the agent or call" + + " #endTestAndRetrieveCoverage() instead of this method and handle the returned coverage." + + " As it is currently configured, the agent will not store or process the recorded coverage" + + " in any way other than sending it to the tia-client via HTTP so it is lost permanently." + ) + } + } + + @Throws(AgentHttpRequestFailedException::class) + private fun readBodyStringNullSafe(body: ResponseBody?): String? { + if (body == null) { + return null + } + + try { + return body.string() + } catch (e: IOException) { + throw AgentHttpRequestFailedException("Unable to read agent HTTP response body string", e) + } + } + + /** + * Signals to the agent that the test runner has finished executing this test and the result of the test run. It + * will also parse the testwise coverage data returned by the agent for this test and return it so it can be + * manually processed by you. The agent will not store or otherwise process this coverage, so be sure to do so + * yourself. + * + * + * This method assumes that the agent is configured to return each test's coverage data via HTTP. It will receive + * and parse the data. If the agent is configured differently, this method will throw a terminal + * [RuntimeException]. In this case, you must reconfigure the agent with the `tia-mode=http` option enabled. + * + * @throws AgentHttpRequestFailedException if communicating with the agent fails or in case of internal errors. This + * method already retries the request once, so this is likely a terminal + * failure. The recorded coverage is likely lost. The caller should log this + * problem appropriately. + */ + @Throws(AgentHttpRequestFailedException::class) + fun endTestAndRetrieveCoverage( + result: TestResultWithMessage + ): TestInfo { + // the agent already records test duration, so we can simply provide a dummy value here + val execution = TestExecution( + uniformPath, 0L, result.result, result.message + ) + val body = handleRequestError( + "Failed to end coverage recording for test case $uniformPath. Coverage for that test case is most likely lost." + ) { api.testFinished(uniformPath.encodeUrl(), execution) } + + val json = readBodyStringNullSafe(body) + if (json.isNullOrBlank()) { + throw AgentConfigurationMismatch( + "You asked the tia-client to retrieve this test's coverage via HTTP" + + " but the agent is not configured for this. Please reconfigure the agent to use `tia-mode=http`." + ) + } + + try { + return deserialize(json, TestInfo::class.java) + } catch (e: IOException) { + throw AgentHttpRequestFailedException( + "Unable to parse the JSON returned by the agent. Maybe you have" + + " a version mismatch between the tia-client and the agent?. Json returned by the agent: `" + json + + "`", e + ) + } + } +} diff --git a/tia-client/src/main/kotlin/com/teamscale/tia/client/TestRun.kt b/tia-client/src/main/kotlin/com/teamscale/tia/client/TestRun.kt new file mode 100644 index 000000000..53c8a1532 --- /dev/null +++ b/tia-client/src/main/kotlin/com/teamscale/tia/client/TestRun.kt @@ -0,0 +1,62 @@ +package com.teamscale.tia.client + +import com.teamscale.report.testwise.model.ETestExecutionResult +import com.teamscale.tia.client.AgentCommunicationUtils.handleRequestError +import com.teamscale.tia.client.UrlUtils.encodeUrl + +/** + * Use this class to report test start and end events and upload testwise coverage to Teamscale. + * + * + * After having run all tests, call [.endTestRun] to create a testwise coverage report and upload it to + * Teamscale. This requires that you configured the agent to upload coverage to Teamscale + * (`tia-mode=teamscale-upload`). + */ +open class TestRun internal constructor(private val api: ITestwiseCoverageAgentApi) { + /** + * Represents the result of running a single test. + */ + class TestResultWithMessage( + /** Whether the test succeeded or failed. */ + val result: ETestExecutionResult, + /** An optional message, e.g. a stack trace in case of test failures. */ + val message: String? + ) + + /** + * Informs the testwise coverage agent that a new test is about to start. + * + * @throws AgentHttpRequestFailedException if communicating with the agent fails or in case of internal errors. In + * this case, the agent probably doesn't know that this test case was + * started, so its coverage is lost. This method already retries the request + * once, so this is likely a terminal failure. The caller should log this + * problem appropriately. Coverage for subsequent test cases could, however, + * potentially still be recorded. Thus, the caller should continue with test + * execution and continue informing the coverage agent about further test + * start and end events. + */ + @Throws(AgentHttpRequestFailedException::class) + fun startTest(uniformPath: String): RunningTest { + handleRequestError( + "Failed to start coverage recording for test case $uniformPath" + ) { api.testStarted(uniformPath.encodeUrl()) } + return RunningTest(uniformPath, api) + } + + /** + * Informs the testwise coverage agent that the caller has finished running all tests and should upload coverage to + * Teamscale. Only call this if you configured the agent to upload coverage to Teamscale + * (`tia-mode=teamscale-upload`). Otherwise, this method will throw an exception. + * + * @throws AgentHttpRequestFailedException if communicating with the agent fails or in case of internal errors. This + * method already retries the request once, so this is likely a terminal + * failure. The recorded coverage is likely lost. The caller should log this + * problem appropriately. + */ + @Throws(AgentHttpRequestFailedException::class) + fun endTestRun(partial: Boolean) { + handleRequestError( + "Failed to create a coverage report and upload it to Teamscale. The coverage is most likely lost" + ) { api.testRunFinished(partial) } + } +} diff --git a/tia-client/src/main/kotlin/com/teamscale/tia/client/TestRunWithClusteredSuggestions.kt b/tia-client/src/main/kotlin/com/teamscale/tia/client/TestRunWithClusteredSuggestions.kt new file mode 100644 index 000000000..c8ca10ea8 --- /dev/null +++ b/tia-client/src/main/kotlin/com/teamscale/tia/client/TestRunWithClusteredSuggestions.kt @@ -0,0 +1,20 @@ +package com.teamscale.tia.client + +import com.teamscale.client.PrioritizableTestCluster + +/** + * Represents a run of prioritized and selected test clusters as reported by the TIA. Use this class to report test + * start and end events and upload testwise coverage to Teamscale. + * + * + * The caller of this class should retrieve the test clusters to execute from [.getPrioritizedClusters], run + * them (in the given order if possible) and report test start and end events via [.startTest] )}. + * + * + * After having run all tests, call [.endTestRun] to create a testwise coverage report and upload it to + * Teamscale. + */ +class TestRunWithClusteredSuggestions internal constructor( + api: ITestwiseCoverageAgentApi, + @JvmField val prioritizedClusters: List? +) : TestRun(api) diff --git a/tia-client/src/main/kotlin/com/teamscale/tia/client/TestRunWithFlatSuggestions.kt b/tia-client/src/main/kotlin/com/teamscale/tia/client/TestRunWithFlatSuggestions.kt new file mode 100644 index 000000000..3f9a5f0bc --- /dev/null +++ b/tia-client/src/main/kotlin/com/teamscale/tia/client/TestRunWithFlatSuggestions.kt @@ -0,0 +1,20 @@ +package com.teamscale.tia.client + +import com.teamscale.client.PrioritizableTest + +/** + * Represents a run of a flat list of prioritized and selected tests as reported by the TIA. Use this class to report + * test start and end events and upload testwise coverage to Teamscale. + * + * + * The caller of this class should retrieve the tests to execute from [prioritizedTests], run them (in the + * given order if possible) and report test start and end events via [startTest])}. + * + * + * After having run all tests, call [endTestRun] to create a testwise coverage report and upload it to + * Teamscale. + */ +class TestRunWithFlatSuggestions internal constructor( + api: ITestwiseCoverageAgentApi, + private val prioritizedTests: List +) : TestRun(api) diff --git a/tia-client/src/main/kotlin/com/teamscale/tia/client/TiaAgent.kt b/tia-client/src/main/kotlin/com/teamscale/tia/client/TiaAgent.kt new file mode 100644 index 000000000..f9c0d2cff --- /dev/null +++ b/tia-client/src/main/kotlin/com/teamscale/tia/client/TiaAgent.kt @@ -0,0 +1,142 @@ +package com.teamscale.tia.client + +import com.teamscale.client.ClusteredTestDetails +import com.teamscale.tia.client.AgentCommunicationUtils.handleRequestError +import okhttp3.HttpUrl +import java.time.Instant + +/** + * Communicates with one Teamscale JaCoCo agent in testwise coverage mode to facilitate the Test Impact analysis. + * + * + * Use this class to retrieve impacted tests from Teamscale, start a [TestRun] based on these selected and + * prioritized tests and upload test-wise coverage after having executed the tests. + * + * + * The caller of this class is responsible for actually executing the tests. + * + *

    Available Tests

    + * This API allows you to pass a list of available tests to start a test run. This list is used for multiple purposes: + * + * + * * To determine if any tests changed, were added or removed: Teamscale will not suggest deleted tests and + * will always suggest changed or added tests. + * * To cluster tests: The list of available tests includes information about test clusters, which are logical + * groups of tests. Teamscale will only prioritize tests against each other if they are in the same cluster. + * + */ +class TiaAgent(private val includeNonImpactedTests: Boolean, url: HttpUrl) { + private val api = ITestwiseCoverageAgentApi.createService(url) + + /** + * Starts a test run but does not ask Teamscale to prioritize and select any test cases. Use this when you only want + * to record test-wise coverage and don't care about TIA's test selection and prioritization. + */ + fun startTestRunWithoutTestSelection() = TestRun(api) + + /** + * Runs the TIA to determine which of the given available tests should be run and in which order. This method + * considers all changes since the last time that test-wise coverage was uploaded. In most situations this is the + * optimal behaviour. + * + * @param availableTests A list of all available tests. This is used to determine which tests need to be run, e.g. + * because they are completely new or changed since the last run. If you provide an empty + * list, no tests will be selected. The clustering information in this list is used to + * construct the test clusters in the returned [TestRunWithClusteredSuggestions]. + * @throws AgentHttpRequestFailedException e.g. if the agent or Teamscale is not reachable or an internal error + * occurs. This method already retries the request once, so this is likely a + * terminal failure. You should simply fall back to running all tests in + * this case and not communicate further with the agent. You should visibly + * report this problem so it can be fixed. + */ + @Throws(AgentHttpRequestFailedException::class) + fun startTestRun( + availableTests: List + ) = startTestRun(availableTests, null, null) + + /** + * Runs the TIA to determine which of the given available tests should be run and in which order. This method + * considers all changes since the last time that test-wise coverage was uploaded. In most situations this is the + * optimal behaviour. + * + * + * Using this method, Teamscale will perform the selection and prioritization based on the tests it currently knows + * about. In this case, it will not automatically include changed or new tests in the selection (since it doesn't + * know about these changes) and it may return deleted tests (since it doesn't know about the deletions). It will + * also not cluster the tests as [.startTestRun] would. + * + * + * **Thus, we recommend that, if possible, you use [.startTestRun] instead.** + * + * @throws AgentHttpRequestFailedException e.g. if the agent or Teamscale is not reachable or an internal error + * occurs. This method already retries the request once, so this is likely a + * terminal failure. You should simply fall back to running all tests in + * this case and not communicate further with the agent. You should visibly + * report this problem so it can be fixed. + */ + @Throws(AgentHttpRequestFailedException::class) + fun startTestRunAssumingUnchangedTests() = + startTestRunAssumingUnchangedTests(null, null) + + /** + * Runs the TIA to determine which of the given available tests should be run and in which order. This method + * considers all changes since the given baseline timestamp. + * + * @param availableTests A list of all available tests. This is used to determine which tests need to be run, e.g. + * because they are completely new or changed since the last run. If you provide an empty + * list, no tests will be selected.The clustering information in this list is used to + * construct the test clusters in the returned [TestRunWithClusteredSuggestions]. + * @param baseline Consider all code changes since this date when calculating the impacted tests. + * @param baselineRevision Same as baseline but accepts a revision (e.g. git SHA1) instead of a branch and timestamp + * @throws AgentHttpRequestFailedException e.g. if the agent or Teamscale is not reachable or an internal error + * occurs. You should simply fall back to running all tests in this case. + */ + @Throws(AgentHttpRequestFailedException::class) + fun startTestRun( + availableTests: List, + baseline: Instant?, + baselineRevision: String? + ): TestRunWithClusteredSuggestions { + val baselineTimestamp = baseline?.toEpochMilli() + val clusters = handleRequestError("Failed to start the test run") { + api.testRunStarted( + includeNonImpactedTests, + baselineTimestamp, + baselineRevision, + availableTests + ) + } + return TestRunWithClusteredSuggestions(api, clusters) + } + + /** + * Runs the TIA to determine which of the given available tests should be run and in which order. This method + * has considered all changes since the given baseline timestamp. + * + * + * Using this method, Teamscale will perform the selection and prioritization based on the tests it currently knows + * about. In this case, it will not automatically include changed or new tests in the selection (since it doesn't + * know about these changes) and it may return deleted tests (since it doesn't know about the deletions). It will * + * also not cluster the tests as [startTestRun] would. + * + * + * **Thus, we recommend that, if possible, you use [startTestRun] + * instead.** + * + * @throws AgentHttpRequestFailedException e.g., if the agent or Teamscale is not reachable or an internal error + * occurs. This method already retries the request once, so this is likely a + * terminal failure. You should fall back to running all tests in + * this case and not communicate further with the agent. You should visibly + * report this problem so it can be fixed. + */ + @Throws(AgentHttpRequestFailedException::class) + fun startTestRunAssumingUnchangedTests( + baseline: Instant?, baselineRevision: String? + ): TestRunWithFlatSuggestions { + val baselineTimestamp = baseline?.toEpochMilli() + val clusters = handleRequestError("Failed to start the test run") { + api.testRunStarted(includeNonImpactedTests, baselineTimestamp, baselineRevision) + } + return TestRunWithFlatSuggestions(api, clusters?.firstOrNull()?.tests ?: emptyList()) + } +} diff --git a/tia-client/src/main/kotlin/com/teamscale/tia/client/UrlUtils.kt b/tia-client/src/main/kotlin/com/teamscale/tia/client/UrlUtils.kt new file mode 100644 index 000000000..7f207babe --- /dev/null +++ b/tia-client/src/main/kotlin/com/teamscale/tia/client/UrlUtils.kt @@ -0,0 +1,32 @@ +package com.teamscale.tia.client + +/** Utils for URL encoding as the Java internal URLEncoder does not handle all escape-worthy symbols for path segments. */ +object UrlUtils { + /** + * Percent-decodes a string, such as used in a URL Path (not a query string / form encode, which uses + for spaces, + * etc). + * Source: https://stackoverflow.com/a/44076794 + */ + @JvmStatic + fun String.encodeUrl() = + replace("%", "%25") + .replace(" ", "%20") + .replace("!", "%21") + .replace("#", "%23") + .replace("$", "%24") + .replace("&", "%26") + .replace("'", "%27") + .replace("(", "%28") + .replace(")", "%29") + .replace("*", "%2A") + .replace("+", "%2B") + .replace(",", "%2C") + .replace("/", "%2F") + .replace(":", "%3A") + .replace(";", "%3B") + .replace("=", "%3D") + .replace("?", "%3F") + .replace("@", "%40") + .replace("[", "%5B") + .replace("]", "%5D") +} diff --git a/tia-client/src/test/java/com/teamscale/tia/client/AgentCommunicationUtilsTest.java b/tia-client/src/test/java/com/teamscale/tia/client/AgentCommunicationUtilsTest.java deleted file mode 100644 index a1e6312f5..000000000 --- a/tia-client/src/test/java/com/teamscale/tia/client/AgentCommunicationUtilsTest.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.teamscale.tia.client; - -import okhttp3.ResponseBody; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import okhttp3.mockwebserver.SocketPolicy; -import org.junit.jupiter.api.Test; -import retrofit2.Call; -import retrofit2.Retrofit; -import retrofit2.http.GET; - -import java.io.IOException; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class AgentCommunicationUtilsTest { - - private interface ITestService { - - /** Test request. */ - @GET("request") - Call testRequest(); - } - - @Test - public void shouldRetryRequestsOnce() throws Exception { - MockWebServer server = new MockWebServer(); - server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AFTER_REQUEST)); - server.enqueue(new MockResponse().setResponseCode(200).setBody("result")); - - ITestService service = new Retrofit.Builder().baseUrl("http://localhost:" + server.getPort()).build() - .create(ITestService.class); - - // should not throw since the second call works - AgentCommunicationUtils.handleRequestError(service::testRequest, "test"); - } - - @Test - public void shouldNotRetryIfFirstRequestSucceeds() throws Exception { - MockWebServer server = new MockWebServer(); - server.enqueue(new MockResponse().setResponseCode(200).setBody("result")); - server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AFTER_REQUEST)); - - ITestService service = new Retrofit.Builder().baseUrl("http://localhost:" + server.getPort()).build() - .create(ITestService.class); - - // should not throw since the first call works - AgentCommunicationUtils.handleRequestError(service::testRequest, "test"); - } - - @Test - public void shouldNotRetryMoreThanOnce() { - MockWebServer server = new MockWebServer(); - server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AFTER_REQUEST)); - server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AFTER_REQUEST)); - server.enqueue(new MockResponse().setResponseCode(200).setBody("result")); - - ITestService service = new Retrofit.Builder().baseUrl("http://localhost:" + server.getPort()).build() - .create(ITestService.class); - - AgentHttpRequestFailedException exception = assertThrows(AgentHttpRequestFailedException.class, - () -> AgentCommunicationUtils.handleRequestError(service::testRequest, "test")); - assertThat(exception.getCause()).isInstanceOf(IOException.class); - } - -} \ No newline at end of file diff --git a/tia-client/src/test/kotlin/com/teamscale/tia/client/AgentCommunicationUtilsTest.kt b/tia-client/src/test/kotlin/com/teamscale/tia/client/AgentCommunicationUtilsTest.kt new file mode 100644 index 000000000..445b7f9fd --- /dev/null +++ b/tia-client/src/test/kotlin/com/teamscale/tia/client/AgentCommunicationUtilsTest.kt @@ -0,0 +1,73 @@ +package com.teamscale.tia.client + +import com.teamscale.tia.client.AgentCommunicationUtils.handleRequestError +import okhttp3.ResponseBody +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.SocketPolicy +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import retrofit2.Call +import retrofit2.Retrofit +import retrofit2.http.GET +import java.io.IOException +import java.util.function.Supplier + +class AgentCommunicationUtilsTest { + private interface ITestService { + /** Test request. */ + @GET("request") + fun testRequest(): Call + } + + @Test + @Throws(Exception::class) + fun shouldRetryRequestsOnce() { + val server = MockWebServer().apply { + enqueue(MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AFTER_REQUEST)) + enqueue(MockResponse().setResponseCode(200).setBody("result")) + } + + val service = Retrofit.Builder().baseUrl("http://localhost:" + server.port) + .build() + .create(ITestService::class.java) + + // should not throw since the second call works + handleRequestError("test") { service.testRequest() } + } + + @Test + @Throws(Exception::class) + fun shouldNotRetryIfFirstRequestSucceeds() { + val server = MockWebServer().apply { + enqueue(MockResponse().setResponseCode(200).setBody("result")) + enqueue(MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AFTER_REQUEST)) + } + + val service = Retrofit.Builder().baseUrl("http://localhost:" + server.port) + .build() + .create(ITestService::class.java) + + // should not throw since the first call works + handleRequestError("test") { service.testRequest() } + } + + @Test + fun shouldNotRetryMoreThanOnce() { + val server = MockWebServer().apply { + enqueue(MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AFTER_REQUEST)) + enqueue(MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AFTER_REQUEST)) + enqueue(MockResponse().setResponseCode(200).setBody("result")) + } + + val exception = Assertions.assertThrows( + AgentHttpRequestFailedException::class.java + ) { + val service = Retrofit.Builder().baseUrl("http://localhost:" + server.port) + .build() + .create(ITestService::class.java) + handleRequestError("test") { service.testRequest() } + } + org.assertj.core.api.Assertions.assertThat(exception.cause).isInstanceOf(IOException::class.java) + } +} \ No newline at end of file From 16e31d228a761c2858aede860ce1d3dee9cb663d Mon Sep 17 00:00:00 2001 From: Constructor Date: Wed, 27 Nov 2024 19:46:38 +0100 Subject: [PATCH 181/186] TS-38628 Tia Client doc cleanup --- .../client/AgentHttpRequestFailedException.kt | 4 ++-- .../tia/client/CommandLineInterface.kt | 21 +++++++--------- .../tia/client/ITestwiseCoverageAgentApi.kt | 2 +- .../com/teamscale/tia/client/TiaAgent.kt | 24 +++++++++---------- 4 files changed, 24 insertions(+), 27 deletions(-) diff --git a/tia-client/src/main/kotlin/com/teamscale/tia/client/AgentHttpRequestFailedException.kt b/tia-client/src/main/kotlin/com/teamscale/tia/client/AgentHttpRequestFailedException.kt index 40fabbf79..ac4590cad 100644 --- a/tia-client/src/main/kotlin/com/teamscale/tia/client/AgentHttpRequestFailedException.kt +++ b/tia-client/src/main/kotlin/com/teamscale/tia/client/AgentHttpRequestFailedException.kt @@ -6,7 +6,7 @@ package com.teamscale.tia.client * problems can be addressed. */ class AgentHttpRequestFailedException : Exception { - constructor(message: String?) : super(message) + constructor(message: String) : super(message) - constructor(message: String?, cause: Throwable?) : super(message, cause) + constructor(message: String, cause: Throwable?) : super(message, cause) } diff --git a/tia-client/src/main/kotlin/com/teamscale/tia/client/CommandLineInterface.kt b/tia-client/src/main/kotlin/com/teamscale/tia/client/CommandLineInterface.kt index 5648736a5..c0cff8ebc 100644 --- a/tia-client/src/main/kotlin/com/teamscale/tia/client/CommandLineInterface.kt +++ b/tia-client/src/main/kotlin/com/teamscale/tia/client/CommandLineInterface.kt @@ -3,7 +3,6 @@ package com.teamscale.tia.client import com.teamscale.client.ClusteredTestDetails import com.teamscale.client.JsonUtils.deserializeList import com.teamscale.client.JsonUtils.serialize -import com.teamscale.client.PrioritizableTestCluster import com.teamscale.client.StringUtils.isEmpty import com.teamscale.report.testwise.model.ETestExecutionResult import com.teamscale.report.testwise.model.TestExecution @@ -15,7 +14,6 @@ import java.io.IOException import java.io.InputStreamReader import java.nio.charset.StandardCharsets import java.util.* -import java.util.function.Supplier import java.util.stream.Collectors /** @@ -24,8 +22,7 @@ import java.util.stream.Collectors class CommandLineInterface(arguments: Array) { private class InvalidCommandLineException(message: String?) : RuntimeException(message) - private val arguments: MutableList = - ArrayList(listOf(*arguments)) + private val arguments = listOf(*arguments).toMutableList() private val command: String private val api: ITestwiseCoverageAgentApi @@ -50,8 +47,7 @@ class CommandLineInterface(arguments: Array) { "endTest" -> endTest() "endTestRun" -> endTestRun() else -> throw InvalidCommandLineException( - "Unknown command '" + command + "'. Should be one of startTestRun, startTest, endTest," + - " endTestRun" + "Unknown command '$command'. Should be one of startTestRun, startTest, endTest, endTestRun" ) } } @@ -72,13 +68,11 @@ class CommandLineInterface(arguments: Array) { private fun endTest() { if (arguments.size < 2) { throw InvalidCommandLineException( - "You must provide the uniform path of the test that is about to be started" + - " as the first argument of the endTest command and the test result as the second." + "You must provide the uniform path of the test that is about to be started as the first argument of the endTest command and the test result as the second." ) } val uniformPath = arguments.removeAt(0) val result = ETestExecutionResult.valueOf(arguments.removeAt(0).uppercase(Locale.getDefault())) - val message = readStdin() // the agent already records test duration, so we can simply provide a dummy value here @@ -109,10 +103,13 @@ class CommandLineInterface(arguments: Array) { val baselineRevision = parseAndRemoveStringParameter("baseline-revision") val availableTests = parseAvailableTestsFromStdin() - val clusters = handleRequestError( + handleRequestError( "Failed to start the test run" - ) { api.testRunStarted(includeNonImpacted, baseline, baselineRevision, availableTests) } - println(serialize(clusters!!)) + ) { + api.testRunStarted(includeNonImpacted, baseline, baselineRevision, availableTests) + }?.let { + println(it.serialize()) + } } @Throws(IOException::class) diff --git a/tia-client/src/main/kotlin/com/teamscale/tia/client/ITestwiseCoverageAgentApi.kt b/tia-client/src/main/kotlin/com/teamscale/tia/client/ITestwiseCoverageAgentApi.kt index b0d67e109..8c5cfc76e 100644 --- a/tia-client/src/main/kotlin/com/teamscale/tia/client/ITestwiseCoverageAgentApi.kt +++ b/tia-client/src/main/kotlin/com/teamscale/tia/client/ITestwiseCoverageAgentApi.kt @@ -70,7 +70,7 @@ interface ITestwiseCoverageAgentApi { ): Call> /** - * Test run finished. Generate test-wise coverage report and upload to Teamscale. + * Test run finished. Generate a test-wise coverage report and upload to Teamscale. * * @param partial Whether the test recording only contains a subset of the available tests. */ diff --git a/tia-client/src/main/kotlin/com/teamscale/tia/client/TiaAgent.kt b/tia-client/src/main/kotlin/com/teamscale/tia/client/TiaAgent.kt index f9c0d2cff..42b765a58 100644 --- a/tia-client/src/main/kotlin/com/teamscale/tia/client/TiaAgent.kt +++ b/tia-client/src/main/kotlin/com/teamscale/tia/client/TiaAgent.kt @@ -56,8 +56,8 @@ class TiaAgent(private val includeNonImpactedTests: Boolean, url: HttpUrl) { /** * Runs the TIA to determine which of the given available tests should be run and in which order. This method - * considers all changes since the last time that test-wise coverage was uploaded. In most situations this is the - * optimal behaviour. + * has considered all changes since the last time that test-wise coverage was uploaded. + * In most situations, this is the optimal behavior. * * * Using this method, Teamscale will perform the selection and prioritization based on the tests it currently knows @@ -68,9 +68,9 @@ class TiaAgent(private val includeNonImpactedTests: Boolean, url: HttpUrl) { * * **Thus, we recommend that, if possible, you use [.startTestRun] instead.** * - * @throws AgentHttpRequestFailedException e.g. if the agent or Teamscale is not reachable or an internal error + * @throws AgentHttpRequestFailedException e.g., if the agent or Teamscale is not reachable or an internal error * occurs. This method already retries the request once, so this is likely a - * terminal failure. You should simply fall back to running all tests in + * terminal failure. You should fall back to running all tests in * this case and not communicate further with the agent. You should visibly * report this problem so it can be fixed. */ @@ -80,16 +80,17 @@ class TiaAgent(private val includeNonImpactedTests: Boolean, url: HttpUrl) { /** * Runs the TIA to determine which of the given available tests should be run and in which order. This method - * considers all changes since the given baseline timestamp. + * has considered all changes since the given baseline timestamp. * - * @param availableTests A list of all available tests. This is used to determine which tests need to be run, e.g. + * @param availableTests A list of all available tests. This is used to determine which tests need to be run, e.g., * because they are completely new or changed since the last run. If you provide an empty - * list, no tests will be selected.The clustering information in this list is used to + * list, no tests will be selected. The clustering information in this list is used to * construct the test clusters in the returned [TestRunWithClusteredSuggestions]. * @param baseline Consider all code changes since this date when calculating the impacted tests. - * @param baselineRevision Same as baseline but accepts a revision (e.g. git SHA1) instead of a branch and timestamp - * @throws AgentHttpRequestFailedException e.g. if the agent or Teamscale is not reachable or an internal error - * occurs. You should simply fall back to running all tests in this case. + * @param baselineRevision Same as baseline but accepts a revision (e.g., git SHA1) instead of a branch and timestamp + * @throws AgentHttpRequestFailedException e.g., if the agent or Teamscale is not reachable or an internal error + * occurs. + * You should fall back to running all tests in this case. */ @Throws(AgentHttpRequestFailedException::class) fun startTestRun( @@ -133,9 +134,8 @@ class TiaAgent(private val includeNonImpactedTests: Boolean, url: HttpUrl) { fun startTestRunAssumingUnchangedTests( baseline: Instant?, baselineRevision: String? ): TestRunWithFlatSuggestions { - val baselineTimestamp = baseline?.toEpochMilli() val clusters = handleRequestError("Failed to start the test run") { - api.testRunStarted(includeNonImpactedTests, baselineTimestamp, baselineRevision) + api.testRunStarted(includeNonImpactedTests, baseline?.toEpochMilli(), baselineRevision) } return TestRunWithFlatSuggestions(api, clusters?.firstOrNull()?.tests ?: emptyList()) } From a770a9bc44dcfee060654487327ae4d65e24d2e9 Mon Sep 17 00:00:00 2001 From: Constructor Date: Thu, 28 Nov 2024 18:45:32 +0100 Subject: [PATCH 182/186] TS-38628 Revert serializer API --- .../kotlin/com/teamscale/tia/client/CommandLineInterface.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tia-client/src/main/kotlin/com/teamscale/tia/client/CommandLineInterface.kt b/tia-client/src/main/kotlin/com/teamscale/tia/client/CommandLineInterface.kt index c0cff8ebc..010964325 100644 --- a/tia-client/src/main/kotlin/com/teamscale/tia/client/CommandLineInterface.kt +++ b/tia-client/src/main/kotlin/com/teamscale/tia/client/CommandLineInterface.kt @@ -1,6 +1,7 @@ package com.teamscale.tia.client import com.teamscale.client.ClusteredTestDetails +import com.teamscale.client.JsonUtils import com.teamscale.client.JsonUtils.deserializeList import com.teamscale.client.JsonUtils.serialize import com.teamscale.client.StringUtils.isEmpty @@ -108,7 +109,7 @@ class CommandLineInterface(arguments: Array) { ) { api.testRunStarted(includeNonImpacted, baseline, baselineRevision, availableTests) }?.let { - println(it.serialize()) + println(serialize(it)) } } From 34cf4f0266ac7d55356b0b8a4d3d62fc6c8c6b42 Mon Sep 17 00:00:00 2001 From: Florian Dreier Date: Fri, 29 Nov 2024 10:03:31 +0100 Subject: [PATCH 183/186] Updated com.xpdustry.kotlin-shadow-relocator plugin --- buildSrc/build.gradle.kts | 8 +------- .../kotlin/com.teamscale.shadow-convention.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 6c15dca0d..328e18b73 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -4,17 +4,11 @@ plugins { repositories { gradlePluginPortal() - maven("https://maven.xpdustry.com/releases") { - name = "xpdustry-releases" - mavenContent { releasesOnly() } - } } dependencies { implementation("com.gradleup.shadow:shadow-gradle-plugin:8.3.5") - implementation("com.xpdustry.ksr:com.xpdustry.ksr.gradle.plugin:1.0.0") { - exclude(group = "com.github.johnrengelman") - } + implementation("com.xpdustry:kotlin-shadow-relocator:2.0.0") implementation("org.ow2.asm:asm:9.7.1") implementation("org.ow2.asm:asm-commons:9.7.1") diff --git a/buildSrc/src/main/kotlin/com.teamscale.shadow-convention.gradle.kts b/buildSrc/src/main/kotlin/com.teamscale.shadow-convention.gradle.kts index 3ebff6747..b0a7f27b0 100644 --- a/buildSrc/src/main/kotlin/com.teamscale.shadow-convention.gradle.kts +++ b/buildSrc/src/main/kotlin/com.teamscale.shadow-convention.gradle.kts @@ -6,7 +6,7 @@ plugins { // https://github.com/GradleUp/shadow id("com.gradleup.shadow") // https://github.com/xpdustry/kotlin-shadow-relocator - id("com.xpdustry.ksr") + id("com.xpdustry.kotlin-shadow-relocator") } tasks.named("shadowJar") { From dac4f216cdcbc826e0a5a82c7157b878d1c4f8d7 Mon Sep 17 00:00:00 2001 From: Andreas Stahlbauer Date: Mon, 2 Dec 2024 10:08:35 +0100 Subject: [PATCH 184/186] TS-31571 Kotlin merge conflicts solved --- .../main/kotlin/com/teamscale/client/ITeamscaleService.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 445c7984d..a383026ab 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/ITeamscaleService.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/ITeamscaleService.kt @@ -111,15 +111,15 @@ interface ITeamscaleService { ): Call> /** Registers a profiler to Teamscale and returns the profiler configuration it should be started with. */ - @Deprecated("") + @Deprecated("This is here for compatibility with older Teamscale version; remove after these are no longer supported.") @POST("api/v9.4.0/running-profilers") fun registerProfilerLegacy( @Query("configuration-id") configurationId: String?, @Body processInformation: ProcessInformation? - ): Call? + ): Call /** Updates the profiler infos and sets the profiler to still alive. */ - @Deprecated("") + @Deprecated("This is here for compatibility with older Teamscale version; remove after these are no longer supported.") @PUT("api/v9.4.0/running-profilers/{profilerId}") fun sendHeartbeatLegacy( @Path("profilerId") profilerId: String?, @@ -127,7 +127,7 @@ interface ITeamscaleService { ): Call /** Removes the profiler identified by given ID. */ - @Deprecated("") + @Deprecated("This is here for compatibility with older Teamscale version; remove after these are no longer supported.") @DELETE("api/v9.4.0/running-profilers/{profilerId}") fun unregisterProfilerLegacy(@Path("profilerId") profilerId: String?): Call From f8759adb178807e02167b36930468f2242225325 Mon Sep 17 00:00:00 2001 From: Andreas Stahlbauer Date: Mon, 2 Dec 2024 10:10:22 +0100 Subject: [PATCH 185/186] TS-31571 Kotlin merge conflicts solved --- .../src/main/java/com/teamscale/client/ITeamscaleService.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 teamscale-client/src/main/java/com/teamscale/client/ITeamscaleService.java 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 e69de29bb..000000000 From a88d5d5234e729ba5179f52f5683fce1d787ffa8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 09:31:04 +0000 Subject: [PATCH 186/186] Update plugin org.gradle.toolchains.foojay-resolver-convention to v0.9.0 --- settings.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index c69ecb086..dfdf27495 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,7 +5,7 @@ pluginManagement { } plugins { - id("org.gradle.toolchains.foojay-resolver-convention") version("0.8.0") + id("org.gradle.toolchains.foojay-resolver-convention") version("0.9.0") } include(":agent")