<commit_before>﻿import {FeatureParser, ITestReporter} from './Parser';
import {StepCollection} from './Steps';

export class SpecRunner {
    private steps: StepCollection = new StepCollection();
    private excludedTags: string[] = [];

    constructor(private testReporter: ITestReporter = new TestReporter()) { }

    addStep(expression: RegExp, step: Function) {
        this.steps.add(expression, step);
    }

    run(...url: string[]) {
        this.readFile(0, url);
    }

    excludeTags(...tags: string[]) {
        for (var i = 0; i < tags.length; i++) {
            this.excludedTags.push(tags[i].replace(/@/g, ''));
        }
    }

    private readFile(index: number, url: string[]) {
        // TODO: detect local file path and use node js to read file
        // This is the default for browser-based tests...
        var cacheBust = '?cb=' + new Date().getTime();
        if (index < url.length) {
            var nextIndex = index + 1;

            var finalCallback = () => { };
            if (nextIndex === url.length) {
                finalCallback = () => { this.testReporter.complete(); };
            }
            this.getFile(url[index], cacheBust, () => { this.readFile(nextIndex, url); }, finalCallback);
        }
    }

    private getFile(url: string, cacheBust: string, successCallback: Function, allCallback: Function) {
        var client = new XMLHttpRequest();
        client.open('GET', url + cacheBust);
        client.onreadystatechange = () => {
            if (client.readyState === 4) {
                try {
                    if (client.status === 200) {
                        this.processSpecification(client.responseText);
                        successCallback();
                    } else {
                        this.testReporter.error('getFile', url, new Error('Error loading specification: ' + client.statusText + ' (' + client.status + ').'));
                    }
                } finally {
                    allCallback();
                }
            }
        }
        client.send();
    }

    private processSpecification(spec: string) {

        var hasParsed = true;
        var composer = new FeatureParser(this.steps, this.testReporter, this.excludedTags);

        /* Normalise line endings before splitting */
        var lines = spec.replace('\r\n', '\n').split('\n');

        for (var i = 0; i < lines.length; i++) {
            var line = lines[i];

            try {
                composer.process(line);
            } catch (ex) {
                hasParsed = false;
                var state = composer.state[0] || { featureTitle: 'Unknown' };
                this.testReporter.error(state.featureTitle, line, ex);
            }
        }

        if (hasParsed) {
            composer.run();
        }
    }
}

export class TestReporter implements ITestReporter {
    summary(featureTitle: string, scenarioTitle: string, isSuccess: boolean) {
        console.info((isSuccess ? '✔' : '✘') + ' ' + featureTitle + ' : ' + scenarioTitle + '\n');
    }

    error(featureTitle: string, condition: string, error: Error) {
        console.error(featureTitle + '\n\n' + condition + '\n\n' + error);
    }

    information(message: string) {
        console.log(message);
    }

    complete() {
        console.log('Run has finished');
    }

    protected escape(input: string) {
        return input.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
    }
}

class TapResult {
    constructor(public hash: number, public isOk: boolean, public description: string) { }

    output() {
        return (this.isOk ? '' : 'not ') + 'ok #' + this.hash + ' ' + this.description
    }
}

export class TapReporter implements ITestReporter {
    private hash: number = 0;
    private results: TapResult[] = [];

    summary(featureTitle: string, scenarioTitle: string, isSuccess: boolean) {
        this.hash++;
        this.results.push(new TapResult(this.hash, isSuccess, featureTitle + ': ' + scenarioTitle));
    }

    error(featureTitle: string, condition: string, error: Error) {
    }

    information(message: string) {
    }

    complete() {
        console.log('1..' + this.results.length);
        for (var i = 0; i < this.results.length; i++) {
            console.log(this.results[i].output());
        }
    }
}

// Assertions lifted from tsUnit and made static here.

export interface IThrowsParameters {
    fn: () => void;
    message?: string;
    errorString?: string;
}

export class Assert {
    public static areIdentical(expected: any, actual: any, message = ''): void {
        if (expected !== actual) {
            throw this.getError('areIdentical failed when given ' +
                this.printVariable(expected) + ' and ' + this.printVariable(actual),
                message);
        }
    }

