-
Notifications
You must be signed in to change notification settings - Fork 29
/
index.js
247 lines (220 loc) · 7.65 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
const core = require("@actions/core");
const github = require("@actions/github");
const glob = require("@actions/glob");
const parser = require("xml2js");
const fs = require("fs");
const path = require("path");
(async () => {
try {
const inputPath = core.getInput("path");
const includeSummary = core.getInput("includeSummary");
const numFailures = core.getInput("numFailures");
const accessToken = core.getInput("access-token");
const name = core.getInput("name");
const globber = await glob.create(inputPath, {
followSymbolicLinks: false,
});
let testSummary = new TestSummary();
testSummary.maxNumFailures = numFailures;
for await (const file of globber.globGenerator()) {
const testsuites = await readTestSuites(file);
for await (const testsuite of testsuites) {
await testSummary.handleTestSuite(testsuite, file);
}
}
const annotation_level = testSummary.isFailedOrErrored() ? "failure" : "notice";
const annotation = {
path: "test",
start_line: 0,
end_line: 0,
start_column: 0,
end_column: 0,
annotation_level,
message: testSummary.toFormattedMessage(),
};
const conclusion = testSummary.annotations.length === 0 ? "success" : "failure";
testSummary.annotations = [annotation, ...testSummary.annotations];
const pullRequest = github.context.payload.pull_request;
const link = (pullRequest && pullRequest.html_url) || github.context.ref;
const status = "completed";
const head_sha =
(pullRequest && pullRequest.head.sha) || github.context.sha;
const annotations = testSummary.annotations;
const createCheckRequest = {
...github.context.repo,
name,
head_sha,
status,
conclusion,
output: {
title: name,
summary: testSummary.toFormattedMessage(),
annotations,
},
};
const octokit = new github.GitHub(accessToken);
await octokit.checks.create(createCheckRequest);
} catch (error) {
core.setFailed(error.message);
}
})();
class TestSummary {
maxNumFailures = -1;
numTests = 0;
numSkipped = 0;
numFailed = 0;
numErrored = 0;
testDuration = 0;
annotations = [];
async handleTestSuite(testsuite, file) {
if (testsuite.$) {
this.testDuration += Number(testsuite.$.time) || 0;
this.numTests += Number(testsuite.$.tests) || 0;
this.numErrored += Number(testsuite.$.errors) || 0;
this.numFailed += Number(testsuite.$.failures) || 0;
this.numSkipped += Number(testsuite.$.skipped) || 0;
}
if (testsuite.testcase) {
for await (const testcase of testsuite.testcase) {
await this.handleTestCase(testcase, file);
}
}
}
async handleTestCase(testcase, file) {
if (!testcase.failure) {
return;
}
if (this.maxNumFailures !== -1 && this.annotations.length >= this.maxNumFailures) {
return;
}
const {filePath, line} = await module.exports.findTestLocation(file, testcase);
this.annotations.push({
path: filePath,
start_line: line,
end_line: line,
start_column: 0,
end_column: 0,
annotation_level: "failure",
title: testcase.$.name,
message: TestSummary.formatFailureMessage(testcase),
raw_details: testcase.failure[0]._ || 'No details'
});
}
static formatFailureMessage(testcase) {
const failure = testcase.failure[0];
if (failure.$ && failure.$.message) {
return `Junit test ${testcase.$.name} failed ${failure.$.message}`;
} else {
return `Junit test ${testcase.$.name} failed`;
}
}
isFailedOrErrored() {
return this.numFailed > 0 || this.numErrored > 0;
}
toFormattedMessage() {
return `Junit Results ran ${this.numTests} in ${this.testDuration} seconds ${this.numErrored} Errored, ${this.numFailed} Failed, ${this.numSkipped} Skipped`;
}
}
/**
* Read JUnit XML report and return the list of all test suites in JSON format.
*
* XML children are mapped to JSON array of objects (ie b in <a><b></b></a> is mapped to
* a.b[0]). XML attributes are mapped to a `$` JSON element (ie <a attr="value" /> is mapped to
* a.$.attr). Tag content are mapped to a `_` JSON element (ie <a>content</a> is mapped to a._).
*
* The `testsuite` are directly the first accessible object in the returned array. Hence, the
* expected schema is:
*
* ```
* [
* {
* // A testsuite
* $: {
* name: 'value',
* // tests, skipped, failures, error, time, ...
* },
* testcase: [
* {
* // A testcase
* $: {
* name: 'value',
* // classname, time, ...
* },
* failure: [{
* $: {
* message: 'value',
* // type, ...
* },
* _: 'failure body'
* }]
* }
* ]
* }
* ]
* ```
*
* @param file filename of the XML to read from
* @returns {Promise<[JSON]>} list of test suites in JSON
*/
async function readTestSuites(file) {
const data = await fs.promises.readFile(file);
const json = await parser.parseStringPromise(data);
if (json.testsuites) {
return json.testsuites.testsuite
}
return [json.testsuite];
}
/**
* Find the file and the line of the test method that is specified in the given test case.
*
* The JUnit test report files are expected to be inside the project repository, next to the sources.
* This is true for reports generated by Gradle, maven surefire and maven failsafe.
*
* The strategy to find the file of the failing test is to look for candidate files having the same
* name that the failing class' canonical name (with '.' replaced by '/'). Then, given the above
* expectation, the nearest candidate to the test report file is selected.
*
* @param testReportFile the file path of the JUnit test report
* @param testcase the JSON test case in the JUnit report
* @returns {Promise<{line: number, filePath: string}>} the line and the file of the failing test method.
*/
async function findTestLocation(testReportFile, testcase) {
const klass = testcase.$.classname.replace(/$.*/g, "").replace(/\./g, "/");
// Search in src directories because some files having the same name of the class may have been
// generated in the build folder.
const filePathGlob = `**/src/**/${klass}.*`;
const filePaths = await glob.create(filePathGlob, {
followSymbolicLinks: false,
});
let bestFilePath;
let bestRelativePathLength = -1;
for await (const candidateFile of filePaths.globGenerator()) {
let candidateRelativeLength = path.relative(testReportFile, candidateFile)
.length;
if (!bestFilePath || candidateRelativeLength < bestRelativePathLength) {
bestFilePath = candidateFile;
bestRelativePathLength = candidateRelativeLength;
}
}
let line = 0;
if (bestFilePath !== undefined) {
const file = await fs.promises.readFile(bestFilePath, {
encoding: "utf-8",
});
//TODO: make this better won't deal with methods with arguments etc
const lines = file.split("\n");
for (let i = 0; i < lines.length; i++) {
if (lines[i].indexOf(testcase.$.name) >= 0) {
line = i + 1; // +1 because the first line is 1 not 0
break;
}
}
} else {
//fall back so see something
bestFilePath = `${klass}`;
}
return { filePath: bestFilePath, line };
}
module.exports.findTestLocation = findTestLocation;
module.exports.readTestSuites = readTestSuites;
module.exports.TestSummary = TestSummary;