/// /// /// interface FileInformation { contents: string; codepage: number; } interface FindFileResult { } interface IOLog { arguments: string[]; executingPath: string; currentDirectory: string; filesRead: { path: string; codepage: number; result?: FileInformation; }[]; filesWritten: { path: string; contents: string; bom: boolean; }[]; filesDeleted: string[]; filesAppended: { path: string; contents: string; }[]; fileExists: { path: string; result?: boolean; }[]; filesFound: { path: string; pattern: string; result?: FindFileResult; }[]; dirs: { path: string; re: string; re_m: boolean; re_g: boolean; re_i: boolean; opts: { recursive?: boolean; }; result?: string[]; }[]; dirExists: { path: string; result?: boolean; }[]; dirsCreated: string[]; pathsResolved: { path: string; result?: string; }[]; } interface PlaybackControl { startReplayFromFile(logFileName: string): void; startReplayFromString(logContents: string): void; startReplayFromData(log: IOLog): void; endReplay(): void; startRecord(logFileName: string): void; endRecord(): void; } module Playback { var recordLog: IOLog = undefined; var replayLog: IOLog = undefined; var recordLogFileNameBase = ''; interface Memoized { (s: string): T; reset(): void; } function memoize(func: (s: string) => T): Memoized { var lookup: { [s: string]: T } = {}; var run: Memoized = >((s: string) => { if (lookup.hasOwnProperty(s)) return lookup[s]; return lookup[s] = func(s); }); run.reset = () => { lookup = null; }; return run; } export interface PlaybackSystem extends ts.System, PlaybackControl { } function createEmptyLog(): IOLog { return { timestamp: (new Date()).toString(), arguments: [], currentDirectory: '', filesRead: [], filesWritten: [], filesDeleted: [], filesAppended: [], fileExists: [], filesFound: [], dirs: [], dirExists: [], dirsCreated: [], pathsResolved: [], executingPath: '' }; } function initWrapper(wrapper: PlaybackControl, underlying: T) { Object.keys(underlying).forEach(prop => { (wrapper)[prop] = (underlying)[prop]; }); wrapper.startReplayFromString = logString => { wrapper.startReplayFromData(JSON.parse(logString)); }; wrapper.startReplayFromData = log => { replayLog = log; // Remove non-found files from the log (shouldn't really need them, but we still record them for diganostic purposes) replayLog.filesRead = replayLog.filesRead.filter(f => f.result.contents !== undefined); }; wrapper.endReplay = () => { replayLog = undefined; }; wrapper.startRecord = (fileNameBase) => { recordLogFileNameBase = fileNameBase; recordLog = createEmptyLog(); }; } function recordReplay(original: T, underlying: any) { function createWrapper(record: T, replay: T): T { return (function () { if (replayLog !== undefined) { return replay.apply(undefined, arguments); } else if (recordLog !== undefined) { return record.apply(undefined, arguments); } else { return original.apply(underlying, arguments); } }); } return createWrapper; } function callAndRecord(underlyingResult: T, logArray: U[], logEntry: U): T { if (underlyingResult !== undefined) { (logEntry).result = underlyingResult; } logArray.push(logEntry); return underlyingResult; } function findResultByFields(logArray: { result?: T }[], expectedFields: {}, defaultValue?: T): T { var predicate = (entry: { result?: T }) => { return Object.getOwnPropertyNames(expectedFields).every((name) => (entry)[name] === (expectedFields)[name]); }; var results = logArray.filter(entry => predicate(entry)); if (results.length === 0) { if (defaultValue !== undefined) { return defaultValue; } else { throw new Error('No matching result in log array for: ' + JSON.stringify(expectedFields)); } } return results[0].result; } function findResultByPath(wrapper: { resolvePath(s: string): string }, logArray: { path: string; result?: T }[], expectedPath: string, defaultValue?: T): T { var normalizedName = ts.normalizeSlashes(expectedPath).toLowerCase(); // Try to find the result through normal fileName for (var i = 0; i < logArray.length; i++) { if (ts.normalizeSlashes(logArray[i].path).toLowerCase() === normalizedName) { return logArray[i].result; } } // Fallback, try to resolve the target paths as well if (replayLog.pathsResolved.length > 0) { var normalizedResolvedName = wrapper.resolvePath(expectedPath).toLowerCase(); for (var i = 0; i < logArray.length; i++) { if (wrapper.resolvePath(logArray[i].path).toLowerCase() === normalizedResolvedName) { return logArray[i].result; } } } // If we got here, we didn't find a match if (defaultValue === undefined) { throw new Error('No matching result in log array for path: ' + expectedPath); } else { return defaultValue; } } var pathEquivCache: any = {}; function pathsAreEquivalent(left: string, right: string, wrapper: { resolvePath(s: string): string }) { var key = left + '-~~-' + right; function areSame(a: string, b: string) { return ts.normalizeSlashes(a).toLowerCase() === ts.normalizeSlashes(b).toLowerCase(); } function check() { if (Harness.Path.getFileName(left).toLowerCase() === Harness.Path.getFileName(right).toLowerCase()) { return areSame(left, right) || areSame(wrapper.resolvePath(left), right) || areSame(left, wrapper.resolvePath(right)) || areSame(wrapper.resolvePath(left), wrapper.resolvePath(right)); } } if (pathEquivCache.hasOwnProperty(key)) { return pathEquivCache[key]; } else { return pathEquivCache[key] = check(); } } function noOpReplay(name: string) { //console.log("Swallowed write operation during replay: " + name); } export function wrapSystem(underlying: ts.System): PlaybackSystem { var wrapper: PlaybackSystem = {}; initWrapper(wrapper, underlying); wrapper.startReplayFromFile = logFn => { wrapper.startReplayFromString(underlying.readFile(logFn)); }; wrapper.endRecord = () => { if (recordLog !== undefined) { var i = 0; var fn = () => recordLogFileNameBase + i + '.json'; while (underlying.fileExists(fn())) i++; underlying.writeFile(fn(), JSON.stringify(recordLog)); recordLog = undefined; } }; Object.defineProperty(wrapper, 'args', { get() { if (replayLog !== undefined) { return replayLog.arguments; } else if (recordLog !== undefined) { recordLog.arguments = underlying.args; } return underlying.args; } }); wrapper.fileExists = recordReplay(wrapper.fileExists, underlying)( (path) => callAndRecord(underlying.fileExists(path), recordLog.fileExists, { path: path }), memoize((path) => { // If we read from the file, it must exist if (findResultByPath(wrapper, replayLog.filesRead, path, null) !== null) { return true; } else { return findResultByFields(replayLog.fileExists, { path: path }, false); } }) ); wrapper.getExecutingFilePath = () => { if (replayLog !== undefined) { return replayLog.executingPath; } else if (recordLog !== undefined) { return recordLog.executingPath = underlying.getExecutingFilePath(); } else { return underlying.getExecutingFilePath(); } }; wrapper.getCurrentDirectory = () => { if (replayLog !== undefined) { return replayLog.currentDirectory || ''; } else if (recordLog !== undefined) { return recordLog.currentDirectory = underlying.getCurrentDirectory(); } else { return underlying.getCurrentDirectory(); } }; wrapper.resolvePath = recordReplay(wrapper.resolvePath, underlying)( (path) => callAndRecord(underlying.resolvePath(path), recordLog.pathsResolved, { path: path }), memoize((path) => findResultByFields(replayLog.pathsResolved, { path: path }, !ts.isRootedDiskPath(ts.normalizeSlashes(path)) && replayLog.currentDirectory ? replayLog.currentDirectory + '/' + path : ts.normalizeSlashes(path)))); wrapper.readFile = recordReplay(wrapper.readFile, underlying)( (path) => { var result = underlying.readFile(path); var logEntry = { path: path, codepage: 0, result: { contents: result, codepage: 0 } }; recordLog.filesRead.push(logEntry); return result; }, memoize((path) => findResultByPath(wrapper, replayLog.filesRead, path).contents)); wrapper.writeFile = recordReplay(wrapper.writeFile, underlying)( (path, contents) => callAndRecord(underlying.writeFile(path, contents), recordLog.filesWritten, { path: path, contents: contents, bom: false }), (path, contents) => noOpReplay('writeFile')); wrapper.exit = (exitCode) => { if (recordLog !== undefined) { wrapper.endRecord(); } underlying.exit(exitCode); }; return wrapper; } }