    public static areNotIdentical(expected: any, actual: any, message = ''): void {
        if (expected === actual) {
            throw this.getError('areNotIdentical failed when given ' +
                this.printVariable(expected) + ' and ' + this.printVariable(actual),
                message);
        }
    }

    public static areCollectionsIdentical(expected: any[], actual: any[], message = ''): void {
        function resultToString(result: number[]): string {
            var msg = '';

            while (result.length > 0) {
                msg = '[' + result.pop() + ']' + msg;
            }

            return msg;
        }

        var compareArray = (expected: any[], actual: any[], result: number[]): void => {
            var indexString = '';

            if (expected === null) {
                if (actual !== null) {
                    indexString = resultToString(result);
                    throw this.getError('areCollectionsIdentical failed when array a' +
                        indexString + ' is null and b' +
                        indexString + ' is not null',
                        message);
                }

                return; // correct: both are nulls
            } else if (actual === null) {
                indexString = resultToString(result);
                throw this.getError('areCollectionsIdentical failed when array a' +
                    indexString + ' is not null and b' +
                    indexString + ' is null',
                    message);
            }

            if (expected.length !== actual.length) {
                indexString = resultToString(result);
                throw this.getError('areCollectionsIdentical failed when length of array a' +
                    indexString + ' (length: ' + expected.length + ') is different of length of array b' +
                    indexString + ' (length: ' + actual.length + ')',
                    message);
            }

            for (var i = 0; i < expected.length; i++) {
                if ((expected[i] instanceof Array) && (actual[i] instanceof Array)) {
                    result.push(i);
                    compareArray(expected[i], actual[i], result);
                    result.pop();
                } else if (expected[i] !== actual[i]) {
                    result.push(i);
                    indexString = resultToString(result);
                    throw this.getError('areCollectionsIdentical failed when element a' +
                        indexString + ' (' + this.printVariable(expected[i]) + ') is different than element b' +
                        indexString + ' (' + this.printVariable(actual[i]) + ')',
                        message);
                }
            }

            return;
        }

        compareArray(expected, actual, []);
    }

    public static areCollectionsNotIdentical(expected: any[], actual: any[], message = ''): void {
        try {
            this.areCollectionsIdentical(expected, actual);
        } catch (ex) {
            return;
        }

        throw this.getError('areCollectionsNotIdentical failed when both collections are identical', message);
    }

    public static isTrue(actual: boolean, message = '') {
        if (!actual) {
            throw this.getError('isTrue failed when given ' + this.printVariable(actual), message);
        }
    }

    public static isFalse(actual: boolean, message = '') {
        if (actual) {
            throw this.getError('isFalse failed when given ' + this.printVariable(actual), message);
        }
    }

    public static isTruthy(actual: any, message = '') {
        if (!actual) {
            throw this.getError('isTrue failed when given ' + this.printVariable(actual), message);
        }
    }

    public static isFalsey(actual: any, message = '') {
        if (actual) {
            throw this.getError('isFalse failed when given ' + this.printVariable(actual), message);
        }
    }

    public static isString(actual: any, message = '') {
        if (typeof actual !== 'string') {
            throw this.getError('isString failed when given ' + this.printVariable(actual), message);
        }
    }

    public static isNumber(actual: any, message = '') {
        if (typeof actual !== 'number') {
            throw this.getError('isNumber failed when given ' + this.printVariable(actual), message);
        }
    }

    public static isBoolean(actual: any, message = '') {
        if (typeof actual !== 'boolean') {
            throw this.getError('isBoolean failed when given ' + this.printVariable(actual), message);
        }
    }

    public static throws(params: IThrowsParameters): void;
    public static throws(actual: () => void, message?: string): void;
    public static throws(a: any, message = '', errorString = '') {
        var actual: () => void;

        if (typeof a === 'function') {
            actual = a;
        } else if (a.fn) {
            actual = a.fn;
            message = a.message;
            errorString = a.exceptionString;
        }

        var isThrown = false;
        try {
            actual();
        } catch (ex) {
            if (!errorString || ex.message === errorString) {
                isThrown = true;
            }

            if (errorString && ex.message !== errorString) {
                throw this.getError('different error string than supplied');
            }

        }
        if (!isThrown) {
            throw this.getError('did not throw an error', message || '');
        }
    }

