API Docs for: 0.9.1
Show:

File: lib/listener/teamCity.js

// Copyright 2014, Yahoo! Inc.
// Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms.

var AbstractListener = require('../abstractListener');
var _ = require('underscore');

/**
 * @class TeamCity listener
 * @extends AbstractListener
 * @constructor
 */
var TeamCityListener = AbstractListener.extend(

	{
		/**
		 * Gets the parent-id of the team-city root messages
		 *
		 * @method getParentId
		 * @return {string}
		 */
		getParentId: function () {
			return this.getConfiguration().parentId;
		},


		/**
		 * Unescapes teamcity messages
		 *
		 * @method _unescape
		 * @param {string} msg
		 * @return {string}
		 * @private
		 */
		_unescape: function (msg) {
			return (msg + '').
				replace(/\|\|/g, "|").
				replace(/\|'/g, "'").
				replace(/\|n/g, "\n").
				replace(/\|r/g, "\r").
				replace(/\|\[/g, "[").
				replace(/\|\]/g, "]");
		},


		/**
		 * Gets a list of relayed messages
		 *
		 * @method getRelayedMessages
		 * @return {object}
		 */
		getRelayedMessages: function () {
			return {
				'testSuiteStarted': [
					{ name: 'name', defaultValue: 'Undeclared', type: 'string' }
				],
				'testSuiteFinished': [
					{ name: 'name', defaultValue: 'Undeclared', type: 'string' }
				],
				'testStarted': [
					{ name: 'name', defaultValue: 'Undeclared', type: 'string' }
				],
				'testFinished': [
					{ name: 'name', defaultValue: 'Undeclared', type: 'string' },
					{ name: 'duration', defaultValue: '0', type: 'int' }
				],
				'testFailed': [
					{ name: 'name', defaultValue: 'Undeclared', type: 'string' },
					{ name: 'message', defaultValue: '-', type: 'string' },
					{ name: 'details', defaultValue: '-', type: 'string' }
				],
				'testError': [
					{ name: 'name', defaultValue: 'Undeclared', type: 'string' },
					{ name: 'message', defaultValue: '-', type: 'string' },
					{ name: 'details', defaultValue: '-', type: 'string' }
				],
				'testIgnored': [
					{ name: 'name', defaultValue: 'Undeclared', type: 'string' },
					{ name: 'message', defaultValue: '-', type: 'string' }
				]
			};
		},

		/**
		 * Gets the mapping from TeamCity into Preceptor format
		 *
		 * @method getMessageMapping
		 * @return {object[]}
		 */
		getMessageMapping: function () {
			return {
				testSuiteStarted: 'suiteStart',
				testSuiteFinished: 'suiteEnd',
				testStarted: 'testStart',
				testFinished: 'testFinished', // Will not be used - possibly relayed to testPassed
				testFailed: 'testFailed',
				testError: 'testError',
				testIgnored: 'testSkipped'
			};
		},

		/**
		 * Parses a string and extracts message information
		 *
		 * @method parse
		 * @param {string} text
		 * @param {object} [placeholder]
		 * @return {string}
		 */
		parse: function (text, placeholder) {

			var messageType, data,
				localText = text,
				relayedMessaged = this.getRelayedMessages(),
				messageTypes = _.keys(relayedMessaged),
				messageMapping = this.getMessageMapping(),
				match = true,
				args,
				currentId,
				idCounter = 0,
				id = +(new Date()),
				idStack = [this.getParentId()],
				idList = {};

			while(match) {

				match = localText.match(/^##teamcity\[(\w*?)\s(.*?)]$/m);

				if (match) {
					messageType = match[1];
					data = match[2];

					// Remove data from stream
					localText = localText.replace(match[0] + "\n", "");

					if (messageTypes.indexOf(messageType) != -1) {

						data = this.processPlaceholder(data, placeholder);

						// Convert a failed to an error if parameter given
						if ((messageType == 'testFailed') && this._parseAttribute(data, 'error', false)) {
							messageType = 'testError';
						}

						args = this._parseAttributes(data, relayedMessaged[messageType]);

						messageType = messageMapping[messageType];

						if (['suiteStart', 'testStart'].indexOf(messageType) !== -1) {
							currentId = id + '-' + (++idCounter);
							idList[args[0]] = currentId;
							args.unshift(idList[idStack[0]]);
							args.unshift(currentId);
							idStack.unshift(args[2]);

						} else if (['testFinished'].indexOf(messageType) !== -1) {

							if (args[0] === idStack[0]) {
								messageType = 'testPassed';
								args.shift();
								args.unshift(idList[idStack[0]]);
								idStack.shift();
							} else {
								continue;
							}

						} else if (args[0] === idStack[0]) {
							args.shift();
							args.unshift(idList[idStack[0]]);
							idStack.shift();

						} else {
							throw new Error('An error was encountered during parsing team-city code: ' + data);
						}

						this.triggerMessage(messageType, args);
					}
				}
			}

			return localText;
		},


		/**
		 * Parses all attributes and returns a list of attributes in the correct order
		 *
		 * @method _parseAttributes
		 * @param {string} attributes
		 * @param {object[]} parameters
		 * @return {*[]}
		 * @private
		 */
		_parseAttributes: function (attributes, parameters) {
			var attributeValue,
				args = [];

			_.each(parameters, function (parameter) {
				attributeValue = this._parseAttribute(attributes, parameter.name, parameter.defaultValue);
				attributeValue = this._castAttribute(attributeValue, parameter.type);

				args.push(attributeValue);
			}, this);

			return args;
		},

		/**
		 * Parses a specific attribute
		 *
		 * @method _parseAttribute
		 * @param {string} attributes
		 * @param {string} attributeName
		 * @param {*} defaultValue
		 * @return {*}
		 * @private
		 */
		_parseAttribute: function (attributes, attributeName, defaultValue) {
			var match = attributes.match(new RegExp(attributeName + "='(.*?)'\s?"));

			if (match) {
				return this._unescape(match[1]);
			} else {
				return defaultValue;
			}
		},

		/**
		 * Casts the attribute value to the correct data-type
		 *
		 * @method _castAttribute
		 * @param {string} value
		 * @param {string} type
		 * @return {*}
		 * @private
		 */
		_castAttribute: function (value, type) {

			if (value === 'null') {
				return null;
			} else if (value == 'undefined') {
				return undefined;
			}

			if (type == 'int') {
				return value * 1;
			} else if (type == 'bool') {
				return !!value;
			} else {
				return value;
			}
		}
	});

module.exports = TeamCityListener;