All files / lib/internal/assert calltracker.js

90.38% Statements 94/104
100% Branches 20/20
80% Functions 4/5
90.38% Lines 94/104

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 101 102 103 104 105111x 111x 111x 111x 111x 111x 111x 111x 111x 111x 111x 111x 111x 111x 111x 111x 111x 111x 111x 111x 111x 111x 111x           19x 19x 19x 1x 1x 19x 1x 1x 18x 18x 18x 18x 18x 18x 18x 18x 19x 19x 19x 19x 19x 19x 19x 19x 12x 12x 10x 10x 10x 10x 12x 12x 12x 2x 2x 12x 19x 19x 19x     11x 11x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 11x 11x     8x 8x 4x 4x 5x 8x 8x 8x 8x 8x 8x 8x   111x 111x  
'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) {
      return;
    }
    const message = errors.length === 1 ?
      errors[0].message :
      'Functions were not called the expected number of times';
    throw new AssertionError({
      message,
      details: errors,
    });
  }
}
 
module.exports = CallTracker;