    public static doesNotThrow(actual: () => void, message?: string): void {
        try {
            actual();
        } catch (ex) {
            throw this.getError('threw an error ' + ex, message || '');
        }
    }

    public static executesWithin(actual: () => void, timeLimit: number, message: string = null): void {
        function getTime() {
            return window.performance.now();
        }

        function timeToString(value: number) {
            return Math.round(value * 100) / 100;
        }

        var startOfExecution = getTime();

        try {
            actual();
        } catch (ex) {
            throw this.getError('isExecuteTimeLessThanLimit fails when given code throws an exception: "' + ex + '"', message);
        }

        var executingTime = getTime() - startOfExecution;
        if (executingTime > timeLimit) {
            throw this.getError('isExecuteTimeLessThanLimit fails when execution time of given code (' + timeToString(executingTime) + ' ms) ' +
                'exceed the given limit(' + timeToString(timeLimit) + ' ms)',
                message);
        }
    }

    public static fail(message = '') {
        throw this.getError('fail', message);
    }

    private static getError(resultMessage: string, message: string = '') {
        if (message) {
            return new Error(resultMessage + '. ' + message);
        }

        return new Error(resultMessage);
    }

    private static getNameOfClass(inputClass: {}) {
        // see: https://www.stevefenton.co.uk/Content/Blog/Date/201304/Blog/Obtaining-A-Class-Name-At-Runtime-In-TypeScript/
        var funcNameRegex = /function (.{1,})\(/;
        var results = (funcNameRegex).exec((<any>inputClass).constructor.toString());
        return (results && results.length > 1) ? results[1] : '';
    }

    private static printVariable(variable: any) {
        if (variable === null) {
            return '"null"';
        }

        if (typeof variable === 'object') {
            return '{object: ' + Assert.getNameOfClass(variable) + '}';
        }

        return '{' + (typeof variable) + '} "' + variable + '"';
    }
}<commit_msg>Remove "#" from TAP output... slight mistake on my part!<commit_after>﻿import {FeatureParser, ITestReporter} from './Parser';
import {StepCollection} from './Steps';

export class SpecRunner {
    private steps: StepCollection = new StepCollection();
    private excludedTags: string[] = [];

    constructor(private testReporter: ITestReporter = new TestReporter()) { }

    addStep(expression: RegExp, step: Function) {
        this.steps.add(expression, step);
    }

    run(...url: string[]) {
        this.readFile(0, url);
    }

    excludeTags(...tags: string[]) {
        for (var i = 0; i < tags.length; i++) {
            this.excludedTags.push(tags[i].replace(/@/g, ''));
        }
    }

    private readFile(index: number, url: string[]) {
        // TODO: detect local file path and use node js to read file
        // This is the default for browser-based tests...
        var cacheBust = '?cb=' + new Date().getTime();
        if (index < url.length) {
            var nextIndex = index + 1;

            var finalCallback = () => { };
            if (nextIndex === url.length) {
                finalCallback = () => { this.testReporter.complete(); };
            }
            this.getFile(url[index], cacheBust, () => { this.readFile(nextIndex, url); }, finalCallback);
        }
    }

    private getFile(url: string, cacheBust: string, successCallback: Function, allCallback: Function) {
        var client = new XMLHttpRequest();
        client.open('GET', url + cacheBust);
        client.onreadystatechange = () => {
            if (client.readyState === 4) {
                try {
                    if (client.status === 200) {
                        this.processSpecification(client.responseText);
                        successCallback();
                    } else {
                        this.testReporter.error('getFile', url, new Error('Error loading specification: ' + client.statusText + ' (' + client.status + ').'));
                    }
                } finally {
                    allCallback();
                }
            }
        }
        client.send();
    }

    private processSpecification(spec: string) {

        var hasParsed = true;
        var composer = new FeatureParser(this.steps, this.testReporter, this.excludedTags);

        /* Normalise line endings before splitting */
        var lines = spec.replace('\r\n', '\n').split('\n');

        for (var i = 0; i < lines.length; i++) {
            var line = lines[i];

            try {
                composer.process(line);
            } catch (ex) {
                hasParsed = false;
                var state = composer.state[0] || { featureTitle: 'Unknown' };
                this.testReporter.error(state.featureTitle, line, ex);
            }
        }

        if (hasParsed) {
            composer.run();
        }
    }
}

export class TestReporter implements ITestReporter {
    summary(featureTitle: string, scenarioTitle: string, isSuccess: boolean) {
        console.info((isSuccess ? '✔' : '✘') + ' ' + featureTitle + ' : ' + scenarioTitle + '\n');
    }

