Skip to content

Commit

Permalink
feat: added infinite wave detection
Browse files Browse the repository at this point in the history
  • Loading branch information
JoshuaKGoldberg committed Oct 10, 2022
1 parent b12a5ab commit 5e6b82a
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 0 deletions.
10 changes: 10 additions & 0 deletions src/runtime/providers/createCoreMutationsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { collectFilteredNodes } from "../collectFilteredNodes";
import { createFileNamesAndServices } from "../createFileNamesAndServices";
import { findMutationsInFile } from "../findMutationsInFile";
import { Provider, ProviderCreator } from "../types";
import { WaveTracker } from "./tracking/WaveTracker";

/**
* Creates a mutations provider that runs the core mutations within TypeStat.
Expand All @@ -34,6 +35,7 @@ export const createCoreMutationsProvider = (allModifiedFiles: Set<string>): Prov
const startTime = Date.now();
const fileMutationsByFileName = new Map<string, ReadonlyArray<Mutation>>();
const { fileNames, services } = fileNamesAndServicesCache.get();
const waveTracker = new WaveTracker();
const waveStartedFromBeginning = lastFileIndex <= 0;
let addedMutations = 0;

Expand Down Expand Up @@ -113,6 +115,14 @@ export const createCoreMutationsProvider = (allModifiedFiles: Set<string>): Prov

waveIndex += 1;

if (waveTracker.addAndCheck(fileMutations)) {
options.output.stdout(chalk.redBright(`It looks like TypeStat has ended up in an infinite loop.${EOL}`));
options.output.stdout(chalk.red(`Bailing out of applying more mutations.${EOL}`));
options.output.stdout(chalk.red(`Please file an issue to help us fix the bug for you:${EOL}`));
options.output.stdout(chalk.redBright(`https://github.com/JoshuaKGoldberg/TypeStat${EOL}`));
return undefined;
}

return { mutationsWave: { fileMutations } };
};
};
Expand Down
95 changes: 95 additions & 0 deletions src/runtime/providers/tracking/WaveTracker.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { WaveTracker } from "./WaveTracker";

describe("WaveTracker", () => {
describe("addAndCheck", () => {
it("returns false for first wave hash", () => {
// Arrange
const waveTracker = new WaveTracker();

// Act
const result = waveTracker.addAndCheck({ a: [] });

// Assert
expect(result).toBe(false);
});

it("returns false for a second, new wave hash", () => {
// Arrange
const waveTracker = new WaveTracker();
waveTracker.addAndCheck({ a: [] });

// Act
const result = waveTracker.addAndCheck({ b: [] });

// Assert
expect(result).toBe(false);
});

it("returns false for a second, duplicate wave hash", () => {
// Arrange
const waveTracker = new WaveTracker();
waveTracker.addAndCheck({ a: [] });

// Act
const result = waveTracker.addAndCheck({ a: [] });

// Assert
expect(result).toBe(false);
});

it("returns true for a duplicate number wave at the threshold", () => {
// Arrange
const waveTracker = new WaveTracker();

for (let i = 0; i < 25; i += 1) {
waveTracker.addAndCheck({ a: [] });
}

// Act
const result = waveTracker.addAndCheck({ a: [] });

// Assert
expect(result).toBe(true);
});

it("returns false for a duplicate number wave interrupted before at the threshold", () => {
// Arrange
const waveTracker = new WaveTracker();

for (let i = 0; i < 10; i += 1) {
waveTracker.addAndCheck({ a: [] });
}
waveTracker.addAndCheck({ b: [] });

for (let i = 0; i < 15; i += 1) {
waveTracker.addAndCheck({ a: [] });
}

// Act
const result = waveTracker.addAndCheck({ a: [] });

// Assert
expect(result).toBe(false);
});

it("returns true for a duplicate number wave resumed before at the threshold", () => {
// Arrange
const waveTracker = new WaveTracker();

for (let i = 0; i < 10; i += 1) {
waveTracker.addAndCheck({ a: [] });
}
waveTracker.addAndCheck({ b: [] });

for (let i = 0; i < 24; i += 1) {
waveTracker.addAndCheck({ a: [] });
}

// Act
const result = waveTracker.addAndCheck({ a: [] });

// Assert
expect(result).toBe(true);
});
});
});
40 changes: 40 additions & 0 deletions src/runtime/providers/tracking/WaveTracker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Mutation } from "automutate";
import { Dictionary } from "../../../shared/maps";

const createHash = (fileMutations: Dictionary<readonly Mutation[]>) => Object.keys(fileMutations).join(",");

/**
* How many waves in a row must be duplicates of previously seen waves to halt.
*/
const threshold = 25;

export class WaveTracker {
/**
* How many times each wave file names hash has been seen.
*/
#previouslySeen = new Set<string>();

/**
* How many waves in a row have now had repetitions greater than the duplicate threshold.
*/
#repeatedCount = 0;

/**
* @param fileMutations A newly created wave of mutations.
* @returns Whether the wave has reached the duplicate threshold.
*/
addAndCheck(fileMutations: Dictionary<readonly Mutation[]>) {
const hash = createHash(fileMutations);
const previouslySeen = this.#previouslySeen.has(hash);

this.#previouslySeen.add(hash);

if (!previouslySeen) {
this.#repeatedCount = 0;
return false;
}

this.#repeatedCount += 1;
return this.#repeatedCount === threshold;
}
}

0 comments on commit 5e6b82a

Please sign in to comment.