"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateTestDefinition = exports.validateTemplateDefinition = exports.setupTestRepoWatcher = exports.setAllTestInfos = exports.setAllTemplates = exports.parseTestRepo = exports.normalizePartialPath = exports.mergeTestAssets = exports.excludeTestAssets = exports.hasTag = exports.getTestsForTemplate = exports.getTestInfos = exports.getTestInfo = exports.getTestsByTagPredicate = exports.getSessionTemplate = exports.getAllTestInfos = exports.getAllTemplates = exports.curateTestAssetPath = exports.containsAsset = exports.completeTestInfos = void 0;
const _ = require("underscore");
const configLoader = require("../lib/config-loader");
const chokidar = require("chokidar");
const crypto = require("crypto");
const fileWalker = require("./file-walker");
const fs = require("fs");
const helpers = require("./helpers");
const jsYaml = require("js-yaml");
const jsonSchema = require("jsonschema");
const os = require("os");
const path = require("path");
const thenify = require("thenify");
let allTemplates = [];
let allTestInfos = [];
let isTestRepoWatcherSetUp = false;
const isWindows = (os.platform().startsWith('win'));
/** Add the actors and steps information to a set of TestInfo objects
 * that only contains the test names */
function completeTestInfos(incompleteTestInfos, completeTestInfos) {
    for (let incompleteTestInfo of incompleteTestInfos) {
        for (let completeTestInfo of completeTestInfos) {
            if (getAssetPartialPath(incompleteTestInfo) === getAssetPartialPath(completeTestInfo)) {
                incompleteTestInfo.actors = completeTestInfo.actors;
                incompleteTestInfo.segments = completeTestInfo.segments;
            }
        }
    }
}
exports.completeTestInfos = completeTestInfos;
/** Returns true if a collection of test assets contains the specified
 * asset and false otherwise. */
function containsAsset(testAssets, testAssetToFind) {
    for (const testAsset of testAssets) {
        let assetPath = normalizePartialPath(testAsset.path || '');
        let assetName = normalizePartialPath(testAsset.name);
        let assetToFindPath = normalizePartialPath(testAssetToFind.path || '');
        let assetToFindName = normalizePartialPath(testAssetToFind.name);
        if (isWindows) {
            assetPath = assetPath.toLocaleLowerCase();
            assetName = assetName.toLocaleLowerCase();
            assetToFindPath = assetToFindPath.toLocaleLowerCase();
            assetToFindName = assetToFindName.toLocaleLowerCase();
        }
        const assetRelativePath = path.join(assetPath, assetName);
        const assetToFindRelativePath = path.join(assetToFindPath, assetToFindName);
        if (assetRelativePath === assetToFindRelativePath) {
            return true;
        }
    }
    return false;
}
exports.containsAsset = containsAsset;
/** If any portion of the asset path is included in the asset name, move that
 * portion to the path property where it belongs */
function curateTestAssetPath(testAsset) {
    if (testAsset.name !== path.basename(testAsset.name)) {
        testAsset.path = path.dirname(testAsset.name);
        testAsset.name = path.basename(testAsset.name);
    }
}
exports.curateTestAssetPath = curateTestAssetPath;
function getAllTemplates() {
    return allTemplates;
}
exports.getAllTemplates = getAllTemplates;
function getAllTestInfos() {
    return allTestInfos;
}
exports.getAllTestInfos = getAllTestInfos;
/** Computes a test asset partial path by concatenating the relative path
 * with the asset name (separated by "/"). */
function getAssetPartialPath(assetName) {
    const assetRelativePath = normalizePartialPath(assetName.path);
    const assetFileName = normalizePartialPath(assetName.name);
    if (!assetRelativePath) {
        return assetFileName;
    }
    else {
        return helpers.format('{0}/{1}', assetRelativePath, assetFileName);
    }
}
/** Reads and returns a test session template from cache. Returns null if the
 * template was not found. */
