Skip to content

Commit 33803e8

Browse files
committed
feat(engine, server-core, server): save a snapshot of input/output in the step context
1 parent 39b40e9 commit 33803e8

File tree

16 files changed

+237
-118
lines changed

16 files changed

+237
-118
lines changed

chutney/engine/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@
3636
<groupId>org.springframework</groupId>
3737
<artifactId>spring-core</artifactId>
3838
</dependency>
39+
<dependency>
40+
<groupId>org.jdom</groupId>
41+
<artifactId>jdom2</artifactId>
42+
</dependency>
3943
<dependency>
4044
<groupId>org.springframework.security</groupId>
4145
<artifactId>spring-security-core</artifactId>

chutney/engine/src/main/java/com/chutneytesting/ExecutionConfiguration.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,20 @@
4444
import com.chutneytesting.engine.infrastructure.delegation.HttpClient;
4545
import com.chutneytesting.tools.ThrowingFunction;
4646
import com.chutneytesting.tools.loader.ExtensionLoaders;
47+
import com.fasterxml.jackson.annotation.JsonInclude;
48+
import com.fasterxml.jackson.databind.ObjectMapper;
49+
import com.fasterxml.jackson.databind.SerializationFeature;
50+
import com.fasterxml.jackson.databind.module.SimpleModule;
4751
import java.lang.reflect.InvocationTargetException;
4852
import java.util.Map;
4953
import java.util.Set;
5054
import java.util.concurrent.ExecutorService;
5155
import java.util.concurrent.Executors;
5256
import java.util.stream.Collectors;
57+
import org.jdom2.Element;
5358
import org.slf4j.Logger;
5459
import org.slf4j.LoggerFactory;
60+
import org.springframework.core.io.Resource;
5561
import org.springframework.util.ReflectionUtils;
5662

