Skip to content

Commit 248b80e

Browse files
authored
generate consistent history ids from final test parameters (fixes #349, via #1323)
1 parent 22cb102 commit 248b80e

33 files changed

Lines changed: 934 additions & 150 deletions

File tree

allure-citrus/src/main/java/io/qameta/allure/citrus/AllureCitrus.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
import static io.qameta.allure.util.ResultsUtils.createThreadLabel;
6161
import static io.qameta.allure.util.ResultsUtils.createTitlePath;
6262
import static io.qameta.allure.util.ResultsUtils.getProvidedLabels;
63+
import static io.qameta.allure.util.ResultsUtils.md5;
6364

6465
/**
6566
* Reports Citrus test execution to Allure.
@@ -187,7 +188,10 @@ public void onTestFailure(final TestCase test, final Throwable cause) {
187188
*/
188189
@Override
189190
public void onTestSkipped(final TestCase test) {
190-
//do nothing
191+
if (!isTestStarted(test)) {
192+
startTest(test);
193+
}
194+
stopTest(test, Status.SKIPPED, null);
191195
}
192196

193197
/**
@@ -217,9 +221,15 @@ public void onTestActionSkipped(final TestCase testCase, final TestAction testAc
217221
private void startTest(final TestCase testCase) {
218222
final AllureExternalKey testKey = createTestKey(testCase);
219223
final Optional<? extends Class<?>> testClass = Optional.ofNullable(testCase.getTestClass());
224+
final String fullName = testClass
225+
.map(Class::getName)
226+
.map(className -> className + "." + testCase.getName())
227+
.orElseGet(testCase::getName);
220228

221229
final TestResult result = new TestResult()
222230
.setName(testCase.getName())
231+
.setFullName(fullName)
232+
.setTestCaseId(md5(fullName))
223233
.setTitlePath(
224234
testClass
225235
.map(ResultsUtils::createTitlePathFromJavaClass)
@@ -267,7 +277,7 @@ private void stopTest(final TestCase testCase,
267277
.collect(Collectors.toList());
268278

269279
getLifecycle().updateTest(testKey, result -> {
270-
result.setParameters(parameters);
280+
result.getParameters().addAll(parameters);
271281
result.setStatus(status);
272282
result.setStatusDetails(details);
273283
});
@@ -286,6 +296,15 @@ private AllureExternalKey createTestKey(final TestCase testCase) {
286296
return testKey;
287297
}
288298

299+
private boolean isTestStarted(final TestCase testCase) {
300+
try {
301+
lock.readLock().lock();
302+
return testKeys.containsKey(testCase);
303+
} finally {
304+
lock.readLock().unlock();
305+
}
306+
}
307+
289308
private AllureExternalKey removeTestKey(final TestCase testCase) {
290309
try {
291310
lock.writeLock().lock();

allure-citrus/src/test/java/io/qameta/allure/citrus/AllureCitrusTest.java

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import com.consol.citrus.Citrus;
1919
import com.consol.citrus.CitrusContext;
2020
import com.consol.citrus.TestCase;
21+
import com.consol.citrus.TestCaseMetaInfo;
2122
import com.consol.citrus.actions.AbstractTestAction;
2223
import com.consol.citrus.actions.FailAction;
2324
import com.consol.citrus.context.TestContext;
@@ -27,6 +28,7 @@
2728
import io.qameta.allure.AllureLifecycle;
2829
import io.qameta.allure.Step;
2930
import io.qameta.allure.model.Parameter;
31+
import io.qameta.allure.model.Stage;
3032
import io.qameta.allure.model.Status;
3133
import io.qameta.allure.model.StatusDetails;
3234
import io.qameta.allure.model.StepResult;
@@ -40,6 +42,7 @@
4042

4143
import java.time.Instant;
4244

45+
import static io.qameta.allure.util.ResultsUtils.md5;
4346
import static org.assertj.core.api.Assertions.assertThat;
4447
import static org.assertj.core.api.Assertions.tuple;
4548
@SuppressWarnings("unchecked")
@@ -203,6 +206,60 @@ void shouldSetParameters() {
203206
);
204207
}
205208

209+
@AllureFeatures.History
210+
@AllureFeatures.Parameters
211+
@Test
212+
void shouldCalculateIdsFromFinalNativeAndRuntimeParameters() {
213+
final DefaultTestDesigner designer = new DefaultTestDesigner();
214+
designer.name("Runtime parameters");
215+
designer.variable("native", "example");
216+
designer.action(new AbstractTestAction() {
217+
@Override
218+
public void doExecute(final TestContext context) {
219+
Allure.parameter("runtime", "value");
220+
Allure.parameter("excluded", "ignored", true);
221+
}
222+
});
223+
224+
final AllureResults results = run(designer);
225+
final TestResult testResult = results.getTestResults().get(0);
226+
assertThat(testResult.getParameters())
227+
.extracting(Parameter::getName, Parameter::getValue, Parameter::getExcluded)
228+
.containsExactlyInAnyOrder(
229+
tuple("native", "example", null),
230+
tuple("runtime", "value", null),
231+
tuple("excluded", "ignored", true)
232+
);
233+
234+
final String fullName = DefaultTestDesigner.class.getName() + ".Runtime parameters";
235+
assertThat(testResult.getTestCaseId())
236+
.isEqualTo(md5(fullName));
237+
assertThat(testResult.getHistoryId())
238+
.isEqualTo(md5(md5(fullName) + "native" + "example" + "runtime" + "value"));
239+
}
240+
241+
@AllureFeatures.SkippedTests
242+
@AllureFeatures.History
243+
@Test
244+
void shouldReportDisabledTestsWithIds() {
245+
final DefaultTestDesigner designer = new DefaultTestDesigner();
246+
designer.name("Disabled test");
247+
designer.variable("native", "value");
248+
designer.status(TestCaseMetaInfo.Status.DISABLED);
249+
250+
final AllureResults results = run(designer);
251+
final String fullName = DefaultTestDesigner.class.getName() + ".Disabled test";
252+
final String testCaseId = md5(fullName);
253+
assertThat(results.getTestResults())
254+
.singleElement()
255+
.satisfies(result -> {
256+
assertThat(result.getStatus()).isEqualTo(Status.SKIPPED);
257+
assertThat(result.getStage()).isEqualTo(Stage.FINISHED);
258+
assertThat(result.getTestCaseId()).isEqualTo(testCaseId);
259+
assertThat(result.getHistoryId()).isEqualTo(md5(testCaseId + "native" + "value"));
260+
});
261+
}
262+
206263
@Step("Run test case {testDesigner}")
207264
private AllureResults run(final TestDesigner testDesigner) {
208265
// a failing citrus test is a valid outcome under test — only fail the harness when

allure-cucumber7-jvm/src/main/java/io/qameta/allure/cucumber7jvm/AllureCucumber7Jvm.java

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -178,11 +178,7 @@ private void handleTestCaseStarted(final TestCaseStarted event) {
178178
// the same way full name is generated for
179179
// org.junit.platform.engine.support.descriptor.ClasspathResourceSource
180180
// to support io.qameta.allure.junitplatform.AllurePostDiscoveryFilter
181-
final String fullName = String.format(
182-
"%s:%d",
183-
getTestCaseUri(testCase),
184-
testCase.getLocation().getLine()
185-
);
181+
final String fullName = getTestCaseLocation(testCase);
186182

187183
final String testCaseUuid = testCase.getId().toString();
188184

@@ -192,7 +188,6 @@ private void handleTestCaseStarted(final TestCaseStarted event) {
192188
final TestResult result = new TestResult()
193189
.setUuid(testCaseUuid)
194190
.setTestCaseId(getTestCaseId(testCase))
195-
.setHistoryId(getHistoryId(testCase))
196191
.setFullName(fullName)
197192
.setTitlePath(titlePath)
198193
.setName(name)
@@ -205,9 +200,7 @@ private void handleTestCaseStarted(final TestCaseStarted event) {
205200
);
206201

207202
if (scenarioDefinition.getExamples() != null) {
208-
result.setParameters(
209-
getExamplesAsParameters(scenarioDefinition, testCase)
210-
);
203+
result.getParameters().addAll(getExamplesAsParameters(scenarioDefinition, testCase));
211204
}
212205

213206
final String description = Stream.of(feature.getDescription(), scenarioDefinition.getDescription())
@@ -364,9 +357,8 @@ private void handleEmbedEvent(final EmbedEvent event) {
364357
);
365358
}
366359

367-
private String getHistoryId(final TestCase testCase) {
368-
final String testCaseLocation = getTestCaseUri(testCase) + COLON + testCase.getLocation().getLine();
369-
return md5(testCaseLocation);
360+
private String getTestCaseLocation(final TestCase testCase) {
361+
return getTestCaseUri(testCase) + COLON + testCase.getLocation().getLine();
370362
}
371363

372364
private String getTestCaseId(final TestCase testCase) {

allure-cucumber7-jvm/src/test/java/io/qameta/allure/cucumber7jvm/AllureCucumber7JvmTest.java

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,7 @@ void shouldPersistHistoryIdForScenarios() {
539539

540540
final List<TestResult> testResults = results.getTestResults();
541541
assertThat(testResults.get(0).getHistoryId())
542-
.isEqualTo("892e5eabe51184301cf1358453c9f052");
542+
.isEqualTo(md5(md5("src/test/resources/features/simple.feature:Add a to b")));
543543
}
544544

545545
@AllureFeatures.History
@@ -550,7 +550,16 @@ void shouldPersistHistoryIdForExamples() {
550550
final List<TestResult> testResults = results.getTestResults();
551551
assertThat(testResults)
552552
.extracting(TestResult::getHistoryId)
553-
.containsExactlyInAnyOrder("c0f824814a130048e9f86358363cf23e", "646aca5d0775cd4f13161e1ea1a68c39");
553+
.containsExactlyInAnyOrder(
554+
md5(
555+
md5("src/test/resources/features/examples.feature:Scenario with Positive Examples")
556+
+ "a1b3result4"
557+
),
558+
md5(
559+
md5("src/test/resources/features/examples.feature:Scenario with Positive Examples")
560+
+ "a2b4result6"
561+
)
562+
);
554563
}
555564

556565
@AllureFeatures.History
@@ -782,6 +791,35 @@ void shouldSupportRuntimeApiInStepsWhenHooksAreUsed() {
782791
);
783792
}
784793

794+
@AllureFeatures.History
795+
@AllureFeatures.Parameters
796+
@Test
797+
void shouldCalculateIdsFromRuntimeParametersAtTestEnd() {
798+
final AllureResults results = runFeature("features/runtimeapi.feature");
799+
800+
final TestResult testResult = results.getTestResults().get(0);
801+
assertThat(testResult.getParameters())
802+
.extracting(Parameter::getName, Parameter::getValue, Parameter::getExcluded)
803+
.containsExactlyInAnyOrder(
804+
tuple("runtime", "value", null),
805+
tuple("excluded", "ignored", true)
806+
);
807+
assertThat(testResult.getTestCaseId())
808+
.isEqualTo(
809+
md5(
810+
"src/test/resources/features/runtimeapi.feature:Scenario with Runtime API usage"
811+
)
812+
);
813+
assertThat(testResult.getHistoryId())
814+
.isEqualTo(
815+
md5(
816+
md5(
817+
"src/test/resources/features/runtimeapi.feature:Scenario with Runtime API usage"
818+
) + "runtimevalue"
819+
)
820+
);
821+
}
822+
785823
@SystemProperty(
786824
name = "cucumber.junit-platform.naming-strategy",
787825
value = "long"

allure-cucumber7-jvm/src/test/java/io/qameta/allure/cucumber7jvm/samples/RuntimeApiSteps.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ public void beforeFeature() {
3434

3535
@When("^step 1$")
3636
public void step1() {
37+
Allure.parameter("runtime", "value");
38+
Allure.parameter("excluded", "ignored", true);
3739
Allure.step("step1 nested");
3840
Allure.link("step1", "https://example.org/step1");
3941
Allure.getLifecycle().getCurrentRootKey().ifPresent(key -> {

allure-java-commons-test/src/main/java/io/qameta/allure/test/AllureTestCommonsUtils.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,15 @@
3232
import java.io.ByteArrayInputStream;
3333
import java.io.IOException;
3434
import java.io.UncheckedIOException;
35+
import java.util.Comparator;
36+
import java.util.List;
3537
import java.util.Locale;
38+
import java.util.Objects;
39+
import java.util.stream.Stream;
3640

3741
import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_DEFAULT;
3842
import static com.fasterxml.jackson.databind.MapperFeature.USE_WRAPPER_NAME_AS_PROPERTY_NAME;
43+
import static io.qameta.allure.util.ResultsUtils.md5;
3944

4045
/**
4146
* Provides utility methods for Allure Java test support support.
@@ -105,6 +110,31 @@ public static void attach(final AllureResults allureResults) {
105110
);
106111
}
107112

113+
/**
114+
* Calculates the compatibility history id expected in adapter tests.
115+
*
116+
* @param testCaseId the test case id
117+
* @param parameters the final parameters
118+
* @return the expected history id
119+
*/
120+
public static String expectedHistoryId(final String testCaseId, final List<Parameter> parameters) {
121+
final StringBuilder source = new StringBuilder(testCaseId);
122+
final Stream<Parameter> parameterStream = Objects.isNull(parameters) ? Stream.empty() : parameters.stream();
123+
parameterStream
124+
.filter(Objects::nonNull)
125+
.filter(parameter -> !Boolean.TRUE.equals(parameter.getExcluded()))
126+
.sorted(
127+
Comparator.comparing((Parameter parameter) -> Objects.toString(parameter.getName(), ""))
128+
.thenComparing(parameter -> Objects.toString(parameter.getValue(), ""))
129+
)
130+
.forEachOrdered(
131+
parameter -> source
132+
.append(Objects.toString(parameter.getName(), ""))
133+
.append(Objects.toString(parameter.getValue(), ""))
134+
);
135+
return md5(source.toString());
136+
}
137+
108138
private static AttachmentOptions attachmentOptions(final String fileName) {
109139
if (fileName.endsWith(DOT + JSON_EXTENSION) || fileName.endsWith(DOT + TEXT_EXTENSION)) {
110140
return AttachmentOptions.empty();

allure-java-commons/src/main/java/io/qameta/allure/AllureLifecycle.java

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import io.qameta.allure.listener.TestLifecycleListener;
2525
import io.qameta.allure.model.Attachment;
2626
import io.qameta.allure.model.FixtureResult;
27+
import io.qameta.allure.model.Parameter;
2728
import io.qameta.allure.model.ScopeFixtureResult;
2829
import io.qameta.allure.model.ScopeFixtureType;
2930
import io.qameta.allure.model.ScopeResult;
@@ -45,6 +46,7 @@
4546
import java.nio.file.Paths;
4647
import java.util.ArrayList;
4748
import java.util.Collection;
49+
import java.util.Comparator;
4850
import java.util.LinkedHashSet;
4951
import java.util.List;
5052
import java.util.Map;
@@ -59,11 +61,13 @@
5961
import java.util.concurrent.atomic.AtomicBoolean;
6062
import java.util.function.BiConsumer;
6163
import java.util.function.Consumer;
64+
import java.util.stream.Stream;
6265

6366
import static io.qameta.allure.AllureConstants.ATTACHMENT_FILE_SUFFIX;
6467
import static io.qameta.allure.util.ResultsUtils.firstNonEmpty;
6568
import static io.qameta.allure.util.ResultsUtils.getStatus;
6669
import static io.qameta.allure.util.ResultsUtils.getStatusDetails;
70+
import static io.qameta.allure.util.ResultsUtils.md5;
6771
import static io.qameta.allure.util.ServiceLoaderUtils.load;
6872

6973
/**
@@ -325,8 +329,10 @@ public void updateTest(final Consumer<TestResult> update) {
325329
}
326330

327331
/**
328-
* Stops test by given key. The test must be running; scope metadata is merged into the test here. Unbinds the
329-
* calling thread only if the test is the calling thread's root.
332+
* Stops test by given key. The test must be running; scope metadata is merged into the test here. If the test has
333+
* a test case id but no history id, a compatibility history id is generated from the test case id and the final
334+
* parameters. A history id supplied by a {@link TestLifecycleListener#beforeTestStop(TestResult)} listener is
335+
* preserved. Unbinds the calling thread only if the test is the calling thread's root.
330336
*
331337
* @param key the external test key
332338
*/
@@ -347,13 +353,37 @@ public void stopTest(final AllureExternalKey key) {
347353
testResult
348354
.setStage(Stage.FINISHED)
349355
.setStop(System.currentTimeMillis());
356+
if (Objects.isNull(testResult.getParameters())) {
357+
testResult.setParameters(new ArrayList<>());
358+
}
350359
applyScopeMetadata(item);
360+
if (Objects.isNull(testResult.getHistoryId()) && Objects.nonNull(testResult.getTestCaseId())) {
361+
testResult.setHistoryId(calculateHistoryId(testResult.getTestCaseId(), testResult.getParameters()));
362+
}
351363
if (isCurrentRoot(key)) {
352364
threadContext.clear();
353365
}
354366
notifier.afterTestStop(testResult);
355367
}
356368

369+
private static String calculateHistoryId(final String testCaseId, final List<Parameter> parameters) {
370+
final StringBuilder source = new StringBuilder(testCaseId);
371+
final Stream<Parameter> parameterStream = Objects.isNull(parameters) ? Stream.empty() : parameters.stream();
372+
parameterStream
373+
.filter(Objects::nonNull)
374+
.filter(parameter -> !Boolean.TRUE.equals(parameter.getExcluded()))
375+
.sorted(
376+
Comparator.comparing((Parameter parameter) -> Objects.toString(parameter.getName(), ""))
377+
.thenComparing(parameter -> Objects.toString(parameter.getValue(), ""))
378+
)
379+
.forEachOrdered(
380+
parameter -> source
381+
.append(Objects.toString(parameter.getName(), ""))
382+
.append(Objects.toString(parameter.getValue(), ""))
383+
);
384+
return md5(source.toString());
385+
}
386+
357387
/**
358388
* Writes test by given key. Waits for the test's pending async attachments before serializing, so the written
359389
* result file is a completion marker: everything it references exists.

0 commit comments

Comments
 (0)