diff --git a/.bsl-language-server.json b/.bsl-language-server.json index 757991a..f1178bf 100644 --- a/.bsl-language-server.json +++ b/.bsl-language-server.json @@ -10,5 +10,13 @@ "debugTestArguments": "./src/cli/main.os e --debug 2801 --file %s -m %s", "getTestsResultPattern": "^[^.]+\\.([^\\s.]+).*$" } + }, + "inlayHint": { + "parameters": { + "methodCall": { + "showDefaultValues": true, + "showParametersWithTheSameName": true + } + } } } diff --git a/README.md b/README.md index 71a88bc..17a1dc0 100644 --- a/README.md +++ b/README.md @@ -743,3 +743,82 @@ $ oneunit execute --debug 2801 } } ``` + +## 3. Интеграция с ИИ-агентами (MCP) + +OneUnit включает MCP-сервер ([Model Context Protocol](https://modelcontextprotocol.io)), позволяющий ИИ-агентам (Claude Code, Cursor, GitHub Copilot и другим MCP-клиентам) обнаруживать и запускать тесты проекта напрямую, без разбора консольного вывода. Сервер построен на библиотеке [autumn-mcp](https://github.com/autumn-library/autumn-mcp) и работает по протоколу stdio. + +### 3.1 Подключение + +Добавьте OneUnit в конфигурацию MCP-серверов вашего клиента (например, `.mcp.json` для Claude Code или `.cursor/mcp.json` для Cursor). + +Linux и macOS: + +```json +{ + "mcpServers": { + "oneunit": { + "command": "oneunit", + "args": ["mcp"] + } + } +} +``` + +Windows — обязательно через `cmd /c`: `oneunit` устанавливается как `oneunit.bat`, а MCP-клиенты запускают серверы без командного процессора и не могут исполнить bat-файл напрямую: + +```json +{ + "mcpServers": { + "oneunit": { + "command": "cmd", + "args": ["/c", "oneunit", "mcp"] + } + } +} +``` + +Для Claude Code то же самое можно сделать одной командой: + +```console +# Linux и macOS +claude mcp add oneunit -- oneunit mcp + +# Windows +claude mcp add oneunit -- cmd /c oneunit mcp +``` + +Сервер запускается в рабочем каталоге проекта и по умолчанию ищет тесты в `./tests`. Зависимости проекта разрешаются так же, как при запуске из командной строки, включая `oscript.cfg` (см. [раздел 2.1.2](#212-работа-с-зависимостями)). + +### 3.2 Инструменты + +Сервер предоставляет два инструмента: + +| Инструмент | Описание | +|---|---| +| `run_tests` | Запускает тесты и возвращает структурированный результат: статистику прогона (`statistics`), проблемы с сообщениями об ошибках и стеками (`problems`), пропущенные тесты с причинами (`skipped_tests`) | +| `discover_tests` | Обнаруживает тесты без их выполнения и возвращает список полных имён тестов | + +Оба инструмента поддерживают параметры фильтрации, аналогичные опциям командной строки: каталоги (`directories`) и файлы (`files`) тестов, поиск в подкаталогах (`recursive`), фильтры по тегам (`tags_include`/`tags_exclude`), наборам (`suites_include`/`suites_exclude`) и методам (`methods_include`/`methods_exclude`), а `run_tests` дополнительно принимает таймаут теста в миллисекундах (`timeout`). Параметр `working_directory` позволяет указать каталог проекта, если он отличается от каталога запуска сервера. + +Параметр `coverage` инструмента `run_tests` добавляет в ответ сводку покрытия кода тестами: общий процент (`percent`), количество покрытых и исполняемых строк (`lines_covered`/`lines_total`) и разбивку по файлам с номерами непокрытых строк — файлы отсортированы от наименее покрытых. + +Пример результата `run_tests`: + +```json +{ + "success": false, + "exit_code": 1, + "statistics": { "total": 4, "passed": 2, "failed": 1, "broken": 1, "skipped": 0, "aborted": 0, "duration_ms": 59 }, + "problems": [ + { + "status": "failed", + "suite": "МойНабор", + "test": "МойТест", + "message": "[Ожидали, что проверяемое значение (Истина) является ЛОЖЬЮ.]", + "details": "Причина:..." + } + ], + "skipped_tests": [] +} +``` diff --git a/packagedef b/packagedef index 64450dc..8b6d435 100644 --- a/packagedef +++ b/packagedef @@ -26,9 +26,10 @@ .ЗависитОт("autumn-cli", "1.3.0") .ЗависитОт("autumn-event-publisher", "1.0.1") .ЗависитОт("autumn-logos", "1.3.3") + .ЗависитОт("autumn-mcp", "1.1.1") .ЗависитОт("collectionos", "0.8.3") .ЗависитОт("coverage", "0.7.0") - .ЗависитОт("decorator", "2.0.7") + .ЗависитОт("decorator", "2.0.8") .ЗависитОт("fs", "1.2.0") .ЗависитОт("lambdas", "0.3.4") .ЗависитОт("logos", "1.7.1") diff --git "a/src/cli/internal/\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/cli/internal/\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" new file mode 100644 index 0000000..21f77b9 --- /dev/null +++ "b/src/cli/internal/\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" @@ -0,0 +1,255 @@ +// Общая логика запуска прогонов дочерним процессом oscript через точку входа +// ядра: тестирование и обнаружение тестов, сбор покрытия кода. Используется +// командами консольного приложения (execute, discover) и MCP-инструментами. + +#Использовать 1commands +#Использовать tempfiles +#Использовать coverage +#Использовать fs +#Использовать "../../../shared" + +Перем _Служебный; // КонсольноеПриложениеТестированиеСлужебный + +// Возвращает параметры запуска прогона со значениями по умолчанию +// +// Возвращаемое значение: +// Структура - Параметры запуска: +// * РабочийКаталог - Строка - Каталог проекта, пустое значение - текущий каталог +// * ЛогВывода - Лог - Лог трансляции вывода прогона, Неопределено - не транслировать +// * ПортОтладки - Число - Порт сервера отладки прогона, 0 - без отладки +// * ПутьКОтчетуGenericCoverage - Строка - Путь к формируемому отчету покрытия GenericCoverage +// * ПутьКОтчетуCobertura - Строка - Путь к формируемому отчету покрытия Cobertura +// +Функция ПараметрыЗапуска() Экспорт + + Параметры = Новый Структура(); + + Параметры.Вставить("РабочийКаталог", ""); + Параметры.Вставить("ЛогВывода", Неопределено); + Параметры.Вставить("ПортОтладки", 0); + Параметры.Вставить("ПутьКОтчетуGenericCoverage", ""); + Параметры.Вставить("ПутьКОтчетуCobertura", ""); + + Возврат Параметры; + +КонецФункции + +// Выполняет тестирование дочерним прогоном +// +// Параметры: +// Детальки - Соответствие из КлючИЗначение - Детальки поделки прогона (фильтры тестов, отчеты, таймаут): +// * Ключ - Строка - Имя детальки поделки, например "КаталогиТестов" +// * Значение - Строка, Число, Булево, Массив из Строка - Значение детальки +// Лог - Лог - Лог вызывающего компонента +// Параметры - см. ПараметрыЗапуска, Неопределено - значения по умолчанию +// +// Возвращаемое значение: +// Структура - Результат прогона: +// * КодВозврата - Число - Код возврата дочернего процесса +// * Вывод - Строка - Захваченный вывод прогона +// +Функция ВыполнитьТестирование(Детальки, Лог, Знач Параметры = Неопределено) Экспорт + + ВыполняемаяКоманда = + " ТестПлан = МенеджерТестирования.Тестировать(); + | Статистика = Поделка.НайтиЖелудь(""РепортерСтатистика"") + | .СтатистикаТестПлана(); + | + | Если Статистика.ПолучитьИлиУмолчание(""НаборовОшибка"", 0) > 0 + | Или Статистика.ПолучитьИлиУмолчание(""НаборовСломано"", 0) > 0 + | Или Статистика.ПолучитьИлиУмолчание(""ТестовОшибка"", 0) > 0 + | Или Статистика.ПолучитьИлиУмолчание(""ТестовСломано"", 0) > 0 Тогда + | ЗавершитьРаботу(1); + | КонецЕсли; + |"; + + Возврат ВыполнитьПрогон(Детальки, ВыполняемаяКоманда, Лог, Параметры); + +КонецФункции + +// Выполняет обнаружение тестов дочерним прогоном +// +// Параметры: +// Детальки - Соответствие из КлючИЗначение - Детальки поделки прогона (фильтры тестов): +// * Ключ - Строка - Имя детальки поделки, например "КаталогиТестов" +// * Значение - Строка, Булево, Массив из Строка - Значение детальки +// РежимВывода - Строка - Режим вывода обнаруженных тестов, см. РежимыВыводаЛога. +// Поддерживаются Дерево и ПлоскийСписок, прочие режимы не выводят ничего +// Лог - Лог - Лог вызывающего компонента +// Параметры - см. ПараметрыЗапуска, Неопределено - значения по умолчанию +// +// Возвращаемое значение: +// Структура - Результат прогона: +// * КодВозврата - Число - Код возврата дочернего процесса +// * Вывод - Строка - Захваченный вывод прогона +// +Функция ВыполнитьОбнаружение(Детальки, Знач РежимВывода, Лог, Знач Параметры = Неопределено) Экспорт + + Если РежимВывода = РежимыВыводаЛога.Дерево Тогда + Репортер = "Поделка.НайтиЖелудь(""РепортерЛогДерево"").ВывестиТестПлан(ТестПлан);"; + ИначеЕсли РежимВывода = РежимыВыводаЛога.ПлоскийСписок Тогда + Репортер = "Поделка.НайтиЖелудь(""РепортерЛогПлоскийСписок"").ВывестиТестПлан(ТестПлан);"; + Иначе + Репортер = ""; + КонецЕсли; + + ВыполняемаяКоманда = СтрШаблон( + "ТестПлан = МенеджерТестирования.Обнаружить(); + |%1 + |", + Репортер + ); + + Возврат ВыполнитьПрогон(Детальки, ВыполняемаяКоманда, Лог, Параметры); + +КонецФункции + +// Возвращает существующий путь в каноническом регистре файловой системы. +// Также разворачивает короткие имена формата 8.3 в полные +// +// Параметры: +// Путь - Строка - Путь к существующему файлу или каталогу +// +// Возвращаемое значение: +// Строка - Путь в регистре, в котором он хранится в файловой системе, +// или исходный путь, если он не существует +// +Функция КаноническийРегистрПути(Знач Путь) Экспорт + + Файл = Новый Файл(Путь); + + Если НЕ Файл.Существует() Тогда + Возврат Путь; + КонецЕсли; + + ПолныйПуть = Файл.ПолноеИмя; + ПутьРодителя = (Новый Файл(Файл.Путь)).ПолноеИмя; + + Если ПутьРодителя = ПолныйПуть Тогда + // Корень диска + Возврат ВРег(ПолныйПуть); + КонецЕсли; + + ИмяВФайловойСистеме = Файл.Имя; + + НайденныеФайлы = НайтиФайлы(ПутьРодителя, Файл.Имя); + Если НайденныеФайлы.Количество() > 0 Тогда + ИмяВФайловойСистеме = НайденныеФайлы[0].Имя; + КонецЕсли; + + Возврат ОбъединитьПути(КаноническийРегистрПути(ПутьРодителя), ИмяВФайловойСистеме); + +КонецФункции + +Функция ВыполнитьПрогон(Детальки, Знач ВыполняемаяКоманда, Лог, Знач Параметры) + + Если Параметры = Неопределено Тогда + Параметры = ПараметрыЗапуска(); + КонецЕсли; + + ФормироватьПокрытие = ЗначениеЗаполнено(Параметры.ПутьКОтчетуGenericCoverage) + ИЛИ ЗначениеЗаполнено(Параметры.ПутьКОтчетуCobertura); + + МенеджерВременныхФайлов = Новый МенеджерВременныхФайлов(); + МенеджерВременныхФайлов.БазовыйКаталог = МенеджерВременныхФайлов.СоздатьКаталог(); + + ИсходныйКаталог = ТекущийКаталог(); + + // Регистр пути важен: отчет покрытия фильтруется по каталогу исходников + // регистрозависимо, а клиенты передают путь проекта в произвольном регистре + // (например, VSCode - с маленькой буквой диска) + УстановитьТекущийКаталог(КаноническийРегистрПути( + ?(ЗначениеЗаполнено(Параметры.РабочийКаталог), Параметры.РабочийКаталог, ИсходныйКаталог) + )); + + Попытка + + ИмяТочкиВхода = _Служебный.СоздатьТочкуВхода( + Детальки, + МенеджерВременныхФайлов, + Лог, + ВыполняемаяКоманда + ); + + Команда = Новый Команда(); + + Команда.УстановитьКоманду("oscript"); + Команда.УстановитьРабочийКаталог(ТекущийКаталог()); + + Если ФормироватьПокрытие Тогда + ПутьКСтатистике = МенеджерВременныхФайлов.НовоеИмяФайла("json"); + Команда.ДобавитьПараметр("-codestat=" + ПутьКСтатистике); + КонецЕсли; + + Если Параметры.ПортОтладки > 0 Тогда + Команда.ДобавитьПараметр("-debug=" + Параметры.ПортОтладки); + КонецЕсли; + + Если СтрНайти(ИмяТочкиВхода, " ") > 0 Тогда + ИмяТочкиВхода = Команда.ОбернутьВКавычки(ИмяТочкиВхода); + КонецЕсли; + + Команда.ДобавитьПараметр(ИмяТочкиВхода); + + Если Параметры.ЛогВывода <> Неопределено Тогда + Команда.ДобавитьЛогВыводаКоманды(Параметры.ЛогВывода); + КонецЕсли; + + КодВозврата = Команда.Исполнить(); + Вывод = Команда.ПолучитьВывод(); + + Если ФормироватьПокрытие Тогда + СформироватьОтчетыПокрытия(ПутьКСтатистике, Параметры, МенеджерВременныхФайлов); + КонецЕсли; + + Исключение + УстановитьТекущийКаталог(ИсходныйКаталог); + МенеджерВременныхФайлов.Удалить(); + ВызватьИсключение; + КонецПопытки; + + УстановитьТекущийКаталог(ИсходныйКаталог); + МенеджерВременныхФайлов.Удалить(); + + Возврат Новый Структура("КодВозврата, Вывод", КодВозврата, Вывод); + +КонецФункции + +Процедура СформироватьОтчетыПокрытия(Знач ПутьКСтатистике, Знач Параметры, МенеджерВременныхФайлов) + + Если НЕ (Новый Файл(ПутьКСтатистике)).Существует() Тогда + Возврат; + КонецЕсли; + + ВременныйКаталог = МенеджерВременныхФайлов.НовоеИмяФайла(); + ФС.ОбеспечитьПустойКаталог(ВременныйКаталог); + + ПроцессорГенерации = Новый ГенераторОтчетаПокрытия(); + + ПроцессорГенерации.ФайлСтатистики(ПутьКСтатистике) + .РабочийКаталог(ВременныйКаталог) + .ИмяФайлаСтатистики("*") + .GenericCoverage("genericCoverage.xml") + .Cobertura("cobertura.xml") + .Сформировать(); + + СкопироватьОтчет(ОбъединитьПути(ВременныйКаталог, "genericCoverage.xml"), Параметры.ПутьКОтчетуGenericCoverage); + СкопироватьОтчет(ОбъединитьПути(ВременныйКаталог, "cobertura.xml"), Параметры.ПутьКОтчетуCobertura); + +КонецПроцедуры + +Процедура СкопироватьОтчет(Знач Источник, Знач Приемник) + + Если НЕ ЗначениеЗаполнено(Приемник) Тогда + Возврат; + КонецЕсли; + + ФС.ОбеспечитьКаталог(Новый Файл(Приемник).Путь); + КопироватьФайл(Источник, Приемник); + +КонецПроцедуры + +&Желудь +Процедура ПриСозданииОбъекта(&Пластилин КонсольноеПриложениеТестированиеСлужебный) + _Служебный = КонсольноеПриложениеТестированиеСлужебный; +КонецПроцедуры diff --git "a/src/cli/internal/\320\234\320\276\320\264\321\203\320\273\320\270/\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\320\242\320\265\321\201\321\202\320\270\321\200\320\276\320\262\320\260\320\275\320\270\320\265\320\241\320\273\321\203\320\266\320\265\320\261\320\275\321\213\320\271.os" "b/src/cli/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\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\320\242\320\265\321\201\321\202\320\270\321\200\320\276\320\262\320\260\320\275\320\270\320\265\320\241\320\273\321\203\320\266\320\265\320\261\320\275\321\213\320\271.os" similarity index 99% rename from "src/cli/internal/\320\234\320\276\320\264\321\203\320\273\320\270/\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\320\242\320\265\321\201\321\202\320\270\321\200\320\276\320\262\320\260\320\275\320\270\320\265\320\241\320\273\321\203\320\266\320\265\320\261\320\275\321\213\320\271.os" rename to "src/cli/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\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\320\242\320\265\321\201\321\202\320\270\321\200\320\276\320\262\320\260\320\275\320\270\320\265\320\241\320\273\321\203\320\266\320\265\320\261\320\275\321\213\320\271.os" index d801946..e8a4ae6 100644 --- "a/src/cli/internal/\320\234\320\276\320\264\321\203\320\273\320\270/\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\320\242\320\265\321\201\321\202\320\270\321\200\320\276\320\262\320\260\320\275\320\270\320\265\320\241\320\273\321\203\320\266\320\265\320\261\320\275\321\213\320\271.os" +++ "b/src/cli/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\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\320\242\320\265\321\201\321\202\320\270\321\200\320\276\320\262\320\260\320\275\320\270\320\265\320\241\320\273\321\203\320\266\320\265\320\261\320\275\321\213\320\271.os" @@ -1,4 +1,4 @@ -#Использовать fs +#Использовать fs #Использовать logos #Использовать decorator #Использовать fluent @@ -329,3 +329,7 @@ ); КонецФункции + +&Желудь +Процедура ПриСозданииОбъекта() +КонецПроцедуры diff --git a/src/cli/main.os b/src/cli/main.os index bc46142..15771ac 100644 --- a/src/cli/main.os +++ b/src/cli/main.os @@ -6,9 +6,10 @@ #Использовать "./internal/isolated" #Использовать "../shared" +// Поделки еще нет - желудь настроек создается точкой сборки напрямую СоветДругогоМастера = Новый СоветДругогоМастера() .ЗначенияДеталек(Новый Соответствие( - КонсольноеПриложениеТестированиеСлужебный.НастройкиЛогоса("cli", "РаскладкаЛогOneUnit") + (Новый КонсольноеПриложениеТестированиеСлужебный()).НастройкиЛогоса("cli", "РаскладкаЛогOneUnit") )); Поделка = Новый Поделка(СоветДругогоМастера); 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\260MCP.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\260MCP.os" new file mode 100644 index 0000000..ba41ede --- /dev/null +++ "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\260MCP.os" @@ -0,0 +1,24 @@ +// Команда запуска MCP-сервера (stdio) для интеграции с ИИ-агентами. +// +// Сервер нельзя собрать в поделке консольного приложения: рогатки ОСени +// исполняются при старте любой поделки, и MCP_Рогатка (autumn-mcp) не может +// сосуществовать с рогаткой autumn-cli - обе захватывают процесс. Поэтому +// autumn-mcp подключается отложенно: точка входа сервера загружается уже +// после старта поделки приложения и собирает собственную поделку с +// выключенными рогатками (см. src/mcp/main.os). + +#Использовать fs + +&ВыполнениеКоманды +Процедура ЗапуститьСервер() Экспорт + + КаталогИсходников = ОбъединитьПути(ТекущийСценарий().Каталог, "..", ".."); + ПутьКТочкеВхода = ФС.НормализоватьПуть(ОбъединитьПути(КаталогИсходников, "mcp", "main.os")); + + ЗагрузитьСценарий(ПутьКТочкеВхода); + +КонецПроцедуры + +&КомандаПриложения(Имя = "mcp", Описание = "Запускает MCP-сервер (stdio) для работы с тестами из ИИ-агентов") +Процедура ПриСозданииОбъекта() +КонецПроцедуры 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 5c459d8..4223292 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" @@ -1,6 +1,3 @@ -#Использовать 1commands -#Использовать tempfiles -#Использовать collectionos #Использовать "../internal" #Использовать "../../shared" @@ -36,13 +33,12 @@ Перем _Лог; Перем _ЛогДляКомандыМенеджера; +Перем _Запускатель; +Перем _Служебный; &ВыполнениеКоманды Процедура Обнаружить() Экспорт - МенеджерВременныхФайлов = Новый МенеджерВременныхФайлов(); - МенеджерВременныхФайлов.БазовыйКаталог = МенеджерВременныхФайлов.СоздатьКаталог(); - Детальки = Новый Соответствие(); Детальки.Вставить("КаталогиТестов", КаталогиТестов); @@ -55,39 +51,19 @@ Детальки.Вставить("ТестовыеМетодыВключаемые", ТестовыеМетодыВключаемые); Детальки.Вставить("ТестовыеМетодыИсключаемые", ТестовыеМетодыИсключаемые); - Если КонсольноеПриложениеТестированиеСлужебный.РежимВыводаЛога(РежимВывода) = РежимыВыводаЛога.Дерево Тогда - Репортер = "Поделка.НайтиЖелудь(""РепортерЛогДерево"").ВывестиТестПлан(ТестПлан);"; - ИначеЕсли КонсольноеПриложениеТестированиеСлужебный.РежимВыводаЛога(РежимВывода) = РежимыВыводаЛога.ПлоскийСписок Тогда - Репортер = "Поделка.НайтиЖелудь(""РепортерЛогПлоскийСписок"").ВывестиТестПлан(ТестПлан);"; - Иначе - Репортер = ""; - КонецЕсли; + ПараметрыЗапуска = _Запускатель.ПараметрыЗапуска(); + ПараметрыЗапуска.ЛогВывода = _ЛогДляКомандыМенеджера; - ИмяВременногоФайла = КонсольноеПриложениеТестированиеСлужебный.СоздатьТочкуВхода( + Результат = _Запускатель.ВыполнитьОбнаружение( Детальки, - МенеджерВременныхФайлов, + _Служебный.РежимВыводаЛога(РежимВывода), _Лог, - СтрШаблон( - "ТестПлан = МенеджерТестирования.Обнаружить(); - |%1 - |", - Репортер - ) + ПараметрыЗапуска ); - Команда = Новый Команда; - - Команда.УстановитьКоманду("oscript"); - Команда.ДобавитьПараметр(ИмяВременногоФайла); - Команда.ДобавитьЛогВыводаКоманды(_ЛогДляКомандыМенеджера); - - КодВозврата = Команда.Исполнить(); - - МенеджерВременныхФайлов.Удалить(); - - Если КодВозврата <> 0 Тогда + Если Результат.КодВозврата <> 0 Тогда Сообщить("Обнаружение завершилось неудачно", СтатусСообщения.ОченьВажное); - ЗавершитьРаботу(КодВозврата); + ЗавершитьРаботу(Результат.КодВозврата); КонецЕсли; КонецПроцедуры @@ -95,9 +71,13 @@ &КомандаПриложения(Имя = "discover d", Описание = "Выводит в консоль все обнаруженные тесты") Процедура ПриСозданииОбъекта( &Лог("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\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 9b515f9..d3a51c9 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" @@ -1,7 +1,5 @@ -#Использовать 1commands -#Использовать tempfiles #Использовать collectionos -#Использовать coverage +#Использовать fs #Использовать "../internal" &ОпцияКаталогиТестов @@ -69,16 +67,12 @@ Перем _Лог; Перем _ЛогДляКомандыМенеджера; +Перем _Запускатель; +Перем _Служебный; &ВыполнениеКоманды Процедура Тестировать() Экспорт - ФормироватьПокрытие = Не ПустаяСтрока(ПутьКОтчетуПокрытияВФорматеGenericCoverage) - ИЛИ Не ПустаяСтрока(ПутьКОтчетуПокрытияВФорматеCobertura); - - МенеджерВременныхФайлов = Новый МенеджерВременныхФайлов(); - МенеджерВременныхФайлов.БазовыйКаталог = МенеджерВременныхФайлов.СоздатьКаталог(); - Списки.ИзЭлементов(ПутьКОтчетуGenericExecution, ПутьКОтчетуJUnit, ПутьКОтчетуOpenTestReport) .ПроцессорКоллекции() .Фильтровать("(Путь) -> Не ПустаяСтрока(Путь)") @@ -100,81 +94,20 @@ Детальки.Вставить("ТестовыеМетодыВключаемые", ТестовыеМетодыВключаемые); Детальки.Вставить("ТестовыеМетодыИсключаемые", ТестовыеМетодыИсключаемые); - Детальки.Вставить("РежимВыводаЛога", КонсольноеПриложениеТестированиеСлужебный.РежимВыводаЛога(РежимВывода)); - - ИмяВременногоФайла = КонсольноеПриложениеТестированиеСлужебный.СоздатьТочкуВхода( - Детальки, - МенеджерВременныхФайлов, - _Лог, - " - | ТестПлан = МенеджерТестирования.Тестировать(); - | Статистика = Поделка.НайтиЖелудь(""РепортерСтатистика"") - | .СтатистикаТестПлана(); - | - | Если Статистика.ПолучитьИлиУмолчание(""НаборовОшибка"", 0) > 0 - | Или Статистика.ПолучитьИлиУмолчание(""НаборовСломано"", 0) > 0 - | Или Статистика.ПолучитьИлиУмолчание(""ТестовОшибка"", 0) > 0 - | Или Статистика.ПолучитьИлиУмолчание(""ТестовСломано"", 0) > 0 Тогда - | ЗавершитьРаботу(1); - | КонецЕсли; - | - |" - ); - - ПутьКСтатистике = МенеджерВременныхФайлов.НовоеИмяФайла("json"); - - Команда = Новый Команда; - - Команда.УстановитьКоманду("oscript"); - - Если ФормироватьПокрытие Тогда - Команда.ДобавитьПараметр("-codestat=" + ПутьКСтатистике); - КонецЕсли; - - Если ПортОтладки > 0 Тогда - Команда.ДобавитьПараметр("-debug=" + ПортОтладки); - КонецЕсли; - - Команда.ДобавитьПараметр(ИмяВременногоФайла); - - Команда.УстановитьИсполнениеЧерезКомандыСистемы(Ложь); - - Команда.ДобавитьЛогВыводаКоманды(_ЛогДляКомандыМенеджера); + Детальки.Вставить("РежимВыводаЛога", _Служебный.РежимВыводаЛога(РежимВывода)); - КодВозврата = Команда.Исполнить(); + ПараметрыЗапуска = _Запускатель.ПараметрыЗапуска(); - Если ФормироватьПокрытие Тогда - - ВременныйКаталог = МенеджерВременныхФайлов.НовоеИмяФайла(); - - ФС.ОбеспечитьПустойКаталог(ВременныйКаталог); - - ПроцессорГенерации = Новый ГенераторОтчетаПокрытия(); - - ПроцессорГенерации.ФайлСтатистики(ПутьКСтатистике) - .РабочийКаталог(ВременныйКаталог) - .ИмяФайлаСтатистики("*") - .GenericCoverage("genericCoverage.xml") - .Cobertura("cobertura.xml") - .Сформировать(); - - Если Не ПустаяСтрока(ПутьКОтчетуПокрытияВФорматеCobertura) Тогда - ФС.ОбеспечитьКаталог(Новый Файл(ПутьКОтчетуПокрытияВФорматеCobertura).Путь); - КопироватьФайл(ОбъединитьПути(ВременныйКаталог, "cobertura.xml"), ПутьКОтчетуПокрытияВФорматеCobertura); - КонецЕсли; - - Если Не ПустаяСтрока(ПутьКОтчетуПокрытияВФорматеGenericCoverage) Тогда - ФС.ОбеспечитьКаталог(Новый Файл(ПутьКОтчетуПокрытияВФорматеGenericCoverage).Путь); - КопироватьФайл(ОбъединитьПути(ВременныйКаталог, "genericCoverage.xml"), ПутьКОтчетуПокрытияВФорматеGenericCoverage); - КонецЕсли; - - КонецЕсли; + ПараметрыЗапуска.ЛогВывода = _ЛогДляКомандыМенеджера; + ПараметрыЗапуска.ПортОтладки = ПортОтладки; + ПараметрыЗапуска.ПутьКОтчетуGenericCoverage = ПутьКОтчетуПокрытияВФорматеGenericCoverage; + ПараметрыЗапуска.ПутьКОтчетуCobertura = ПутьКОтчетуПокрытияВФорматеCobertura; - МенеджерВременныхФайлов.Удалить(); + Результат = _Запускатель.ВыполнитьТестирование(Детальки, _Лог, ПараметрыЗапуска); - Если КодВозврата <> 0 Тогда + Если Результат.КодВозврата <> 0 Тогда Сообщить("Тестирование завершилось неудачно", СтатусСообщения.ОченьВажное); - ЗавершитьРаботу(КодВозврата); + ЗавершитьРаботу(Результат.КодВозврата); Иначе Сообщить("Тестирование завершилось удачно", СтатусСообщения.Информация); КонецЕсли; @@ -184,9 +117,13 @@ &КомандаПриложения(Имя = "execute e", Описание = "Тестирует проект") Процедура ПриСозданииОбъекта( &Лог("oscript.lib.oneunit.cli") Лог, - &Пластилин ЛогДляКомандыМенеджера) + &Пластилин ЛогДляКомандыМенеджера, + &Пластилин ЗапускательТестирования, + &Пластилин КонсольноеПриложениеТестированиеСлужебный) _Лог = Лог; _ЛогДляКомандыМенеджера = ЛогДляКомандыМенеджера; + _Запускатель = ЗапускательТестирования; + _Служебный = КонсольноеПриложениеТестированиеСлужебный; КонецПроцедуры 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" new file mode 100644 index 0000000..671242b --- /dev/null +++ "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" @@ -0,0 +1,184 @@ +// Формирует отчет о прогоне в формате JSON: статистика тестов, проблемы +// (упавшие и сломанные тесты и наборы) и пропущенные тесты с причинами. +// Отчет машиночитаемый, используется MCP-инструментами (src/mcp). + +#Использовать collectionos +#Использовать "../../../shared" + +Перем _ПутьКОтчету; + +Перем _РепортерСтатистика; + +&ПодпискаНаСобытие("ИсполнениеТестПланКонец") +Процедура ИсполнениеТестПланКонец(ТестПлан) Экспорт + + Если Не ЗначениеЗаполнено(_ПутьКОтчету) Тогда + Возврат; + КонецЕсли; + + ВывестиТестПлан(ТестПлан); + +КонецПроцедуры + +Процедура ВывестиТестПлан(ТестПлан) + + Проблемы = Новый Массив(); + Пропущенные = Новый Массив(); + + Для Каждого Набор Из ТестПлан.Дети() Цикл + ОбработатьНабор(Набор, Набор.Имя(), Проблемы, Пропущенные); + КонецЦикла; + + Отчет = Новый Структура(); + Отчет.Вставить("statistics", Статистика()); + Отчет.Вставить("problems", Проблемы); + Отчет.Вставить("skipped_tests", Пропущенные); + + ЗаписьJSON = Новый ЗаписьJSON(); + ЗаписьJSON.ОткрытьФайл(_ПутьКОтчету); + ЗаписатьJSON(ЗаписьJSON, Отчет); + ЗаписьJSON.Закрыть(); + +КонецПроцедуры + +Процедура ОбработатьНабор(Определение, Знач ИмяНабора, Проблемы, Пропущенные) + + Результат = _РепортерСтатистика.Результат(Определение); + Состояние = Результат.Состояние(); + + Если Состояние = СостоянияВыполненияТестов.Ошибка + Или Состояние = СостоянияВыполненияТестов.Сломан Тогда + + // Набор не был выполнен - тесты внутри не имеют результатов + Проблемы.Добавить(Проблема(Определение.ПолноеИмя(), Неопределено, Результат)); + + ИначеЕсли Состояние = СостоянияВыполненияТестов.Пропущен + Или Состояние = СостоянияВыполненияТестов.Прерван Тогда + + Пропущенные.Добавить(Пропуск(Определение.ПолноеИмя(), Неопределено, Результат)); + + Иначе + + Для Каждого Дитя Из Определение.Дети() Цикл + + Если Дитя.ТипОпределения() = ТипыОпределенийТестов.Контейнер Тогда + ОбработатьНабор(Дитя, ИмяНабора, Проблемы, Пропущенные); + Иначе + ОбработатьТест(Дитя, ИмяНабора, Проблемы, Пропущенные); + КонецЕсли; + + КонецЦикла; + + КонецЕсли; + +КонецПроцедуры + +Процедура ОбработатьТест(Тест, Знач ИмяНабора, Проблемы, Пропущенные) + + Результат = _РепортерСтатистика.Результат(Тест); + Состояние = Результат.Состояние(); + + Если Состояние = СостоянияВыполненияТестов.Ошибка + Или Состояние = СостоянияВыполненияТестов.Сломан Тогда + + Проблемы.Добавить(Проблема(ИмяНабора, Тест.Имя(), Результат)); + + ИначеЕсли Состояние = СостоянияВыполненияТестов.Пропущен + Или Состояние = СостоянияВыполненияТестов.Прерван Тогда + + Пропущенные.Добавить(Пропуск(ИмяНабора, Тест.Имя(), Результат)); + + КонецЕсли; + +КонецПроцедуры + +Функция Статистика() + + СтатистикаПлана = _РепортерСтатистика.СтатистикаТестПлана(); + + Статистика = Новый Структура(); + + Статистика.Вставить("total", СтатистикаПлана.ПолучитьИлиУмолчание("ТестовОбнаружено", 0)); + Статистика.Вставить("passed", СтатистикаПлана.ПолучитьИлиУмолчание("ТестовУспешно", 0)); + Статистика.Вставить("failed", СтатистикаПлана.ПолучитьИлиУмолчание("ТестовОшибка", 0)); + Статистика.Вставить("broken", СтатистикаПлана.ПолучитьИлиУмолчание("ТестовСломано", 0)); + Статистика.Вставить("skipped", СтатистикаПлана.ПолучитьИлиУмолчание("ТестовПропущено", 0)); + Статистика.Вставить("aborted", СтатистикаПлана.ПолучитьИлиУмолчание("ТестовПрервано", 0)); + Статистика.Вставить("duration_ms", СтатистикаПлана.ПолучитьИлиУмолчание("ВремяВыполнения", 0)); + + Возврат Статистика; + +КонецФункции + +Функция Проблема(Знач ИмяНабора, Знач ИмяТеста, Результат) + + Проблема = ЗаписьОтчета(ИмяНабора, ИмяТеста, Результат); + + Проблема.Вставить("message", ОписанияПричин(Результат)); + Проблема.Вставить("details", ПодробныеПричины(Результат)); + + Возврат Проблема; + +КонецФункции + +Функция Пропуск(Знач ИмяНабора, Знач ИмяТеста, Результат) + + Пропуск = ЗаписьОтчета(ИмяНабора, ИмяТеста, Результат); + + Пропуск.Вставить("reason", ОписанияПричин(Результат)); + + Возврат Пропуск; + +КонецФункции + +Функция ЗаписьОтчета(Знач ИмяНабора, Знач ИмяТеста, Результат) + + Запись = Новый Структура(); + + Запись.Вставить("status", СтатусСостояния(Результат.Состояние())); + Запись.Вставить("suite", ИмяНабора); + + Если ИмяТеста <> Неопределено Тогда + Запись.Вставить("test", ИмяТеста); + КонецЕсли; + + Возврат Запись; + +КонецФункции + +Функция ОписанияПричин(Результат) + + Возврат СтрСоединить(Результат.ОписанияПричин().ВМассив(), Символы.ПС); + +КонецФункции + +Функция ПодробныеПричины(Результат) + + Возврат Результат.Причины() + .ПроцессорКоллекции() + .Обработать("Причина -> ТестированиеСлужебный.ПодробноеОписаниеОшибки(""Причина"", Причина)") + .ВСтроку(Символы.ПС); + +КонецФункции + +Функция СтатусСостояния(Знач Состояние) + + Если Состояние = СостоянияВыполненияТестов.Ошибка Тогда + Возврат "failed"; + ИначеЕсли Состояние = СостоянияВыполненияТестов.Сломан Тогда + Возврат "broken"; + ИначеЕсли Состояние = СостоянияВыполненияТестов.Пропущен Тогда + Возврат "skipped"; + ИначеЕсли Состояние = СостоянияВыполненияТестов.Прерван Тогда + Возврат "aborted"; + Иначе + Возврат "passed"; + КонецЕсли; + +КонецФункции + +&Желудь +Процедура ПриСозданииОбъекта(&Блестяшка ПутьКОтчетуJSON, &Пластилин РепортерСтатистика) + _ПутьКОтчету = ПутьКОтчетуJSON; + _РепортерСтатистика = РепортерСтатистика; +КонецПроцедуры diff --git "a/src/core/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\240\320\276\320\263\320\260\321\202\320\272\320\260\320\242\320\265\321\201\321\202\320\270\321\200\320\276\320\262\320\260\320\275\320\270\320\265.os" "b/src/core/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\240\320\276\320\263\320\260\321\202\320\272\320\260\320\242\320\265\321\201\321\202\320\270\321\200\320\276\320\262\320\260\320\275\320\270\320\265.os" index 7329a36..e700ab6 100644 --- "a/src/core/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\240\320\276\320\263\320\260\321\202\320\272\320\260\320\242\320\265\321\201\321\202\320\270\321\200\320\276\320\262\320\260\320\275\320\270\320\265.os" +++ "b/src/core/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\240\320\276\320\263\320\260\321\202\320\272\320\260\320\242\320\265\321\201\321\202\320\270\321\200\320\276\320\262\320\260\320\275\320\270\320\265.os" @@ -8,6 +8,7 @@ Перем _ПутьКОтчетуJUnit; Перем _ПутьКОтчетуGenericExecution; Перем _ПутьКОтчетуOpenTestReport; +Перем _ПутьКОтчетуJSON; Процедура ПриЗапускеПриложения() Экспорт @@ -17,6 +18,10 @@ _Поделка.НайтиЖелудь("РепортерJUnit", Массивы.ИзЭлементов(_ПутьКОтчетуJUnit)); КонецЕсли; + Если ЗначениеЗаполнено(_ПутьКОтчетуJSON) Тогда + _Поделка.НайтиЖелудь("РепортерJSON", Массивы.ИзЭлементов(_ПутьКОтчетуJSON)); + КонецЕсли; + Если ЗначениеЗаполнено(_ПутьКОтчетуGenericExecution) Тогда _Поделка.НайтиЖелудь("РепортерGenericExecution", Массивы.ИзЭлементов(_ПутьКОтчетуGenericExecution)); КонецЕсли; @@ -39,6 +44,7 @@ &Деталька("OneUnit.ПутьКОтчетуJUnit") ПутьКОтчетуJUnit, &Деталька("OneUnit.ПутьКОтчетуGenericExecution") ПутьКОтчетуGenericExecution, &Деталька("OneUnit.ПутьКОтчетуOpenTestReport") ПутьКОтчетуOpenTestReport, + &Деталька("OneUnit.ПутьКОтчетуJSON") ПутьКОтчетуJSON, &Деталька("OneUnit.РежимВыводаЛога") РежимВыводаЛога) _Поделка = Поделка; @@ -48,5 +54,6 @@ _ПутьКОтчетуJUnit = ФС.ПолныйПуть(ПутьКОтчетуJUnit); _ПутьКОтчетуGenericExecution = ФС.ПолныйПуть(ПутьКОтчетуGenericExecution); _ПутьКОтчетуOpenTestReport = ФС.ПолныйПуть(ПутьКОтчетуOpenTestReport); + _ПутьКОтчетуJSON = ФС.ПолныйПуть(ПутьКОтчетуJSON); КонецПроцедуры diff --git "a/src/mcp/internal/\320\232\320\273\320\260\321\201\321\201\321\213/MCP\320\230\320\275\321\201\321\202\321\200\321\203\320\274\320\265\320\275\321\202\321\213\320\241\320\273\321\203\320\266\320\265\320\261\320\275\321\213\320\271.os" "b/src/mcp/internal/\320\232\320\273\320\260\321\201\321\201\321\213/MCP\320\230\320\275\321\201\321\202\321\200\321\203\320\274\320\265\320\275\321\202\321\213\320\241\320\273\321\203\320\266\320\265\320\261\320\275\321\213\320\271.os" new file mode 100644 index 0000000..181e8d4 --- /dev/null +++ "b/src/mcp/internal/\320\232\320\273\320\260\321\201\321\201\321\213/MCP\320\230\320\275\321\201\321\202\321\200\321\203\320\274\320\265\320\275\321\202\321\213\320\241\320\273\321\203\320\266\320\265\320\261\320\275\321\213\320\271.os" @@ -0,0 +1,425 @@ +// Служебные операции MCP-инструментов: сборка деталек прогона, выполнение +// прогона через общий запускатель тестирования (как это делают команды +// консольного приложения) и разбор JSONL-вывода прогона. + +#Использовать fluent +#Использовать collectionos + +#Использовать "../../../cli/internal" +#Использовать "../../../shared" + +Перем _Запускатель; // ЗапускательТестирования +Перем _Лог; + +// Собирает детальки фильтров тестов для поделки дочернего прогона +// +// Параметры: +// Каталоги - Массив из Строка - Каталоги поиска тестовых наборов +// Файлы - Массив из Строка - Файлы тестовых наборов +// ИскатьВПодкаталогах - Булево - Искать тестовые наборы в подкаталогах +// ТегиВключаемые - Массив из Строка - Теги, включаемые в прогон +// ТегиИсключаемые - Массив из Строка - Теги, исключаемые из прогона +// НаборыВключаемые - Массив из Строка - Имена наборов, включаемые в прогон +// НаборыИсключаемые - Массив из Строка - Имена наборов, исключаемые из прогона +// МетодыВключаемые - Массив из Строка - Имена методов, включаемые в прогон +// МетодыИсключаемые - Массив из Строка - Имена методов, исключаемые из прогона +// +// Возвращаемое значение: +// Соответствие из КлючИЗначение - Детальки фильтров для поделки прогона: +// * Ключ - Строка - Имя детальки поделки, например "КаталогиТестов" +// * Значение - Строка, Булево, Массив из Строка - Значение детальки +// +Функция ДеталькиФильтров(Знач Каталоги, Знач Файлы, Знач ИскатьВПодкаталогах, + Знач ТегиВключаемые, Знач ТегиИсключаемые, Знач НаборыВключаемые, Знач НаборыИсключаемые, + Знач МетодыВключаемые, Знач МетодыИсключаемые) Экспорт + + Детальки = Новый Соответствие(); + + Детальки.Вставить("КаталогиТестов", МассивЗначений(Каталоги)); + Детальки.Вставить("ФайлыТестов", МассивЗначений(Файлы)); + Детальки.Вставить("ИскатьВПодкаталогах", ИскатьВПодкаталогах = Истина); + Детальки.Вставить("ТегиВключаемые", МассивЗначений(ТегиВключаемые)); + Детальки.Вставить("ТегиИсключаемые", МассивЗначений(ТегиИсключаемые)); + Детальки.Вставить("ТестовыеНаборыВключаемые", МассивЗначений(НаборыВключаемые)); + Детальки.Вставить("ТестовыеНаборыИсключаемые", МассивЗначений(НаборыИсключаемые)); + Детальки.Вставить("ТестовыеМетодыВключаемые", МассивЗначений(МетодыВключаемые)); + Детальки.Вставить("ТестовыеМетодыИсключаемые", МассивЗначений(МетодыИсключаемые)); + + Возврат Детальки; + +КонецФункции + +// Выполняет тестирование дочерним прогоном с формированием JSON-отчета +// +// Параметры: +// Детальки - см. ДеталькиФильтров - Детальки фильтров прогона +// Таймаут - Число - Таймаут выполнения каждого теста в миллисекундах +// ПутьКОтчетуJSON - Строка - Путь к формируемому файлу JSON-отчета +// РабочийКаталог - Строка - Каталог проекта, по умолчанию текущий каталог +// ПутьКОтчетуПокрытия - Строка - Путь к формируемому файлу отчета покрытия +// в формате GenericCoverage. Пустая строка - не собирать покрытие +// +// Возвращаемое значение: +// Структура - Результат прогона: +// * КодВозврата - Число - Код возврата дочернего процесса +// * Вывод - Строка - Захваченный вывод прогона +// +Функция ВыполнитьТестирование(Детальки, Знач Таймаут, Знач ПутьКОтчетуJSON, Знач РабочийКаталог, + Знач ПутьКОтчетуПокрытия = "") Экспорт + + Детальки.Вставить("Таймаут", ?(ЗначениеЗаполнено(Таймаут) И Таймаут > 0, Таймаут, 0)); + Детальки.Вставить("ПутьКОтчетуJSON", ПутьКОтчетуJSON); + Детальки.Вставить("РежимВыводаЛога", РежимыВыводаЛога.Ничего); + + ПараметрыЗапуска = _Запускатель.ПараметрыЗапуска(); + + ПараметрыЗапуска.РабочийКаталог = РабочийКаталог; + ПараметрыЗапуска.ПутьКОтчетуGenericCoverage = ПутьКОтчетуПокрытия; + + Возврат _Запускатель.ВыполнитьТестирование(Детальки, _Лог, ПараметрыЗапуска); + +КонецФункции + +// Выполняет обнаружение тестов дочерним прогоном +// +// Параметры: +// Детальки - см. ДеталькиФильтров - Детальки фильтров прогона +// РабочийКаталог - Строка - Каталог проекта, по умолчанию текущий каталог +// +// Возвращаемое значение: +// Структура - Результат прогона: +// * КодВозврата - Число - Код возврата дочернего процесса +// * Вывод - Строка - Захваченный вывод прогона +// +Функция ВыполнитьОбнаружение(Детальки, Знач РабочийКаталог) Экспорт + + ПараметрыЗапуска = _Запускатель.ПараметрыЗапуска(); + ПараметрыЗапуска.РабочийКаталог = РабочийКаталог; + + Возврат _Запускатель.ВыполнитьОбнаружение( + Детальки, + РежимыВыводаЛога.ПлоскийСписок, + _Лог, + ПараметрыЗапуска + ); + +КонецФункции + +// Извлекает из JSONL-вывода прогона значения указанного поля событий лога +// +// Параметры: +// Вывод - Строка - Захваченный вывод дочернего прогона +// ИмяПоля - Строка - Имя поля события лога, например "ИмяТеста" +// +// Возвращаемое значение: +// Массив из Строка - Непустые значения поля в порядке вывода +// +Функция ЗначенияПоляЛога(Знач Вывод, Знач ИмяПоля) Экспорт + + Возврат ПроцессорыКоллекций.ИзКоллекции(СтрРазделить(Вывод, Символы.ПС, Ложь)) + .Обработать(Новый Действие(ЭтотОбъект, "ПоляСобытияЛога")) + .Обработать("Поля -> Поля.Получить(ИмяПоля)", Новый Структура("ИмяПоля", ИмяПоля)) + .Фильтровать("Значение -> ЗначениеЗаполнено(Значение)") + .ВМассив(); + +КонецФункции + +// Читает JSON-отчет прогона (РепортерJSON) +// +// Параметры: +// ПутьКОтчету - Строка - Путь к файлу отчета +// +// Возвращаемое значение: +// Структура - Содержимое отчета: +// * statistics - Структура - Статистика прогона: +// ** total - Число - Тестов обнаружено +// ** passed - Число - Тестов успешно +// ** failed - Число - Тестов с ошибками утверждений +// ** broken - Число - Тестов сломано +// ** skipped - Число - Тестов пропущено +// ** aborted - Число - Тестов прервано +// ** duration_ms - Число - Длительность прогона в миллисекундах +// * problems - Массив из Структура - Упавшие и сломанные тесты и наборы: +// ** status - Строка - Статус: "failed" или "broken" +// ** suite - Строка - Имя тестового набора +// ** test - Строка - Имя теста, отсутствует для проблемы всего набора +// ** message - Строка - Краткие причины +// ** details - Строка - Подробные причины со стеками +// * skipped_tests - Массив из Структура - Пропущенные тесты и наборы: +// ** status - Строка - Статус: "skipped" или "aborted" +// ** suite - Строка - Имя тестового набора +// ** test - Строка - Имя теста, отсутствует для пропуска всего набора +// ** reason - Строка - Причины пропуска +// Неопределено - Файл отчета не существует или не разобран (прогон не состоялся +// или завершился аварийно, не дописав отчет) +// +Функция ПрочитатьОтчет(Знач ПутьКОтчету) Экспорт + + Если НЕ (Новый Файл(ПутьКОтчету)).Существует() Тогда + Возврат Неопределено; + КонецЕсли; + + ЧтениеJSON = Новый ЧтениеJSON(); + ЧтениеJSON.ОткрытьФайл(ПутьКОтчету); + + Попытка + Отчет = ПрочитатьJSON(ЧтениеJSON); + Исключение + Отчет = Неопределено; + КонецПопытки; + + ЧтениеJSON.Закрыть(); + + Возврат Отчет; + +КонецФункции + +// Строит сводку покрытия по отчету в формате GenericCoverage +// +// Параметры: +// ПутьКОтчету - Строка - Путь к файлу отчета GenericCoverage +// +// Возвращаемое значение: +// Структура - Сводка покрытия: +// * percent - Число - Общий процент покрытия +// * lines_covered - Число - Количество покрытых строк +// * lines_total - Число - Количество исполняемых строк +// * files - Массив из Структура - Сводки по файлам, отсортированы по возрастанию покрытия: +// ** file - Строка - Путь к файлу +// ** percent - Число - Процент покрытия файла +// ** uncovered_lines - Массив из Число - Номера непокрытых строк, отсутствует при полном покрытии +// Неопределено - Файл отчета не существует или не разобран +// +Функция СводкаПокрытия(Знач ПутьКОтчету) Экспорт + + Если НЕ (Новый Файл(ПутьКОтчету)).Существует() Тогда + Возврат Неопределено; + КонецЕсли; + + СтрокиФайлов = Новый СписокМассив(); + + ВсегоСтрок = 0; + ПокрытоСтрок = 0; + + ЧтениеXML = Новый ЧтениеXML(); + ЧтениеXML.ОткрытьФайл(ПутьКОтчету); + + ТекущийФайл = Неопределено; + ПокрытоВФайле = 0; + + Попытка + + Пока ЧтениеXML.Прочитать() Цикл + + Если ЧтениеXML.ТипУзла <> ТипУзлаXML.НачалоЭлемента Тогда + Продолжить; + КонецЕсли; + + Если ЧтениеXML.Имя = "file" Тогда + + ЗавершитьСводкуФайла(СтрокиФайлов, ТекущийФайл, ПокрытоВФайле); + + ТекущийФайл = Новый Структура(); + ТекущийФайл.Вставить("file", ЧтениеXML.ЗначениеАтрибута("path")); + ТекущийФайл.Вставить("uncovered_lines", Новый Массив()); + + ПокрытоВФайле = 0; + + ИначеЕсли ЧтениеXML.Имя = "lineToCover" И ТекущийФайл <> Неопределено Тогда + + ВсегоСтрок = ВсегоСтрок + 1; + + Если ЧтениеXML.ЗначениеАтрибута("covered") = "true" Тогда + ПокрытоСтрок = ПокрытоСтрок + 1; + ПокрытоВФайле = ПокрытоВФайле + 1; + Иначе + ТекущийФайл.uncovered_lines.Добавить(Число(ЧтениеXML.ЗначениеАтрибута("lineNumber"))); + КонецЕсли; + + КонецЕсли; // BSLLS:IfElseIfEndsWithElse-off + + КонецЦикла; + + Исключение + ЧтениеXML.Закрыть(); + Возврат Неопределено; + КонецПопытки; + + ЧтениеXML.Закрыть(); + + ЗавершитьСводкуФайла(СтрокиФайлов, ТекущийФайл, ПокрытоВФайле); + + СтрокиФайлов.Сортировать(Новый Действие(ЭтотОбъект, "СравнитьСводкиФайлов")); + + Сводка = Новый Структура(); + Сводка.Вставить("percent", ПроцентПокрытия(ПокрытоСтрок, ВсегоСтрок)); + Сводка.Вставить("lines_covered", ПокрытоСтрок); + Сводка.Вставить("lines_total", ВсегоСтрок); + Сводка.Вставить("files", СтрокиФайлов.ВМассив()); + + Возврат Сводка; + +КонецФункции + +// Сравнивает сводки покрытия файлов: по возрастанию покрытия, +// при равном покрытии - по имени файла +// +// Параметры: +// Первая - Структура - Сводка файла, см. СводкаПокрытия +// Вторая - Структура - Сводка файла, см. СводкаПокрытия +// +// Возвращаемое значение: +// Число - 1 если первая сводка больше, -1 если вторая, 0 если равны +// +Функция СравнитьСводкиФайлов(Первая, Вторая) Экспорт + + Если Первая.percent <> Вторая.percent Тогда + Возврат ?(Первая.percent > Вторая.percent, 1, -1); + КонецЕсли; + + Если Первая.file = Вторая.file Тогда + Возврат 0; + КонецЕсли; + + Возврат ?(Первая.file > Вторая.file, 1, -1); + +КонецФункции + +Процедура ЗавершитьСводкуФайла(СтрокиФайлов, ТекущийФайл, Знач ПокрытоВФайле) + + Если ТекущийФайл = Неопределено Тогда + Возврат; + КонецЕсли; + + ТекущийФайл.Вставить("percent", + ПроцентПокрытия( + ПокрытоВФайле, + ПокрытоВФайле + ТекущийФайл.uncovered_lines.Количество() + ) + ); + + Если ТекущийФайл.uncovered_lines.Количество() = 0 Тогда + ТекущийФайл.Удалить("uncovered_lines"); + КонецЕсли; + + СтрокиФайлов.Добавить(ТекущийФайл); + +КонецПроцедуры + +// Преобразует JSONL-вывод прогона в человекочитаемый текст: у событий лога +// берется сообщение, прочие строки (например, ошибки компиляции) остаются как есть +// +// Параметры: +// Вывод - Строка - Захваченный вывод дочернего прогона +// +// Возвращаемое значение: +// Строка - Текст вывода +// +Функция ТекстВывода(Знач Вывод) Экспорт + + Возврат ПроцессорыКоллекций.ИзКоллекции(СтрРазделить(Вывод, Символы.ПС, Ложь)) + .Обработать(Новый Действие(ЭтотОбъект, "ТекстСтрокиВывода")) + .Фильтровать("Текст -> НЕ ПустаяСтрока(Текст)") + .ВСтроку(Символы.ПС); + +КонецФункции + +// Возвращает читаемый текст строки вывода прогона: у события лога берется +// сообщение, прочие строки (например, ошибки компиляции) остаются как есть +// +// Параметры: +// СтрокаВывода - Строка - Строка захваченного вывода +// +// Возвращаемое значение: +// Строка - Текст строки, пустая строка для событий лога без сообщения +// +Функция ТекстСтрокиВывода(Знач СтрокаВывода) Экспорт + + Если ПустаяСтрока(СтрокаВывода) Тогда + Возврат ""; + КонецЕсли; + + Поля = ПоляСобытияЛога(СтрокаВывода); + + Если Поля.Количество() = 0 Тогда + Возврат СокрЛП(СтрокаВывода); + КонецЕсли; + + Возврат ?(ЗначениеЗаполнено(Поля.Получить("msg")), Поля.Получить("msg"), ""); + +КонецФункции + +// Разбирает строку JSONL-вывода прогона в поля события лога +// +// Параметры: +// СтрокаВывода - Строка - Строка захваченного вывода +// +// Возвращаемое значение: +// Соответствие из КлючИЗначение - Поля события лога, +// пустое, если строка не является событием лога: +// * Ключ - Строка - Имя поля, например "msg" или "ИмяТеста" +// * Значение - Произвольный - Значение поля +// +Функция ПоляСобытияЛога(Знач СтрокаВывода) Экспорт + + СтрокаВывода = СокрЛП(СтрЗаменить(СтрокаВывода, Символ(65279), "")); + + Если НЕ СтрНачинаетсяС(СтрокаВывода, "{") Тогда + Возврат Новый Соответствие(); + КонецЕсли; + + Попытка + + ЧтениеJSON = Новый ЧтениеJSON(); + ЧтениеJSON.УстановитьСтроку(СтрокаВывода); + + Возврат ПрочитатьJSON(ЧтениеJSON, Истина); + + Исключение + Возврат Новый Соответствие(); + КонецПопытки; + +КонецФункции + +Функция ПроцентПокрытия(Знач Покрыто, Знач Всего) + + Если Всего = 0 Тогда + Возврат 100; + КонецЕсли; + + Возврат Окр(Покрыто / Всего * 100, 1); + +КонецФункции + +// Нормализует значение параметра инструмента, пришедшее из JSON MCP-клиента: +// клиент может не передать параметр вовсе, прислать пустые или нестроковые +// элементы вопреки схеме +// +// Параметры: +// Значения - Массив, Неопределено - Значение параметра инструмента как есть +// +// Возвращаемое значение: +// Массив из Строка - Непустые значения, приведенные к строкам +// +Функция МассивЗначений(Знач Значения) + + Если Значения = Неопределено Тогда + Возврат Новый Массив(); + КонецЕсли; + + Возврат ПроцессорыКоллекций.ИзКоллекции(Значения) + .Фильтровать("Значение -> ЗначениеЗаполнено(Значение)") + .Обработать("Значение -> Строка(Значение)") + .ВМассив(); + +КонецФункции + +&Желудь +Процедура ПриСозданииОбъекта( + &Пластилин ЗапускательТестирования, + &Лог("oscript.lib.oneunit.mcp") Лог) + + _Запускатель = ЗапускательТестирования; + _Лог = Лог; + +КонецПроцедуры diff --git a/src/mcp/main.os b/src/mcp/main.os new file mode 100644 index 0000000..aab623b --- /dev/null +++ b/src/mcp/main.os @@ -0,0 +1,28 @@ +// Точка входа MCP-сервера OneUnit (stdio). +// +// Загружается командой `oneunit mcp` через ЗагрузитьСценарий или запускается +// напрямую: oscript src/mcp/main.os. Сервер собирается ОСенью: autumn-mcp +// находит в поделке желуди с аннотацией &Инструмент и публикует их MCP-клиенту. +// +// Сервер намеренно собирается отдельной поделкой с выключенным автозапуском +// рогаток: рогатки ОСени исполняются при старте любой поделки, поэтому +// MCP_Рогатка (autumn-mcp) не может сосуществовать с рогаткой консольного +// приложения (autumn-cli) - рогатка сервера запускается вручную. + +#Использовать autumn +#Использовать autumn-mcp +#Использовать logos + +#Использовать "." + +// stdout занят протоколом MCP - информационные логи старта поделки глушатся +Логирование.ПолучитьЛог("oscript.lib.autumn.ЗапускательПриложения").УстановитьУровень(УровниЛога.Предупреждение); +Логирование.ПолучитьЛог("oscript.lib.autumn.application.context").УстановитьУровень(УровниЛога.Предупреждение); + +Детальки = Новый Соответствие(); +Детальки.Вставить("core.ЗапускатьРогатки", Ложь); + +Поделка = Новый Поделка(Новый СоветДругогоМастера().ЗначенияДеталек(Детальки)); +Поделка.ЗапуститьПриложение(); + +Поделка.НайтиЖелудь("MCP_Рогатка").ПриЗапускеПриложения(); diff --git "a/src/mcp/\320\232\320\273\320\260\321\201\321\201\321\213/\320\230\320\275\321\201\321\202\321\200\321\203\320\274\320\265\320\275\321\202\320\222\321\213\320\277\320\276\320\273\320\275\320\270\321\202\321\214\320\242\320\265\321\201\321\202\321\213.os" "b/src/mcp/\320\232\320\273\320\260\321\201\321\201\321\213/\320\230\320\275\321\201\321\202\321\200\321\203\320\274\320\265\320\275\321\202\320\222\321\213\320\277\320\276\320\273\320\275\320\270\321\202\321\214\320\242\320\265\321\201\321\202\321\213.os" new file mode 100644 index 0000000..81b0c3f --- /dev/null +++ "b/src/mcp/\320\232\320\273\320\260\321\201\321\201\321\213/\320\230\320\275\321\201\321\202\321\200\321\203\320\274\320\265\320\275\321\202\320\222\321\213\320\277\320\276\320\273\320\275\320\270\321\202\321\214\320\242\320\265\321\201\321\202\321\213.os" @@ -0,0 +1,198 @@ +// MCP-инструмент запуска тестов: выполняет прогон дочерним процессом через +// точку входа ядра и возвращает структурированный результат из JSON-отчета +// прогона (РепортерJSON). + +#Использовать tempfiles + +#Использовать "../internal" + +&ПараметрИнструмента( + Имя = "directories", + Описание = "Каталоги поиска тестовых наборов. Если не заданы ни каталоги, ни файлы, используется ./tests", + Тип = "string", + ЭтоМассив = Истина +) +Перем Каталоги; + +&ПараметрИнструмента( + Имя = "files", + Описание = "Пути к отдельным файлам тестовых наборов", + Тип = "string", + ЭтоМассив = Истина +) +Перем Файлы; + +&ПараметрИнструмента( + Имя = "recursive", + Описание = "Искать тестовые наборы в подкаталогах", + Тип = "boolean" +) +Перем ИскатьВПодкаталогах; + +&ПараметрИнструмента( + Имя = "tags_include", + Описание = "Выполнять только тесты с указанными тегами", + Тип = "string", + ЭтоМассив = Истина +) +Перем ТегиВключаемые; + +&ПараметрИнструмента( + Имя = "tags_exclude", + Описание = "Не выполнять тесты с указанными тегами", + Тип = "string", + ЭтоМассив = Истина +) +Перем ТегиИсключаемые; + +&ПараметрИнструмента( + Имя = "suites_include", + Описание = "Выполнять только тестовые наборы с указанными именами", + Тип = "string", + ЭтоМассив = Истина +) +Перем НаборыВключаемые; + +&ПараметрИнструмента( + Имя = "suites_exclude", + Описание = "Не выполнять тестовые наборы с указанными именами", + Тип = "string", + ЭтоМассив = Истина +) +Перем НаборыИсключаемые; + +&ПараметрИнструмента( + Имя = "methods_include", + Описание = "Выполнять только тестовые методы с указанными именами", + Тип = "string", + ЭтоМассив = Истина +) +Перем МетодыВключаемые; + +&ПараметрИнструмента( + Имя = "methods_exclude", + Описание = "Не выполнять тестовые методы с указанными именами", + Тип = "string", + ЭтоМассив = Истина +) +Перем МетодыИсключаемые; + +&ПараметрИнструмента( + Имя = "timeout", + Описание = "Таймаут выполнения каждого теста в миллисекундах", + Тип = "integer" +) +Перем Таймаут; + +&ПараметрИнструмента( + Имя = "working_directory", + Описание = "Каталог проекта, из которого запускаются тесты. По умолчанию - текущий каталог MCP-сервера", + Тип = "string" +) +Перем РабочийКаталог; + +&ПараметрИнструмента( + Имя = "coverage", + Описание = "Собрать покрытие кода тестами и вернуть сводку: общий процент и непокрытые строки по файлам", + Тип = "boolean" +) +Перем СобиратьПокрытие; + +Перем _Служебный; // MCPИнструментыСлужебный + +&Инструмент( + Имя = "run_tests", + Описание = "Запускает тесты OneUnit (фреймворк тестирования OneScript) и возвращает структурированный + |результат прогона: статистику, проблемы (упавшие и сломанные тесты с сообщениями об ошибках + |и стеками) и пропущенные тесты с причинами" +) +Процедура ПриСозданииОбъекта(&Пластилин MCPИнструментыСлужебный) + _Служебный = MCPИнструментыСлужебный; +КонецПроцедуры + +// Подсказки MCP-клиенту о поведении инструмента +// +// Возвращаемое значение: +// Структура - Блок annotations протокола MCP +// +Функция АннотацииИнструмента() Экспорт + + Аннотации = Новый Структура(); + Аннотации.Вставить("title", "Запуск тестов OneUnit"); + Аннотации.Вставить("openWorldHint", Ложь); + + Возврат Аннотации; + +КонецФункции + +&ВыполнениеИнструмента +Функция ВыполнитьТесты() Экспорт + + МенеджерВременныхФайлов = Новый МенеджерВременныхФайлов(); + ПутьКОтчету = МенеджерВременныхФайлов.НовоеИмяФайла("json"); + + ПутьКОтчетуПокрытия = ""; + Если СобиратьПокрытие = Истина Тогда + ПутьКОтчетуПокрытия = МенеджерВременныхФайлов.НовоеИмяФайла("xml"); + КонецЕсли; + + Попытка + + Детальки = _Служебный.ДеталькиФильтров( + Каталоги, + Файлы, + ИскатьВПодкаталогах, + ТегиВключаемые, + ТегиИсключаемые, + НаборыВключаемые, + НаборыИсключаемые, + МетодыВключаемые, + МетодыИсключаемые + ); + + РезультатПрогона = _Служебный.ВыполнитьТестирование( + Детальки, + Таймаут, + ПутьКОтчету, + РабочийКаталог, + ПутьКОтчетуПокрытия + ); + + Отчет = _Служебный.ПрочитатьОтчет(ПутьКОтчету); + + Результат = Новый Структура(); + Результат.Вставить("success", РезультатПрогона.КодВозврата = 0); + Результат.Вставить("exit_code", РезультатПрогона.КодВозврата); + + Если Отчет = Неопределено Тогда + // Прогон не состоялся (например, ошибка компиляции тестов) - + // отчета нет, для диагностики возвращается вывод консоли + Результат.Вставить("console_output", _Служебный.ТекстВывода(РезультатПрогона.Вывод)); + Иначе + Результат.Вставить("statistics", Отчет.statistics); + Результат.Вставить("problems", Отчет.problems); + Результат.Вставить("skipped_tests", Отчет.skipped_tests); + КонецЕсли; + + Если ЗначениеЗаполнено(ПутьКОтчетуПокрытия) Тогда + + СводкаПокрытия = _Служебный.СводкаПокрытия(ПутьКОтчетуПокрытия); + + Если СводкаПокрытия <> Неопределено Тогда + Результат.Вставить("coverage", СводкаПокрытия); + КонецЕсли; + + КонецЕсли; + + Исключение + // Сервер живет дольше одного вызова - временные файлы + // не должны копиться при ошибках прогона + МенеджерВременныхФайлов.Удалить(); + ВызватьИсключение; + КонецПопытки; + + МенеджерВременныхФайлов.Удалить(); + + Возврат Результат; + +КонецФункции diff --git "a/src/mcp/\320\232\320\273\320\260\321\201\321\201\321\213/\320\230\320\275\321\201\321\202\321\200\321\203\320\274\320\265\320\275\321\202\320\236\320\261\320\275\320\260\321\200\321\203\320\266\320\270\321\202\321\214\320\242\320\265\321\201\321\202\321\213.os" "b/src/mcp/\320\232\320\273\320\260\321\201\321\201\321\213/\320\230\320\275\321\201\321\202\321\200\321\203\320\274\320\265\320\275\321\202\320\236\320\261\320\275\320\260\321\200\321\203\320\266\320\270\321\202\321\214\320\242\320\265\321\201\321\202\321\213.os" new file mode 100644 index 0000000..f2a0fc7 --- /dev/null +++ "b/src/mcp/\320\232\320\273\320\260\321\201\321\201\321\213/\320\230\320\275\321\201\321\202\321\200\321\203\320\274\320\265\320\275\321\202\320\236\320\261\320\275\320\260\321\200\321\203\320\266\320\270\321\202\321\214\320\242\320\265\321\201\321\202\321\213.os" @@ -0,0 +1,148 @@ +// MCP-инструмент обнаружения тестов: находит тесты дочерним прогоном без +// их выполнения и возвращает список полных имен тестов. + +#Использовать "../internal" + +&ПараметрИнструмента( + Имя = "directories", + Описание = "Каталоги поиска тестовых наборов. Если не заданы ни каталоги, ни файлы, используется ./tests", + Тип = "string", + ЭтоМассив = Истина +) +Перем Каталоги; + +&ПараметрИнструмента( + Имя = "files", + Описание = "Пути к отдельным файлам тестовых наборов", + Тип = "string", + ЭтоМассив = Истина +) +Перем Файлы; + +&ПараметрИнструмента( + Имя = "recursive", + Описание = "Искать тестовые наборы в подкаталогах", + Тип = "boolean" +) +Перем ИскатьВПодкаталогах; + +&ПараметрИнструмента( + Имя = "tags_include", + Описание = "Обнаруживать только тесты с указанными тегами", + Тип = "string", + ЭтоМассив = Истина +) +Перем ТегиВключаемые; + +&ПараметрИнструмента( + Имя = "tags_exclude", + Описание = "Не обнаруживать тесты с указанными тегами", + Тип = "string", + ЭтоМассив = Истина +) +Перем ТегиИсключаемые; + +&ПараметрИнструмента( + Имя = "suites_include", + Описание = "Обнаруживать только тестовые наборы с указанными именами", + Тип = "string", + ЭтоМассив = Истина +) +Перем НаборыВключаемые; + +&ПараметрИнструмента( + Имя = "suites_exclude", + Описание = "Не обнаруживать тестовые наборы с указанными именами", + Тип = "string", + ЭтоМассив = Истина +) +Перем НаборыИсключаемые; + +&ПараметрИнструмента( + Имя = "methods_include", + Описание = "Обнаруживать только тестовые методы с указанными именами", + Тип = "string", + ЭтоМассив = Истина +) +Перем МетодыВключаемые; + +&ПараметрИнструмента( + Имя = "methods_exclude", + Описание = "Не обнаруживать тестовые методы с указанными именами", + Тип = "string", + ЭтоМассив = Истина +) +Перем МетодыИсключаемые; + +&ПараметрИнструмента( + Имя = "working_directory", + Описание = "Каталог проекта, в котором обнаруживаются тесты. По умолчанию - текущий каталог MCP-сервера", + Тип = "string" +) +Перем РабочийКаталог; + +Перем _Служебный; // MCPИнструментыСлужебный + +&Инструмент( + Имя = "discover_tests", + Описание = "Обнаруживает тесты OneUnit (фреймворк тестирования OneScript) без их выполнения + |и возвращает список полных имен тестов" +) +Процедура ПриСозданииОбъекта(&Пластилин MCPИнструментыСлужебный) + _Служебный = MCPИнструментыСлужебный; +КонецПроцедуры + +// Подсказки MCP-клиенту о поведении инструмента +// +// Возвращаемое значение: +// Структура - Блок annotations протокола MCP +// +Функция АннотацииИнструмента() Экспорт + + Аннотации = Новый Структура(); + Аннотации.Вставить("title", "Обнаружение тестов OneUnit"); + Аннотации.Вставить("readOnlyHint", Истина); + Аннотации.Вставить("openWorldHint", Ложь); + + Возврат Аннотации; + +КонецФункции + +&ВыполнениеИнструмента +Функция ОбнаружитьТесты() Экспорт + + Детальки = _Служебный.ДеталькиФильтров( + Каталоги, + Файлы, + ИскатьВПодкаталогах, + ТегиВключаемые, + ТегиИсключаемые, + НаборыВключаемые, + НаборыИсключаемые, + МетодыВключаемые, + МетодыИсключаемые + ); + + РезультатПрогона = _Служебный.ВыполнитьОбнаружение( + Детальки, + РабочийКаталог + ); + + Результат = Новый Структура(); + Результат.Вставить("success", РезультатПрогона.КодВозврата = 0); + Результат.Вставить("exit_code", РезультатПрогона.КодВозврата); + + Если РезультатПрогона.КодВозврата = 0 Тогда + + Тесты = _Служебный.ЗначенияПоляЛога(РезультатПрогона.Вывод, "ИмяТеста"); + + Результат.Вставить("count", Тесты.Количество()); + Результат.Вставить("tests", Тесты); + + Иначе + Результат.Вставить("console_output", _Служебный.ТекстВывода(РезультатПрогона.Вывод)); + КонецЕсли; + + Возврат Результат; + +КонецФункции diff --git "a/tests/MCP\320\230\320\275\321\201\321\202\321\200\321\203\320\274\320\265\320\275\321\202\321\213.os" "b/tests/MCP\320\230\320\275\321\201\321\202\321\200\321\203\320\274\320\265\320\275\321\202\321\213.os" new file mode 100644 index 0000000..03bfbc2 --- /dev/null +++ "b/tests/MCP\320\230\320\275\321\201\321\202\321\200\321\203\320\274\320\265\320\275\321\202\321\213.os" @@ -0,0 +1,586 @@ +// Тесты MCP-инструментов (src/mcp): сборка деталек прогона, разбор +// JSONL-вывода и чтение JSON-отчета прогона (РепортерJSON). +// +// Внимание: у этого сценария намеренно нет конструктора с аннотацией &ТестовыйНабор, +// см. пояснение в Исполнитель.os + +#Использовать asserts +#Использовать tempfiles +#Использовать collectionos +#Использовать logos +#Использовать "./support" +#Использовать "../src/cli/internal" +#Использовать "../src/mcp/internal" + +&ПослеВсех +Процедура УдалитьВременныеФайлы() Экспорт + ВременныеФайлы.Удалить(); +КонецПроцедуры + +#Область ТестыДеталекФильтров + +&Тест +Процедура ДеталькиФильтровЗаполняютсяПереданнымиЗначениями() Экспорт + + Каталоги = Новый Массив; + Каталоги.Добавить("./tests"); + Каталоги.Добавить("./more"); + + Теги = Новый Массив; + Теги.Добавить("smoke"); + + Детальки = Служебный().ДеталькиФильтров( + Каталоги, + Неопределено, + Истина, + Теги, + Неопределено, + Неопределено, + Неопределено, + Неопределено, + Неопределено + ); + + Ожидаем.Что(Детальки.Получить("КаталогиТестов")).ИмеетДлину(2); + Ожидаем.Что(Детальки.Получить("КаталогиТестов")[1]).Равно("./more"); + Ожидаем.Что(Детальки.Получить("ФайлыТестов")).ИмеетДлину(0); + Ожидаем.Что(Детальки.Получить("ИскатьВПодкаталогах")).ЭтоИстина(); + Ожидаем.Что(Детальки.Получить("ТегиВключаемые")).ИмеетДлину(1); + Ожидаем.Что(Детальки.Получить("ТестовыеНаборыВключаемые")).ИмеетДлину(0); + +КонецПроцедуры + +&Тест +Процедура ДеталькиФильтровРазличаютПозиционныеПараметры() Экспорт + + Детальки = Служебный().ДеталькиФильтров( + Массивы.ИзЭлементов("каталог"), + Массивы.ИзЭлементов("файл.os"), + Истина, + Массивы.ИзЭлементов("тег-вкл"), + Массивы.ИзЭлементов("тег-искл"), + Массивы.ИзЭлементов("набор-вкл"), + Массивы.ИзЭлементов("набор-искл"), + Массивы.ИзЭлементов("метод-вкл"), + Массивы.ИзЭлементов("метод-искл") + ); + + Ожидаем.Что(Детальки.Получить("КаталогиТестов")[0]).Равно("каталог"); + Ожидаем.Что(Детальки.Получить("ФайлыТестов")[0]).Равно("файл.os"); + Ожидаем.Что(Детальки.Получить("ИскатьВПодкаталогах")).ЭтоИстина(); + Ожидаем.Что(Детальки.Получить("ТегиВключаемые")[0]).Равно("тег-вкл"); + Ожидаем.Что(Детальки.Получить("ТегиИсключаемые")[0]).Равно("тег-искл"); + Ожидаем.Что(Детальки.Получить("ТестовыеНаборыВключаемые")[0]).Равно("набор-вкл"); + Ожидаем.Что(Детальки.Получить("ТестовыеНаборыИсключаемые")[0]).Равно("набор-искл"); + Ожидаем.Что(Детальки.Получить("ТестовыеМетодыВключаемые")[0]).Равно("метод-вкл"); + Ожидаем.Что(Детальки.Получить("ТестовыеМетодыИсключаемые")[0]).Равно("метод-искл"); + +КонецПроцедуры + +&Тест +Процедура ПризнакПоискаВПодкаталогахПоУмолчаниюЛожь() Экспорт + + Детальки = Служебный().ДеталькиФильтров( + Неопределено, + Неопределено, + Неопределено, + Неопределено, + Неопределено, + Неопределено, + Неопределено, + Неопределено, + Неопределено + ); + + Ожидаем.Что(Детальки.Получить("ИскатьВПодкаталогах")).ЭтоЛожь(); + +КонецПроцедуры + +&Тест +Процедура ПустыеЗначенияФильтровОтбрасываются() Экспорт + + Файлы = Новый Массив; + Файлы.Добавить(""); + Файлы.Добавить("./tests/Тест.os"); + Файлы.Добавить(Неопределено); + + Детальки = Служебный().ДеталькиФильтров( + Неопределено, + Файлы, + Ложь, + Неопределено, + Неопределено, + Неопределено, + Неопределено, + Неопределено, + Неопределено + ); + + Ожидаем.Что(Детальки.Получить("ФайлыТестов")).ИмеетДлину(1); + Ожидаем.Что(Детальки.Получить("ФайлыТестов")[0]).Равно("./tests/Тест.os"); + +КонецПроцедуры + +#КонецОбласти + +#Область ТестыРазбораВыводаПрогона + +&Тест +Процедура ЗначенияПоляЛогаИзвлекаютсяИзJSONLВывода() Экспорт + + Вывод = "{""msg"":"""",""ИмяТеста"":""Набор.Тест1""}" + Символы.ПС + + "строка не в формате JSON" + Символы.ПС + + "{""msg"":""сообщение без имени теста""}" + Символы.ПС + + "{""msg"":"""",""ИмяТеста"":""Набор.Тест2""}"; + + Тесты = Служебный().ЗначенияПоляЛога(Вывод, "ИмяТеста"); + + Ожидаем.Что(Тесты).ИмеетДлину(2); + Ожидаем.Что(Тесты[0]).Равно("Набор.Тест1"); + Ожидаем.Что(Тесты[1]).Равно("Набор.Тест2"); + +КонецПроцедуры + +&Тест +Процедура ТекстВыводаСодержитСообщенияЛогаИПрочиеСтроки() Экспорт + + Вывод = "{""msg"":""Сообщение лога"",""level"":""INFO""}" + Символы.ПС + + "Ошибка компиляции сценария" + Символы.ПС + + " " + Символы.ПС + + "{""msg"":""""}"; + + Текст = Служебный().ТекстВывода(Вывод); + + Ожидаем.Что(Текст).Равно("Сообщение лога" + Символы.ПС + "Ошибка компиляции сценария"); + +КонецПроцедуры + +&Тест +Процедура СтрокаПохожаяНаJSONНоБитаяПропускается() Экспорт + + Вывод = "{битая строка не в формате JSON" + Символы.ПС + + "{""msg"":"""",""ИмяТеста"":""Набор.Тест""}"; + + Тесты = Служебный().ЗначенияПоляЛога(Вывод, "ИмяТеста"); + + Ожидаем.Что(Тесты).ИмеетДлину(1); + Ожидаем.Что(Тесты[0]).Равно("Набор.Тест"); + +КонецПроцедуры + +#КонецОбласти + +#Область ТестыВыполненияПрогонов + +&Тест +Процедура ТестированиеФормируетОтчетJSON() Экспорт + + ПутьКОтчету = ВременныеФайлы.НовоеИмяФайла("json"); + + Детальки = Служебный().ДеталькиФильтров( + Неопределено, + Массивы.ИзЭлементов("./tests/fixtures/Тест.os"), + Ложь, + Неопределено, + Неопределено, + Неопределено, + Неопределено, + Неопределено, + Неопределено + ); + + Результат = Служебный().ВыполнитьТестирование(Детальки, 0, ПутьКОтчету, Неопределено); + + Ожидаем.Что(Результат.КодВозврата).Равно(0); + + Отчет = Служебный().ПрочитатьОтчет(ПутьКОтчету); + + Ожидаем.Что(Отчет).Не_().ЭтоНеопределено(); + Ожидаем.Что(Отчет.statistics.total).Равно(2); + +КонецПроцедуры + +&Тест +Процедура ОбнаружениеВозвращаетИменаТестовВВыводе() Экспорт + + Детальки = Служебный().ДеталькиФильтров( + Неопределено, + Массивы.ИзЭлементов("./tests/fixtures/Тест.os"), + Ложь, + Неопределено, + Неопределено, + Неопределено, + Неопределено, + Неопределено, + Неопределено + ); + + Результат = Служебный().ВыполнитьОбнаружение(Детальки, Неопределено); + + Ожидаем.Что(Результат.КодВозврата).Равно(0); + + Тесты = Служебный().ЗначенияПоляЛога(Результат.Вывод, "ИмяТеста"); + + Ожидаем.Что(Тесты).ИмеетДлину(2); + Ожидаем.Что(Тесты[0]).Равно("Тест.Тест1"); + +КонецПроцедуры + +#КонецОбласти + +#Область ТестыОтчетаJSON + +&Тест +Процедура БезФайлаОтчетаВозвращаетсяНеопределено() Экспорт + + Отчет = Служебный().ПрочитатьОтчет(ВременныеФайлы.НовоеИмяФайла("json")); + + Ожидаем.Что(Отчет).ЭтоНеопределено(); + +КонецПроцедуры + +&Тест +Процедура НедописанныйФайлОтчетаВозвращаетсяКакНеопределено() Экспорт + + // Аварийно завершившийся прогон может оставить недописанный отчет - + // вместо исключения инструмент откатывается к консольному выводу + Ожидаем.Что(Служебный().ПрочитатьОтчет(ЗаписатьФайл("json", "{""statistics"": {""tot"))).ЭтоНеопределено(); + +КонецПроцедуры + +&Тест +Процедура СтатистикаЧитаетсяИзОтчета() Экспорт + + Статистика = Отчет().statistics; + + Ожидаем.Что(Статистика.total).Равно(4); + Ожидаем.Что(Статистика.passed).Равно(2); + Ожидаем.Что(Статистика.failed).Равно(1); + Ожидаем.Что(Статистика.broken).Равно(1); + Ожидаем.Что(Статистика.skipped).Равно(0); + Ожидаем.Что(Статистика.aborted).Равно(0); + +КонецПроцедуры + +&Тест +Процедура УпавшийТестПопадаетВПроблемыСПричиной() Экспорт + + Проблема = Проблема(Отчет(), "СостояниеОшибка"); + + Ожидаем.Что(Проблема).Не_().ЭтоНеопределено(); + Ожидаем.Что(Проблема.status).Равно("failed"); + Ожидаем.Что(Проблема.suite).Равно("НаборКрасныхТестов"); + Ожидаем.Что(СтрНайти(Проблема.message, "Ошибка утверждения")).Больше(0); + Ожидаем.Что(СтрНайти(Проблема.details, "НаборКрасныхТестов.os")).Больше(0); + +КонецПроцедуры + +&Тест +Процедура СломанныйТестПопадаетВПроблемыСоСтатусомBroken() Экспорт + + Проблема = Проблема(Отчет(), "СостояниеСломан"); + + Ожидаем.Что(Проблема).Не_().ЭтоНеопределено(); + Ожидаем.Что(Проблема.status).Равно("broken"); + Ожидаем.Что(СтрНайти(Проблема.message, "Сломано")).Больше(0); + +КонецПроцедуры + +&Тест +Процедура СломанныйНаборПопадаетВПроблемыБезИмениТеста() Экспорт + + Проблема = ПроблемаНабора(Отчет(), "НаборПадающийВКонструкторе"); + + Ожидаем.Что(Проблема).Не_().ЭтоНеопределено(); + Ожидаем.Что(Проблема.status).Равно("broken"); + Ожидаем.Что(Проблема.Свойство("test")).ЭтоЛожь(); + Ожидаем.Что(СтрНайти(Проблема.details, "Не удаётся инстанцировать")).Больше(0); + +КонецПроцедуры + +&Тест +Процедура ВыключенныйТестПопадаетВПропущенные() Экспорт + + Пропуск = Пропуск(ОтчетСПропусками(), "НаборСВыключеннымиТестами", "Выключен"); + + Ожидаем.Что(Пропуск).Не_().ЭтоНеопределено(); + Ожидаем.Что(Пропуск.status).Равно("skipped"); + Ожидаем.Что(СтрНайти(Пропуск.reason, "На мне аннотация Выключен")).Больше(0); + +КонецПроцедуры + +&Тест +Процедура ВыключенныйНаборПопадаетВПропущенныеЦеликом() Экспорт + + Пропуск = Пропуск(ОтчетСПропусками(), "НаборВыключенный", Неопределено); + + Ожидаем.Что(Пропуск).Не_().ЭтоНеопределено(); + Ожидаем.Что(Пропуск.status).Равно("skipped"); + +КонецПроцедуры + +&Тест +Процедура ПрерванныеТестыИКонтейнерыПолучаютСтатусAborted() Экспорт + + Отчет = ОтчетСПропусками(); + + // Одиночный прерванный тест + Пропуск = Пропуск(Отчет, "НаборСПрерваннымиТестами", "ПрерванныйТест"); + + Ожидаем.Что(Пропуск).Не_().ЭтоНеопределено(); + Ожидаем.Что(Пропуск.status).Равно("aborted"); + Ожидаем.Что(СтрНайти(Пропуск.reason, "Я прервался")).Больше(0); + + // Итерация прерванного повторяемого теста разворачивается в отдельный тест, + // набором указывается ближайший тестовый набор, а не контейнер итераций + ПропускИтерации = Пропуск(Отчет, "НаборСПрерваннымиТестами", "Повторение 3 из 4"); + + Ожидаем.Что(ПропускИтерации).Не_().ЭтоНеопределено(); + Ожидаем.Что(ПропускИтерации.status).Равно("aborted"); + +КонецПроцедуры + +&Тест +Процедура УспешныеКонтейнерыНеПопадаютВОтчет() Экспорт + + Отчет = ОтчетСПропусками(); + + Ожидаем.Что(Отчет.problems).ИмеетДлину(0); + + Для Каждого Пропуск Из Отчет.skipped_tests Цикл + Ожидаем.Что(СтрНайти(Пропуск.suite, "ПовторяемыйТест")).Равно(0); + КонецЦикла; + +КонецПроцедуры + +&Тест +Процедура РепортерБезПутиКОтчетуНичегоНеПишет() Экспорт + + // Поделка без прогона: репортер еще не создан рогаткой тестирования + Детальки = Новый Соответствие; + Детальки.Вставить("OneUnit.КаталогиТестов", ""); + Детальки.Вставить("OneUnit.ФайлыТестов", ""); + + Поделка = ПрогоныТестов.СоздатьПоделку(Детальки); + + Репортер = Поделка.НайтиЖелудь("РепортерJSON", Массивы.ИзЭлементов("")); + + // Репортер без пути к отчету игнорирует событие, не обращаясь к тест-плану + Репортер.ИсполнениеТестПланКонец(Неопределено); + + Ожидаем.Что(Истина).ЭтоИстина(); + +КонецПроцедуры + +#КонецОбласти + +#Область ТестыСводкиПокрытия + +&Тест +Процедура БезФайлаПокрытияВозвращаетсяНеопределено() Экспорт + + Сводка = Служебный().СводкаПокрытия(ВременныеФайлы.НовоеИмяФайла("xml")); + + Ожидаем.Что(Сводка).ЭтоНеопределено(); + +КонецПроцедуры + +&Тест +Процедура БитыйФайлПокрытияВозвращаетсяКакНеопределено() Экспорт + + Ожидаем.Что(Служебный().СводкаПокрытия(ЗаписатьФайл("xml", ""); + ЗаписьТекста.Закрыть(); + + Сводка = Служебный().СводкаПокрытия(ПутьКОтчету); + + Ожидаем.Что(Сводка.percent).Равно(100); + Ожидаем.Что(Сводка.lines_total).Равно(0); + Ожидаем.Что(Сводка.files).ИмеетДлину(0); + +КонецПроцедуры + +&Тест +Процедура СводкаПокрытияСчитаетПроцентыИНепокрытыеСтроки() Экспорт + + ПутьКОтчету = ВременныеФайлы.НовоеИмяФайла("xml"); + + ТекстОтчета = + " + | + | + | + | + | + | + | + | + |"; + + ЗаписьТекста = Новый ЗаписьТекста(ПутьКОтчету); + ЗаписьТекста.Записать(ТекстОтчета); + ЗаписьТекста.Закрыть(); + + Сводка = Служебный().СводкаПокрытия(ПутьКОтчету); + + Ожидаем.Что(Сводка.lines_total).Равно(4); + Ожидаем.Что(Сводка.lines_covered).Равно(3); + Ожидаем.Что(Сводка.percent).Равно(75); + + Файлы = Сводка.files; + + Ожидаем.Что(Файлы).ИмеетДлину(2); + + // Наименее покрытый файл - первый + Ожидаем.Что(Файлы[0].file).Равно("src/Наполовину.os"); + Ожидаем.Что(Файлы[0].percent).Равно(50); + Ожидаем.Что(Файлы[0].uncovered_lines).ИмеетДлину(1); + Ожидаем.Что(Файлы[0].uncovered_lines[0]).Равно(7); + + Ожидаем.Что(Файлы[1].file).Равно("src/Полностью.os"); + Ожидаем.Что(Файлы[1].percent).Равно(100); + Ожидаем.Что(Файлы[1].Свойство("uncovered_lines")).ЭтоЛожь(); + +КонецПроцедуры + +#КонецОбласти + +#Область СлужебныеМетоды + +Функция Служебный() + + // Вне поделки MCP-сервера желудь собирается вручную + Возврат Новый MCPИнструментыСлужебный( + Новый ЗапускательТестирования(Новый КонсольноеПриложениеТестированиеСлужебный()), + Логирование.ПолучитьЛог("oscript.lib.oneunit.тесты") + ); + +КонецФункции + +Функция Отчет() + + ПутьКОтчету = ПрогоныТестов.ВременныйПуть("MCPОтчетJSON", "json"); + + ДопДетальки = Новый Соответствие; + ДопДетальки.Вставить("OneUnit.ПутьКОтчетуJSON", ПутьКОтчету); + + ФайлыТестов = Новый Массив; + ФайлыТестов.Добавить("./tests/fixtures/Тест.os"); + ФайлыТестов.Добавить("./tests/fixtures/НаборКрасныхТестов.os"); + ФайлыТестов.Добавить("./tests/fixtures/НаборПадающийВКонструкторе.os"); + + ПрогоныТестов.ПрогнатьОднажды( + "MCPОтчетJSON", + "Тест,НаборКрасныхТестов,НаборПадающийВКонструкторе", + "", + СтрСоединить(ФайлыТестов, ","), + ДопДетальки + ); + + Возврат Служебный().ПрочитатьОтчет(ПутьКОтчету); + +КонецФункции + +Функция ПрогонСПропусками() + + ДопДетальки = Новый Соответствие; + ДопДетальки.Вставить( + "OneUnit.ПутьКОтчетуJSON", + ПрогоныТестов.ВременныйПуть("MCPОтчетJSONПропуски", "json") + ); + + ФайлыТестов = Новый Массив; + ФайлыТестов.Добавить("./tests/fixtures/НаборСВыключеннымиТестами.os"); + ФайлыТестов.Добавить("./tests/fixtures/НаборВыключенный.os"); + ФайлыТестов.Добавить("./tests/fixtures/НаборСПрерваннымиТестами.os"); + ФайлыТестов.Добавить("./tests/fixtures/ПовторяемыйТест.os"); + + Возврат ПрогоныТестов.ПрогнатьОднажды( + "MCPОтчетJSONПропуски", + "НаборСВыключеннымиТестами,НаборВыключенный,НаборСПрерваннымиТестами,ПовторяемыйТест", + "", + СтрСоединить(ФайлыТестов, ","), + ДопДетальки + ); + +КонецФункции + +Функция ОтчетСПропусками() + + ПрогонСПропусками(); + + Возврат Служебный().ПрочитатьОтчет( + ПрогоныТестов.ВременныйПуть("MCPОтчетJSONПропуски", "json") + ); + +КонецФункции + +Функция Пропуск(Отчет, Знач ИмяНабора, Знач ИмяТеста) + + Для Каждого Пропуск Из Отчет.skipped_tests Цикл + + Если Пропуск.suite = ИмяНабора И ЗначениеСвойства(Пропуск, "test") = ИмяТеста Тогда + Возврат Пропуск; + КонецЕсли; + + КонецЦикла; + + Возврат Неопределено; + +КонецФункции + +Функция Проблема(Отчет, Знач ИмяТеста) + + Для Каждого Проблема Из Отчет.problems Цикл + Если ЗначениеСвойства(Проблема, "test") = ИмяТеста Тогда + Возврат Проблема; + КонецЕсли; + КонецЦикла; + + Возврат Неопределено; + +КонецФункции + +Функция ПроблемаНабора(Отчет, Знач ИмяНабора) + + Для Каждого Проблема Из Отчет.problems Цикл + Если Проблема.suite = ИмяНабора И НЕ Проблема.Свойство("test") Тогда + Возврат Проблема; + КонецЕсли; + КонецЦикла; + + Возврат Неопределено; + +КонецФункции + +Функция ЗначениеСвойства(Запись, Знач ИмяСвойства) + + Значение = Неопределено; + Запись.Свойство(ИмяСвойства, Значение); + + Возврат Значение; + +КонецФункции + +Функция ЗаписатьФайл(Знач Расширение, Знач Текст) + + Путь = ВременныеФайлы.НовоеИмяФайла(Расширение); + + ЗаписьТекста = Новый ЗаписьТекста(Путь); + ЗаписьТекста.Записать(Текст); + ЗаписьТекста.Закрыть(); + + Возврат Путь; + +КонецФункции + +#КонецОбласти diff --git "a/tests/support/\320\234\320\276\320\264\321\203\320\273\320\270/\320\226\321\203\321\200\320\275\320\260\320\273\320\222\321\213\320\277\320\276\320\273\320\275\320\265\320\275\320\270\321\217.os" "b/tests/support/\320\234\320\276\320\264\321\203\320\273\320\270/\320\226\321\203\321\200\320\275\320\260\320\273\320\222\321\213\320\277\320\276\320\273\320\275\320\265\320\275\320\270\321\217.os" index 7a91f8a..1253eb1 100644 --- "a/tests/support/\320\234\320\276\320\264\321\203\320\273\320\270/\320\226\321\203\321\200\320\275\320\260\320\273\320\222\321\213\320\277\320\276\320\273\320\275\320\265\320\275\320\270\321\217.os" +++ "b/tests/support/\320\234\320\276\320\264\321\203\320\273\320\270/\320\226\321\203\321\200\320\275\320\260\320\273\320\222\321\213\320\277\320\276\320\273\320\275\320\265\320\275\320\270\321\217.os" @@ -2,7 +2,7 @@ // Используется фикстурами исполнения для регистрации порядка вызовов, // так как компанейские наборы не сохраняют состояние между методами. -Перем Записи Экспорт; // Массив - метки вызовов в порядке выполнения +Перем Записи Экспорт; // Массив из Строка - метки вызовов в порядке выполнения Процедура Записать(Метка) Экспорт Записи.Добавить(Метка); diff --git "a/tests/support/\320\234\320\276\320\264\321\203\320\273\320\270/\320\237\321\200\320\276\320\263\320\276\320\275\321\213\320\242\320\265\321\201\321\202\320\276\320\262.os" "b/tests/support/\320\234\320\276\320\264\321\203\320\273\320\270/\320\237\321\200\320\276\320\263\320\276\320\275\321\213\320\242\320\265\321\201\321\202\320\276\320\262.os" index 69b89c0..2eb5954 100644 --- "a/tests/support/\320\234\320\276\320\264\321\203\320\273\320\270/\320\237\321\200\320\276\320\263\320\276\320\275\321\213\320\242\320\265\321\201\321\202\320\276\320\262.os" +++ "b/tests/support/\320\234\320\276\320\264\321\203\320\273\320\270/\320\237\321\200\320\276\320\263\320\276\320\275\321\213\320\242\320\265\321\201\321\202\320\276\320\262.os" @@ -1,4 +1,4 @@ -// Вспомогательный модуль для тестов, выполняющих вложенные прогоны фреймворка. +// Вспомогательный модуль для тестов, выполняющих вложенные прогоны фреймворка. // // Важно: тестовые сценарии и фикстуры, создающие поделки, не должны иметь // конструкторов с аннотацией &ТестовыйНабор — аннотированные типы видны @@ -35,10 +35,15 @@ // ВключаемыеНаборы - Строка - Включающий фильтр имён наборов через запятую // КаталогиТестов - Строка - Каталоги поиска тестов через запятую // ФайлыТестов - Строка - Файлы тестов через запятую -// ДопДетальки - Соответствие - Дополнительные детальки поделки +// ДопДетальки - Соответствие из КлючИЗначение - Дополнительные детальки поделки: +// * Ключ - Строка - Имя детальки с префиксом "OneUnit.", например "OneUnit.ПутьКОтчетуJSON" +// * Значение - Строка - Значение детальки // // Возвращаемое значение: -// Структура - Поля: План, Статистика +// Структура - Результаты прогона: +// * Поделка - Поделка - Поделка вложенного прогона +// * Статистика - РепортерСтатистика - Желудь статистики прогона +// * План - ТестПлан - Выполненный тест-план // Функция Прогнать(ВключаемыеНаборы, КаталогиТестов = "", ФайлыТестов = "", ДопДетальки = Неопределено) Экспорт @@ -72,10 +77,12 @@ // ВключаемыеНаборы - Строка - Включающий фильтр имён наборов через запятую // КаталогиТестов - Строка - Каталоги поиска тестов через запятую // ФайлыТестов - Строка - Файлы тестов через запятую -// ДопДетальки - Соответствие - Дополнительные детальки поделки +// ДопДетальки - Соответствие из КлючИЗначение - Дополнительные детальки поделки: +// * Ключ - Строка - Имя детальки с префиксом "OneUnit.", например "OneUnit.ПутьКОтчетуJSON" +// * Значение - Строка - Значение детальки // // Возвращаемое значение: -// Структура - Поля: План, Статистика +// см. Прогнать // Функция ПрогнатьОднажды(Ключ, ВключаемыеНаборы, КаталогиТестов = "", ФайлыТестов = "", ДопДетальки = Неопределено) Экспорт 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 0004e9c..3e16db4 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" @@ -22,7 +22,7 @@ &ИсточникЗначение("TREE", "Дерево") Процедура РежимВыводаЛогаРаспознается(Режим, Ожидаемый) Экспорт - Ожидаем.Что(КонсольноеПриложениеТестированиеСлужебный.РежимВыводаЛога(Режим)) + Ожидаем.Что(Служебный().РежимВыводаЛога(Режим)) .Равно(Ожидаемый); КонецПроцедуры @@ -31,7 +31,7 @@ Процедура НеизвестныйРежимВыводаВызываетИсключение() Экспорт Попытка - КонсольноеПриложениеТестированиеСлужебный.РежимВыводаЛога("nonsense"); + Служебный().РежимВыводаЛога("nonsense"); Исключение ИнформацияОбОшибке = ИнформацияОбОшибке(); КонецПопытки; @@ -48,7 +48,7 @@ &Тест Процедура НастройкиЛогосаСтроятсяПоКомпонентуИРаскладке() Экспорт - Настройки = КонсольноеПриложениеТестированиеСлужебный.НастройкиЛогоса("core", "МояРаскладка"); + Настройки = Служебный().НастройкиЛогоса("core", "МояРаскладка"); Логгер = Настройки.Получить("logos.logger").Получить("oscript.lib.oneunit.core"); @@ -75,7 +75,7 @@ ЗаписьТекста.ЗаписатьСтроку("custom.key=Значение"); ЗаписьТекста.Закрыть(); - Результат = КонсольноеПриложениеТестированиеСлужебный.ПрочитатьКонфигурационныйФайл(ПутьКФайлу); + Результат = Служебный().ПрочитатьКонфигурационныйФайл(ПутьКФайлу); // Произвольные ключи читаются как есть Ожидаем.Что(Результат.ПолучитьИлиУмолчание("custom.key", "")).Равно("Значение"); @@ -109,7 +109,7 @@ // ФиксированныйМассив, чтобы закрепить поддержку обоих типов массивов в детальках Детальки = Детальки(Массивы.ИзЭлементов("./tests/fixtures"), ФайлыТестов); - ТочкаВхода = КонсольноеПриложениеТестированиеСлужебный.СоздатьТочкуВхода( + ТочкаВхода = Служебный().СоздатьТочкуВхода( Детальки, МенеджерВременныхФайлов, Логирование.ПолучитьЛог("oscript.lib.oneunit.тесты"), @@ -156,7 +156,7 @@ Детальки = Детальки(Новый Массив, Новый Массив); - КонсольноеПриложениеТестированиеСлужебный.СоздатьТочкуВхода( + Служебный().СоздатьТочкуВхода( Детальки, МенеджерВременныхФайлов, Логирование.ПолучитьЛог("oscript.lib.oneunit.тесты"), @@ -180,6 +180,131 @@ #КонецОбласти +#Область ТестыЗапускателяТестирования + +&Тест +Процедура ЗеленыйПрогонВозвращаетНольИФормируетПокрытие() Экспорт + + МенеджерВременныхФайлов = Новый МенеджерВременныхФайлов(); + ПутьКОтчетуПокрытия = МенеджерВременныхФайлов.НовоеИмяФайла("xml"); + ПутьКОтчетуCobertura = МенеджерВременныхФайлов.НовоеИмяФайла("xml"); + + Параметры = Запускатель().ПараметрыЗапуска(); + Параметры.ПутьКОтчетуGenericCoverage = ПутьКОтчетуПокрытия; + Параметры.ПутьКОтчетуCobertura = ПутьКОтчетуCobertura; + + Результат = Запускатель().ВыполнитьТестирование( + Детальки(Новый Массив, Массивы.ИзЭлементов("./tests/fixtures/Тест.os"), РежимыВыводаЛога.Ничего), + Логирование.ПолучитьЛог("oscript.lib.oneunit.тесты"), + Параметры + ); + + Ожидаем.Что(Результат.КодВозврата).Равно(0); + + Ожидаем.Что(Новый Файл(ПутьКОтчетуПокрытия).Существует(), "Отчет покрытия существует").ЭтоИстина(); + Ожидаем.Что(СтрНайти(ПрочитатьФайл(ПутьКОтчетуПокрытия), "lineToCover"), "Отчет содержит строки").Больше(0); + + Ожидаем.Что(Новый Файл(ПутьКОтчетуCobertura).Существует(), "Отчет Cobertura существует").ЭтоИстина(); + + МенеджерВременныхФайлов.Удалить(); + +КонецПроцедуры + +&Тест +Процедура КрасныйПрогонВозвращаетНеНоль() Экспорт + + Результат = Запускатель().ВыполнитьТестирование( + Детальки(Новый Массив, Массивы.ИзЭлементов("./tests/fixtures/НаборКрасныхТестов.os"), РежимыВыводаЛога.Ничего), + Логирование.ПолучитьЛог("oscript.lib.oneunit.тесты") + ); + + Ожидаем.Что(Результат.КодВозврата).Равно(1); + + // Ошибки исполнения выводятся даже в режиме Ничего + Ожидаем.Что(СтрНайти(Результат.Вывод, "СостояниеОшибка")).Больше(0); + +КонецПроцедуры + +&Тест +Процедура ОбнаружениеВыводитПлоскийСписокТестов() Экспорт + + Результат = Запускатель().ВыполнитьОбнаружение( + Детальки(Новый Массив, Массивы.ИзЭлементов("./tests/fixtures/Тест.os"), РежимыВыводаЛога.ПлоскийСписок), + РежимыВыводаЛога.ПлоскийСписок, + Логирование.ПолучитьЛог("oscript.lib.oneunit.тесты") + ); + + Ожидаем.Что(Результат.КодВозврата).Равно(0); + Ожидаем.Что(СтрНайти(Результат.Вывод, "Тест.Тест1")).Больше(0); + +КонецПроцедуры + +&Тест +Процедура ОбнаружениеВыводитДеревоТестов() Экспорт + + // При трансляции вывода в лог вывод не буферизуется - как у команд приложения + Параметры = Запускатель().ПараметрыЗапуска(); + Параметры.ЛогВывода = Логирование.ПолучитьЛог("oscript.lib.oneunit.тесты"); + + Результат = Запускатель().ВыполнитьОбнаружение( + Детальки(Новый Массив, Массивы.ИзЭлементов("./tests/fixtures/Тест.os"), РежимыВыводаЛога.Дерево), + РежимыВыводаЛога.Дерево, + Логирование.ПолучитьЛог("oscript.lib.oneunit.тесты"), + Параметры + ); + + Ожидаем.Что(Результат.КодВозврата).Равно(0); + +КонецПроцедуры + +&Тест +Процедура ОбнаружениеБезРежимаВыводаНеВыводитТесты() Экспорт + + Результат = Запускатель().ВыполнитьОбнаружение( + Детальки(Новый Массив, Массивы.ИзЭлементов("./tests/fixtures/Тест.os"), РежимыВыводаЛога.Ничего), + РежимыВыводаЛога.Ничего, + Логирование.ПолучитьЛог("oscript.lib.oneunit.тесты") + ); + + Ожидаем.Что(Результат.КодВозврата).Равно(0); + Ожидаем.Что(СтрНайти(Результат.Вывод, "Тест1")).Равно(0); + +КонецПроцедуры + +#КонецОбласти + +#Область ТестыКаноническогоРегистраПути + +&Тест +&ВключенНаОС("Windows") +Процедура КаноническийРегистрПутиВосстанавливаетРегистрФайловойСистемы() Экспорт + + МенеджерВременныхФайлов = Новый МенеджерВременныхФайлов(); + БазовыйКаталог = МенеджерВременныхФайлов.СоздатьКаталог(); + + СоздатьКаталог(ОбъединитьПути(БазовыйКаталог, "MixedCase")); + + ПутьВНеверномРегистре = ОбъединитьПути(БазовыйКаталог, "mIXEDcASE"); + + Канонический = Запускатель().КаноническийРегистрПути(ПутьВНеверномРегистре); + + Ожидаем.Что(СтрЗаканчиваетсяНа(Канонический, "MixedCase")).ЭтоИстина(); + + МенеджерВременныхФайлов.Удалить(); + +КонецПроцедуры + +&Тест +Процедура КаноническийРегистрНесуществующегоПутиВозвращаетсяКакЕсть() Экспорт + + Путь = ОбъединитьПути(ВременныеФайлы.НовоеИмяФайла(), "НеСуществует"); + + Ожидаем.Что(Запускатель().КаноническийРегистрПути(Путь)).Равно(Путь); + +КонецПроцедуры + +#КонецОбласти + #Область ТестыЛогаКоманды &Тест @@ -248,14 +373,26 @@ #Область СлужебныеПроцедурыИФункции -Функция Детальки(КаталогиТестов, ФайлыТестов) +Функция Служебный() + Возврат Новый КонсольноеПриложениеТестированиеСлужебный(); +КонецФункции + +Функция Запускатель() + Возврат Новый ЗапускательТестирования(Служебный()); +КонецФункции + +Функция Детальки(КаталогиТестов, ФайлыТестов, Знач РежимВыводаЛога = "") + + Если НЕ ЗначениеЗаполнено(РежимВыводаЛога) Тогда + РежимВыводаЛога = РежимыВыводаЛога.Дерево; + КонецЕсли; Детальки = Новый Соответствие; Детальки.Вставить("КаталогиТестов", КаталогиТестов); Детальки.Вставить("ФайлыТестов", ФайлыТестов); Детальки.Вставить("ИскатьВПодкаталогах", Ложь); Детальки.Вставить("Таймаут", 0); - Детальки.Вставить("РежимВыводаЛога", РежимыВыводаЛога.Дерево); + Детальки.Вставить("РежимВыводаЛога", РежимВыводаЛога); Возврат Детальки;