// // Copyright (c) Microsoft Corporation. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // /// /// /// /// /// /// // this will work in the browser via browserify var _chai: typeof chai = require('chai'); var assert: typeof _chai.assert = _chai.assert; declare var __dirname: any; // Node-specific var global = Function("return this").call(null); module Utils { var global = Function("return this").call(null); // Setup some globals based on the current environment export enum ExecutionEnvironment { Node, Browser, CScript } export function getExecutionEnvironment() { if (typeof WScript !== "undefined" && typeof ActiveXObject === "function") { return ExecutionEnvironment.CScript; } else if (process && (process).execPath && (process).execPath.indexOf("node") !== -1) { return ExecutionEnvironment.Node; } else { return ExecutionEnvironment.Browser; } } export var currentExecutionEnvironment = getExecutionEnvironment(); export function evalFile(fileContents: string, filename: string, nodeContext?: any) { var environment = getExecutionEnvironment(); switch (environment) { case ExecutionEnvironment.CScript: case ExecutionEnvironment.Browser: eval(fileContents); break; case ExecutionEnvironment.Node: var vm = require('vm'); if (nodeContext) { vm.runInNewContext(fileContents, nodeContext, filename); } else { vm.runInThisContext(fileContents, filename); } break; default: throw new Error('Unknown context'); } } /** Splits the given string on \r\n or on only \n if that fails */ export function splitContentByNewlines(content: string) { // Split up the input file by line // Note: IE JS engine incorrectly handles consecutive delimiters here when using RegExp split, so // we have to string-based splitting instead and try to figure out the delimiting chars var lines = content.split('\r\n'); if (lines.length === 1) { lines = content.split('\n'); } return lines; } /** Reads a file under /tests */ export function readTestFile(path: string) { if (path.indexOf('tests') < 0) { path = "tests/" + path; } try { var content = sys.readFile(Harness.userSpecifiedroot + path); } catch (err) { return undefined; } return content; } export function memoize(f: T): T { var cache: { [idx: string]: any } = {}; return (() => { var key = Array.prototype.join.call(arguments); var cachedResult = cache[key]; if (cachedResult) { return cachedResult; } else { return cache[key] = f.apply(this, arguments); } }) } } module Harness.Path { export function getFileName(fullPath: string) { return fullPath.replace(/^.*[\\\/]/, ''); } export function filePath(fullPath: string) { fullPath = switchToForwardSlashes(fullPath); var components = fullPath.split("/"); var path: string[] = components.slice(0, components.length - 1); return path.join("/") + "/"; } export function switchToForwardSlashes(path: string) { return path.replace(/\\/g, "/").replace(/\/\//g, '/'); } } module Harness { export interface IO { readFile(path: string): string; writeFile(path: string, contents: string): void; directoryName(path: string): string; createDirectory(path: string): void; fileExists(filename: string): boolean; directoryExists(path: string): boolean; deleteFile(filename: string): void; listFiles(path: string, filter: RegExp, options?: { recursive?: boolean }): string[]; log(text: string): void; } module IOImpl { declare class Enumerator { public atEnd(): boolean; public moveNext(): boolean; public item(): any; constructor(o: any); } export module CScript { var fso: any; if (global.ActiveXObject) { fso = new global.ActiveXObject("Scripting.FileSystemObject"); } else { fso = {}; } export var readFile: typeof IO.readFile = sys.readFile; export var writeFile: typeof IO.writeFile = sys.writeFile; export var directoryName: typeof IO.directoryName = fso.GetParentFolderName; export var directoryExists: typeof IO.directoryExists = fso.FolderExists; export var fileExists: typeof IO.fileExists = fso.FileExists; export var log: typeof IO.log = global.WScript && global.WScript.StdOut.WriteLine; export function createDirectory(path: string) { if (directoryExists(path)) { fso.CreateFolder(path); } } export function deleteFile(path: string) { if (fileExists(path)) { fso.DeleteFile(path, true); // true: delete read-only files } } export var listFiles: typeof IO.listFiles = (path, spec?, options?) => { options = options || <{ recursive?: boolean; }>{}; function filesInFolder(folder: any, root: string): string[] { var paths: string[] = []; var fc: any; if (options.recursive) { fc = new Enumerator(folder.subfolders); for (; !fc.atEnd(); fc.moveNext()) { paths = paths.concat(filesInFolder(fc.item(), root + "\\" + fc.item().Name)); } } fc = new Enumerator(folder.files); for (; !fc.atEnd(); fc.moveNext()) { if (!spec || fc.item().Name.match(spec)) { paths.push(root + "\\" + fc.item().Name); } } return paths; } var folder: any = fso.GetFolder(path); var paths: string[] = []; return filesInFolder(folder, path); } } export module Node { declare var require: any; var fs: any, pathModule: any; if (require) { fs = require('fs'); pathModule = require('path'); } else { fs = pathModule = {}; } export var readFile: typeof IO.readFile = sys.readFile; export var writeFile: typeof IO.writeFile = sys.writeFile; export var fileExists: typeof IO.fileExists = fs.existsSync; export var log: typeof IO.log = console.log; export function createDirectory(path: string) { if (!directoryExists(path)) { fs.mkdirSync(path); } } export function deleteFile(path: string) { try { fs.unlinkSync(path); } catch (e) { } } export function directoryExists(path: string): boolean { return fs.existsSync(path) && fs.statSync(path).isDirectory(); } export function directoryName(path: string) { var dirPath = pathModule.dirname(path); // Node will just continue to repeat the root path, rather than return null if (dirPath === path) { dirPath = null; } else { return dirPath; } } export var listFiles: typeof IO.listFiles = (path, spec?, options?) => { options = options || <{ recursive?: boolean; }>{}; function filesInFolder(folder: string): string[] { var paths: string[] = []; var files = fs.readdirSync(folder); for (var i = 0; i < files.length; i++) { var pathToFile = pathModule.join(folder, files[i]); var stat = fs.statSync(pathToFile); if (options.recursive && stat.isDirectory()) { paths = paths.concat(filesInFolder(pathToFile)); } else if (stat.isFile() && (!spec || files[i].match(spec))) { paths.push(pathToFile); } } return paths; } return filesInFolder(path); } } export module Network { var serverRoot = "http://localhost:8888/"; // Unused? var newLine = '\r\n'; var currentDirectory = () => ''; var supportsCodePage = () => false; module Http { function waitForXHR(xhr: XMLHttpRequest) { while (xhr.readyState !== 4) { } return { status: xhr.status, responseText: xhr.responseText }; } /// Ask the server to use node's path.resolve to resolve the given path function getResolvedPathFromServer(path: string) { var xhr = new XMLHttpRequest(); try { xhr.open("GET", path + "?resolve", false); xhr.send(); } catch (e) { return { status: 404, responseText: null }; } return waitForXHR(xhr); } export interface XHRResponse { status: number; responseText: string; } /// Ask the server for the contents of the file at the given URL via a simple GET request export function getFileFromServerSync(url: string): XHRResponse { var xhr = new XMLHttpRequest(); try { xhr.open("GET", url, false); xhr.send(); } catch (e) { return { status: 404, responseText: null }; } return waitForXHR(xhr); } /// Submit a POST request to the server to do the given action (ex WRITE, DELETE) on the provided URL export function writeToServerSync(url: string, action: string, contents?: string): XHRResponse { var xhr = new XMLHttpRequest(); try { var action = '?action=' + action; xhr.open('POST', url + action, false); xhr.setRequestHeader('Access-Control-Allow-Origin', '*'); xhr.send(contents); } catch (e) { return { status: 500, responseText: null }; } return waitForXHR(xhr); } } export function createDirectory(path: string) { // Do nothing (?) } export function deleteFile(path: string) { Http.writeToServerSync(serverRoot + path, 'DELETE', null); } export function directoryExists(path: string): boolean { return false; } function directoryNameImpl(path: string) { var dirPath = path; // root of the server if (dirPath.match(/localhost:\d+$/) || dirPath.match(/localhost:\d+\/$/)) { dirPath = null; // path + filename } else if (dirPath.indexOf('.') === -1) { dirPath = dirPath.substring(0, dirPath.lastIndexOf('/')); // path } else { // strip any trailing slash if (dirPath.match(/.*\/$/)) { dirPath = dirPath.substring(0, dirPath.length - 2); } var dirPath = dirPath.substring(0, dirPath.lastIndexOf('/')); } return dirPath; } export var directoryName: typeof IO.directoryName = Utils.memoize(directoryNameImpl); export function fileExists(path: string): boolean { var response = Http.getFileFromServerSync(serverRoot + path); return response.status === 200; } export function _listFilesImpl(path: string, spec?: RegExp, options?: any) { var response = Http.getFileFromServerSync(serverRoot + path); if (response.status === 200) { var results = response.responseText.split(','); if (spec) { return results.filter(file => spec.test(file)); } else { return results; } } else { return ['']; } }; export var listFiles = Utils.memoize(_listFilesImpl); export var log = console.log; export function readFile(file: string) { var response = Http.getFileFromServerSync(serverRoot + file); if (response.status === 200) { return response.responseText; } else { return null; } } export function writeFile(path: string, contents: string) { Http.writeToServerSync(serverRoot + path, 'WRITE', contents); } } } export var IO: IO; switch (Utils.getExecutionEnvironment()) { case Utils.ExecutionEnvironment.CScript: IO = IOImpl.CScript; break; case Utils.ExecutionEnvironment.Node: IO = IOImpl.Node; break; case Utils.ExecutionEnvironment.Browser: IO = IOImpl.Network; break; } } module Harness { var typescriptServiceFileName = "typescriptServices.js"; // Services files are exported because we need to eval them at global scope in order for them to be available everywhere export var typescriptServiceFile: string; var tcServicesFilename = "services.js"; export var libFolder: string; switch (Utils.getExecutionEnvironment()) { case Utils.ExecutionEnvironment.CScript: libFolder = Path.filePath(global['WScript'].ScriptFullName); tcServicesFilename = "built/local/services.js"; break; case Utils.ExecutionEnvironment.Node: libFolder = (__dirname + '/'); tcServicesFilename = "built/local/services.js"; break; case Utils.ExecutionEnvironment.Browser: libFolder = "bin/"; tcServicesFilename = "built/local/services.js"; break; default: throw new Error('Unknown context'); } export var tcServicesFile = IO.readFile(tcServicesFilename); export interface SourceMapEmitterCallback { (emittedFile: string, emittedLine: number, emittedColumn: number, sourceFile: string, sourceLine: number, sourceColumn: number, sourceName: string): void; } // Settings export var userSpecifiedroot = ""; /** Functionality for compiling TypeScript code */ export module Compiler { /** Aggregate various writes into a single array of lines. Useful for passing to the * TypeScript compiler to fill with source code or errors. */ export class WriterAggregator implements ITextWriter { public lines: string[] = []; public currentLine = undefined; public Write(str: string) { // out of memory usage concerns avoid using + or += if we're going to do any manipulation of this string later this.currentLine = [(this.currentLine || ''), str].join(''); } public WriteLine(str: string) { // out of memory usage concerns avoid using + or += if we're going to do any manipulation of this string later this.lines.push([(this.currentLine || ''), str].join('')); this.currentLine = undefined; } public Close() { if (this.currentLine !== undefined) { this.lines.push(this.currentLine); } this.currentLine = undefined; } public reset() { this.lines = []; this.currentLine = undefined; } } export interface IEmitterIOHost { writeFile(path: string, contents: string, writeByteOrderMark: boolean): void; resolvePath(path: string): string; } /** Mimics having multiple files, later concatenated to a single file. */ export class EmitterIOHost implements IEmitterIOHost { private fileCollection: any = {}; /** create file gets the whole path to create, so this works as expected with the --out parameter */ public writeFile(s: string, contents: string, writeByteOrderMark: boolean): void { var writer: ITextWriter; if (this.fileCollection[s]) { writer = this.fileCollection[s]; } else { writer = new Harness.Compiler.WriterAggregator(); this.fileCollection[s] = writer; } writer.Write(contents); writer.Close(); } public resolvePath(s: string) { return s; } public reset() { this.fileCollection = {}; } public toArray(): { fileName: string; file: WriterAggregator; }[] { var result: { fileName: string; file: WriterAggregator; }[] = []; for (var p in this.fileCollection) { if (this.fileCollection.hasOwnProperty(p)) { var current = this.fileCollection[p]; if (current.lines.length > 0) { if (p.indexOf('.d.ts') !== -1) { current.lines.unshift(['////[', Path.getFileName(p), ']'].join('')); } result.push({ fileName: p, file: this.fileCollection[p] }); } } } return result; } } export var libText = IO.readFile(libFolder + "lib.d.ts"); export var libTextMinimal = IO.readFile('bin/lib.core.d.ts'); export function createCompilerHost(filemap: { [filename: string]: ts.SourceFile; }, writeFile: (fn: string, contents: string) => void): ts.CompilerHost { return { getCurrentDirectory: sys.getCurrentDirectory, getCancellationToken: (): any => undefined, getSourceFile: (fn, languageVersion) => { if (fn in filemap) { return filemap[fn]; } else { var lib = 'lib.d.ts'; if (fn.substr(fn.length - lib.length) === lib) { return filemap[fn] = ts.createSourceFile('lib.d.ts', libTextMinimal, languageVersion); } // Don't throw here -- the compiler might be looking for a test that actually doesn't exist as part of the TC return null; } }, getDefaultLibFilename: () => 'lib.d.ts', writeFile: writeFile, getCanonicalFileName: getCanonicalFileName, useCaseSensitiveFileNames: () => sys.useCaseSensitiveFileNames } } export class HarnessCompiler { private inputFiles: { unitName: string; content: string }[] = []; private compileOptions: ts.CompilerOptions; private settings: Harness.TestCaseParser.CompilerSetting[] = []; private lastErrors: MinimalDiagnostic[]; public reset() { this.inputFiles = []; this.settings = []; this.lastErrors = []; } public emitAllDeclarations() { // NEWTODO: Do something here? } public reportCompilationErrors() { return this.lastErrors; } public setCompilerSettingsFromOptions(tcSettings: ts.CompilerOptions) { this.settings = Object.keys(tcSettings).map(k => ({ flag: k, value: (tcSettings)[k] })); } public setCompilerSettings(tcSettings: Harness.TestCaseParser.CompilerSetting[]) { this.settings = tcSettings; } public addInputFiles(files: { unitName: string; content: string }[]) { files.forEach(file => this.addInputFile(file)); } public addInputFile(file: { unitName: string; content: string }) { this.inputFiles.push(file); } public compile(options?: ts.CompilerOptions) { this.compileOptions = options || { noResolve: false }; } public emitAll(ioHost?: IEmitterIOHost) { this.compileFiles(this.inputFiles, [], (result) => { result.files.forEach(file => { ioHost.writeFile(file.fileName, file.code, false); }); }, () => { }, this.compileOptions); } public compileFiles(inputFiles: { unitName: string; content: string }[], otherFiles: { unitName: string; content?: string }[], onComplete: (result: CompilerResult) => void, settingsCallback?: (settings: ts.CompilerOptions) => void, options?: ts.CompilerOptions) { options = options || { noResolve: false }; options.target = options.target || ts.ScriptTarget.ES3; if (settingsCallback) { settingsCallback(null); } this.settings.forEach(setting => { switch (setting.flag.toLowerCase()) { // "filename", "comments", "declaration", "module", "nolib", "sourcemap", "target", "out", "outDir", "noimplicitany", "noresolve" case "module": case "modulegentarget": if (typeof setting.value === 'string') { if (setting.value.toLowerCase() === 'amd') { options.module = ts.ModuleKind.AMD; } else if (setting.value.toLowerCase() === 'commonjs') { options.module = ts.ModuleKind.CommonJS; } else if (setting.value.toLowerCase() === 'unspecified') { options.module = ts.ModuleKind.None; } else { throw new Error('Unknown module type ' + setting.value); } } else { options.module = setting.value; } break; case "target": case 'codegentarget': if (typeof setting.value === 'string') { if (setting.value.toLowerCase() === 'es3') { options.target = ts.ScriptTarget.ES3; } else if (setting.value.toLowerCase() === 'es5') { options.target = ts.ScriptTarget.ES5; } else { throw new Error('Unknown compile target ' + setting.value); } } else { options.target = setting.value; } break; case 'noresolve': options.noResolve = !!setting.value; break; case 'noimplicitany': options.noImplicitAny = !!setting.value; break; case 'nolib': options.noLib = !!setting.value; break; case 'out': case 'outfileoption': options.out = setting.value; break; case 'outdiroption': case 'outdir': options.outDir = setting.value; break; case 'sourceroot': options.sourceRoot = setting.value; break; case 'sourcemap': options.sourceMap = !!setting.value; break; case 'declaration': options.declaration = !!setting.value; break; case 'mapsourcefiles': case 'maproot': case 'generatedeclarationfiles': case 'usecasesensitivefileresolution': case 'gatherDiagnostics': case 'codepage': case 'createFileLog': case 'comments': case 'filename': case 'propagateenumconstants': case 'removecomments': case 'watch': case 'allowautomaticsemicoloninsertion': case 'locale': // Not supported yet break; default: throw new Error('Unsupported compiler setting ' + setting.flag); } }); var filemap: { [name: string]: ts.SourceFile; } = {}; var register = (file: { unitName: string; content: string; }) => { var filename = Path.switchToForwardSlashes(file.unitName); filemap[filename] = ts.createSourceFile(filename, file.content, options.target || ts.ScriptTarget.ES3); }; inputFiles.forEach(register); otherFiles.forEach(register); var fileOutputs: { fileName: string; file: string; }[] = []; var programFiles = inputFiles.map(file => file.unitName); var program = ts.createProgram(programFiles, options, createCompilerHost(filemap, (fn, contents) => fileOutputs.push({ fileName: fn, file: contents }))); var hadParseErrors = program.getDiagnostics().length > 0; var checker = program.getTypeChecker(); checker.checkProgram(); // only emit if there weren't parse errors var sourceMapData: ts.SourceMapData[]; if (!hadParseErrors) { sourceMapData = checker.emitFiles(); } var errors: MinimalDiagnostic[] = []; program.getDiagnostics().concat(checker.getDiagnostics()).forEach(err => { // TODO: new compiler formats errors after this point to add . and newlines so we'll just do it manually for now errors.push({ filename: err.file && err.file.filename, start: err.start, end: err.start + err.length, line: 0, character: 0, message: err.messageText }); }); this.lastErrors = errors; var result = new CompilerResult(fileOutputs, errors, []); // Covert the source Map data into the baseline result.updateSourceMapRecord(program, sourceMapData); onComplete(result); return options; } } /* TODO: Delete? export function makeDefaultCompilerSettings(options?: { useMinimalDefaultLib: boolean; noImplicitAny: boolean; }) { var useMinimalDefaultLib = options ? options.useMinimalDefaultLib : true; var noImplicitAny = options ? options.noImplicitAny : false; var settings = new TypeScript.CompilationSettings(); settings.codeGenTarget = TypeScript.LanguageVersion.EcmaScript5; settings.moduleGenTarget = TypeScript.ModuleGenTarget.Synchronous; settings.noLib = useMinimalDefaultLib; settings.noResolve = false; settings.noImplicitAny = noImplicitAny; return settings; } */ /** Recreate the harness compiler instance to its default settings */ export function recreate(options?: { useMinimalDefaultLib: boolean; noImplicitAny: boolean; }) { } /** The harness' compiler instance used when tests are actually run. Reseting or changing settings of this compiler instance must be done within a testcase (i.e., describe/it) */ var harnessCompiler: HarnessCompiler; /** Returns the singleton harness compiler instance for generating and running tests. If required a fresh compiler instance will be created, otherwise the existing singleton will be re-used. */ export function getCompiler(opts?: { useExistingInstance: boolean; optionsForFreshInstance?: { useMinimalDefaultLib: boolean; noImplicitAny: boolean; } }) { return harnessCompiler = harnessCompiler || new HarnessCompiler(); } // This does not need to exist strictly speaking, but many tests will need to be updated if it's removed export function compileString(code: string, unitName: string, callback: (result: CompilerResult) => void) { // NEWTODO: Re-implement 'compileString' throw new Error('compileString NYI'); //var harnessCompiler = Harness.Compiler.getCompiler(Harness.Compiler.CompilerInstance.RunTime); //harnessCompiler.compileString(code, unitName, callback); } export interface MinimalDiagnostic { filename: string; start: number; end: number; line: number; character: number; message: string; } export interface GeneratedFile { fileName: string; code: string; } export function stringEndsWith(str: string, end: string) { return str.substr(str.length - end.length) === end; } /** Contains the code and errors of a compilation and some helper methods to check its status. */ export class CompilerResult { public files: GeneratedFile[] = []; public errors: MinimalDiagnostic[] = []; public declFilesCode: GeneratedFile[] = []; public sourceMaps: GeneratedFile[] = []; public sourceMapRecord: string; /** @param fileResults an array of strings for the fileName and an ITextWriter with its code */ constructor(fileResults: { fileName: string; file: string; }[], errors: MinimalDiagnostic[], sourceMapRecordLines: string[]) { var lines: string[] = []; fileResults.forEach(emittedFile => { var fileObj = { fileName: emittedFile.fileName, code: emittedFile.file }; if (stringEndsWith(emittedFile.fileName, '.d.ts')) { // .d.ts file, add to declFiles emit this.declFilesCode.push(fileObj); } else if (stringEndsWith(emittedFile.fileName, '.js')) { // .js file, add to files this.files.push(fileObj); } else if (stringEndsWith(emittedFile.fileName, '.js.map')) { this.sourceMaps.push(fileObj); } else { throw new Error('Unrecognized file extension for file ' + emittedFile.fileName); } }); this.errors = errors; this.sourceMapRecord = sourceMapRecordLines.join('\r\n'); } public updateSourceMapRecord(program: ts.Program, sourceMapData: ts.SourceMapData[]) { if (sourceMapData) { this.sourceMapRecord = Harness.SourceMapRecoder.getSourceMapRecord(sourceMapData, program, this.files); } } public isErrorAt(line: number, column: number, message: string) { for (var i = 0; i < this.errors.length; i++) { if ((this.errors[i].line + 1) === line && (this.errors[i].character + 1) === column && this.errors[i].message === message) return true; } return false; } } } export module TestCaseParser { /** all the necesarry information to set the right compiler settings */ export interface CompilerSetting { flag: string; value: string; } /** All the necessary information to turn a multi file test into useful units for later compilation */ export interface TestUnitData { content: string; name: string; fileOptions: any; originalFilePath: string; references: string[]; } // Regex for parsing options in the format "@Alpha: Value of any sort" var optionRegex = /^[\/]{2}\s*@(\w+)\s*:\s*(\S*)/gm; // multiple matches on multiple lines // List of allowed metadata names var fileMetadataNames = ["filename", "comments", "declaration", "module", "nolib", "sourcemap", "target", "out", "outDir", "noimplicitany", "noresolve"]; function extractCompilerSettings(content: string): CompilerSetting[] { var opts: CompilerSetting[] = []; var match: RegExpExecArray; while ((match = optionRegex.exec(content)) != null) { opts.push({ flag: match[1], value: match[2] }); } return opts; } /** Given a test file containing // @Filename directives, return an array of named units of code to be added to an existing compiler instance */ export function makeUnitsFromTest(code: string, fileName: string): { settings: CompilerSetting[]; testUnitData: TestUnitData[]; } { var settings = extractCompilerSettings(code); // List of all the subfiles we've parsed out var files: TestUnitData[] = []; var lines = Utils.splitContentByNewlines(code); // Stuff related to the subfile we're parsing var currentFileContent: string = null; var currentFileOptions: any = {}; var currentFileName: any = null; var refs: string[] = []; for (var i = 0; i < lines.length; i++) { var line = lines[i]; var testMetaData = optionRegex.exec(line); if (testMetaData) { // Comment line, check for global/file @options and record them optionRegex.lastIndex = 0; var fileNameIndex = fileMetadataNames.indexOf(testMetaData[1].toLowerCase()); if (fileNameIndex === -1) { throw new Error('Unrecognized metadata name "' + testMetaData[1] + '". Available file metadata names are: ' + fileMetadataNames.join(', ')); } else if (fileNameIndex === 0) { currentFileOptions[testMetaData[1]] = testMetaData[2]; } else { continue; } // New metadata statement after having collected some code to go with the previous metadata if (currentFileName) { // Store result file var newTestFile = { content: currentFileContent, name: currentFileName, fileOptions: currentFileOptions, originalFilePath: fileName, references: refs }; files.push(newTestFile); // Reset local data currentFileContent = null; currentFileOptions = {}; currentFileName = testMetaData[2]; refs = []; } else { // First metadata marker in the file currentFileName = testMetaData[2]; } } else { // Subfile content line // Append to the current subfile content, inserting a newline needed if (currentFileContent === null) { currentFileContent = ''; } else { // End-of-line currentFileContent = currentFileContent + '\n'; } currentFileContent = currentFileContent + line; } } // normalize the fileName for the single file case currentFileName = files.length > 0 ? currentFileName : Path.getFileName(fileName); // EOF, push whatever remains var newTestFile2 = { content: currentFileContent || '', name: currentFileName, fileOptions: currentFileOptions, originalFilePath: fileName, references: refs }; files.push(newTestFile2); return { settings: settings, testUnitData: files }; } } /** Support class for baseline files */ export module Baseline { var firstRun = true; export interface BaselineOptions { LineEndingSensitive?: boolean; Subfolder?: string; } export function localPath(fileName: string, subfolder?: string) { return baselinePath(fileName, 'local', subfolder); } function referencePath(fileName: string, subfolder?: string) { return baselinePath(fileName, 'reference', subfolder); } function baselinePath(fileName: string, type: string, subfolder?: string) { if (subfolder !== undefined) { return Harness.userSpecifiedroot + 'tests/baselines/' + subfolder + '/' + type + '/' + fileName; } else { return Harness.userSpecifiedroot + 'tests/baselines/' + type + '/' + fileName; } } var fileCache: { [idx: string]: boolean } = {} function generateActual(actualFilename: string, generateContent: () => string): string { // For now this is written using TypeScript, because sys is not available when running old test cases. // But we need to move to sys once we have // Creates the directory including its parent if not already present function createDirectoryStructure(dirName: string) { if (fileCache[dirName] || IO.directoryExists(dirName)) { fileCache[dirName] = true; return; } var parentDirectory = IO.directoryName(dirName); if (parentDirectory != "") { createDirectoryStructure(parentDirectory); } IO.createDirectory(dirName); fileCache[dirName] = true; } var parentDir = IO.directoryName(actualFilename); // .../tests/baselines/local var parentParentDir = IO.directoryName(IO.directoryName(actualFilename)) // .../tests/baselines // Create folders if needed createDirectoryStructure(Harness.IO.directoryName(actualFilename)); // Delete the actual file in case it fails if (IO.fileExists(actualFilename)) { IO.deleteFile(actualFilename); } var actual = generateContent(); if (actual === undefined) { throw new Error('The generated content was "undefined". Return "null" if no baselining is required."'); } // Store the content in the 'local' folder so we // can accept it later (manually) if (actual !== null) { IO.writeFile(actualFilename, actual); } return actual; } function compareToBaseline(actual: string, relativeFilename: string, opts: BaselineOptions) { // actual is now either undefined (the generator had an error), null (no file requested), // or some real output of the function if (actual === undefined) { // Nothing to do return; } var refFilename = referencePath(relativeFilename, opts && opts.Subfolder); if (actual === null) { actual = ''; } var expected = ''; if (IO.fileExists(refFilename)) { expected = IO.readFile(refFilename); } var lineEndingSensitive = opts && opts.LineEndingSensitive; if (!lineEndingSensitive) { expected = expected.replace(/\r\n?/g, '\n') actual = actual.replace(/\r\n?/g, '\n') } return { expected: expected, actual: actual }; } function writeComparison(expected: string, actual: string, relativeFilename: string, actualFilename: string, descriptionForDescribe: string) { if (expected != actual) { // Overwrite & issue error var errMsg = 'The baseline file ' + relativeFilename + ' has changed'; throw new Error(errMsg); } } export function runBaseline( descriptionForDescribe: string, relativeFilename: string, generateContent: () => string, runImmediately = false, opts?: BaselineOptions): void { var actual = undefined; var actualFilename = localPath(relativeFilename, opts && opts.Subfolder); if (runImmediately) { actual = generateActual(actualFilename, generateContent); var comparison = compareToBaseline(actual, relativeFilename, opts); writeComparison(comparison.expected, comparison.actual, relativeFilename, actualFilename, descriptionForDescribe); } else { actual = generateActual(actualFilename, generateContent); var comparison = compareToBaseline(actual, relativeFilename, opts); writeComparison(comparison.expected, comparison.actual, relativeFilename, actualFilename, descriptionForDescribe); } } } export function isLibraryFile(filePath: string): boolean { return filePath.indexOf('lib.d.ts') >= 0 || filePath.indexOf('lib.core.d.ts') >= 0; } if (Error) (Error).stackTraceLimit = 100; } // TODO: not sure why Utils.evalFile isn't working with this, eventually will concat it like old compiler instead of eval eval(Harness.tcServicesFile);