﻿$engine JScript
$uname TestRunner
$dname Менеджер юнит-тестов скриптов
$addin SnegopatMainScript
$addin global
$addin stdlib

////////////////////////////////////////////////////////////////////////////////////////
////{ Cкрипт "Менеджер юнит-тестов скриптов" (testrunner.js) для проекта "Снегопат"
////
//// Описание: Реализует автоматический запуск юнит-тестов для сриптов проекта "Снегопат".
//// Автор: Александр Кунташов <kuntashov@gmail.com>, http://compaud.ru/blog
////}
////////////////////////////////////////////////////////////////////////////////////////

global.connectGlobals(SelfScript)

stdlib.require("SettingsManagement.js", SelfScript);

var jsUnitCore = stdlib.require("jsUnitCore.js");

////////////////////////////////////////////////////////////////////////////////////////
////{ Макросы
////

function macrosПоказать()
{
    GetTestRunner().Show();
}

function macrosСкрыть()
{
    GetTestRunner().Close();
}

/* Возвращает название макроса по умолчанию - вызывается, когда пользователь 
дважды щелкает мышью по названию скрипта в окне Снегопата. */
function getDefaultMacros() {
    return 'Показать';
}

//}

////////////////////////////////////////////////////////////////////////////////////////
////{ TestRunner
////

function TestRunner()
{
    TestRunner._instance = this;

    this.errorCount = 0;
    this.successCount = 0;
    this.failureCount = 0;
    
    this.form = loadScriptForm("scripts\\DevTools\\testrunner.ssf", this)
        
    this.allTests = this.form.ЭлементыФормы.тпДеревоТестов.Значение;                     
    this.allTests.Колонки.Добавить("object");
                     
    this.loadedTestAddins = [];
                     
    // Варианты состояний выполнения тестов
    this.STATE_NOT_RUN = 0;
    this.STATE_SUCCESS = 1;
    this.STATE_IGNORE  = 2;    
    this.STATE_FAILURE = 3;
            
    // Иконки состояний.
    this.StateIcons = {  
        Gray : this.form.ЭлементыФормы.ПолеКартинкиСерый.Картинка,
        Green: this.form.ЭлементыФормы.ПолеКартинкиЗеленый.Картинка,
        Yellow: this.form.ЭлементыФормы.ПолеКартинкиЖелтый.Картинка,
        Red: this.form.ЭлементыФормы.ПолеКартинкиКрасный.Картинка
    }                
    
    // Флаг, сигнализирующий, что тесты запускались.
    this.testingDone = false;

    this.defaultSettings = {
        ReloadBeforeRunAll : false,
        LogOnSuccess : false
    };
    
    this.settings = SettingsManagement.CreateManager(SelfScript.uniqueName, this.defaultSettings);
    this.settings.LoadSettings();
    this.settings.ApplyToForm(this.form);    
}

TestRunner.prototype.Show = function ()
{
    this.form.Open();
}

TestRunner.prototype.Close = function ()
{
    if (this.form.IsOpen())
        this.form.Close();
}

TestRunner.prototype.resetCounters = function()
{
    this.errorCount = 0;
    this.successCount = 0;
    this.failureCount = 0;
    
    this.updateTotals();
}

TestRunner.prototype.updateTotals = function ()
{
    this.form.ЭлементыФормы.КоличествоТестовВсего.Значение = this.testsCount;
    this.form.ЭлементыФормы.КоличествоУспешныхТестов.Значение = this.successCount;
    this.form.ЭлементыФормы.КоличествоПроваленныхТестов.Значение = this.failureCount;
}

TestRunner.prototype.initProgressBar = function ()
{
    this.switchProgressBar(true);
    this.form.ЭлементыФормы.ИндикаторВыполнения.МинимальноеЗначение = 0;
    this.form.ЭлементыФормы.ИндикаторВыполнения.МаксимальноеЗначение = this.testsCount;
    this.form.ЭлементыФормы.ИндикаторВыполнения.Шаг = 1;
    this.form.ЭлементыФормы.ИндикаторВыполнения.Значение = 0;    
}

TestRunner.prototype.progressBarDoStep = function ()
{
    this.form.ЭлементыФормы.ИндикаторВыполнения.Значение = this.form.ЭлементыФормы.ИндикаторВыполнения.Значение + 1;
}

