All files / lib/internal/assert calltracker.js

90% Statements 90/100
100% Branches 16/16
80% Functions 4/5
90% Lines 90/100

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101104x 104x 104x 104x 104x 104x 104x 104x 104x 104x 104x 104x 104x 104x 104x 104x 104x 104x 104x 104x 104x 104x 104x           17x 17x 17x 1x 1x 17x 1x 1x 16x 16x 16x 16x 16x 16x 16x 16x 17x 17x 17x 17x 17x 17x 17x 17x 10x 10x 8x 8x 8x 8x 10x 10x 10x 2x 2x 10x 17x 17x 17x     9x 9x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 9x 9x     6x 6x 2x 2x 2x 2x 2x 6x   104x 104x  
'use strict';
 
const {
  ArrayPrototypePush,
  Error,
  FunctionPrototype,
  Proxy,
  ReflectApply,
  SafeSet,
} = primordials;
 
const {
  codes: {
    ERR_UNAVAILABLE_DURING_EXIT,
  },
} = require('internal/errors');
const AssertionError = require('internal/assert/assertion_error');
const {
  validateUint32,
} = require('internal/validators');
 
const noop = FunctionPrototype;
 
class CallTracker {

  #callChecks = new SafeSet();

  calls(fn, exact = 1) {
    if (process._exiting)
      throw new ERR_UNAVAILABLE_DURING_EXIT();
    if (typeof fn === 'number') {
      exact = fn;
      fn = noop;
    } else if (fn === undefined) {
      fn = noop;
    }
 
    validateUint32(exact, 'exact', true);
 
    const context = {
      exact,
      actual: 0,
      // eslint-disable-next-line no-restricted-syntax
      stackTrace: new Error(),
      name: fn.name || 'calls'
    };
    const callChecks = this.#callChecks;
    callChecks.add(context);
 
    return new Proxy(fn, {
      __proto__: null,
      apply(fn, thisArg, argList) {
        context.actual++;
        if (context.actual === context.exact) {
          // Once function has reached its call count remove it from
          // callChecks set to prevent memory leaks.
          callChecks.delete(context);
        }
        // If function has been called more than expected times, add back into
        // callchecks.
        if (context.actual === context.exact + 1) {
          callChecks.add(context);
        }
        return ReflectApply(fn, thisArg, argList);
      },
    });
  }

  report() {
    const errors = [];
    for (const context of this.#callChecks) {
      // If functions have not been called exact times
      if (context.actual !== context.exact) {
        const message = `Expected the ${context.name} function to be ` +
                        `executed ${context.exact} time(s) but was ` +
                        `executed ${context.actual} time(s).`;
        ArrayPrototypePush(errors, {
          message,
          actual: context.actual,
          expected: context.exact,
          operator: context.name,
          stack: context.stackTrace
        });
      }
    }
    return errors;
  }

  verify() {
    const errors = this.report();
    if (errors.length > 0) {
      throw new AssertionError({
        message: 'Function(s) were not called the expected number of times',
        details: errors,
      });
    }
  }
}
 
module.exports = CallTracker;