forked from AdmitadSDK/mocha-testlink-reporter
-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
282 lines (247 loc) · 9.19 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
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
'use strict'
const TestLink = require('testlink-xmlrpc')
const { ExecutionStatus } = require('testlink-xmlrpc/lib/constants')
const mocha = require('mocha')
const {
EVENT_RUN_BEGIN,
EVENT_TEST_FAIL,
EVENT_TEST_PASS,
EVENT_SUITE_END
} = mocha.Runner.constants
class TestLinkReporter extends mocha.reporters.Spec {
constructor (runner, options) {
super(runner, options)
const reporterOptions = options.reporterOptions
if (!this.parseReporterOptions(reporterOptions)) {
console.log('TestLink reporter is disabled due to insufficient configuration')
return
}
this.testlink = this.establishTestLinkConnection(reporterOptions)
// The chain is used to report test statuses in the order they become available during execution
// An alternative would be to collect the statuses and publish them in one go at the end, but
// they would be lost if the execution is aborted or the system crashes
this.promiseChain = this.testlink.checkDevKey().catch(console.error)
runner
.once(EVENT_RUN_BEGIN, () => {
this.lookupTestProject(reporterOptions)
this.createTestPlan(reporterOptions)
this.createBuild(reporterOptions)
this.promiseChain = this.promiseChain.catch(console.error)
})
.on(EVENT_SUITE_END, suite => // tests marked as skipped are ignored
!suite.isPending() && this.publishTestResults(suite.title, caseId => this.suiteOptions(caseId, suite))
)
.on(EVENT_TEST_PASS, test =>
this.publishTestResults(test.title, caseId => this.tcOptions(caseId, test.duration))
)
.on(EVENT_TEST_FAIL, (test, err) =>
this.publishTestResults(test.title, caseId => this.tcOptions(caseId, test.duration, err))
)
}
/**
* Updates the TestLink status of each case id mentioned in the supplied title.
* The latest active versions of the tests are added to the test plan.
* @param {string} title of the test to extract case ids from
* @param {Function} optionsGen returns options based on caseId
*/
publishTestResults (title, optionsGen) {
for (const caseId of this.titleToCaseIds(title)) {
this.promiseChain = this.promiseChain
.then(async () => {
const options = optionsGen(caseId)
const version = await this.getLastActiveTestCaseVersion(options.testcaseexternalid)
await this.testlink.addTestCaseToTestPlan({
testprojectid: this.testProject.id,
testplanid: this.testplanid,
testcaseexternalid: options.testcaseexternalid,
version
})
return this.testlink.reportTCResult(options)
})
.catch(console.error)
}
}
/**
* @param {string} testcaseexternalid e.g. XPJ-1
* @returns {int} latest version of the test case that is in active state.
*/
async getLastActiveTestCaseVersion (testcaseexternalid) {
let tc = await this.testlink.getTestCase({ testcaseexternalid })
let version = parseInt(tc[0].version)
while (version > 1 && tc[0].active !== '1') {
version--
tc = await this.testlink.getTestCase({ testcaseexternalid, version })
}
return version
}
/**
* Builds a connection object based on the parameters specified in the command line.
* @param {object} options passed in --reporter-options command-line parameter
* @returns {TestLink} object
*/
establishTestLinkConnection (options) {
const url = new URL(options.URL)
return new TestLink({
host: url.hostname,
port: url.port,
secure: url.protocol === 'https:',
apiKey: options.apiKey
})
}
/**
* Infers missing configuration options from the environment variables
* @param {object} [options] the reporter options
* @returns false if the configuration is incomplete
*/
parseReporterOptions (options) {
options = options || {}
options.URL = options.URL || process.env.TESTLINK_URL
options.apiKey = options.apiKey || process.env.TESTLINK_API_KEY
options.prefix = options.prefix || process.env.TESTLINK_PREFIX
options.buildname = options.buildname || process.env.TESTLINK_BUILD
options.buildid = options.buildid || process.env.TESTLINK_BUILD_ID
options.testplanname = options.testplanname || process.env.TESTLINK_PLAN
options.testplanid = options.testplanid || process.env.TESTLINK_PLAN_ID
for (const opt of ['URL', 'apiKey', 'prefix']) {
if (!options[opt]) {
console.error(`Missing ${opt} option`)
return false
}
}
if ((options['buildid'] || options['buildname']) && !(options['testplanid'] || options['testplanname'])) {
console.error('Missing testplanid or testplanname option')
return false
}
return true
}
/**
* Creates a test plan if it doesn't exist.
* @param {object} options reporter options
*/
createTestPlan (options) {
if (options['testplanid']) {
this.testplanid = options['testplanid']
} else {
this.promiseChain = this.promiseChain.then(async () => {
const testplanname = options['testplanname'] ? options['testplanname'] : `autoplan ${new Date().toISOString()}`
const testPlans = await this.testlink.getProjectTestPlans({
testprojectid: this.testProject.id
})
let testPlan = Array.isArray(testPlans) && testPlans.find(p => p.name === testplanname)
if (!testPlan) {
const res = await this.testlink.createTestPlan({
testplanname,
prefix: options.prefix
})
testPlan = res[0]
console.log(`A new test plan '${testplanname}' with id ${testPlan.id} was created in TestLink`)
}
this.testplanid = testPlan.id
})
}
}
/**
* Creates a build if it doesn't exist. Assumes that a test plan already exists
* @param {object} options reporter options
*/
createBuild (options) {
if (options['buildid']) {
this.buildid = options.buildid
} else {
this.promiseChain = this.promiseChain.then(async () => {
const buildname = options['buildname'] ? options['buildname'] : 'autobuild'
const builds = await this.testlink.getBuildsForTestPlan({
testplanid: this.testplanid
})
let build = Array.isArray(builds) && builds.find(b => b.name === buildname)
if (!build) {
const res = await this.testlink.createBuild({
testplanid: this.testplanid,
buildname,
buildnotes: '',
active: true,
open: true,
releasedate: ''
})
build = res[0]
console.log(`A new build '${buildname}' with id ${build.id} was created in TestLink`)
}
this.buildid = build.id
})
}
}
/**
* Look up the test project from its prefix
* @param {object} options reporter options
*/
lookupTestProject (options) {
this.promiseChain = this.promiseChain.then(async () => {
const projects = await this.testlink.getProjects()
this.testProject = projects.find(p => p.prefix === options['prefix'])
if (!this.testProject) {
throw Error(`No project with prefix ${options['prefix']} was found`)
}
})
}
/**
* Extracts TestLink ids of the form [XPJ-112]. A single case (title) may have several ids specified.
* @param {string} title of the test case
* @returns {Array} of case ids
*/
titleToCaseIds (title) {
const caseIds = []
const re = /\[(\w+-\d+)\]/g
for (const match of title.matchAll(re)) {
caseIds.push(match[1])
}
return caseIds
}
/**
* Generates the options for a TestLink case with steps that are mapped to a mocha suite with tests
* @param {string} testcaseexternalid e.g. XPJ-112
* @param {Suite} suite that is mapped to a TestLink test case
* @returns {object} with test suite options
*/
suiteOptions (testcaseexternalid, suite) {
// the suite is failed if any of its tests failed
const status = suite.tests.some(t => t.isFailed()) ? ExecutionStatus.FAILED : ExecutionStatus.PASSED
// the sum total duration of the constituent tests
const execduration = suite.tests.reduce((acc, t) => acc + t.duration, 0) / 60000
// collect the statuses of the constituent tests
const steps = suite.tests.map((t, idx) => {
return {
step_number: idx + 1,
result: t.isPending() ? ExecutionStatus.NOT_RUN : (t.isPassed() ? ExecutionStatus.PASSED : ExecutionStatus.FAILED),
notes: t.err ? t.err.stack : '' }
})
return {
testcaseexternalid,
testplanid: this.testplanid,
status,
buildid: this.buildid,
execduration,
steps
}
}
/**
* Call this function only within EVENT_TEST_PASS and EVENT_TEST_FAIL handlers
* @param {string} testcaseexternalid e.g. XPJ-112
* @param {int} duration test.duration
* @param {Error} err object
* @returns {object} with test case options
*/
tcOptions (testcaseexternalid, duration, err) {
const status = err ? ExecutionStatus.FAILED : ExecutionStatus.PASSED
const notes = err ? err.stack : ''
return {
testcaseexternalid,
testplanid: this.testplanid,
status,
buildid: this.buildid,
execduration: duration / 60000,
notes,
steps: []
}
}
}
module.exports = TestLinkReporter