Skip to content

Commit 948d2cb

Browse files
committed
[DRAFT] Issue-105 Add artifacts support for steps
1 parent 7f9ac6d commit 948d2cb

10 files changed

Lines changed: 165 additions & 14 deletions

File tree

java-reporter-core/src/main/java/io/testomat/core/annotation/Step.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@
99
@Target(ElementType.METHOD)
1010
public @interface Step {
1111
String value() default "";
12+
String[] artifacts() default {};
1213
}

java-reporter-core/src/main/java/io/testomat/core/client/request/NativeRequestBodyBuilder.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import io.testomat.core.constants.ApiRequestFields;
1515
import io.testomat.core.exception.FailedToCreateRunBodyException;
1616
import io.testomat.core.facade.methods.artifact.ReportedTestStorage;
17+
import io.testomat.core.facade.methods.artifact.TempArtifactDirectoriesStorage;
1718
import io.testomat.core.facade.methods.label.LabelStorage;
1819
import io.testomat.core.facade.methods.logmethod.LogStorage;
1920
import io.testomat.core.facade.methods.meta.MetaStorage;
@@ -158,6 +159,18 @@ private Map<String, Object> buildTestResultMap(TestResult result) throws JsonPro
158159
}
159160

160161
if (result.getSteps() != null && !result.getSteps().isEmpty()) {
162+
result.getSteps().forEach(step -> {
163+
List<String> links =
164+
TempArtifactDirectoriesStorage.STEP_DIRECTORIES
165+
.remove(step.getId());
166+
if (links == null || links.isEmpty()) {
167+
return;
168+
}
169+
step.setArtifacts(
170+
links.toArray(new String[0])
171+
);
172+
});
173+
161174
List<Map<String, Object>> stepsMap = convertStepsToMap(result.getSteps());
162175
body.put("steps", stepsMap);
163176
System.out.println("DEBUG: Adding " + result.getSteps().size() + " steps to request body for test: " + result.getTitle());
@@ -203,6 +216,14 @@ private List<Map<String, Object>> convertStepsToMap(List<TestStep> steps) {
203216
stepMap.put("title", step.getStepTitle());
204217
}
205218

219+
if (step.getArtifacts() != null) {
220+
stepMap.put("artifacts", step.getArtifacts());
221+
}
222+
223+
if (step.getStatus() != null) {
224+
stepMap.put("status", step.getStatus());
225+
}
226+
206227
stepMap.put("duration", step.getDuration());
207228

208229
if (step.getSubsteps() != null && !step.getSubsteps().isEmpty()) {

java-reporter-core/src/main/java/io/testomat/core/facade/Testomatio.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
import io.testomat.core.facade.methods.label.LabelStorage;
55
import io.testomat.core.facade.methods.logmethod.LogStorage;
66
import io.testomat.core.facade.methods.meta.MetaStorage;
7+
import io.testomat.core.step.StepLifecycle;
78
import java.util.List;
89
import java.util.Map;
10+
import java.util.UUID;
911

1012
/**
1113
* Main public API facade for Testomat.io integration.
@@ -21,6 +23,15 @@ public static void artifact(String... directories) {
2123
ServiceRegistryUtil.getService(ArtifactManager.class).storeDirectories(directories);
2224
}
2325

26+
public static void stepArtifact(String... directories) {
27+
UUID stepId = StepLifecycle.current();
28+
ServiceRegistryUtil.getService(ArtifactManager.class).storeStepDirectories(stepId, directories);
29+
}
30+
31+
public static void stepArtifact(UUID stepId, String... directories) {
32+
ServiceRegistryUtil.getService(ArtifactManager.class).storeStepDirectories(stepId, directories);
33+
}
34+
2435
public static void meta(String key, String value) {
2536
MetaStorage.TEMP_META_STORAGE.get().put(key, value);
2637
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
11
package io.testomat.core.facade.methods.artifact;
22

33
import java.util.ArrayList;
4+
import java.util.Collections;
45
import java.util.List;
6+
import java.util.UUID;
7+
import java.util.concurrent.ConcurrentHashMap;
58

69
/**
710
* Thread-local storage for temporarily holding artifact file paths during test execution.
811
* Ensures thread safety when multiple tests run concurrently.
912
*/
1013
public class TempArtifactDirectoriesStorage {
1114
public static final ThreadLocal<List<String>> DIRECTORIES = ThreadLocal.withInitial(ArrayList::new);
15+
public static final ConcurrentHashMap<UUID, List<String>> STEP_DIRECTORIES = new ConcurrentHashMap<>();
1216

1317
public static void store(String dir) {
1418
DIRECTORIES.get().add(dir);
1519
}
20+
public static void stepStore(UUID stepId, String dir) {
21+
STEP_DIRECTORIES
22+
.computeIfAbsent(stepId,
23+
k -> Collections.synchronizedList(new ArrayList<>()))
24+
.add(dir);
25+
}
1626
}

java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/client/AwsService.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ public void uploadAllArtifactsForTest(String testName, String rid, String testId
7474
S3Credentials credentials = CredentialsManager.getCredentials();
7575
List<String> uploadedArtifactsLinks = processArtifacts(artifactDirectories, testName, rid, credentials);
7676

77+
if (!TempArtifactDirectoriesStorage.STEP_DIRECTORIES.isEmpty()) {
78+
processStepArtifacts(testName, rid, credentials);
79+
}
80+
7781
storeArtifactLinkData(testName, rid, testId, uploadedArtifactsLinks);
7882

7983
// Clear artifact directories after processing
@@ -92,6 +96,19 @@ private List<String> processArtifacts(List<String> artifactDirectories, String t
9296
return uploadedLinks;
9397
}
9498

99+
private void processStepArtifacts(String testName, String rid, S3Credentials credentials) {
100+
101+
TempArtifactDirectoriesStorage.STEP_DIRECTORIES
102+
.forEach((stepId, list) -> list.replaceAll(dir -> {
103+
if (dir.startsWith("http")) {
104+
return dir;
105+
}
106+
String key = keyGenerator.generateKey(dir, rid, testName);
107+
uploadArtifact(dir, key, credentials);
108+
return urlGenerator.generateUrl(credentials.getBucket(), key);
109+
}));
110+
}
111+
95112
private void storeArtifactLinkData(String testName, String rid, String testId, List<String> uploadedLinks) {
96113
ArtifactLinkData linkData = new ArtifactLinkData(testName, rid, testId, uploadedLinks);
97114
ArtifactLinkDataStorage.ARTEFACT_LINK_DATA_STORAGE.add(linkData);

java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/manager/ArtifactManager.java

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,34 @@
1111
import java.nio.file.InvalidPathException;
1212
import java.nio.file.Path;
1313
import java.nio.file.Paths;
14+
import java.util.UUID;
15+
import java.util.function.Consumer;
1416
import org.slf4j.Logger;
1517
import org.slf4j.LoggerFactory;
1618

1719
public class ArtifactManager {
1820
private static final Logger log = LoggerFactory.getLogger(ArtifactManager.class);
1921

2022
public void storeDirectories(String... directories) {
23+
store(directories,
24+
TempArtifactDirectoriesStorage::store,
25+
"Invalid artifact path provided: {}");
26+
}
27+
28+
public void storeStepDirectories(UUID stepId, String... directories) {
29+
store(directories,
30+
dir -> TempArtifactDirectoriesStorage.stepStore(stepId, dir),
31+
"Invalid step artifact path provided: {}");
32+
}
33+
34+
private void store(String[] directories, Consumer<String> storage, String logMessage) {
35+
2136
for (String dir : directories) {
22-
if (isValidFilePath(dir)) {
23-
TempArtifactDirectoriesStorage.store(dir);
24-
} else {
25-
log.info("Invalid artifact path provided: {}", dir);
37+
if (!isValidFilePath(dir)) {
38+
log.info(logMessage, dir);
39+
continue;
2640
}
41+
storage.accept(dir);
2742
}
2843
}
2944

java-reporter-core/src/main/java/io/testomat/core/step/StepAspect.java

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package io.testomat.core.step;
22

3+
import static io.testomat.core.facade.Testomatio.stepArtifact;
4+
35
import io.testomat.core.annotation.Step;
6+
import java.util.UUID;
47
import org.aspectj.lang.ProceedingJoinPoint;
58
import org.aspectj.lang.annotation.Around;
69
import org.aspectj.lang.annotation.Aspect;
@@ -29,40 +32,45 @@ public class StepAspect {
2932
@Around("execution(@io.testomat.core.annotation.Step * *(..)) && @annotation(step)")
3033
public Object aroundStep(ProceedingJoinPoint joinPoint, Step step) throws Throwable {
3134
String stepName = resolveStepName(joinPoint, step);
35+
String[] artifacts = resolveAttachments(step);
3236
long startTime = System.currentTimeMillis();
3337

3438
log.info("Step aspect triggered for: {}", stepName);
35-
39+
StepLifecycle.start(UUID.randomUUID());
40+
Object result;
3641
try {
37-
return executeStepSuccessfully(joinPoint, stepName, startTime);
42+
result = executeStepSuccessfully(joinPoint, stepName, artifacts, startTime);
3843
} catch (Throwable e) {
39-
handleStepFailure(stepName, startTime, e);
44+
handleStepFailure(stepName, artifacts, startTime, e);
4045
throw e;
46+
} finally {
47+
StepLifecycle.finish();
4148
}
49+
return result;
4250
}
4351

44-
private Object executeStepSuccessfully(ProceedingJoinPoint joinPoint, String stepName, long startTime) throws Throwable {
52+
private Object executeStepSuccessfully(ProceedingJoinPoint joinPoint, String stepName, String[] artifacts, long startTime) throws Throwable {
4553
Object result = joinPoint.proceed();
4654
long duration = calculateDuration(startTime);
4755

48-
recordStep(stepName, duration);
56+
recordStep(stepName, artifacts, StepStatus.passed, duration);
4957
log.info("Step '{}' added to storage. Total steps: {}", stepName, StepStorage.getSteps().size());
5058

5159
return result;
5260
}
5361

54-
private void handleStepFailure(String stepName, long startTime, Throwable e) {
62+
private void handleStepFailure(String stepName, String[] artifacts, long startTime, Throwable e) {
5563
long duration = calculateDuration(startTime);
5664
log.error("Step '{}' failed after {} ms", stepName, duration, e);
57-
recordStep(stepName, duration);
65+
recordStep(stepName, artifacts, StepStatus.failed, duration);
5866
}
5967

6068
private long calculateDuration(long startTime) {
6169
return System.currentTimeMillis() - startTime;
6270
}
6371

64-
private void recordStep(String stepName, long duration) {
65-
TestStep testStep = createTestStep(stepName, duration);
72+
private void recordStep(String stepName, String[] artifacts, StepStatus stepStatus, long duration) {
73+
TestStep testStep = createTestStep(stepName, artifacts, stepStatus, duration);
6674
StepStorage.addStep(testStep);
6775
}
6876

@@ -87,6 +95,13 @@ private String getStepNameTemplate(ProceedingJoinPoint joinPoint, Step step) {
8795
return signature.getName();
8896
}
8997

98+
private String[] resolveAttachments(Step step) {
99+
if (step.artifacts() != null && step.artifacts().length > 0) {
100+
return step.artifacts();
101+
}
102+
return null;
103+
}
104+
90105
/**
91106
* Substitutes parameter placeholders in the step name with actual parameter values.
92107
* Supports both indexed placeholders {0}, {1}, etc. and named placeholders {parameterName}.
@@ -157,12 +172,17 @@ private String formatParameterValue(Object value) {
157172
* @param durationMillis the execution duration in milliseconds
158173
* @return populated TestStep object
159174
*/
160-
private TestStep createTestStep(String stepName, long durationMillis) {
175+
private TestStep createTestStep(String stepName, String[] artifacts, StepStatus stepStatus, long durationMillis) {
161176
TestStep testStep = new TestStep();
177+
testStep.setId(StepLifecycle.current());
162178
testStep.setCategory("user");
163179
testStep.setStepTitle(stepName);
180+
testStep.setStatus(stepStatus);
164181
testStep.setDuration(durationMillis);
165182

183+
if (artifacts != null) {
184+
stepArtifact(testStep.getId(), artifacts);
185+
}
166186
log.debug("Step '{}' completed in {} ms", stepName, durationMillis);
167187

168188
return testStep;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package io.testomat.core.step;
2+
3+
import java.util.ArrayDeque;
4+
import java.util.Deque;
5+
import java.util.UUID;
6+
7+
public class StepLifecycle {
8+
9+
private static final ThreadLocal<Deque<UUID>> CURRENT_STEPS =
10+
ThreadLocal.withInitial(ArrayDeque::new);
11+
12+
public static void start(UUID stepId) {
13+
CURRENT_STEPS.get().push(stepId);
14+
}
15+
16+
public static void finish() {
17+
CURRENT_STEPS.get().pop();
18+
}
19+
20+
public static UUID current() {
21+
return CURRENT_STEPS.get().peek();
22+
}
23+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package io.testomat.core.step;
2+
3+
public enum StepStatus {
4+
passed, failed, none
5+
}

java-reporter-core/src/main/java/io/testomat/core/step/TestStep.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
22

33
import java.util.ArrayList;
44
import java.util.List;
5+
import java.util.UUID;
56

67
public class TestStep {
8+
private UUID id;
79
private String category;
810
private String stepTitle;
11+
private StepStatus status;
912
private double duration;
1013
private List<TestStep> substeps = new ArrayList<>();
14+
private String[] artifacts;
1115

1216
public String getCategory() {
1317
return category;
@@ -40,4 +44,28 @@ public List<TestStep> getSubsteps() {
4044
public void setSubsteps(List<TestStep> substeps) {
4145
this.substeps = substeps;
4246
}
47+
48+
public String[] getArtifacts() {
49+
return artifacts;
50+
}
51+
52+
public void setArtifacts(String[] artifacts) {
53+
this.artifacts = artifacts;
54+
}
55+
56+
public void setId(UUID id) {
57+
this.id = id;
58+
}
59+
60+
public UUID getId() {
61+
return id;
62+
}
63+
64+
public StepStatus getStatus() {
65+
return status;
66+
}
67+
68+
public void setStatus(StepStatus status) {
69+
this.status = status;
70+
}
4371
}

0 commit comments

Comments
 (0)