TestRunner.prototype.switchProgressBar = function (progressBarVisible)
{
    this.form.ЭлементыФормы.НадписьВсего.Видимость = !progressBarVisible;
    this.form.ЭлементыФормы.КоличествоТестовВсего.Видимость = !progressBarVisible;
    this.form.ЭлементыФормы.НадписьУспешно.Видимость = !progressBarVisible;
    this.form.ЭлементыФормы.КоличествоУспешныхТестов.Видимость = !progressBarVisible;
    this.form.ЭлементыФормы.НадписьПровалено.Видимость = !progressBarVisible;
    this.form.ЭлементыФормы.КоличествоПроваленныхТестов.Видимость = !progressBarVisible;
    this.form.ЭлементыФормы.ИндикаторВыполнения.Видимость = !!progressBarVisible;
}

TestRunner.prototype.unloadAllTests = function ()
{
    this.allTests.Строки.Очистить();

    for (var i=0; i<this.loadedTestAddins.length; i++)
    {
        if (this.loadedTestAddins[i].uniqueName)
            addins.unloadAddin(this.loadedTestAddins[i]);
    }
    
    this.loadedTestAddins = [];
    this.testingDone = false;
}

TestRunner.prototype.loadTests = function(path)
{    
    this.unloadAllTests();

    this.testsCount = 0;
    
    this.walkFilesAndLoad(path, this.allTests);
    
    if (this.allTests.Строки.Количество() == 0)
    {
        Message("В каталоге " + path + " тест-кейсов не найдено!");
        return;
    }
        
    // Развернем все уровни дерева.
    for(var i=0; i<this.allTests.Строки.Количество(); i++)
        this.form.ЭлементыФормы.тпДеревоТестов.Развернуть(this.allTests.Строки.Получить(i), true);
        
    this.updateTotals();
    this.testingDone = false;
}

TestRunner.prototype.isTestAddinFile = function(file)
{
    // Имя тестового скрипта должно начинаться с префикса "test"
    if (!file.Name.match(/^test/i))
        return false;
    
    // Поддерживаются пока только скрипты.
    if (!file.Extension.match(/js|vbs/i))
        return false;

    return true;
}

TestRunner.prototype.walkFilesAndLoad = function(path, parentNode)
{
    var f = v8New("File", path);

    var files = f.IsFile() ? [ f ] : FindFiles(path, "*", false);
    
    for (var i=0; i<files.length; i++)
    {
        var Файл = files[i];
        
        if (Файл.ЭтоКаталог())
        {
            var newNode = this.addTestGroup(parentNode, Файл);
            this.walkFilesAndLoad(Файл.ПолноеИмя, newNode);
            
            // Пустые каталоги не показываем в дереве тестов.
            if (newNode.Rows.Count() == 0)
                parentNode.Rows.Delete(newNode);
        }
        else if (this.isTestAddinFile(Файл))
        {
            try
            {
                var testAddin = this.loadTestAddin(Файл.ПолноеИмя);
                
                if (testAddin)
                {
                    var newNode = this.addTestCase(parentNode, testAddin);
                                                                                
                    if (newNode.Rows.Count() == 0)
                    {
                        jsUnitCore.warn("Скрипт не содержит макросов и не будет загружен: " + testAddin.fullPath);
                        parentNode.Rows.Delete(newNode);
                            
                        addins.unloadAddin(testAddin);
                            
                        if(!testAddin.uniqueName.length) 
                            delete testAddin;                            
                    }
                    else 
                    {
                        this.loadedTestAddins.push(testAddin);
                    }
                }
            }
            catch (e)
            {
                jsUnitCore.warn("Ошибка загрузки скрипта: " + Файл.ПолноеИмя);
                // TODO: выводить информацию об ошибке подробнее.
            }
        }            
    }        
}

TestRunner.prototype.loadTestAddin = function(path)
{
    var fullLoadString = "script:" + path;
    
    var testAddin = addins.byFullPath(fullLoadString);
    
    if (!testAddin)
    {
        // Тест-аддины будем подгружать в группу "Подгружаемые библиотеки".        
        libGroup = SnegopatMainScript.AddinsTreeGroups.LoadedLibs;
            
        // Загружаем тестовый аддин.
        try 
        {
           testAddin = addins.loadAddin(fullLoadString, libGroup);
        }
        catch(e)
        {
            jsUnitCore("TestRunner::loadTestAddin: Тестовый скрипт не загружен: " + path);
            return null;
        }
    }
    
    return testAddin;
}

TestRunner.prototype.addTestGroup = function(parentNode, Файл)
{
    var newNode = parentNode.Строки.Добавить();
    newNode.НазваниеТеста = Файл.Имя;
    newNode.ВремяВыполнения = 0;
    newNode.ПолныйПуть = Файл.ПолноеИмя;
    newNode.Состояние = this.STATE_NOT_RUN;
    newNode.object = null;
    return newNode;
}

