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

Fixed test step result sync, made it independent from updating test issues #17

Merged
merged 11 commits into from
Nov 2, 2022
52 changes: 30 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,11 @@ A list of other mapping implementations.

#### DefaultSummaryMapper

This maps Java test methods to Jira *Tests* and Java classes to Jira *Test Sets* by their name, when no keys are present in the annotations. Additionally, it creates the issues when they don't exist. You enable that feature by passing that mapper in your `XrayResultsSynchronizer`.
This maps Java test methods to Jira *Tests* and Java classes to Jira *Test Sets* by their name, when no keys are present in the annotations.

Please note, that this mapper creates the issues when they don't exist! See above for more details how it's work.

You enable that feature by passing that mapper in your `XrayResultsSynchronizer`.

```java
public class MyXrayResultsSynchronizer extends AbstractXrayResultsSynchronizer {
Expand Down Expand Up @@ -227,60 +231,64 @@ Please note, that
- `queryTest` is also called if you use `@XrayTest` annotation, but without key attribute
- `queryTestSet` is also called if you `@XrayTestSet` annotation, but without key attribute

#### Update entities
#### Creating new entities

By default, the Xray connector doesn't create any issues. You can enable that by passing `true` in the interface.

The `XrayMapper` also provides callbacks for updating entities.
Please note, that existing issues will be updated automatically. All manual changes like test steps will be overwritten.

```java
public class GenericMapper implements XrayMapper {

@Override
public void updateTestExecution(XrayTestExecutionIssue xrayTestExecutionIssue, ExecutionContext executionContext) {
xrayTestExecutionIssue.getTestEnvironments().add("Test");
xrayTestExecutionIssue.setFixVersions(List.of(new JiraNameReference("1.0")));
public boolean shouldCreateNewTestSet(ClassContext classContext) {
return true;
}

@Override
public void updateTestSet(XrayTestSetIssue xrayTestSetIssue, ClassContext classContext) {
xrayTestSetIssue.getLabels().add("TestAutomation");
public boolean shouldCreateNewTest(MethodContext methodContext) {
return true;
}

@Override
public void updateTest(XrayTestIssue xrayTestIssue, MethodContext methodContext) {
xrayTestIssue.getLabels().add("TestAutomation");
public String getDefaultTestIssueSummery(MethodContext methodContext) {
return String.format("%s_%s", methodContext.getClassContext().getName(), methodContext.getName());
}
}
```

You can use these methods to update the Jira issues right before importing. Please mind, that not all features are supported by the [Xray import API](#references).
If you create new test issues, Xray connector will use the method `getDefaultTestIssueSummery` for generate new issue summary.
martingrossmann marked this conversation as resolved.
Show resolved Hide resolved

#### Creating new entities
In the example above new created test issues get the summery according the format `<TestClass_TestMethod>` like `MyTestClass_testSomething`.
martingrossmann marked this conversation as resolved.
Show resolved Hide resolved

By default, the Xray connector doesn't create any issues. You can enable that by passing `true` in the interface.
#### Updating existing entities

The `XrayMapper` also provides callbacks for updating entities.

To update Xray testsets and test issues you have to allow to create new issues (see [Creating new entities](#creating-new-entities)).
martingrossmann marked this conversation as resolved.
Show resolved Hide resolved

```java
public class GenericMapper implements XrayMapper {

@Override
public boolean shouldCreateNewTestSet(ClassContext classContext) {
return true;
public void updateTestExecution(XrayTestExecutionIssue xrayTestExecutionIssue, ExecutionContext executionContext) {
xrayTestExecutionIssue.getTestEnvironments().add("Test");
xrayTestExecutionIssue.setFixVersions(List.of(new JiraNameReference("1.0")));
}

@Override
public boolean shouldCreateNewTest(MethodContext methodContext) {
return true;
public void updateTestSet(XrayTestSetIssue xrayTestSetIssue, ClassContext classContext) {
xrayTestSetIssue.getLabels().add("TestAutomation");
}

@Override
public String getDefaultTestIssueSummery(MethodContext methodContext) {
return String.format("%s_%s", methodContext.getClassContext().getName(), methodContext.getName());
public void updateTest(XrayTestIssue xrayTestIssue, MethodContext methodContext) {
xrayTestIssue.getLabels().add("TestAutomation");
}
}
```

If you create new test issues, Xray connector will use the method `getDefaultTestIssueSummery` for generate new issue summary.

In the example above new created test issues get the summery according the format `<TestClass_TestMethod>` like `MyTestClass_testSomething`.
You can use these methods to update the Jira issues right before importing. Please mind, that not all features are supported by the [Xray import API](#references).

#### How to use JqlQuery

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package eu.tsystems.mms.tic.testerra.plugins.xray.mapper.jira;

import java.util.Arrays;
import java.util.List;

/**
* Created on 2022-10-11
*
* @author mgn
*/
public class JiraError {

martingrossmann marked this conversation as resolved.
Show resolved Hide resolved
private String[] messages;

private String summary;

private String project;

public List<String> getMessages() {
return Arrays.asList(messages);
}

public String getSummary() {
return summary;
}

public String getProject() {
return project;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

import com.fasterxml.jackson.annotation.JsonFormat;
import eu.tsystems.mms.tic.testerra.plugins.xray.jql.predefined.TestType;
import eu.tsystems.mms.tic.testerra.plugins.xray.mapper.jira.JiraError;
import eu.tsystems.mms.tic.testerra.plugins.xray.mapper.jira.JiraIssue;
import eu.tsystems.mms.tic.testerra.plugins.xray.mapper.jira.JiraKeyReference;
import eu.tsystems.mms.tic.testerra.plugins.xray.mapper.jira.JiraNameReference;
Expand Down Expand Up @@ -91,14 +92,20 @@ public ResultTestIssueImport getTestIssues() {

public static class ResultTestIssueImport {

private JiraKeyReference[] success;
private JiraKeyReference[] success = {};

private JiraError[] error = {};

public ResultTestIssueImport() {
}

public List<JiraKeyReference> getSuccess() {
return Arrays.asList(success);
}

public List<JiraError> getError() {
return Arrays.asList(this.error);
}
}

public static class Info extends AbstractInfo {
Expand Down Expand Up @@ -324,19 +331,8 @@ public void addEvidence(Evidence evidence) {
private Status status;
private List<Step> steps;

public TestRun() {
}

public TestRun(JiraIssue issue) {
this(issue.getKey());
this.testInfo = new Info();
this.testInfo.setDescription(issue.getDescription());
this.testInfo.setSummary(issue.getSummary());
this.testInfo.setLabels(issue.getLabels());
this.testInfo.setDefinition(issue.getSummary());
this.testInfo.setType(TestType.AutomatedGeneric);
this.testInfo.setProjectKey(issue.getProject().getKey());
}
// public TestRun() {
Zsar marked this conversation as resolved.
Show resolved Hide resolved
// }

public TestRun(String testKey) {
this.testKey = testKey;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,22 +221,29 @@ private synchronized void flushSyncQueue() {
boolean areNewTestsToImport = testSetSyncQueue.stream().anyMatch(xrayTestSetIssue -> {
return xrayTestSetIssue.getTestKeys().stream().anyMatch(key -> key.contains(XrayUtils.PREFIX_NEW_ISSUE));
});
if (areNewTestsToImport) {
xrayTestExecutionImport.getResultTestIssueImport().getSuccess().forEach(jiraIssueReference -> {
try {
// Replace the temporary key with real Jira key from the result 'xrayTestExecutionImport'
JiraIssue issue = xrayUtils.getIssue(jiraIssueReference.getKey());
testSetSyncQueue.forEach(xrayTestSetIssue -> {
Optional<String> findKey = xrayTestSetIssue.getTestKeys().stream().filter(key -> key.contains(issue.getSummary())).findFirst();
if (findKey.isPresent()) {
xrayTestSetIssue.getTestKeys().removeIf(key -> key.contains(issue.getSummary()));
xrayTestSetIssue.getTestKeys().add(issue.getKey());
}
});
} catch (IOException e) {
log().error(String.format("Unable to read %s", IssueType.TestExecution), e);
}
});
if (areNewTestsToImport && xrayTestExecutionImport.getResultTestIssueImport() != null) {
Zsar marked this conversation as resolved.
Show resolved Hide resolved
if (xrayTestExecutionImport.getResultTestIssueImport().getError().size() > 0) {
log().error("Error at syncing with Jira");
xrayTestExecutionImport.getResultTestIssueImport().getError()
.forEach(error -> log().error("{} - {}: {}", error.getProject(), error.getSummary(), error.getMessages().toString()));
Copy link

Choose a reason for hiding this comment

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

It would be better to concatenate these into one log statement together with the very plain "Error at syncing with Jira", such that said statement cannot be separated e.g. if several loggers write into the same file. As-is the individual statements may be scattered arbitrarily far in a sufficiently degenerate logger setup.

return;
} else if (xrayTestExecutionImport.getResultTestIssueImport().getSuccess().size() > 0) {
xrayTestExecutionImport.getResultTestIssueImport().getSuccess().forEach(jiraIssueReference -> {
try {
// Replace the temporary key with real Jira key from the result 'xrayTestExecutionImport'
JiraIssue issue = xrayUtils.getIssue(jiraIssueReference.getKey());
testSetSyncQueue.forEach(xrayTestSetIssue -> {
Optional<String> findKey = xrayTestSetIssue.getTestKeys().stream().filter(key -> key.contains(issue.getSummary())).findFirst();
if (findKey.isPresent()) {
Zsar marked this conversation as resolved.
Show resolved Hide resolved
xrayTestSetIssue.getTestKeys().removeIf(key -> key.contains(issue.getSummary()));
Zsar marked this conversation as resolved.
Show resolved Hide resolved
xrayTestSetIssue.getTestKeys().add(issue.getKey());
}
});
} catch (IOException e) {
log().error(String.format("Unable to read %s", IssueType.TestExecution), e);
}
});
}
}

// After fixing temporary key of test issues, the test set can create or update
Expand Down Expand Up @@ -296,7 +303,7 @@ public void onTestStatusUpdate(TestStatusUpdateEvent event) {
// Get the method's Test issues by annotation
final Set<XrayTestIssue> currentTestIssues = getTestIssuesForMethod(realMethod);

// If a method annotation or class annotation was found
// If no method annotation, but a class annotation was found
if (currentTestIssues.isEmpty() || optionalXrayTestSetIssue.isPresent()) {
final String cacheKey = testResult.getMethod().getQualifiedName();

Expand Down Expand Up @@ -352,16 +359,62 @@ public void onTestStatusUpdate(TestStatusUpdateEvent event) {
*/
currentTestIssues.stream()
.peek(issue -> xrayMapper.updateTest(issue, methodContext))
.map(XrayTestExecutionImport.TestRun::new)
.peek(test -> updateTestImport(test, methodContext))
.map(issue -> {
XrayTestExecutionImport.TestRun run = new XrayTestExecutionImport.TestRun(issue.getKey());
this.updateTestInfoForImport(run, issue, methodContext);
Copy link

Choose a reason for hiding this comment

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

This could have been another peek, for laconeia.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hm... the content of map() is to complex.

Copy link

Choose a reason for hiding this comment

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

Should work by constructing new XrayTestExecutionImport.TestRun(issue.getKey() inside updateTestInfoForImport, thereby eliminating its first parameter. The method is private.

[...]

	// Update test by mapper, convert to import entity and add to sync queue
	currentTestIssues.stream()
			.peek(issue -> xrayMapper.updateTest(issue, methodContext))
			.map(issue -> this.updateTestInfoForImport(issue, methodContext))
			.peek(testRun -> updateTestRunForImport(testRun, methodContext))
			.forEach(testRunSyncQueue::add);

	if (testRunSyncQueue.size() >= xrayConfig.getSyncFrequencyTests()) {
		flushSyncQueue();
	}
}

/**
 * Update the Info object for creating or updating Xray tests.
 * If no Info object is defined, the Xray test will not be updated.
 */
private XrayTestExecutionImport.TestRun updateTestInfoForImport(XrayTestIssue issue, MethodContext methodContext) {
	if (this.getXrayMapper().shouldCreateNewTest(methodContext)) {
		XrayTestExecutionImport.TestRun.Info info = new XrayTestExecutionImport.TestRun.Info();
		info.setDescription(issue.getDescription());
		info.setSummary(issue.getSummary());
		info.setLabels(issue.getLabels());
		info.setDefinition(issue.getSummary());
		info.setType(TestType.AutomatedGeneric);
		info.setProjectKey(issue.getProject().getKey());
		
		// The test's test type needs to be {@link TestType.Manual} to support test steps.
		info.setType(TestType.Manual);

		List<TestStep> testerraTestSteps = methodContext.readTestSteps().collect(Collectors.toList());
		for (TestStep testerraTestStep : testerraTestSteps) {
			if (testerraTestStep.isInternalTestStep()) {
				continue;
			}
			// The test steps definitions
			final XrayTestExecutionImport.TestStep importTestStep = new XrayTestExecutionImport.TestStep();
			importTestStep.setAction(testerraTestStep.getName());
			// We always expect the step to pass
			importTestStep.setResult(XrayTestExecutionImport.TestRun.Status.PASS.toString());
			info.addStep(importTestStep);
		}

		final var testRun = new XrayTestExecutionImport.TestRun(issue.getKey());
		testRun.setTestInfo(info);
		return testRun;
	}
}

... FWIW: This is not really function-al code to begin with. It might instead be better to not-Stream this and for-each it instead, to avoid the red herring:

// Update test by mapper, convert to import entity and add to sync queue
for (final var issue : currentTestIssues) {
	xrayMapper.updateTest(issue, methodContext);
	final var run = new XrayTestExecutionImport.TestRun(issue.getKey());
	this.updateTestInfoForImport(run, issue, methodContext);
	this.updateTestRunForImport(run, methodContext);
	testRunSyncQueue.add(run);
}

(without changes in the update[...] functions)

... But feel free to reject any of these ideas - it's not particularly important and I can also live with this part as-is.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point. I moved the creation of a new TestRun into the method.

return run;
})
.peek(testRun -> updateTestRunForImport(testRun, methodContext))
.forEach(testRunSyncQueue::add);

if (testRunSyncQueue.size() >= xrayConfig.getSyncFrequencyTests()) {
flushSyncQueue();
}
}

private void updateTestImport(XrayTestExecutionImport.TestRun testRun, MethodContext methodContext) {
/**
* Update the Info object for creating or updating Xray tests.
* If no Info object is defined, the Xray test will not update.
*/
private void updateTestInfoForImport(XrayTestExecutionImport.TestRun testRun, XrayTestIssue issue, MethodContext methodContext) {
if (this.getXrayMapper().shouldCreateNewTest(methodContext)) {

XrayTestExecutionImport.TestRun.Info info = new XrayTestExecutionImport.TestRun.Info();
info.setDescription(issue.getDescription());
info.setSummary(issue.getSummary());
info.setLabels(issue.getLabels());
info.setDefinition(issue.getSummary());
info.setType(TestType.AutomatedGeneric);
info.setProjectKey(issue.getProject().getKey());

/*
* The test's test type needs to be {@link TestType.Manual} to support test steps.
Copy link

Choose a reason for hiding this comment

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

Simple line comment instead of a three-line block containing one line of content?

*/
info.setType(TestType.Manual);

List<TestStep> testerraTestSteps = methodContext.readTestSteps().collect(Collectors.toList());
for (TestStep testerraTestStep : testerraTestSteps) {
if (testerraTestStep.isInternalTestStep()) {
continue;
}
/*
* The test steps definitions
*/
final XrayTestExecutionImport.TestStep importTestStep = new XrayTestExecutionImport.TestStep();
importTestStep.setAction(testerraTestStep.getName());
// We always expect the step to pass
importTestStep.setResult(XrayTestExecutionImport.TestRun.Status.PASS.toString());
testRun.getTestInfo().addStep(importTestStep);
}

testRun.setTestInfo(info);
}
}

/**
* Update the TestRun object with result information include test steps
*/
private void updateTestRunForImport(XrayTestExecutionImport.TestRun testRun, MethodContext methodContext) {
ITestResult testResult = methodContext.getTestNgResult().get();

testRun.setStart(new Date(testResult.getStartMillis()));
Expand All @@ -385,15 +438,11 @@ private void updateTestImport(XrayTestExecutionImport.TestRun testRun, MethodCon
testRun.setFinish(new Date(testResult.getEndMillis()));

/**
* The test's test type needs to be {@link TestType.Manual} to support test steps.
* Add the results for test steps.
* The steps will be sync with the order of appearance
*/
testRun.getTestInfo().setType(TestType.Manual);

final int lastFailedTestStepIndex = methodContext.getLastFailedTestStepIndex();

List<TestStep> testerraTestSteps = methodContext.readTestSteps()
.collect(Collectors.toList());

List<TestStep> testerraTestSteps = methodContext.readTestSteps().collect(Collectors.toList());
int stepIndex = -1;
for (TestStep testerraTestStep : testerraTestSteps) {
++stepIndex;
Expand All @@ -402,16 +451,7 @@ private void updateTestImport(XrayTestExecutionImport.TestRun testRun, MethodCon
continue;
}

/**
* The test steps definitions
*/
final XrayTestExecutionImport.TestStep importTestStep = new XrayTestExecutionImport.TestStep();
importTestStep.setAction(testerraTestStep.getName());
// We always expect the step to pass
importTestStep.setResult(XrayTestExecutionImport.TestRun.Status.PASS.toString());
testRun.getTestInfo().addStep(importTestStep);

/**
/*
* The actual Test Run step
*/
XrayTestExecutionImport.TestRun.Status actualStatus = XrayTestExecutionImport.TestRun.Status.PASS;
Expand All @@ -423,7 +463,7 @@ private void updateTestImport(XrayTestExecutionImport.TestRun testRun, MethodCon
testRunStep.setStatus(actualStatus);
testRun.addStep(testRunStep);

testerraTestStep.getTestStepActions().stream()
testerraTestStep.readActions()
.flatMap(TestStepAction::readEntries)
.forEach(entry -> {
if (entry instanceof ErrorContext) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ default JqlQuery queryTest(MethodContext methodContext) {
}

/**
* If true, try to create a Xray Test
* If true, try to create or update a Xray Test
* <p>
* The create/update process includes issue attributes and test steps.
*/
default boolean shouldCreateNewTest(MethodContext methodContext) {
return false;
Expand Down Expand Up @@ -133,8 +135,6 @@ default void updateTest(XrayTestIssue xrayTestIssue, MethodContext methodContext

/**
* Returns the default test issue summery for creating new tests or searching for mapping
* @param methodContext
* @return
*/
default String getDefaultTestIssueSummery(MethodContext methodContext) {
return methodContext.getName();
Expand Down