diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5df8b0c..9ba75e0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,6 +11,6 @@ jobs: uses: autumn-library/workflows/.github/workflows/release.yml@main with: package_mask: "oneunit-*.ospx" - oscript_version: "2.1.0" + oscript_version: "2.2.0" secrets: PUSH_TOKEN: ${{ secrets.PUSH_TOKEN }} diff --git a/packagedef b/packagedef index 6f9f00b..38ff319 100644 --- a/packagedef +++ b/packagedef @@ -9,7 +9,7 @@ .АдресРепозитория("https://github.com/sfaqer/oneunit") .Описание(ОписаниеКонсольногоПриложения.ПолноеИмяПриложения()) .ВерсияМанифеста("1.4.0") - .ВерсияСреды("2.0.0") + .ВерсияСреды("2.2.0") .ВключитьФайл("src") .ВключитьФайл("docs") diff --git "a/src/cli/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\233\320\276\320\263\320\224\320\273\321\217\320\232\320\276\320\274\320\260\320\275\320\264\321\213\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\260.os" "b/src/cli/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\233\320\276\320\263\320\224\320\273\321\217\320\232\320\276\320\274\320\260\320\275\320\264\321\213\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\260.os" index 0348114..e6d260b 100644 --- "a/src/cli/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\233\320\276\320\263\320\224\320\273\321\217\320\232\320\276\320\274\320\260\320\275\320\264\321\213\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\260.os" +++ "b/src/cli/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\233\320\276\320\263\320\224\320\273\321\217\320\232\320\276\320\274\320\260\320\275\320\264\321\213\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\260.os" @@ -1,64 +1,100 @@ #Использовать collectionos Перем _Лог; +Перем _Хвосты; // Соответствие из КлючИЗначение - Недописанные строки потоков вывода: + // * Ключ - Булево - Истина для потока ошибок + // * Значение - Строка - Недописанная строка Процедура Ошибка(Знач Сообщение) Экспорт - ОбработатьСтрокуЛога(Сообщение, Истина); + ОбработатьПорциюВывода(Сообщение, Истина); КонецПроцедуры Процедура Информация(Знач Сообщение) Экспорт - ОбработатьСтрокуЛога(Сообщение, Ложь); + ОбработатьПорциюВывода(Сообщение, Ложь); КонецПроцедуры -Процедура ОбработатьСтрокуЛога(Сообщение, ЭтоОшибка) +// Выводит недописанные строки, оставшиеся после завершения процесса +// +Процедура Дописать() Экспорт - Сообщения = СтрРазделить(Сообщение, Символы.ПС); + Для каждого Хвост Из _Хвосты Цикл + Если Хвост.Значение <> "" Тогда + ВывестиСтроку(Хвост.Значение, Хвост.Ключ); + КонецЕсли; + КонецЦикла; - Для Каждого Сообщение Из Сообщения Цикл + _Хвосты.Очистить(); - Сообщение = СтрЗаменить(Сообщение, Символы.ВК, ""); - // В win при переключении консоли в UTF-8 появляется символ с кодом 65279 в начале сообщения и парсер JSON падает - Сообщение = СтрЗаменить(Сообщение, Символ(65279), ""); +КонецПроцедуры - Попытка +// Вывод процесса приходит порциями, как его прочитал 1commands: порция может +// оборвать строку и обычно заканчивается переводом строки. Выводятся только +// целые строки, недописанная ждет следующей порции - иначе каждый стык порций +// давал бы в консоли пустую строку или строку, разорванную надвое +// +Процедура ОбработатьПорциюВывода(Знач Порция, Знач ЭтоОшибка) - ЧтениеJSON = Новый ЧтениеJSON(); - ЧтениеJSON.УстановитьСтроку(Сообщение); - Поля = ПрочитатьJSON(ЧтениеJSON, Истина, Массивы.ИзЭлементов("time")); - ТекстСообщения = Поля.Получить("msg"); + Хвост = _Хвосты.Получить(ЭтоОшибка); - Уровень = Поля.Получить("level"); + Если Хвост = Неопределено Тогда + Хвост = ""; + КонецЕсли; - Если Уровень = "DEBUG" Тогда - Уровень = УровниЛога.Отладка; - ИначеЕсли Уровень = "INFO" Тогда - Уровень = УровниЛога.Информация; - ИначеЕсли Уровень = "WARN" Тогда - Уровень = УровниЛога.Предупреждение; - ИначеЕсли Уровень = "ERROR" Тогда - Уровень = УровниЛога.Ошибка; - ИначеЕсли Уровень = "FATAL" Тогда - Уровень = УровниЛога.КритичнаяОшибка; - КонецЕсли; // BSLLS:IfElseIfEndsWithElse-off + Строки = СтрРазделить(Хвост + Порция, Символы.ПС); - _Лог.ПоляИз(Поля) - .Вывести(ТекстСообщения, Уровень); + _Хвосты.Вставить(ЭтоОшибка, Строки[Строки.ВГраница()]); + Строки.Удалить(Строки.ВГраница()); - Исключение + Для Каждого СтрокаВывода Из Строки Цикл + ВывестиСтроку(СтрокаВывода, ЭтоОшибка); + КонецЦикла; - Если ЭтоОшибка Тогда - _Лог.Ошибка(Сообщение); - Иначе - _Лог.Информация(Сообщение); - КонецЕсли; +КонецПроцедуры - КонецПопытки; +Процедура ВывестиСтроку(Знач Сообщение, Знач ЭтоОшибка) - КонецЦикла; + Сообщение = СтрЗаменить(Сообщение, Символы.ВК, ""); + // В win при переключении консоли в UTF-8 появляется символ с кодом 65279 в начале сообщения и парсер JSON падает + Сообщение = СтрЗаменить(Сообщение, Символ(65279), ""); + + Попытка + + ЧтениеJSON = Новый ЧтениеJSON(); + ЧтениеJSON.УстановитьСтроку(Сообщение); + Поля = ПрочитатьJSON(ЧтениеJSON, Истина, Массивы.ИзЭлементов("time")); + ТекстСообщения = Поля.Получить("msg"); + + Уровень = Поля.Получить("level"); + + Если Уровень = "DEBUG" Тогда + Уровень = УровниЛога.Отладка; + ИначеЕсли Уровень = "INFO" Тогда + Уровень = УровниЛога.Информация; + ИначеЕсли Уровень = "WARN" Тогда + Уровень = УровниЛога.Предупреждение; + ИначеЕсли Уровень = "ERROR" Тогда + Уровень = УровниЛога.Ошибка; + ИначеЕсли Уровень = "FATAL" Тогда + Уровень = УровниЛога.КритичнаяОшибка; + КонецЕсли; // BSLLS:IfElseIfEndsWithElse-off + + _Лог.ПоляИз(Поля) + .Вывести(ТекстСообщения, Уровень); + + Исключение + + Если ЭтоОшибка Тогда + _Лог.Ошибка(Сообщение); + Иначе + _Лог.Информация(Сообщение); + КонецЕсли; + + КонецПопытки; КонецПроцедуры &Желудь Процедура ПриСозданииОбъекта(&Лог("oscript.lib.oneunit.cli") Лог) - _Лог = Лог; + _Лог = Лог; + _Хвосты = Новый Соответствие; КонецПроцедуры diff --git "a/src/cli/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260\320\236\320\261\320\275\320\260\321\200\321\203\320\266\320\270\321\202\321\214.os" "b/src/cli/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260\320\236\320\261\320\275\320\260\321\200\321\203\320\266\320\270\321\202\321\214.os" index 4223292..af9cb0d 100644 --- "a/src/cli/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260\320\236\320\261\320\275\320\260\321\200\321\203\320\266\320\270\321\202\321\214.os" +++ "b/src/cli/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260\320\236\320\261\320\275\320\260\321\200\321\203\320\266\320\270\321\202\321\214.os" @@ -61,6 +61,8 @@ ПараметрыЗапуска ); + _ЛогДляКомандыМенеджера.Дописать(); + Если Результат.КодВозврата <> 0 Тогда Сообщить("Обнаружение завершилось неудачно", СтатусСообщения.ОченьВажное); ЗавершитьРаботу(Результат.КодВозврата); diff --git "a/src/cli/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260\320\242\320\265\321\201\321\202\320\270\321\200\320\276\320\262\320\260\321\202\321\214.os" "b/src/cli/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260\320\242\320\265\321\201\321\202\320\270\321\200\320\276\320\262\320\260\321\202\321\214.os" index e8b7632..36e25d5 100644 --- "a/src/cli/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260\320\242\320\265\321\201\321\202\320\270\321\200\320\276\320\262\320\260\321\202\321\214.os" +++ "b/src/cli/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260\320\242\320\265\321\201\321\202\320\270\321\200\320\276\320\262\320\260\321\202\321\214.os" @@ -117,6 +117,8 @@ Результат = _Запускатель.ВыполнитьТестирование(Детальки, _Лог, ПараметрыЗапуска); + _ЛогДляКомандыМенеджера.Дописать(); + Если Результат.КодВозврата <> 0 Тогда Сообщить("Тестирование завершилось неудачно", СтатусСообщения.ОченьВажное); ЗавершитьРаботу(Результат.КодВозврата); diff --git "a/src/core/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\230\321\201\320\277\320\276\320\273\320\275\320\270\321\202\320\265\320\273\321\214\320\230\320\267\320\276\320\273\321\217\321\206\320\270\320\270.os" "b/src/core/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\230\321\201\320\277\320\276\320\273\320\275\320\270\321\202\320\265\320\273\321\214\320\230\320\267\320\276\320\273\321\217\321\206\320\270\320\270.os" index 105a5d7..58a5930 100644 --- "a/src/core/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\230\321\201\320\277\320\276\320\273\320\275\320\270\321\202\320\265\320\273\321\214\320\230\320\267\320\276\320\273\321\217\321\206\320\270\320\270.os" +++ "b/src/core/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\230\321\201\320\277\320\276\320\273\320\275\320\270\321\202\320\265\320\273\321\214\320\230\320\267\320\276\320\273\321\217\321\206\320\270\320\270.os" @@ -1,7 +1,7 @@ // Выполняет изолированные наборы и тесты и вклеивает их результаты в общий прогон. // Изолированный набор прогоняется в собственной среде - свежей поделке или дочернем -// процессе - а его результаты переэмитятся событиями исполнения на публикатор -// родителя, так что для репортеров и статистики это обычные тесты. +// процессе, - а события его исполнения ретранслируются в текущий прогон по ходу, +// так что для репортеров и статистики это обычные тесты. // Для чистого обнаружения (discover) состав изолированного набора обнаруживается // в его среде без исполнения тестов и вклеивается в план зеркалами. @@ -15,12 +15,14 @@ Перем _Поделка; // Поделка - Поделка текущего прогона Перем _ПубликаторСобытий; // ПубликаторСобытий - Публикатор событий исполнения текущего прогона Перем _Запускатель; // ЗапускательТестирования - Запускатель дочерних прогонов +Перем _ПереносРезультатов; // ПереносРезультатов - Перевод результатов из записей +Перем _РепортерПланJSON; // РепортерПланJSON - Записи состава плана Перем _КаталогСтатистикиПокрытия; // Строка - Каталог статистики покрытия прогона, пустая строка - без покрытия Перем _ПрерыватьПриПадении; // Булево - Прекращать прогон после первого упавшего теста Перем _Лог; // Лог - Лог ядра -// Выполняет изолированный тестовый набор в отдельной среде и публикует его -// результаты в текущий прогон. Отказ изолированной среды не прерывает прогон - +// Выполняет изолированный тестовый набор в отдельной среде, ретранслируя события +// его исполнения в текущий прогон. Отказ изолированной среды не прерывает прогон - // набор помечается сломанным. // // Параметры: @@ -96,15 +98,7 @@ Прогон = ВыполнитьВПоделке(ОпределениеНабора, ОпределениеНабора, "", Истина); - ФрешНабор = НайтиНаборВПлане(Прогон.План, ОпределениеНабора.Имя()); - - Если ФрешНабор = Неопределено Тогда - Возврат; - КонецЕсли; - - Для каждого ФрешДитя Из ФрешНабор.Дети() Цикл - ОпределениеНабора.ДобавитьТест(ПостроитьЗеркало(ФрешДитя, ОпределениеНабора)); - КонецЦикла; + НовыйРетранслятор(ОпределениеНабора).ВклеитьСостав(_РепортерПланJSON.ЗаписиНаборов(Прогон.План)); КонецПроцедуры @@ -121,17 +115,7 @@ МенеджерВременныхФайлов.Удалить(); - Для каждого ЗаписьНабора Из План.suites Цикл - - Если ЗаписьНабора.suite <> ОпределениеНабора.Имя() Тогда - Продолжить; - КонецЕсли; - - Для каждого ЗаписьТеста Из ЗаписьНабора.tests Цикл - ОпределениеНабора.ДобавитьТест(ЗеркалоЗаписиПлана(ЗаписьТеста, ОпределениеНабора)); - КонецЦикла; - - КонецЦикла; + НовыйРетранслятор(ОпределениеНабора).ВклеитьСостав(План.suites); КонецПроцедуры @@ -175,7 +159,7 @@ ВызватьИсключение СтрШаблон( "Дочернее обнаружение завершилось аварийно, не сформировав план:%1%2", Символы.ПС, - РазвернутьЗаписиЛога(Результат.Вывод) + _ПереносРезультатов.РазвернутьЗаписиЛога(Результат.Вывод) ); КонецЕсли; @@ -183,48 +167,6 @@ КонецФункции -// Строит узел-зеркало по записи состава плана дочернего обнаружения -// -// Параметры: -// Запись - Структура - Запись теста состава плана, см. РепортерПланJSON.ЗаписатьПлан -// Родитель - ОпределениеТестНабора, ОпределениеТеста - Родитель зеркала в плане -// -// Возвращаемое значение: -// ОпределениеТеста - Узел-зеркало -// -Функция ЗеркалоЗаписиПлана(Запись, Родитель) - - Дети = Новый СписокМассив; - - ЭтоКонтейнер = Запись.Свойство("children"); - - Зеркало = _Поделка.НайтиЖелудь( - "ОпределениеТеста", - Массивы.ИзЭлементов( - ?(ЭтоКонтейнер, ТипыОпределенийТестов.Контейнер, ТипыОпределенийТестов.Тест), - Запись.test, - Дети, - Родитель, - ЗначениеСвойства(Запись, "method", Запись.test), - Новый СписокМассив, - 0, - Новый СписокМассив, - Новый СписокМассив, - 0, - Неопределено - ) - ); - - Если ЭтоКонтейнер Тогда - Для каждого ЗаписьДитя Из Запись.children Цикл - Дети.Добавить(ЗеркалоЗаписиПлана(ЗаписьДитя, Зеркало)); - КонецЦикла; - КонецЕсли; - - Возврат Зеркало; - -КонецФункции - Процедура ИсполнитьТестВПоделке(Тест, ОпределениеНабора) Попытка @@ -289,7 +231,10 @@ МенеджерВременныхФайлов.Удалить(); Если Прогон.Отчет = Неопределено Тогда - ЗавершитьТест(Тест, РезультатСломан(ОшибкаДочернегоПрогона(Прогон.Вывод))); + ЗавершитьТест(Тест, РезультатСломан(_ПереносРезультатов.ПричинаСВыводом( + "Дочерний прогон изолированного теста завершился аварийно, не сформировав отчет", + Прогон.Вывод + ))); Возврат; КонецЕсли; @@ -332,7 +277,7 @@ Вывод )); Иначе - РезультатДитя = РезультатЗаписиТеста(Запись); + РезультатДитя = _ПереносРезультатов.РезультатЗаписи(Запись); КонецЕсли; _ПубликаторСобытий.ОпубликоватьСобытие( @@ -357,13 +302,13 @@ )); КонецЕсли; - Возврат РезультатЗаписиТеста(Запись); + Возврат _ПереносРезультатов.РезультатЗаписи(Запись); КонецФункции -// Возвращает результат узла изолированного прогона. Для контейнера дополнительно -// публикует события исполнения итераций, сопоставляя их с детьми родного -// определения по именам +// Возвращает результат узла изолированного прогона со временем выполнения, +// измеренным там. Для контейнера дополнительно публикует события исполнения +// итераций, сопоставляя их с детьми родного определения по именам // // Параметры: // ФрешУзел - ОпределениеТеста - Узел изолированного прогона @@ -377,7 +322,9 @@ Функция РезультатПоддерева(ФрешУзел, Родное, ФрешСтат, Знач Вывод) Если Родное.ТипОпределения() <> ТипыОпределенийТестов.Контейнер Тогда - Возврат ФрешСтат.Результат(ФрешУзел); + Возврат ФрешСтат.Результат(ФрешУзел).ПеренестиВремяВыполнения( + ФрешСтат.СтатистикаТеста(ФрешУзел).ПолучитьИлиУмолчание("ВремяВыполнения", 0) + ); КонецЕсли; Для каждого Дитя Из Родное.Дети() Цикл @@ -410,77 +357,68 @@ Процедура ИсполнитьВПоделке(ОпределениеНабора) + Ретранслятор = НовыйРетранслятор(ОпределениеНабора); + + Вывод = ""; + ОшибкаСреды = Неопределено; + Попытка - Прогон = ВыполнитьВПоделке(ОпределениеНабора, ОпределениеНабора, ""); + Вывод = ВыполнитьВПоделке(ОпределениеНабора, ОпределениеНабора, "", Ложь, Ретранслятор).Вывод; Исключение - // Сдерживание отказов: изолированная среда не поднялась или упало - // обнаружение - набор сломан, но общий прогон продолжается - ЗавершитьНабор(ОпределениеНабора, РезультатСломан(ИнформацияОбОшибке())); - Возврат; + ОшибкаСреды = ИнформацияОбОшибке(); КонецПопытки; - ФрешНабор = НайтиНаборВПлане(Прогон.План, ОпределениеНабора.Имя()); - - Если ФрешНабор = Неопределено Тогда - // Родитель набор обнаружил, а дочерний прогон - нет: изолированная среда - // собралась не так, как ожидалось (например, совет подменил детальки прогона) - ЗавершитьНабор(ОпределениеНабора, РезультатСломан(ОшибкаНаборНеОбнаружен(ОпределениеНабора, Прогон.Вывод))); - Возврат; - КонецЕсли; - - Для каждого ФрешДитя Из ФрешНабор.Дети() Цикл - - Зеркало = ПостроитьЗеркало(ФрешДитя, ОпределениеНабора); - ОпределениеНабора.ДобавитьТест(Зеркало); - - ОпубликоватьПоддерево(ФрешДитя, Зеркало, Прогон.Статистика); - - КонецЦикла; - - ЗавершитьНабор(ОпределениеНабора, Прогон.Статистика.Результат(ФрешНабор)); + ЗавершитьНабор(ОпределениеНабора, Ретранслятор.Завершить(Вывод, ОшибкаСреды)); КонецПроцедуры Процедура ИсполнитьВПроцессе(ОпределениеНабора) - МенеджерВременныхФайлов = Новый МенеджерВременныхФайлов(); + Ретранслятор = НовыйРетранслятор(ОпределениеНабора); + + ОшибкаСреды = Неопределено; Попытка - Прогон = ВыполнитьВДочернемПроцессе( - ОпределениеНабора, - ОпределениеНабора, - МенеджерВременныхФайлов, - "" - ); + ТранслироватьДочернийПроцесс(ОпределениеНабора, Ретранслятор); Исключение - // Сдерживание отказов: дочерний прогон не удалось запустить - - // набор сломан, но общий прогон продолжается - МенеджерВременныхФайлов.Удалить(); - ЗавершитьНабор(ОпределениеНабора, РезультатСломан(ИнформацияОбОшибке())); - Возврат; + // Дочерний прогон не удалось запустить + ОшибкаСреды = ИнформацияОбОшибке(); КонецПопытки; - МенеджерВременныхФайлов.Удалить(); + ЗавершитьНабор(ОпределениеНабора, Ретранслятор.Завершить("", ОшибкаСреды)); - Если Прогон.Отчет = Неопределено Тогда - // Отчета нет - дочерний процесс умер, не дописав его - ЗавершитьНабор(ОпределениеНабора, РезультатСломан(ОшибкаДочернегоПрогона(Прогон.Вывод))); - Возврат; - КонецЕсли; +КонецПроцедуры + +// Запускает дочерний прогон файла изолированного набора с трансляцией событий +// исполнения: вывод процесса по мере поступления разбирает ретранслятор +// +// Параметры: +// ОпределениеНабора - ОпределениеТестНабора - Плейсхолдер изолированного набора +// Ретранслятор - РетрансляторСобытийИзоляции - Ретранслятор событий набора +// +Процедура ТранслироватьДочернийПроцесс(ОпределениеНабора, Ретранслятор) + + ПараметрыЗапуска = ПараметрыДочернегоПрогона(ОпределениеНабора, ОпределениеНабора); - ВклеитьОтчет(ОпределениеНабора, Прогон.Отчет); + ПараметрыЗапуска.ЛогВывода = Ретранслятор; + ПараметрыЗапуска.ТрансляцияСобытий = Истина; + + _Запускатель.ВыполнитьТестирование( + ДеталькиДочернегоПрогона(ОпределениеНабора, ОпределениеНабора, ""), + _Лог, + ПараметрыЗапуска + ); КонецПроцедуры -// Запускает дочерний прогон файла изолированного набора с формированием -// JSON-отчета и возвращает разобранный отчет +// Запускает дочерний прогон файла набора, отфильтрованного до изолированного +// теста, с формированием JSON-отчета и возвращает разобранный отчет // // Параметры: -// Источник - ОпределениеТестНабора, ОпределениеТеста - Определение с описанием изоляции -// ОпределениеНабора - ОпределениеТестНабора - Набор изолированного прогона +// Источник - ОпределениеТеста - Изолированный тест +// ОпределениеНабора - ОпределениеТестНабора - Набор изолированного теста // МенеджерВременныхФайлов - МенеджерВременныхФайлов - Менеджер временных файлов прогона -// ФильтрМетода - Строка - Имя тестового метода для изоляции одного теста, -// пустая строка - прогон всего набора +// ФильтрМетода - Строка - Имя тестового метода изолированного теста // // Возвращаемое значение: // Структура - Результаты дочернего прогона: @@ -491,21 +429,64 @@ ПутьКОтчету = МенеджерВременныхФайлов.НовоеИмяФайла("json"); + Детальки = ДеталькиДочернегоПрогона(Источник, ОпределениеНабора, ФильтрМетода); + Детальки.Вставить("ПутьКОтчетуJSON", ПутьКОтчету); + + Результат = _Запускатель.ВыполнитьТестирование( + Детальки, + _Лог, + ПараметрыДочернегоПрогона(Источник, ОпределениеНабора) + ); + + Возврат Новый Структура( + "Отчет, Вывод", + _Запускатель.ПрочитатьОтчет(ПутьКОтчету), + Результат.Вывод + ); + +КонецФункции + +// Возвращает детальки дочернего прогона изолированного набора или теста +// +// Параметры: +// Источник - ОпределениеТестНабора, ОпределениеТеста - Определение с описанием изоляции +// ОпределениеНабора - ОпределениеТестНабора - Набор изолированного прогона +// ФильтрМетода - Строка - Имя тестового метода для изоляции одного теста, +// пустая строка - прогон всего набора +// +// Возвращаемое значение: +// Соответствие из КлючИЗначение - Детальки прогона, см. ЗапускательТестирования.ВыполнитьТестирование +// +Функция ДеталькиДочернегоПрогона(Источник, ОпределениеНабора, Знач ФильтрМетода) + Детальки = Новый Соответствие; Детальки.Вставить("КаталогиТестов", Новый Массив); Детальки.Вставить("ФайлыТестов", Массивы.ИзЭлементов(ОпределениеНабора.Путь())); Детальки.Вставить("ИскатьВПодкаталогах", Ложь); Детальки.Вставить("Таймаут", Источник.Таймаут()); Детальки.Вставить("РежимВыводаЛога", РежимыВыводаЛога.Ничего); - Детальки.Вставить("ПутьКОтчетуJSON", ПутьКОтчету); Детальки.Вставить("ВыполняетсяВИзоляции", Истина); Детальки.Вставить("ТестовыеМетодыВключаемые", ФильтрМетода); Детальки.Вставить("ПрерыватьПриПервомПадении", _ПрерыватьПриПадении); - // Вывод дочернего прогона не транслируется в консоль родителя, чтобы не - // разрывать его собственный вывод: пер-тестовые выводы и причины падений - // переносятся из отчета внутрь результатов тестов, а целиком захваченный - // вывод используется как причина при аварийном завершении прогона + Возврат Детальки; + +КонецФункции + +// Возвращает параметры запуска дочернего прогона изолированного набора или теста. +// Вывод дочернего прогона не транслируется в консоль родителя, чтобы не разрывать +// его собственный вывод: результаты тестов переносятся структурно, а захваченный +// вывод используется как причина при аварийном завершении прогона +// +// Параметры: +// Источник - ОпределениеТестНабора, ОпределениеТеста - Определение с описанием изоляции +// ОпределениеНабора - ОпределениеТестНабора - Набор изолированного прогона +// +// Возвращаемое значение: +// Структура - Параметры запуска, см. ЗапускательТестирования.ПараметрыЗапуска +// +Функция ПараметрыДочернегоПрогона(Источник, ОпределениеНабора) + ПараметрыЗапуска = _Запускатель.ПараметрыЗапуска(); ПараметрыЗапуска.Импорты = Импорты(Источник, ОпределениеНабора); @@ -513,17 +494,7 @@ ПараметрыЗапуска.РаскладкаЛога = "РаскладкаЛогOneUnit"; ПараметрыЗапуска.ПутьКСтатистикеПокрытия = ПутьКСтатистикеПокрытия(); - Результат = _Запускатель.ВыполнитьТестирование( - Детальки, - _Лог, - ПараметрыЗапуска - ); - - Возврат Новый Структура( - "Отчет, Вывод", - _Запускатель.ПрочитатьОтчет(ПутьКОтчету), - Результат.Вывод - ); + Возврат ПараметрыЗапуска; КонецФункции @@ -572,88 +543,14 @@ КонецФункции -// Вклеивает результаты дочернего прогона из JSON-отчета в план родителя: -// каждый тест отчета публикуется событиями исполнения как обычный тест -// -// Параметры: -// ОпределениеНабора - ОпределениеТестНабора - Плейсхолдер изолированного набора -// Отчет - см. ЗапускательТестирования.ПрочитатьОтчет -// -Процедура ВклеитьОтчет(ОпределениеНабора, Отчет) - - Для каждого ЗаписьТеста Из Отчет.tests Цикл - - Зеркало = ЗеркалоЗаписиТеста(ЗаписьТеста, ОпределениеНабора); - ОпределениеНабора.ДобавитьТест(Зеркало); - - _ПубликаторСобытий.ОпубликоватьСобытие(Зеркало, "ИсполнениеТестНачало", Новый Массив); - _ПубликаторСобытий.ОпубликоватьСобытие( - Зеркало, - "ИсполнениеТестКонец", - Массивы.ИзЭлементов(РезультатЗаписиТеста(ЗаписьТеста)) - ); - - КонецЦикла; - - ЗавершитьНабор(ОпределениеНабора, РезультатНабораИзОтчета(ОпределениеНабора.Имя(), Отчет)); - -КонецПроцедуры - -Функция ЗеркалоЗаписиТеста(ЗаписьТеста, ОпределениеНабора) - - Возврат _Поделка.НайтиЖелудь( - "ОпределениеТеста", - Массивы.ИзЭлементов( - ТипыОпределенийТестов.Тест, - ЗаписьТеста.test, - Новый СписокМассив, - ОпределениеНабора, - ЗаписьТеста.test, - Новый СписокМассив, - 0, - Новый СписокМассив, - Новый СписокМассив, - 0, - Неопределено - ) - ); - -КонецФункции - -Функция РезультатЗаписиТеста(ЗаписьТеста) - - Состояние = СостояниеПоСтатусу(ЗаписьТеста.status); - - Причины = Новый СписокМассив; - - Если Состояние = СостоянияВыполненияТестов.Ошибка - Или Состояние = СостоянияВыполненияТестов.Сломан Тогда - - Причины.Добавить(ПричинаИзОтчета(ЗаписьТеста)); - - ИначеЕсли Состояние = СостоянияВыполненияТестов.Пропущен - Или Состояние = СостоянияВыполненияТестов.Прерван Тогда - - Причины.Добавить(ЗначениеСвойства(ЗаписьТеста, "message", "")); - - КонецЕсли; // BSLLS:IfElseIfEndsWithElse-off - - Возврат Результат( - Состояние, - Причины, - ЗначениеСвойства(ЗаписьТеста, "output", "") - ); - -КонецФункции - Функция РезультатНабораИзОтчета(Знач ИмяНабора, Отчет) // Проблема или пропуск уровня набора - запись без имени теста Для каждого Проблема Из Отчет.problems Цикл Если Не Проблема.Свойство("test") И Проблема.suite = ИмяНабора Тогда Возврат Результат( - СостояниеПоСтатусу(Проблема.status), - Списки.ИзЭлементов(ПричинаИзОтчета(Проблема)), + _ПереносРезультатов.СостояниеПоСтатусу(Проблема.status), + Списки.ИзЭлементов(_ПереносРезультатов.ПричинаЗаписи(Проблема)), "" ); КонецЕсли; @@ -662,7 +559,7 @@ Для каждого Пропуск Из Отчет.skipped_tests Цикл Если Не Пропуск.Свойство("test") И Пропуск.suite = ИмяНабора Тогда Возврат Результат( - СостояниеПоСтатусу(Пропуск.status), + _ПереносРезультатов.СостояниеПоСтатусу(Пропуск.status), Списки.ИзЭлементов(ЗначениеСвойства(Пропуск, "reason", "")), "" ); @@ -674,164 +571,6 @@ КонецФункции -// Заворачивает сообщение и подробности записи отчета в ИнформациюОбОшибке: -// репортеры родителя ожидают у причин проблем интерфейс информации об ошибке. -// Подробности передаются как готовое описание - без служебного стека обертки -// -// Параметры: -// Запись - Структура - Запись отчета с полями message и details -// -// Возвращаемое значение: -// ИнформацияОбОшибке - Причина с сообщением и подробностями дочернего прогона -// -Функция ПричинаИзОтчета(Запись) - - Сообщение = ЗначениеСвойства(Запись, "message", "Дочерний прогон завершился с ошибкой"); - Описание = ЗначениеСвойства(Запись, "details", ""); - - Если Не ЗначениеЗаполнено(Описание) Тогда - Описание = СтрШаблон( - " - |Причина: - |%1 - |", - Сообщение - ); - КонецЕсли; - - Возврат ПричинаСГотовымОписанием(Сообщение, Описание); - -КонецФункции - -Функция ОшибкаДочернегоПрогона(Знач Вывод) - - Сообщение = "Дочерний прогон изолированного набора завершился аварийно, не сформировав отчет"; - - Возврат ПричинаСГотовымОписанием(Сообщение, ОписаниеПричиныСВыводом(Сообщение, Вывод)); - -КонецФункции - -// Составляет подробное описание причины, дополняя сообщение захваченным выводом -// дочернего прогона - единственным свидетельством того, что происходило -// в изолированной среде -// -// Параметры: -// Сообщение - Строка - Краткое описание причины -// Вывод - Строка - Захваченный вывод дочернего прогона -// -// Возвращаемое значение: -// Строка - Подробное описание причины -// -Функция ОписаниеПричиныСВыводом(Знач Сообщение, Знач Вывод) - - Вывод = РазвернутьЗаписиЛога(Вывод); - - Если Не ЗначениеЗаполнено(СокрЛП(Вывод)) Тогда - Вывод = "<дочерний прогон ничего не вывел>"; - КонецЕсли; - - Возврат СтрШаблон( - " - |Причина: - |%1 - | - |Вывод дочернего прогона: - |%2 - |", - Сообщение, - Вывод - ); - -КонецФункции - -// Разворачивает записи JSON-раскладки лога в захваченном выводе дочернего прогона -// в их сообщения: лог изолированной среды мог быть настроен не так, как у родителя, -// а сырые JSON-записи в описании причины нечитаемы -// -// Параметры: -// Вывод - Строка - Захваченный вывод дочернего прогона -// -// Возвращаемое значение: -// Строка - Вывод с сообщениями вместо JSON-записей лога -// -Функция РазвернутьЗаписиЛога(Знач Вывод) - - Строки = Новый Массив; - - Для каждого СтрокаВывода Из СтрРазделить(Вывод, Символы.ПС) Цикл - Строки.Добавить(СообщениеЗаписиЛога(СтрокаВывода)); - КонецЦикла; - - Возврат СтрСоединить(Строки, Символы.ПС); - -КонецФункции - -Функция СообщениеЗаписиЛога(Знач СтрокаВывода) - - Если Не СтрНачинаетсяС(СокрЛП(СтрокаВывода), "{") Тогда - Возврат СтрокаВывода; - КонецЕсли; - - Попытка - Чтение = Новый ЧтениеJSON(); - Чтение.УстановитьСтроку(СтрокаВывода); - Запись = ПрочитатьJSON(Чтение, Истина); - Чтение.Закрыть(); - Исключение - // Не запись лога, а просто строка с фигурной скобкой - оставляем как есть - Возврат СтрокаВывода; - КонецПопытки; - - Если ТипЗнч(Запись) <> Тип("Соответствие") Тогда - Возврат СтрокаВывода; - КонецЕсли; - - Сообщение = Запись.Получить("msg"); - - Возврат ?(ТипЗнч(Сообщение) = Тип("Строка"), Сообщение, СтрокаВывода); - -КонецФункции - -// Создает причину, подробное описание которой уже сформировано дочерним прогоном: -// ТестированиеСлужебный.ПодробноеОписаниеОшибки вернет его как есть, не подмешивая -// модуль, строку и стек этой обертки -// -// Параметры: -// Сообщение - Строка - Краткое описание причины -// ГотовоеОписание - Строка - Подробное описание, сформированное дочерним прогоном -// -// Возвращаемое значение: -// ИнформацияОбОшибке - Причина с готовым подробным описанием -// -Функция ПричинаСГотовымОписанием(Знач Сообщение, Знач ГотовоеОписание) - - Попытка - ВызватьИсключение Новый ИнформацияОбОшибке( - Сообщение, - Новый Структура("ПеренесенноеОписание", ГотовоеОписание) - ); - Исключение - Возврат ИнформацияОбОшибке(); - КонецПопытки; - -КонецФункции - -Функция СостояниеПоСтатусу(Знач Статус) - - Если Статус = "failed" Тогда - Возврат СостоянияВыполненияТестов.Ошибка; - ИначеЕсли Статус = "broken" Тогда - Возврат СостоянияВыполненияТестов.Сломан; - ИначеЕсли Статус = "skipped" Тогда - Возврат СостоянияВыполненияТестов.Пропущен; - ИначеЕсли Статус = "aborted" Тогда - Возврат СостоянияВыполненияТестов.Прерван; - Иначе - Возврат СостоянияВыполненияТестов.Успех; - КонецЕсли; - -КонецФункции - Функция ЗначениеСвойства(Запись, Знач ИмяСвойства, Знач Умолчание) Значение = Умолчание; @@ -850,6 +589,8 @@ // ФильтрМетода - Строка - Имя тестового метода для изоляции одного теста, // пустая строка - прогон всего набора // ТолькоОбнаружение - Булево - Составить план, не исполняя тесты +// Ретранслятор - РетрансляторСобытийИзоляции - Ретранслятор, которому свежая поделка +// транслирует события исполнения, Неопределено - без трансляции // // Возвращаемое значение: // Структура - Результаты изолированного прогона: @@ -858,7 +599,12 @@ // * Статистика - РепортерСтатистика - Статистика прогона // * Вывод - Строка - Захваченный консольный вывод прогона // -Функция ВыполнитьВПоделке(Источник, ОпределениеНабора, Знач ФильтрМетода, Знач ТолькоОбнаружение = Ложь) +Функция ВыполнитьВПоделке( + Источник, + ОпределениеНабора, + Знач ФильтрМетода, + Знач ТолькоОбнаружение = Ложь, + Ретранслятор = Неопределено) Совет = ПостроитьСовет(Источник, ОпределениеНабора); @@ -889,6 +635,11 @@ Поделка = Новый Поделка(Совет); Поделка.ЗапуститьПриложение(); + Если Ретранслятор <> Неопределено Тогда + Ретранслятор.ПубликоватьМимоПерехвата(ПотокВывода); + Поделка.НайтиЖелудь("ТрансляторСобытийИзоляции", Массивы.ИзЭлементов(Ретранслятор)); + КонецЕсли; + Если ТолькоОбнаружение Тогда План = Поделка.НайтиЖелудь("МенеджерТестирования").Обнаружить(); Иначе @@ -896,12 +647,12 @@ КонецЕсли; Исключение - ТестированиеСлужебный.ЗавершитьПерехватВывода(); + ЗавершитьПерехватВывода(Ретранслятор); ПотокВывода.Закрыть(); ВызватьИсключение; КонецПопытки; - ТестированиеСлужебный.ЗавершитьПерехватВывода(); + ЗавершитьПерехватВывода(Ретранслятор); Прогон = Новый Структура; Прогон.Вставить("Поделка", Поделка); @@ -915,6 +666,16 @@ КонецФункции +Процедура ЗавершитьПерехватВывода(Ретранслятор) + + ТестированиеСлужебный.ЗавершитьПерехватВывода(); + + Если Ретранслятор <> Неопределено Тогда + Ретранслятор.ПубликоватьМимоПерехвата(Неопределено); + КонецЕсли; + +КонецПроцедуры + // Строит совет создания изолированной поделки. При наличии лямбды совета // исполняет ее в контексте набора, иначе возвращает совет по умолчанию. // @@ -953,7 +714,7 @@ // Функция НайтиНаборВПлане(План, Знач Имя) - Для каждого Набор Из План.Дети() Цикл + Для Каждого Набор Из План.Дети() Цикл Если Набор.Имя() = Имя Тогда Возврат Набор; КонецЕсли; @@ -965,7 +726,7 @@ Функция НайтиТестВУзле(Узел, Знач Имя) - Для каждого Дитя Из Узел.Дети() Цикл + Для Каждого Дитя Из Узел.Дети() Цикл Если Дитя.Имя() = Имя Тогда Возврат Дитя; КонецЕсли; @@ -977,7 +738,7 @@ Функция НайтиЗаписьТеста(Отчет, Знач ИмяТеста) - Для каждого Запись Из Отчет.tests Цикл + Для Каждого Запись Из Отчет.tests Цикл Если Запись.test = ИмяТеста Тогда Возврат Запись; КонецЕсли; @@ -1007,6 +768,10 @@ КонецФункции +Функция НовыйРетранслятор(ОпределениеНабора) + Возврат _Поделка.НайтиЖелудь("РетрансляторСобытийИзоляции", Массивы.ИзЭлементов(ОпределениеНабора)); +КонецФункции + Процедура ЗавершитьТест(Тест, Результат) _ПубликаторСобытий.ОпубликоватьСобытие( @@ -1041,103 +806,13 @@ // Функция ОшибкаТестНеВыполнен(Тест, Знач Пояснение, Знач Вывод) - Сообщение = СтрШаблон( - "Тест %1 не был выполнен в изолированной среде: %2", - Тест.Имя(), - Пояснение - ); - - Возврат ПричинаСГотовымОписанием(Сообщение, ОписаниеПричиныСВыводом(Сообщение, Вывод)); - -КонецФункции - -Функция ОшибкаНаборНеОбнаружен(ОпределениеНабора, Знач Вывод) - - Сообщение = СтрШаблон( - "Изолированный набор %1 не был обнаружен в дочернем прогоне", - ОпределениеНабора.Имя() - ); - - Возврат ПричинаСГотовымОписанием(Сообщение, ОписаниеПричиныСВыводом(Сообщение, Вывод)); - -КонецФункции - -// Строит узел-зеркало определения теста, повторяющий структуру узла -// изолированного прогона, для публикации событий в родительский план -// -// Параметры: -// ФрешУзел - ОпределениеТеста - Узел изолированного прогона -// РодительЗеркала - ОпределениеТестНабора, ОпределениеТеста - Родитель зеркала в плане -// -// Возвращаемое значение: -// ОпределениеТеста - Узел-зеркало -// -Функция ПостроитьЗеркало(ФрешУзел, РодительЗеркала) - - Дети = Новый СписокМассив; - - Зеркало = _Поделка.НайтиЖелудь( - "ОпределениеТеста", - Массивы.ИзЭлементов( - ФрешУзел.ТипОпределения(), - ФрешУзел.Имя(), - Дети, - РодительЗеркала, - ФрешУзел.ИмяМетода(), - Новый СписокМассив, - 0, - Новый СписокМассив, - Новый СписокМассив, - ФрешУзел.Порядок(), - Неопределено - ) + Возврат _ПереносРезультатов.ПричинаСВыводом( + СтрШаблон("Тест %1 не был выполнен в изолированной среде: %2", Тест.Имя(), Пояснение), + Вывод ); - Если ФрешУзел.ТипОпределения() = ТипыОпределенийТестов.Контейнер Тогда - Для каждого ФрешДитя Из ФрешУзел.Дети() Цикл - Дети.Добавить(ПостроитьЗеркало(ФрешДитя, Зеркало)); - КонецЦикла; - КонецЕсли; - - Возврат Зеркало; - КонецФункции -// Публикует события исполнения для поддерева зеркала, перенося в текущий прогон -// результаты, накопленные статистикой изолированного прогона -// -// Параметры: -// ФрешУзел - ОпределениеТеста - Узел изолированного прогона -// Зеркало - ОпределениеТеста - Соответствующий узел-зеркало -// ФрешСтат - РепортерСтатистика - Статистика изолированного прогона -// -Процедура ОпубликоватьПоддерево(ФрешУзел, Зеркало, ФрешСтат) - - _ПубликаторСобытий.ОпубликоватьСобытие(Зеркало, "ИсполнениеТестНачало", Новый Массив); - - Если ФрешУзел.ТипОпределения() = ТипыОпределенийТестов.Контейнер Тогда - - ФрешДети = ФрешУзел.Дети(); - ДетиЗеркал = Зеркало.Дети(); - - Для Индекс = 0 По ФрешДети.Количество() - 1 Цикл - ОпубликоватьПоддерево(ФрешДети.Получить(Индекс), ДетиЗеркал.Получить(Индекс), ФрешСтат); - КонецЦикла; - - Результат = РезультатУспех(); - - Иначе - Результат = ФрешСтат.Результат(ФрешУзел); - КонецЕсли; - - _ПубликаторСобытий.ОпубликоватьСобытие( - Зеркало, - "ИсполнениеТестКонец", - Массивы.ИзЭлементов(Результат) - ); - -КонецПроцедуры - Функция РезультатСломан(ИнформацияОбОшибке) Возврат Результат(СостоянияВыполненияТестов.Сломан, Списки.ИзЭлементов(ИнформацияОбОшибке)); КонецФункции @@ -1158,6 +833,8 @@ &Пластилин Поделка, &Пластилин ПубликаторСобытий, &Пластилин ЗапускательТестирования, + &Пластилин ПереносРезультатов, + &Пластилин РепортерПланJSON, &Деталька("OneUnit.КаталогСтатистикиПокрытия", ЗначениеПоУмолчанию = "") КаталогСтатистикиПокрытия, &Деталька("OneUnit.ПрерыватьПриПервомПадении", ЗначениеПоУмолчанию = Ложь) ПрерыватьПриПадении, &Лог("oscript.lib.oneunit.core") Лог) @@ -1165,6 +842,8 @@ _Поделка = Поделка; _ПубликаторСобытий = ПубликаторСобытий; _Запускатель = ЗапускательТестирования; + _ПереносРезультатов = ПереносРезультатов; + _РепортерПланJSON = РепортерПланJSON; _КаталогСтатистикиПокрытия = КаталогСтатистикиПокрытия; _ПрерыватьПриПадении = ПрерыватьПриПадении; _Лог = Лог; diff --git "a/src/core/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\260\320\275\320\260\320\273\320\241\320\276\320\261\321\213\321\202\320\270\320\271\320\230\320\267\320\276\320\273\321\217\321\206\320\270\320\270.os" "b/src/core/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\260\320\275\320\260\320\273\320\241\320\276\320\261\321\213\321\202\320\270\320\271\320\230\320\267\320\276\320\273\321\217\321\206\320\270\320\270.os" new file mode 100644 index 0000000..d1cef5d --- /dev/null +++ "b/src/core/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\260\320\275\320\260\320\273\320\241\320\276\320\261\321\213\321\202\320\270\320\271\320\230\320\267\320\276\320\273\321\217\321\206\320\270\320\270.os" @@ -0,0 +1,61 @@ +// Канал трансляции событий из дочернего процесса изолированного прогона: каждое +// сообщение - отдельная строка stdout, маркер и JSON в одну строку. Сообщения +// идут вперемешку с прочим выводом процесса, который остается его выводом. +// +// Строка канала - чистый ASCII: символы вне ASCII экранируются, и кодовая страница +// консоли не искажает имена наборов и тестов. Консоль в UTF-8 начинает порции +// вывода процесса с BOM - при разборе он отбрасывается. + +Перем _Маркер; // Строка - Префикс строки-сообщения + +// Выводит сообщение строкой в stdout дочернего процесса - приемник транслятора +// событий изолированного прогона на уровне "Процесс" +// +// Параметры: +// Сообщение - Структура - Сообщение трансляции, см. ТрансляторСобытийИзоляции +// +Процедура Принять(Сообщение) Экспорт + + ЗаписьJSON = Новый ЗаписьJSON(); + ЗаписьJSON.УстановитьСтроку(Новый ПараметрыЗаписиJSON( + ПереносСтрокJSON.Нет, + , + , + ЭкранированиеСимволовJSON.СимволыВнеASCII + )); + ЗаписатьJSON(ЗаписьJSON, Сообщение); + + Консоль.ВывестиСтроку(_Маркер + ЗаписьJSON.Закрыть()); + +КонецПроцедуры + +// Разбирает строку вывода дочернего процесса +// +// Параметры: +// СтрокаВывода - Строка - Строка вывода без перевода строки +// +// Возвращаемое значение: +// Структура - Сообщение трансляции, см. ТрансляторСобытийИзоляции +// Неопределено - строка не сообщение, а обычный вывод +// +Функция Разобрать(Знач СтрокаВывода) Экспорт + + СтрокаВывода = СтрЗаменить(СтрЗаменить(СтрокаВывода, Символ(65279), ""), Символы.ВК, ""); + + Если Не СтрНачинаетсяС(СтрокаВывода, _Маркер) Тогда + Возврат Неопределено; + КонецЕсли; + + ЧтениеJSON = Новый ЧтениеJSON(); + ЧтениеJSON.УстановитьСтроку(Сред(СтрокаВывода, СтрДлина(_Маркер) + 1)); + Сообщение = ПрочитатьJSON(ЧтениеJSON); + ЧтениеJSON.Закрыть(); + + Возврат Сообщение; + +КонецФункции + +&Желудь +Процедура ПриСозданииОбъекта() + _Маркер = "##oneunit:"; +КонецПроцедуры diff --git "a/src/core/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\237\320\265\321\200\320\265\320\275\320\276\321\201\320\240\320\265\320\267\321\203\320\273\321\214\321\202\320\260\321\202\320\276\320\262.os" "b/src/core/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\237\320\265\321\200\320\265\320\275\320\276\321\201\320\240\320\265\320\267\321\203\320\273\321\214\321\202\320\260\321\202\320\276\320\262.os" new file mode 100644 index 0000000..c0de4fd --- /dev/null +++ "b/src/core/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\237\320\265\321\200\320\265\320\275\320\276\321\201\320\240\320\265\320\267\321\203\320\273\321\214\321\202\320\260\321\202\320\276\320\262.os" @@ -0,0 +1,314 @@ +// Переводит результаты выполнения тестов в записи и обратно. Записями результаты +// уходят в JSON-отчет прогона и переезжают из изолированной среды в родительский +// прогон, поэтому формат у отчета и у трансляции событий один. + +#Использовать collectionos +#Использовать "../../../shared" + +Перем _Поделка; // Поделка - Поделка текущего прогона + +// Возвращает запись результата выполнения +// +// Параметры: +// Результат - РезультатВыполненияТеста - Результат теста или набора +// +// Возвращаемое значение: +// Структура - Запись результата: +// * status - Строка - Статус: "passed", "failed", "broken", "skipped" или "aborted" +// * message - Строка - Краткие причины, отсутствует у успешных +// * details - Строка - Подробные причины со стеками, только у упавших и сломанных +// * output - Строка - Захваченный вывод, отсутствует при пустом выводе +// +Функция ЗаписьРезультата(Результат) Экспорт + + Запись = Новый Структура(); + + Запись.Вставить("status", СтатусСостояния(Результат.Состояние())); + + Если Результат.Состояние() = СостоянияВыполненияТестов.Ошибка + Или Результат.Состояние() = СостоянияВыполненияТестов.Сломан Тогда + + // Подробности со стеками есть только у проблем - + // причины пропусков и прерываний описываются строками + Запись.Вставить("message", ОписанияПричин(Результат)); + Запись.Вставить("details", ПодробныеПричины(Результат)); + + ИначеЕсли Результат.Состояние() <> СостоянияВыполненияТестов.Успех Тогда + Запись.Вставить("message", ОписанияПричин(Результат)); + КонецЕсли; // BSLLS:IfElseIfEndsWithElse-off + + Если Не ПустаяСтрока(Результат.Вывод()) Тогда + Запись.Вставить("output", Результат.Вывод()); + КонецЕсли; + + Возврат Запись; + +КонецФункции + +// Восстанавливает результат выполнения по записи. Время выполнения из записи +// переносится в результат: запись приходит из среды, где тест выполнялся +// +// Параметры: +// Запись - Структура - Запись результата, см. ЗаписьРезультата, и необязательное +// поле duration_ms - Число - время выполнения в миллисекундах +// +// Возвращаемое значение: +// РезультатВыполненияТеста - Восстановленный результат +// +Функция РезультатЗаписи(Запись) Экспорт + + Состояние = СостояниеПоСтатусу(Запись.status); + + Причины = Новый СписокМассив; + + Если Состояние = СостоянияВыполненияТестов.Ошибка + Или Состояние = СостоянияВыполненияТестов.Сломан Тогда + + Причины.Добавить(ПричинаЗаписи(Запись)); + + ИначеЕсли Состояние = СостоянияВыполненияТестов.Пропущен + Или Состояние = СостоянияВыполненияТестов.Прерван Тогда + + Причины.Добавить(ЗначениеСвойства(Запись, "message", "")); + + КонецЕсли; // BSLLS:IfElseIfEndsWithElse-off + + Результат = _Поделка.НайтиЖелудь( + "РезультатВыполненияТеста", + Массивы.ИзЭлементов( + Состояние, + Причины, + ЗначениеСвойства(Запись, "output", "") + ) + ); + + Если Запись.Свойство("duration_ms") Тогда + Результат.ПеренестиВремяВыполнения(Запись.duration_ms); + КонецЕсли; + + Возврат Результат; + +КонецФункции + +// Заворачивает сообщение и подробности записи в ИнформациюОбОшибке: +// репортеры ожидают у причин проблем интерфейс информации об ошибке +// +// Параметры: +// Запись - Структура - Запись с полями message и details +// +// Возвращаемое значение: +// ИнформацияОбОшибке - Причина с сообщением и подробностями записи +// +Функция ПричинаЗаписи(Запись) Экспорт + + Сообщение = ЗначениеСвойства(Запись, "message", "Дочерний прогон завершился с ошибкой"); + Описание = ЗначениеСвойства(Запись, "details", ""); + + Если Не ЗначениеЗаполнено(Описание) Тогда + Описание = СтрШаблон( + " + |Причина: + |%1 + |", + Сообщение + ); + КонецЕсли; + + Возврат ПричинаСГотовымОписанием(Сообщение, Описание); + +КонецФункции + +// Создает причину, подробное описание которой уже сформировано другой средой: +// ТестированиеСлужебный.ПодробноеОписаниеОшибки вернет его как есть, не подмешивая +// модуль, строку и стек этой обертки +// +// Параметры: +// Сообщение - Строка - Краткое описание причины +// ГотовоеОписание - Строка - Подробное описание, сформированное другой средой +// +// Возвращаемое значение: +// ИнформацияОбОшибке - Причина с готовым подробным описанием +// +Функция ПричинаСГотовымОписанием(Знач Сообщение, Знач ГотовоеОписание) Экспорт + + Попытка + ВызватьИсключение Новый ИнформацияОбОшибке( + Сообщение, + Новый Структура("ПеренесенноеОписание", ГотовоеОписание) + ); + Исключение + Возврат ИнформацияОбОшибке(); + КонецПопытки; + +КонецФункции + +// Создает причину, подробное описание которой дополнено захваченным выводом +// изолированной среды - единственным свидетельством того, что там происходило +// +// Параметры: +// Сообщение - Строка - Краткое описание причины +// Вывод - Строка - Захваченный вывод изолированной среды +// +// Возвращаемое значение: +// ИнформацияОбОшибке - Причина с готовым подробным описанием +// +Функция ПричинаСВыводом(Знач Сообщение, Знач Вывод) Экспорт + + Вывод = РазвернутьЗаписиЛога(Вывод); + + Если Не ЗначениеЗаполнено(СокрЛП(Вывод)) Тогда + Вывод = "<дочерний прогон ничего не вывел>"; + КонецЕсли; + + Возврат ПричинаСГотовымОписанием(Сообщение, СтрШаблон( + " + |Причина: + |%1 + | + |Вывод дочернего прогона: + |%2 + |", + Сообщение, + Вывод + )); + +КонецФункции + +// Разворачивает записи JSON-раскладки лога в захваченном выводе изолированной +// среды в их сообщения: лог изолированной среды мог быть настроен не так, +// как у родителя, а сырые JSON-записи в описании причины нечитаемы +// +// Параметры: +// Вывод - Строка - Захваченный вывод изолированной среды +// +// Возвращаемое значение: +// Строка - Вывод с сообщениями вместо JSON-записей лога +// +Функция РазвернутьЗаписиЛога(Знач Вывод) Экспорт + + Строки = Новый Массив; + + Для Каждого СтрокаВывода Из СтрРазделить(Вывод, Символы.ПС) Цикл + Строки.Добавить(СообщениеЗаписиЛога(СтрокаВывода)); + КонецЦикла; + + Возврат СтрСоединить(Строки, Символы.ПС); + +КонецФункции + +// Возвращает краткие описания причин результата одной строкой +// +// Параметры: +// Результат - РезультатВыполненияТеста - Результат теста или набора +// +// Возвращаемое значение: +// Строка - Краткие описания причин через перенос строки +// +Функция ОписанияПричин(Результат) Экспорт + Возврат СтрСоединить(Результат.ОписанияПричин().ВМассив(), Символы.ПС); +КонецФункции + +// Возвращает подробные описания причин результата со стеками одной строкой +// +// Параметры: +// Результат - РезультатВыполненияТеста - Результат теста или набора +// +// Возвращаемое значение: +// Строка - Подробные описания причин через перенос строки +// +Функция ПодробныеПричины(Результат) Экспорт + + Возврат Результат.Причины() + .ПроцессорКоллекции() + .Обработать("Причина -> ТестированиеСлужебный.ПодробноеОписаниеОшибки(""Причина"", Причина)") + .ВСтроку(Символы.ПС); + +КонецФункции + +// Возвращает статус записи по состоянию выполнения +// +// Параметры: +// Состояние - Строка - Состояние выполнения, см. СостоянияВыполненияТестов +// +// Возвращаемое значение: +// Строка - Статус: "passed", "failed", "broken", "skipped" или "aborted" +// +Функция СтатусСостояния(Знач Состояние) Экспорт + + Если Состояние = СостоянияВыполненияТестов.Ошибка Тогда + Возврат "failed"; + ИначеЕсли Состояние = СостоянияВыполненияТестов.Сломан Тогда + Возврат "broken"; + ИначеЕсли Состояние = СостоянияВыполненияТестов.Пропущен Тогда + Возврат "skipped"; + ИначеЕсли Состояние = СостоянияВыполненияТестов.Прерван Тогда + Возврат "aborted"; + Иначе + Возврат "passed"; + КонецЕсли; + +КонецФункции + +// Возвращает состояние выполнения по статусу записи +// +// Параметры: +// Статус - Строка - Статус записи, см. СтатусСостояния +// +// Возвращаемое значение: +// Строка - Состояние выполнения, см. СостоянияВыполненияТестов +// +Функция СостояниеПоСтатусу(Знач Статус) Экспорт + + Если Статус = "failed" Тогда + Возврат СостоянияВыполненияТестов.Ошибка; + ИначеЕсли Статус = "broken" Тогда + Возврат СостоянияВыполненияТестов.Сломан; + ИначеЕсли Статус = "skipped" Тогда + Возврат СостоянияВыполненияТестов.Пропущен; + ИначеЕсли Статус = "aborted" Тогда + Возврат СостоянияВыполненияТестов.Прерван; + Иначе + Возврат СостоянияВыполненияТестов.Успех; + КонецЕсли; + +КонецФункции + +Функция СообщениеЗаписиЛога(Знач СтрокаВывода) + + Если Не СтрНачинаетсяС(СокрЛП(СтрокаВывода), "{") Тогда + Возврат СтрокаВывода; + КонецЕсли; + + Попытка + Чтение = Новый ЧтениеJSON(); + Чтение.УстановитьСтроку(СтрокаВывода); + Запись = ПрочитатьJSON(Чтение, Истина); + Чтение.Закрыть(); + Исключение + // Не запись лога, а просто строка с фигурной скобкой - оставляем как есть + Возврат СтрокаВывода; + КонецПопытки; + + Если ТипЗнч(Запись) <> Тип("Соответствие") Тогда + Возврат СтрокаВывода; + КонецЕсли; + + Сообщение = Запись.Получить("msg"); + + Возврат ?(ТипЗнч(Сообщение) = Тип("Строка"), Сообщение, СтрокаВывода); + +КонецФункции + +Функция ЗначениеСвойства(Запись, Знач ИмяСвойства, Знач Умолчание) + + Значение = Умолчание; + Запись.Свойство(ИмяСвойства, Значение); + + Возврат Значение; + +КонецФункции + +&Желудь +Процедура ПриСозданииОбъекта(&Пластилин Поделка) + _Поделка = Поделка; +КонецПроцедуры diff --git "a/src/core/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\240\320\265\320\267\321\203\320\273\321\214\321\202\320\260\321\202\320\222\321\213\320\277\320\276\320\273\320\275\320\265\320\275\320\270\321\217\320\242\320\265\321\201\321\202\320\260.os" "b/src/core/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\240\320\265\320\267\321\203\320\273\321\214\321\202\320\260\321\202\320\222\321\213\320\277\320\276\320\273\320\275\320\265\320\275\320\270\321\217\320\242\320\265\321\201\321\202\320\260.os" index fdd070e..fc1f9ca 100644 --- "a/src/core/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\240\320\265\320\267\321\203\320\273\321\214\321\202\320\260\321\202\320\222\321\213\320\277\320\276\320\273\320\275\320\265\320\275\320\270\321\217\320\242\320\265\321\201\321\202\320\260.os" +++ "b/src/core/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\240\320\265\320\267\321\203\320\273\321\214\321\202\320\260\321\202\320\222\321\213\320\277\320\276\320\273\320\275\320\265\320\275\320\270\321\217\320\242\320\265\321\201\321\202\320\260.os" @@ -4,6 +4,7 @@ Перем _Причины; Перем _Вывод; Перем _ОписанияПричин; +Перем _ПеренесенноеВремяВыполнения; // Число - Время, измеренное изолированной средой, Неопределено - не переносилось Функция Состояние() Экспорт Возврат _Состояние; @@ -21,6 +22,32 @@ Возврат _Вывод; КонецФункции +// Время выполнения, измеренное средой, где тест выполнялся на самом деле. +// Результат изолированного теста публикуется родителю, когда тест уже +// отработал, и замер родителя показал бы время доставки, а не выполнения +// +// Возвращаемое значение: +// Число - Время выполнения в миллисекундах +// Неопределено - время измеряет статистика текущего прогона +// +Функция ПеренесенноеВремяВыполнения() Экспорт + Возврат _ПеренесенноеВремяВыполнения; +КонецФункции + +// Переносит в результат время выполнения, измеренное изолированной средой. +// Вызывается до публикации результата +// +// Параметры: +// Миллисекунды - Число - Время выполнения в миллисекундах +// +// Возвращаемое значение: +// РезультатВыполненияТеста - Этот результат +// +Функция ПеренестиВремяВыполнения(Знач Миллисекунды) Экспорт + _ПеренесенноеВремяВыполнения = Миллисекунды; + Возврат ЭтотОбъект; +КонецФункции + Процедура ОбработкаПолученияПредставления(Представление, СтандартнаяОбработка) // BSLLS:UnusedLocalMethod-off СтандартнаяОбработка = Ложь; Представление = СтрШаблон("%1 %2", Состояние(), ОписанияПричин()); diff --git "a/src/core/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\240\320\265\320\277\320\276\321\200\321\202\320\265\321\200JSON.os" "b/src/core/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\240\320\265\320\277\320\276\321\200\321\202\320\265\321\200JSON.os" index ffa4466..7603b47 100644 --- "a/src/core/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\240\320\265\320\277\320\276\321\200\321\202\320\265\321\200JSON.os" +++ "b/src/core/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\240\320\265\320\277\320\276\321\200\321\202\320\265\321\200JSON.os" @@ -9,6 +9,7 @@ Перем _ПутьКОтчету; // Строка - Путь к формируемому файлу отчета Перем _РепортерСтатистика; // РепортерСтатистика - Статистика текущего прогона +Перем _ПереносРезультатов; // ПереносРезультатов - Перевод результатов в записи отчета &ПодпискаНаСобытие("ИсполнениеТестПланКонец") Процедура ИсполнениеТестПланКонец(ТестПлан) Экспорт @@ -106,21 +107,9 @@ _РепортерСтатистика.СтатистикаТеста(Тест).ПолучитьИлиУмолчание("ВремяВыполнения", 0) ); - Если Результат.Состояние() = СостоянияВыполненияТестов.Ошибка - Или Результат.Состояние() = СостоянияВыполненияТестов.Сломан Тогда - - // Подробности со стеками есть только у проблем - - // причины пропусков и прерываний описываются строками - Запись.Вставить("message", ОписанияПричин(Результат)); - Запись.Вставить("details", ПодробныеПричины(Результат)); - - ИначеЕсли Результат.Состояние() <> СостоянияВыполненияТестов.Успех Тогда - Запись.Вставить("message", ОписанияПричин(Результат)); - КонецЕсли; // BSLLS:IfElseIfEndsWithElse-off - - Если Не ПустаяСтрока(Результат.Вывод()) Тогда - Запись.Вставить("output", Результат.Вывод()); - КонецЕсли; + Для каждого Поле Из _ПереносРезультатов.ЗаписьРезультата(Результат) Цикл + Запись.Вставить(Поле.Ключ, Поле.Значение); + КонецЦикла; Возврат Запись; @@ -148,8 +137,8 @@ Проблема = ЗаписьОтчета(ИмяНабора, ИмяТеста, Результат); - Проблема.Вставить("message", ОписанияПричин(Результат)); - Проблема.Вставить("details", ПодробныеПричины(Результат)); + Проблема.Вставить("message", _ПереносРезультатов.ОписанияПричин(Результат)); + Проблема.Вставить("details", _ПереносРезультатов.ПодробныеПричины(Результат)); Возврат Проблема; @@ -159,7 +148,7 @@ Пропуск = ЗаписьОтчета(ИмяНабора, ИмяТеста, Результат); - Пропуск.Вставить("reason", ОписанияПричин(Результат)); + Пропуск.Вставить("reason", _ПереносРезультатов.ОписанияПричин(Результат)); Возврат Пропуск; @@ -169,7 +158,7 @@ Запись = Новый Структура(); - Запись.Вставить("status", СтатусСостояния(Результат.Состояние())); + Запись.Вставить("status", _ПереносРезультатов.СтатусСостояния(Результат.Состояние())); Запись.Вставить("suite", ИмяНабора); Если ИмяТеста <> Неопределено Тогда @@ -180,39 +169,14 @@ КонецФункции -Функция ОписанияПричин(Результат) - - Возврат СтрСоединить(Результат.ОписанияПричин().ВМассив(), Символы.ПС); - -КонецФункции - -Функция ПодробныеПричины(Результат) - - Возврат Результат.Причины() - .ПроцессорКоллекции() - .Обработать("Причина -> ТестированиеСлужебный.ПодробноеОписаниеОшибки(""Причина"", Причина)") - .ВСтроку(Символы.ПС); - -КонецФункции - -Функция СтатусСостояния(Знач Состояние) - - Если Состояние = СостоянияВыполненияТестов.Ошибка Тогда - Возврат "failed"; - ИначеЕсли Состояние = СостоянияВыполненияТестов.Сломан Тогда - Возврат "broken"; - ИначеЕсли Состояние = СостоянияВыполненияТестов.Пропущен Тогда - Возврат "skipped"; - ИначеЕсли Состояние = СостоянияВыполненияТестов.Прерван Тогда - Возврат "aborted"; - Иначе - Возврат "passed"; - КонецЕсли; - -КонецФункции - &Желудь -Процедура ПриСозданииОбъекта(&Блестяшка ПутьКОтчетуJSON, &Пластилин РепортерСтатистика) +Процедура ПриСозданииОбъекта( + &Блестяшка ПутьКОтчетуJSON, + &Пластилин РепортерСтатистика, + &Пластилин ПереносРезультатов) + _ПутьКОтчету = ПутьКОтчетуJSON; _РепортерСтатистика = РепортерСтатистика; + _ПереносРезультатов = ПереносРезультатов; + КонецПроцедуры diff --git "a/src/core/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\240\320\265\320\277\320\276\321\200\321\202\320\265\321\200\320\237\320\273\320\260\320\275JSON.os" "b/src/core/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\240\320\265\320\277\320\276\321\200\321\202\320\265\321\200\320\237\320\273\320\260\320\275JSON.os" index f22c9e4..37c3fe5 100644 --- "a/src/core/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\240\320\265\320\277\320\276\321\200\321\202\320\265\321\200\320\237\320\273\320\260\320\275JSON.os" +++ "b/src/core/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\240\320\265\320\277\320\276\321\200\321\202\320\265\321\200\320\237\320\273\320\260\320\275JSON.os" @@ -10,6 +10,29 @@ // Процедура ЗаписатьПлан(ТестПлан, Знач Путь) Экспорт + ЗаписьJSON = Новый ЗаписьJSON(); + ЗаписьJSON.ОткрытьФайл(Путь); + ЗаписатьJSON(ЗаписьJSON, Новый Структура("suites", ЗаписиНаборов(ТестПлан))); + ЗаписьJSON.Закрыть(); + +КонецПроцедуры + +// Возвращает состав тест-плана записями наборов: тот же состав, что уходит +// в файл плана, передается и трансляцией событий изолированного прогона +// +// Параметры: +// ТестПлан - ТестПлан - Составленный тест-план +// +// Возвращаемое значение: +// Массив из Структура - Записи наборов: +// * suite - Строка - Имя набора +// * tests - Массив из Структура - Записи тестов набора: +// ** test - Строка - Имя теста +// ** method - Строка - Имя тестового метода +// ** children - Массив из Структура - Записи итераций, только у контейнера +// +Функция ЗаписиНаборов(ТестПлан) Экспорт + Наборы = Новый Массив; Для каждого Набор Из ТестПлан.Дети() Цикл @@ -28,12 +51,9 @@ КонецЦикла; - ЗаписьJSON = Новый ЗаписьJSON(); - ЗаписьJSON.ОткрытьФайл(Путь); - ЗаписатьJSON(ЗаписьJSON, Новый Структура("suites", Наборы)); - ЗаписьJSON.Закрыть(); + Возврат Наборы; -КонецПроцедуры +КонецФункции Функция ЗаписьУзла(Узел) diff --git "a/src/core/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\240\320\265\320\277\320\276\321\200\321\202\320\265\321\200\320\241\321\202\320\260\321\202\320\270\321\201\321\202\320\270\320\272\320\260.os" "b/src/core/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\240\320\265\320\277\320\276\321\200\321\202\320\265\321\200\320\241\321\202\320\260\321\202\320\270\321\201\321\202\320\270\320\272\320\260.os" index 4915617..d0a58ee 100644 --- "a/src/core/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\240\320\265\320\277\320\276\321\200\321\202\320\265\321\200\320\241\321\202\320\260\321\202\320\270\321\201\321\202\320\270\320\272\320\260.os" +++ "b/src/core/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\240\320\265\320\277\320\276\321\200\321\202\320\265\321\200\320\241\321\202\320\260\321\202\320\270\321\201\321\202\320\270\320\272\320\260.os" @@ -157,6 +157,12 @@ МестноеВремя('00010101' + (ТекущаяУниверсальнаяДатаВМиллисекундах() / 1000)) ); + Если Результат.ПеренесенноеВремяВыполнения() <> Неопределено Тогда + // Тест выполнялся в изолированной среде, сюда доставлен только его итог: + // начало отсчитывается от завершения на измеренное там время + ПеренестиВремяВыполнения(СтатистикаТеста(Тест), Результат.ПеренесенноеВремяВыполнения()); + КонецЕсли; + СтатистикаТеста(Тест).Вставить( "ВремяВыполнения", СтатистикаТеста(Тест).ПолучитьИлиУмолчание("ВремяЗавершения", 0) @@ -192,6 +198,15 @@ #Область СлужебныеПроцедурыИФункции +Процедура ПеренестиВремяВыполнения(Статистика, Знач Миллисекунды) + + ВремяНачала = Статистика.ПолучитьИлиУмолчание("ВремяЗавершения", 0) - Миллисекунды; + + Статистика.Вставить("ВремяНачала", ВремяНачала); + Статистика.Вставить("ДатаНачала", МестноеВремя('00010101' + (ВремяНачала / 1000))); + +КонецПроцедуры + Процедура ВывестиОшибки() Ошибки = Результаты.КлючиИЗначения() diff --git "a/src/core/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\240\320\265\321\202\321\200\320\260\320\275\321\201\320\273\321\217\321\202\320\276\321\200\320\241\320\276\320\261\321\213\321\202\320\270\320\271\320\230\320\267\320\276\320\273\321\217\321\206\320\270\320\270.os" "b/src/core/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\240\320\265\321\202\321\200\320\260\320\275\321\201\320\273\321\217\321\202\320\276\321\200\320\241\320\276\320\261\321\213\321\202\320\270\320\271\320\230\320\267\320\276\320\273\321\217\321\206\320\270\320\270.os" new file mode 100644 index 0000000..0b3646f --- /dev/null +++ "b/src/core/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\240\320\265\321\202\321\200\320\260\320\275\321\201\320\273\321\217\321\202\320\276\321\200\320\241\320\276\320\261\321\213\321\202\320\270\320\271\320\230\320\267\320\276\320\273\321\217\321\206\320\270\320\270.os" @@ -0,0 +1,465 @@ +// Ретранслирует в текущий прогон события изолированного набора по мере того, как +// их присылает изолированная среда (см. ТрансляторСобытийИзоляции): строит зеркала +// по составу набора до первого теста и публикует для них события исполнения. +// Когда среда завершилась, закрывает то, что она не успела завершить, и отдает +// результат набора. +// +// На уровне "Процесс" принимает вывод дочернего процесса как лог вывода команды +// 1commands (методы Информация и Ошибка): строки-сообщения канала ретранслируются, +// остальное копится как вывод прогона для причин отказов. + +#Использовать collectionos +#Использовать "../../../shared" + +Перем _ОпределениеНабора; // ОпределениеТестНабора - Плейсхолдер изолированного набора +Перем _Поделка; // Поделка - Поделка текущего прогона +Перем _ПубликаторСобытий; // ПубликаторСобытий - Публикатор событий текущего прогона +Перем _ПереносРезультатов; // ПереносРезультатов - Перевод результатов из записей +Перем _Канал; // КаналСобытийИзоляции - Разбор строк-сообщений вывода процесса + +Перем _СоставПолучен; // Булево - Изолированная среда прислала состав плана +Перем _ИндексНабора; // Число - Индекс набора в плане среды, Неопределено - набор не обнаружен +Перем _Зеркала; // Соответствие из КлючИЗначение - Зеркала по путям: + // * Ключ - Строка - Путь узла от набора, индексы через точку + // * Значение - ОпределениеТеста - Зеркало +Перем _ПорядокЗеркал; // Массив из ОпределениеТеста - Зеркала в порядке обхода состава +Перем _Начатые; // Соответствие из КлючИЗначение - Зеркала, начало которых опубликовано: + // * Ключ - ОпределениеТеста - Зеркало + // * Значение - Булево - Истина +Перем _Завершенные; // Соответствие из КлючИЗначение - Зеркала, конец которых опубликован: + // * Ключ - ОпределениеТеста - Зеркало + // * Значение - Булево - Истина +Перем _РезультатНабора; // РезультатВыполненияТеста - Результат набора в среде, Неопределено - не пришел +Перем _ОжидаемыйНомер; // Число - Номер следующего сообщения трансляции +Перем _СообщенияПотеряны; // Булево - Нумерация сообщений прерывалась: часть событий не дошла +Перем _Вывод; // Массив из Строка - Вывод дочернего процесса помимо сообщений +Перем _Хвост; // Строка - Недочитанная строка вывода дочернего процесса +Перем _ПотокПерехвата; // ПотокВПамяти - Перехват вывода изолированной поделки, Неопределено - без перехвата + +// Принимает сообщение трансляции изолированной среды +// +// Параметры: +// Сообщение - Структура - Сообщение трансляции, см. ТрансляторСобытийИзоляции +// +Процедура Принять(Сообщение) Экспорт + + // Разрыв нумерации - потерянное сообщение, например строка канала, склеенная + // с посторонним выводом: без него набор нельзя считать целым + Если Сообщение.seq <> _ОжидаемыйНомер Тогда + _СообщенияПотеряны = Истина; + КонецЕсли; + + _ОжидаемыйНомер = Сообщение.seq + 1; + + Если Сообщение.event = "plan" Тогда + ВклеитьСостав(Сообщение.suites); + Возврат; + КонецЕсли; + + Если _ИндексНабора = Неопределено Или Сообщение.path[0] <> _ИндексНабора Тогда + Возврат; + КонецЕсли; + + Если Сообщение.event = "suiteEnd" Тогда + _РезультатНабора = _ПереносРезультатов.РезультатЗаписи(Сообщение.result); + Возврат; + КонецЕсли; + + Зеркало = _Зеркала.Получить(КлючПути(Сообщение.path)); + + Если Зеркало = Неопределено Тогда + Возврат; + КонецЕсли; + + Если Сообщение.event = "testStart" Тогда + + Начать(Зеркало); + + ИначеЕсли Сообщение.event = "testEnd" Тогда + + // Начало могло потеряться: репортерам нужна пара событий + Начать(Зеркало); + + _Завершенные.Вставить(Зеркало, Истина); + Опубликовать( + Зеркало, + "ИсполнениеТестКонец", + Массивы.ИзЭлементов(_ПереносРезультатов.РезультатЗаписи(Сообщение.result)) + ); + + КонецЕсли; // BSLLS:IfElseIfEndsWithElse-off + +КонецПроцедуры + +// Строит зеркала изолированного набора по составу плана его среды. Зеркала +// строятся все сразу, до событий их исполнения: репортеры определяют положение +// узла среди соседей по составу родителя +// +// Параметры: +// ЗаписиНаборов - Массив из Структура - Записи наборов, см. РепортерПланJSON.ЗаписиНаборов +// +Процедура ВклеитьСостав(ЗаписиНаборов) Экспорт + + _СоставПолучен = Истина; + + Для ИндексНабора = 0 По ЗаписиНаборов.ВГраница() Цикл + + Если ЗаписиНаборов[ИндексНабора].suite <> _ОпределениеНабора.Имя() Тогда + Продолжить; + КонецЕсли; + + _ИндексНабора = ИндексНабора; + + ЗаписиТестов = ЗаписиНаборов[ИндексНабора].tests; + + Для Индекс = 0 По ЗаписиТестов.ВГраница() Цикл + _ОпределениеНабора.ДобавитьТест( + ПостроитьЗеркало(ЗаписиТестов[Индекс], _ОпределениеНабора, XMLСтрока(Индекс), Индекс) + ); + КонецЦикла; + + Прервать; + + КонецЦикла; + +КонецПроцедуры + +// Принимает очередную порцию стандартного вывода дочернего процесса - +// интерфейс лога вывода команды 1commands +// +// Параметры: +// Текст - Строка - Порция вывода, строки в ней могут быть оборваны +// +Процедура Информация(Знач Текст) Экспорт + + Строки = СтрРазделить(_Хвост + Текст, Символы.ПС); + + _Хвост = Строки[Строки.ВГраница()]; + Строки.Удалить(Строки.ВГраница()); + + Для каждого СтрокаВывода Из Строки Цикл + ПринятьСтроку(СтрокаВывода); + КонецЦикла; + +КонецПроцедуры + +// Принимает очередную порцию вывода ошибок дочернего процесса - +// интерфейс лога вывода команды 1commands +// +// Параметры: +// Текст - Строка - Порция вывода ошибок +// +Процедура Ошибка(Знач Текст) Экспорт + _Вывод.Добавить(Текст); +КонецПроцедуры + +// Включает публикацию событий мимо перехвата вывода изолированной поделки: +// события приходят изнутри ее исполнения, а вывод текущего прогона (дерево тестов) +// не должен попасть в перехват +// +// Параметры: +// Поток - ПотокВПамяти - Поток перехвата вывода изолированной поделки, +// Неопределено - перехват завершен +// +Процедура ПубликоватьМимоПерехвата(Поток) Экспорт + _ПотокПерехвата = Поток; +КонецПроцедуры + +// Закрывает то, что изолированная среда не успела завершить, и возвращает +// результат набора +// +// Параметры: +// ВыводСреды - Строка - Захваченный вывод изолированной поделки, +// вывод дочернего процесса ретранслятор накопил сам +// ОшибкаСреды - ИнформацияОбОшибке - Отказ, не давший среде отработать, +// Неопределено - среда отработала +// +// Возвращаемое значение: +// РезультатВыполненияТеста - Результат изолированного набора +// +Функция Завершить(Знач ВыводСреды, ОшибкаСреды = Неопределено) Экспорт + + Если _Хвост <> "" Тогда + ПринятьСтроку(_Хвост); + _Хвост = ""; + КонецЕсли; + + Вывод = СтрСоединить(_Вывод, Символы.ПС) + ВыводСреды; + + ЗакрытьНезавершенные(Вывод); + + Если ОшибкаСреды <> Неопределено Тогда + // Сдерживание отказов: среда не поднялась или упало ее обнаружение - + // набор сломан, но общий прогон продолжается + Возврат РезультатСломан(ОшибкаСреды); + КонецЕсли; + + Если _СообщенияПотеряны Тогда + Возврат РезультатСломан(_ПереносРезультатов.ПричинаСВыводом( + "Трансляция событий изолированного набора потеряла сообщения: результаты части тестов не дошли", + Вывод + )); + КонецЕсли; + + Если Не _СоставПолучен Тогда + Возврат РезультатСломан(_ПереносРезультатов.ПричинаСВыводом( + "Дочерний прогон изолированного набора завершился аварийно, не начав исполнение", + Вывод + )); + КонецЕсли; + + Если _ИндексНабора = Неопределено Тогда + // Родитель набор обнаружил, а изолированная среда - нет: она собралась + // не так, как ожидалось (например, совет подменил детальки прогона) + Возврат РезультатСломан(_ПереносРезультатов.ПричинаСВыводом( + СтрШаблон("Изолированный набор %1 не был обнаружен в дочернем прогоне", _ОпределениеНабора.Имя()), + Вывод + )); + КонецЕсли; + + Если _РезультатНабора = Неопределено Тогда + Возврат РезультатСломан(_ПереносРезультатов.ПричинаСВыводом( + "Дочерний прогон изолированного набора завершился аварийно, не завершив набор", + Вывод + )); + КонецЕсли; + + Возврат _РезультатНабора; + +КонецФункции + +Процедура ПринятьСтроку(Знач СтрокаВывода) + + Попытка + Сообщение = _Канал.Разобрать(СтрокаВывода); + Исключение + // Строка с маркером, но не сообщение: остается выводом, а потерю + // сообщения покажет разрыв нумерации + Сообщение = Неопределено; + КонецПопытки; + + Если Сообщение = Неопределено Тогда + _Вывод.Добавить(СтрЗаменить(СтрЗаменить(СтрокаВывода, Символ(65279), ""), Символы.ВК, "")); + Иначе + Принять(Сообщение); + КонецЕсли; + +КонецПроцедуры + +// Закрывает начатые, но не завершенные зеркала: среда завершилась посреди +// исполнения. Итерации закрываются раньше своих контейнеров. Зеркала, до которых +// среда не дошла, остаются без результата, как тесты набора, упавшего до них +// +// Параметры: +// Вывод - Строка - Вывод изолированной среды для причин +// +Процедура ЗакрытьНезавершенные(Знач Вывод) + + Для каждого Зеркало Из _ПорядокЗеркал Цикл + + Если Зеркало.ТипОпределения() = ТипыОпределенийТестов.Контейнер + Или Не НачатоНоНеЗавершено(Зеркало) Тогда + Продолжить; + КонецЕсли; + + Причина = _ПереносРезультатов.ПричинаСВыводом( + СтрШаблон( + "Тест %1 не был выполнен в изолированной среде: среда завершилась посреди теста", + Зеркало.Имя() + ), + Вывод + ); + + _Завершенные.Вставить(Зеркало, Истина); + + Опубликовать( + Зеркало, + "ИсполнениеТестКонец", + Массивы.ИзЭлементов(РезультатСломан(Причина)) + ); + + КонецЦикла; + + // Контейнеры - в обратном порядке обхода, после всех своих итераций + Для Индекс = 0 По _ПорядокЗеркал.ВГраница() Цикл + + Зеркало = _ПорядокЗеркал[_ПорядокЗеркал.ВГраница() - Индекс]; + + Если Зеркало.ТипОпределения() <> ТипыОпределенийТестов.Контейнер + Или Не НачатоНоНеЗавершено(Зеркало) Тогда + Продолжить; + КонецЕсли; + + _Завершенные.Вставить(Зеркало, Истина); + + Опубликовать( + Зеркало, + "ИсполнениеТестКонец", + Массивы.ИзЭлементов(Результат(СостоянияВыполненияТестов.Успех, Списки.ИзЭлементов())) + ); + + КонецЦикла; + +КонецПроцедуры + +// Публикует начало зеркала, если оно еще не опубликовано, - вместе с началом +// его контейнера: события по зеркалу приходят парой и вложенными, даже если +// сообщение о начале потерялось по дороге +// +// Параметры: +// Зеркало - ОпределениеТеста - Зеркало теста, итерации или контейнера +// +Процедура Начать(Зеркало) + + Если _Начатые.Получить(Зеркало) = Истина Тогда + Возврат; + КонецЕсли; + + Если ТипЗнч(Зеркало.Родитель()) = Тип("ОпределениеТеста") Тогда + Начать(Зеркало.Родитель()); + КонецЕсли; + + _Начатые.Вставить(Зеркало, Истина); + Опубликовать(Зеркало, "ИсполнениеТестНачало", Новый Массив); + +КонецПроцедуры + +Функция НачатоНоНеЗавершено(Зеркало) + Возврат _Начатые.Получить(Зеркало) = Истина И _Завершенные.Получить(Зеркало) <> Истина; +КонецФункции + +// Строит зеркало узла по записи состава и регистрирует его по пути +// +// Параметры: +// Запись - Структура - Запись теста состава, см. РепортерПланJSON.ЗаписиНаборов +// Родитель - ОпределениеТестНабора, ОпределениеТеста - Родитель зеркала +// Ключ - Строка - Путь узла от набора, индексы через точку +// Порядок - Число - Порядок узла среди соседей: равен его индексу, чтобы +// сортировка набора сохранила порядок изолированной среды +// +// Возвращаемое значение: +// ОпределениеТеста - Зеркало +// +Функция ПостроитьЗеркало(Запись, Родитель, Знач Ключ, Знач Порядок) + + Дети = Новый СписокМассив; + + ЭтоКонтейнер = Запись.Свойство("children"); + + Зеркало = _Поделка.НайтиЖелудь( + "ОпределениеТеста", + Массивы.ИзЭлементов( + ?(ЭтоКонтейнер, ТипыОпределенийТестов.Контейнер, ТипыОпределенийТестов.Тест), + Запись.test, + Дети, + Родитель, + ЗначениеСвойства(Запись, "method", Запись.test), + Новый СписокМассив, + 0, + Новый СписокМассив, + Новый СписокМассив, + Порядок, + Неопределено + ) + ); + + _Зеркала.Вставить(Ключ, Зеркало); + _ПорядокЗеркал.Добавить(Зеркало); + + Если ЭтоКонтейнер Тогда + Для Индекс = 0 По Запись.children.ВГраница() Цикл + Дети.Добавить(ПостроитьЗеркало( + Запись.children[Индекс], + Зеркало, + Ключ + "." + XMLСтрока(Индекс), + Индекс + )); + КонецЦикла; + КонецЕсли; + + Возврат Зеркало; + +КонецФункции + +Функция КлючПути(Путь) + + Индексы = Новый Массив; + + // Первый индекс - набор в плане среды, ключи зеркал отсчитываются от набора + Для Позиция = 1 По Путь.ВГраница() Цикл + Индексы.Добавить(XMLСтрока(Путь[Позиция])); + КонецЦикла; + + Возврат СтрСоединить(Индексы, "."); + +КонецФункции + +Процедура Опубликовать(Источник, Знач ИмяСобытия, ПараметрыСобытия) + + Если _ПотокПерехвата = Неопределено Тогда + _ПубликаторСобытий.ОпубликоватьСобытие(Источник, ИмяСобытия, ПараметрыСобытия); + Возврат; + КонецЕсли; + + // Перехваты вывода ведутся стеком: событие приходит между тестами изолированной + // поделки, когда на вершине стека ее перехват, и снимается ровно он + ТестированиеСлужебный.ЗавершитьПерехватВывода(); + + Попытка + _ПубликаторСобытий.ОпубликоватьСобытие(Источник, ИмяСобытия, ПараметрыСобытия); + Исключение + ТестированиеСлужебный.НачатьПерехватВывода(_ПотокПерехвата); + ВызватьИсключение; + КонецПопытки; + + ТестированиеСлужебный.НачатьПерехватВывода(_ПотокПерехвата); + +КонецПроцедуры + +Функция РезультатСломан(Причина) + Возврат Результат(СостоянияВыполненияТестов.Сломан, Списки.ИзЭлементов(Причина)); +КонецФункции + +Функция Результат(Знач Состояние, Причины) + Возврат _Поделка.НайтиЖелудь( + "РезультатВыполненияТеста", + Массивы.ИзЭлементов(Состояние, Причины, "") + ); +КонецФункции + +Функция ЗначениеСвойства(Запись, Знач ИмяСвойства, Знач Умолчание) + + Значение = Умолчание; + Запись.Свойство(ИмяСвойства, Значение); + + Возврат Значение; + +КонецФункции + +&Желудь +&Характер("Компанейский") +Процедура ПриСозданииОбъекта( + &Блестяшка ОпределениеНабора, + &Пластилин Поделка, + &Пластилин ПубликаторСобытий, + &Пластилин ПереносРезультатов, + &Пластилин КаналСобытийИзоляции) + + _ОпределениеНабора = ОпределениеНабора; + _Поделка = Поделка; + _ПубликаторСобытий = ПубликаторСобытий; + _ПереносРезультатов = ПереносРезультатов; + _Канал = КаналСобытийИзоляции; + + _СоставПолучен = Ложь; + _Зеркала = Новый Соответствие; + _ПорядокЗеркал = Новый Массив; + _Начатые = Новый Соответствие; + _Завершенные = Новый Соответствие; + _Вывод = Новый Массив; + _Хвост = ""; + + _ОжидаемыйНомер = 1; + _СообщенияПотеряны = Ложь; + +КонецПроцедуры diff --git "a/src/core/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\242\321\200\320\260\320\275\321\201\320\273\321\217\321\202\320\276\321\200\320\241\320\276\320\261\321\213\321\202\320\270\320\271\320\230\320\267\320\276\320\273\321\217\321\206\320\270\320\270.os" "b/src/core/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\242\321\200\320\260\320\275\321\201\320\273\321\217\321\202\320\276\321\200\320\241\320\276\320\261\321\213\321\202\320\270\320\271\320\230\320\267\320\276\320\273\321\217\321\206\320\270\320\270.os" new file mode 100644 index 0000000..346ec08 --- /dev/null +++ "b/src/core/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\242\321\200\320\260\320\275\321\201\320\273\321\217\321\202\320\276\321\200\320\241\320\276\320\261\321\213\321\202\320\270\320\271\320\230\320\267\320\276\320\273\321\217\321\206\320\270\320\270.os" @@ -0,0 +1,130 @@ +// Транслирует события исполнения изолированного прогона родителю по ходу +// исполнения, а не после него. Сообщения - структуры: +// * event - Строка - Вид сообщения: +// "plan" - состав плана на старте исполнения, поле suites - записи наборов, +// см. РепортерПланJSON.ЗаписиНаборов; +// "testStart" - начало теста или контейнера, поле path; +// "testEnd" - конец теста или контейнера, поля path и result; +// "suiteEnd" - конец набора, поля path и result. +// * seq - Число - Номер сообщения, сквозной с 1: по разрыву нумерации родитель +// узнает о потерянном сообщении, а не принимает набор без него за целый +// * path - Массив из Число - Путь узла индексами от плана: набор, тест, итерация +// * result - Структура - Запись результата с полем duration_ms, см. ПереносРезультатов +// +// Родитель строит зеркала по составу до первого теста и находит их по тем же путям. +// Желудь создается родителем с приемником, как рогатка создает репортеры: без него +// транслятора в поделке нет. + +Перем _Приемник; // Произвольный - Приемник сообщений, реализует Принять(Сообщение) +Перем _ПереносРезультатов; // ПереносРезультатов - Перевод результатов в записи +Перем _РепортерПланJSON; // РепортерПланJSON - Записи состава плана +Перем _ВремяНачала; // Соответствие из КлючИЗначение - Время начала узлов: + // * Ключ - ОпределениеТестНабора, ОпределениеТеста - Узел плана + // * Значение - Число - Время начала в миллисекундах +Перем _НомерСообщения; // Число - Номер последнего отправленного сообщения + +&ПодпискаНаСобытие("ИсполнениеТестПланНачало") +Процедура ИсполнениеТестПланНачало(ТестПлан) Экспорт + + Отправить(Новый Структура( + "event, suites", + "plan", + _РепортерПланJSON.ЗаписиНаборов(ТестПлан) + )); + +КонецПроцедуры + +&ПодпискаНаСобытие("ИсполнениеТестНаборНачало") +Процедура ИсполнениеТестНаборНачало(ТестНабор) Экспорт + _ВремяНачала.Вставить(ТестНабор, ТекущаяУниверсальнаяДатаВМиллисекундах()); +КонецПроцедуры + +&ПодпискаНаСобытие("ИсполнениеТестНаборКонец") +Процедура ИсполнениеТестНаборКонец(ТестНабор, Результат) Экспорт + + Отправить(Новый Структура( + "event, path, result", + "suiteEnd", + Путь(ТестНабор), + ЗаписьРезультата(ТестНабор, Результат) + )); + +КонецПроцедуры + +&ПодпискаНаСобытие("ИсполнениеТестНачало") +Процедура ИсполнениеТестНачало(Тест) Экспорт + + _ВремяНачала.Вставить(Тест, ТекущаяУниверсальнаяДатаВМиллисекундах()); + + Отправить(Новый Структура( + "event, path", + "testStart", + Путь(Тест) + )); + +КонецПроцедуры + +&ПодпискаНаСобытие("ИсполнениеТестКонец") +Процедура ИсполнениеТестКонец(Тест, Результат) Экспорт + + Отправить(Новый Структура( + "event, path, result", + "testEnd", + Путь(Тест), + ЗаписьРезультата(Тест, Результат) + )); + +КонецПроцедуры + +Процедура Отправить(Сообщение) + + _НомерСообщения = _НомерСообщения + 1; + Сообщение.Вставить("seq", _НомерСообщения); + + _Приемник.Принять(Сообщение); + +КонецПроцедуры + +Функция ЗаписьРезультата(Узел, Результат) + + Запись = _ПереносРезультатов.ЗаписьРезультата(Результат); + + // Замер свой, а не статистики: порядок подписчиков одного события не задан, + // и статистика могла еще не обработать конец узла + ВремяНачала = _ВремяНачала.Получить(Узел); + + Если ВремяНачала <> Неопределено Тогда + Запись.Вставить("duration_ms", ТекущаяУниверсальнаяДатаВМиллисекундах() - ВремяНачала); + _ВремяНачала.Удалить(Узел); + КонецЕсли; + + Возврат Запись; + +КонецФункции + +Функция Путь(Знач Узел) + + Путь = Новый Массив; + + Пока ТипЗнч(Узел) <> Тип("ТестПлан") Цикл + Путь.Вставить(0, Узел.Родитель().Дети().Индекс(Узел)); + Узел = Узел.Родитель(); + КонецЦикла; + + Возврат Путь; + +КонецФункции + +&Желудь +Процедура ПриСозданииОбъекта( + &Блестяшка Приемник, + &Пластилин ПереносРезультатов, + &Пластилин РепортерПланJSON) + + _Приемник = Приемник; + _ПереносРезультатов = ПереносРезультатов; + _РепортерПланJSON = РепортерПланJSON; + _ВремяНачала = Новый Соответствие; + _НомерСообщения = 0; + +КонецПроцедуры diff --git "a/src/shared/\320\232\320\273\320\260\321\201\321\201\321\213/\320\227\320\260\320\277\321\203\321\201\320\272\320\260\321\202\320\265\320\273\321\214\320\242\320\265\321\201\321\202\320\270\321\200\320\276\320\262\320\260\320\275\320\270\321\217.os" "b/src/shared/\320\232\320\273\320\260\321\201\321\201\321\213/\320\227\320\260\320\277\321\203\321\201\320\272\320\260\321\202\320\265\320\273\321\214\320\242\320\265\321\201\321\202\320\270\321\200\320\276\320\262\320\260\320\275\320\270\321\217.os" index 06962c6..3d4240a 100644 --- "a/src/shared/\320\232\320\273\320\260\321\201\321\201\321\213/\320\227\320\260\320\277\321\203\321\201\320\272\320\260\321\202\320\265\320\273\321\214\320\242\320\265\321\201\321\202\320\270\321\200\320\276\320\262\320\260\320\275\320\270\321\217.os" +++ "b/src/shared/\320\232\320\273\320\260\321\201\321\201\321\213/\320\227\320\260\320\277\321\203\321\201\320\272\320\260\321\202\320\265\320\273\321\214\320\242\320\265\321\201\321\202\320\270\321\200\320\276\320\262\320\260\320\275\320\270\321\217.os" @@ -15,7 +15,9 @@ // Возвращаемое значение: // Структура - Параметры запуска: // * РабочийКаталог - Строка - Каталог проекта, пустое значение - текущий каталог -// * ЛогВывода - Лог - Лог трансляции вывода прогона, Неопределено - не транслировать +// * ЛогВывода - Лог, Произвольный - Лог трансляции вывода прогона или объект с методами +// Информация(Текст) и Ошибка(Текст), которому 1commands отдает вывод процесса +// порциями по мере чтения, Неопределено - не транслировать // * ПортОтладки - Число - Порт сервера отладки прогона, 0 - без отладки // * ПутьКОтчетуGenericCoverage - Строка - Путь к формируемому отчету покрытия GenericCoverage // * ПутьКОтчетуCobertura - Строка - Путь к формируемому отчету покрытия Cobertura @@ -28,6 +30,9 @@ // исполняется в прогоне с захватом экземпляра набора, поднятого // напрямую через Новый, пустая строка - совет по умолчанию // * РаскладкаЛога - Строка - Раскладка лога прогона, по умолчанию JSONРаскладкаСообщения +// * ТрансляцияСобытий - Булево - Транслировать события исполнения в stdout прогона +// строками канала событий изоляции (для изолированных прогонов: +// родитель ретранслирует их по ходу исполнения) // * КаталогЯдра - Строка - Каталог ядра (core чужой поставки, рядом обязан лежать // shared), которым выполняется прогон. Пустое значение - ядро, поставляемое // с текущим экземпляром OneUnit. Нужен приложениям, которые поставляют @@ -54,6 +59,7 @@ Параметры.Вставить("Импорты", Новый Массив); Параметры.Вставить("ЛямбдаСовета", ""); Параметры.Вставить("РаскладкаЛога", "JSONРаскладкаСообщения"); + Параметры.Вставить("ТрансляцияСобытий", Ложь); Параметры.Вставить("КаталогЯдра", ""); Возврат Параметры; @@ -76,7 +82,22 @@ // Функция ВыполнитьТестирование(Детальки, Лог, Знач Параметры = Неопределено) Экспорт - ВыполняемаяКоманда = + Если Параметры <> Неопределено + И Параметры.Свойство("ТрансляцияСобытий") + И Параметры.ТрансляцияСобытий Тогда + + Трансляция = + " Поделка.НайтиЖелудь( + | ""ТрансляторСобытийИзоляции"", + | Массивы.ИзЭлементов(Поделка.НайтиЖелудь(""КаналСобытийИзоляции"")) + | ); + |"; + + Иначе + Трансляция = ""; + КонецЕсли; + + ВыполняемаяКоманда = Трансляция + " ТестПлан = МенеджерТестирования.Тестировать(); | Статистика = Поделка.НайтиЖелудь(""РепортерСтатистика"") | .СтатистикаТестПлана(); diff --git "a/tests/fixtures/isolation/\320\230\320\267\320\276\320\273\320\270\321\200\320\276\320\262\320\260\320\275\320\275\321\213\320\271\320\235\320\260\320\261\320\276\321\200\320\241\320\237\320\260\321\203\320\267\320\276\320\271.os" "b/tests/fixtures/isolation/\320\230\320\267\320\276\320\273\320\270\321\200\320\276\320\262\320\260\320\275\320\275\321\213\320\271\320\235\320\260\320\261\320\276\321\200\320\241\320\237\320\260\321\203\320\267\320\276\320\271.os" new file mode 100644 index 0000000..50faf45 --- /dev/null +++ "b/tests/fixtures/isolation/\320\230\320\267\320\276\320\273\320\270\321\200\320\276\320\262\320\260\320\275\320\275\321\213\320\271\320\235\320\260\320\261\320\276\321\200\320\241\320\237\320\260\321\203\320\267\320\276\320\271.os" @@ -0,0 +1,12 @@ +// Изолированный на уровне поделки набор с заметно долгим тестом: время теста +// измеряется в изолированной среде и переносится в статистику родителя. + +&Тест +Процедура ТестСПаузой() Экспорт + Приостановить(300); +КонецПроцедуры + +&ТестовыйНабор +&Изолированный +Процедура ПриСозданииОбъекта() +КонецПроцедуры diff --git "a/tests/fixtures/isolation/\320\230\320\267\320\276\320\273\320\270\321\200\320\276\320\262\320\260\320\275\320\275\321\213\320\271\320\237\321\200\320\276\321\206\320\265\321\201\321\201\320\275\321\213\320\271\320\235\320\260\320\261\320\276\321\200\320\241\320\220\320\262\320\260\321\200\320\270\320\265\320\271.os" "b/tests/fixtures/isolation/\320\230\320\267\320\276\320\273\320\270\321\200\320\276\320\262\320\260\320\275\320\275\321\213\320\271\320\237\321\200\320\276\321\206\320\265\321\201\321\201\320\275\321\213\320\271\320\235\320\260\320\261\320\276\321\200\320\241\320\220\320\262\320\260\321\200\320\270\320\265\320\271.os" new file mode 100644 index 0000000..af2e196 --- /dev/null +++ "b/tests/fixtures/isolation/\320\230\320\267\320\276\320\273\320\270\321\200\320\276\320\262\320\260\320\275\320\275\321\213\320\271\320\237\321\200\320\276\321\206\320\265\321\201\321\201\320\275\321\213\320\271\320\235\320\260\320\261\320\276\321\200\320\241\320\220\320\262\320\260\321\200\320\270\320\265\320\271.os" @@ -0,0 +1,36 @@ +// Изолированный на уровне процесса набор, дочерний процесс которого умирает +// посреди исполнения: результат теста, выполненного до аварии, уже передан +// родителю, до следующих тестов дело не доходит. +// +// Процесс завершается из подписки на начало второго теста: тесты выполняются +// в фоновых заданиях, где ЗавершитьРаботу прерывает только задание, а события +// публикуются в главном потоке. + +#Использовать asserts + +&Тест +Процедура АПервыйУспевает() Экспорт + Ожидаем.Что(Истина).ЭтоИстина(); +КонецПроцедуры + +&Тест +Процедура БВторойНеУспевает() Экспорт + Ожидаем.Что(Истина).ЭтоИстина(); +КонецПроцедуры + +&Тест +Процедура ВТретийНеНачинается() Экспорт + Ожидаем.Что(Истина).ЭтоИстина(); +КонецПроцедуры + +&ПодпискаНаСобытие("ИсполнениеТестНачало") +Процедура УронитьПроцессНаВторомТесте(Тест) Экспорт + Если Тест.Имя() = "БВторойНеУспевает" Тогда + ЗавершитьРаботу(3); + КонецЕсли; +КонецПроцедуры + +&ТестовыйНабор(Характер = "Одиночка") +&Изолированный(Уровень = "Процесс") +Процедура ПриСозданииОбъекта() +КонецПроцедуры diff --git "a/tests/fixtures/isolation/\320\230\320\267\320\276\320\273\320\270\321\200\320\276\320\262\320\260\320\275\320\275\321\213\320\271\320\237\321\200\320\276\321\206\320\265\321\201\321\201\320\275\321\213\320\271\320\235\320\260\320\261\320\276\321\200\320\241\320\237\320\260\321\203\320\267\320\276\320\271.os" "b/tests/fixtures/isolation/\320\230\320\267\320\276\320\273\320\270\321\200\320\276\320\262\320\260\320\275\320\275\321\213\320\271\320\237\321\200\320\276\321\206\320\265\321\201\321\201\320\275\321\213\320\271\320\235\320\260\320\261\320\276\321\200\320\241\320\237\320\260\321\203\320\267\320\276\320\271.os" new file mode 100644 index 0000000..192a901 --- /dev/null +++ "b/tests/fixtures/isolation/\320\230\320\267\320\276\320\273\320\270\321\200\320\276\320\262\320\260\320\275\320\275\321\213\320\271\320\237\321\200\320\276\321\206\320\265\321\201\321\201\320\275\321\213\320\271\320\235\320\260\320\261\320\276\321\200\320\241\320\237\320\260\321\203\320\267\320\276\320\271.os" @@ -0,0 +1,12 @@ +// Изолированный на уровне процесса набор с заметно долгим тестом: время теста +// измеряется в дочернем процессе и переносится в статистику родителя. + +&Тест +Процедура ТестСПаузой() Экспорт + Приостановить(300); +КонецПроцедуры + +&ТестовыйНабор +&Изолированный(Уровень = "Процесс") +Процедура ПриСозданииОбъекта() +КонецПроцедуры diff --git "a/tests/fixtures/isolation/\320\230\320\267\320\276\320\273\320\270\321\200\320\276\320\262\320\260\320\275\320\275\321\213\320\271\320\237\321\200\320\276\321\206\320\265\321\201\321\201\320\275\321\213\320\271\320\235\320\260\320\261\320\276\321\200\320\241\320\237\320\276\321\200\321\207\320\265\320\271\320\232\320\260\320\275\320\260\320\273\320\260.os" "b/tests/fixtures/isolation/\320\230\320\267\320\276\320\273\320\270\321\200\320\276\320\262\320\260\320\275\320\275\321\213\320\271\320\237\321\200\320\276\321\206\320\265\321\201\321\201\320\275\321\213\320\271\320\235\320\260\320\261\320\276\321\200\320\241\320\237\320\276\321\200\321\207\320\265\320\271\320\232\320\260\320\275\320\260\320\273\320\260.os" new file mode 100644 index 0000000..7ceb42c --- /dev/null +++ "b/tests/fixtures/isolation/\320\230\320\267\320\276\320\273\320\270\321\200\320\276\320\262\320\260\320\275\320\275\321\213\320\271\320\237\321\200\320\276\321\206\320\265\321\201\321\201\320\275\321\213\320\271\320\235\320\260\320\261\320\276\321\200\320\241\320\237\320\276\321\200\321\207\320\265\320\271\320\232\320\260\320\275\320\260\320\273\320\260.os" @@ -0,0 +1,31 @@ +// Изолированный на уровне процесса набор, в stdout которого посреди прогона +// попадает посторонний вывод без перевода строки: ближайшая строка канала событий +// склеивается с ним и теряется. Родитель обязан заметить потерю по разрыву +// нумерации сообщений, а не принять набор без нее за целый. +// +// Вывод - из подписки на событие: события публикуются в главном потоке, вне +// перехвата вывода тестов, тем же потоком консоли, что и строки канала. + +#Использовать asserts + +&Тест +Процедура АПервый() Экспорт + Ожидаем.Что(Истина).ЭтоИстина(); +КонецПроцедуры + +&Тест +Процедура БВторой() Экспорт + Ожидаем.Что(Истина).ЭтоИстина(); +КонецПроцедуры + +&ПодпискаНаСобытие("ИсполнениеТестКонец") +Процедура ИспортитьКаналПослеПервогоТеста(Тест, Результат) Экспорт // BSLLS:UnusedParameters-off + Если Тест.Имя() = "АПервый" Тогда + Консоль.Вывести("посторонний вывод без перевода строки"); + КонецЕсли; +КонецПроцедуры + +&ТестовыйНабор(Характер = "Одиночка") +&Изолированный(Уровень = "Процесс") +Процедура ПриСозданииОбъекта() +КонецПроцедуры diff --git "a/tests/\320\230\320\267\320\276\320\273\321\217\321\206\320\270\321\217.os" "b/tests/\320\230\320\267\320\276\320\273\321\217\321\206\320\270\321\217.os" index 831ce22..9158cb1 100644 --- "a/tests/\320\230\320\267\320\276\320\273\321\217\321\206\320\270\321\217.os" +++ "b/tests/\320\230\320\267\320\276\320\273\321\217\321\206\320\270\321\217.os" @@ -214,6 +214,137 @@ КонецПроцедуры +&Тест +Процедура ВремяТестаИзолированногоПроцессаПереноситсяИзСреды() Экспорт + + // Время теста измеряется там, где он выполнялся: события дочернего процесса + // доходят до родителя порциями вывода, и замер родителя дал бы время доставки + ПроверитьПеренесенноеВремя("ИзолированныйПроцессныйНаборСПаузой", "ТестСПаузой"); + +КонецПроцедуры + +&Тест +Процедура ВремяТестаИзолированнойПоделкиПереноситсяИзСреды() Экспорт + + ПроверитьПеренесенноеВремя("ИзолированныйНаборСПаузой", "ТестСПаузой"); + +КонецПроцедуры + +&Тест +Процедура СтрокаКаналаСобытийНеЗависитОтКодировкиКонсоли() Экспорт + + // Кодовая страница консоли не должна искажать имена в строке канала, а BOM, + // которым консоль в UTF-8 начинает порции вывода процесса, - прятать сообщение + ЗаписьНабора = Новый Структура("suite, tests", "НаборСКириллицей", Новый Массив); + + Наборы = Новый Массив; + Наборы.Добавить(ЗаписьНабора); + + Канал = Новый КаналСобытийИзоляции(); + + Буфер = Новый ПотокВПамяти(); + ТестированиеСлужебный.НачатьПерехватВывода(Буфер); + + Попытка + Канал.Принять(Новый Структура("event, seq, suites", "plan", 1, Наборы)); + Исключение + ТестированиеСлужебный.ЗавершитьПерехватВывода(); + ВызватьИсключение; + КонецПопытки; + + ТестированиеСлужебный.ЗавершитьПерехватВывода(); + + СтрокаКанала = СокрЛП(ПрогоныТестов.ПрочитатьПоток(Буфер)); + + СимволыВнеASCII = 0; + + Для Позиция = 1 По СтрДлина(СтрокаКанала) Цикл + Если КодСимвола(СтрокаКанала, Позиция) > 127 Тогда + СимволыВнеASCII = СимволыВнеASCII + 1; + КонецЕсли; + КонецЦикла; + + Ожидаем.Что(СимволыВнеASCII, "символов вне ASCII в строке канала").Равно(0); + + Сообщение = Канал.Разобрать(Символ(65279) + СтрокаКанала); + + Ожидаем.Что(Сообщение, "сообщение за BOM").Не_().ЭтоНеопределено(); + Ожидаем.Что(Сообщение.suites[0].suite).Равно("НаборСКириллицей"); + +КонецПроцедуры + +&Тест +Процедура ПотеряСообщенияКаналаЛомаетНабор() Экспорт + + // Строка канала, склеенная с посторонним выводом, теряется: потерю выдает + // разрыв нумерации, и набор ломается, а не остается зеленым без результата + Прогон = ПрогоныТестов.Прогнать("ИзолированныйПроцессныйНаборСПорчейКанала", "./tests/fixtures/isolation"); + Статистика = Прогон.Статистика.СтатистикаТестПлана(); + + Ожидаем.Что(Статистика.ПолучитьИлиУмолчание("НаборовСломано", 0)).Равно(1); + + // Потерян конец первого теста или начало второго - смотря чей подписчик + // сработал раньше. Счетчики сходятся в обоих случаях: потерянное начало + // достраивается, незавершенный тест ломается + Ожидаем.Что(Статистика.ПолучитьИлиУмолчание("ТестовОбнаружено", 0)).Равно(2); + Ожидаем.Что( + Статистика.ПолучитьИлиУмолчание("ТестовУспешно", 0) + + Статистика.ПолучитьИлиУмолчание("ТестовСломано", 0) + ).Равно(2); + + Набор = ПрогоныТестов.НайтиНабор(Прогон.План, "ИзолированныйПроцессныйНаборСПорчейКанала"); + Результат = Прогон.Статистика.Результат(Набор); + + Ожидаем.Что(СтрНайти(Результат.ОписанияПричин().Получить(0), "потеряла сообщения")).Больше(0); + +КонецПроцедуры + +&Тест +Процедура ДеревоИзолированногоПроцессаЗнаетПоследнийТест() Экспорт + + // Зеркала строятся по составу набора до первого теста: последним в дереве + // помечен только последний тест, а не каждый вклеенный по очереди + Вывод = ВыводПрогонаДеревом("ИзолированныйПроцессныйКрасныйНабор"); + + Ожидаем.Что(СтрокаТестаВДереве(Вывод, "ЗеленыйТест")).Содержит("├─"); + Ожидаем.Что(СтрокаТестаВДереве(Вывод, "КрасныйТест")).Содержит("└─"); + +КонецПроцедуры + +&Тест +Процедура ДеревоИзолированнойПоделкиЗнаетПоследнийТест() Экспорт + + Вывод = ВыводПрогонаДеревом("ИзолированныйЗеленыйНабор"); + + Ожидаем.Что(СтрокаТестаВДереве(Вывод, "ПервыйЗеленый")).Содержит("├─"); + Ожидаем.Что(СтрокаТестаВДереве(Вывод, "ВторойЗеленый")).Содержит("└─"); + +КонецПроцедуры + +&Тест +Процедура АварияДочернегоПроцессаНеТеряетВыполненныеТесты() Экспорт + + // События приходят по ходу исполнения: тест, выполненный до аварии, уже + // передан родителю. Успело ли уйти начало второго теста, зависит от порядка + // подписчиков в дочернем процессе, поэтому проверяется инвариант: начатый + // и не завершенный тест ломается, до третьего дело не доходит + Прогон = ПрогоныТестов.Прогнать("ИзолированныйПроцессныйНаборСАварией", "./tests/fixtures/isolation"); + Статистика = Прогон.Статистика.СтатистикаТестПлана(); + + Обнаружено = Статистика.ПолучитьИлиУмолчание("ТестовОбнаружено", 0); + + Ожидаем.Что(Статистика.ПолучитьИлиУмолчание("ТестовУспешно", 0)).Равно(1); + Ожидаем.Что(Обнаружено).МеньшеИлиРавно(2); + Ожидаем.Что(Статистика.ПолучитьИлиУмолчание("ТестовСломано", 0)).Равно(Обнаружено - 1); + Ожидаем.Что(Статистика.ПолучитьИлиУмолчание("НаборовСломано", 0)).Равно(1); + + Набор = ПрогоныТестов.НайтиНабор(Прогон.План, "ИзолированныйПроцессныйНаборСАварией"); + Результат = Прогон.Статистика.Результат(Набор); + + Ожидаем.Что(СтрНайти(Результат.ОписанияПричин().Получить(0), "завершился аварийно, не завершив набор")).Больше(0); + +КонецПроцедуры + &Тест Процедура ИзолированныйНаборВклеиваетИтерацииКонтейнера() Экспорт @@ -520,6 +651,53 @@ КонецФункции +Процедура ПроверитьПеренесенноеВремя(Знач ИмяНабора, Знач ИмяТеста) + + Прогон = ПрогоныТестов.Прогнать(ИмяНабора, "./tests/fixtures/isolation"); + Тест = ПрогоныТестов.НайтиТест(ПрогоныТестов.НайтиНабор(Прогон.План, ИмяНабора), ИмяТеста); + + Перенесенное = Прогон.Статистика.Результат(Тест).ПеренесенноеВремяВыполнения(); + + Ожидаем.Что(Перенесенное, "время перенесено из изолированной среды").Не_().ЭтоНеопределено(); + Ожидаем.Что(Перенесенное).БольшеИлиРавно(250); + Ожидаем.Что(Прогон.Статистика.СтатистикаТеста(Тест).ПолучитьИлиУмолчание("ВремяВыполнения", 0)) + .Равно(Перенесенное); + +КонецПроцедуры + +Функция ВыводПрогонаДеревом(Знач ВключаемыеНаборы) + + ДопДетальки = Новый Соответствие; + ДопДетальки.Вставить("OneUnit.РежимВыводаЛога", РежимыВыводаЛога.Дерево); + + ПотокВывода = Новый ПотокВПамяти(); + ТестированиеСлужебный.НачатьПерехватВывода(ПотокВывода); + + Попытка + ПрогоныТестов.Прогнать(ВключаемыеНаборы, "./tests/fixtures/isolation", "", ДопДетальки); + Исключение + ТестированиеСлужебный.ЗавершитьПерехватВывода(); + ВызватьИсключение; + КонецПопытки; + + ТестированиеСлужебный.ЗавершитьПерехватВывода(); + + Возврат ПрогоныТестов.ПрочитатьПоток(ПотокВывода); + +КонецФункции + +Функция СтрокаТестаВДереве(Знач Вывод, Знач ИмяТеста) + + Для каждого СтрокаВывода Из СтрРазделить(Вывод, Символы.ПС) Цикл + Если СтрНайти(СтрокаВывода, "─") > 0 И СтрНайти(СтрокаВывода, ИмяТеста) > 0 Тогда + Возврат СтрокаВывода; + КонецЕсли; + КонецЦикла; + + ВызватьИсключение СтрШаблон("В дереве прогона нет строки теста %1:%2%3", ИмяТеста, Символы.ПС, Вывод); + +КонецФункции + Функция ЧислоВхождений(Знач Текст, Знач Подстрока) Вхождения = 0; diff --git "a/tests/\320\232\320\276\320\275\321\201\320\276\320\273\321\214\320\275\320\276\320\265\320\237\321\200\320\270\320\273\320\276\320\266\320\265\320\275\320\270\320\265.os" "b/tests/\320\232\320\276\320\275\321\201\320\276\320\273\321\214\320\275\320\276\320\265\320\237\321\200\320\270\320\273\320\276\320\266\320\265\320\275\320\270\320\265.os" index b37a1be..a3884e6 100644 --- "a/tests/\320\232\320\276\320\275\321\201\320\276\320\273\321\214\320\275\320\276\320\265\320\237\321\200\320\270\320\273\320\276\320\266\320\265\320\275\320\270\320\265.os" +++ "b/tests/\320\232\320\276\320\275\321\201\320\276\320\273\321\214\320\275\320\276\320\265\320\237\321\200\320\270\320\273\320\276\320\266\320\265\320\275\320\270\320\265.os" @@ -487,16 +487,17 @@ ТестированиеСлужебный.НачатьПерехватВывода(Буфер); + // Порции вывода процесса, как их отдает 1commands, заканчиваются переводом строки Для каждого Уровень Из УровниВыводимые Цикл - Лог.Информация(СтрШаблон("{""msg"": ""СообщениеУровня%1"", ""level"": ""%1""}", Уровень)); + Лог.Информация(СтрШаблон("{""msg"": ""СообщениеУровня%1"", ""level"": ""%1""}", Уровень) + Символы.ПС); КонецЦикла; // Отладочный уровень перебрасывается, но по умолчанию не выводится - Лог.Информация("{""msg"": ""СообщениеУровняDEBUG"", ""level"": ""DEBUG""}"); + Лог.Информация("{""msg"": ""СообщениеУровняDEBUG"", ""level"": ""DEBUG""}" + Символы.ПС); // Сообщения не в формате JSON перебрасываются как есть - Лог.Ошибка("ПростоТекстОшибки"); - Лог.Информация("ПростоИнформационныйТекст"); + Лог.Ошибка("ПростоТекстОшибки" + Символы.ПС); + Лог.Информация("ПростоИнформационныйТекст" + Символы.ПС); ТестированиеСлужебный.ЗавершитьПерехватВывода(); @@ -518,6 +519,43 @@ КонецПроцедуры +&Тест +Процедура ЛогКомандыСклеиваетПорцииВЦелыеСтроки() Экспорт + + // 1commands отдает вывод процесса порциями, как прочитал: порция может + // оборвать строку и обычно заканчивается переводом строки. Ни стык порций, + // ни обрыв не должны давать в консоли пустых или разорванных строк + ДеталькиПоделки = Новый Соответствие; + ДеталькиПоделки.Вставить("OneUnit.КаталогиТестов", ""); + ДеталькиПоделки.Вставить("OneUnit.ФайлыТестов", ""); + + Лог = ПрогоныТестов.СоздатьПоделку(ДеталькиПоделки).НайтиЖелудь("ЛогДляКомандыМенеджера"); + + Буфер = Новый ПотокВПамяти(); + + ТестированиеСлужебный.НачатьПерехватВывода(Буфер); + + Попытка + Лог.Информация("ПерваяСтрока" + Символы.ВК + Символы.ПС + "Втора"); + Лог.Информация("яСтрока" + Символы.ВК + Символы.ПС); + Лог.Информация("ТретьяСтрока"); + Лог.Дописать(); + Исключение + ТестированиеСлужебный.ЗавершитьПерехватВывода(); + ВызватьИсключение; + КонецПопытки; + + ТестированиеСлужебный.ЗавершитьПерехватВывода(); + + Строки = СтрРазделить(СокрЛП(ПрогоныТестов.ПрочитатьПоток(Буфер)), Символы.ПС, Истина); + + Ожидаем.Что(Строки).ИмеетДлину(3); + Ожидаем.Что(Строки[0]).Содержит("ПерваяСтрока"); + Ожидаем.Что(Строки[1]).Содержит("ВтораяСтрока"); + Ожидаем.Что(Строки[2]).Содержит("ТретьяСтрока"); + +КонецПроцедуры + #КонецОбласти #Область ТестыОписанияПриложения