'use strict'; /** * From mocha-teamcity-reporter * The MIT License * Copyright (c) 2016 Jamie Sherriff */ const TEST_IGNORED = `##teamcity[testIgnored name='%s' message='%s' flowId='%s']`; const SUITE_START = `##teamcity[testSuiteStarted name='%s' flowId='%s']`; const SUITE_END = `##teamcity[testSuiteFinished name='%s' duration='%s' flowId='%s']`; const TEST_START = `##teamcity[testStarted name='%s' captureStandardOutput='true' flowId='%s']`; const TEST_FAILED = `##teamcity[testFailed name='%s' message='%s' details='%s' captureStandardOutput='true' flowId='%s']`; const TEST_END = `##teamcity[testFinished name='%s' duration='%s' flowId='%s']`; const BLOCK_OPENED = `##teamcity[blockOpened name='%s' flowId='%s']`; const BLOCK_CLOSED = `##teamcity[blockClosed name='%s' flowId='%s']`; /** * from teamcity-service-messages * Copyright (c) 2013 Aaron Forsander * * Escape string for TeamCity output. * @see https://confluence.jetbrains.com/display/TCD65/Build+Script+Interaction+with+TeamCity#BuildScriptInteractionwithTeamCity-servMsgsServiceMessages */ const format = require('format-util'); function tcEscape(str) { if (!str) { return ''; } return str .toString() .replace(/\x1B.*?m/g, '') // eslint-disable-line no-control-regex .replace(/\|/g, '||') .replace(/\n/g, '|n') .replace(/\r/g, '|r') .replace(/\[/g, '|[') .replace(/\]/g, '|]') .replace(/\u0085/g, '|x') // next line .replace(/\u2028/g, '|l') // line separator .replace(/\u2029/g, '|p') // paragraph separator .replace(/'/g, '|\''); } function formatMessage() { let formattedArguments = []; const args = Array.prototype.slice.call(arguments, 0); // Format all arguments for TC display (it escapes using the pipe char). let tcMessage = args.shift(); args.forEach((param) => { formattedArguments.push(tcEscape(param)); }); formattedArguments.unshift(tcMessage); return format(...formattedArguments); } const resolve = require('path').resolve; const SourceMapConsumer = require('source-map').SourceMapConsumer; const _ = require('lodash'); const PathUtils = require('karma/lib/utils/path-utils'); /** * From karma * The MIT License * Copyright (C) 2011-2019 Google, Inc. */ // This ErrorFormatter is copied from standard karma's, // but without warning in case of failed original location finding function createErrorFormatter(config, emitter, SourceMapConsumer) { const basePath = config.basePath; const urlRoot = config.urlRoot === '/' ? '' : (config.urlRoot || ''); let lastServedFiles = []; emitter.on('file_list_modified', (files) => { lastServedFiles = files.served; }); const URL_REGEXP = new RegExp('(?:https?:\\/\\/' + config.hostname + '(?:\\:' + config.port + ')?' + ')?\\/?' + urlRoot + '\\/?' + '(base/|absolute)' + // prefix, including slash for base/ to create relative paths. '((?:[A-z]\\:)?[^\\?\\s\\:]*)' + // path '(\\?\\w*)?' + // sha '(\\:(\\d+))?' + // line '(\\:(\\d+))?' + // column '', 'g'); const cache = new WeakMap(); function getSourceMapConsumer(sourceMap) { if (!cache.has(sourceMap)) { cache.set(sourceMap, new SourceMapConsumer(sourceMap)); } return cache.get(sourceMap) } return function (input, indentation) { indentation = _.isString(indentation) ? indentation : ''; if (_.isError(input)) { input = input.message; } else if (_.isEmpty(input)) { input = ''; } else if (!_.isString(input)) { input = JSON.stringify(input, null, indentation); } let msg = input.replace(URL_REGEXP, function (stackTracePath, prefix, path, __, ___, line, ____, column) { const normalizedPath = prefix === 'base/' ? `${basePath}/${path}` : path; const file = lastServedFiles.find((file) => file.path === normalizedPath); if (file && file.sourceMap && line) { line = +line; column = +column; // When no column is given and we default to 0, it doesn't make sense to only search for smaller // or equal columns in the sourcemap, let's search for equal or greater columns. const bias = column ? SourceMapConsumer.GREATEST_LOWER_BOUND : SourceMapConsumer.LEAST_UPPER_BOUND; try { const zeroBasedColumn = Math.max(0, (column || 1) - 1); const original = getSourceMapConsumer(file.sourceMap).originalPositionFor({line, column: zeroBasedColumn, bias}); // If there is no original position/source for the current stack trace path, then // we return early with the formatted generated position. This handles the case of // generated code which does not map to anything, see Case 1 of the source-map spec. // https://sourcemaps.info/spec.html. if (original.source === null) { return PathUtils.formatPathMapping(path, line, column) } // Source maps often only have a local file name, resolve to turn into a full path if // the path is not absolute yet. const oneBasedOriginalColumn = original.column == null ? original.column : original.column + 1; return `${PathUtils.formatPathMapping(resolve(path, original.source), original.line, oneBasedOriginalColumn)} <- ${PathUtils.formatPathMapping(path, line, column)}` } catch (e) { // do nothing } } return PathUtils.formatPathMapping(path, line, column) || prefix }); if (indentation) { msg = indentation + msg.replace(/\n/g, '\n' + indentation); } return config.formatError ? config.formatError(msg) : msg + '\n' } } /** * From karma-teamcity-reporter. * The MIT License * Copyright (C) 2011-2013 Vojta Jína and contributors */ const hashString = function (s) { let hash = 0; let i; let chr; let len; if (s === 0) return hash for (i = 0, len = s.length; i < len; i++) { chr = s.charCodeAt(i); hash = ((hash << 5) - hash) + chr; hash |= 0; } return hash }; // This reporter extends karma-teamcity-reporter // It is necessary, because karma-teamcity-reporter can't write browser's log // And additionally it overrides flushLogs, because flushLogs adds redundant spaces after some messages const KarmaKotlinReporter = function (baseReporterDecorator, config, emitter) { baseReporterDecorator(this); const self = this; const formatError = createErrorFormatter(config, emitter, SourceMapConsumer); const END_KOTLIN_TEST = "'--END_KOTLIN_TEST--"; const reporter = this; const initializeBrowser = function (browser) { reporter.browserResults[browser.id] = { name: browser.name, log: [], consoleCollector: [], consoleResultCollector: [], lastSuite: null, flowId: 'karmaTC' + hashString(browser.name + ((new Date()).getTime())) + browser.id }; }; this.onRunStart = function (browsers) { this.write(formatMessage(BLOCK_OPENED, 'JavaScript Unit Tests')); this.browserResults = {}; // Support Karma 0.10 (TODO: remove) browsers.forEach(initializeBrowser); }; this.onBrowserStart = function (browser) { initializeBrowser(browser); }; const concatenateFqn = function (result) { return `${result.suite.join(".")}.${result.description}` }; this.onBrowserLog = (browser, log, type) => { this.checkBrowserResult(browser); const browserResult = this.browserResults[browser.id]; if (log.startsWith(END_KOTLIN_TEST)) { const result = JSON.parse(log.substring(END_KOTLIN_TEST.length, log.length - 1)); browserResult.consoleResultCollector[concatenateFqn(result)] = browserResult.consoleCollector; browserResult.consoleCollector = []; return } if (browserResult) { browserResult.consoleCollector.push(log.slice(1, -1)); } }; this.specSuccess = function (browser, result) { this.checkBrowserResult(browser); const log = this.getLog(browser, result); const testName = result.description; log.push(formatMessage(TEST_START, testName)); this.browserResults[browser.id].consoleResultCollector[concatenateFqn(result)].forEach(item => { log.push(item); }); log.push(formatMessage(TEST_END, testName, result.time)); }; this.specFailure = function (browser, result) { this.checkBrowserResult(browser); const log = this.getLog(browser, result); const testName = result.description; log.push(formatMessage(TEST_START, testName)); this.browserResults[browser.id].consoleResultCollector[concatenateFqn(result)].forEach(item => { log.push(item); }); log.push(formatMessage(TEST_FAILED, testName, "FAILED", result.log .map(log => formatError(log)) .join('\n\n') )); log.push(formatMessage(TEST_END, testName, result.time)); }; this.specSkipped = function (browser, result) { this.checkBrowserResult(browser); const log = this.getLog(browser, result); const testName = result.description; log.push(formatMessage(TEST_IGNORED, testName)); }; this.onRunComplete = function () { Object.keys(this.browserResults).forEach(function (browserId) { const browserResult = self.browserResults[browserId]; const log = browserResult.log; if (browserResult.lastSuite) { log.push(formatMessage(SUITE_END, browserResult.lastSuite)); } self.flushLogs(browserResult); }); self.write(formatMessage(BLOCK_CLOSED, 'JavaScript Unit Tests')); }; this.checkBrowserResult = function (browser) { if (!this.browserResults[browser.id]) { initializeBrowser(browser); } }; this.getLog = function (browser, result) { const browserResult = this.browserResults[browser.id]; let suiteName = browser.name; const moduleName = result.suite.join(' '); if (moduleName) { suiteName = moduleName.concat('.', suiteName); } const log = browserResult.log; if (browserResult.lastSuite !== suiteName) { if (browserResult.lastSuite) { log.push(formatMessage(SUITE_END, browserResult.lastSuite)); } this.flushLogs(browserResult); browserResult.lastSuite = suiteName; log.push(formatMessage(SUITE_START, suiteName)); } return log }; this.flushLogs = function (browserResult) { while (browserResult.log.length > 0) { let line = browserResult.log.shift(); line = line.replace("flowId='%s'", "flowId='" + browserResult.flowId + "'"); this.write(line); } }; }; KarmaKotlinReporter.$inject = ['baseReporterDecorator', 'config', 'emitter']; module.exports = { 'reporter:karma-kotlin-reporter': ['type', KarmaKotlinReporter] };