From 20103a35d664798a4ccd94a92dc52af670254448 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Tue, 26 Mar 2019 17:45:22 -0700 Subject: [PATCH] fix(@angular-devkit/build-angular): fix countOccurences And add testing for it. --- .../plugins/webpack/analytics.ts | 15 ++- .../plugins/webpack/analytics_spec.ts | 95 +++++++++++++++++++ 2 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 packages/angular_devkit/build_angular/plugins/webpack/analytics_spec.ts diff --git a/packages/angular_devkit/build_angular/plugins/webpack/analytics.ts b/packages/angular_devkit/build_angular/plugins/webpack/analytics.ts index fd50a1004a51..03d4fdc4d5bc 100644 --- a/packages/angular_devkit/build_angular/plugins/webpack/analytics.ts +++ b/packages/angular_devkit/build_angular/plugins/webpack/analytics.ts @@ -42,14 +42,23 @@ export function countOccurrences(source: string, match: string, wordBreak = fals // We condition here so branch prediction happens out of the loop, not in it. if (wordBreak) { const re = /\w/; - for (let pos = source.lastIndexOf(match); pos >= 0; pos = source.lastIndexOf(match, pos - 1)) { - if (!(re.test(source[pos - 1]) || re.test(source[pos + match.length]))) { + for (let pos = source.lastIndexOf(match); pos >= 0; pos = source.lastIndexOf(match, pos)) { + if (!(re.test(source[pos - 1] || '') || re.test(source[pos + match.length] || ''))) { count++; // 1 match, AH! AH! AH! 2 matches, AH! AH! AH! } + + pos -= match.length; + if (pos < 0) { + break; + } } } else { - for (let pos = source.lastIndexOf(match); pos >= 0; pos = source.lastIndexOf(match, pos - 1)) { + for (let pos = source.lastIndexOf(match); pos >= 0; pos = source.lastIndexOf(match, pos)) { count++; // 1 match, AH! AH! AH! 2 matches, AH! AH! AH! + pos -= match.length; + if (pos < 0) { + break; + } } } diff --git a/packages/angular_devkit/build_angular/plugins/webpack/analytics_spec.ts b/packages/angular_devkit/build_angular/plugins/webpack/analytics_spec.ts new file mode 100644 index 000000000000..e0266f68bdee --- /dev/null +++ b/packages/angular_devkit/build_angular/plugins/webpack/analytics_spec.ts @@ -0,0 +1,95 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { countOccurrences } from './analytics'; + + +function _randomString(len: number) { + const charSpace = `0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ`; + + let s = ''; + for (let i = 0; i < len; i++) { + s += charSpace[Math.floor(Math.random() * charSpace.length)]; + } + + return s; +} + +describe('countOccurrences', () => { + // Every use cases is a text, search, word break or not, and expected result. + const useCases: [string, string, boolean, number][] = [ + ['abc1def1ghi1jkl1mno1pqrs1tuvw1xyz', '1', false, 7], // 0 + ['abc1def12ghi1jkl1mno12pqrs12tuvw1xyz12', '12', false, 4], // 1 + ['abc', 'abc', false, 1], // 2 + ['abc', 'abc', true, 1], // 3 + ['aaaaa', 'aaa', false, 1], // 4 + ['aa aaa', 'aaa', true, 1], // 5 + ['aaaaaa', 'aaa', false, 2], // 6 + ['aaa aaa', 'aaa', true, 2], // 7 + ['a', 'a', false, 1], // 8 + ['a', 'a', true, 1], // 9 + ]; + + useCases.forEach(([text, search, wordBreak, expected], i) => { + it(`works (${i})`, () => { + expect(countOccurrences(text, search, wordBreak)).toBe(expected); + }); + }); + + // Random testing. + it('can count (random, wordBreak=false)', () => { + // Generate a random string with injected search strings in it. + let text = _randomString(10000); + const search = _randomString(100).toLowerCase(); + const nb = Math.floor(Math.random() * 200 + 100); + + // Insert nb search string in. + new Array(nb).fill(0) + // Map it with a random position. + .map(() => Math.floor(Math.random() * text.length)) + // Sort from highest to lowest. + .sort((a, b) => b - a) + // Insert the search string for each position created this way. + .forEach(pos => { + text = text.slice(0, pos) + search + text.slice(pos); + }); + + expect(countOccurrences(text, search, false)).toBe(nb); + expect(countOccurrences(text, search, true)).toBe(0); + }); + + it('can count (random, wordBreak=true)', () => { + // Generate a random string with injected search strings in it. + let text = _randomString(10000); + const search = _randomString(100).toLowerCase(); + let nb = Math.floor(Math.random() * 200 + 100); + + // Insert nb search string in. + new Array(nb).fill(0) + // Map it with a random position. + .map(() => Math.floor(Math.random() * text.length)) + // Sort from highest to lowest. + .sort((a, b) => b - a) + // Insert the search string for each position created this way. + .forEach(pos => { + switch (Math.floor(Math.random() * 5)) { + case 0: + // Do not insert a wordbreak. + text = text.slice(0, pos) + search + text.slice(pos); + nb--; + break; + + case 1: text = text.slice(0, pos) + ' ' + search + ' ' + text.slice(pos); break; + case 2: text = text.slice(0, pos) + '(' + search + '$' + text.slice(pos); break; + case 3: text = text.slice(0, pos) + '|' + search + ')' + text.slice(pos); break; + case 4: text = text.slice(0, pos) + '-' + search + '.' + text.slice(pos); break; + } + }); + + expect(countOccurrences(text, search, true)).toBe(nb); + }); +});