    error(featureTitle: string, condition: string, error: Error) {
        console.error(featureTitle + '\n\n' + condition + '\n\n' + error);
    }

    information(message: string) {
        console.log(message);
    }

    complete() {
        console.log('Run has finished');
    }

    protected escape(input: string) {
        return input.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
    }
}

class TapResult {
    constructor(public hash: number, public isOk: boolean, public description: string) { }

    output() {
        return (this.isOk ? '' : 'not ') + 'ok ' + this.hash + ' ' + this.description
    }
}

export class TapReporter implements ITestReporter {
    private hash: number = 0;
    private results: TapResult[] = [];

    summary(featureTitle: string, scenarioTitle: string, isSuccess: boolean) {
        this.hash++;
        this.results.push(new TapResult(this.hash, isSuccess, featureTitle + ': ' + scenarioTitle));
    }

    error(featureTitle: string, condition: string, error: Error) {
    }

    information(message: string) {
    }

    complete() {
        console.log('1..' + this.results.length);
        for (var i = 0; i < this.results.length; i++) {
            console.log(this.results[i].output());
        }
    }
}

// Assertions lifted from tsUnit and made static here.

export interface IThrowsParameters {
    fn: () => void;
    message?: string;
    errorString?: string;
}

export class Assert {
    public static areIdentical(expected: any, actual: any, message = ''): void {
        if (expected !== actual) {
            throw this.getError('areIdentical failed when given ' +
                this.printVariable(expected) + ' and ' + this.printVariable(actual),
                message);
        }
    }

    public static areNotIdentical(expected: any, actual: any, message = ''): void {
        if (expected === actual) {
            throw this.getError('areNotIdentical failed when given ' +
                this.printVariable(expected) + ' and ' + this.printVariable(actual),
                message);
        }
    }

    public static areCollectionsIdentical(expected: any[], actual: any[], message = ''): void {
        function resultToString(result: number[]): string {
            var msg = '';

            while (result.length > 0) {
                msg = '[' + result.pop() + ']' + msg;
            }

            return msg;
        }

        var compareArray = (expected: any[], actual: any[], result: number[]): void => {
            var indexString = '';

            if (expected === null) {
                if (actual !== null) {
                    indexString = resultToString(result);
                    throw this.getError('areCollectionsIdentical failed when array a' +
                        indexString + ' is null and b' +
                        indexString + ' is not null',
                        message);
                }

                return; // correct: both are nulls
            } else if (actual === null) {
                indexString = resultToString(result);
                throw this.getError('areCollectionsIdentical failed when array a' +
                    indexString + ' is not null and b' +
                    indexString + ' is null',
                    message);
            }

            if (expected.length !== actual.length) {
                indexString = resultToString(result);
                throw this.getError('areCollectionsIdentical failed when length of array a' +
                    indexString + ' (length: ' + expected.length + ') is different of length of array b' +
                    indexString + ' (length: ' + actual.length + ')',
                    message);
            }

            for (var i = 0; i < expected.length; i++) {
                if ((expected[i] instanceof Array) && (actual[i] instanceof Array)) {
                    result.push(i);
                    compareArray(expected[i], actual[i], result);
                    result.pop();
                } else if (expected[i] !== actual[i]) {
                    result.push(i);
                    indexString = resultToString(result);
                    throw this.getError('areCollectionsIdentical failed when element a' +
                        indexString + ' (' + this.printVariable(expected[i]) + ') is different than element b' +
                        indexString + ' (' + this.printVariable(actual[i]) + ')',
                        message);
                }
            }

            return;
        }

        compareArray(expected, actual, []);
    }

    public static areCollectionsNotIdentical(expected: any[], actual: any[], message = ''): void {
        try {
            this.areCollectionsIdentical(expected, actual);
        } catch (ex) {
            return;
        }

        throw this.getError('areCollectionsNotIdentical failed when both collections are identical', message);
    }

