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; } } }