TestRunner.prototype.addTestCase = function(parentNode, testAddin)
{
    var newNode = parentNode.Rows.Add();
    newNode.НазваниеТеста = testAddin.uniqueName;
    newNode.ВремяВыполнения = 0;
    newNode.ПолныйПуть = testAddin.fullPath;
    newNode.Состояние = this.STATE_NOT_RUN;
    newNode.object = testAddin;

    // Добавим тест-методы. Тест-метод - это макросы с именами вида macrosTestИмяТеста.
    var macroses = new VBArray(testAddin.macroses()).toArray();
    for(var m in macroses)
        if (macroses[m].match(/^Test/))
            this.addTest(newNode, macroses[m], testAddin);
        
    return newNode;
}

TestRunner.prototype.addTest = function(parentNode, testName, testAddin)
{
    var newNode = parentNode.Строки.Добавить();
    newNode.НазваниеТеста = testName;
    newNode.ВремяВыполнения = 0;
    newNode.ПолныйПуть = testAddin.fullPath;
    newNode.Состояние = this.STATE_NOT_RUN;
    newNode.object = new Test(testAddin, testName);    
    
    this.testsCount++;
    
    return newNode;
}

TestRunner.prototype.runAllTests = function()
{    
    jsUnitCore.SetErrorHandler(function (exception) { throw exception; });

    /* Устанавливаем заранее, чтобы флаг был взведен даже если 
    нас остановит какой-нибудь эксепшен. */
    this.testingDone = true;

    for (var i = 0; i < this.allTests.Строки.Количество(); i++)
    {
        var ТекущаяСтрока = this.allTests.Строки.Получить(i);

        var beginTime = new Date();        
        
        ТекущаяСтрока.Состояние = this.runTest(ТекущаяСтрока);
        
        ТекущаяСтрока.ВремяВыполнения = (new Date() - beginTime) / 1000;        
    }  
    
    jsUnitCore.ResetErrorHandler();
}

TestRunner.prototype.runTest = function (СтрокаТестов)
{   
    var Состояние = this.STATE_SUCCESS;
    
    if (СтрокаТестов.object && jsUnitCore.JsUnit._trueTypeOf(СтрокаТестов.object) == 'Test')
    {
        Состояние = this.executeTestFunction(СтрокаТестов);
        this.progressBarDoStep();
    }
    else
    {   
        if (СтрокаТестов.Строки.Количество() == 0)
            return this.STATE_IGNORE;
    
        for (var i = 0; i < СтрокаТестов.Строки.Количество(); i++)
        {
            var ТекущаяСтрока = СтрокаТестов.Строки.Получить(i);

            var beginTime = new Date();        
            
            ТекущаяСтрока.Состояние = this.runTest(ТекущаяСтрока);
            
            ТекущаяСтрока.ВремяВыполнения = (new Date() - beginTime) / 1000;        
            
            if (ТекущаяСтрока.Состояние != this.STATE_SUCCESS)
                Состояние = this.STATE_FAILURE;                             
            
        }    
        
    }
    
    return Состояние;
}

TestRunner.prototype.setTestStatus = function(test, excep)
{
    var message = 'Тест ' + test.fullTestName + ' ';

    if (excep == null) 
    {
        test.status = this.STATE_SUCCESS;
        this.successCount++;
        message += 'выполнен успешно';
    } 
    else 
    {
        test.exception = excep;

        if (!excep.isJsUnitFailure) 
        {
            this.errorCount++;
            test.status = this.STATE_FAILURE;
            message += ' остановлен из-за ошибки в нем (exception or error)';
        }
        else 
        {
            //debugger;
            this.failureCount++;            
            test.status = this.STATE_FAILURE;
            message += " провалился (assertion failed)" 
                + (excep.comment ? "\n\t" + excep.comment : "") 
                + (excep.jsUnitMessage ? "\n\t" + excep.jsUnitMessage : "");
        }        
    }

    test.message = message;
    
    if (test.status != this.STATE_SUCCESS || this.settings.current.LogOnSuccess)
        Message(message);    
    
    return test.status;
}

TestRunner.prototype.executeTestFunction = function(СтрокаТеста)
{
    var theTest = СтрокаТеста.object;
    var testAddin = theTest.addin;
    var testFunctionName = 'macros' + theTest.testName;
     
    var exception = null;
    var timeBefore = new Date();
    
    try 
    {
       if (testAddin.object.setUp !== jsUnitCore.JSUNIT_UNDEFINED_VALUE)
            testAddin.object.setUp();
            
       testAddin.object[testFunctionName].call(null);
    }
    catch (e1) 
    {
        exception = e1;
    }
    finally 
    {
        try 
        {
            if (testAddin.object.tearDown !== jsUnitCore.JSUNIT_UNDEFINED_VALUE)
                testAddin.object.tearDown();
        }
        catch (e2) 
        {
            //Unlike JUnit, only assign a tearDown exception to excep if there is not already an exception from the test body
            if (exception == null)
                exception = e2;
        }
    }
    
    СтрокаТеста.ВремяВыполнения = (new Date() - timeBefore) / 1000;

    return this.setTestStatus(theTest, exception);
}

