Skip to content

Commit

Permalink
add reducing method MERGE_FEATURES_WITH_RETEST_MARKING_FLACKY
Browse files Browse the repository at this point in the history
  • Loading branch information
aqaexplorer authored and VitaliyaRyabchuk committed Jan 21, 2021
1 parent e2bac56 commit 953274f
Show file tree
Hide file tree
Showing 12 changed files with 559 additions and 26 deletions.
28 changes: 22 additions & 6 deletions src/main/java/net/masterthought/cucumber/json/Element.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package net.masterthought.cucumber.json;

import java.time.LocalDateTime;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import net.masterthought.cucumber.Configuration;
Expand All @@ -12,6 +10,8 @@
import net.masterthought.cucumber.util.Util;
import org.apache.commons.lang.StringUtils;

import java.time.LocalDateTime;

public class Element implements Durationable {

// Start: attributes from JSON file report
Expand All @@ -31,11 +31,11 @@ public class Element implements Durationable {
*/
@JsonProperty("start_timestamp")
private final LocalDateTime startTime = null;
private final Step[] steps = new Step[0];
private final Hook[] before = new Hook[0];
private final Hook[] after = new Hook[0];
private Step[] steps = new Step[0];
private Hook[] before = new Hook[0];
private Hook[] after = new Hook[0];
@JsonDeserialize(using = TagsDeserializer.class)
private final Tag[] tags = new Tag[0];
private Tag[] tags = new Tag[0];
// End: attributes from JSON file report

private static final String SCENARIO_TYPE = "scenario";
Expand Down Expand Up @@ -65,6 +65,10 @@ public Tag[] getTags() {
return tags;
}

public void setTags(Tag[] tags) {
this.tags = tags;
}

public Status getStatus() {
return elementStatus;
}
Expand Down Expand Up @@ -131,6 +135,18 @@ public String getFormattedDuration() {
return Util.formatDuration(duration);
}

public boolean isStatus(Status status) {
for (Step step : steps) {
step.setMetaData();
}

beforeStatus = new StatusCounter(before).getFinalStatus();
afterStatus = new StatusCounter(after).getFinalStatus();
stepsStatus = new StatusCounter(steps).getFinalStatus();
elementStatus = calculateElementStatus();
return elementStatus == status;
}

public void setMetaData(Feature feature, Configuration configuration) {
this.feature = feature;

Expand Down
21 changes: 16 additions & 5 deletions src/main/java/net/masterthought/cucumber/json/Hook.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import org.apache.commons.lang.StringUtils;

import net.masterthought.cucumber.json.deserializers.OutputsDeserializer;
import net.masterthought.cucumber.json.support.Embedded;
import net.masterthought.cucumber.json.support.Resultsable;
import org.apache.commons.lang.StringUtils;

public class Hook implements Resultsable {
public class Hook implements Resultsable, Embedded {

// Start: attributes from JSON file report
private final Result result = null;
Expand All @@ -18,7 +18,7 @@ public class Hook implements Resultsable {
private final Output[] outputs = new Output[0];

// foe Ruby reports
private final Embedding[] embeddings = new Embedding[0];
private Embedding[] embeddings = new Embedding[0];
// End: attributes from JSON file report

@Override
Expand All @@ -36,17 +36,23 @@ public Output[] getOutputs() {
return outputs;
}

@Override
public Embedding[] getEmbeddings() {
return embeddings;
}

@Override
public void setEmbeddings(Embedding[] embeddings) {
this.embeddings = embeddings;
}

/**
* Checks if the hook has content meaning as it has at least attachment or result with error message.
*
* @return <code>true</code> if the hook has content otherwise <code>false</code>
*/
public boolean hasContent() {
if (embeddings.length > 0) {
if (hasEmbeddings()) {
// assuming that if the embedding exists then it is not empty
return true;
}
Expand All @@ -56,4 +62,9 @@ public boolean hasContent() {
// TODO: hook with 'output' should be treated as empty or not?
return false;
}

@Override
public boolean hasEmbeddings() {
return embeddings.length > 0;
}
}
6 changes: 5 additions & 1 deletion src/main/java/net/masterthought/cucumber/json/Result.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public class Result implements Durationable {
// for all cases where Result is not present or completed
private final Status status = Status.UNDEFINED;
@JsonProperty("error_message")
private final String errorMessage = null;
private String errorMessage = null;
private final Long duration = 0L;
// End: attributes from JSON file report

Expand All @@ -36,6 +36,10 @@ public String getErrorMessage() {
return errorMessage;
}

public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}

public final String getErrorMessageTitle() {
if (errorMessage != null) {
String[] title = errorMessage.split("[\\p{Space}]+");
Expand Down
26 changes: 19 additions & 7 deletions src/main/java/net/masterthought/cucumber/json/Step.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import org.apache.commons.lang3.ArrayUtils;

import net.masterthought.cucumber.json.deserializers.OutputsDeserializer;
import net.masterthought.cucumber.json.support.Argument;
import net.masterthought.cucumber.json.support.Embedded;
import net.masterthought.cucumber.json.support.Resultsable;
import net.masterthought.cucumber.json.support.Status;
import net.masterthought.cucumber.json.support.StatusCounter;
import org.apache.commons.lang3.ArrayUtils;

public class Step implements Resultsable {
public class Step implements Resultsable, Embedded {

// Start: attributes from JSON file report
private String name = null;
Expand All @@ -23,8 +23,8 @@ public class Step implements Resultsable {
// protractor-cucumber-framework - mapping arguments to rows
@JsonProperty("arguments")
private final Argument[] arguments = new Argument[0];
private final Match match = null;
private final Embedding[] embeddings = new Embedding[0];
private Match match = null;
private Embedding[] embeddings = new Embedding[0];

@JsonDeserialize(using = OutputsDeserializer.class)
@JsonProperty("output")
Expand All @@ -34,8 +34,8 @@ public class Step implements Resultsable {
private final DocString docString = null;

// hooks are supported since Cucumber-JVM 3.x.x
private final Hook[] before = new Hook[0];
private final Hook[] after = new Hook[0];
private Hook[] before = new Hook[0];
private Hook[] after = new Hook[0];
// End: attributes from JSON file report

private Status beforeStatus;
Expand Down Expand Up @@ -76,10 +76,16 @@ public Match getMatch() {
return match;
}

@Override
public Embedding[] getEmbeddings() {
return embeddings;
}

@Override
public void setEmbeddings(Embedding[] embeddings) {
this.embeddings = embeddings;
}

@Override
public Result getResult() {
return result;
Expand Down Expand Up @@ -113,4 +119,10 @@ public void setMetaData() {
beforeStatus = new StatusCounter(before).getFinalStatus();
afterStatus = new StatusCounter(after).getFinalStatus();
}

@Override
public boolean hasEmbeddings() {
return embeddings.length > 0;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package net.masterthought.cucumber.json.support;

import net.masterthought.cucumber.json.Embedding;

/**
* Ensures that class delivers method for embeddings.
*
* @author Vitaliya Ryabchuk (aqaexplorer@github)
*/
public interface Embedded {

Embedding[] getEmbeddings();

void setEmbeddings(Embedding[] embeddings);

boolean hasEmbeddings();

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

/**
* Defines all possible statuses provided by cucumber library.
*
*
* @author Damian Szczepanik (damianszczepanik@github)
*/
@JsonDeserialize(using = StatusDeserializer.class)
Expand Down Expand Up @@ -43,4 +43,5 @@ public String getLabel() {
public boolean isPassed() {
return this == PASSED;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,16 @@ public enum ReducingMethod {
*/
MERGE_FEATURES_WITH_RETEST,

/**
* Works as MERGE_FEATURES_WITH_RETEST plus marking as FLACKY tests that after rerun were passed
* Before merging it checks that scenario was failed and now it's passed and mark it as flacky (add @flacky tag).
* Also it gets an exception from firstly failed scenario and sets it to passed scenario in the same step/hook so
* you can analyze the reason but not to fail test suite.
* Plus it will also move any attachments to passed scenario in the same step/hook from firstly failed scenario AFTER HOOK
* (The common cucumber solution to embed screenshot in after hook if scenario failed)
*/
MERGE_FEATURES_WITH_RETEST_MARKING_FLACKY,

/**
* Skip empty JSON reports. If this flag is not selected then report generation fails on empty file.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ public final class ReportFeatureMergerFactory {

private List<ReportFeatureMerger> mergers = Arrays.asList(
new ReportFeatureByIdMerger(),
new ReportFeatureWithRetestMerger()
new ReportFeatureWithRetestMerger(),
new ReportFeatureWithRetestMarkingFlackyMerger()
);

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package net.masterthought.cucumber.reducers;

import static java.util.Arrays.stream;
import static java.util.stream.Stream.concat;
import static net.masterthought.cucumber.json.support.Status.FAILED;
import static net.masterthought.cucumber.json.support.Status.PASSED;
import static org.apache.commons.lang3.ArrayUtils.add;
import static org.apache.commons.lang3.ArrayUtils.addAll;

import net.masterthought.cucumber.json.Element;
import net.masterthought.cucumber.json.Feature;
import net.masterthought.cucumber.json.Hook;
import net.masterthought.cucumber.json.Tag;
import net.masterthought.cucumber.json.support.Resultsable;

import java.util.List;
import java.util.NoSuchElementException;
import java.util.stream.Stream;

/**
* Merge list of given features in the same way as ReportFeatureWithRetestMerger plus marking flacky tests
* (that were failed and after rerun are passed) not failing the last one and moves exception and attachments from
* previously failed scenario for better analyzing
* <p>
* Uses when need to generate a report with rerun results analyzing flacky tests.
*/
final class ReportFeatureWithRetestMarkingFlackyMerger extends ReportFeatureWithRetestMerger {

@Override
public boolean test(List<ReducingMethod> reducingMethods) {
return reducingMethods != null
&& reducingMethods.contains(ReducingMethod.MERGE_FEATURES_WITH_RETEST_MARKING_FLACKY);
}

@Override
protected void replace(Feature feature, Element[] elements, int i, Element current, int indexOfPreviousResult,
boolean hasBackground) {
if (ifFlacky(feature.getElements()[indexOfPreviousResult], current)) {
addFlackyTag(current);
addExceptionWithEmbeddings(feature.getElements()[indexOfPreviousResult], current);
}
super.replace(feature, elements, i, current, indexOfPreviousResult, hasBackground);

}

boolean ifFlacky(Element target, Element candidate) {
return target.isStatus(FAILED) && candidate.isStatus(PASSED);
}

void addFlackyTag(Element candidate) {
Tag[] tags = add(candidate.getTags(), new Tag("@flacky"));
candidate.setTags(tags);
}

void addExceptionWithEmbeddings(Element target, Element candidate) {
Resultsable targetFailed = getFailedResultsable(mergeResultsable(target));
setErrorMessage(targetFailed, mergeResultsable(candidate));
setEmbeddingsInAfterHookIfPresent(target, candidate, targetFailed);
}

void setErrorMessage(Resultsable targetFailed, Stream<Resultsable> candidateResultsableStream) {
candidateResultsableStream.filter(resultsable -> resultsable.getMatch().getLocation()
.equals(targetFailed.getMatch().getLocation()))
.findFirst().ifPresent(step
-> step.getResult().setErrorMessage(targetFailed.getResult().getErrorMessage()));
}

Resultsable getFailedResultsable(Stream<Resultsable> resultsableStream) {
return resultsableStream.filter(resultsable -> resultsable.getResult().getStatus() == FAILED)
.findFirst()
.orElseThrow(() -> new NoSuchElementException("No hook or step with failed status was found"));
}

Stream<Resultsable> mergeResultsable(Element element) {
Stream<Hook> candidateHooks = concat(stream(element.getBefore()), stream(element.getAfter()));
return concat(candidateHooks, getStepsWithStepHooks(element));
}

Stream<Resultsable> getStepsWithStepHooks(Element previous) {
Stream<Resultsable> stream = stream(previous.getSteps());

for (int i = 0; i <= previous.getSteps().length - 1; i++) {
Resultsable[] before = previous.getSteps()[i].getBefore();
Resultsable[] after = previous.getSteps()[i].getAfter();
Stream<Resultsable> hooks = concat(stream(before), stream(after));
stream = Stream.concat(stream, hooks);
}
return stream;
}

void setEmbeddingsInAfterHookIfPresent(Element previous, Element current, Resultsable previousFailed) {
stream(previous.getAfter())
.filter(Hook::hasEmbeddings)
.findFirst().ifPresent(previousEmbeddedAfter ->
stream(current.getSteps())
.filter(hook -> hook.getMatch().getLocation().equals(previousFailed.getMatch().getLocation()))
.findFirst().ifPresent(hook
-> hook.setEmbeddings(addAll(hook.getEmbeddings(), previousEmbeddedAfter.getEmbeddings()))));
}

}
Loading

0 comments on commit 953274f

Please sign in to comment.