├── .gitignore
├── README.md
├── pom.xml
├── testrail-connector
├── pom.xml
└── src
│ ├── main
│ └── java
│ │ └── com
│ │ └── nullin
│ │ └── testrail
│ │ ├── ResultStatus.java
│ │ ├── TestRailArgs.java
│ │ ├── TestRailListener.java
│ │ ├── TestRailReporter.java
│ │ ├── annotations
│ │ └── TestRailCase.java
│ │ ├── client
│ │ ├── APIClient.java
│ │ ├── ClientException.java
│ │ └── TestRailClient.java
│ │ ├── dto
│ │ ├── Case.java
│ │ ├── Milestone.java
│ │ ├── Plan.java
│ │ ├── PlanEntry.java
│ │ ├── Result.java
│ │ ├── Run.java
│ │ ├── Section.java
│ │ ├── Suite.java
│ │ └── Test.java
│ │ └── internal
│ │ ├── GenerateTestCases.java
│ │ └── TestTheListener.java
│ └── test
│ ├── java
│ └── com
│ │ └── nullin
│ │ └── testrail
│ │ └── sampleproj
│ │ ├── TestClassA.java
│ │ ├── TestClassB.java
│ │ └── pkg
│ │ └── TestClassC.java
│ └── resources
│ └── testng.xml
└── testrail-utils
├── pom.xml
└── src
└── main
└── java
└── com
└── nullin
└── testrail
├── tools
├── InvalidJiraReferenceFinder.java
└── UnstableTestsFinder.java
└── util
├── SoftReportingAssertion.java
└── TestRailScreenshotListener.java
/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | pom.xml.tag
3 | pom.xml.releaseBackup
4 | pom.xml.next
5 | release.properties
6 | .idea/
7 | deploy.py
8 | *.iml
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | testrail-integration
2 | ====================
3 |
4 | A library for connecting to TestRail and reporting results for automated tests executed using TestNG.
5 |
6 | Requirements/Assumptions
7 | ------------------------
8 |
9 | ### TestRail Configuration
10 |
11 | * Project should be setup as a single repository with baseline support.
12 | * A custom string field named `automation_id` should be added to the `Case` fields for the project.
13 | * ~~All suite names should be unique~~
14 | * Automation IDs need to be unique within a TestRail test suite
15 |
16 | ### Test Code
17 |
18 | * Annotations need to be added to test cases in Java code. See `TestRailCase` annotation
19 | * Automation IDs need to be added as first parameter of the data-driven tests
20 |
21 | ### Other Assumptions
22 |
23 | * TestRail Test Plan needs to be pre-created and associated with the required suite.
24 | * TestRail Test Plan can contain multiple plan entries (runs), but each of that run is associated with the same Suite. e.g.
25 | this allows us to separate API and UI tests into two different entries with in the same Test Plan.
26 | * If the same automation id appears twice for the same, result will only be reported once. This is an invalid configuration
27 | and should be fixed quickly.
28 |
29 | How to use `automation_id`
30 | ---------------------
31 |
32 | The custom string field `automation_id` in TestRail contains unique strings that are referenced by test code to report
33 | results. This unique string is associated to tests in multiple ways:
34 |
35 | ### Sing Test Method
36 |
37 | Simply add `TestRailCase` annotation to the test method and the listener will take care of reporting the result
38 |
39 | ```java
40 |
41 | @TestRailCase("testC1")
42 | @Test
43 | public void test1() {
44 | Assert.assertEquals(1, 1);
45 | }
46 | ```
47 |
48 | In above example, a `PASS` will be reported for test with automation id `testC1`.
49 |
50 | ### Data-driven Test
51 |
52 | No annotation needs to be added in this case. We assume that the first parameter passed into the data driven test is
53 | the unique automation id for the data-driven tests.
54 |
55 | ```java
56 |
57 | @DataProvider(name = "DP")
58 | public Object[][] getData() {
59 | return new Object[][] {
60 | {"testA2", 10, 10},
61 | {"testA3", 10, 5}
62 | };
63 | }
64 |
65 | @Test(dataProvider = "DP")
66 | public void test2(String testId, int x, int y) {
67 | Assert.assertEquals(x, y);
68 | }
69 | ```
70 |
71 | In above example, a `PASS` will be reported for test case with automation id `testA2` and a `FAILURE` for `testA3`.
72 |
73 | ### Test Method for Multiple TestRail Test Cases
74 |
75 | By adding `TestRailCase` annotation with `selfReporting` set to true, we tell the `TestRailReporter` to skip this
76 | test when processing it as it's responsible for it's own reporting of results.
77 |
78 | ```java
79 |
80 | @TestRailCase(selfReporting = true)
81 | @Test
82 | public void test5() {
83 |
84 | //do something
85 |
86 | Map result = new HashMap();
87 | result.put(TestRailReporter.KEY_STATUS, ResultStatus.PASS);
88 | TestRailReporter.getInstance().reportResult("testC3", result);
89 |
90 | //do something
91 |
92 | result.put(TestRailReporter.KEY_STATUS, ResultStatus.FAIL);
93 | result.put(TestRailReporter.KEY_THROWABLE, new IOException("Something very bad happened!!"));
94 | TestRailReporter.getInstance().reportResult("testC4", result);
95 | }
96 | ```
97 |
98 | In above example, a `PASS` will be reported for test case with automation id `testC3` and a `FAILURE` for `testC4`. This
99 | is a simplified example; in actual code, you might want to extend TestNG's `SoftAssert` and delegate the reporting to
100 | that class.
101 |
102 | Listeners and Reporters
103 | -----------------------
104 |
105 | `com.nullin.testrail.TestRailListener` implements `ITestListener` and `IConfigurationListener` interfaces from TestNG,
106 | and is responsible for reporting results to TestRail. `TestRailListener` internally uses `com.nullin.testrail.TestRailReporter`
107 | to report results to TestRail. Test code can also directly use methods in `TestRailReporter` as shown in example above.
108 |
109 | ### Listener Configuration
110 |
111 | Following system properties need to be configured during startup to configure/enable TestRail reporter
112 |
113 | * `testRail.enabled` : boolean (true|false) value to enable/disable reporting to TestRail.
114 | * `testRail.url` : Base URL to TestRail.
115 | * `testRail.username` : User to use to authenticate to TestRail instance.
116 | * `testRail.password` : Password to authenticate to TestRail instance.
117 | * `testRail.testPlanId` : ID of a pre-created Test Plan in TestRail. This is an integer that you can get via APIs. Via,
118 | TestRail UI, this is the integer part of an ID that is shown next to a Test Plan. E.g. for `R3285` id displayed in UI, it will
119 | be `3285`.
120 |
121 | ### Listener Startup
122 |
123 | During startup, we try and connect to TestRail and get the Test Plan using the user specified id. If any of this fails,
124 | we disable the `TestRailReporter`, log this condition, but do not fail the test execution.
125 |
126 | Also, during startup, we create a cache of all Test Cases and associated `automation id`s. During this process, we log
127 | all instances when we encounter the same `automation id` multiple times.
128 |
129 | ### Listener Logging
130 |
131 | We use `java.util.logging`.
132 |
133 |
134 | Extending `TestRailListener`
135 | ----------------------------
136 |
137 | We have two methods that can be overridden by extending `TestRailListener`
138 |
139 | 1. `public String getScreenshotUrl(ITestResult result)`
140 | Extend this method to return a URL for the captured screen shot for UI tests. If specified, we inline the image into TestRail result.
141 |
142 | 2. `public Map getMoreInformation(ITestResult result)`
143 | Extend this method to provide more information about the test result or the test run in general. We have used this
144 | method to specify information about environment variables, target environment etc.
145 |
146 | __NOTE__: the test class/method/parameter information is automatically logged and doesn't need to be returned by this method.
147 |
148 | Workflow
149 | --------
150 |
151 | ```
152 |
153 | - Connect and get a list of all test suites in the project and cache in a map
154 | - Also, get the list of all tests cases within the suites and create a map of the automation IDs to case IDs
155 | - If same automation id is associated with multiple case ids in same suite, raise error and quit
156 | - Get the test plan based on the specified plan name or id
157 | - For each result to be reported, get the method and it's enclosing class (multiple runs for same suite,
158 | not supported yet. Need to handle configurationIds for that to work)
159 | - get the annotations and figure out the automation id
160 | - if the test was DD, the first parameter should be automation id
161 | - from automation id, get the case id
162 | - now we have case id and test run id
163 | - report the result
164 | - Finish
165 |
166 | ```
167 |
168 | Future work
169 | --------------------
170 |
171 | - Support creating test runs with configurations
172 | - Output tests that were not reported into file w/ reason for failure
173 | - Create a new test plan (if an id isn't already specified) and associated the specified test suites
174 | - When done with all results, close the test plan (by default or based on user specified parameter)
175 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
3 | 4.0.0
4 |
5 | com.nullin
6 | testrail-integration
7 | 2.3.5-SNAPHSOT
8 | pom
9 |
10 | TestRail Integration
11 | https://github.com/nullin/testrail-integration
12 |
13 |
14 | UTF-8
15 |
16 |
17 |
18 | testrail-connector
19 | testrail-utils
20 |
21 |
22 |
23 |
24 |
25 | com.google.guava
26 | guava
27 | 18.0
28 |
29 |
30 | org.testng
31 | testng
32 | 6.8.8
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | org.apache.maven.plugins
41 | maven-surefire-plugin
42 | 2.9
43 |
44 | true
45 |
46 |
47 |
48 | org.apache.maven.plugins
49 | maven-compiler-plugin
50 | 3.3
51 |
52 | -Xlint:all
53 | -Xlint:-serial
54 | true
55 | lines,vars,source
56 | true
57 | true
58 | true
59 | 1.7
60 | 1.7
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | my-repo
69 | https://github.com/nullin/mvn-repo
70 |
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/testrail-connector/pom.xml:
--------------------------------------------------------------------------------
1 |
3 | 4.0.0
4 |
5 |
6 | com.nullin
7 | testrail-integration
8 | 2.3.5-SNAPHSOT
9 |
10 |
11 | testrail-connector
12 | jar
13 |
14 | TestRail Connector
15 | https://github.com/nullin/testrail-integration/testrail-connector
16 |
17 |
18 |
19 | com.google.guava
20 | guava
21 |
22 |
23 | org.testng
24 | testng
25 |
26 |
27 | org.apache.httpcomponents
28 | httpclient
29 | 4.3.2
30 |
31 |
32 | com.fasterxml.jackson.core
33 | jackson-core
34 | 2.4.1.1
35 |
36 |
37 | com.fasterxml.jackson.dataformat
38 | jackson-dataformat-xml
39 | 2.4.1
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/testrail-connector/src/main/java/com/nullin/testrail/ResultStatus.java:
--------------------------------------------------------------------------------
1 | package com.nullin.testrail;
2 |
3 | /**
4 | * Result status that corresponds to TestRail
5 | *
6 | * @author nullin
7 | */
8 | public enum ResultStatus {
9 | PASS,
10 | FAIL,
11 | SKIP, //shows up as blocked in TestRail
12 | UNTESTED
13 | }
14 |
--------------------------------------------------------------------------------
/testrail-connector/src/main/java/com/nullin/testrail/TestRailArgs.java:
--------------------------------------------------------------------------------
1 | package com.nullin.testrail;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 |
6 | /**
7 | * Arguments for {@link com.nullin.testrail.TestRailListener}
8 | *
9 | * @author nullin
10 | */
11 | public class TestRailArgs {
12 |
13 | //if the listener is enabled or not
14 | private Boolean enabled;
15 | //test plan id (if one already exists)
16 | private Integer testPlanId;
17 | //suite names
18 | private List suiteNames;
19 | //url to the TestRail instance
20 | private String url;
21 | //username to login to TestRail
22 | private String username;
23 | //password to login to TestRail
24 | private String password;
25 |
26 | private TestRailArgs() {}
27 |
28 | public static TestRailArgs getNewTestRailListenerArgs() {
29 | TestRailArgs args = new TestRailArgs();
30 | args.enabled = Boolean.valueOf(System.getProperty("testRail.enabled"));
31 |
32 | if (args.enabled == null || !args.enabled) {
33 | return args; //no need to process further. TestRail reporting is not enabled
34 | }
35 |
36 | String planId = System.getProperty("testRail.testPlanId");
37 | if (planId == null) {
38 | throw new IllegalArgumentException("TestRail Test Plan ID not specified");
39 | } else {
40 | try {
41 | args.testPlanId = Integer.valueOf(planId);
42 | } catch(NumberFormatException ex) {
43 | throw new IllegalArgumentException("Plan Id is not an integer as expected");
44 | }
45 | }
46 |
47 | String suiteNamesStr = System.getProperty("testRail.suiteNames");
48 | if (suiteNamesStr != null) {
49 | try {
50 | String[] suiteNamesArr = suiteNamesStr.split(",");
51 | args.suiteNames = new ArrayList();
52 | for (String suiteName : suiteNamesArr) {
53 | if (suiteName != null && !suiteName.trim().isEmpty()) {
54 | args.suiteNames.add(suiteName.trim());
55 | }
56 | }
57 |
58 | } catch(NumberFormatException ex) {
59 | throw new IllegalArgumentException("Plan Id is not an integer as expected");
60 | }
61 | }
62 |
63 | if ((args.url = System.getProperty("testRail.url")) == null) {
64 | throw new IllegalArgumentException("TestRail URL not specified (testRail.url)");
65 | }
66 |
67 | if ((args.username = System.getProperty("testRail.username")) == null) {
68 | throw new IllegalArgumentException("TestRail user not specified (testRail.username)");
69 | }
70 |
71 | if ((args.password = System.getProperty("testRail.password")) == null) {
72 | throw new IllegalArgumentException("TestRail password not specified (testRail.password)");
73 | }
74 |
75 | return args;
76 | }
77 |
78 | public Boolean getEnabled() {
79 | return enabled;
80 | }
81 |
82 | public Integer getTestPlanId() {
83 | return testPlanId;
84 | }
85 |
86 | public List getSuiteNames() {
87 | return suiteNames;
88 | }
89 |
90 | public String getUrl() {
91 | return url;
92 | }
93 |
94 | public String getUsername() {
95 | return username;
96 | }
97 |
98 | public String getPassword() {
99 | return password;
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/testrail-connector/src/main/java/com/nullin/testrail/TestRailListener.java:
--------------------------------------------------------------------------------
1 | package com.nullin.testrail;
2 |
3 | import java.lang.reflect.Method;
4 | import java.util.Arrays;
5 | import java.util.Collections;
6 | import java.util.HashMap;
7 | import java.util.LinkedHashMap;
8 | import java.util.Map;
9 | import java.util.logging.Logger;
10 |
11 | import com.nullin.testrail.annotations.TestRailCase;
12 | import org.testng.IConfigurationListener;
13 | import org.testng.ITestContext;
14 | import org.testng.ITestListener;
15 | import org.testng.ITestResult;
16 | import org.testng.annotations.Test;
17 |
18 | /**
19 | * A TestNG listener to report results to TestRail instance
20 | *
21 | * @author nullin
22 | */
23 | public class TestRailListener implements ITestListener, IConfigurationListener {
24 |
25 | private Logger logger = Logger.getLogger(TestRailListener.class.getName());
26 |
27 | private TestRailReporter reporter;
28 | private boolean enabled;
29 |
30 | /**
31 | * Store the result associated with a failed configuration here. This can
32 | * then be used when reporting the result of a skipped test to provide
33 | * additional information in TestRail
34 | */
35 | private ThreadLocal testSkipResult = new ThreadLocal();
36 |
37 | public TestRailListener() {
38 | try {
39 | reporter = TestRailReporter.getInstance();
40 | enabled = reporter.isEnabled();
41 | } catch (Throwable ex) {
42 | logger.severe("Ran into exception initializing reporter: " + ex.getMessage());
43 | ex.printStackTrace();
44 | }
45 | }
46 |
47 | /**
48 | * Reports the result for the test method to TestRail
49 | * @param result TestNG test result
50 | */
51 | private void reportResult(ITestResult result) {
52 | if (!enabled) {
53 | return; //do nothing
54 | }
55 |
56 | try {
57 | Method method = result.getMethod().getConstructorOrMethod().getMethod();
58 | String className = result.getTestClass().getName();
59 | String methodName = result.getMethod().getMethodName();
60 | String id = className + "#" + methodName;
61 | Object[] params = result.getParameters();
62 | String firstParam = null;
63 | if (params != null && params.length > 0) {
64 | id += "(" + params[0] + ")";
65 | firstParam = String.valueOf(params[0]);
66 | }
67 | int status = result.getStatus();
68 | Throwable throwable = result.getThrowable();
69 |
70 | TestRailCase trCase = method.getAnnotation(TestRailCase.class);
71 | Test test = method.getAnnotation(Test.class);
72 | String automationId;
73 | if (trCase == null) {
74 | if (null != test.dataProvider() && !test.dataProvider().isEmpty()) {
75 | if (firstParam == null) {
76 | logger.severe("Didn't find the first parameter for DD test " + id + ". Result not reported.");
77 | return; //nothing more to do
78 | }
79 | automationId = firstParam;
80 | } else {
81 | logger.severe(String.format("Test case %s is not annotated with TestRailCase annotation. " +
82 | "Result not reported", id));
83 | return; //nothing more to do
84 | }
85 | } else {
86 | automationId = trCase.value();
87 | }
88 |
89 | if (automationId == null || automationId.isEmpty()) {
90 | //case id not specified on method, check if this is a DD method
91 | if (!trCase.selfReporting()) {
92 | //self reporting test cases are responsible of reporting results on their own
93 | logger.warning("Didn't find automation id nor is the test self reporting for test " + id +
94 | ". Please check test configuration.");
95 | return; //nothing more to do
96 | } else {
97 | return; //nothing to do as the test is marked as self reporting
98 | }
99 | }
100 |
101 | Map props = new HashMap();
102 | long elapsed = (result.getEndMillis() - result.getStartMillis()) / 1000;
103 | elapsed = elapsed == 0 ? 1 : elapsed; //we can only track 1 second as the smallest unit
104 | props.put("elapsed", elapsed + "s");
105 | props.put("status", getStatus(status));
106 | props.put("throwable", throwable);
107 | //override if needed
108 | if (status == ITestResult.SKIP) {
109 | ITestResult skipResult = testSkipResult.get();
110 | if (skipResult != null) {
111 | props.put("throwable", skipResult.getThrowable());
112 | }
113 | }
114 | props.put("screenshotUrl", getScreenshotUrl(result));
115 | Map moreInfo = new LinkedHashMap();
116 | moreInfo.put("class", result.getMethod().getRealClass().getCanonicalName());
117 | moreInfo.put("method", result.getMethod().getMethodName());
118 | if (result.getParameters() != null) {
119 | moreInfo.put("parameters", Arrays.toString(result.getParameters()));
120 | }
121 | moreInfo.putAll(getMoreInformation(result));
122 | props.put("moreInfo", moreInfo);
123 | reporter.reportResult(automationId, props);
124 | } catch(Exception ex) {
125 | //only log and do nothing else
126 | logger.severe("Ran into exception " + ex.getMessage());
127 | }
128 | }
129 |
130 | public void onTestStart(ITestResult result) {
131 | //not reporting a started status
132 | }
133 |
134 | public void onTestSuccess(ITestResult result) {
135 | reportResult(result);
136 | }
137 |
138 | public void onTestFailure(ITestResult result) {
139 | reportResult(result);
140 | }
141 |
142 | public void onTestSkipped(ITestResult result) {
143 | if (result.getThrowable() != null) {
144 | //test failed, but is reported as skipped because of RetryAnalyzer.
145 | //so, changing result status and reporting this as failure instead.
146 | result.setStatus(ITestResult.FAILURE);
147 | }
148 | reportResult(result);
149 | }
150 |
151 | public void onTestFailedButWithinSuccessPercentage(ITestResult result) {
152 | //nothing here
153 | }
154 |
155 | public void onStart(ITestContext context) {
156 | //nothing here
157 | }
158 |
159 | public void onFinish(ITestContext context) {
160 | //nothing here
161 | }
162 |
163 | /**
164 | * TestRail currently doesn't support uploading screenshots via APIs. Suggested method is
165 | * to upload screenshots to another server and provide a URL in the test comments.
166 | *
167 | * This method should be overridden in a sub-class to provide the URL for the screenshot.
168 | *
169 | * @param result result of test execution
170 | * @return the URL to where the screenshot can be accessed
171 | */
172 | public String getScreenshotUrl(ITestResult result) {
173 | return null; //should be extended & overridden if needed
174 | }
175 |
176 | /**
177 | * In case, we want to log more information about the test execution, this method can be used.
178 | *
179 | * NOTE: the test class/method/parameter information is automatically logged.
180 | *
181 | * This method should be overridden in a sub-class to provide map containing information
182 | * that should be displayed for each test result in TestRail
183 | */
184 | public Map getMoreInformation(ITestResult result) {
185 | return Collections.emptyMap(); //should be extended & overridden if needed
186 | }
187 |
188 | /**
189 | * @param status TestNG specific status code
190 | * @return TestRail specific status IDs
191 | */
192 | private ResultStatus getStatus(int status) {
193 | switch (status) {
194 | case ITestResult.SUCCESS:
195 | return ResultStatus.PASS;
196 | case ITestResult.FAILURE:
197 | return ResultStatus.FAIL;
198 | case ITestResult.SUCCESS_PERCENTAGE_FAILURE:
199 | return ResultStatus.FAIL;
200 | case ITestResult.SKIP:
201 | return ResultStatus.SKIP;
202 | default:
203 | return ResultStatus.UNTESTED;
204 | }
205 | }
206 |
207 | public void onConfigurationSuccess(ITestResult iTestResult) {
208 | testSkipResult.remove();
209 | }
210 |
211 | public void onConfigurationFailure(ITestResult iTestResult) {
212 | testSkipResult.set(iTestResult);
213 | }
214 |
215 | public void onConfigurationSkip(ITestResult iTestResult) {
216 | //nothing here
217 | }
218 | }
219 |
--------------------------------------------------------------------------------
/testrail-connector/src/main/java/com/nullin/testrail/TestRailReporter.java:
--------------------------------------------------------------------------------
1 | package com.nullin.testrail;
2 |
3 | import java.io.ByteArrayOutputStream;
4 | import java.io.IOException;
5 | import java.io.PrintStream;
6 | import java.io.UnsupportedEncodingException;
7 | import java.nio.charset.Charset;
8 | import java.nio.charset.StandardCharsets;
9 | import java.util.HashMap;
10 | import java.util.HashSet;
11 | import java.util.List;
12 | import java.util.Map;
13 | import java.util.Set;
14 | import java.util.logging.Logger;
15 |
16 | import com.nullin.testrail.client.ClientException;
17 | import com.nullin.testrail.client.TestRailClient;
18 | import com.nullin.testrail.dto.Case;
19 | import com.nullin.testrail.dto.Plan;
20 | import com.nullin.testrail.dto.PlanEntry;
21 | import com.nullin.testrail.dto.Run;
22 | import com.nullin.testrail.dto.Test;
23 |
24 | /**
25 | * This class is responsible with communicating with TestRail and reporting results to it.
26 | *
27 | * It's invoked by {@link com.nullin.testrail.TestRailListener} and has methods that can be
28 | * directly invoked from the test code as well.
29 | *
30 | * @author nullin
31 | */
32 | public class TestRailReporter {
33 |
34 | private Logger logger = Logger.getLogger(TestRailReporter.class.getName());
35 | private TestRailClient client;
36 | private Map caseIdLookupMap;
37 | private Map testToRunIdMap;
38 | private Boolean enabled;
39 | private String config;
40 |
41 | //keys for the properties map that is used to pass test information into this reporter
42 | public static final String KEY_MORE_INFO = "moreInfo";
43 | public static final String KEY_SCREENSHOT_URL = "screenshotUrl";
44 | public static final String KEY_STATUS = "status";
45 | public static final String KEY_ELAPSED = "elapsed";
46 | public static final String KEY_THROWABLE = "throwable";
47 |
48 | private static class Holder {
49 | private static final TestRailReporter INSTANCE = new TestRailReporter();
50 | }
51 |
52 | public static TestRailReporter getInstance() {
53 | return Holder.INSTANCE;
54 | }
55 |
56 | /**
57 | * Initializes the required state including any required caches.
58 | *
59 | * This method is allowed to throw exception so that we can stop test execution
60 | * to fix invalid configuration before proceeding further
61 | *
62 | * @throws java.io.IOException
63 | * @throws com.nullin.testrail.client.ClientException
64 | */
65 | private TestRailReporter() {
66 | TestRailArgs args = TestRailArgs.getNewTestRailListenerArgs();
67 | enabled = args.getEnabled();
68 | enabled = enabled == null ? false : enabled;
69 |
70 | if (!enabled) {
71 | logger.info("TestRail listener is not enabled. Results will not be reported to TestRail.");
72 | return;
73 | }
74 |
75 | logger.info("TestRail listener is enabled. Configuring...");
76 | try {
77 | client = new TestRailClient(args.getUrl(), args.getUsername(), args.getPassword());
78 |
79 | //prepare the test plan and stuff
80 | Plan plan = client.getPlan(args.getTestPlanId());
81 |
82 | /*
83 | We will make an assumption that the plan can contains multiple entries, but all the
84 | entries would be associated with the same suite. This helps simplify the automated
85 | reporting of results.
86 |
87 | Another assumption is that a test with a given automation id will not re-appear twice
88 | for the same configuration set. Multiple instances of the same configuration set
89 | is possible. If same automation id and configuration set combination is repeated, the
90 | result would only be reported once.
91 | */
92 | Set suiteIdSet = new HashSet();
93 | List planEntries = plan.entries;
94 |
95 | int projectId = 0;
96 | int suiteId = 0;
97 | testToRunIdMap = new HashMap();
98 | for (PlanEntry entry : planEntries) {
99 | suiteIdSet.add(suiteId = entry.suiteId);
100 | for (Run run : entry.runs) {
101 | projectId = run.projectId;
102 | List tests = client.getTests(run.id);
103 | for (Test test : tests) {
104 | testToRunIdMap.put(test.automationId + run.config, run.id);
105 | }
106 | }
107 | }
108 |
109 | caseIdLookupMap = cacheCaseIdLookupMap(client, projectId, suiteId);
110 |
111 | //check some constraints
112 | if (suiteIdSet.size() != 1) {
113 | throw new IllegalStateException("Referenced plan " + plan.id + " has multiple test suites (" +
114 | suiteIdSet + "). This configuration is currently not supported.");
115 | }
116 |
117 | /*
118 | This should be specified when starting the JVM for test execution. It should match exactly at least
119 | one of the configurations used in the test runs. This, along with the automation id of the test
120 | are used to identify the run id.
121 | */
122 | config = System.getProperty("testRail.runConfig");
123 | } catch(Exception ex) {
124 | //wrap in a Runtime and throw again
125 | //why? because we don't want to handle it and we want
126 | //to stop the execution asap.
127 | throw new RuntimeException(ex);
128 | }
129 | }
130 |
131 | /**
132 | * Gets all the test cases associated with the test run and caches a map of the
133 | * associated automation id's to the case ids
134 | *
135 | * @param projectId
136 | * @param suiteId
137 | * @return Map with keys as automation ids and corresponding values as the case ids.
138 | */
139 | private Map cacheCaseIdLookupMap(TestRailClient client, int projectId, int suiteId)
140 | throws IOException, ClientException {
141 | List cases = client.getCases(projectId, suiteId, 0, null);
142 | Map lookupMap = new HashMap();
143 | for (Case c : cases) {
144 | if (c.automationId == null || c.automationId.isEmpty()) {
145 | continue; //ignore empty automation IDs
146 | }
147 |
148 | if (lookupMap.get(c.automationId) != null) {
149 | logger.severe("Found multiple tests cases with same automation id. " +
150 | "Case Ids " + lookupMap.get(c.automationId) + " & " + c.id);
151 | } else {
152 | lookupMap.put(c.automationId, c.id);
153 | }
154 | }
155 | return lookupMap;
156 | }
157 |
158 | /**
159 | * Reports results to testrail
160 | * @param automationId test automation id
161 | * @param properties test properties. Following values are supported:
162 | *
163 | *
{@link #KEY_ELAPSED}: elapsed time as string
164 | *
{@link #KEY_MORE_INFO}: more test information as Map
165 | *
{@link #KEY_SCREENSHOT_URL}: screen shot url as string
166 | *
{@link #KEY_STATUS}: {@link ResultStatus} of the test
167 | *
{@link #KEY_THROWABLE}: any associated {@link Throwable}
168 | *
169 | */
170 | public void reportResult(String automationId, Map properties) {
171 | if (!enabled) {
172 | return; //do nothing
173 | }
174 |
175 | ResultStatus resultStatus = (ResultStatus)properties.get(KEY_STATUS);
176 | Throwable throwable = (Throwable)properties.get(KEY_THROWABLE);
177 | String elapsed = (String)properties.get(KEY_ELAPSED);
178 | String screenshotUrl = (String)properties.get(KEY_SCREENSHOT_URL);
179 | Map moreInfo = (Map)properties.get(KEY_MORE_INFO);
180 |
181 | try {
182 | Integer caseId = caseIdLookupMap.get(automationId);
183 | if (caseId == null) {
184 | logger.severe("Didn't find case id for test with automation id " + automationId);
185 | return; //nothing more to do
186 | }
187 |
188 | StringBuilder comment = new StringBuilder("More Info (if any):\n");
189 | if (moreInfo != null && !moreInfo.isEmpty()) {
190 | for (Map.Entry entry: moreInfo.entrySet()) {
191 | comment.append("- ").append(entry.getKey()).append(" : ")
192 | .append('`').append(entry.getValue()).append("`\n");
193 | }
194 | } else {
195 | comment.append("- `none`\n");
196 | }
197 | comment.append("\n");
198 | if (screenshotUrl != null && !screenshotUrl.isEmpty()) {
199 | comment.append(".append(screenshotUrl).append(")\n\n");
200 | }
201 | if (resultStatus.equals(ResultStatus.SKIP)) {
202 | comment.append("Test skipped because of configuration method failure. " +
203 | "Related config error (if captured): \n\n");
204 | comment.append(getStackTraceAsString(throwable));
205 | }
206 | if (resultStatus.equals(ResultStatus.FAIL)) {
207 | comment.append("Test failed with following exception (if captured): \n\n");
208 | comment.append(getStackTraceAsString(throwable));
209 | }
210 |
211 | //add the result
212 | Map body = new HashMap();
213 | body.put("status_id", getStatus(resultStatus));
214 | body.put("comment", new String(comment.toString().getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8));
215 | body.put("elapsed", elapsed);
216 |
217 | Integer runId = testToRunIdMap.get(automationId + config);
218 | if (runId == null) {
219 | throw new IllegalArgumentException("Unable to find run id for test with automation id "
220 | + automationId + " and configuration set as " + config);
221 | }
222 | client.addResultForCase(runId, caseId, body);
223 | } catch(Exception ex) {
224 | //only log and do nothing else
225 | logger.severe("Ran into exception " + ex.getMessage());
226 | }
227 | }
228 |
229 | private String getStackTraceAsString(Throwable throwable) throws UnsupportedEncodingException {
230 | if (throwable == null) {
231 | return "";
232 | }
233 | ByteArrayOutputStream os = new ByteArrayOutputStream();
234 | throwable.printStackTrace(new PrintStream(os));
235 | String str = new String(os.toByteArray(), "UTF-8");
236 | str = " " + str.replace("\n", "\n ").replace("\t", " "); //better printing
237 | return str;
238 | }
239 |
240 | /**
241 | * @param status TestNG specific status code
242 | * @return TestRail specific status IDs
243 | */
244 | private int getStatus(ResultStatus status) {
245 | switch (status) {
246 | case PASS:
247 | return 1; //Passed
248 | case FAIL:
249 | return 5; //Failed
250 | case SKIP:
251 | return 2; //Blocked
252 | default:
253 | return 3; //Untested
254 | }
255 | }
256 |
257 | public boolean isEnabled() {
258 | return enabled;
259 | }
260 |
261 | }
262 |
--------------------------------------------------------------------------------
/testrail-connector/src/main/java/com/nullin/testrail/annotations/TestRailCase.java:
--------------------------------------------------------------------------------
1 | package com.nullin.testrail.annotations;
2 |
3 | import java.lang.annotation.Retention;
4 | import java.lang.annotation.RetentionPolicy;
5 |
6 | /**
7 | * Represents one or more cases in TestRail
8 | *
9 | * @author nullin
10 | */
11 | @Retention(RetentionPolicy.RUNTIME)
12 | public @interface TestRailCase {
13 |
14 | //automation id in TestRail (if any).
15 | //NOTE: "automation_id" custom field needs to be added in TestRail
16 | String value() default "";
17 | //if true, any value for automation id is ignored
18 | //lets the listener know that it should not raise a warning for no automation id
19 | boolean selfReporting() default false;
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/testrail-connector/src/main/java/com/nullin/testrail/client/APIClient.java:
--------------------------------------------------------------------------------
1 | package com.nullin.testrail.client;
2 |
3 | import java.io.ByteArrayOutputStream;
4 | import java.io.IOException;
5 | import java.io.UnsupportedEncodingException;
6 | import java.util.ArrayList;
7 | import java.util.List;
8 | import java.util.logging.Logger;
9 |
10 | import org.apache.http.Header;
11 | import org.apache.http.HttpEntity;
12 | import org.apache.http.HttpResponse;
13 | import org.apache.http.client.HttpClient;
14 | import org.apache.http.client.methods.HttpGet;
15 | import org.apache.http.client.methods.HttpPost;
16 | import org.apache.http.entity.StringEntity;
17 | import org.apache.http.impl.client.HttpClientBuilder;
18 | import org.apache.http.message.BasicHeader;
19 |
20 | /**
21 | * Client to talk to TestRail API end points
22 | *
23 | * @author nullin
24 | */
25 | public class APIClient {
26 |
27 | private HttpClient httpClient;
28 | private String url;
29 | private Logger logger = Logger.getLogger(APIClient.class.getName());
30 |
31 | public APIClient(String url, String user, String password) {
32 | try {
33 | List headerList = new ArrayList();
34 | headerList.add(new BasicHeader("Content-Type", "application/json"));
35 | headerList.add(new BasicHeader("Authorization", "Basic " + getAuthorization(user, password)));
36 |
37 | httpClient = HttpClientBuilder.create().setDefaultHeaders(headerList).build();
38 | this.url = url + "/index.php?/api/v2/";
39 | logger.fine("Created API client for " + url);
40 | } catch (Exception e) {
41 | throw new RuntimeException(e);
42 | }
43 | }
44 |
45 | private String getAuthorization(String user, String password) {
46 | try {
47 | return getBase64((user + ":" + password).getBytes("UTF-8"));
48 | } catch (UnsupportedEncodingException e) {
49 | // Not thrown
50 | }
51 |
52 | return "";
53 | }
54 |
55 | /**
56 | * @see https://github.com/gurock/testrail-api/blob/master/java/com/gurock/testrail/APIClient.java
57 | */
58 | private String getBase64(byte[] buffer) {
59 | final char[] map = {
60 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
61 | 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
62 | 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd',
63 | 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
64 | 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x',
65 | 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7',
66 | '8', '9', '+', '/'
67 | };
68 |
69 | StringBuffer sb = new StringBuffer();
70 | for (int i = 0; i < buffer.length; i++) {
71 | byte b0 = buffer[i++], b1 = 0, b2 = 0;
72 |
73 | int bytes = 3;
74 | if (i < buffer.length) {
75 | b1 = buffer[i++];
76 | if (i < buffer.length) {
77 | b2 = buffer[i];
78 | } else {
79 | bytes = 2;
80 | }
81 | } else {
82 | bytes = 1;
83 | }
84 |
85 | int total = (b0 << 16) | (b1 << 8) | b2;
86 |
87 | switch (bytes) {
88 | case 3:
89 | sb.append(map[(total >> 18) & 0x3f]);
90 | sb.append(map[(total >> 12) & 0x3f]);
91 | sb.append(map[(total >> 6) & 0x3f]);
92 | sb.append(map[total & 0x3f]);
93 | break;
94 |
95 | case 2:
96 | sb.append(map[(total >> 18) & 0x3f]);
97 | sb.append(map[(total >> 12) & 0x3f]);
98 | sb.append(map[(total >> 6) & 0x3f]);
99 | sb.append('=');
100 | break;
101 |
102 | case 1:
103 | sb.append(map[(total >> 18) & 0x3f]);
104 | sb.append(map[(total >> 12) & 0x3f]);
105 | sb.append('=');
106 | sb.append('=');
107 | break;
108 | }
109 | }
110 |
111 | return sb.toString();
112 | }
113 |
114 | public String invokeHttpGet(String uriSuffix) throws IOException, ClientException {
115 | logger.fine("Invoking " + uriSuffix);
116 | HttpGet httpGet = new HttpGet(url + uriSuffix);
117 | return consumeResponse(httpClient.execute(httpGet));
118 | }
119 |
120 | public String invokeHttpPost(String uriSuffix, String jsonData) throws IOException, ClientException {
121 | logger.fine("Invoking " + uriSuffix + " with jsonData " + jsonData);
122 | HttpPost httpPost = new HttpPost(url + uriSuffix);
123 | StringEntity reqEntity = new StringEntity(jsonData);
124 | httpPost.setEntity(reqEntity);
125 | return consumeResponse(httpClient.execute(httpPost));
126 | }
127 |
128 | public String consumeResponse(HttpResponse response) throws ClientException, IOException {
129 | int status = response.getStatusLine().getStatusCode();
130 | HttpEntity entity = response.getEntity();
131 | ByteArrayOutputStream os = new ByteArrayOutputStream();
132 | entity.writeTo(os);
133 | String content = os.toString("UTF-8");
134 |
135 | logger.fine("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
136 | logger.fine(response.getStatusLine().toString());
137 | logger.fine(content);
138 | logger.fine("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
139 |
140 | if (status != 200) {
141 | throw new ClientException("Received status code " + status + " with content '" + content + "'");
142 | }
143 |
144 | return content;
145 | }
146 |
147 | }
--------------------------------------------------------------------------------
/testrail-connector/src/main/java/com/nullin/testrail/client/ClientException.java:
--------------------------------------------------------------------------------
1 | package com.nullin.testrail.client;
2 |
3 | /**
4 | * Exception raised something goes wrong communicating with TestRail server
5 | *
6 | * @author nullin
7 | */
8 | public class ClientException extends Exception
9 | {
10 | public ClientException(String message)
11 | {
12 | super(message);
13 | }
14 |
15 | public ClientException(String message, Exception ex)
16 | {
17 | super(message, ex);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/testrail-connector/src/main/java/com/nullin/testrail/client/TestRailClient.java:
--------------------------------------------------------------------------------
1 | package com.nullin.testrail.client;
2 |
3 | import java.io.IOException;
4 | import java.util.HashMap;
5 | import java.util.List;
6 | import java.util.Map;
7 |
8 | import com.fasterxml.jackson.annotation.JsonInclude;
9 | import com.fasterxml.jackson.core.type.TypeReference;
10 | import com.fasterxml.jackson.databind.DeserializationFeature;
11 | import com.fasterxml.jackson.databind.ObjectMapper;
12 | import com.fasterxml.jackson.databind.SerializationFeature;
13 | import com.nullin.testrail.dto.Case;
14 | import com.nullin.testrail.dto.Milestone;
15 | import com.nullin.testrail.dto.Plan;
16 | import com.nullin.testrail.dto.PlanEntry;
17 | import com.nullin.testrail.dto.Result;
18 | import com.nullin.testrail.dto.Run;
19 | import com.nullin.testrail.dto.Section;
20 | import com.nullin.testrail.dto.Suite;
21 | import com.nullin.testrail.dto.Test;
22 |
23 | /**
24 | * TestRail Client for endpoints described at
25 | * {@link http://docs.gurock.com/testrail-api2/start}
26 | *
27 | * Contains methods to talk to all the various endpoints for all the
28 | * different object types within this one class. The method parameters
29 | * translate to the fields accepted as part of the request URL as well as a
30 | * map of fields that can be passed as body of the POST requests
31 | *
32 | * @author nullin
33 | */
34 | public class TestRailClient {
35 |
36 | //underlying api client
37 | private APIClient client;
38 | //(de)-serializes objects to/from json
39 | private ObjectMapper objectMapper;
40 |
41 | /**
42 | * Creates an instance of the client and setups up required state
43 | *
44 | * @param url
45 | * @param username
46 | * @param password
47 | */
48 | public TestRailClient(String url, String username, String password) {
49 | client = new APIClient(url, username, password);
50 | objectMapper = new ObjectMapper();
51 | objectMapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false);
52 | objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
53 | //TODO: should probably remove this
54 | objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
55 | }
56 |
57 | /*
58 | Plans
59 | */
60 |
61 | public Plan getPlan(int planId) throws IOException, ClientException {
62 | return objectMapper.readValue(client.invokeHttpGet("get_plan/" + planId), Plan.class);
63 | }
64 |
65 | public List getPlans(int projectId, Map filters)
66 | throws IOException, ClientException {
67 | String url = "get_plans/" + projectId;
68 | if (filters != null) {
69 | for (Map.Entry entry : filters.entrySet()) {
70 | url += "&" + entry.getKey() + "=" + entry.getValue();
71 | }
72 | }
73 | return objectMapper.readValue(client.invokeHttpGet(url), new TypeReference>(){});
74 | }
75 |
76 | public Plan addPlan(int projectId, String name, Integer milestoneId, List entries)
77 | throws IOException, ClientException {
78 | Map body = new HashMap();
79 | body.put("name", name);
80 | if (milestoneId != null) {
81 | body.put("milestone_id", String.valueOf(milestoneId));
82 | }
83 | if (entries != null) {
84 | body.put("entries", entries);
85 | }
86 | return objectMapper.readValue(
87 | client.invokeHttpPost("add_plan/" + projectId, objectMapper.writeValueAsString(body)),
88 | Plan.class);
89 | }
90 |
91 | public PlanEntry addPlanEntry(int planId, int suiteId) throws IOException, ClientException {
92 | Map body = new HashMap();
93 | body.put("suite_id", String.valueOf(suiteId));
94 | return objectMapper.readValue(
95 | client.invokeHttpPost("add_plan_entry/" + planId, objectMapper.writeValueAsString(body)),
96 | PlanEntry.class);
97 | }
98 |
99 | public Plan closePlan(int planId) throws IOException, ClientException {
100 | return objectMapper.readValue(client.invokeHttpPost("close_plan/" + planId, ""), Plan.class);
101 | }
102 |
103 | public void deletePlan(int planId) throws IOException, ClientException {
104 | client.invokeHttpPost("delete_plan/" + planId, "");
105 | }
106 |
107 | /*
108 | Results
109 | */
110 |
111 | public List getResults(int testId) throws IOException, ClientException {
112 | String url = "get_results/" + testId;
113 | return objectMapper.readValue(client.invokeHttpGet(url), new TypeReference>(){});
114 | }
115 |
116 | public List getResultsForRun(int runId, Map filters) throws IOException, ClientException {
117 | String url = "get_results_for_run/" + runId;
118 | if (filters != null) {
119 | for (Map.Entry entry : filters.entrySet()) {
120 | url += "&" + entry.getKey() + "=" + entry.getValue();
121 | }
122 | }
123 | return objectMapper.readValue(client.invokeHttpGet(url), new TypeReference>(){});
124 | }
125 |
126 | /**
127 | *
128 | * @return
129 | */
130 | public List getResultsForCase(int runId, int caseId) throws IOException, ClientException {
131 | String url = "get_results_for_case/" + runId + "/" + caseId + "&limit=10";
132 | return objectMapper.readValue(client.invokeHttpGet(url), new TypeReference>(){});
133 | }
134 |
135 | public Result addResultForCase(int runId, int caseId, int statusId, String comment)
136 | throws IOException, ClientException {
137 | String url = "add_result_for_case/" + runId + "/" + caseId;
138 | Map body = new HashMap();
139 | body.put("status_id", String.valueOf(statusId));
140 | body.put("comment", comment);
141 | return objectMapper.readValue(client.invokeHttpPost(url, objectMapper.writeValueAsString(body)), Result.class);
142 | }
143 |
144 | public Result addResultForCase(int runId, int caseId, Map properties)
145 | throws IOException, ClientException {
146 | String url = "add_result_for_case/" + runId + "/" + caseId;
147 | return objectMapper.readValue(client.invokeHttpPost(url, objectMapper.writeValueAsString(properties)), Result.class);
148 | }
149 |
150 | /*
151 | Tests
152 | */
153 |
154 | public Test getTest(int testId) throws IOException, ClientException {
155 | return objectMapper.readValue(client.invokeHttpGet("get_test/" + testId), Test.class);
156 | }
157 |
158 | public List getTests(int runId) throws IOException, ClientException {
159 | return objectMapper.readValue(client.invokeHttpGet("get_tests/" + runId), new TypeReference>(){});
160 | }
161 |
162 | /*
163 | Cases
164 | */
165 |
166 | public Case addCase(int sectionId, String title, Map fields)
167 | throws IOException, ClientException {
168 | Map body = new HashMap();
169 | body.put("title", title);
170 | if (fields != null) {
171 | body.putAll(fields);
172 | }
173 | return objectMapper.readValue(
174 | client.invokeHttpPost("add_case/" + sectionId, objectMapper.writeValueAsString(body)),
175 | Case.class);
176 | }
177 |
178 | public Case getCase(int caseId) throws IOException, ClientException {
179 | return objectMapper.readValue(client.invokeHttpGet("get_case/" + caseId), Case.class);
180 | }
181 |
182 | public List getCases(int projectId, int suiteId, int sectionId, Map filters)
183 | throws IOException, ClientException {
184 | String url = "get_cases/" + projectId;
185 | if (suiteId > 0) {
186 | url += "&suite_id=" + suiteId;
187 | }
188 | if (sectionId > 0) {
189 | url += "§ion_id=" + sectionId;
190 | }
191 | if (filters != null) {
192 | for (Map.Entry entry : filters.entrySet()) {
193 | url += "&" + entry.getKey() + "=" + entry.getValue();
194 | }
195 | }
196 | return objectMapper.readValue(client.invokeHttpGet(url), new TypeReference>(){});
197 | }
198 |
199 | /**
200 | * Needed when you need to work with custom fields that are not part of the {@link Case} class
201 | * @param projectId
202 | * @param suiteId
203 | * @param sectionId
204 | * @param filters
205 | * @return
206 | * @throws IOException
207 | * @throws ClientException
208 | */
209 | public List
106 | * @param asserts test asserts
107 | */
108 | public static void assertAll(SoftReportingAssertion... asserts) {
109 | StringBuilder allAssertsMessages = new StringBuilder();
110 | for (SoftReportingAssertion sr_assert : asserts) {
111 | if (sr_assert.hasRunAtLeastOnce()) {
112 | try {
113 | sr_assert.assertAll();
114 | } catch (AssertionError assertionError) {
115 | allAssertsMessages.append("\n").append(sr_assert.getTestAutomationId())
116 | .append("\n\t").append(assertionError.getMessage()).append("\n");
117 | }
118 | } else {
119 | //not been invoked at all
120 | Map properties = new HashMap<>();
121 | properties.put(TestRailReporter.KEY_STATUS, ResultStatus.SKIP);
122 | properties.put(TestRailReporter.KEY_THROWABLE,
123 | new AssertionError(sr_assert.getTestAutomationId() + " assert was not executed"));
124 | properties.put(TestRailReporter.KEY_MORE_INFO, sr_assert.getMoreInfo(3));
125 | TestRailReporter.getInstance().reportResult(sr_assert.getTestAutomationId(), properties);
126 | }
127 | }
128 |
129 | String allAssertMsgsStr = allAssertsMessages.toString();
130 | if (!allAssertMsgsStr.isEmpty()) {
131 | throw new AssertionError(allAssertsMessages.toString());
132 | }
133 | }
134 |
135 | @Override
136 | public void assertAll() {
137 | super.assertAll();
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/testrail-utils/src/main/java/com/nullin/testrail/util/TestRailScreenshotListener.java:
--------------------------------------------------------------------------------
1 | package com.nullin.testrail.util;
2 |
3 | import org.testng.ITestResult;
4 | import com.google.common.collect.Lists;
5 |
6 | /**
7 | * An example of TestRail listener extended to take screenshots and pass this information to.
8 | *
9 | * @author nullin
10 | */
11 | public class TestRailScreenshotListener extends com.nullin.testrail.TestRailListener {
12 |
13 | public String getScreenshotUrl(ITestResult result) {
14 | if (!Lists.newArrayList(ITestResult.FAILURE,
15 | ITestResult.SUCCESS_PERCENTAGE_FAILURE).contains(result.getStatus())) {
16 | //if the result doesn't represent a failure, no need to take screenshots
17 | return null;
18 | }
19 |
20 | try {
21 | // Do stuff to take screen shot
22 | // WebDriver driver = TWebDriverHolder.getWebDriver();
23 | // String base64 = ((TakesScreenshot) driver).getScreenshotAs(OutputType.BASE64);
24 | // byte[] decodedScreenshot = Base64.decodeBase64(base64.getBytes());
25 |
26 | String testname = getTestName(result);
27 | String timestamp = String.valueOf(System.currentTimeMillis());
28 | String fileName = timestamp + "_" + testname + ".png";
29 |
30 | return "http://some.hostname.com/" + fileName;
31 | } catch (Exception e) {
32 | //do nothing and only log the exception here
33 | }
34 | return null;
35 | }
36 |
37 | private String getTestName(ITestResult result) {
38 | String testName = result.getTestClass().getRealClass().getName() + "." + result.getMethod().getMethodName();
39 | if (result.getParameters() != null && result.getParameters().length > 0) {
40 | testName += result.getParameters()[0];
41 | }
42 |
43 | return testName;
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------