<commit_before>const _ = require('lodash');
const chai = require('chai');

chai.use(function(_chai, utils) {
  utils.addChainableMethod(chai.Assertion.prototype, 'haveErrors', function(...expectedErrors) {

    const obj = utils.flag(this, 'object');

    const actualErrors = obj.errors;
    new chai.Assertion(actualErrors).to.be.an('array');

    _.each(actualErrors, error => {
      error.location = error.location.toString();
    });

    expectedErrors = _.flatten(expectedErrors);
    const missingErrors = expectedErrors.slice();
    const extraErrors = actualErrors.slice();

    _.each(expectedErrors, expectedError => {
      const matchingError = _.find(actualErrors, error => _.isEqual(error, expectedError));
      if (matchingError) {
        extraErrors.splice(extraErrors.indexOf(matchingError), 1);
        missingErrors.splice(missingErrors.indexOf(matchingError), 1);
      }
    });

    var messages = [];
    if (missingErrors.length) {
      messages.push(`The following errors were not found:\n${describeErrors(missingErrors)}`);
    }
    if (extraErrors.length) {
      messages.push(`The following extra errors were found:\n${describeErrors(extraErrors)}`);
    }

    new chai.Assertion(obj.errors).assert(
      _.isEmpty(messages),
      `Expected specific errors.\n\n${messages.join('\n\n')}`,
      `Expected different errors.\n\n${messages.join('\n\n')}`
    );
  });
});

function describeErrors(errors) {
  return errors.map(error => JSON.stringify(error)).join('\n');
}
<commit_msg>Make sure not to find the same error twice when comparing errors<commit_after>const _ = require('lodash');
const chai = require('chai');

chai.use(function(_chai, utils) {
  utils.addChainableMethod(chai.Assertion.prototype, 'haveErrors', function(...expectedErrors) {

    const obj = utils.flag(this, 'object');

    const actualErrors = obj.errors;
    new chai.Assertion(actualErrors).to.be.an('array');

    _.each(actualErrors, error => {
      error.location = error.location.toString();
    });

    expectedErrors = _.flatten(expectedErrors);
    const missingErrors = expectedErrors.slice();
    const extraErrors = actualErrors.slice();

    _.each(expectedErrors, expectedError => {
      const matchingError = _.find(extraErrors, error => _.isEqual(error, expectedError));
      if (matchingError) {
        extraErrors.splice(extraErrors.indexOf(matchingError), 1);
        missingErrors.splice(missingErrors.indexOf(matchingError), 1);
      }
    });

    var messages = [];
    if (missingErrors.length) {
      messages.push(`The following errors were not found:\n${describeErrors(missingErrors)}`);
    }
    if (extraErrors.length) {
      messages.push(`The following extra errors were found:\n${describeErrors(extraErrors)}`);
    }

    new chai.Assertion(obj.errors).assert(
      _.isEmpty(messages),
      `Expected specific errors.\n\n${messages.join('\n\n')}`,
      `Expected different errors.\n\n${messages.join('\n\n')}`
    );
  });
});

function describeErrors(errors) {
  return errors.map(error => JSON.stringify(error)).join('\n');
}

{'fraction_non_alphanumeric': 0.1533980582524272, 'fraction_numerical': 0.0012944983818770227, 'mean_word_length': 3.799689440993789, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 11, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2304123', 'n_tokens_mistral': 914, 'n_tokens_neox': 899, 'n_words': 165}