function getSessionTemplate(templateName) {
    let normalizedNameToFind = normalizePartialPath(templateName.name);
    if (!normalizedNameToFind) {
        return null;
    }
    let normalizedPathToFind = normalizePartialPath(templateName.path);
    if (isWindows) {
        normalizedNameToFind = normalizedNameToFind.toLocaleLowerCase();
        normalizedPathToFind = normalizedPathToFind.toLocaleLowerCase();
    }
    const normalizedRelativePathToFind = path.join(normalizedPathToFind, normalizedNameToFind);
    for (let currentTemplate of allTemplates) {
        let normalizedName = normalizePartialPath(currentTemplate.name);
        let normalizedPath = normalizePartialPath(currentTemplate.path);
        if (isWindows) {
            normalizedName = normalizedName.toLocaleLowerCase();
            normalizedPath = normalizedPath.toLocaleLowerCase();
        }
        const normalizedRelativePath = path.join(normalizedPath, normalizedName);
        if (normalizedRelativePath === normalizedRelativePathToFind) {
            return currentTemplate;
        }
    }
    return null;
}
exports.getSessionTemplate = getSessionTemplate;
/** Returns all the tests from a collection whose tags conform to the
 * specified expression, for example "(tag1 || tag2) && tag3". */
function getTestsByTagPredicate(allTests, tagPredicate) {
    const results = [];
    for (let testInfo of allTests) {
        const currentPredicate = tagPredicate.replace(/[\w\-+\[\].\/]+/g, function (match) {
            return hasTag(testInfo, match) ? 'true' : 'false';
        });
        try {
            if (eval(currentPredicate)) {
                results.push(testInfo);
            }
        }
        catch (err) {
            console.log(`ERROR: Failed to evaluate expression "${tagPredicate}" while selecting tests by tags. The error message was: ${err.message}. Please review the expression and correct it.`);
            break;
        }
    }
    return results;
}
exports.getTestsByTagPredicate = getTestsByTagPredicate;
/** Returns the TestInfo object corresponding to a TestAssetName. */
function getTestInfo(selectionTestName) {
    for (let repoTestName of allTestInfos) {
        const selectionTestFullPath = getAssetPartialPath(selectionTestName);
        const repoTestFullPath = getAssetPartialPath(repoTestName);
        if (selectionTestFullPath === repoTestFullPath) {
            return {
                actors: repoTestName.actors,
                dataDriven: repoTestName.dataDriven,
                name: repoTestName.name,
                path: repoTestName.path,
                segments: repoTestName.segments,
                tags: repoTestName.tags
            };
        }
    }
    return null;
}
exports.getTestInfo = getTestInfo;
/** Returns the array of TestInfo objects corresponding to an array of
 *  TestAssetName objects. */
