View Javadoc
1   /*
2    * Copyright 2023 Adrian Herscu
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package dev.aherscu.qa.testrail.reporter;
18  
19  import static dev.aherscu.qa.testing.utils.ObjectMapperUtils.*;
20  import static dev.aherscu.qa.testing.utils.UriUtils.*;
21  import static java.text.MessageFormat.*;
22  import static java.util.Collections.*;
23  import static java.util.Objects.*;
24  import static org.apache.commons.io.FileUtils.*;
25  
26  import java.io.*;
27  import java.net.*;
28  import java.util.*;
29  
30  import org.apache.commons.io.*;
31  import org.apache.commons.io.filefilter.*;
32  import org.testng.xml.*;
33  
34  import com.fasterxml.jackson.annotation.*;
35  import com.google.common.collect.*;
36  import com.samskivert.mustache.*;
37  import com.tngtech.jgiven.report.model.*;
38  
39  import dev.aherscu.qa.jgiven.reporter.*;
40  import lombok.*;
41  import lombok.experimental.*;
42  import lombok.extern.slf4j.*;
43  
44  /**
45   * Per method test reporter uploading results with screenshot attachments to
46   * TestRail.
47   *
48   * @see #with(XmlSuite) support for TestNG Listener configuration
49   */
50  @SuperBuilder(toBuilder = true)
51  @NoArgsConstructor(force = true)
52  @Slf4j
53  @ToString(callSuper = true)
54  public class TestRailReporter extends QaJGivenPerMethodReporter {
55      @Getter
56      @AllArgsConstructor
57      private enum Status {
58          SUCCESS(1), FAILED(5);
59  
60          final int id;
61  
62          static Status from(final ExecutionStatus status) {
63              return status == ExecutionStatus.SUCCESS ? SUCCESS : FAILED;
64          }
65      }
66  
67      @JsonIgnoreProperties(ignoreUnknown = true)
68      static class AttachScreenshotsResponse {
69          @JsonProperty("attachment_id")
70          String id;
71      }
72  
73      @JsonIgnoreProperties(ignoreUnknown = true)
74      static class ResultForCaseResponse {
75          @JsonProperty("id")
76          String id;
77          @JsonProperty("test_id")
78          String testId;
79      }
80  
81      // FIXME use .mustache extension
82      @SuppressWarnings("hiding")
83      public static final String DEFAULT_TEMPLATE_RESOURCE =
84          "/permethod-reporter.testrail";
85      private final URI          testRailUrl;
86      private final String       testRailRunId;
87  
88      private static Collection<File> listScreenshots(final File directory) {
89          return directory.exists()
90              ? listFiles(directory, new SuffixFileFilter(".png"), null)
91              : emptyList();
92      }
93  
94      private static TestRailClient testRailClient(final URI testRailUrl) {
95          val testRailClient = new TestRailClient(testRailUrl.toString());
96          testRailClient.setUser(usernameFrom(testRailUrl));
97          testRailClient.setPassword(passwordFrom(testRailUrl));
98          return testRailClient;
99      }
100 
101     @Override
102     protected Mustache.Compiler compiler() {
103         return Mustache.compiler().withEscaper(Escapers.NONE);
104     }
105 
106     @Override
107     protected TestRailReportModel reportModel(File targetReportFile) {
108         return TestRailReportModel.builder()
109             .outputDirectory(outputDirectory)
110             .targetReportFile(targetReportFile)
111             .build();
112     }
113 
114     /**
115      * Builds a new reporter configured with additional TestNG XML suite
116      * parameters:
117      * <dl>
118      * <dt>testRailRunId</dt>
119      * <dd>the TestRail Run to report to</dd>
120      * <dt>testRailUrl</dt>
121      * <dd>the TestRail location</dd>
122      * </dl>
123      *
124      * @param xmlSuite
125      *            TestNG XML suite
126      * @return reporter configured
127      */
128     @Override
129     protected TestRailReporter with(final XmlSuite xmlSuite) {
130         return ((TestRailReporter) super.with(xmlSuite))
131             .toBuilder()
132             .templateResource(templateResourceParamFrom(xmlSuite,
133                 DEFAULT_TEMPLATE_RESOURCE))
134             .testRailRunId(
135                 requireNonNull(xmlSuite.getParameter("testRailRunId"),
136                     "testRailRunId parameter not found in current TestNG XML"))
137             .testRailUrl(URI.create(
138                 requireNonNull(xmlSuite.getParameter("testRailUrl"),
139                     "testRailUrl parameter not found in current TestNG XML")))
140             .build();
141     }
142 
143     @Override
144     protected void reportGenerated(
145         final ScenarioModel scenarioModel,
146         final File reportFile) {
147         super.reportGenerated(scenarioModel, reportFile);
148 
149         val testCaseId = readAttributesOf(reportFile)
150             // TODO make it configurable
151             .get("dev.aherscu.qa.jgiven.commons.tags.Reference");
152 
153         try {
154             val addResultForCaseResponse =
155                 addResultForCase(scenarioModel, reportFile, testCaseId);
156             log.debug(
157                 "reported result id {} for case {} on run {} to {}/index.php?/tests/view/{}",
158                 addResultForCaseResponse.id,
159                 testCaseId, testRailRunId, testRailUrl,
160                 addResultForCaseResponse.testId);
161 
162             listScreenshots(
163                 new File(outputDirectory, targetNameFor(scenarioModel)))
164                 .forEach(file -> {
165                     log.trace("attaching {}", file);
166                     val attachScreenshotsResponse =
167                         addAttachmentToResult(addResultForCaseResponse.id,
168                             file);
169                     log.debug("attached {}", attachScreenshotsResponse.id);
170                 });
171 
172         } catch (final Exception e) {
173             log.error("failed to report case {} on run {} -> {}",
174                 testCaseId, testRailRunId, e.toString());
175         }
176     }
177 
178     private AttachScreenshotsResponse addAttachmentToResult(
179         final String resultId,
180         final File screenshot) {
181         return fromJson(testRailClient(testRailUrl)
182             .sendPost(format("add_attachment_to_result/{0}",
183                 resultId),
184                 screenshot.toString())
185             .toString(),
186             AttachScreenshotsResponse.class);
187     }
188 
189     private ResultForCaseResponse addResultForCase(
190         final ScenarioModel scenarioModel,
191         final File reportFile, final String testCaseId) throws IOException {
192         try (val fileReader = new FileReader(reportFile)) {
193             return fromJson(testRailClient(testRailUrl)
194                 .sendPost(format("add_result_for_case/{0}/{1}",
195                     testRailRunId,
196                     testCaseId),
197                     ImmutableMap.builder()
198                         .put("status_id",
199                             Status.from(scenarioModel.getExecutionStatus()).id)
200                         .put("comment",
201                             IOUtils.toString(fileReader))
202                         .build())
203                 .toString(),
204                 ResultForCaseResponse.class);
205         }
206     }
207 }