Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add reducing method MERGE_FEATURES_WITH_RETEST_MARKING_FLACKY #986

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

method which start from isXXX should only perform fast checking and here I see some logic

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 {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it looks more like a class than interface


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;
}

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nothing has changed here, revert the file

}
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,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copyErrorMessage - what do you think?

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"));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when this exception may be thrown?

are you catching such exception anywhere?

}

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sometimes with static import ...

}

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));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it proper to concat before and after ?

stream = Stream.concat(stream, hooks);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... sometimes not

}
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