mochajs / mocha

☕️ simple, flexible, fun javascript test framework for node.js & the browser
https://mochajs.org
MIT License
22.6k stars 3.01k forks source link

TypeError: Cannot read property 'length' of undefined #4821

Closed Vitalii4as closed 9 months ago

Vitalii4as commented 2 years ago

Prerequisites

Description

Getting error when trying to run tests in parallel mode with custom reporters

Steps to Reproduce

  1. Run tests with custom reporter with --parallel flag

Expected behavior: Test executed without errors Actual behavior: TypeError: Cannot read property 'length' of undefined

TypeError: Cannot read property 'length' of undefined\n    at processTicksAndRejections (internal/process/task_queues.js:95:5)

Reproduces how often: 100%

Versions

Additional Information

Custom reporter code

'use strict';
/**
 * @module JSON
 */
/**
 * Module dependencies.
 */

const Base = require('mocha/lib/reporters/base');
const constants = require('mocha/lib/runner').constants;
const path = require('path');
const fs = require('fs');
const EVENT_TEST_PASS = constants.EVENT_TEST_PASS;
const EVENT_TEST_FAIL = constants.EVENT_TEST_FAIL;
const EVENT_TEST_END = constants.EVENT_TEST_END;
const EVENT_TEST_BEGIN = constants.EVENT_TEST_BEGIN;
const EVENT_RUN_END = constants.EVENT_RUN_END;
const EVENT_TEST_PENDING = constants.EVENT_TEST_PENDING;
const EVENT_SUITE_BEGIN = constants.EVENT_SUITE_BEGIN;

/**
 * Expose `JSON`.
 */

exports = module.exports = JSONReporter;

let stdOutResult = {};
/**
 * Constructs a new `JSON` reporter instance.
 *
 * @public
 * @class JSON
 * @memberof Mocha.reporters
 * @extends Mocha.reporters.Base
 * @param {Runner} runner - Instance triggers reporter actions.
 * @param {Object} [options] - runner options
 */
function JSONReporter(runner, options) {
    Base.call(this, runner, options);

    const self = this;
    const tests = [];
    const pending = [];
    const failures = [];
    const passes = [];

    const originalStdoutWrite = process.stdout.write.bind(process.stdout);

    const workersReportFolder = options.reporterOptions?.workersFolder
        ? options.reporterOptions.workersFolder
        : `workers`;

    if (runner.isParallelMode()) {
        if (!fs.existsSync(workersReportFolder)) {
            fs.mkdirSync(workersReportFolder);
        }

        runner.workerReporter(`${__dirname}/json-reporter.js`); // current reporter
    }

    runner.on(EVENT_TEST_BEGIN, function (test) {
        console.log(this.stats.tests + 1 + ') ' + test.fullTitle());

        process.stdout.write = (chunk, encoding, callback) => {
            if (typeof chunk === 'string') {
                stdOutResult[test.fullTitle()] = (stdOutResult[test.fullTitle()] ?? '') + chunk;
            }

            return originalStdoutWrite(chunk, encoding, callback);
        };
    });

    runner.on(EVENT_TEST_END, function (test) {
        process.stdout.write = originalStdoutWrite;
        test.stdOut = stdOutResult[test.fullTitle()];
        tests.push(test);
    });

    runner.on(EVENT_TEST_PASS, function (test) {
        process.stdout.write = originalStdoutWrite;
        passes.push(test);
    });

    runner.on(EVENT_TEST_FAIL, function (test) {
        process.stdout.write = originalStdoutWrite;
        failures.push(test);
        console.log(`❌ [TEST FAILED]: ${test.fullTitle()}`, test.err);
    });

    runner.on(EVENT_TEST_PENDING, function (test) {
        process.stdout.write = originalStdoutWrite;
        pending.push(test);
        console.log(`[PENDING]: ${test.fullTitle()}`);
    });

    runner.once(EVENT_RUN_END, function () {
        try {
            process.stdout.write = originalStdoutWrite;

            const obj = {
                stats: self.stats,
                tests: tests.map(clean),
                pending: pending.map(clean),
                failures: failures.map(clean),
                passes: passes.map(clean),
            };

            runner.testResults = obj;

            if (!runner.isParallelMode()) {
                const reportPath = `${workersReportFolder}/test-${process.pid}.json`;

                fs.writeFileSync(reportPath, JSON.stringify(obj, null, 2), {flag: 'w'});
            } else {
                fs.writeFileSync(
                    options.reporterOptions && options.reporterOptions.output
                        ? options.reporterOptions.output
                        : 'test-report.json',
                    JSON.stringify(obj, null, 2),
                    {flag: 'w'}
                );
            }
        } catch (error) {
            console.error(error);
        }
    });
}

/**
 * Return a plain-object representation of `test`
 * free of cyclic properties etc.
 *
 * @private
 * @param {Object} test
 * @return {Object}
 */
function clean(test) {
    let err = test.err || {};
    if (err instanceof Error) {
        err = errorJSON(err);
    }

    return {
        title: test.title,
        fullTitle: test.fullTitle(),
        file: test.file,
        duration: test.duration,
        currentRetry: test.currentRetry(),
        stdOut: test.stdOut,
        err: cleanCycles(err),
    };
}

/**
 * Replaces any circular references inside `obj` with '[object Object]'
 *
 * @private
 * @param {Object} obj
 * @return {Object}
 */
function cleanCycles(obj) {
    const cache = [];
    return JSON.parse(
        JSON.stringify(obj, function (key, value) {
            if (typeof value === 'object' && value !== null) {
                if (cache.indexOf(value) !== -1) {
                    // Instead of going in a circle, we'll print [object Object]
                    return '' + value;
                }
                cache.push(value);
            }

            return value;
        }),
    );
}

/**
 * Transform an Error object into a JSON object.
 *
 * @private
 * @param {Error} err
 * @return {Object}
 */
function errorJSON(err) {
    const res = {};
    Object.getOwnPropertyNames(err).forEach(function (key) {
        res[key] = err[key];
    }, err);
    return res;
}

JSONReporter.description = 'single JSON object';
juergba commented 2 years ago

@Vitalii4as I'm not going to debug this one for you, it is your custom reporter. Find out where your reporter crashes, which property/variable is undefined. Then let's see what we evtl. can do for you.

Vitalii4as commented 2 years ago

It does not crash in the reporter, it crashes inside nodejs internals:

TypeError: Cannot read property 'length' of undefined
    at processTicksAndRejections (internal/process/task_queues.js:95:5)

This is probably because I'm running the same reporter for workers.

runner.workerReporter(`${__dirname}/json-reporter.js`);
juergba commented 2 years ago

Then probably you should try this way: #4403

JoshuaKGoldberg commented 9 months ago

I believe this is a user question where the best help given was to point to a PR with lots of debugging advice. Someone please yell at me if I'm wrong and there's actually a fix to be made to Mocha. 🙂