TestRunner.prototype.getDefaultTestsDir = function()
{
    var mainFolder = profileRoot.getValue("Snegopat/MainFolder");
    var f = v8New("Файл", mainFolder + "scripts\\Tests");
    if (f.Существует() && f.ЭтоКаталог())
        return f.ПолноеИмя;
    
    return mainFolder;
}

TestRunner.prototype.SaveSettings = function ()
{
    this.settings.ReadFromForm(this.form);
    this.settings.SaveSettings();
    this.form.Модифицированность = false;
    this.form.ЭлементыФормы.КнопкаПрименить.Доступность = false;
}

TestRunner.prototype.DiscardSettings = function ()
{
    this.settings.ApplyToForm(this.form);
    this.form.Модифицированность = false;    
}

TestRunner.prototype.isTestsLoaded = function ()
{
    return (this.allTests.Rows.Count() > 0);
}

////////////////////////////////////////////////////////////////////////////////////////
//// ОБРАБОТЧИКИ СОБЫТИЙ ФОРМЫ И ЕЕ ЭЛЕМЕНТОВ.
////

TestRunner.prototype.КнопкаЗагрузитьТестыНажатие = function(Элемент)
{
    var ВыборКаталога = v8New("ДиалогВыбораФайла", РежимДиалогаВыбораФайла.ВыборКаталога);
    ВыборКаталога.ПолноеИмяФайла = "";
    ВыборКаталога.Заголовок = "Выберите каталог c тестами";
    ВыборКаталога.Каталог = this.getDefaultTestsDir();
 
    if (ВыборКаталога.Выбрать())
    {
        this.form.Путь = ВыборКаталога.Каталог;
        this.loadTests(ВыборКаталога.Каталог);
    }
}

TestRunner.prototype.КнопкаЗагрузитьТестыЗагрузитьТестКейс = function (Элемент)
{
    var ВыборФайла = v8New("ДиалогВыбораФайла", РежимДиалогаВыбораФайла.Открытие);
    ВыборФайла.Заголовок = "Выберите тестовый скрипт";
    ВыборФайла.Каталог = this.getDefaultTestsDir();
        
    if (ВыборФайла.Выбрать())
    {
        this.form.Путь = ВыборФайла.ПолноеИмяФайла;
        this.loadTests(ВыборФайла.ПолноеИмяФайла);
    }
}

TestRunner.prototype.reloadTests = function()
{
    if (this.isTestsLoaded())
    {
        this.switchProgressBar(false);
        this.resetCounters();
        this.loadTests(this.form.Путь);
    }
}

TestRunner.prototype.КнопкаПерезагрузитьНажатие = function (Элемент)
{
    if (!this.isTestsLoaded())
    {
        Предупреждение("Сначала загрузите тесты!");
        return;
    }
    
    this.reloadTests();
}

TestRunner.prototype.КнопкаВыполнитьВсеТестыНажатие = function (Элемент)
{
    if (this.settings.current.ReloadBeforeRunAll && this.testingDone)
        this.reloadTests();

    this.resetCounters();
    this.initProgressBar();
        
    this.runAllTests();
    
    this.updateTotals();
    this.switchProgressBar(false);    
}

TestRunner.prototype.КнопкаВыполнитьВыделенныйНажатие = function (Элемент)
{
    jsUnitCore.SetErrorHandler(function(e){ throw e; });
    
    Message("Не реализовано");
    
    jsUnitCore.ResetErrorHandler();
}

TestRunner.prototype.тпДеревоТестовПриВыводеСтроки = function(Элемент, ОформлениеСтроки, ДанныеСтроки)
{
    var Ячейки = ОформлениеСтроки.val.Ячейки;
        
    // Устанавливаем иконку состояния выполнения.
    var Состояние = ДанныеСтроки.val.Состояние;
    
    if (Состояние == this.STATE_SUCCESS)
        Ячейки.НазваниеТеста.УстановитьКартинку(this.StateIcons.Green)        
        
    else if (Состояние == this.STATE_IGNORE)
        Ячейки.НазваниеТеста.УстановитьКартинку(this.StateIcons.Yellow)
        
    else if (Состояние == this.STATE_FAILURE)
        Ячейки.НазваниеТеста.УстановитьКартинку(this.StateIcons.Red)
        
    else
        Ячейки.НазваниеТеста.УстановитьКартинку(this.StateIcons.Gray)
        
}

