diff --git a/java-reporter-core/pom.xml b/java-reporter-core/pom.xml index 45f63d28..1d3e6516 100644 --- a/java-reporter-core/pom.xml +++ b/java-reporter-core/pom.xml @@ -7,7 +7,7 @@ io.testomat java-reporter-core - 0.8.13 + 0.9.0 jar Testomat.io Reporter Core diff --git a/java-reporter-core/src/main/java/io/testomat/core/client/request/NativeRequestBodyBuilder.java b/java-reporter-core/src/main/java/io/testomat/core/client/request/NativeRequestBodyBuilder.java index 8e1233b6..f17c07d8 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/client/request/NativeRequestBodyBuilder.java +++ b/java-reporter-core/src/main/java/io/testomat/core/client/request/NativeRequestBodyBuilder.java @@ -25,6 +25,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; /** * Default implementation of {@link RequestBodyBuilder} for Testomat.io API requests. @@ -175,8 +176,13 @@ private Map buildTestResultMap(TestResult result) throws JsonPro addLinks(body, rid); } - body.put("overwrite", "true"); - ReportedTestStorage.store(body); + body.put("overwrite", Optional.ofNullable(result.isOverwrite()).orElse(true)); + + Map storageEntry = new HashMap<>(); + storageEntry.put(ApiRequestFields.TEST_ID, result.getTestId()); + storageEntry.put(ApiRequestFields.RID, result.getRid()); + ReportedTestStorage.store(storageEntry); + return body; } diff --git a/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/ReportedTestStorage.java b/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/ReportedTestStorage.java index e2f7ddff..7d0db84f 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/ReportedTestStorage.java +++ b/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/ReportedTestStorage.java @@ -1,8 +1,12 @@ package io.testomat.core.facade.methods.artifact; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -15,12 +19,37 @@ public class ReportedTestStorage { private static final List> STORAGE = new CopyOnWriteArrayList<>(); /** - * Stores test execution data. + * Stores test execution metadata in the internal storage. + *

+ * The method adds a new entry containing {@code test_id} and {@code rid} + * only if an entry with the same values does not already exist. + * The check and insertion are performed atomically using synchronization + * to prevent duplicates in concurrent environments. * - * @param body test data map containing test results and metadata + * @param body a map containing test execution data; expected to include + * {@code "test_id"} and {@code "rid"} keys */ public static void store(Map body) { - STORAGE.add(body); + + Object testId = body.get("test_id"); + Object rid = body.get("rid"); + + synchronized (STORAGE) { + + boolean exists = STORAGE.stream().anyMatch(m -> + Objects.equals(m.get("test_id"), testId) && + Objects.equals(m.get("rid"), rid) + ); + + if (!exists) { + Map map = new HashMap<>(); + map.put("test_id", testId); + map.put("rid", rid); + + STORAGE.add(map); + } + } + log.debug("Stored body: {}", body); } @@ -39,11 +68,23 @@ public static List> getStorage() { * @param artifactLinkData list of artifact link data to associate with tests */ public static void linkArtifactsToTests(List artifactLinkData) { - for (ArtifactLinkData data : artifactLinkData) { + Map> index = STORAGE.stream() - .filter(body -> data.getRid().equals(body.get("rid"))) - .forEach(body -> body.put("artifacts", data.getLinks())); + .collect(Collectors.toMap( + m -> m.get("rid"), + m -> m + )); + + for (ArtifactLinkData data : artifactLinkData) { + Map body = index.get(data.getRid()); + if (body != null) { + @SuppressWarnings("unchecked") + List artifacts = + (List) body.computeIfAbsent("artifacts", k -> new ArrayList<>()); + artifacts.addAll(data.getLinks()); + } } + for (ArtifactLinkData data : artifactLinkData) { log.debug("Linked: testId - {}, testName - {}, rid - {}, links - {}", data.getTestId(), data.getTestName(), data.getRid(), data.getLinks().get(0)); diff --git a/java-reporter-core/src/main/java/io/testomat/core/model/TestResult.java b/java-reporter-core/src/main/java/io/testomat/core/model/TestResult.java index da9bd813..14cda50c 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/model/TestResult.java +++ b/java-reporter-core/src/main/java/io/testomat/core/model/TestResult.java @@ -48,13 +48,17 @@ public class TestResult { /** Test steps executed during the test */ private List steps; + /** Test result is overwritten when the test is executed multiple times */ + private Boolean overwrite; + public TestResult() { } public TestResult(String title, String testId, String suiteTitle, String file, String status, String message, String stack, - Object example, String rid, List steps) { + Object example, String rid, Boolean overwrite, + List steps) { this.title = title; this.testId = testId; this.suiteTitle = suiteTitle; @@ -64,6 +68,7 @@ public TestResult(String title, String testId, this.stack = stack; this.example = example; this.rid = rid; + this.overwrite = overwrite; this.steps = steps; } @@ -81,6 +86,7 @@ public static class Builder { private String stack; private Object example; private String rid; + private Boolean overwrite; private List steps; public Builder withTitle(String title) { @@ -128,13 +134,18 @@ public Builder withRid(String rid) { return this; } + public Builder withOverwrite(Boolean overwrite) { + this.overwrite = overwrite; + return this; + } + public Builder withSteps(List steps) { this.steps = steps; return this; } public TestResult build() { - return new TestResult(title, testId, suiteTitle, file, status, message, stack, example, rid, steps); + return new TestResult(title, testId, suiteTitle, file, status, message, stack, example, rid, overwrite, steps); } } @@ -223,6 +234,14 @@ public List getSteps() { return steps; } + public void setOverwrite(Boolean overwrite) { + this.overwrite = overwrite; + } + + public Boolean isOverwrite() { + return overwrite; + } + public void setSteps(List steps) { this.steps = steps; } diff --git a/java-reporter-core/src/test/java/io/testomat/core/artifact/ReportedTestStorageTest.java b/java-reporter-core/src/test/java/io/testomat/core/artifact/ReportedTestStorageTest.java index fbd4769b..cc66b824 100644 --- a/java-reporter-core/src/test/java/io/testomat/core/artifact/ReportedTestStorageTest.java +++ b/java-reporter-core/src/test/java/io/testomat/core/artifact/ReportedTestStorageTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Arrays; @@ -25,21 +26,6 @@ void cleanup() { ReportedTestStorage.getStorage().clear(); } - @Test - @DisplayName("Should store test data") - void testStoreTestData() { - Map body = new HashMap<>(); - body.put("title", "Test 1"); - body.put("status", "passed"); - - ReportedTestStorage.store(body); - - List> storage = ReportedTestStorage.getStorage(); - assertEquals(1, storage.size()); - assertEquals("Test 1", storage.get(0).get("title")); - assertEquals("passed", storage.get(0).get("status")); - } - @Test @DisplayName("Should store multiple test data entries") void testStoreMultipleEntries() { @@ -190,69 +176,21 @@ void testEmptyArtifactLinkList() { assertEquals(1, storage.size()); } - @Test - @DisplayName("Should store test data with various field types") - void testStoreWithVariousFieldTypes() { - Map body = new HashMap<>(); - body.put("title", "Complex Test"); - body.put("status", "passed"); - body.put("duration", 150.5); - body.put("steps", Arrays.asList("step1", "step2")); - body.put("metadata", Map.of("key1", "value1", "key2", "value2")); - - ReportedTestStorage.store(body); - - List> storage = ReportedTestStorage.getStorage(); - Map stored = storage.get(storage.size() - 1); - - assertEquals("Complex Test", stored.get("title")); - assertEquals("passed", stored.get("status")); - assertEquals(150.5, stored.get("duration")); - assertNotNull(stored.get("steps")); - assertNotNull(stored.get("metadata")); - } - - @Test - @DisplayName("Should maintain insertion order") - void testInsertionOrder() { - for (int i = 0; i < 5; i++) { - Map body = new HashMap<>(); - body.put("title", "Test " + i); - body.put("order", i); - ReportedTestStorage.store(body); - } - - List> storage = ReportedTestStorage.getStorage(); - - // Check that we have at least the 5 tests we just added - assertTrue(storage.size() >= 5); - - // Find our tests and verify order - int lastIndex = storage.size(); - for (int i = 0; i < 5; i++) { - Map test = storage.get(lastIndex - 5 + i); - assertEquals("Test " + i, test.get("title")); - assertEquals(i, test.get("order")); - } - } - @Test @DisplayName("Should handle null values in test data") void testNullValuesInTestData() { Map body = new HashMap<>(); - body.put("title", "Test with nulls"); - body.put("message", null); - body.put("stack", null); + body.put("test_id", null); + body.put("rid", null); ReportedTestStorage.store(body); List> storage = ReportedTestStorage.getStorage(); Map stored = storage.get(storage.size() - 1); - assertEquals("Test with nulls", stored.get("title")); - assertTrue(stored.containsKey("message")); - assertTrue(stored.containsKey("stack")); - assertEquals(null, stored.get("message")); - assertEquals(null, stored.get("stack")); + assertTrue(stored.containsKey("test_id")); + assertTrue(stored.containsKey("rid")); + assertNull(stored.get("test_id")); + assertNull(stored.get("rid")); } } diff --git a/java-reporter-core/src/test/java/io/testomat/core/model/TestResultTest.java b/java-reporter-core/src/test/java/io/testomat/core/model/TestResultTest.java index 5177b9f7..ad8a98ae 100644 --- a/java-reporter-core/src/test/java/io/testomat/core/model/TestResultTest.java +++ b/java-reporter-core/src/test/java/io/testomat/core/model/TestResultTest.java @@ -30,6 +30,7 @@ void testConstructor() { "stack trace", "example data", "rid-456", + true, steps ); diff --git a/java-reporter-cucumber/pom.xml b/java-reporter-cucumber/pom.xml index 27e525b8..f0f68599 100644 --- a/java-reporter-cucumber/pom.xml +++ b/java-reporter-cucumber/pom.xml @@ -6,7 +6,7 @@ io.testomat java-reporter-cucumber - 0.7.9 + 0.7.10 jar Testomat.io Java Reporter Cucumber @@ -51,7 +51,7 @@ io.testomat java-reporter-core - 0.8.13 + 0.9.0 org.slf4j diff --git a/java-reporter-junit/pom.xml b/java-reporter-junit/pom.xml index bc8545ea..89711168 100644 --- a/java-reporter-junit/pom.xml +++ b/java-reporter-junit/pom.xml @@ -6,7 +6,7 @@ io.testomat java-reporter-junit - 0.8.0 + 0.8.1 jar Testomat.io Java Reporter JUnit @@ -51,7 +51,7 @@ io.testomat java-reporter-core - 0.8.13 + 0.9.0 org.slf4j diff --git a/java-reporter-junit/src/test/java/io/testomat/junit/methodexporter/FileParserTest.java b/java-reporter-junit/src/test/java/io/testomat/junit/methodexporter/FileParserTest.java index d94a638b..cea5bdb5 100644 --- a/java-reporter-junit/src/test/java/io/testomat/junit/methodexporter/FileParserTest.java +++ b/java-reporter-junit/src/test/java/io/testomat/junit/methodexporter/FileParserTest.java @@ -12,11 +12,13 @@ import io.testomat.junit.exception.MethodExporterException; import io.testomat.junit.methodexporter.parser.FileParser; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -347,19 +349,19 @@ void shouldHandleNullFilepathGracefully() { @Test @DisplayName("Should handle relative paths") void shouldHandleRelativePaths(@TempDir Path tempDir) throws IOException { - // Given String javaContent = "public class RelativePathTest {}"; Path testFile = tempDir.resolve("RelativePathTest.java"); - java.nio.file.Files.write(testFile, javaContent.getBytes()); - - // Create relative path from current directory + Files.writeString(testFile, javaContent); Path currentDir = Paths.get("").toAbsolutePath(); - Path relativePath = currentDir.relativize(testFile); + Path pathToUse; - // When - CompilationUnit result = fileParser.parseFile(relativePath.toString()); + if (Objects.equals(currentDir.getRoot(), testFile.getRoot())) { + pathToUse = currentDir.relativize(testFile); + } else { + pathToUse = testFile.toAbsolutePath(); + } - // Then + CompilationUnit result = fileParser.parseFile(pathToUse.toString()); assertNotNull(result); assertTrue(result.toString().contains("RelativePathTest")); } diff --git a/java-reporter-karate/pom.xml b/java-reporter-karate/pom.xml index 19f155f7..7e417686 100644 --- a/java-reporter-karate/pom.xml +++ b/java-reporter-karate/pom.xml @@ -6,7 +6,7 @@ io.testomat java-reporter-karate - 0.2.1 + 0.2.2 jar Testomat.io Java Reporter Karate @@ -52,7 +52,7 @@ io.testomat java-reporter-core - 0.8.13 + 0.9.0 io.karatelabs diff --git a/java-reporter-testng/pom.xml b/java-reporter-testng/pom.xml index 8124ceaa..f6db8543 100644 --- a/java-reporter-testng/pom.xml +++ b/java-reporter-testng/pom.xml @@ -6,7 +6,7 @@ io.testomat java-reporter-testng - 0.7.8 + 0.7.9 jar Testomat.io Java Reporter TestNG @@ -47,7 +47,7 @@ io.testomat java-reporter-core - 0.8.13 + 0.9.0 org.slf4j diff --git a/java-reporter-testng/src/main/java/io/testomat/testng/reporter/TestNgTestResultReporter.java b/java-reporter-testng/src/main/java/io/testomat/testng/reporter/TestNgTestResultReporter.java index 7fe68937..920c1777 100644 --- a/java-reporter-testng/src/main/java/io/testomat/testng/reporter/TestNgTestResultReporter.java +++ b/java-reporter-testng/src/main/java/io/testomat/testng/reporter/TestNgTestResultReporter.java @@ -1,5 +1,7 @@ package io.testomat.testng.reporter; +import static io.testomat.core.constants.CommonConstants.FAILED; +import static io.testomat.core.constants.CommonConstants.PASSED; import static io.testomat.core.constants.CommonConstants.SKIPPED; import io.testomat.core.exception.ReportTestResultException; @@ -13,7 +15,9 @@ import io.testomat.testng.extractor.TestNgParameterExtractor; import io.testomat.testng.extractor.TestNgTestWrapper; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Set; import org.testng.ISuite; import org.testng.ITestResult; @@ -31,14 +35,16 @@ public class TestNgTestResultReporter { private final TestNgMetaDataExtractor metaDataExtractor; private final TestNgParameterExtractor parameterExtractor; private final GlobalRunManager runManager; - private final Set processedTests; + private final List processedTests; + private final Set testIds; public TestNgTestResultReporter() { this.resultConstructor = new TestNgTestResultConstructor(); this.metaDataExtractor = new TestNgMetaDataExtractor(); this.parameterExtractor = new TestNgParameterExtractor(); this.runManager = GlobalRunManager.getInstance(); - this.processedTests = new HashSet<>(); + this.processedTests = new ArrayList<>(); + this.testIds = new HashSet<>(); } /** @@ -52,7 +58,8 @@ public TestNgTestResultReporter(TestNgTestResultConstructor resultConstructor, this.metaDataExtractor = metaDataExtractor; this.parameterExtractor = parameterExtractor; this.runManager = runManager; - this.processedTests = new HashSet<>(); + this.testIds = new HashSet<>(); + this.processedTests = new ArrayList<>(); } /** @@ -71,13 +78,15 @@ public void reportTestResult(ITestResult result, String status) { String rid = parameterExtractor.generateRid(result); String methodKey = rid != null ? baseKey + "-" + rid : baseKey; - if (processedTests.contains(methodKey)) { + TestNgTestWrapper wrapper = TestNgTestWrapper.forRegularTest(result); + TestMetadata metadata = metaDataExtractor.extractTestMetadata(wrapper); + + if (processedTests.contains(methodKey) && !testIds.contains(metadata.getTestId())) { return; } processedTests.add(methodKey); - TestNgTestWrapper wrapper = TestNgTestWrapper.forRegularTest(result); - TestMetadata metadata = metaDataExtractor.extractTestMetadata(wrapper); + testIds.add(metadata.getTestId()); Object example = parameterExtractor.extractExample(result); @@ -153,6 +162,15 @@ private void reportTestResultWithParameters(TestMetadata metadata, String status TestResultWrapper wrapper = builder.build(); TestResult result = resultConstructor.constructTestRunResult(wrapper); + + if (testIds.contains(result.getTestId())) { + result.setOverwrite(false); + + if (!PASSED.equals(result.getStatus())) { + result.setStatus(FAILED); + } + } + runManager.reportTest(result); } catch (Exception e) {