    public static isTrue(actual: boolean, message = '') {
        if (!actual) {
            throw this.getError('isTrue failed when given ' + this.printVariable(actual), message);
        }
    }

    public static isFalse(actual: boolean, message = '') {
        if (actual) {
            throw this.getError('isFalse failed when given ' + this.printVariable(actual), message);
        }
    }

    public static isTruthy(actual: any, message = '') {
        if (!actual) {
            throw this.getError('isTrue failed when given ' + this.printVariable(actual), message);
        }
    }

    public static isFalsey(actual: any, message = '') {
        if (actual) {
            throw this.getError('isFalse failed when given ' + this.printVariable(actual), message);
        }
    }

    public static isString(actual: any, message = '') {
        if (typeof actual !== 'string') {
            throw this.getError('isString failed when given ' + this.printVariable(actual), message);
        }
    }

    public static isNumber(actual: any, message = '') {
        if (typeof actual !== 'number') {
            throw this.getError('isNumber failed when given ' + this.printVariable(actual), message);
        }
    }

    public static isBoolean(actual: any, message = '') {
        if (typeof actual !== 'boolean') {
            throw this.getError('isBoolean failed when given ' + this.printVariable(actual), message);
        }
    }

    public static throws(params: IThrowsParameters): void;
    public static throws(actual: () => void, message?: string): void;
    public static throws(a: any, message = '', errorString = '') {
        var actual: () => void;

        if (typeof a === 'function') {
            actual = a;
        } else if (a.fn) {
            actual = a.fn;
            message = a.message;
            errorString = a.exceptionString;
        }

        var isThrown = false;
        try {
            actual();
        } catch (ex) {
            if (!errorString || ex.message === errorString) {
                isThrown = true;
            }

            if (errorString && ex.message !== errorString) {
                throw this.getError('different error string than supplied');
            }

        }
        if (!isThrown) {
            throw this.getError('did not throw an error', message || '');
        }
    }

    public static doesNotThrow(actual: () => void, message?: string): void {
        try {
            actual();
        } catch (ex) {
            throw this.getError('threw an error ' + ex, message || '');
        }
    }

    public static executesWithin(actual: () => void, timeLimit: number, message: string = null): void {
        function getTime() {
            return window.performance.now();
        }

        function timeToString(value: number) {
            return Math.round(value * 100) / 100;
        }

        var startOfExecution = getTime();

        try {
            actual();
        } catch (ex) {
            throw this.getError('isExecuteTimeLessThanLimit fails when given code throws an exception: "' + ex + '"', message);
        }

        var executingTime = getTime() - startOfExecution;
        if (executingTime > timeLimit) {
            throw this.getError('isExecuteTimeLessThanLimit fails when execution time of given code (' + timeToString(executingTime) + ' ms) ' +
                'exceed the given limit(' + timeToString(timeLimit) + ' ms)',
                message);
        }
    }

    public static fail(message = '') {
        throw this.getError('fail', message);
    }

    private static getError(resultMessage: string, message: string = '') {
        if (message) {
            return new Error(resultMessage + '. ' + message);
        }

        return new Error(resultMessage);
    }

    private static getNameOfClass(inputClass: {}) {
        // see: https://www.stevefenton.co.uk/Content/Blog/Date/201304/Blog/Obtaining-A-Class-Name-At-Runtime-In-TypeScript/
        var funcNameRegex = /function (.{1,})\(/;
        var results = (funcNameRegex).exec((<any>inputClass).constructor.toString());
        return (results && results.length > 1) ? results[1] : '';
    }

    private static printVariable(variable: any) {
        if (variable === null) {
            return '"null"';
        }

        if (typeof variable === 'object') {
            return '{object: ' + Assert.getNameOfClass(variable) + '}';
        }

        return '{' + (typeof variable) + '} "' + variable + '"';
    }
}
{'fraction_non_alphanumeric': 0.13140027158718748, 'fraction_numerical': 0.002316478951992971, 'mean_word_length': 1.859965733866362, 'pattern_counts': {'":': 0, '<': 17, '<?xml version=': 0, '>': 33, 'https://': 2, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2254984', 'n_tokens_mistral': 6793, 'n_tokens_neox': 6509, 'n_words': 1674}