Skip to content

Commit 19d23ca

Browse files
Aaron Forsanderjason0x43
authored andcommitted
Add a TeamCity reporter
A new `teamcity` reporter has been added that generates TeamCity-compatible test result output. Closes #125
1 parent ee52d3d commit 19d23ca

File tree

3 files changed

+248
-0
lines changed

3 files changed

+248
-0
lines changed

lib/reporters/teamcity.js

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/**
2+
* This reporter enables Intern to interact with TeamCity.
3+
* http://confluence.jetbrains.com/display/TCD8/Build+Script+Interaction+with+TeamCity
4+
*
5+
* Portions of this module are based on functions from teamcity-service-messages:
6+
* https://github.com/pifantastic/teamcity-service-messages.
7+
*/
8+
define([], function () {
9+
var teamcity = {
10+
/** Start times for test suites. */
11+
_suiteStarts: {},
12+
13+
/** Unique ID used to track messages in a build. */
14+
_flowId: 1,
15+
16+
/**
17+
* Escape a string for TeamCity output.
18+
*
19+
* @param {string} string
20+
* @return {string}
21+
*
22+
* Based on Message.prototype.escape from teamcity-service-messages
23+
*/
24+
_escapeString: function (string) {
25+
var replacer = /['\n\r\|\[\]\u0100-\uffff]/g,
26+
map = {
27+
'\'': '|\'',
28+
'|': '||',
29+
'\n': '|n',
30+
'\r': '|r',
31+
'[': '|[',
32+
']': '|]'
33+
};
34+
35+
return string.replace(replacer, function (character) {
36+
if (character in map) {
37+
return map[character];
38+
}
39+
if (/[^\u0000-\u00ff]/.test(character)) {
40+
return '|0x' + character.charCodeAt(0).toString(16);
41+
}
42+
return '';
43+
});
44+
},
45+
46+
/**
47+
* Output a TeamCity message.
48+
*
49+
* @param {string} type
50+
* @param {Object} args
51+
*
52+
* Based on Message.prototype.formatArgs from teamcity-service-messages
53+
*/
54+
_sendMessage: function (type, args) {
55+
args.flowId = ++teamcity._flowId;
56+
args.timestamp = new Date().toISOString();
57+
args = Object.keys(args).map(function (key) {
58+
var value = args[key].toString();
59+
return key + '=' + '\'' + teamcity._escapeString(value) + '\'';
60+
}).join(' ');
61+
console.log('##teamcity[' + type + ' ' + args + ']');
62+
},
63+
64+
'/test/start': function (test) {
65+
teamcity._sendMessage('testStarted', { name: test.id });
66+
},
67+
68+
'/test/end': function (test) {
69+
teamcity._sendMessage('testFinished', {
70+
name: test.id,
71+
duration: test.timeElapsed
72+
});
73+
},
74+
75+
'/test/fail': function (test) {
76+
var message = {
77+
name: test.id,
78+
message: test.error.message
79+
};
80+
81+
if (test.error.actual && test.error.expected) {
82+
message.type = 'comparisonFailure';
83+
message.expected = test.error.expected;
84+
message.actual = test.error.actual;
85+
}
86+
87+
teamcity._sendMessage('testFailed', message);
88+
},
89+
90+
'/suite/start': function (suite) {
91+
if (suite.root) {
92+
return;
93+
}
94+
95+
var startDate = teamcity._suiteStarts[suite.id] = new Date();
96+
97+
teamcity._sendMessage('testSuiteStarted', {
98+
name: suite.id,
99+
startDate: startDate
100+
});
101+
},
102+
103+
'/suite/end': function (suite) {
104+
if (suite.root) {
105+
return;
106+
}
107+
108+
teamcity._sendMessage('testSuiteFinished', {
109+
name: suite.id,
110+
duration: new Date() - teamcity._suiteStarts[suite.id]
111+
});
112+
}
113+
};
114+
115+
return teamcity;
116+
});

tests/all.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ define([
77
'./lib/interfaces/bdd',
88
'./lib/interfaces/object',
99
'./lib/reporters/console',
10+
'dojo/has!host-node?./lib/reporters/teamcity',
1011
'dojo/has!host-node?./lib/reporters/lcov'
1112
], function () {});

tests/lib/reporters/teamcity.js

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
define([
2+
'intern!object',
3+
'intern/chai!assert',
4+
'../../../lib/Suite',
5+
'../../../lib/Test',
6+
'../../../lib/reporters/teamcity'
7+
], function (registerSuite, assert, Suite, Test, reporter) {
8+
if (typeof console !== 'object') {
9+
// IE<10 does not provide a global console object when Developer Tools is turned off
10+
return;
11+
}
12+
13+
function mockConsole(method, callback) {
14+
var oldMethod = console[method];
15+
console[method] = callback;
16+
return {
17+
remove: function () {
18+
console[method] = oldMethod;
19+
}
20+
};
21+
}
22+
23+
var messagePatterns = {
24+
'/suite/start': '^##teamcity\\[testSuiteStarted name=\'{id}\'',
25+
'/suite/end': '^##teamcity\\[testSuiteFinished name=\'{id}\' duration=\'\\d+\'',
26+
'/test/start': '^##teamcity\\[testStarted name=\'{id}\'',
27+
'/test/end': '^##teamcity\\[testFinished name=\'{id}\' duration=\'\\d+\'',
28+
'/test/fail': '^##teamcity\\[testFailed name=\'{id}\' message=\'{message}\''
29+
};
30+
31+
function testSuite(suite, topic, type) {
32+
var actualMessage,
33+
handle = mockConsole('log', function (message) {
34+
actualMessage = message;
35+
}),
36+
expected = messagePatterns[topic].replace('{id}', suite.id);
37+
38+
try {
39+
reporter[topic](suite);
40+
assert.ok(actualMessage, 'console.log should be called when the reporter ' + topic + ' method is called');
41+
assert.match(
42+
actualMessage,
43+
new RegExp(expected),
44+
'console.log should be called with a ' + type + ' message');
45+
}
46+
finally {
47+
handle.remove();
48+
}
49+
}
50+
51+
function testTest(test, topic, type) {
52+
var actualMessage,
53+
handle = mockConsole('log', function (message) {
54+
actualMessage = message;
55+
}),
56+
expected = messagePatterns[topic].replace('{id}', test.id);
57+
58+
if (test.error) {
59+
expected = expected.replace('{message}', test.error.message);
60+
}
61+
62+
try {
63+
reporter[topic](test);
64+
assert.ok(actualMessage, 'console.log should be called when the reporter ' + topic + ' method is called');
65+
assert.match(
66+
actualMessage,
67+
new RegExp(expected),
68+
'console.log should be called with a ' + type + ' message');
69+
}
70+
finally {
71+
handle.remove();
72+
}
73+
}
74+
75+
registerSuite({
76+
name: 'intern/lib/reporters/teamcity',
77+
78+
'/suite/start': function () {
79+
var suite = new Suite({ name: 'suite' });
80+
testSuite(suite, '/suite/start', 'testSuiteStarted');
81+
},
82+
83+
'/suite/end': (function () {
84+
var suite = {
85+
'successful suite': function () {
86+
var suite = new Suite({ name: 'suite', tests: [ new Test({ hasPassed: true }) ] });
87+
reporter._suiteStarts[suite.id] = 0;
88+
testSuite(suite, '/suite/end', 'testSuiteFinished');
89+
},
90+
91+
'failed suite': function () {
92+
var suite = new Suite({ name: 'suite', tests: [ new Test({ hasPassed: false }) ] });
93+
reporter._suiteStarts[suite.id] = 0;
94+
testSuite(suite, '/suite/end', 'testSuiteFinished');
95+
}
96+
};
97+
98+
return suite;
99+
})(),
100+
101+
'/test/start': function () {
102+
var test = new Test({
103+
name: 'test',
104+
timeElapsed: 123,
105+
parent: { name: 'parent', id: 'parent' },
106+
error: new Error('Oops')
107+
});
108+
testTest(test, '/test/start', 'testStarted');
109+
},
110+
111+
'/test/end': function () {
112+
var test = new Test({
113+
name: 'test',
114+
timeElapsed: 123,
115+
parent: { name: 'parent', id: 'parent' },
116+
error: new Error('Oops')
117+
});
118+
testTest(test, '/test/end', 'testFinished');
119+
},
120+
121+
'/test/fail': function () {
122+
var test = new Test({
123+
name: 'test',
124+
timeElapsed: 123,
125+
parent: { name: 'parent', id: 'parent' },
126+
error: new Error('Oops')
127+
});
128+
testTest(test, '/test/fail', 'testFailed');
129+
}
130+
});
131+
});

0 commit comments

Comments
 (0)