function getTestInfos(selectionTestNames) {
    const resultTestInfos = [];
    for (let selectionTestName of selectionTestNames) {
        const testInfo = getTestInfo(selectionTestName);
        if (testInfo) {
            resultTestInfos.push(testInfo);
        }
    }
    return resultTestInfos;
}
exports.getTestInfos = getTestInfos;
function getTestsForTemplate(allTests, template) {
    return __awaiter(this, void 0, void 0, function* () {
        if (!template) {
            return [];
        }
        let results = template.tests || [];
        const sortTestsByPathAndName = (tests) => {
            return tests.sort((t1, t2) => {
                const t1Path = t1.path || '';
                const t2Path = t2.path || '';
                if (t1Path < t2Path) {
                    return -1;
                }
                else if (t1Path > t2Path) {
                    return 1;
                }
                else {
                    if (t1.name < t2.name) {
                        return -1;
                    }
                    else if (t1.name > t2.name) {
                        return 1;
                    }
                }
            });
            ;
        };
        if (template.includeTestsWithTags) {
            const testsByTagPredicate = getTestsByTagPredicate(allTests, template.includeTestsWithTags);
            results = mergeTestAssets(results, sortTestsByPathAndName(testsByTagPredicate));
        }
        try {
            if (template.includeTestsFromTemplates) {
                for (let currentTemplateName of template.includeTestsFromTemplates) {
                    const currentTemplate = yield getSessionTemplate(currentTemplateName);
                    const templateTests = yield getTestsForTemplate(allTests, currentTemplate);
                    results = mergeTestAssets(results, templateTests);
                }
            }
        }
        catch (err) {
            const includeTestsFromTemplates = JSON.stringify(template.includeTestsFromTemplates);
            console.log(`ERROR: Failed to parse the "includeTestsFromTemplates" property with value ${includeTestsFromTemplates}`);
        }
        // Exclude tests that are missing the name property
        results = results.filter(t => t.name);
        // Trim the ".yaml" extension from test name, if present
        results.forEach(function (testAsset) {
            testAsset.name = testAsset.name.replace(/\.yaml\s*$/i, '');
        });
        const config = configLoader.getConfig();
        results.forEach(t => curateTestAssetPath(t));
        const setupTests = results.filter(t => t.name.startsWith(config.setupTestPrefix));
        const teardownTests = results.filter(t => t.name.startsWith(config.teardownTestPrefix));
        const allOtherTests = results.filter(t => {
            return !(t.name.startsWith(config.setupTestPrefix) || t.name.startsWith(config.teardownTestPrefix));
        });
        // Test names prefixed with "(setup)" are placed first and test names
        // prefixed with "(teardown)" go last.
        return [].concat(sortTestsByPathAndName(setupTests), allOtherTests, sortTestsByPathAndName(teardownTests).reverse());
    });
}
exports.getTestsForTemplate = getTestsForTemplate;
/** Returns true if a test contains the specified tag and false otherwise. */
function hasTag(testInfo, requiredTag) {
    let tagWasFound = false;
    for (let testTag of testInfo.tags || []) {
        if (testTag === requiredTag) {
            tagWasFound = true;
            break;
        }
    }
    return tagWasFound;
}
exports.hasTag = hasTag;
/** Returns the test assets that exist in the first collection but not in the second. */
function excludeTestAssets(testAssets1, testAssets2) {
    let resultingAssets = [];
    for (let testAsset of testAssets1) {
        if (!containsAsset(testAssets2, testAsset)) {
            resultingAssets.push(testAsset);
        }
    }
    return resultingAssets;
}
exports.excludeTestAssets = excludeTestAssets;
/** Returns the test assets that exist in the first or second collection. Makes
 * sure to skip duplicates. */
function mergeTestAssets(testAssets1, testAssets2) {
    // Clone first collection
    let mergedAssets = testAssets1.concat();
    for (let testAsset of testAssets2) {
        if (!containsAsset(testAssets1, testAsset)) {
            mergedAssets.push(testAsset);
        }
    }
    return mergedAssets;
}
exports.mergeTestAssets = mergeTestAssets;
/** Normalizes a partial path using path.normalize, but also replaces "\" with "/"
 * and trims any "\", "/", "." and space from the beginning and the end of the path.
 * The purpose is to make it possible to safely compare two partial paths. */
