Cleanup pass on the conformance_pattern tests.

This change reorders some tests, and uniformizes them in the use of "toHaveFailures", which is far more descriptive when failing than just "1 expected to be 2".

PiperOrigin-RevId: 273949911
diff --git a/internal/tsetse/tests/ban_conformance_pattern/fixer_test.ts b/internal/tsetse/tests/ban_conformance_pattern/fixer_test.ts
index 24e53ad..59843a6 100644
--- a/internal/tsetse/tests/ban_conformance_pattern/fixer_test.ts
+++ b/internal/tsetse/tests/ban_conformance_pattern/fixer_test.ts
@@ -20,7 +20,7 @@
 
 const uppercaseFixerBuilt: Fixer = buildReplacementFixer((node: ts.Node) => {
   return {replaceWith: node.getText().toUpperCase()};
-})
+});
 
 // The initial config and source off which we run those checks.
 const baseConfig = {
@@ -39,28 +39,26 @@
       const rule = new ConformancePatternRule(baseConfig, uppercaseFixer);
       const results = compileAndCheck(rule, source);
 
-      expect(results).toHaveNFailures(1, baseConfig);
-      expect(results[0]).toBeFailureMatching({
+      expect(results).toHaveFailuresMatching({
         matchedCode: `q.cite = 'some example string'`,
-        messageText: 'found citation'
+        messageText: 'found citation',
+        fix: [
+          {start: 50, end: 80, replacement: `Q.CITE = 'SOME EXAMPLE STRING'`}
+        ]
       });
-      expect(results[0]).toHaveFixMatching([
-        {start: 50, end: 80, replacement: `Q.CITE = 'SOME EXAMPLE STRING'`}
-      ]);
     });
 
     it('for a single match (alternate fixer)', () => {
       const rule = new ConformancePatternRule(baseConfig, uppercaseFixerBuilt);
       const results = compileAndCheck(rule, source);
 
-      expect(results).toHaveNFailures(1, baseConfig);
-      expect(results[0]).toBeFailureMatching({
+      expect(results).toHaveFailuresMatching({
         matchedCode: `q.cite = 'some example string'`,
-        messageText: 'found citation'
+        messageText: 'found citation',
+        fix: [
+          {start: 50, end: 80, replacement: `Q.CITE = 'SOME EXAMPLE STRING'`}
+        ]
       });
-      expect(results[0]).toHaveFixMatching([
-        {start: 50, end: 80, replacement: `Q.CITE = 'SOME EXAMPLE STRING'`}
-      ]);
     });
 
     it('for several matches', () => {
@@ -69,27 +67,30 @@
           source + `q.cite = 'some other example string';\n`;
       const results = compileAndCheck(rule, sourceTwoMatches);
 
-      expect(results).toHaveNFailures(2, baseConfig);
-      expect(results[0]).toBeFailureMatching({
-        matchedCode: `q.cite = 'some example string'`,
-        messageText: 'found citation'
-      });
-      expect(results[1]).toBeFailureMatching({
-        matchedCode: `q.cite = 'some other example string'`,
-        messageText: 'found citation'
-      });
-      expect(results[0]).toHaveFixMatching([
-        {start: 50, end: 80, replacement: `Q.CITE = 'SOME EXAMPLE STRING'`}
-      ]);
+      expect(results).toHaveFailuresMatching(
+          {
+            matchedCode: `q.cite = 'some example string'`,
+            messageText: 'found citation',
+            fix: [{
+              start: 50,
+              end: 80,
+              replacement: `Q.CITE = 'SOME EXAMPLE STRING'`
+            }]
+          },
+          {
+            matchedCode: `q.cite = 'some other example string'`,
+            messageText: 'found citation',
+            fix: [{
+              start: 82,
+              end: 118,
+              replacement: `Q.CITE = 'SOME OTHER EXAMPLE STRING'`
+            }]
+          });
+
       expect(results[0].fixToReadableStringInContext())
           .toBe(
               `Suggested fix:\n` +
               `- Replace the full match with: Q.CITE = 'SOME EXAMPLE STRING'`);
-      expect(results[1]).toHaveFixMatching([{
-        start: 82,
-        end: 118,
-        replacement: `Q.CITE = 'SOME OTHER EXAMPLE STRING'`
-      }]);
       expect(results[1].fixToReadableStringInContext())
           .toBe(
               `Suggested fix:\n` +
diff --git a/internal/tsetse/tests/ban_conformance_pattern/name_call_non_constant_argument_test.ts b/internal/tsetse/tests/ban_conformance_pattern/name_call_non_constant_argument_test.ts
index 44c9a31..dba2664 100644
--- a/internal/tsetse/tests/ban_conformance_pattern/name_call_non_constant_argument_test.ts
+++ b/internal/tsetse/tests/ban_conformance_pattern/name_call_non_constant_argument_test.ts
@@ -3,11 +3,12 @@
 import {compileAndCheck, customMatchers} from '../../util/testing/test_support';
 
 describe('BANNED_NAME_CALL_NON_CONSTANT_ARGUMENT', () => {
-  const rule = new ConformancePatternRule({
+  const config = {
     errorMessage: 'do not call bar.foo with non-literal 1st arg',
     kind: PatternKind.BANNED_NAME_CALL_NON_CONSTANT_ARGUMENT,
     values: ['bar:0']
-  });
+  };
+  const rule = new ConformancePatternRule(config);
 
   it('matches simple examples', () => {
     const sources = [
@@ -17,8 +18,7 @@
     ];
     const results = compileAndCheck(rule, ...sources);
 
-    expect(results.length).toBe(1);
-    expect(results[0]).toBeFailureMatching({
+    expect(results).toHaveFailuresMatching({
       matchedCode: `foo.bar(window.name, 1)`,
       messageText: 'do not call bar.foo with non-literal 1st arg'
     });
@@ -31,7 +31,7 @@
     ];
     const results = compileAndCheck(rule, ...sources);
 
-    expect(results.length).toBe(0);
+    expect(results).toHaveNoFailures();
   });
 
   it('looks at the right position', () => {
@@ -48,13 +48,13 @@
     ];
     const results = compileAndCheck(rule, ...sources);
 
-    expect(results.length).toBe(2);
-    expect(results[0]).toBeFailureMatching({
-      matchedCode: `foo.aaa(1, window.name)`,
-    });
-    expect(results[1]).toBeFailureMatching({
-      matchedCode: `foo.bbb(window.name)`,
-    });
+    expect(results).toHaveFailuresMatching(
+        {
+          matchedCode: `foo.aaa(1, window.name)`,
+        },
+        {
+          matchedCode: `foo.bbb(window.name)`,
+        });
   });
 
   it('supports static methods', () => {
@@ -72,8 +72,7 @@
     ];
     const results = compileAndCheck(rule, ...sources);
 
-    expect(results.length).toBe(1);
-    expect(results[0]).toBeFailureMatching({
+    expect(results).toHaveFailuresMatching({
       matchedCode: `Car.buildFromParts(window.name)`,
     });
   });
@@ -88,8 +87,7 @@
     const sources = [`URL.createObjectURL(window.name);\n`];
     const results = compileAndCheck(rule, ...sources);
 
-    expect(results.length).toBe(1);
-    expect(results[0]).toBeFailureMatching({
+    expect(results).toHaveFailuresMatching({
       matchedCode: `URL.createObjectURL(window.name)`,
     });
   });
@@ -104,8 +102,7 @@
     const sources = [`eval(window.name);\n`];
     const results = compileAndCheck(rule, ...sources);
 
-    expect(results.length).toBe(1);
-    expect(results[0]).toBeFailureMatching({
+    expect(results).toHaveFailuresMatching({
       matchedCode: `eval(window.name)`,
     });
   });
diff --git a/internal/tsetse/tests/ban_conformance_pattern/name_test.ts b/internal/tsetse/tests/ban_conformance_pattern/name_test.ts
index d3ffdd0..4e8609c 100644
--- a/internal/tsetse/tests/ban_conformance_pattern/name_test.ts
+++ b/internal/tsetse/tests/ban_conformance_pattern/name_test.ts
@@ -4,39 +4,29 @@
 
 describe('BANNED_NAME', () => {
   it('matches simple example of globals', () => {
-    const rule = new ConformancePatternRule({
+    const config = {
       errorMessage: 'no Infinity',
       kind: PatternKind.BANNED_NAME,
       values: ['Infinity']
-    });
-    const source = [
-      `Infinity; 1+1;`,
-    ].join('\n');
-    const results = compileAndCheck(rule, source);
+    };
+    const source = `Infinity; 1+1;`;
+    const results = compileAndCheck(new ConformancePatternRule(config), source);
 
-    expect(results.length).toBe(1);
-    expect(results[0]).toBeFailureMatching({
-      matchedCode: `Infinity`,
-      messageText: 'no Infinity'
-    });
+    expect(results).toHaveFailuresMatching(
+        {matchedCode: `Infinity`, messageText: 'no Infinity'});
   });
 
   it('matches namespaced globals', () => {
-    const rule = new ConformancePatternRule({
+    const config = {
       errorMessage: 'no blob url',
       kind: PatternKind.BANNED_NAME,
       values: ['URL.createObjectURL']
-    });
-    const source = [
-      `URL.createObjectURL({});`,
-    ].join('\n');
-    const results = compileAndCheck(rule, source);
+    };
+    const source = `URL.createObjectURL({});`;
+    const results = compileAndCheck(new ConformancePatternRule(config), source);
 
-    expect(results.length).toBe(1);
-    expect(results[0]).toBeFailureMatching({
-      matchedCode: `createObjectURL`,
-      messageText: 'no blob url'
-    });
+    expect(results).toHaveFailuresMatching(
+        {matchedCode: `createObjectURL`, messageText: 'no blob url'});
   });
 
   it('does not choke on type aliases', () => {
@@ -46,21 +36,21 @@
     // Symbols that verify ts.SymbolFlags.Alias, and ts.SymbolFlags.TypeAlias is
     // not acceptable (the typechecker will throw).
 
+    const config = {
+      errorMessage: 'should not trigger',
+      kind: PatternKind.BANNED_NAME,
+      values: ['whatever']
+    };
     const sources = [
       `export type Foo = {bar: number, baz: (x:string)=>void}`,
       `import {Foo} from './file_0';
        export const c: Foo["baz"] = (x:string)=>{};`,
       `import {c} from './file_1'; c(window.name);`
     ];
-    const results = compileAndCheck(
-        new ConformancePatternRule({
-          errorMessage: 'should not trigger',
-          kind: PatternKind.BANNED_NAME,
-          values: ['whatever']
-        }),
-        ...sources);
+    const results =
+        compileAndCheck(new ConformancePatternRule(config), ...sources);
 
-    expect(results.length).toBe(0);
+    expect(results).toHaveNoFailures();
   });
 });
 
diff --git a/internal/tsetse/tests/ban_conformance_pattern/property_non_constant_write_test.ts b/internal/tsetse/tests/ban_conformance_pattern/property_non_constant_write_test.ts
index b545571..753e08a 100644
--- a/internal/tsetse/tests/ban_conformance_pattern/property_non_constant_write_test.ts
+++ b/internal/tsetse/tests/ban_conformance_pattern/property_non_constant_write_test.ts
@@ -7,17 +7,15 @@
     const source = `const q = document.createElement('q');\n` +
         `q.cite = 'some example string';\n` +  // literal
         `q.cite = window.name;\n`;             // non-literal
-    const rule = new ConformancePatternRule({
+    const config = {
       errorMessage: 'do not cite dynamically',
       kind: PatternKind.BANNED_PROPERTY_NON_CONSTANT_WRITE,
       values: ['HTMLQuoteElement.prototype.cite']
-    });
-    const results = compileAndCheck(rule, source);
+    };
+    const results = compileAndCheck(new ConformancePatternRule(config), source);
 
-    expect(results.length).toBe(1);
-    expect(results[0])
-        .toBeFailureMatching(
-            {start: 71, end: 91, messageText: 'do not cite dynamically'});
+    expect(results).toHaveFailuresMatching(
+        {start: 71, end: 91, messageText: 'do not cite dynamically'});
   });
 });
 
diff --git a/internal/tsetse/tests/ban_conformance_pattern/property_write_test.ts b/internal/tsetse/tests/ban_conformance_pattern/property_write_test.ts
index e0f3254..5aa9f56 100644
--- a/internal/tsetse/tests/ban_conformance_pattern/property_write_test.ts
+++ b/internal/tsetse/tests/ban_conformance_pattern/property_write_test.ts
@@ -3,77 +3,81 @@
 import {compileAndCheck, customMatchers} from '../../util/testing/test_support';
 
 describe('BANNED_PROPERTY_WRITE', () => {
-  const rule = new ConformancePatternRule({
-    errorMessage: 'do not cite',
-    kind: PatternKind.BANNED_PROPERTY_WRITE,
-    values: ['HTMLQuoteElement.prototype.cite']
-  });
+  describe('simpler matcher tests', () => {
+    const config = {
+      errorMessage: 'do not cite',
+      kind: PatternKind.BANNED_PROPERTY_WRITE,
+      values: ['HTMLQuoteElement.prototype.cite']
+    };
+    const rule = new ConformancePatternRule(config);
 
-  it('matches simple examples', () => {
-    const source = [
-      `const q = document.createElement('q');`,
-      `q.cite = 'some example string';`,
-    ].join('\n');
-    const results = compileAndCheck(rule, source);
+    it('matches simple examples', () => {
+      const source = [
+        `const q = document.createElement('q');`,
+        `q.cite = 'some example string';`,
+      ].join('\n');
+      const results = compileAndCheck(rule, source);
 
-    expect(results.length).toBe(1);
-    expect(results[0]).toBeFailureMatching({
-      matchedCode: `q.cite = 'some example string'`,
-      messageText: 'do not cite'
+      expect(results).toHaveFailuresMatching({
+        matchedCode: `q.cite = 'some example string'`,
+        messageText: 'do not cite'
+      });
     });
-  });
 
-  it('matches precisely, even with whitespace or comments', () => {
-    const source = [
-      `const q = document.createElement('q');`,
-      `    q.cite = 'exampleA';`,
-      `q.cite = 'exampleB'    ;`,
-      `/* test1 */ q.cite = /* test2 */ 'exampleC' /* test3 */;`,
-    ].join('\n');
-    const results = compileAndCheck(rule, source);
+    it('matches precisely, even with whitespace or comments', () => {
+      const source = [
+        `const q = document.createElement('q');`,
+        `    q.cite = 'exampleA';`,
+        `q.cite = 'exampleB'    ;`,
+        `/* test1 */ q.cite = /* test2 */ 'exampleC' /* test3 */;`,
+      ].join('\n');
+      const results = compileAndCheck(rule, source);
 
-    expect(results.length).toBe(3);
-    expect(results[0]).toBeFailureMatching({
-      matchedCode: `q.cite = 'exampleA'`,
-      messageText: 'do not cite'
+      expect(results).toHaveFailuresMatching(
+          {matchedCode: `q.cite = 'exampleA'`, messageText: 'do not cite'},
+          {matchedCode: `q.cite = 'exampleB'`, messageText: 'do not cite'}, {
+            matchedCode: `q.cite = /* test2 */ 'exampleC'`,
+            messageText: 'do not cite'
+          });
     });
-    expect(results[1]).toBeFailureMatching({
-      matchedCode: `q.cite = 'exampleB'`,
-      messageText: 'do not cite'
+
+    it('understands function prototypes', () => {
+      const source = [
+        `function foo(q:HTMLQuoteElement) {`,
+        `    q.cite = 'some example string';`,
+        `}`,
+      ].join('\n');
+      const results = compileAndCheck(rule, source);
+
+      expect(results).toHaveFailuresMatching({
+        matchedCode: `q.cite = 'some example string'`,
+        messageText: 'do not cite'
+      });
     });
-    expect(results[2]).toBeFailureMatching({
-      matchedCode: `q.cite = /* test2 */ 'exampleC'`,
-      messageText: 'do not cite'
+
+    it('understands imported symbols', () => {
+      const sources = [
+        `const q = document.createElement('q'); export {q};`,
+        `import {q} from './file_0'; q.cite = window.name;`
+      ];
+      const results = compileAndCheck(rule, ...sources);
+
+      expect(results).toHaveFailuresMatching({
+        matchedCode: 'q.cite = window.name',
+        fileName: 'file_1.ts',
+        messageText: 'do not cite',
+      });
     });
-  })
 
-  it('understands function prototypes', () => {
-    const source = [
-      `function foo(q:HTMLQuoteElement) {`,
-      `    q.cite = 'some example string';`,
-      `}`,
-    ].join('\n');
-    const results = compileAndCheck(rule, source);
+    it('understands shadowing', () => {
+      const source = [
+        `const q = document.createElement('q');`,
+        `const f1 = (q: {cite: string}) => { q.cite = 'example 1'; };`,
+      ].join('\n');
+      const rule = new ConformancePatternRule(config);
+      const results = compileAndCheck(rule, source);
 
-    expect(results.length).toBe(1);
-    expect(results[0]).toBeFailureMatching({
-      matchedCode: `q.cite = 'some example string'`,
-      messageText: 'do not cite'
-    });
-  });
-
-  it('understands imported symbols', () => {
-    const sources = [
-      `const q = document.createElement('q'); export {q};`,
-      `import {q} from './file_0'; q.cite = window.name;`
-    ];
-    const results = compileAndCheck(rule, ...sources);
-
-    expect(results.length).toBe(1);
-    expect(results[0]).toBeFailureMatching({
-      matchedCode: 'q.cite = window.name',
-      fileName: 'file_1.ts',
-      messageText: 'do not cite',
+      expect(results).toHaveNoFailures();
     });
   });
 
@@ -94,42 +98,27 @@
     };
 
     it('banning Parent.x matches (instance of Child).x', () => {
-      const ruleOnParent = new ConformancePatternRule({
+      const configOnParent = {
         errorMessage: 'found write to x',
         kind: PatternKind.BANNED_PROPERTY_WRITE,
         values: ['Parent.prototype.x']
-      });
-      const r = compileAndCheck(ruleOnParent, source);
-      expect(r.length).toBe(1);
-      expect(r[0]).toBeFailureMatching(expectedFailure);
+      };
+      const ruleOnParent = new ConformancePatternRule(configOnParent);
+      const results = compileAndCheck(ruleOnParent, source);
+
+      expect(results).toHaveFailuresMatching(expectedFailure);
     });
 
     it('banning Child.x matches x defined on Parent', () => {
-      const ruleOnChild = new ConformancePatternRule({
+      const configOnChild = {
         errorMessage: 'found write to x',
         kind: PatternKind.BANNED_PROPERTY_WRITE,
         values: ['Child.prototype.x']
-      });
-      const r = compileAndCheck(ruleOnChild, source);
-      expect(r.length).toBe(1);
-      expect(r[0]).toBeFailureMatching(expectedFailure);
-    });
-  });
+      };
+      const ruleOnChild = new ConformancePatternRule(configOnChild);
+      const results = compileAndCheck(ruleOnChild, source);
 
-  describe('with shadowing', () => {
-    it('does not over-match', () => {
-      const source = [
-        `const q = document.createElement('q');`,
-        `const f1 = (q: {cite: string}) => { q.cite = 'example 1'; };`,
-      ].join('\n');
-      const rule = new ConformancePatternRule({
-        errorMessage: 'do not cite',
-        kind: PatternKind.BANNED_PROPERTY_WRITE,
-        values: ['HTMLQuoteElement.prototype.cite']
-      });
-      const results = compileAndCheck(rule, source);
-
-      expect(results.length).toBe(0);
+      expect(results).toHaveFailuresMatching(expectedFailure);
     });
   });
 });
diff --git a/internal/tsetse/tests/ban_conformance_pattern/whitelist_test.ts b/internal/tsetse/tests/ban_conformance_pattern/whitelist_test.ts
index a1c7342..93e17bd 100644
--- a/internal/tsetse/tests/ban_conformance_pattern/whitelist_test.ts
+++ b/internal/tsetse/tests/ban_conformance_pattern/whitelist_test.ts
@@ -26,7 +26,7 @@
       const rule = new ConformancePatternRule(config);
       const results = compileAndCheck(rule, source);
 
-      expect(results).toHaveNFailures(1, config);
+      expect(results).toHaveNFailures(1);
     });
 
     it('matches if there is an empty whitelist group', () => {
@@ -39,7 +39,7 @@
       const rule = new ConformancePatternRule(config);
       const results = compileAndCheck(rule, source);
 
-      expect(results).toHaveNFailures(1, config);
+      expect(results).toHaveNFailures(1);
     });
 
     it('respects prefix-based whitelists (matching test)', () => {
@@ -53,7 +53,7 @@
       const rule = new ConformancePatternRule(config);
       const results = compileAndCheck(rule, source);
 
-      expect(results).toHaveNFailures(0, config);
+      expect(results).toHaveNoFailures();
     });
 
     it('respects prefix-based whitelists (non-matching test)', () => {
@@ -67,7 +67,7 @@
       const rule = new ConformancePatternRule(config);
       const results = compileAndCheck(rule, source);
 
-      expect(results).toHaveNFailures(1, config);
+      expect(results).toHaveNFailures(1);
     });
 
     it('respects regex-based whitelists', () => {
@@ -81,7 +81,7 @@
       const rule = new ConformancePatternRule(config);
       const results = compileAndCheck(rule, source);
 
-      expect(results).toHaveNFailures(0, config);
+      expect(results).toHaveNoFailures();
     });
 
     it('accepts several regex-based whitelists', () => {
@@ -99,7 +99,7 @@
       // Testing two times the same file so that both regexps match.
       const results = compileAndCheck(rule, source, source);
 
-      expect(results).toHaveNFailures(0, config);
+      expect(results).toHaveNoFailures();
     });
 
     it('throws on creation of invalid regexps', () => {
@@ -111,6 +111,7 @@
         }]
       };
       expect(() => {
+        // tslint:disable-next-line:no-unused-expression
         new ConformancePatternRule(config);
       }).toThrowError(/Invalid regular expression/);
     });
diff --git a/internal/tsetse/util/testing/test_support.ts b/internal/tsetse/util/testing/test_support.ts
index fb2460e..e474ec7 100644
--- a/internal/tsetse/util/testing/test_support.ts
+++ b/internal/tsetse/util/testing/test_support.ts
@@ -9,7 +9,6 @@
 import {Checker} from '../../checker';
 import {Failure, fixToString} from '../../failure';
 import {AbstractRule} from '../../rule';
-import {Config} from '../pattern_config';
 
 
 
@@ -74,18 +73,146 @@
   return file ? file.fileName : 'unknown';
 }
 
+/**
+ * Returns the location the temp directory for that platform (with forward
+ * slashes).
+ */
 export function getTempDirForWhitelist() {
   // TS uses forward slashes on Windows ¯\_(ツ)_/¯
-  return os.platform() == 'win32' ? os.tmpdir().replace(/\\/g, '/') :
-                                    os.tmpdir();
+  return os.platform() === 'win32' ? os.tmpdir().replace(/\\/g, '/') :
+                                     os.tmpdir();
 }
 
-// Custom matcher for Jasmine, for a better experience matching fixes.
+interface FailureExpectations {
+  fileName?: string;
+  start?: number;
+  end?: number;
+  matchedCode?: string;
+  messageText?: string;
+  fix?: FixExpectations;
+}
+
+type FixExpectations = [{
+  fileName?: string;
+  start?: number;
+  end?: number;
+  replacement?: string;
+}];
+
+
+function failureMatchesExpectation(
+    f1: Failure, exp: FailureExpectations): {pass: boolean, message: string} {
+  const diagnostic = f1.toDiagnostic();
+  let regrets = '';
+  if (exp === undefined) {
+    regrets += 'The matcher requires two arguments. ';
+  }
+  if (exp.fileName) {
+    if (!diagnostic.file) {
+      regrets += `Expected diagnostic to have a source file, but it had ${
+          diagnostic.file}. `;
+    } else if (!diagnostic.file.fileName.endsWith(exp.fileName)) {
+      regrets +=
+          `Expected ${diagnostic.file.fileName} to end with ${exp.fileName}. `;
+    }
+  }
+  if (exp.messageText !== undefined &&
+      exp.messageText !== diagnostic.messageText) {
+    regrets +=
+        expectation('errorMessage', exp.messageText, diagnostic.messageText);
+  }
+  if (exp.start !== undefined && diagnostic.start !== exp.start) {
+    regrets += expectation('start', exp.start, diagnostic.start);
+  }
+  if (exp.end !== undefined && diagnostic.end !== exp.end) {
+    regrets += expectation('end', exp.end, diagnostic.end);
+  }
+  if (exp.matchedCode) {
+    if (!diagnostic.file) {
+      regrets += `Expected diagnostic to have a source file, but it had ${
+          diagnostic.file}. `;
+    } else if (diagnostic.start === undefined) {
+      // I don't know how this could happen, but typings say so.
+      regrets += `Expected diagnostic to have a starting position. `;
+    } else {
+      const foundMatchedCode = diagnostic.file.getFullText().slice(
+          Number(diagnostic.start), diagnostic.end);
+      if (foundMatchedCode !== exp.matchedCode) {
+        regrets += `Expected diagnostic to match ${exp.matchedCode}, but was ${
+            foundMatchedCode} (from ${Number(diagnostic.start)} to ${
+            diagnostic.end}). `;
+      }
+    }
+  }
+  if (exp.fix) {
+    const {pass, message: fixMessage} = fixMatchesExpectation(f1, exp.fix);
+    if (!pass) {
+      regrets += fixMessage;
+    }
+  }
+  return {pass: regrets === '', message: regrets};
+}
+
+function fixMatchesExpectation(failure: Failure, expected: FixExpectations):
+    {pass: boolean, message: string} {
+  let regrets = '';
+  const actualFix = failure.toDiagnostic().fix;
+  if (!actualFix) {
+    regrets += `Expected ${failure.toString()} to have fix ${
+        JSON.stringify(expected)}. `;
+  } else if (actualFix.changes.length !== expected.length) {
+    regrets += `Expected ${expected.length} individual changes, got ${
+        actualFix.changes.length}. `;
+    if (actualFix.changes.length) {
+      regrets += '\n' + fixToString(actualFix);
+    }
+  } else {
+    for (let i = 0; i < expected.length; i++) {
+      const e = expected[i];
+      const a = actualFix.changes[i];
+      if (e.start !== undefined && e.start !== a.start) {
+        regrets +=
+            expectation(`${i}th individualChange's start`, e.start, a.start);
+      }
+      if (e.end !== undefined && e.end !== a.end) {
+        regrets += expectation(`${i}th individualChange's end`, e.end, a.end);
+      }
+      if (e.replacement !== undefined && e.replacement !== a.replacement) {
+        regrets += expectation(
+            `${i}th individualChange's replacement`, e.replacement,
+            a.replacement);
+      }
+      if (e.fileName !== undefined && e.fileName !== a.sourceFile.fileName) {
+        regrets += expectation(
+            `${i}th individualChange's fileName`, e.fileName,
+            a.sourceFile.fileName);
+      }
+      // TODO: Consider adding matchedCode as for the failure matcher.
+    }
+  }
+
+  return {pass: regrets === '', message: regrets};
+}
+
+
+/**
+ * Custom matcher for Jasmine, for a better experience matching failures and
+ * fixes.
+ */
 export const customMatchers: jasmine.CustomMatcherFactories = {
 
+  toBeFailureMatching(): jasmine.CustomMatcher {
+    return {compare: failureMatchesExpectation};
+  },
+
+  /** Checks that a Failure has the expected Fix field. */
+  toHaveFixMatching(): jasmine.CustomMatcher {
+    return {compare: fixMatchesExpectation};
+  },
+
   toHaveNFailures(): jasmine.CustomMatcher {
     return {
-      compare: (actual: Failure[], expected: Number, config?: Config) => {
+      compare: (actual: Failure[], expected: number) => {
         if (actual.length === expected) {
           return {pass: true};
         } else {
@@ -94,123 +221,33 @@
           if (actual.length) {
             message += '\n' + actual.map(f => f.toString()).join('\n');
           }
-          if (config) {
-            message += `\nConfig: {kind:${config.kind}, values:${
-                JSON.stringify(config.values)}, whitelist:${
-                JSON.stringify(config.whitelistEntries)} }`;
-          }
           return {pass: false, message};
         }
       }
     };
   },
 
-  toBeFailureMatching(): jasmine.CustomMatcher {
+  toHaveFailuresMatching(): jasmine.CustomMatcher {
     return {
-      compare: (actualFailure: Failure, exp: {
-        fileName?: string,
-        start?: number,
-        end?: number,
-        matchedCode?: string,
-        messageText?: string,
-      }) => {
-        const actualDiagnostic = actualFailure.toDiagnostic();
-        let regrets = '';
-        if (exp === undefined) {
-          regrets += 'The matcher requires two arguments. ';
-        }
-        if (exp.fileName) {
-          if (!actualDiagnostic.file) {
-            regrets += `Expected diagnostic to have a source file, but it had ${
-                actualDiagnostic.file}. `;
-          } else if (!actualDiagnostic.file.fileName.endsWith(exp.fileName)) {
-            regrets += `Expected ${
-                actualDiagnostic.file.fileName} to end with ${exp.fileName}. `;
+      compare: (actual: Failure[], ...expected: FailureExpectations[]) => {
+        if (actual.length !== expected.length) {
+          let message =
+              `Expected ${expected} Failures, but found ${actual.length}.`;
+          if (actual.length) {
+            message += '\n' + actual.map(f => f.toString()).join('\n');
           }
+          return {pass: false, message};
         }
-        if (exp.messageText !== undefined &&
-            exp.messageText != actualDiagnostic.messageText) {
-          regrets += expectation(
-              'errorMessage', exp.messageText, actualDiagnostic.messageText);
-        }
-        if (exp.start !== undefined && actualDiagnostic.start !== exp.start) {
-          regrets += expectation('start', exp.start, actualDiagnostic.start);
-        }
-        if (exp.end !== undefined && actualDiagnostic.end !== exp.end) {
-          regrets += expectation('end', exp.end, actualDiagnostic.end);
-        }
-        if (exp.matchedCode) {
-          if (!actualDiagnostic.file) {
-            regrets += `Expected diagnostic to have a source file, but it had ${
-                actualDiagnostic.file}. `;
-          } else if (actualDiagnostic.start === undefined) {
-            // I don't know how this could happen, but typings say so.
-            regrets += `Expected diagnostic to have a starting position. `;
-          } else {
-            const foundMatchedCode = actualDiagnostic.file.getFullText().slice(
-                Number(actualDiagnostic.start), actualDiagnostic.end);
-            if (foundMatchedCode != exp.matchedCode) {
-              regrets += `Expected diagnostic to match ${
-                  exp.matchedCode}, but was ${foundMatchedCode} (from ${
-                  Number(
-                      actualDiagnostic.start)} to ${actualDiagnostic.end}). `;
-            }
+        let pass = true, message = '';
+        for (let i = 0; i < actual.length; i++) {
+          const comparison = failureMatchesExpectation(actual[i], expected[i]);
+          pass = pass && comparison.pass;
+          if (comparison.message) {
+            message += `For failure ${i}: ${comparison.message}\n`;
           }
+          message += comparison.message;
         }
-        return {pass: regrets === '', message: regrets};
-      }
-    };
-  },
-
-  /** Checks that a Failure has the expected Fix field. */
-  toHaveFixMatching(): jasmine.CustomMatcher {
-    return {
-      compare: (actualFailure: Failure, exp: [{
-                  fileName?: string,
-                  start?: number,
-                  end?: number,
-                  replacement?: string
-                }]) => {
-        let regrets = '';
-        const actualFix = actualFailure.toDiagnostic().fix;
-        if (!actualFix) {
-          regrets += `Expected ${actualFailure.toString()} to have fix ${
-              JSON.stringify(exp)}. `;
-        } else if (actualFix.changes.length != exp.length) {
-          regrets += `Expected ${exp.length} individual changes, got ${
-              actualFix.changes.length}. `;
-          if (actualFix.changes.length) {
-            regrets += '\n' + fixToString(actualFix);
-          }
-        } else {
-          for (let i = 0; i < exp.length; i++) {
-            const e = exp[i];
-            const a = actualFix.changes[i];
-            if (e.start !== undefined && e.start !== a.start) {
-              regrets += expectation(
-                  `${i}th individualChange's start`, e.start, a.start);
-            }
-            if (e.end !== undefined && e.end !== a.end) {
-              regrets +=
-                  expectation(`${i}th individualChange's end`, e.end, a.end);
-            }
-            if (e.replacement !== undefined &&
-                e.replacement !== a.replacement) {
-              regrets += expectation(
-                  `${i}th individualChange's replacement`, e.replacement,
-                  a.replacement);
-            }
-            if (e.fileName !== undefined &&
-                e.fileName !== a.sourceFile.fileName) {
-              regrets += expectation(
-                  `${i}th individualChange's fileName`, e.fileName,
-                  a.sourceFile.fileName);
-            }
-            // TODO: Consider adding matchedCode as for the failure matcher.
-          }
-        }
-
-        return {pass: regrets === '', message: regrets};
+        return {pass, message};
       }
     };
   },
@@ -227,11 +264,23 @@
         };
       }
     };
-  }
+  },
 
+  toHaveNoFailures(): jasmine.CustomMatcher {
+    return {
+      compare: (actual: Failure[]) => {
+        if (actual.length !== 0) {
+          let message = `Expected no Failures, but found ${actual.length}.`;
+          message += '\n' + actual.map(f => f.toString()).join('\n');
+          return {pass: false, message};
+        }
+        return {pass: true, message: ''};
+      }
+    };
+  }
 };
 
-function expectation(fieldname: string, expectation: any, actual: any) {
+function expectation<T>(fieldname: string, expectation: T, actual: T) {
   return `Expected .${fieldname} to be ${expectation}, was ${actual}. `;
 }
 
@@ -239,23 +288,21 @@
 declare global {
   namespace jasmine {
     interface Matchers<T> {
-      toBeFailureMatching(expected: {
-        fileName?: string,
-        start?: number,
-        end?: number,
-        matchedCode?: string,
-        messageText?: string,
-      }): void;
+      toBeFailureMatching(expected: FailureExpectations): void;
+
 
       /** Checks that a Failure has the expected Fix field. */
-      toHaveFixMatching(expected: [
-        {fileName?: string, start?: number, end?: number, replacement?: string}
-      ]): void;
-
-      toHaveNFailures(expected: Number, config?: Config): void;
+      toHaveFixMatching(expected: FixExpectations): void;
 
       /** Asserts that a Failure has no fix. */
       toHaveNoFix(): void;
+
+      toHaveNFailures(expected: number): void;
+
+      toHaveFailuresMatching(...expected: FailureExpectations[]): void;
+
+      /** Asserts that the results are empty. */
+      toHaveNoFailures(): void;
     }
   }
 }