TestRunner.prototype.КнопкаНастройкиНажатие = function (Элемент)
{
    this.settings.ApplyToForm(this.form);
    this.form.Панель.ТекущаяСтраница = this.form.Панель.Страницы.Настройки;
}

TestRunner.prototype.КнопкаНазадНажатие = function (Элемент)
{
    if (this.form.Модифицированность)
    {
        var answ = DoQueryBox("Настройки были изменены. Сохранить?", QuestionDialogMode.YesNoCancel);
        var retCodes = DialogReturnCode;
        
        if (answ == retCodes.Cancel)
            return;
            
        if (answ == retCodes.Yes)
        {
            this.SaveSettings();
        }
        else
        {
            // Откатим изменения настроек.
            this.DiscardSettings();
        }        
    }
    
    this.form.Панель.ТекущаяСтраница = this.form.Панель.Страницы.Тестирование;        
}

TestRunner.prototype.КнопкаПрименитьНажатие = function (Элемент)
{
    this.SaveSettings();
}

TestRunner.prototype.АвтоматическиПерезагружатьПередВыполнениемПриИзменении = function (Элемент)
{
    this.form.Модифицированность = true;
    this.form.ЭлементыФормы.КнопкаПрименить.Доступность = true;
}
           
TestRunner.prototype.LogOnSuccessПриИзменении = function(Элемент)
{
    this.form.Модифицированность = true;
    this.form.ЭлементыФормы.КнопкаПрименить.Доступность = true;
}
           
TestRunner.prototype.ПриОткрытии = function ()
{   
    this.resetCounters();
    this.switchProgressBar(false);
    this.form.ЭлементыФормы.КнопкаПрименить.Доступность = false;    
    this.form.Путь = "<Тесты не загружены>";
}

TestRunner.prototype.ПриЗакрытии = function ()
{
    this.unloadAllTests();    
}
////} TestRunner

////////////////////////////////////////////////////////////////////////////////////////
////{ ВСПОМОГАТЕЛЬНЫЕ ОБЪЕКТЫ И ФУНКЦИИ.
////

function FindFiles(path, mask)
{
    // Из snegopat.js.
    // TODO: Перенести в библиотеку Utils.
	// На NT-системах порядок выдачи файлов в FindFirstFile/FindNextFile неопределен, те они НЕОБЯЗАТЕЛЬНО
	// выдаются отсортированными по именам, поэтому на разных машинах могут выдаваться в разном порядке.
	// Кроме того, каталоги и файлы могут выдаваться вперемешку.
	// Также в документации к НайтиФайлы нет никакого упоминания о порядке выдачи файлов.
	// В случае зависимости одного стандартного скрипта от другого (например, snegopatwnd.js при
	// загрузке сразу обращается к global_context.js) это может привести к проблемам.
	// Поэтом примем такой порядок загрузки:
	// Сначала загружаются все подкатологи, отсортированные по имени.
	// Затем загржаются все файлы, отсортированные по имени.
    
    var allFiles = new Array();
    var files = new Array();
   
    var filesArray = new Enumerator(globalContext("{22A21030-E1D6-46A0-9465-F0A5427BE011}").НайтиФайлы(path.replace(/\\/g, '/'), "*", false));   
    for ( ; !filesArray.atEnd(); filesArray.moveNext())
    {
        var file = filesArray.item();
        (file.ЭтоКаталог() ? allFiles : files).push(file);
    }

    var sortByNames = function (i1, i2) { 
        return i1.Имя.toLowerCase().localeCompare(i2.Имя.toLowerCase()) 
    };

    allFiles.sort(sortByNames);
    files.sort(sortByNames);

    for (var i=0; i<files.length; i++)
        allFiles.push(files[i]);
        
    return allFiles;
}

function Test(addin, testName)
{
    this.addin = addin;
    this.fullTestName = addin.uniqueName + "::" + testName;
    this.testName = testName;
    this.exeption = null;
    this.message = "";
}
////} ВСПОМОГАТЕЛЬНЫЕ ОБЪЕКТЫ И ФУНКЦИИ.

////////////////////////////////////////////////////////////////////////////////////////
////{ StartUp
////
function GetTestRunner()
{
    if (!TestRunner._instance)
        new TestRunner();
    
    return TestRunner._instance;
}

GetTestRunner().Show();

////}


