From 2cdc7ae59e499324a26cca4efefd229f303601a0 Mon Sep 17 00:00:00 2001 From: qameta-ci Date: Wed, 28 Jun 2023 12:17:24 +0000 Subject: [PATCH 01/11] set next development version 2.24 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index b1d69a20f..de2753ed4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=2.23.0 +version=2.24-SNAPSHOT org.gradle.daemon=true org.gradle.parallel=true From 10b1bcc4a102b6c1cd42914c928d1d50cd430199 Mon Sep 17 00:00:00 2001 From: Valery Yatsynovich Date: Mon, 10 Jul 2023 16:16:09 +0300 Subject: [PATCH 02/11] fix target maven repository url in readme (via #934) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 567df89e6..920687399 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ The repository contains new versions of adaptors for JVM-based test frameworks. -All the artifacts are deployed to `https://dl.bintray.com/qameta/maven`. +All the artifacts are deployed to `https://repo1.maven.org/maven2/io/qameta/allure/`. ## TestNG @@ -219,4 +219,4 @@ more usage example look into module `allure-awaitility` Usage example: ``` Awaitility.setDefaultConditionEvaluationListener(new AllureAwaitilityListener()); -``` \ No newline at end of file +``` From 466aa0bbce60b52fdba4c736028c3aecdb0c91b2 Mon Sep 17 00:00:00 2001 From: a-simeshin <89605031+a-simeshin@users.noreply.github.com> Date: Mon, 10 Jul 2023 16:17:38 +0300 Subject: [PATCH 03/11] add interceptors for httpclient5 (via #935) --- README.md | 19 ++ allure-httpclient5/build.gradle.kts | 27 +++ allure-httpclient5/readme.md | 56 ++++++ .../httpclient5/AllureHttpClient5Request.java | 83 ++++++++ .../AllureHttpClient5Response.java | 100 ++++++++++ .../httpclient5/AllureHttpEntityUtils.java | 96 +++++++++ .../AllureHttpClient5DeleteTest.java | 140 +++++++++++++ .../httpclient5/AllureHttpClient5GetTest.java | 176 +++++++++++++++++ .../AllureHttpClient5PostTest.java | 187 ++++++++++++++++++ .../src/test/resources/allure.properties | 2 + build.gradle.kts | 1 + settings.gradle.kts | 1 + 12 files changed, 888 insertions(+) create mode 100644 allure-httpclient5/build.gradle.kts create mode 100644 allure-httpclient5/readme.md create mode 100644 allure-httpclient5/src/main/java/io/qameta/allure/httpclient5/AllureHttpClient5Request.java create mode 100644 allure-httpclient5/src/main/java/io/qameta/allure/httpclient5/AllureHttpClient5Response.java create mode 100644 allure-httpclient5/src/main/java/io/qameta/allure/httpclient5/AllureHttpEntityUtils.java create mode 100644 allure-httpclient5/src/test/java/io/qameta/allure/httpclient5/AllureHttpClient5DeleteTest.java create mode 100644 allure-httpclient5/src/test/java/io/qameta/allure/httpclient5/AllureHttpClient5GetTest.java create mode 100644 allure-httpclient5/src/test/java/io/qameta/allure/httpclient5/AllureHttpClient5PostTest.java create mode 100644 allure-httpclient5/src/test/resources/allure.properties diff --git a/README.md b/README.md index 920687399..668eac198 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,25 @@ Usage example: .addInterceptorLast(new AllureHttpClientResponse()); ``` +## Http client 5 +Interceptors for Apache [httpclient5](https://hc.apache.org/httpcomponents-client-5.2.x/index.html). +Additional info can be found in module `allure-httpclient5` + +```xml + + io.qameta.allure + allure-httpclient5 + $LATEST_VERSION + +``` + +Usage example: +```java +final HttpClientBuilder builder = HttpClientBuilder.create() + .addRequestInterceptorFirst(new AllureHttpClient5Request("your-request-template-attachment.ftl")) + .addResponseInterceptorLast(new AllureHttpClient5Response("your-response-template-attachment.ftl")); +``` + ## JAX-RS Filter Filter that can be used with JAX-RS compliant clients such as RESTeasy and Jersey diff --git a/allure-httpclient5/build.gradle.kts b/allure-httpclient5/build.gradle.kts new file mode 100644 index 000000000..eb42ad2da --- /dev/null +++ b/allure-httpclient5/build.gradle.kts @@ -0,0 +1,27 @@ +description = "Allure Apache HttpClient5 Integration" + +dependencies { + api(project(":allure-attachments")) + implementation("org.apache.httpcomponents.client5:httpclient5") + testImplementation("com.github.tomakehurst:wiremock") + testImplementation("io.github.glytching:junit-extensions") + testImplementation("org.assertj:assertj-core") + testImplementation("org.junit.jupiter:junit-jupiter-api") + testImplementation("org.mockito:mockito-core") + testImplementation("org.slf4j:slf4j-simple") + testImplementation(project(":allure-java-commons-test")) + testImplementation(project(":allure-junit-platform")) + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") +} + +tasks.jar { + manifest { + attributes(mapOf( + "Automatic-Module-Name" to "io.qameta.allure.httpclient5" + )) + } +} + +tasks.test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/allure-httpclient5/readme.md b/allure-httpclient5/readme.md new file mode 100644 index 000000000..b307e4e4f --- /dev/null +++ b/allure-httpclient5/readme.md @@ -0,0 +1,56 @@ +## Allure-httpclient5 +Extended logging for requests and responses with [httpclient5](https://mvnrepository.com/artifact/org.apache.httpcomponents.client5/httpclient5) +This library does not support `httpclient` due to package and API changes between `httpclient` and `httpclient5`. +To work with `httpclient`, it is recommended to use the `allure-httpclient` library. + +## Wiki +https://hc.apache.org/httpcomponents-client-5.2.x/ +https://hc.apache.org/httpcomponents-client-5.2.x/quickstart.html +https://hc.apache.org/httpcomponents-client-5.2.x/migration-guide/index.html +https://hc.apache.org/httpcomponents-client-5.2.x/examples.html + +## Additional features +Implemented: +- The `httpclient5` library uses `gzip` compression by default. Interceptors attach message bodies in decompressed form +- `HttpEntityEnclosingRequest` is removed from `httpclient5`. Request interceptor works wo `HttpEntityEnclosingRequest` + +Not tested: +- The httpclient5 library support Async interactions (Not tested) + +## Examples + +```java +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import io.qameta.allure.httpclient5.AllureHttpClient5Request; +import io.qameta.allure.httpclient5.AllureHttpClient5Response; + +class Test { + + @Test + void smokeGetShouldNotThrowThenReturnCorrectResponseMessage() throws IOException { + final HttpClientBuilder builder = HttpClientBuilder.create() + .addRequestInterceptorFirst(new AllureHttpClient5Request()) + .addResponseInterceptorLast(new AllureHttpClient5Response()); + + try (CloseableHttpClient httpClient = builder.build()) { + final HttpGet httpGet = new HttpGet("/hello"); + httpClient.execute(httpGet, response -> { + assertThat(EntityUtils.toString(response.getEntity())).isEqualTo(BODY_STRING); + return response; + }); + } + } +} +``` + +In addition to using standard templates for formatting, you can use your custom `ftl` templates along the path +`/resources/tpl/...`. For examples, you can use templates from the `allure-attachments` module. + +```java + final HttpClientBuilder builder = HttpClientBuilder.create() + .addRequestInterceptorFirst(new AllureHttpClient5Request("your-request-template-attachment.ftl")) + .addResponseInterceptorLast(new AllureHttpClient5Response("your-response-template-attachment.ftl")); +``` \ No newline at end of file diff --git a/allure-httpclient5/src/main/java/io/qameta/allure/httpclient5/AllureHttpClient5Request.java b/allure-httpclient5/src/main/java/io/qameta/allure/httpclient5/AllureHttpClient5Request.java new file mode 100644 index 000000000..d593c4576 --- /dev/null +++ b/allure-httpclient5/src/main/java/io/qameta/allure/httpclient5/AllureHttpClient5Request.java @@ -0,0 +1,83 @@ +/* + * Copyright 2019 Qameta Software OÜ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.qameta.allure.httpclient5; + +import io.qameta.allure.attachment.AttachmentData; +import io.qameta.allure.attachment.AttachmentProcessor; +import io.qameta.allure.attachment.AttachmentRenderer; +import io.qameta.allure.attachment.DefaultAttachmentProcessor; +import io.qameta.allure.attachment.FreemarkerAttachmentRenderer; +import io.qameta.allure.attachment.http.HttpRequestAttachment; +import org.apache.hc.core5.http.EntityDetails; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpRequestInterceptor; +import org.apache.hc.core5.http.protocol.HttpContext; + +import java.util.stream.Stream; + +import static io.qameta.allure.attachment.http.HttpRequestAttachment.Builder.create; + +/** + * @author a-simeshin (Simeshin Artem) + */ +@SuppressWarnings("PMD.MethodArgumentCouldBeFinal") +public class AllureHttpClient5Request implements HttpRequestInterceptor { + + private final AttachmentRenderer renderer; + private final AttachmentProcessor processor; + + public AllureHttpClient5Request() { + this("http-request.ftl"); + } + + public AllureHttpClient5Request(final String templateName) { + this(new FreemarkerAttachmentRenderer(templateName), new DefaultAttachmentProcessor()); + } + + public AllureHttpClient5Request(final AttachmentRenderer renderer, + final AttachmentProcessor processor) { + this.renderer = renderer; + this.processor = processor; + } + + /** + * Processes the HTTP request and adds an attachment to the Allure Attachment processor. + * + * @param request the HTTP request + * @param entity the entity details + * @param context the HTTP context + */ + @Override + public void process(HttpRequest request, EntityDetails entity, HttpContext context) { + final String attachmentName = getAttachmentName(request); + final HttpRequestAttachment.Builder builder = create(attachmentName, request.getRequestUri()); + builder.setMethod(request.getMethod()); + + Stream.of(request.getHeaders()).forEach(header -> builder.setHeader(header.getName(), header.getValue())); + + if (entity instanceof HttpEntity && ((HttpEntity) entity).isRepeatable() && entity.getContentLength() != 0) { + builder.setBody(AllureHttpEntityUtils.getBody((HttpEntity) entity)); + } + + processor.addAttachment(builder.build(), renderer); + } + + private String getAttachmentName(final HttpRequest request) { + return String.format("Request_%s_%s", request.getMethod(), request.getRequestUri()); + } + +} diff --git a/allure-httpclient5/src/main/java/io/qameta/allure/httpclient5/AllureHttpClient5Response.java b/allure-httpclient5/src/main/java/io/qameta/allure/httpclient5/AllureHttpClient5Response.java new file mode 100644 index 000000000..97e6c1c86 --- /dev/null +++ b/allure-httpclient5/src/main/java/io/qameta/allure/httpclient5/AllureHttpClient5Response.java @@ -0,0 +1,100 @@ +/* + * Copyright 2019 Qameta Software OÜ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.qameta.allure.httpclient5; + +import io.qameta.allure.attachment.AttachmentData; +import io.qameta.allure.attachment.AttachmentProcessor; +import io.qameta.allure.attachment.AttachmentRenderer; +import io.qameta.allure.attachment.DefaultAttachmentProcessor; +import io.qameta.allure.attachment.FreemarkerAttachmentRenderer; +import io.qameta.allure.attachment.http.HttpResponseAttachment; +import org.apache.hc.core5.http.EntityDetails; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.HttpResponseInterceptor; +import org.apache.hc.core5.http.io.entity.BufferedHttpEntity; +import org.apache.hc.core5.http.message.BasicClassicHttpResponse; +import org.apache.hc.core5.http.protocol.HttpContext; + +import java.io.IOException; +import java.util.stream.Stream; + +import static io.qameta.allure.attachment.http.HttpResponseAttachment.Builder.create; + +/** + * @author a-simeshin (Simeshin Artem) + */ +@SuppressWarnings({ + "checkstyle:ParameterAssignment", + "PMD.MethodArgumentCouldBeFinal", + "PMD.AvoidReassigningParameters"}) +public class AllureHttpClient5Response implements HttpResponseInterceptor { + private final AttachmentRenderer renderer; + private final AttachmentProcessor processor; + private static final String NO_BODY = "No body present"; + + public AllureHttpClient5Response() { + this("http-response.ftl"); + } + + public AllureHttpClient5Response(final String templateName) { + this(new FreemarkerAttachmentRenderer(templateName), new DefaultAttachmentProcessor()); + } + + public AllureHttpClient5Response(final AttachmentRenderer renderer, + final AttachmentProcessor processor) { + this.renderer = renderer; + this.processor = processor; + } + + /** + * Processes the HTTP response and adds an attachment to the Allure Attachment processor. + * + * @param response the HTTP response + * @param entity the entity details, may be null for no response body responses + * @param context the HTTP context + * @throws IOException if an I/O error occurs + */ + @Override + public void process(HttpResponse response, EntityDetails entity, HttpContext context) throws IOException { + final HttpResponseAttachment.Builder builder = create("Response"); + builder.setResponseCode(response.getCode()); + + Stream.of(response.getHeaders()).forEach(header -> builder.setHeader(header.getName(), header.getValue())); + + final HttpEntity originalHttpEntity = (HttpEntity) entity; + if (originalHttpEntity != null && !originalHttpEntity.isRepeatable()) { + // Looks like a bug or completely new logic. It's not enough to replace chaining EntityDetails entity. + // To read the response body twice, It needs to put in the context also + entity = new BufferedHttpEntity(originalHttpEntity); + final BasicClassicHttpResponse responseEntity = + (BasicClassicHttpResponse) context.getAttribute("http.response"); + responseEntity.setEntity((HttpEntity) entity); + + final String responseBody = AllureHttpEntityUtils.getBody((HttpEntity) entity); + if (responseBody == null || responseBody.isEmpty()) { + builder.setBody(NO_BODY); + } else { + builder.setBody(responseBody); + } + } else { + builder.setBody(NO_BODY); + } + + processor.addAttachment(builder.build(), renderer); + } + +} diff --git a/allure-httpclient5/src/main/java/io/qameta/allure/httpclient5/AllureHttpEntityUtils.java b/allure-httpclient5/src/main/java/io/qameta/allure/httpclient5/AllureHttpEntityUtils.java new file mode 100644 index 000000000..342093031 --- /dev/null +++ b/allure-httpclient5/src/main/java/io/qameta/allure/httpclient5/AllureHttpEntityUtils.java @@ -0,0 +1,96 @@ +/* + * Copyright 2023 Qameta Software OÜ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.qameta.allure.httpclient5; + +import io.qameta.allure.AllureResultsWriteException; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.ParseException; +import org.apache.hc.core5.http.io.entity.EntityUtils; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.zip.GZIPInputStream; + +/** + * Utility class for working with HTTP entity in Allure framework. + */ +@SuppressWarnings({"checkstyle:ParameterAssignment", "PMD.AssignmentInOperand"}) +public final class AllureHttpEntityUtils { + + private AllureHttpEntityUtils() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); + } + + /** + * Retrieves the body of the HTTP entity as a string. + * + * @param httpEntity the HTTP entity + * @return the body of the HTTP entity as a string + * @throws AllureResultsWriteException if an error occurs while reading the entity body + */ + static String getBody(final HttpEntity httpEntity) { + try { + final String contentEncoding = httpEntity.getContentEncoding(); + if (contentEncoding != null && contentEncoding.contains("gzip")) { + return unpackGzipEntityString(httpEntity); + } else { + return EntityUtils.toString(httpEntity, getContentEncoding(httpEntity.getContentEncoding())); + } + } catch (IOException | ParseException e) { + throw new AllureResultsWriteException("Can't read request message body to String", e); + } + } + + /** + * Retrieves the content encoding of the HTTP entity. + * + * @param contentEncoding the content encoding value + * @return the charset corresponding to the content encoding, or UTF-8 if the encoding is invalid + */ + static Charset getContentEncoding(final String contentEncoding) { + try { + return Charset.forName(contentEncoding); + } catch (IllegalArgumentException ignored) { + return StandardCharsets.UTF_8; + } + } + + /** + * Unpacks the GZIP-encoded entity string. + * + * @param entity the GZIP-encoded HTTP entity + * @return the unpacked entity string + * @throws IOException if an error occurs while unpacking the entity + */ + static String unpackGzipEntityString(final HttpEntity entity) throws IOException { + final GZIPInputStream gis = new GZIPInputStream(entity.getContent()); + final Charset contentEncoding = getContentEncoding(entity.getContentEncoding()); + try (InputStreamReader inputStreamReader = new InputStreamReader(gis, contentEncoding)) { + try (BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) { + final StringBuilder outStr = new StringBuilder(); + String line; + while ((line = bufferedReader.readLine()) != null) { + outStr.append(line); + } + return outStr.toString(); + } + } + } + +} diff --git a/allure-httpclient5/src/test/java/io/qameta/allure/httpclient5/AllureHttpClient5DeleteTest.java b/allure-httpclient5/src/test/java/io/qameta/allure/httpclient5/AllureHttpClient5DeleteTest.java new file mode 100644 index 000000000..453b57bc4 --- /dev/null +++ b/allure-httpclient5/src/test/java/io/qameta/allure/httpclient5/AllureHttpClient5DeleteTest.java @@ -0,0 +1,140 @@ +/* + * Copyright 2019 Qameta Software OÜ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.qameta.allure.httpclient5; + +import com.github.tomakehurst.wiremock.WireMockServer; +import io.qameta.allure.attachment.AttachmentData; +import io.qameta.allure.attachment.AttachmentProcessor; +import io.qameta.allure.attachment.AttachmentRenderer; +import org.apache.hc.client5.http.classic.methods.HttpDelete; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import java.util.Objects; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.configureFor; +import static com.github.tomakehurst.wiremock.client.WireMock.delete; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * @author a-simeshin (Simeshin Artem). + */ +@SuppressWarnings({"unchecked", "PMD.JUnitTestContainsTooManyAsserts"}) +class AllureHttpClient5DeleteTest { + + private static final String DELETE_URL = "http://localhost:%d/delete"; + private static final String HELLO_RESOURCE_PATH = "/delete"; + + private WireMockServer server; + + @BeforeEach + void setUp() { + server = new WireMockServer(options().dynamicPort()); + server.start(); + configureFor(server.port()); + + stubFor(delete(HELLO_RESOURCE_PATH).willReturn( + aResponse() + .withStatus(204)) + ); + } + + @AfterEach + void tearDown() { + if (Objects.nonNull(server)) { + server.stop(); + } + } + + @Test + void smokeDeleteShouldNotThrowThenReturnCorrectCode() { + final HttpClientBuilder builder = HttpClientBuilder.create() + .addRequestInterceptorFirst(new AllureHttpClient5Request()) + .addResponseInterceptorLast(new AllureHttpClient5Response()); + + assertDoesNotThrow(() -> { + try (CloseableHttpClient httpClient = builder.build()) { + final HttpDelete httpDelete = new HttpDelete(String.format(DELETE_URL, server.port())); + httpClient.execute(httpDelete, response -> { + assertThat(response.getCode()).isEqualTo(204); + return response; + }); + } + }); + } + + @Test + void shouldCreateDeleteRequestAttachment() throws Exception { + final AttachmentRenderer renderer = mock(AttachmentRenderer.class); + final AttachmentProcessor processor = mock(AttachmentProcessor.class); + + final HttpClientBuilder builder = HttpClientBuilder.create() + .addRequestInterceptorFirst(new AllureHttpClient5Request(renderer, processor)); + + try (CloseableHttpClient httpClient = builder.build()) { + final HttpDelete httpDelete = new HttpDelete(String.format(DELETE_URL, server.port())); + httpClient.execute(httpDelete, response -> { + assertThat(response.getCode()).isEqualTo(204); + return response; + }); + } + + final ArgumentCaptor captor = ArgumentCaptor.forClass(AttachmentData.class); + + verify(processor, times(1)).addAttachment(captor.capture(), eq(renderer)); + assertThat(captor.getAllValues()) + .hasSize(1) + .extracting("url") + .containsExactly(HELLO_RESOURCE_PATH); + } + @Test + void shouldCreateDeleteResponseAttachmentWithEmptyBody() throws Exception { + final AttachmentRenderer renderer = mock(AttachmentRenderer.class); + final AttachmentProcessor processor = mock(AttachmentProcessor.class); + + final HttpClientBuilder builder = HttpClientBuilder.create() + .addResponseInterceptorLast(new AllureHttpClient5Response(renderer, processor)); + + try (CloseableHttpClient httpClient = builder.build()) { + final HttpDelete httpDelete = new HttpDelete(String.format(DELETE_URL, server.port())); + httpClient.execute(httpDelete, response -> { + assertThat(response.getCode()).isEqualTo(204); + return response; + }); + } + + final ArgumentCaptor captor = ArgumentCaptor.forClass(AttachmentData.class); + verify(processor, times(1)) + .addAttachment(captor.capture(), eq(renderer)); + + assertThat(captor.getAllValues()) + .hasSize(1) + .extracting("body") + .containsExactly("No body present"); + } +} diff --git a/allure-httpclient5/src/test/java/io/qameta/allure/httpclient5/AllureHttpClient5GetTest.java b/allure-httpclient5/src/test/java/io/qameta/allure/httpclient5/AllureHttpClient5GetTest.java new file mode 100644 index 000000000..4453f2e49 --- /dev/null +++ b/allure-httpclient5/src/test/java/io/qameta/allure/httpclient5/AllureHttpClient5GetTest.java @@ -0,0 +1,176 @@ +/* + * Copyright 2019 Qameta Software OÜ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.qameta.allure.httpclient5; + +import com.github.tomakehurst.wiremock.WireMockServer; +import io.qameta.allure.attachment.AttachmentData; +import io.qameta.allure.attachment.AttachmentProcessor; +import io.qameta.allure.attachment.AttachmentRenderer; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import java.util.Objects; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.configureFor; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * @author a-simeshin (Simeshin Artem). + */ +@SuppressWarnings({"unchecked", "PMD.JUnitTestContainsTooManyAsserts"}) +class AllureHttpClient5GetTest { + + private static final String BODY_STRING = "Hello world!"; + private static final String HELLO_RESOURCE_PATH = "/hello"; + private static final String HELLO_GET_RETURN_BODY = "http://localhost:%d/hello"; + private static final String HELLO_GET_201_NO_BODY = "http://localhost:%d/empty"; + + + private WireMockServer server; + + @BeforeEach + void setUp() { + server = new WireMockServer(options().dynamicPort()); + server.start(); + configureFor(server.port()); + + stubFor(get(HELLO_RESOURCE_PATH).willReturn( + aResponse() + .withHeader("Content-Type", "application/json") + .withBody(BODY_STRING) + )); + stubFor(get("/empty").willReturn( + aResponse() + .withStatus(200)) + ); + } + + @AfterEach + void tearDown() { + if (Objects.nonNull(server)) { + server.stop(); + } + } + + @Test + void smokeGetShouldNotThrowThenReturnCorrectResponseMessage() { + final HttpClientBuilder builder = HttpClientBuilder.create() + .addRequestInterceptorFirst(new AllureHttpClient5Request()) + .addResponseInterceptorLast(new AllureHttpClient5Response()); + + assertDoesNotThrow(() -> { + try (CloseableHttpClient httpClient = builder.build()) { + final HttpGet httpGet = new HttpGet(String.format(HELLO_GET_RETURN_BODY, server.port())); + httpClient.execute(httpGet, response -> { + assertThat(EntityUtils.toString(response.getEntity())).isEqualTo(BODY_STRING); + return response; + }); + } + }); + } + + @Test + void shouldCreateGetRequestAttachment() throws Exception { + final AttachmentRenderer renderer = mock(AttachmentRenderer.class); + final AttachmentProcessor processor = mock(AttachmentProcessor.class); + + final HttpClientBuilder builder = HttpClientBuilder.create() + .addRequestInterceptorLast(new AllureHttpClient5Request(renderer, processor)); + + try (CloseableHttpClient httpClient = builder.build()) { + final HttpGet httpGet = new HttpGet(String.format(HELLO_GET_RETURN_BODY, server.port())); + httpClient.execute(httpGet, response -> { + assertThat(EntityUtils.toString(response.getEntity())).isEqualTo(BODY_STRING); + return response; + }); + } + + final ArgumentCaptor captor = ArgumentCaptor.forClass(AttachmentData.class); + + verify(processor, times(1)).addAttachment(captor.capture(), eq(renderer)); + assertThat(captor.getAllValues()) + .hasSize(1) + .extracting("url") + .containsExactly(HELLO_RESOURCE_PATH); + } + + @Test + void shouldCreateGetResponseAttachment() throws Exception { + final AttachmentRenderer renderer = mock(AttachmentRenderer.class); + final AttachmentProcessor processor = mock(AttachmentProcessor.class); + + final HttpClientBuilder builder = HttpClientBuilder.create() + .addResponseInterceptorLast(new AllureHttpClient5Response(renderer, processor)); + + try (CloseableHttpClient httpClient = builder.build()) { + final HttpGet httpGet = new HttpGet(String.format(HELLO_GET_RETURN_BODY, server.port())); + httpClient.execute(httpGet, response -> { + assertThat(EntityUtils.toString(response.getEntity())).isEqualTo(BODY_STRING); + return response; + }); + } + + final ArgumentCaptor captor = ArgumentCaptor.forClass(AttachmentData.class); + verify(processor, times(1)) + .addAttachment(captor.capture(), eq(renderer)); + + assertThat(captor.getAllValues()) + .hasSize(1) + .extracting("responseCode") + .containsExactly(200); + } + + @Test + void shouldCreateGetResponseAttachmentWithEmptyBody() throws Exception { + final AttachmentRenderer renderer = mock(AttachmentRenderer.class); + final AttachmentProcessor processor = mock(AttachmentProcessor.class); + + final HttpClientBuilder builder = HttpClientBuilder.create() + .addResponseInterceptorLast(new AllureHttpClient5Response(renderer, processor)); + + try (CloseableHttpClient httpClient = builder.build()) { + final HttpGet httpGet = new HttpGet(String.format(HELLO_GET_201_NO_BODY, server.port())); + httpClient.execute(httpGet, response -> { + assertThat(EntityUtils.toString(response.getEntity())).isEqualTo(""); + return response; + }); + } + + final ArgumentCaptor captor = ArgumentCaptor.forClass(AttachmentData.class); + verify(processor, times(1)) + .addAttachment(captor.capture(), eq(renderer)); + + assertThat(captor.getAllValues()) + .hasSize(1) + .extracting("body") + .containsExactly("No body present"); + } +} diff --git a/allure-httpclient5/src/test/java/io/qameta/allure/httpclient5/AllureHttpClient5PostTest.java b/allure-httpclient5/src/test/java/io/qameta/allure/httpclient5/AllureHttpClient5PostTest.java new file mode 100644 index 000000000..7220171dc --- /dev/null +++ b/allure-httpclient5/src/test/java/io/qameta/allure/httpclient5/AllureHttpClient5PostTest.java @@ -0,0 +1,187 @@ +/* + * Copyright 2019 Qameta Software OÜ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.qameta.allure.httpclient5; + +import com.github.tomakehurst.wiremock.WireMockServer; +import io.qameta.allure.attachment.AttachmentData; +import io.qameta.allure.attachment.AttachmentProcessor; +import io.qameta.allure.attachment.AttachmentRenderer; +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.http.io.entity.StringEntity; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import java.util.Objects; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.configureFor; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * @author a-simeshin (Simeshin Artem). + */ +@SuppressWarnings({"unchecked", "PMD.JUnitTestContainsTooManyAsserts"}) +class AllureHttpClient5PostTest { + + private static final String BODY_STRING = "Hello world!"; + private static final String POST_REQUEST_BODY = "hello post request body"; + private static final String HELLO_RESOURCE_PATH = "/hello"; + private static final String HELLO_POST_RETURN_BODY = "http://localhost:%d/hello"; + private static final String HELLO_POST_201_NO_BODY = "http://localhost:%d/empty"; + + + private WireMockServer server; + + @BeforeEach + void setUp() { + server = new WireMockServer(options().dynamicPort()); + server.start(); + configureFor(server.port()); + + stubFor(post(HELLO_RESOURCE_PATH).willReturn( + aResponse() + .withHeader("Content-Type", "application/json") + .withBody(BODY_STRING) + )); + stubFor(post("/empty").willReturn( + aResponse() + .withStatus(201)) + ); + } + + @AfterEach + void tearDown() { + if (Objects.nonNull(server)) { + server.stop(); + } + } + + @Test + void smokePostShouldNotThrowThenReturnCorrectResponseMessage() { + final HttpClientBuilder builder = HttpClientBuilder.create() + .addRequestInterceptorFirst(new AllureHttpClient5Request()) + .addResponseInterceptorLast(new AllureHttpClient5Response()); + + assertDoesNotThrow(() -> { + try (CloseableHttpClient httpClient = builder.build()) { + final HttpPost httpPost = new HttpPost(String.format(HELLO_POST_RETURN_BODY, server.port())); + httpPost.setEntity(new StringEntity(POST_REQUEST_BODY, ContentType.APPLICATION_JSON)); + httpClient.execute(httpPost, response -> { + response.getCode(); + assertThat(EntityUtils.toString(response.getEntity())).isEqualTo(BODY_STRING); + return response; + }); + } + }); + } + + @Test + void shouldCreatePostRequestAttachment() throws Exception { + final AttachmentRenderer renderer = mock(AttachmentRenderer.class); + final AttachmentProcessor processor = mock(AttachmentProcessor.class); + + final HttpClientBuilder builder = HttpClientBuilder.create() + .addRequestInterceptorFirst(new AllureHttpClient5Request(renderer, processor)); + + try (CloseableHttpClient httpClient = builder.build()) { + final HttpPost httpPost = new HttpPost(String.format(HELLO_POST_RETURN_BODY, server.port())); + httpPost.setEntity(new StringEntity(POST_REQUEST_BODY, ContentType.APPLICATION_JSON)); + httpClient.execute(httpPost, response -> { + response.getCode(); + assertThat(EntityUtils.toString(response.getEntity())).isEqualTo(BODY_STRING); + return response; + }); + } + + final ArgumentCaptor captor = ArgumentCaptor.forClass(AttachmentData.class); + + verify(processor, times(1)).addAttachment(captor.capture(), eq(renderer)); + assertThat(captor.getAllValues()) + .hasSize(1) + .extracting("url") + .containsExactly(HELLO_RESOURCE_PATH); + } + + @Test + void shouldCreatePostResponseAttachment() throws Exception { + final AttachmentRenderer renderer = mock(AttachmentRenderer.class); + final AttachmentProcessor processor = mock(AttachmentProcessor.class); + + final HttpClientBuilder builder = HttpClientBuilder.create() + .addResponseInterceptorLast(new AllureHttpClient5Response(renderer, processor)); + + try (CloseableHttpClient httpClient = builder.build()) { + final HttpPost httpPost = new HttpPost(String.format(HELLO_POST_RETURN_BODY, server.port())); + httpPost.setEntity(new StringEntity(POST_REQUEST_BODY, ContentType.APPLICATION_JSON)); + httpClient.execute(httpPost, response -> { + response.getCode(); + assertThat(EntityUtils.toString(response.getEntity())).isEqualTo(BODY_STRING); + return response; + }); + } + + final ArgumentCaptor captor = ArgumentCaptor.forClass(AttachmentData.class); + verify(processor, times(1)) + .addAttachment(captor.capture(), eq(renderer)); + + assertThat(captor.getAllValues()) + .hasSize(1) + .extracting("responseCode") + .containsExactly(200); + } + + @Test + void shouldCreatePostResponseAttachmentWithEmptyBody() throws Exception { + final AttachmentRenderer renderer = mock(AttachmentRenderer.class); + final AttachmentProcessor processor = mock(AttachmentProcessor.class); + + final HttpClientBuilder builder = HttpClientBuilder.create() + .addResponseInterceptorLast(new AllureHttpClient5Response(renderer, processor)); + + try (CloseableHttpClient httpClient = builder.build()) { + final HttpPost httpPost = new HttpPost(String.format(HELLO_POST_201_NO_BODY, server.port())); + httpPost.setEntity(new StringEntity(POST_REQUEST_BODY, ContentType.APPLICATION_JSON)); + httpClient.execute(httpPost, response -> { + response.getCode(); + assertThat(EntityUtils.toString(response.getEntity())).isEqualTo(""); + return response; + }); + } + + final ArgumentCaptor captor = ArgumentCaptor.forClass(AttachmentData.class); + verify(processor, times(1)) + .addAttachment(captor.capture(), eq(renderer)); + + assertThat(captor.getAllValues()) + .hasSize(1) + .extracting("body") + .containsExactly("No body present"); + } +} diff --git a/allure-httpclient5/src/test/resources/allure.properties b/allure-httpclient5/src/test/resources/allure.properties new file mode 100644 index 000000000..9c0b0a2d7 --- /dev/null +++ b/allure-httpclient5/src/test/resources/allure.properties @@ -0,0 +1,2 @@ +allure.results.directory=build/allure-results +allure.label.epic=#project.description# diff --git a/build.gradle.kts b/build.gradle.kts index 04c4bdaef..6a04155e1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -151,6 +151,7 @@ configure(libs) { dependency("net.sourceforge.pmd:pmd-java:6.46.0") dependency("org.apache.commons:commons-lang3:3.12.0") dependency("org.apache.httpcomponents:httpclient:4.5.13") + dependency("org.apache.httpcomponents.client5:httpclient5:5.2.1") dependency("org.aspectj:aspectjrt:${assertJVersion}") dependency("org.aspectj:aspectjweaver:${assertJVersion}") dependency("org.assertj:assertj-core:3.23.1") diff --git a/settings.gradle.kts b/settings.gradle.kts index 5aa1340d2..0c027ef82 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -15,6 +15,7 @@ include("allure-descriptions-javadoc") include("allure-grpc") include("allure-hamcrest") include("allure-httpclient") +include("allure-httpclient5") include("allure-java-commons") include("allure-java-commons-test") include("allure-java-migration") From caf0c4b14facfc04ef82d2d2515694dfb44c24e5 Mon Sep 17 00:00:00 2001 From: Valery Yatsynovich Date: Thu, 3 Aug 2023 14:14:44 +0300 Subject: [PATCH 04/11] introduce adaptor for jbehave 5 (via #937) --- .github/ISSUE_TEMPLATE/bug_report.yml | 1 + .github/labeler.yml | 2 +- allure-jbehave5/build.gradle.kts | 27 ++ .../allure/jbehave5/AllureJbehave5.java | 298 +++++++++++++++++ .../allure/jbehave5/AllureJbehave5Test.java | 316 ++++++++++++++++++ .../allure/jbehave5/LoggingReporter.java | 93 ++++++ .../jbehave5/samples/BrokenStorySteps.java | 27 ++ .../jbehave5/samples/SimpleStorySteps.java | 50 +++ .../src/test/resources/allure.properties | 2 + .../test/resources/simplelogger.properties | 3 + .../src/test/resources/stories/broken.story | 2 + .../src/test/resources/stories/comment.story | 7 + .../test/resources/stories/description.story | 17 + .../src/test/resources/stories/examples.story | 11 + .../src/test/resources/stories/failed.story | 6 + .../src/test/resources/stories/given.story | 5 + .../src/test/resources/stories/long.story | 14 + .../src/test/resources/stories/multiply.story | 20 ++ .../resources/stories/precondition-a.story | 3 + .../resources/stories/precondition-b.story | 3 + .../src/test/resources/stories/simple.story | 6 + .../test/resources/stories/undefined.story | 2 + settings.gradle.kts | 1 + 23 files changed, 915 insertions(+), 1 deletion(-) create mode 100644 allure-jbehave5/build.gradle.kts create mode 100644 allure-jbehave5/src/main/java/io/qameta/allure/jbehave5/AllureJbehave5.java create mode 100644 allure-jbehave5/src/test/java/io/qameta/allure/jbehave5/AllureJbehave5Test.java create mode 100644 allure-jbehave5/src/test/java/io/qameta/allure/jbehave5/LoggingReporter.java create mode 100644 allure-jbehave5/src/test/java/io/qameta/allure/jbehave5/samples/BrokenStorySteps.java create mode 100644 allure-jbehave5/src/test/java/io/qameta/allure/jbehave5/samples/SimpleStorySteps.java create mode 100644 allure-jbehave5/src/test/resources/allure.properties create mode 100644 allure-jbehave5/src/test/resources/simplelogger.properties create mode 100644 allure-jbehave5/src/test/resources/stories/broken.story create mode 100644 allure-jbehave5/src/test/resources/stories/comment.story create mode 100644 allure-jbehave5/src/test/resources/stories/description.story create mode 100644 allure-jbehave5/src/test/resources/stories/examples.story create mode 100644 allure-jbehave5/src/test/resources/stories/failed.story create mode 100644 allure-jbehave5/src/test/resources/stories/given.story create mode 100644 allure-jbehave5/src/test/resources/stories/long.story create mode 100644 allure-jbehave5/src/test/resources/stories/multiply.story create mode 100644 allure-jbehave5/src/test/resources/stories/precondition-a.story create mode 100644 allure-jbehave5/src/test/resources/stories/precondition-b.story create mode 100644 allure-jbehave5/src/test/resources/stories/simple.story create mode 100644 allure-jbehave5/src/test/resources/stories/undefined.story diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 5fddcebc4..fb19a1b5d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -41,6 +41,7 @@ body: - allure-java-commons - allure-jax-rs - allure-jbehave + - allure-jbehave5 - allure-jsonunit - allure-junit-platform - allure-junit4 diff --git a/.github/labeler.yml b/.github/labeler.yml index c22e48e72..a419c97f9 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -38,7 +38,7 @@ - "allure-jax-rs/**" "theme:jbehave": - - "allure-jbehave/**" + - "allure-jbehave*/**" "theme:jsonunit": - "allure-jsonunit/**" diff --git a/allure-jbehave5/build.gradle.kts b/allure-jbehave5/build.gradle.kts new file mode 100644 index 000000000..256e5af6d --- /dev/null +++ b/allure-jbehave5/build.gradle.kts @@ -0,0 +1,27 @@ +description = "Allure JBehave 5 Integration" + +val jbehaveVersion = "5.1.1" + +dependencies { + api(project(":allure-java-commons")) + implementation("org.jbehave:jbehave-core:$jbehaveVersion") + testImplementation("org.assertj:assertj-core") + testImplementation("org.junit.jupiter:junit-jupiter-api") + testImplementation("org.mockito:mockito-core") + testImplementation("org.slf4j:slf4j-simple") + testImplementation(project(":allure-java-commons-test")) + testImplementation(project(":allure-junit-platform")) + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") +} + +tasks.jar { + manifest { + attributes(mapOf( + "Automatic-Module-Name" to "io.qameta.allure.jbehave5" + )) + } +} + +tasks.test { + useJUnitPlatform() +} diff --git a/allure-jbehave5/src/main/java/io/qameta/allure/jbehave5/AllureJbehave5.java b/allure-jbehave5/src/main/java/io/qameta/allure/jbehave5/AllureJbehave5.java new file mode 100644 index 000000000..3c38c214c --- /dev/null +++ b/allure-jbehave5/src/main/java/io/qameta/allure/jbehave5/AllureJbehave5.java @@ -0,0 +1,298 @@ +/* + * Copyright 2019 Qameta Software OÜ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.qameta.allure.jbehave5; + +import io.qameta.allure.Allure; +import io.qameta.allure.AllureLifecycle; +import io.qameta.allure.model.Label; +import io.qameta.allure.model.Parameter; +import io.qameta.allure.model.Stage; +import io.qameta.allure.model.Status; +import io.qameta.allure.model.StatusDetails; +import io.qameta.allure.model.StepResult; +import io.qameta.allure.model.TestResult; +import org.jbehave.core.failures.UUIDExceptionWrapper; +import org.jbehave.core.model.ExamplesTable; +import org.jbehave.core.model.Scenario; +import org.jbehave.core.model.Step; +import org.jbehave.core.model.Story; +import org.jbehave.core.reporters.NullStoryReporter; +import org.jbehave.core.steps.StepCreator; +import org.jbehave.core.steps.Timing; + +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Deque; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.stream.Collectors; + +import static io.qameta.allure.util.ResultsUtils.bytesToHex; +import static io.qameta.allure.util.ResultsUtils.createFrameworkLabel; +import static io.qameta.allure.util.ResultsUtils.createHostLabel; +import static io.qameta.allure.util.ResultsUtils.createLanguageLabel; +import static io.qameta.allure.util.ResultsUtils.createParameter; +import static io.qameta.allure.util.ResultsUtils.createStoryLabel; +import static io.qameta.allure.util.ResultsUtils.createThreadLabel; +import static io.qameta.allure.util.ResultsUtils.getMd5Digest; +import static io.qameta.allure.util.ResultsUtils.getStatus; +import static io.qameta.allure.util.ResultsUtils.getStatusDetails; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonList; +import static java.util.Comparator.comparing; + +public class AllureJbehave5 extends NullStoryReporter { + + private final AllureLifecycle lifecycle; + + private final ThreadLocal currentStory = new InheritableThreadLocal<>(); + + private final ThreadLocal currentScenario = new InheritableThreadLocal<>(); + + private final Map> scenarioUuids = new ConcurrentHashMap<>(); + + private final ThreadLocal> givenStories = ThreadLocal.withInitial(LinkedList::new); + + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + + @SuppressWarnings("unused") + public AllureJbehave5() { + this(Allure.getLifecycle()); + } + + public AllureJbehave5(final AllureLifecycle lifecycle) { + this.lifecycle = lifecycle; + } + + @Override + public void beforeStory(final Story story, final boolean givenStory) { + if (givenStory) { + givenStories.get().push(story); + } else { + currentStory.set(story); + } + } + + @Override + public void afterStory(final boolean givenStory) { + if (givenStory) { + givenStories.get().pop(); + } else { + currentStory.remove(); + } + } + + @Override + public void beforeScenario(final Scenario scenario) { + if (isGivenStory()) { + return; + } + currentScenario.set(scenario); + + if (notParameterised(scenario)) { + final String uuid = UUID.randomUUID().toString(); + usingWriteLock(() -> scenarioUuids.put(scenario, new ArrayList<>(singletonList(uuid)))); + startTestCase(uuid, scenario, emptyMap()); + } else { + usingWriteLock(() -> scenarioUuids.put(scenario, new ArrayList<>())); + } + } + + @Override + public void beforeExamples(final List steps, final ExamplesTable table) { + if (isGivenStory()) { + return; + } + final Scenario scenario = currentScenario.get(); + lock.writeLock().lock(); + try { + scenarioUuids.put(scenario, new ArrayList<>()); + } finally { + lock.writeLock().unlock(); + } + } + + @Override + public void example(final Map tableRow, final int exampleIndex) { + if (isGivenStory()) { + return; + } + final Scenario scenario = currentScenario.get(); + final String uuid = UUID.randomUUID().toString(); + usingWriteLock(() -> scenarioUuids.getOrDefault(scenario, new ArrayList<>()).add(uuid)); + startTestCase(uuid, scenario, tableRow); + } + + @Override + public void afterScenario(final Timing timing) { + if (isGivenStory()) { + return; + } + final Scenario scenario = currentScenario.get(); + usingReadLock(() -> { + final List uuids = scenarioUuids.getOrDefault(scenario, emptyList()); + uuids.forEach(this::stopTestCase); + }); + currentScenario.remove(); + usingWriteLock(() -> scenarioUuids.remove(scenario)); + } + + @Override + public void beforeStep(final Step step) { + final String stepUuid = UUID.randomUUID().toString(); + getLifecycle().startStep(stepUuid, new StepResult().setName(step.getStepAsString())); + } + + @Override + public void successful(final String step) { + getLifecycle().updateTestCase(result -> result.setStatus(Status.PASSED)); + getLifecycle().updateStep(result -> result.setStatus(Status.PASSED)); + getLifecycle().stopStep(); + } + + @Override + public void ignorable(final String step) { + getLifecycle().stopStep(); + } + + @Override + public void pending(final StepCreator.PendingStep step) { + getLifecycle().updateStep(result -> result.setStatus(Status.SKIPPED)); + getLifecycle().stopStep(); + } + + @Override + public void notPerformed(final String step) { + getLifecycle().stopStep(); + } + + @Override + public void failed(final String step, final Throwable cause) { + final Throwable unwrapped = cause instanceof UUIDExceptionWrapper + ? cause.getCause() + : cause; + + + final Status status = getStatus(unwrapped).orElse(Status.FAILED); + final StatusDetails statusDetails = getStatusDetails(unwrapped).orElseGet(StatusDetails::new); + + getLifecycle().updateStep(result -> { + result.setStatus(status); + result.setStatusDetails(statusDetails); + }); + + getLifecycle().updateTestCase(result -> { + result.setStatus(status); + result.setStatusDetails(statusDetails); + }); + + getLifecycle().stopStep(); + } + + + public AllureLifecycle getLifecycle() { + return lifecycle; + } + + protected void startTestCase(final String uuid, + final Scenario scenario, + final Map tableRow) { + final Story story = currentStory.get(); + + final String name = scenario.getTitle(); + final String fullName = String.format("%s: %s", story.getName(), name); + + final List parameters = tableRow.entrySet().stream() + .map(entry -> createParameter(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + + final List