Exit Full View

Glok / build / js / packages_imported / kotlin-test-js-runner / 0.0.1 / karma-kotlin-reporter.js

'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]
};