Skip to content

Commit f78ee3d

Browse files
authored
add spring test context integration (via allure-framework#72)
1 parent 700435f commit f78ee3d

14 files changed

Lines changed: 402 additions & 0 deletions

File tree

allure-servlet-api/build.gradle

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
description = 'Allure Servlet API v3 integration'
2+
3+
apply from: "${gradleScriptDir}/maven-publish.gradle"
4+
apply from: "${gradleScriptDir}/bintray.gradle"
5+
apply plugin: 'maven'
6+
7+
dependencies {
8+
compile project(':allure-attachments')
9+
compile 'javax.servlet:javax.servlet-api'
10+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package io.qameta.allure.servletapi;
2+
3+
import io.qameta.allure.attachment.http.HttpRequestAttachment;
4+
import io.qameta.allure.attachment.http.HttpResponseAttachment;
5+
import org.slf4j.Logger;
6+
import org.slf4j.LoggerFactory;
7+
8+
import javax.servlet.http.HttpServletRequest;
9+
import javax.servlet.http.HttpServletResponse;
10+
import java.io.BufferedReader;
11+
import java.io.IOException;
12+
import java.util.Collections;
13+
import java.util.stream.Stream;
14+
15+
import static io.qameta.allure.attachment.http.HttpRequestAttachment.Builder.create;
16+
import static io.qameta.allure.attachment.http.HttpResponseAttachment.Builder.create;
17+
18+
/**
19+
* @author charlie (Dmitry Baev).
20+
*/
21+
public final class HttpServletAttachmentBuilder {
22+
23+
private static final Logger LOGGER = LoggerFactory.getLogger(HttpServletAttachmentBuilder.class);
24+
25+
private HttpServletAttachmentBuilder() {
26+
throw new IllegalStateException();
27+
}
28+
29+
public static HttpRequestAttachment buildRequest(final HttpServletRequest request) {
30+
final HttpRequestAttachment.Builder requestBuilder = create("Request", request.getRequestURI());
31+
Collections.list(request.getHeaderNames())
32+
.forEach(name -> {
33+
final String value = request.getHeader(name);
34+
requestBuilder.withHeader(name, value);
35+
});
36+
37+
Stream.of(request.getCookies())
38+
.forEach(cookie -> requestBuilder.withCookie(cookie.getName(), cookie.getValue()));
39+
requestBuilder.withBody(getBody(request));
40+
return requestBuilder.build();
41+
}
42+
43+
public static HttpResponseAttachment buildResponse(final HttpServletResponse response) {
44+
final HttpResponseAttachment.Builder responseBuilder = create("Response");
45+
response.getHeaderNames()
46+
.forEach(name -> response.getHeaders(name)
47+
.forEach(value -> responseBuilder.withHeader(name, value)));
48+
return responseBuilder.build();
49+
}
50+
51+
public static String getBody(final HttpServletRequest request) {
52+
final StringBuilder sb = new StringBuilder();
53+
try (BufferedReader reader = request.getReader()) {
54+
readBody(sb, reader);
55+
} catch (IOException e) {
56+
LOGGER.warn("Could not read request body", e);
57+
}
58+
return sb.toString();
59+
}
60+
61+
@SuppressWarnings("PMD.AssignmentInOperand")
62+
public static void readBody(final StringBuilder sb,
63+
final BufferedReader reader) throws IOException {
64+
String line;
65+
while ((line = reader.readLine()) != null) {
66+
sb.append(line);
67+
}
68+
}
69+
}

allure-spring-boot/build.gradle

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
description = 'Allure Servlet API v3'
2+
3+
apply from: "${gradleScriptDir}/maven-publish.gradle"
4+
apply from: "${gradleScriptDir}/bintray.gradle"
5+
apply plugin: 'maven'
6+
7+
dependencies {
8+
compile project(':allure-attachments')
9+
compile project(':allure-spring4-webmvc')
10+
compile 'org.springframework.boot:spring-boot-autoconfigure'
11+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package io.qameta.allure.spring4;
2+
3+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
4+
import org.springframework.context.annotation.Configuration;
5+
6+
/**
7+
* @author charlie (Dmitry Baev).
8+
*/
9+
@Configuration
10+
@ConditionalOnMissingBean(value = AllureSpring4WebMvc.class)
11+
public class AllureSpringWebmvcAutoconfigure extends AllureWebMvcConfigurerAdapter {
12+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Allure TestExecutionListener for the Spring TestContext Framework
2+
#
3+
org.springframework.boot.autoconfigure.EnableAutoConfiguration=io.qameta.allure.spring4.AllureSpringWebmvcAutoconfigure

allure-spring4-test/build.gradle

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
description = 'Allure Spring4 test context'
2+
3+
apply from: "${gradleScriptDir}/maven-publish.gradle"
4+
apply from: "${gradleScriptDir}/bintray.gradle"
5+
apply plugin: 'maven'
6+
7+
dependencies {
8+
compile project(':allure-java-commons')
9+
compile 'org.springframework:spring-test'
10+
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package io.qameta.allure.spring4;
2+
3+
import io.qameta.allure.Allure;
4+
import io.qameta.allure.AllureLifecycle;
5+
import io.qameta.allure.Epic;
6+
import io.qameta.allure.Feature;
7+
import io.qameta.allure.Issue;
8+
import io.qameta.allure.Owner;
9+
import io.qameta.allure.Severity;
10+
import io.qameta.allure.Story;
11+
import io.qameta.allure.TmsLink;
12+
import io.qameta.allure.model.Label;
13+
import io.qameta.allure.model.Link;
14+
import io.qameta.allure.model.Status;
15+
import io.qameta.allure.model.StatusDetails;
16+
import io.qameta.allure.model.TestResult;
17+
import io.qameta.allure.util.ResultsUtils;
18+
import org.springframework.test.context.TestContext;
19+
import org.springframework.test.context.TestExecutionListener;
20+
21+
import java.lang.annotation.Annotation;
22+
import java.lang.reflect.Method;
23+
import java.math.BigInteger;
24+
import java.security.MessageDigest;
25+
import java.security.NoSuchAlgorithmException;
26+
import java.util.List;
27+
import java.util.Objects;
28+
import java.util.Optional;
29+
import java.util.UUID;
30+
import java.util.stream.Collectors;
31+
import java.util.stream.Stream;
32+
33+
import static io.qameta.allure.util.ResultsUtils.getHostName;
34+
import static io.qameta.allure.util.ResultsUtils.getStatus;
35+
import static io.qameta.allure.util.ResultsUtils.getStatusDetails;
36+
import static io.qameta.allure.util.ResultsUtils.getThreadName;
37+
import static java.nio.charset.StandardCharsets.UTF_8;
38+
39+
/**
40+
* @author charlie (Dmitry Baev).
41+
*/
42+
@SuppressWarnings("PMD.ExcessiveImports")
43+
public class AllureSpring4 implements TestExecutionListener {
44+
45+
private static final String MD_5 = "md5";
46+
47+
private final ThreadLocal<String> testCases
48+
= InheritableThreadLocal.withInitial(() -> UUID.randomUUID().toString());
49+
50+
private final AllureLifecycle lifecycle;
51+
52+
public AllureSpring4() {
53+
this.lifecycle = Allure.getLifecycle();
54+
}
55+
56+
@Override
57+
public void beforeTestClass(final TestContext testContext) throws Exception {
58+
//do nothing
59+
}
60+
61+
@Override
62+
public void prepareTestInstance(final TestContext testContext) throws Exception {
63+
//do nothing
64+
}
65+
66+
@Override
67+
public void beforeTestMethod(final TestContext testContext) throws Exception {
68+
final String uuid = testCases.get();
69+
final Class<?> testClass = testContext.getTestClass();
70+
final Method testMethod = testContext.getTestMethod();
71+
final String id = getHistoryId(testClass, testMethod);
72+
73+
final TestResult result = new TestResult()
74+
.withUuid(uuid)
75+
.withHistoryId(id)
76+
.withName(testMethod.getName())
77+
.withFullName(String.format("%s.%s", testClass.getCanonicalName(), testMethod.getName()))
78+
.withLinks(getLinks(testClass, testMethod))
79+
.withLabels(
80+
new Label().withName("package").withValue(testClass.getCanonicalName()),
81+
new Label().withName("testClass").withValue(testClass.getCanonicalName()),
82+
new Label().withName("testMethod").withValue(testMethod.getName()),
83+
84+
new Label().withName("suite").withValue(testClass.getName()),
85+
86+
new Label().withName("host").withValue(getHostName()),
87+
new Label().withName("thread").withValue(getThreadName())
88+
);
89+
90+
result.getLabels().addAll(getLabels(testClass, testMethod));
91+
getDisplayName(testMethod).ifPresent(result::setName);
92+
getLifecycle().scheduleTestCase(result);
93+
getLifecycle().startTestCase(uuid);
94+
}
95+
96+
@Override
97+
public void afterTestMethod(final TestContext testContext) throws Exception {
98+
final String uuid = testCases.get();
99+
testCases.remove();
100+
getLifecycle().updateTestCase(uuid, testResult -> {
101+
testResult.setStatus(getStatus(testContext.getTestException()).orElse(Status.PASSED));
102+
if (Objects.isNull(testResult.getStatusDetails())) {
103+
testResult.setStatusDetails(new StatusDetails());
104+
}
105+
getStatusDetails(testContext.getTestException()).ifPresent(statusDetails -> {
106+
testResult.getStatusDetails().setMessage(statusDetails.getMessage());
107+
testResult.getStatusDetails().setTrace(statusDetails.getTrace());
108+
});
109+
});
110+
getLifecycle().stopTestCase(uuid);
111+
getLifecycle().writeTestCase(uuid);
112+
}
113+
114+
@Override
115+
public void afterTestClass(final TestContext testContext) throws Exception {
116+
//do nothing
117+
}
118+
119+
public AllureLifecycle getLifecycle() {
120+
return lifecycle;
121+
}
122+
123+
private Optional<String> getDisplayName(final Method method) {
124+
return Optional.ofNullable(method.getAnnotation(DisplayName.class))
125+
.map(DisplayName::value);
126+
}
127+
128+
private List<Link> getLinks(final Class<?> testClass, final Method testMethod) {
129+
return Stream.of(
130+
getAnnotations(testClass, testMethod, io.qameta.allure.Link.class).map(ResultsUtils::createLink),
131+
getAnnotations(testClass, testMethod, Issue.class).map(ResultsUtils::createLink),
132+
getAnnotations(testClass, testMethod, TmsLink.class).map(ResultsUtils::createLink)
133+
).reduce(Stream::concat).orElseGet(Stream::empty).collect(Collectors.toList());
134+
}
135+
136+
private List<Label> getLabels(final Class<?> testClass, final Method testMethod) {
137+
return Stream.of(
138+
getAnnotations(testClass, testMethod, Epic.class).map(ResultsUtils::createLabel),
139+
getAnnotations(testClass, testMethod, Feature.class).map(ResultsUtils::createLabel),
140+
getAnnotations(testClass, testMethod, Story.class).map(ResultsUtils::createLabel),
141+
getAnnotations(testClass, testMethod, Severity.class).map(ResultsUtils::createLabel),
142+
getAnnotations(testClass, testMethod, Owner.class).map(ResultsUtils::createLabel)
143+
).reduce(Stream::concat).orElseGet(Stream::empty).collect(Collectors.toList());
144+
}
145+
146+
private <T extends Annotation> Stream<T> getAnnotations(
147+
final Class<?> testClass, final Method testMethod, final Class<T> annotation) {
148+
return Stream.of(annotation)
149+
.flatMap(clazz -> Stream.concat(
150+
Stream.of(testClass.getAnnotationsByType(clazz)),
151+
Stream.of(testMethod.getAnnotationsByType(clazz)))
152+
);
153+
}
154+
155+
private String getHistoryId(final Class<?> testClass, final Method testMethod) {
156+
final MessageDigest digest = getMessageDigest();
157+
digest.update(testClass.getCanonicalName().getBytes(UTF_8));
158+
digest.update(testMethod.getName().getBytes(UTF_8));
159+
Stream.of(testMethod.getParameterTypes())
160+
.map(Class::getCanonicalName)
161+
.map(name -> name.getBytes(UTF_8))
162+
.forEach(digest::update);
163+
return new BigInteger(1, digest.digest()).toString(16);
164+
}
165+
166+
private MessageDigest getMessageDigest() {
167+
try {
168+
return MessageDigest.getInstance(MD_5);
169+
} catch (NoSuchAlgorithmException e) {
170+
throw new IllegalStateException("Could not find md5 hashing algorithm", e);
171+
}
172+
}
173+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package io.qameta.allure.spring4;
2+
3+
import java.lang.annotation.Documented;
4+
import java.lang.annotation.ElementType;
5+
import java.lang.annotation.Inherited;
6+
import java.lang.annotation.Retention;
7+
import java.lang.annotation.RetentionPolicy;
8+
import java.lang.annotation.Target;
9+
10+
/**
11+
* Used to change display name for test in the report.
12+
*/
13+
@Documented
14+
@Inherited
15+
@Retention(RetentionPolicy.RUNTIME)
16+
@Target({ElementType.METHOD, ElementType.TYPE})
17+
public @interface DisplayName {
18+
19+
String value();
20+
21+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Allure TestExecutionListener for the Spring TestContext Framework
2+
#
3+
org.springframework.test.context.TestExecutionListener=io.qameta.allure.spring4.AllureSpring4

allure-spring4-webmvc/build.gradle

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
description = 'Allure Spring 4 Web MVC integration'
2+
3+
apply from: "${gradleScriptDir}/maven-publish.gradle"
4+
apply from: "${gradleScriptDir}/bintray.gradle"
5+
apply plugin: 'maven'
6+
7+
dependencies {
8+
compile project(':allure-servlet-api')
9+
compile 'org.springframework:spring-webmvc'
10+
}

0 commit comments

Comments
 (0)