5763
public class ExecutionConfiguration {
@@ -65,7 +71,7 @@ public class ExecutionConfiguration {
6571

6672
private final SpelFunctions spelFunctions;
6773
private final Set<StepExecutionStrategy> stepExecutionStrategies;
68-
74+
private static ObjectMapper objectMapper;
6975
private final Long reporterTTL;
7076

7177
public ExecutionConfiguration() {
@@ -105,6 +111,21 @@ public ExecutionEngine executionEngine() {
105111
return executionEngine;
106112
}
107113

114+
public static ObjectMapper reportObjectMapper() {
115+
if (objectMapper == null) {
116+
SimpleModule jdomElementModule = new SimpleModule();
117+
jdomElementModule.addSerializer(Element.class, new JDomElementSerializer());
118+
objectMapper = new ObjectMapper()
119+
.addMixIn(Resource.class, MyMixInForIgnoreType.class)
120+
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
121+
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
122+
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
123+
.registerModule(jdomElementModule)
124+
.findAndRegisterModules();
125+
}
126+
return objectMapper;
127+
}
128+
108129
private ActionTemplateLoader createActionTemplateLoaderV2() {
109130
return new DefaultActionTemplateLoader<>(
110131
"chutney.actions",
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2017-2023 Enedis
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
package com.chutneytesting;
19+
20+
import com.fasterxml.jackson.core.JsonGenerator;
21+
import com.fasterxml.jackson.databind.SerializerProvider;
22+
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
23+
import java.io.IOException;
24+
import java.io.Serial;
25+
import org.jdom2.Element;
26+
import org.jdom2.output.Format;
27+
import org.jdom2.output.XMLOutputter;
28+
29+
public class JDomElementSerializer extends StdSerializer<Element> {
30+
31+
@Serial
32+
private static final long serialVersionUID = 1L;
33+
34+
public JDomElementSerializer() {
35+
this(null);
36+
}
37+
38+
protected JDomElementSerializer(Class<Element> t) {
39+
super(t);
40+
}
41+
42+
@Override
43+
public void serialize(Element element, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
44+
String xmlString = new XMLOutputter(Format.getCompactFormat()).outputString(element);
45+
jsonGenerator.writeObject(xmlString);
46+
}
47+
}

chutney/server/src/main/java/com/chutneytesting/tools/ui/MyMixInForIgnoreType.java renamed to chutney/engine/src/main/java/com/chutneytesting/MyMixInForIgnoreType.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
/*
2-
* Copyright 2017-2023 Enedis
2+
* Copyright 2017-2023 Enedis
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
66
* You may obtain a copy of the License at
77
*
8-
* http://www.apache.org/licenses/LICENSE-2.0
8+
* http://www.apache.org/licenses/LICENSE-2.0
99
*
1010
* Unless required by applicable law or agreed to in writing, software
1111
* distributed under the License is distributed on an "AS IS" BASIS,
1212
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
15+
*
1516
*/
1617

17-
package com.chutneytesting.tools.ui;
18+
package com.chutneytesting;
1819

1920
import com.fasterxml.jackson.annotation.JsonIgnoreType;
2021

chutney/engine/src/main/java/com/chutneytesting/engine/api/execution/StepExecutionReportMapper.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ static StepExecutionReportDto toDto(StepExecutionReport report) {
3838
report.information,
3939
report.errors,
4040
report.steps.stream().map(StepExecutionReportMapper::toDto).collect(Collectors.toList()),
41-
StepContextMapper.toDto(report.scenarioContext, report.evaluatedInputs, report.stepResults),
41+
StepContextMapper.toDto(report.scenarioContext, report.evaluatedInputsSnapshot, report.stepResultsSnapshot),
4242
report.type,
4343
report.targetName,
4444
report.targetUrl,
@@ -49,11 +49,11 @@ static StepExecutionReportDto toDto(StepExecutionReport report) {
4949
static class StepContextMapper {
5050

5151
@SuppressWarnings("unchecked")
52-
static StepExecutionReportDto.StepContextDto toDto(Map<String, Object> scenarioContext, Map<String, Object> evaluatedInput, Map<String, Object> stepResults) {
52+
static StepExecutionReportDto.StepContextDto toDto(Map<String, Object> scenarioContext, Map<String, Object> evaluatedInputSnapshot, Map<String, Object> stepResultsSnapshot) {
5353
return new StepExecutionReportDto.StepContextDto(
5454
scenarioContext != null ? scenarioContext : EMPTY_MAP,
55-
evaluatedInput != null ? evaluatedInput : EMPTY_MAP,
56-
stepResults != null ? stepResults : EMPTY_MAP
55+
evaluatedInputSnapshot != null ? evaluatedInputSnapshot : EMPTY_MAP,
56+
stepResultsSnapshot != null ? stepResultsSnapshot : EMPTY_MAP
5757
);
5858
}
5959

chutney/engine/src/main/java/com/chutneytesting/engine/domain/execution/engine/step/Step.java

Lines changed: 71 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import static java.util.Collections.unmodifiableMap;
2222
import static java.util.Optional.ofNullable;
2323

24+
import com.chutneytesting.ExecutionConfiguration;
2425
import com.chutneytesting.action.spi.ActionExecutionResult;
2526
import com.chutneytesting.action.spi.injectable.Target;
2627
import com.chutneytesting.engine.domain.environment.TargetImpl;
@@ -39,6 +40,8 @@
3940
import com.chutneytesting.engine.domain.execution.report.StepExecutionReport;
4041
import com.chutneytesting.engine.domain.execution.strategies.StepStrategyDefinition;
4142
import com.chutneytesting.tools.Try;
43+
import com.fasterxml.jackson.core.JsonProcessingException;
44+
import com.fasterxml.jackson.databind.ObjectMapper;
4245
import com.google.common.collect.Lists;
4346
import com.google.common.collect.Maps;
4447
import java.time.Duration;
@@ -67,7 +70,7 @@ public class Step {
6770
private Target target;
6871
private final StepExecutor executor;
6972
private final StepDataEvaluator dataEvaluator;
70-
73+
private static final ObjectMapper objectMapper = ExecutionConfiguration.reportObjectMapper();
7174
private StepContext stepContext;
7275

7376
public Step(StepDataEvaluator dataEvaluator, StepDefinition definition, StepExecutor executor, List<Step> steps) {
@@ -77,7 +80,7 @@ public Step(StepDataEvaluator dataEvaluator, StepDefinition definition, StepExec
7780
this.executor = executor;
7881
this.steps = steps;
7982
this.state = new StepState(definition.name);
80-
this.stepContext = new StepContext();
83+
this.stepContext = new StepContext(objectMapper);
8184
}
8285

8386
public static Step nonExecutable(StepDefinition definition) {
@@ -111,7 +114,7 @@ public Status execute(ScenarioExecution scenarioExecution, ScenarioContext scena
111114
target = dataEvaluator.evaluateTarget(target, evaluationContext);
112115
resolveName(evaluationContext);
113116
Try
114-
.exec(() -> this.stepContext = new StepContext(scenarioContext, localContext, evaluatedInputs))
117+
.exec(() -> this.stepContext = new StepContext(scenarioContext, localContext, evaluatedInputs, objectMapper))
115118
.ifSuccess(stepContextExecuted -> {
116119
executor.execute(scenarioExecution, target, this);
117120
if (Status.SUCCESS.equals(this.state.status())) {
@@ -154,6 +157,10 @@ public String name() {
154157
return this.state.name();
155158
}
156159

160+
public ObjectMapper getObjectMapper() {
161+
return objectMapper;
162+
}
163+
157164
public void resolveName(Map<String, Object> context) {
158165
this.state.setName(dataEvaluator.silentEvaluateString(state.name(), context));
159166
}
@@ -329,10 +336,6 @@ public void addStepExecution(Step step) {
329336
this.steps.add(step);
330337
}
331338

332-
public void addStepExecution(List<Step> steps) {
333-
steps.forEach(this::addStepExecution);
334-
}
335-
336339
public Map<String, Object> getEvaluatedInputs() {
337340
return unmodifiableMap(this.stepContext.getEvaluatedInputs());
338341
}
@@ -345,6 +348,14 @@ public Map<String, Object> getStepOutputs() {
345348
return unmodifiableMap(this.stepContext.getStepOutputs());
346349
}
347350

351+
public Map<String, Object> getStepContextInputSnapshot() {
352+
return this.stepContext.stepContextSnapshot.getInputsSnapshot();
353+
}
354+
355+
public Map<String, Object> getStepContextOutputSnapshot() {
356+
return this.stepContext.stepContextSnapshot.getOutputsSnapshot();
357+
}
358+
348359
public void removeStepExecution() {
349360
this.steps.clear();
350361
}
@@ -356,20 +367,27 @@ private static class StepContext {
356367
private final Map<String, Object> localContext;
357368
private final Map<String, Object> evaluatedInputs;
358369
private final Map<String, Object> stepOutputs;
370+
private StepContextSnapshot stepContextSnapshot;
359371

360-
private StepContext() {
361-
this(new ScenarioContextImpl(), new LinkedHashMap<>(), new LinkedHashMap<>());
372+
private StepContext(ObjectMapper objectMapper) {
373+
this(new ScenarioContextImpl(), new LinkedHashMap<>(), new LinkedHashMap<>(), objectMapper);
362374
}
363375

364-
private StepContext(ScenarioContext scenarioContext, Map<String, Object> localContext, Map<String, Object> evaluatedInputs) throws EvaluationException {
365-
this(scenarioContext, localContext, evaluatedInputs, new LinkedHashMap<>());
376+
private StepContext(ScenarioContext scenarioContext, Map<String, Object> localContext, Map<String, Object> evaluatedInputs, ObjectMapper objectMapper) throws EvaluationException {
377+
this(scenarioContext, localContext, evaluatedInputs, new LinkedHashMap<>(), objectMapper);
366378
}
367379

368-
private StepContext(ScenarioContext scenarioContext, Map<String, Object> localContext, Map<String, Object> evaluatedInputs, Map<String, Object> stepOutputs) {
380+
private StepContext(ScenarioContext scenarioContext, Map<String, Object> localContext, Map<String, Object> evaluatedInputs, Map<String, Object> stepOutputs, ObjectMapper objectMapper) {
369381
this.scenarioContext = scenarioContext;
370382
this.localContext = localContext;
371383
this.evaluatedInputs = evaluatedInputs;
372384
this.stepOutputs = stepOutputs;
385+
this.stepContextSnapshot = new StepContextSnapshot(objectMapper);
386+
}
387+
388+
private StepContext copySnapshotsInputOutput() {
389+
this.stepContextSnapshot = new StepContextSnapshot(evaluatedInputs, stepOutputs, objectMapper);
390+
return this;
373391
}
374392

375393
private Map<String, Object> evaluationContext() {
@@ -387,17 +405,6 @@ private Map<String, Object> getEvaluatedInputs() {
387405
return ofNullable(evaluatedInputs).orElse(emptyMap());
388406
}
389407

390-
@SafeVarargs
391-
private void addLocalContext(Map.Entry<String, Object>... entries) {
392-
this.addLocalContext(Map.ofEntries(entries));
393-
}
394-
395-
private void addLocalContext(Map<String, Object> localContext) {
396-
if (localContext != null) {
397-
this.localContext.putAll(localContext);
398-
}
399-
}
400-
401408
private void addStepOutputs(Map<String, Object> stepOutputs) {
402409
if (stepOutputs != null) {
403410
this.stepOutputs.putAll(stepOutputs);
@@ -415,7 +422,47 @@ private Map<String, Object> getStepOutputs() {
415422
}
416423

417424
private StepContext copy() {
418-
return new StepContext(scenarioContext.unmodifiable(), unmodifiableMap(localContext), unmodifiableMap(evaluatedInputs), unmodifiableMap(stepOutputs));
425+
return new StepContext(scenarioContext.unmodifiable(), unmodifiableMap(localContext), unmodifiableMap(evaluatedInputs), unmodifiableMap(stepOutputs), objectMapper).copySnapshotsInputOutput();
426+
}
427+
}
428+
429+
public static class StepContextSnapshot {
430+
private final Map<String, Object> inputsSnapshot;
431+
private final Map<String, Object> outputsSnapshot;
432+
private final ObjectMapper objectMapper;
433+
434+
public StepContextSnapshot(ObjectMapper objectMapper) {
435+
this.objectMapper = objectMapper;
436+
this.inputsSnapshot = emptyMap();
437+
this.outputsSnapshot = emptyMap();
438+
}
439+
440+
public StepContextSnapshot(Map<String, Object> inputsSnapshot, Map<String, Object> outputsSnapshot, ObjectMapper objectMapper) {
441+
this.objectMapper = objectMapper;
442+
this.inputsSnapshot = mapStringObjectToString(inputsSnapshot);
443+
this.outputsSnapshot = mapStringObjectToString(outputsSnapshot);
444+
}
445+
446+
public Map<String, Object> getInputsSnapshot() {
447+
return unmodifiableMap(inputsSnapshot);
448+
}
449+
450+
public Map<String, Object> getOutputsSnapshot() {
451+
return unmodifiableMap(outputsSnapshot);
452+
}
453+
454+
private Map<String, Object> mapStringObjectToString(Map<String, Object> originalMap) {
455+
Map<String, Object> stringMap = new HashMap<>();
456+
originalMap.forEach((key, value) -> {
457+
try {
458+
String stringObject = objectMapper.writeValueAsString(value);
459+
Object jsonObject = objectMapper.readTree(stringObject);
460+
stringMap.put(key, jsonObject);
461+
} catch (JsonProcessingException e) {
462+
throw new RuntimeException(e);
463+
}
464+
});
465+
return stringMap;
419466
}
420467
}
421468
}

chutney/engine/src/main/java/com/chutneytesting/engine/domain/execution/report/StepExecutionReport.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,13 @@ public class StepExecutionReport implements Status.HavingStatus {
4141
public final String targetUrl;
4242
public final String strategy;
4343
public final Map<String, Object> evaluatedInputs;
44+
public final Map<String, Object> evaluatedInputsSnapshot;
4445

4546
@JsonIgnore
4647
public Map<String, Object> stepResults;
4748
@JsonIgnore
49+
public Map<String, Object> stepResultsSnapshot;
50+
@JsonIgnore
4851
public Map<String, Object> scenarioContext;
4952

5053
@JsonCreator
@@ -62,7 +65,7 @@ public StepExecutionReport(Long executionId,
6265
String targetUrl,
6366
String strategy
6467
) {
65-
this(executionId, name, environment, duration, startDate, status, information, errors, steps, type, targetName, targetUrl, strategy, null, null, null);
68+
this(executionId, name, environment, duration, startDate, status, information, errors, steps, type, targetName, targetUrl, strategy, null, null, null, null, null);
6669
}
6770

6871
public StepExecutionReport(Long executionId,
@@ -80,7 +83,9 @@ public StepExecutionReport(Long executionId,
8083
String strategy,
8184
Map<String, Object> evaluatedInputs,
8285
Map<String, Object> stepResults,
83-
Map<String, Object> scenarioContext
86+
Map<String, Object> scenarioContext,
87+
Map<String, Object> evaluatedInputsSnapshot,
88+
Map<String, Object> stepResultsSnapshot
8489
) {
8590
this.executionId = executionId;
8691
this.name = name;
@@ -96,7 +101,9 @@ public StepExecutionReport(Long executionId,
96101
this.targetUrl = targetUrl;
97102
this.strategy = strategy;
98103
this.evaluatedInputs = evaluatedInputs != null ? evaluatedInputs : emptyMap();
104+
this.evaluatedInputsSnapshot = evaluatedInputsSnapshot != null ? evaluatedInputsSnapshot : emptyMap();
99105
this.stepResults = stepResults != null ? stepResults : emptyMap();
106+
this.stepResultsSnapshot = stepResultsSnapshot != null ? stepResultsSnapshot : emptyMap();
100107
this.scenarioContext = scenarioContext != null ? scenarioContext : emptyMap();
101108
}
102109

0 commit comments

Comments
 (0)