function normalizePartialPath(relativePath) {
    const trimmedPath = helpers.trimChars(relativePath || '', '\\/\. ');
    if (!trimmedPath) {
        return '';
    }
    else {
        return path.normalize(trimmedPath)
            .replace(/[\\]/g, '/');
    }
}
exports.normalizePartialPath = normalizePartialPath;
function parseTestRepo() {
    return __awaiter(this, void 0, void 0, function* () {
        const config = configLoader.getConfig();
        if (!config.testRepoDir) {
            throw new Error('The test repo directory was not defined. Please make sure the path is correctly specified in the testRepoDir parameter in the server.yaml file.');
        }
        const testRepoDirFullPath = path.resolve(config.testRepoDir);
        if (!helpers.directoryExists(testRepoDirFullPath)) {
            throw new Error(helpers.format('The test repo directory "{0}" doesn\'t exist. Please make sure the path is correctly specified in your server.yaml file.', testRepoDirFullPath));
        }
        const testsDir = path.join(testRepoDirFullPath, 'tests');
        const templatesDir = path.join(testRepoDirFullPath, 'templates');
        let newTestInfos = [];
        let newTemplateInfos = [];
        const readFileAsync = thenify(fs.readFile);
        // Recursively browse the tests directory and extract test information
        if (helpers.directoryExists(testsDir)) {
            yield fileWalker.walkRecursive(testsDir, function (parentDirName, fileName, fileStats) {
                return __awaiter(this, void 0, void 0, function* () {
                    if (fileStats.isFile()) {
                        const fileFullPath = path.join(parentDirName, fileName);
                        try {
                            const extName = path.extname(fileFullPath).toLowerCase();
                            if (extName === '.yaml' || extName === '.yml') {
                                const fileData = yield readFileAsync(fileFullPath);
                                const testDefString = fileData.toString('utf8');
                                if (testDefString.trim().length === 0) {
                                    console.log(`Ignoring empty file ${fileFullPath}`);
                                    return;
                                }
                                const testDef = jsYaml.safeLoad(testDefString);
                                const validationResult = validateTestDefinition(testDef);
                                if (validationResult.errors.length == 0) {
                                    const testInfo = {
                                        actors: _.unique(testDef.actors.map((a) => { return a.actorType || a.actor; })),
                                        dataDriven: !!testDef.dataSet,
                                        hash: crypto.createHash('sha1').update(fileData).digest('hex'),
                                        name: path.basename(fileFullPath, path.extname(fileFullPath)),
                                        path: path.dirname(fileFullPath).substring(testsDir.length + 1).replace(/\\/g, '/'),
                                        segments: _.flatten(testDef.actors.map((a) => {
                                            return (a.segments || a.steps).map((s) => {
                                                return s.segment || s.step || s.index;
                                            });
                                        })).sort(),
                                        tags: testDef.tags || []
                                    };
                                    newTestInfos.push(testInfo);
                                }
                                else {
                                    console.log(helpers.format("Test definition file {0} failed validation", fileFullPath));
                                    console.log(validationResult.errors);
                                }
                            }
                        }
                        catch (err) {
                            console.log(helpers.format('WARN: Failed to parse test definition file "{0}". {1}', fileFullPath, err.message));
                        }
                    }
                });
            });
        }
        // Sort tests by path and file name
        newTestInfos = _(newTestInfos).chain().sortBy(function (ti) {
            return ti.path;
        }).sortBy(function (ti) {
            return ti.name;
        }).value();
        allTestInfos = newTestInfos;
        // Recursively browse the templates directory and extract session template information
        if (helpers.directoryExists(templatesDir)) {
            yield fileWalker.walkRecursive(templatesDir, function (parentDirName, fileName, fileStats) {
                return __awaiter(this, void 0, void 0, function* () {
                    if (fileStats.isFile()) {
                        const fileFullPath = path.join(parentDirName, fileName);
                        try {
                            if (path.extname(fileFullPath).toLowerCase() === '.yaml') {
                                const fileData = yield readFileAsync(fileFullPath);
                                const templateString = fileData.toString('utf8');
                                if (templateString.trim().length === 0) {
                                    console.log(`Ignoring empty file ${fileFullPath}`);
                                    return;
                                }
                                const template = jsYaml.safeLoad(templateString);
                                const validationResult = validateTemplateDefinition(template);
                                if (validationResult.errors.length == 0) {
                                    const templateInfo = Object.assign({ name: path.basename(fileFullPath, path.extname(fileFullPath)), path: path.dirname(fileFullPath).substring(templatesDir.length + 1).replace(/\\/g, '/') }, template);
                                    newTemplateInfos.push(templateInfo);
                                }
                                else {
                                    console.log(helpers.format("Session template file {0} failed validation", fileFullPath));
                                    console.log(validationResult.errors);
                                }
                            }
                        }
                        catch (err) {
                            console.log(helpers.format('WARN: Failed to parse session template file "{0}". {1}', fileFullPath, err.message));
                        }
                    }
                });
            });
        }
        // Sort templates by path and file name
        newTemplateInfos = _(newTemplateInfos).chain().sortBy(function (ti) {
            return ti.path;
        }).sortBy(function (ti) {
            return ti.name;
        }).value();
        allTemplates = newTemplateInfos;
    });
}
exports.parseTestRepo = parseTestRepo;
/** Sets the test template cache to the specified array. */
function setAllTemplates(newTemplates) {
    allTemplates = newTemplates;
}
exports.setAllTemplates = setAllTemplates;
/** Sets the tests cache to the specified array. */
function setAllTestInfos(newTests) {
    allTestInfos = newTests;
}
exports.setAllTestInfos = setAllTestInfos;
function setupTestRepoWatcher() {
    const config = configLoader.getConfig();
    if (isTestRepoWatcherSetUp) {
        return;
    }
    isTestRepoWatcherSetUp = true;
    const testRepoDirFullPath = path.resolve(config.testRepoDir);
    const watcher = chokidar.watch(testRepoDirFullPath, {
        ignored: /(^|[\/\\])\../,
        ignoreInitial: true
    });
    function reloadRepo(filePath, fileStat) {
        return __awaiter(this, void 0, void 0, function* () {
            console.log('Detected update: ' + filePath);
            yield parseTestRepo();
            console.log('The test repository was reloaded');
        });
    }
    var debouncedReloadRepo = _.debounce(reloadRepo, 1000);
    // Watch test repo for changes and reload, as necessary
    watcher
        .on('add', (filePath, fileStat) => {
        debouncedReloadRepo(filePath, fileStat);
    })
        .on('change', (filePath, fileStat) => {
        debouncedReloadRepo(filePath, fileStat);
    })
        .on('unlink', (filePath, fileStat) => {
        debouncedReloadRepo(filePath, fileStat);
    });
}
exports.setupTestRepoWatcher = setupTestRepoWatcher;
function validateTemplateDefinition(testDef) {
    var validator = new jsonSchema.Validator();
    let sessionTemplateSchema = {
        "id": "/TemplateDefinition",
        "type": "object",
        "properties": {
            "actorTags": {
                "type": "array"
            },
            "description": { "type": ["string", null] },
            "sessionLabel": { "type": ["string", null] },
            "maxIterations": { "type": ["number", null] },
            "tests": {
                "type": "array"
            }
        },
        "required": []
    };
    return validator.validate(testDef, sessionTemplateSchema);
}
exports.validateTemplateDefinition = validateTemplateDefinition;
function validateTestDefinition(testDef) {
    var validator = new jsonSchema.Validator();
    let testDefAction = {
        "id": "/TestDefAction",
        "type": "object",
        "properties": {
            "description": { "type": "string" },
            "action": { "type": "string" },
            "args": {
                "type": "object"
            }
        }
    };
    let testDefSegment = {
        "id": "/TestDefSegment",
        "type": "object",
        "properties": {
            "description": { "type": "string" },
            "index": { "type": "number" },
            "step": { "type": "number" },
            "segment": { "type": "number" },
            "actions": {
                "type": "array",
                // "required": true,
                "items": {
                    "type": "object" // TODO: Do proper validation here
                }
            }
        }
    };
    let testDefActor = {
        "id": "/TestDefActor",
        "type": "object",
        "properties": {
            "actorType": { "type": "string" },
            "description": { "type": "string" },
            "steps": {
                "type": "array",
            },
            "segments": {
                "type": "array"
            }
        }
    };
    let testDefSchema = {
        "id": "/TestDefinition",
        "type": "object",
        "properties": {
            "description": { "type": ["string", null] },
            "actors": {
                "type": "array"
            }
        }
    };
    validator.addSchema(testDefSegment, '/TestDefAction');
    validator.addSchema(testDefSegment, '/TestDefSegment');
    validator.addSchema(testDefActor, '/TestDefActor');
    return validator.validate(testDef, testDefSchema);
}
exports.validateTestDefinition = validateTestDefinition;
//# sourceMappingURL=test-repo.js.map