Оглавление
Под капотом – загрузка библиотек
Соглашение о структуре каталогов
Тестирование с помощью 1testrunner
Публичные и приватные классы и модули
Введение
В своей предыдущей статье я рассказывал об имеющихся в экосистеме OneScript полезных библиотеках и приложениях. В этой статье мы попробуем написать свою библиотеку с нуля. Заранее извиняюсь за огромное количество упоминаний слова «библиотека» в тексте статьи :)
В рамках текущего повествования будут рассматриваться библиотеки, написанные на языке 1С. Написание библиотеки на C#/F#/VB.Net для OneScript заслуживает отдельной статьи.
Для начала напомню, что такое «библиотека». Библиотека – это особым образом упакованный сценарий (или набор сценариев), которые можно переиспользовать в других сценариях и подключать с помощью инструкции #Использовать. Инструкция #Использовать (в базовом варианте) умеет подключать библиотеки «по имени» и по пути к корневому каталогу библиотеки.
Подготовка окружения
Определимся с необходимым окружением.
- Естественно, нам понадобится сам движок - http://oscript.io/
- Разработку я буду вести в Visual Studio Code - https://code.visualstudio.com/
- Для упрощения редактирования сценариев OneScript у меня установлен плагин «Language 1C (BSL) Plugin)» - https://marketplace.visualstudio.com/items?itemName=xDrivenDevelopment.language-1c-bsl
- Для упрощения запуска и получения возможностей отладки, установим отладчик OneScript для Visual Studio Code. Если вы используете "стабильную" версию движка OneScript, то вы можете установить его через MarketPlace, встроенный в VSCode - имя пакета OneScript Debug. Если вы используете "ночную" версию движка, то необходимо скачать файл с расширением «vsix» со страницы http://oscript.io/download и установить его вручную через команду "Установка из VSIX" в VSCode
В рабочем каталоге создадим два новых подкаталога:
- calculation – здесь будет жить наша библиотека
- my_project – здесь будет жить проект, который использует библиотеку
Как и положено в любом how-to, мы будем писать библиотеку, складывающую и умножающую числа :) Еще и тестами ее покроем.
Первый прототип
Для начала заложим прототип нашей библиотеки. Для удобства я открою в VSCode рабочий каталог с двумя подкаталогами:
В корне каталога calculation создадим файл Вычислитель.os, который и будет отвечать за основную логику вычисления. В теле модуля расположим следующую нехитрую процедуру:
Функция Сложить(Слагаемое1, Слагаемое2) Экспорт
Возврат Слагаемое1 + Слагаемое2;
КонецФункции
В корне каталога my_project создадим script.os, который будет изображать наше полезное прикладное ПО, использующее библиотеку calculation
Содержимое скрипта script.os:
#Использовать "../calculation"
Сумма = Вычислитель.Сложить(2, 2);
Сообщить(Сумма);
Разберемся построчно с написанным.
В первой строке используется специальная инструкция «Использовать», которая пытается найти и подключить библиотеку. Данной инструкции передается путь к корневому каталогу библиотеки, т.е. происходит подключение «по пути». Путь может быть как абсолютным, так и относительным. Для удобства мы используем относительный.
На следующей значащей строке происходит вызов функции «Сложить» у объекта Вычислитель. Разницу между модулями и классами и рассмотрим чуть позже, пока что можете запомнить, что это «модуль».
В последней строке мы выводим в консоль полученное значение.
Как же теперь это запустить? Есть несколько вариантов.
Классический – открыть консоль или встроенный в VSC терминал (сочетание клавиш ctrl-ё или команда «Посмотреть: Переключить интегрированный терминал») и выполнить скрипт напрямую с помощью интерпретатора oscript:
Второй вариант – с помощью встроенных команд по запуску скриптов. Нажмем F1 и в поисковой строке наберем «task». Выберем пункт «Задачи: Выполнить задачу»:
Появится еще одно окно, в котором нам нужно выполнить одну команду «oscript: OneScript: run»
Но мы же не просто так ставили отладчик?
Третий способ – запуск скрипта с помощью 1Script Debugger. Для начала нам надо создать специальный конфигурационный файл launch.json. Сделать это можно почти автоматически – нужно нажать в редакторе клавишу F5, в вывалившемся сверху окошке выбрать “1Script Debugger”. После этого в подкаталоге .vscode появится файл launch.json примерно со следующим содержимым:
Его назначение – хранить информацию о «конфигурациях запуска». Одну такую конфигурацию мы только что и создали.
Вернемся к нашему скрипту script.os и снова нажмем F5. После запуска на некоторое время должно появиться с окошком контроля выполнения кода (на скриншоте сверху слева) и в «Консоли отладки» (на скриншоте снизу) должно вывестись «4».
Если вы вообще не видите какого-либо окна снизу, то открыть «Консоль отладки» можно:
- нажатием комбинации клавиш Ctrl+Shift+Y
- выполнив команду «Посмотреть: Консоль отладки» из командного меню, вызываемого по клавише F1
- выполнив команду «Вид» -> «Консоль отладки» из главного меню (вверху окна VSCode)
- переключившись на вкладку «Отладка» (в левой части VSCode) и нажав кнопку «Консоль отладки»
Тем, кто успел открыть терминал, нужно будет вручную ткнуть по заголовку вкладки «Консоль отладки»
Точки останова тоже работают. И «табло» есть – тут оно называется «Контрольные значения». Оно не такое богатое, как в конфигураторе, но тоже очень сильно помогает.
Поздравляю, вы написали свою первую библиотеку! :)
Под капотом – загрузка библиотек
Постараемся понять, почему и как оно работает.
Пока мы не успели ничего сконфигурировать в нашей библиотеке - наш единственный сценарий «Вычислитель» лежит в корне каталога библиотеки. Когда мы «используем» с помощью соответствующей инструкции какую-либо библиотеку, OneScript выполняет определенную последовательность действий.
Сначала интерпретатор в корневом каталоге библиотеки ищет и пытается выполнить специальный «волшебный» сценарий package-loader.os – так называемый «загрузчик библиотек». Это специальный сценарий, который, как следует из его названия, занимается загрузкой библиотек – добавлением в область видимости классов и модулей, которые предоставляются библиотекой. В специальной процедуре-обработчике «ПриЗагрузкеБиблиотеки» среди параметров есть «СтандартнаяОбработка». Если в результате работы загрузчика она останется выставлена в «Истина» или файл package-loader.os в корне библиотеки вовсе отсутствует, то загрузка библиотеки переходит к следующему шагу.
На следующем шаге выполняется загрузчик «по умолчанию» - это такой же файл package-loader.os, который располагается в «системном каталоге библиотек». Значение системного каталога библиотек берется из параметра lib.system в файле oscript.cfg, расположенного либо в каталоге с точкой входа в приложение (первоначально запускаемый сценарий, например my_project/script.os) либо в каталоге самого интерпретатора oscript.exe. В базовом варианте (как у нас), lib.system начинает вести в место установки OneScript, подкаталог lib.
В моем случае, OneScript установлен в %USERPROFILE%/AppData/Local/ovm/current. Расположение каталогов и файлов на скриншоте ниже.
В загрузчике из системного каталога библиотек есть пара хитростей, о которых мы поговорим чуть позже. Сейчас важно знать, что и этот загрузчик не смог обработать нашу библиотеку, поэтому процесс загрузки библиотеки переходит к следующему (заключительному шагу)
На заключительном шаге загрузки библиотеки в дело вступает уже сам движок OneScript. Он максимально категоричен – ищет все файлы с расширением os в корневом каталоге библиотеки и подключает их как «модули». Именно это и произошло в нашем случае – файл Вычислитель из корня библиотеки был подключен как «модуль» и стал доступен для вызова и обращения «через точку» в сценарии script.os.
Схематично данный процесс можно представить так:
Чтобы развеять страх по поводу всей этой конструкции загрузчиков, попробуем вмешаться в процесс.
Создадим в каталоге calculation файл package-loader.os со следующим содержимым:
Процедура ПриЗагрузкеБиблиотеки(Путь, СтандартнаяОбработка, Отказ)
Сообщить("Я - загрузчик библиотеки calculation!");
СтандартнаяОбработка = Ложь;
ПутьКМодулю = ОбъединитьПути(ТекущийСценарий().Каталог, "Вычислитель.os");
ДобавитьМодуль(ПутьКМодулю, "Вычислитель");
КонецПроцедуры
В первой строке обозначена сигнатура метода. В параметре «Путь» передается путь к каталогу библиотеки, СтандартнаяОбработка – флаг продолжения выполнения последовательности действий по процессу загрузки, а Отказ – стандартный флаг сигнализирования об ошибках при загрузке библиотеки.
Вторая строка просто выводит сообщение в консоль.
Третья строка говорит интерпретатору OneScript, что текущий сценарий сможет загрузить эту библиотеку, и после его выполнения дополнительных действий совершать не требуется (см. схему).
В четвертой строке мы вычисляем путь к модулю, который хотим подключить. Для этого используются функции:
- «ТекущийСценарий», возвращающая информацию о выполняемом в данный момент файле сценария (в том числе его расположение)
- «ОбъединитьПути», складывающая различные части пути к файлам с учетом системных разделителей пути
В пятой строке вызывается процедура «ДобавитьМодуль», выполняющая фактическую регистрацию объекта в пространстве имен, с двумя параметрами:
- Путь к подключаемому сценарию
- Имя, под которым будет зарегистрирован объект
В пару к процедуре «ДобавитьМодуль» существует процедура «ДобавитьКласс»
Если мы снова откроем сценарий script.os и нажмем F5, то увидим в консоли отладки новое сообщение:
Наш загрузчик успешно отработал. Он не только вывел сообщение в консоль, но и корректно зарегистрировал модуль.
Соглашение о структуре каталогов
Рано или поздно библиотека выйдет за пределы одного-двух файлов в корне репозитория. Захочется вынести отдельно исходники библиотеки, тесты, документацию, служебные файлы… В конце концов, скоро мы и до классов дойдем, а стандартный загрузчик библиотек (на уровне интерпретатора) умеет подключать модули, но не классы.
К счастью, мы можем помочь «загрузчику библиотек» системного каталога библиотек, определенным образом расположив файлы сценариев в каталоге библиотеки.
Существует «соглашение о структуре каталогов» библиотеки. Все файлы *.os, которые располагаются в каталогах:
- Классы
- Classes
- src/Классы
- src/Classes
подключаются как «классы».
Аналогичным образом все файлы *.os, которые располагаются в каталогах:
- Модули
- Modules
- src/Модули
- src/Modules
подключатся как «модули».
Соглашение это не кровью написанное, а вполне определенным образом зашито в системном загрузчике библиотек. Если вы откроете файл package.loader (МестоУстановкиOneScript/lib/package-loader.os), то в процедуре «ПриЗагрузкеБиблиотеки» увидите вызов процедуры «ОбработатьСтруктуруКаталоговПоСоглашению». Именно в этой процедуре и указан список каталогов, в котором производится поиск.
Попробуем воспользоваться стандартным загрузчиком библиотек.
Для начала удалим файл package-loader.os из каталога calculation – он нам больше не нужен. Затем создадим подкаталог «src» и в нем подкаталог «Модули». Переместим туда файл «Вычислитель.os» и снова выполним (F5) сценарий script.os
Если все сделано без ошибок, то в консоли отладки будет выведена сумма двух чисел.
Убедимся, что это отработал системный загрузчик библиотек, а не очередная «магия» интерпретатора OneScript. Переключимся на вкладку «Терминал», запустим cmd (если по умолчанию терминал запускается в powershell) и выполним следующую команду:
(set OSLIB_LOADER_TRACE=1) && (oscript .\my_project\script.os)
Установка переменной среды OSLIB_LOADER_TRACE=1 заставит системный загрузчик библиотек активно информировать о своих действиях в консоль.
В выводе команды виден текст обработки каталогов «по соглашению».
Данный текст будет выводиться только при использовании OneScript версии 1.0.19 и новее, либо после обновления системного загрузчика библиотек актуальным текстом сценария
Первые тесты
Один мой друг показывал мне список задач (с реального проекта) по доработке с описанием задачи и способом приемки работ. Напротив большинства пунктов стояло загадочное «проверка визуально». Сейчас наша библиотека проверяется только «визуально», а это несколько несерьезно.
Попробуем добавить первый тест!
В каталоге библиотеки calculation создадим подкаталог tests и новый файл ПроверкаВычисления.os
Содержимое файла:
#Использовать asserts
#Использовать ".."
Результат = Вычислитель.Сложить(2, 2);
Ожидаем.Что(Результат).Равно(5);
В первой строке с помощью инструкции «Использовать» подключается библиотека asserts. Asserts – это библиотека, содержащая модули, помогающие в написании тестов. Она содержит два модуля: «Утверждения» и «Ожидаем» – для тестирования в unit- (модульное) и fluent- (текучее, цепное) стилях соответственно. Исходники библиотеки доступны по ссылке: https://github.com/oscript-library/asserts
Обратите внимание, имя библиотеки asserts указано без кавычек. Это способ подключения библиотек «по имени». Вычисление пути библиотеки делается очень просто – берется системный (и дополнительный, если он объявлен) каталог библиотек, к нему добавляется имя библиотеки – на выходе путь к каталогу библиотеки. Далее всё по той же схеме, что и при подключении библиотеки «по пути».
Второй строкой следует загадочная конструкция с использованием «..». В этом тоже нет никакой магии. Вспомним, что наш сценарий теста лежит в каталоге tests. Подключение библиотеки «по пути» умеет работать с относительными каталогами. Две точки – это обозначение родительского каталога. Складываем все три знания и получаем, что загрузчик библиотек будет пытаться загрузить каталог calculation, являющийся корнем нашей библиотеки. А в этом корне есть каталог src и вообще он всячески удовлетворяет «соглашению о структуре каталогов».
Таким образом на третьей строке нам становится доступен объект «Вычислитель» и его методы.
На четвертой строке применяется библиотека asserts. «Ожидаем» – это обращение к модулю этой библиотеки, функция «Что» устанавливает контекст проверки – мы проверяем результат работы функции «Сложить», функция «Равно» будет проверять переданное ей значение с сохраненным в контексте.
Если сейчас запустить этот сценарий (F5), то в «Консоли отладки» мы получим сообщение:
В этом сообщении есть две проблемы.
Пока у нас есть единственный тест, мы еще способны удержать в голове, что происходит. Когда количество тестов будет расти, стандартного сообщения об исключении начнет не хватать. Мы можем помочь сами себе, заполнив второй параметр в функции «Что» текстом сообщения с исключением:
Теперь помимо текста исключения от библиотеки «asserts» (о том, что 4 не равно 5) мы видим и наше дополнительное сообщение об ошибке.
Вторая проблема этого сообщения – тесты падают :) Причем падают по нашей вине. Поправим аргумент функции «Равно» на 4. После этого тест должен завершиться без ошибок.
Тестирование с помощью 1testrunner
Для тестирования библиотек сообществом разработано несколько фреймворков тестирования. Основные из них:
- 1testrunner – для модульного и сценарного тестирования – является портом фреймворка xUnitFor1C для OneScript
- 1bdd – для проверки поведения на основе требований в виде feature-файлов – не является прямым портом, но в целом похож на фреймворк Vanessa-Behavior
Данные фрейморки позволяют получать подробные отчеты о тестировании в нескольких форматах. Если эти фреймворки еще не установлены на вашем компьютере, их можно поставить через OneScript Package Manager, выполнив команду «opm install 1testrunner» и «opm install 1bdd» соответственно.
Адаптируем наш первый тест к использованию фреймворка 1testrunner. Для этого нам надо добавить в наш сценарий ПроверкаВычисления.os специальную функцию ПолучитьСписокТестов, возвращающую массив имен экспортных процедур, осуществляющих тестирование. А сами строки по проверке результата сложения перенести в эту новую процедуру. Дополнительно надо добавить две процедуры (ПередЗапускомТеста и ПослеЗапускаТеста), в которых при необходимости можно дополнительно сконфигурировать тест (нам пока нечего конфигурировать, поэтому мы не станем этого делать). Должно получиться что-то вроде такого:
#Использовать asserts
#Использовать ".."
Функция ПолучитьСписокТестов(ЮнитТестирование) Экспорт
ВсеТесты = Новый Массив;
ВсеТесты.Добавить("ТестДолжен_ПроверитьЧтоДваПлюсДваРавноЧетыре");
Возврат ВсеТесты;
КонецФункции
Процедура ПередЗапускомТеста() Экспорт
КонецПроцедуры
Процедура ПослеЗапускаТеста() Экспорт
КонецПроцедуры
Процедура ТестДолжен_ПроверитьЧтоДваПлюсДваРавноЧетыре() Экспорт
Результат = Вычислитель.Сложить(2, 2);
Ожидаем.Что(Результат, "Ожидаем, что 2 + 2 = 4").Равно(4);
КонецПроцедуры
Разработку тестов для пограничных значений вроде сложения положительного и отрицательного чисел, нулей и прочего оставлю в качестве домашнего задания.
Как теперь все это запустить?
В редакторе VSCode (а точнее в плагине для поддержки языка 1C и OneScript) уже зашито несколько задач по тестированию. Нажмем F1 и в поисковой строке наберем «task». Выберем пункт «Задачи: Выполнить задачу»:
Появится еще одно окно, в котором нам нужно выполнить одну из команд «1testrunner»:
Если у вас открыт рабочий каталог сразу с двумя подкаталогами, то задача «Testing project» упадет с ошибкой, т.к. по умолчанию запускаются тесты из каталога tests, а у меня он вложен в подкаталог calculation. Конечно же, это можно подправить, нажав на шестеренку справа от задачи и скорректировав путь к тестам, но я просто выполню задачу «Testing current test-file».
Наградой нам станет «зеленая полоса» от фреймворка 1testrunner и сообщение о том, что все наши тесты (один :) ) пройдены.
Похожего результата можно добиться и запуская 1testrunner из терминала. Например, вот так можно запустить все тесты из каталога «tests»:
Классы и модули
В чем же разница между «модулями» и «классами», про которые я периодически рассказываю? Ключевых различий между ними два.
Первое различие - это способ инстанцирования объекта, другими словами – способ получения объекта, с которым можно начать что-то делать.
«Модуль» в контексте OneScript – это такой сценарий, обращаться к которому можно сразу же после запуска приложения. Если проводить аналогию с 1С, то это что-то вроде «общего модуля».
«Класс» в контексте OneScript – это такой сценарий, обращаться к которому можно только после получения конкретного «экземпляра» объекта с помощью ключевого слова Новый. По аналогии с 1С – это любые объекты: массив, структура, таблица значений. Только в OneScript эти классы не ограничены метаданными конфигурации, их можно создавать самому.
Второе различие вытекает из первого. Все сценарии в OneScript могут содержать переменные, объявленные в заголовке сценария с помощью ключевого слова Перем. Обращаю ваше внимание – в модулях тоже может содержаться несколько верхнеуровневых переменных.
Технически и модули и классы – это просто некие объекты в памяти, хранящие значения набора свойств и содержимое методов. Соответственно значения, сохраненные в свойствах (переменных) модуля, будут одинаковыми во всех местах вызова этого модуля (т.к. модуль инстанцируется один раз при начале работы системе). А значения свойств у классов будут различаться у каждого инстанцированного экземпляра класса.
Я рекомендую еще раз перечитать предыдущий абзац. И еще разок.
Возвращаемся к разработке нашей библиотеки - расширим ее потенциальные возможности.
Наш вычислитель планируется сделать очень умным. Например, он может уметь складывать числа, хранящиеся не только в памяти, но и на диске, или в SQL базе. Не спрашивайте зачем :)
Для хранения складываемых значений нам понадобится передавать в вычислитель не сами числа, а их хранилища. Для хранения чисел и способа их получения удобно использовать классы.
Следуя принципу Test-First сначала напишем тест на новую функциональность. Добавим в тестовый сценарий «ПроверкаВычисления» новую процедуру:
Процедура ТестДолжен_ПроверитьСложениеДвухЧиселВПамяти() Экспорт
ЧислоВПамяти1 = Новый ЧислоВПамяти(1);
ЧислоВПамяти2 = Новый ЧислоВПамяти(3);
Результат = Вычислитель.Сложить(ЧислоВПамяти1, ЧислоВПамяти2);
Ожидаем.Что(Результат, "Ожидаем, что сложение чисел из памяти работает").Равно(4);
КонецПроцедуры
В данном тесте я создаю два экземпляра (еще не разработанного) класса ЧислоВПамяти, передавая им в качестве параметра сохраняемое значение (единицу или тройку). В результате работы вычислителя мы должны получить четыре.
Не забудем добавить новый тест в функцию «ПолучитьСписокТестов».
Дополнительно добавим новый тест, проверяющий работу нашего класса. Создадим в каталоге calculation/tests новый файл ЧислоВПамяти.os
#Использовать asserts
#Использовать ".."
Функция ПолучитьСписокТестов(ЮнитТестирование) Экспорт
ВсеТесты = Новый Массив;
ВсеТесты.Добавить("ТестДолжен_ПроверитьСохранениеЗначенийВЧислоВПамяти");
Возврат ВсеТесты;
КонецФункции
Процедура ПередЗапускомТеста() Экспорт
КонецПроцедуры
Процедура ПослеЗапускаТеста() Экспорт
КонецПроцедуры
Процедура ТестДолжен_ПроверитьСохранениеЗначенийВЧислоВПамяти() Экспорт
ЧислоВПамяти = Новый ЧислоВПамяти(10);
Ожидаем.Что(
ЧислоВПамяти.Получить(),
"Ожидаем, число 10 сохранилось в памяти объекта ЧислоВПамяти"
).Равно(10);
КонецПроцедуры
В данном тесте мы создаем новый экземпляр класса ЧислоВПамяти (передав ему начальное значение 10) и пытаемся получить это значение из класса.
После написания тестов мне уже примерно понятно, как должен работать новый класс. В завершение процесса написания тестов запустим прогон тестов через фреймворк 1testrunner:
1testrunner -runall .\calculation\tests\
В теле сообщения помимо текстов исключения видно общую статистику:
Наконец, переходим к разработке! В каталоге calculation/src создадим новый подкаталог Классы. В новом подкаталоге создадим файл ЧислоВПамяти.os – хранилище нашего нового класса.
В качестве реализации класса предлагаю такое решение «в лоб»:
Перем ХранимоеЗначение;
Процедура ПриСозданииОбъекта(УстанавливаемоеЗначение)
ХранимоеЗначение = УстанавливаемоеЗначение;
КонецПроцедуры
Функция Получить() Экспорт
Возврат ХранимоеЗначение;
КонецФункции
В качестве хранилища значения «числа в памяти» будет выступать переменная на уровне класса. С помощью специального процедуры «ПриСозданииОбъекта» (аналогия в 1С – ПриСозданииНаСервере у формы) мы описываем конструктор объекта. В нашем случае при вызове конструктора необходимо будет передать 1 параметр, который сохранится в переменной ХранимоеЗначение.
Конструктор объекта может и отсутствовать. Тогда инстанцирование объекта происходит без параметров, через вызов Новый ИмяКласса()
Функция «Получить» является способом возвращения значения, сохраненного в объекте.
Если снова запустить полный прогон тестов, то можно заметить, что количество прошедших тестов увеличилось – тест класса ЧислоВПамяти перестал выкидывать исключение.
Для «починки» последнего теста необходимо поправить получение значений слагаемых в модуле «Вычислитель». Откроем его (calculation/src/Модули/Вычислитель.os) и заменим реализацию функции сложить на следующую:
Функция Сложить(Слагаемое1, Слагаемое2) Экспорт
РеальноеСлагаемое1 = ПолучитьРеальноеСлагаемое(Слагаемое1);
РеальноеСлагаемое2 = ПолучитьРеальноеСлагаемое(Слагаемое2);
Возврат РеальноеСлагаемое1 + РеальноеСлагаемое2;
КонецФункции
Функция ПолучитьРеальноеСлагаемое(Слагаемое)
Если ТипЗнч(Слагаемое) = Тип("Число") Тогда
РеальноеСлагаемое = Слагаемое;
Иначе
РеальноеСлагаемое = Слагаемое.Получить();
КонецЕсли;
Возврат РеальноеСлагаемое;
КонецФункции
Во-первых, добавим функцию ПолучитьРеальноеСлагаемое, которое будет возвращать число, которое нужно сложить. Если вдруг в Вычислитель передали реальное «число», то будет возвращаться сам параметр. Если же был передан какой-то объект, например, ЧислоВПамяти, будет выполняться попытка «получения» значения из этого объекта.
На этот раз запуск тестов должен показать три зеленых теста!
На сладкое, добавим немного «защитного программирования». Как автор класса ЧислоВПамяти, я хочу быть уверенным, что в конструктор объекта передается именно число, а не, например, строка.
Test-first! В тесте ЧислоВПамяти (calculation/tests/ЧислоВПамяти.os) добавим новый тест со следующим содержанием:
Процедура ТестДолжен_ПроверитьВыбросИсключенияПриСохраненииСтроки() Экспорт
Попытка
ЧислоВПамяти = Новый ЧислоВПамяти("Строка");
ВызватьИсключение "Должен был произойти вызов исключения!";
Исключение
Ожидаем.Что(
ОписаниеОшибки(),
"Описание ошибки содержит информацию о неверном аргументе"
).Содержит("Передан аргумент неверного типа");
КонецПопытки;
КонецПроцедуры
В данном тесте используется трюк ожидания исключения. Для начала в попытке пытаемся создать новое ЧислоВПамяти, передав в конструктор «Строку». Если наш код работает корректно, то конструктор объекта выдаст исключение и мы провалимся в секцию «Исключение». Если же этого не произошло, мы явно вызываем исключение с текстом «Должен был произойти вызов исключения!». В любом случае мы попадаем в секцию «Исключение», где с помощью модуля из asserts «ожидаем», что ОписаниеОшибки(), содержит нужный нам текст.
Для “обычных” методов классов/модулей можно воспользоваться конструкцией Ожидаем.Что(Объект).Метод(ИмяМетода).ВыбрасываетИсключение(ТекстИсключения)
Запуск всех тестов добавит единичку в список провалившихся тестов.
Вернемся в класс ЧислоВПамяти (calculation/src/Классы/ЧислоВПамяти.os). Проверим тип аргумента с помощью библиотеки asserts:
#Использовать asserts
Перем ХранимоеЗначение;
Процедура ПриСозданииОбъекта(УстанавливаемоеЗначение)
Ожидаем.Что(УстанавливаемоеЗначение, "Передан аргумент неверного типа").ИмеетТип("Число");
ХранимоеЗначение = УстанавливаемоеЗначение;
КонецПроцедуры
Функция Получить() Экспорт
Возврат ХранимоеЗначение;
КонецФункции
Первым делом добавим подключение библиотеки в начале модуля с помощью инструкции «Использовать». Затем в конструкторе объекта (процедуре ПриСозданииОбъекта) с помощью модуля «Ожидаем» проверим, что переданное значение ИмеетТип Число. Как видите, библиотеку asserts можно использовать не только в тестировании, но при написании конкретного прикладного кода.
Теперь все тесты снова станут зелеными.
Подобным образом можно добавить хранение чисел в любом доступном хранилище, будь то файл на диске, облачный сервис с http-интерфейсом доступа или полноценная 1С с подключением через ComОбъект (пожалуйста, не надо так).
Перечисления
При разработке библиотек периодически возникает желание создать что-то вроде перечисления. Режимы работы, типы операций… Попробуем заложить фундамент расширяемости в нашу библиотеку, добавив универсальную функцию вычисления результата и перечисление, указывающее на тип операции вычисления.
В качестве хранилища значений перечисления будет выступать «модуль» с экспортными переменными. Вы же помните, что модуль может содержать переменные?
Создадим файл calculation/src/Модули/ТипыОпераций.os:
Содержимое файла:
Перем Сложение Экспорт;
Перем Умножение Экспорт;
Сложение = 0;
Умножение = 1;
Здесь в качестве «элементов перечисления» выступают экспортные переменные (свойства) модуля. Обращение к такому перечислению будет почти аналогично 1С:
ТипыОпераций.Сложение
В качестве значений переменных могут выступать любые данные, главное, чтобы «элементы перечисления» имели различные сравниваемые значения переменных – числа, строки, булево и т.д.
Обращаю внимание, что данные значения «перечисления» по сути являются изменяемыми данными, прикладная библиотека может выполнить код в духе:
ТипыОпераций.Сложение = 123;
И он выполнится без ошибок – свойство-то экспортное, то есть доступное всем и каждому. Для исключения побочных эффектов можно применять методику get-еров:
- у значений элементов перечисления убирается ключевое Экспорт
- под каждый элемент перечисления создается функция (обычно одноименная), возвращающая значение элемента перечисления. Например, ТипыОпераций.Сложение()
Такой подход позволит обеспечить неизменяемость значений перечисления, но и перестает выглядеть как собственно «перечисления». Какой способ применять – выбирать вам.
После создания перечисления добавим универсальную функцию вычисления результата.
Test first! :)
Откроем файл с тестами (calculation/tests/ПроверкаВычисления.os) и добавим два новых теста:
Процедура ТестДолжен_ПроверитьСложениеЧерезУниверсальнуюФункцию() Экспорт
Результат = Вычислитель.ВыполнитьВычисление(ТипыОпераций.Сложение, 2, 2);
Ожидаем.Что(Результат, "Ожидаем, что сложение через универсальную функцию работает").Равно(4);
КонецПроцедуры
Процедура ТестДолжен_ПроверитьУмножениеЧерезУниверсальнуюФункцию() Экспорт
Результат = Вычислитель.ВыполнитьВычисление(ТипыОпераций.Умножение, 2, 3);
Ожидаем.Что(Результат, "Ожидаем, что умножение через универсальную функцию работает").Равно(6);
КонецПроцедуры
В данных тестах мы предполагаем, что в модуле «Вычислитель» появится универсальная функция ВыполнитьВычисление, в которую можно передать тип операции и параметры-числа.
Перейдем к реализации данной функции. В файл calculation/src/Модули/Вычислитель.os добавим пару новых функций и напишем их реализации:
Функция ВыполнитьВычисление(ТипОперации, Операнд1, Операнд2) Экспорт
Если ТипОперации = ТипыОпераций.Сложение Тогда
Возврат Сложить(Операнд1, Операнд2);
ИначеЕсли ТипОперации = ТипыОпераций.Умножение Тогда
Возврат Умножить(Операнд1, Операнд2);
Иначе
ВызватьИсключение "Неизвестный тип операции";
КонецЕсли;
КонецФункции
Функция Умножить(Множитель1, Множитель2) Экспорт
РеальныйМножитель1 = ПолучитьРеальноеСлагаемое(Множитель1);
РеальныйМножитель2 = ПолучитьРеальноеСлагаемое(Множитель2);
Возврат РеальныйМножитель1 * РеальныйМножитель2;
КонецФункции
В функции «ВыполнитьВычисление» первым делом пытаемся понять, что за тип операции нам передали. В зависимости от типа операции вызываем ту или иную функцию вычисления. Рефакторинг функции «ПолучитьРеальноеСлагаемое» (ее имя и имена переменных внутри) оставлю на вас – теперь же тесты есть, уже не страшно. Кстати, они снова зеленые!
Под капотом – lib.config
К сожалению, «соглашение именования каталогов» не всегда применимо. С ростом нашей библиотеки нам может понадобится по-другому группировать файлы в каталогах. Ортодоксальные разработчики именуют файлы латиницей, при этом хотят обращаться модулям и классам на кириллице. Или вообще сделать двуязычные имена модулей/классов – не дублировать же файлы на диске!
Для решения этих задач (и не только перечисленных) в «системный загрузчик библиотек» было добавлено еще одно «соглашение» о правилах загрузки. На этот раз оно не завязано на структуру каталогов – описание типов подключаемых сценариев (и их системных) имен описывается в отдельном текстовом файле lib.config, расположенном в корневом каталоге библиотеки:
Описание имеющихся модулей и классов выглядит так:
<package-def>
<class name="ЧислоВПамяти" file="src/Классы/ЧислоВПамяти.os"/>
<module name="Вычислитель" file="src/Модули/Вычислитель.os"/>
<module name="ТипыОпераций" file="src/Модули/ТипыОпераций.os"/>
</package-def>
Структура файла говорит сама за себя. Расскажу о нескольких важных моментах:
1) Для системного загрузчика библиотек проверка на наличие lib.config в корне библиотеки является более приоритетным, чем загрузка по «соглашению о структуре каталогов». Если библиотека содержит файл lib.config, то подключение сценариев по «соглашению» выполняться не будет.
2) Файл lib.config может содержать несколько строчек, ведущих на один и тот же файл. Например, вы можете добавить английский синоним модулю Вычислитель – для этого надо всего лишь скопировать строчку с подключением «вычислителя» и в качестве значения атрибута name указать Calculation. OneScript подключит этот сценарий два раза, создав два разных объекта в памяти – для английского синонима и для русского.
3) Более того, вы можете подключать один и тот же файл сценария как разные типы – модуль и класс. Тогда в глобальном пространстве имен прикладного сценария будет доступен и «модуль» (со своим отдельным хранилищем значений переменных), так и инстанцирование новых объектов через конструктор, с использованием ключевого слова «Новый». Этим, кстати, пользуется библиотека tempfiles, предназначенная для управления создаваемыми временными файлами. По имени ВременныеФайлы доступен «глобальный менеджер временных файлов», единый для всех точек обращения к этому модулю, а при необходимости, вы можете дополнительно проинициализировать «Новый МенеджерВременныхФайлов». С точки зрения реализации это один и тот же файл сценария, просто два раза прописанный в lib.config.
Автодополнение
Редактор Visual Studio Code (а точнее плагин Language 1C (BSL) для него) поддерживает базовые возможности по автодополнению методов классов и модулей, предоставляемых библиотеками.
Для работы системы автодополнения нужно выполнить три шага:
- На вашей локальной машине должен стоять пакет-приложение oscript-config. Поставить его можно из хаба пакетов с помощью команды opm install oscript-config. Поставили один раз и забыли.
- Библиотека, от которой вы хотите получить автодополнение, должна содержать файл lib.config. Именно этот файл ищет плагин для VSCode и по нему определяет, какие файлы (и их содержимое) надо добавлять в систему автодополнения.
- Объект, к которому идет обращение, должен называться так же, как он подключается к системе типов самого движка OneScript. Если с модулями проблем нет (кому придет в голову сохранять ссылку на модуль в другой переменной), то с классами есть небольшая засада. Для получения автодополнения имен методов, например, у класса УправлениеКонфигуратором (из пакета v8runner), имя переменной должно равно УправлениеКонфигуратором.
Т.е. после нажатия на Ctrl+Space в таком коде (после точки) вы сможете увидеть список его методов:
УправлениеКонфигуратором = Новый УправлениеКонфигуратором();
УправлениеКонфигуратором.
Аналогично для и для нашего класса ЧислоВПамяти. Пользователи смогут пользоваться автодополнением только если будут именовать переменные так же, как и класс (привет, Массив = Новый Массив!)
Пункт, который зависит от нас, как от авторов библиотеки – наличие файла lib.config – уже выполнен, так что у вас есть все шансы получить благодарного пользователя не только потому, что ваша библиотека работает, но и потому, что ей удобно пользоваться.
Публичные и приватные классы и модули
Все типы классов и модулей, подключаемых в OneScript, попадают в «глобальную область видимости». Т.е. будучи подключенными раз, они будут доступных из всех закоулков вашего прикладного приложения (в рамках одного запуска, естественно). Поэтому понятие «приватных», т.е. невидимых для внешнего пользователя классов и модулей в OneScript несколько размыто.
Однако, с учетом работающей системы автодополнения, мы можем хотя бы ограничить количество классов и модулей, доступных к выбору по Ctrl+Space.
В качестве примера реализации такого сокрытия возьмем библиотеку fluent - https://github.com/nixel2007/oscript-fluent/tree/v0.3.1
Если обратиться к содержимому lib.config в корне библиотеки, то можно увидеть два объекта – один класс и один модуль:
Однако, дерево файлов выглядит интереснее:
На верхнем уровне каталога src созданы уже стандартные подкаталоги Классы и Модули, в которых находятся файлы, объявленные в lib.config. Дополнительно в src располагается каталог «internal», в котором так же есть свои «классы» и «модули».
Если открыть, например, файл src/Классы/ПроцессорКоллекций, то в заголовке файла можно увидеть такую конструкцию:
После подключения стандартных библиотек «по имени» происходит подключение каталога “../internal”.
Вся эта конструкция как-то работает, при этом «внутренние» класс и модуль (с постфиксом «служебный» недоступны в системе автодополнения. Попробуем разобраться, что же происходит.
- При «использовании» пакета fluent системный загрузчик библиотек считывает содержимое файла lib.config
- По содержимому lib.config движок OneScript подключает класс ПроцессорКоллекции и модуль ПроцессорыКоллекций
- При инициализации указанных класса и модуля происходит использование библиотеки «../internal» «по пути»
- Системный загрузчик библиотек анализирует содержимое каталога «../internal», не находит там файла lib.config, но определяет соответствие «соглашению о структуре каталогов»
- Движок OneScript подключает «служебные» модуль и класс.
Параллельно этому процессу система автодополнения Visual Studio Code анализирует файл lib.config, располагающийся в корне библиотеки fluent и подключает описанные там класс и модуль.
Таким образом с одной стороны (со стороны движка OneScript) у нас есть полностью работающая библиотека с четырьмя сценариями – двумя классами («публичным» и «служебным») и двумя модулями (так же «публичным» и «служебным»). А со стороны системы автодополнения было прочитано только два сценария – один класс и один модуль.
«Честным» сокрытием внутренних классов это можно назвать с большой натяжкой, но выкручиваемся как можем :)
Еще раз напоминаю, что все классы и модули подключаются в глобальную область видимости. Как следствие возможен конфликт имен между различными библиотеками. Не стоит называть «внутренний» модуль каким-то очень распространенным именем, например, «Параметры». Назовите его «Параметры_ИмяВашейБиблиотеки». Вам как разработчику не составит труда при реализации вашей внутренней логики печатать на пару символов больше, а остальные пользователи скажут вам «спасибо» за бесконфликтную библиотеку.
Сборка пакета
Итак, наш пакет готов к тому, чтобы поделиться им с сообществом. Большинство хороших и известных библиотек собираются в специальное «пакеты» и публикуются в хаб пакетов. «Пакет» - это особым образом упакованный zip-архив, который OneScript Package Manager (opm) умеет собирать, публиковать и устанавливать.
Все хорошие пакеты помимо содержимого в виде кода имеют описание библиотеки в файле README.md. Md – это расширение файлов, обозначающее язык MarkDown. Практически все репозитории, которые вы встретите на просторах GitHub, имеют описание в виде файла README.md.
Создадим в корне каталога calculation файл README.md примерно со следующим содержимым:
Если выполнить команду «Markdown: Открыть область предварительный просмотра» (или нажать комбинацию клавиш ctrl-k, затем v), то откроется отдельное окно, в котором отобразится итоговый внешний вид файла MarkDown:
Для сборки пакета нам необходимо заполнить специальное «описание пакета» - небольшой сценарий, в котором указываются данные о пакете.
Создадим в каталоге calculation новый файл с именем packagedef (без расширения) – package definition:
Содержимое файла:
Описание.Имя("calculation")
.Версия("1.0.0")
.Автор("Nikita Gryzlov")
.АдресАвтора("nixel2007@gmail.com")
.Описание("Арифметические операции для любителей функционального программирования")
.ВерсияСреды("1.0.17")
.ВключитьФайл("src")
.ВключитьФайл("lib.config")
.ВключитьФайл("README.md")
.ЗависитОт("asserts", "0.3.1")
;
Эти эльфийские письмена на самом деле не делают ничего магического. Packagedef – это специальный файл, который используется opm при сборке пакета. Это обычный сценарий на языке 1C/OneScript. При его инициализации в область видимости добавляется экземпляр класса «ОписаниеПакета», содержимое этого класса можно подсмотреть в репозитории opm (https://github.com/oscript-library/opm/blob/master/src/%D0%9A%D0%BB%D0%B0%D1%81%D1%81%D1%8B/%D0%9E%D0%BF%D0%B8%D1%81%D0%B0%D0%BD%D0%B8%D0%B5%D0%9F%D0%B0%D0%BA%D0%B5%D1%82%D0%B0.os). В файле packagedef мы вызываем методы этого класса, которые занимаются заполнением содержимого.
Пробежимся по используемым функциям:
- Описание пакета обычно начинается с указания идентификатора (имени) библиотеки. Далее в произвольном порядке следуют все остальные поля
- Версия пакета (библиотеки)
- Автор
- Email автора для связи
- Текстовое описание пакета
- ВерсияСреды – минимальная версия движка OneScript, требуемая для использования данного пакета
- ВключитьФайл – пути к файлам и каталогам, которые необходимо добавить в пакет при сборке. Сюда же можно добавить файлы примеров использования, любую документацию и вообще все, что вы пожелаете загрузить на конечный компьютер пользователя
- ЗависитОт – зависимости нашего пакета от других пакетов. Класс ЧислоВПамяти использует библиотеку asserts для проверки типов переданных значений, поэтому нам нужно указать эту зависимость в описании пакета. При установке нашего пакета opm будет контролировать наличие всех зависимостей и доустанавливать нужные при необходимости
Если начать копаться во внутренностях opm, то можно выяснить, что если packagedef будет содержать вызовы функций «ОпределяетКласс» и «ОпределяетМодуль», то файл lib.config сформируется автоматически. Т.е. в целом мы могли его и не формировать руками, а описать в едином месте в packagedef. Однако, если структура каталогов библиотеки не будет удовлетворять «соглашению о структуре каталогов», то нам самим будет тяжелее ее разрабатывать и тестировать, т.к. тесты и отладчик не будут знать, какие классы и откуда загружать. Поэтому моя личная рекомендация – формируем файл руками и «включаем» его в packagedef.
Описание пакета готово, попробуем собрать наш пакет! Для этого выполним команду opm build с указанием пути к каталогу, в котором располагается файл packagedef:
На выходе получаем файл ospx – собранный пакет. При желании его можно открыть любым zip-архиватором и поисследовать.
Что мы можем делать с этим пакетом? Как минимум мы можем его установить!
У opm есть команда install, среди параметров которой есть параметр -f, отвечающий за путь к файлу пакета, который необходимо установить. Если он не указан, то пакет скачивается с хаба пакетов в облаке, иначе берется указанный файл.
Если OneScript у вас установлен «как обычно» в Program files, то может потребоваться запуск командной строки с правами администратора.
Выполнение этой команды привело к установке пакета calculation в системный каталог пакетов. Физически это выглядит так:
Создался новый каталог по имени пакета (calculation), в каталоге расположены файлы, которые мы указали как «включаемые» в packagedef.
Зачем это все? Теперь мы можем «использовать» нашу библиотеку «по имени»! Вернемся в сценарий my_project/script.os и заменим строку «использования» библиотеки с «пути» (с кавычками и двумя точками) на подключение «по имени» (без кавычек и каких-либо путей):
Сценарий продолжает работать и выдавать в консоль цифру «4», а значит, мы сделали все правильно.
Если переоткрыть редактор VSCode (или выполнить команду Language 1C (BSL): Update reference cache), то наша библиотека отобразиться Синтакс-Помощнике, в разделе oscript-library:
Публикация пакета в хаб
Наша библиотека собрана в пакет, дело осталось за малым – поделиться библиотекой со всем миром! Конечно же, можно скидывать файл ospx коллегам в скайп/гиттер/слак, но лучше разместить его в хабе пакетов. Для этого нужно сделать три вещи:
- Исходники библиотеки должны быть опубликованы на GitHub. Я не буду подробно останавливаться на принципах работы с гитом, на ИС есть достаточно базовых статей на эту тему. От вас не требуется ничего сверхтяжелого – создать новый репозиторий на GitHub, закоммитить все содержимое библиотеки и запушить изменения.
- Постучаться по различным каналам связи к администраторам организации oscript-library (https://github.com/oscript-library) на GitHub или написать в Gitter-канал библиотеки пакетов (https://gitter.im/EvilBeaver/oscript-library) – дать ссылку на ваш реп и попросить добавить в хаб пакетов. Через некоторое время вам скажут «угумс!» и вы можете приступать непосредственно к публикации.
- Публикация пакета производится так же с помощью утилиты opm.
Теперь подробнее о публикации. Публикация выполняется с помощью команды opm push. Как следует из справки нам необходимо указать два или три параметра:
--file – это путь к файлу ospx. Можно использовать маску поиска.
--token – токен авторизации с сервера GitHub. Новый токен можно создать новый на странице https://github.com/settings/tokens. Сохраните значение токена, если планируете им пользоваться в дальнейшем, GitHub отображает его только в момент генерации, после обновления страницы значение пропадет.
--channel – канал публикации. Если вы публикуете изменения из git-репозитория и ветки master, то указание этого параметра не требуется.
Обычно, я выполняю публикацию с помощью следующей команды:
opm push --file *.ospx --token МойГитхабТокен
После успешного выполнения команды в Gitter-канале oscript-library появится примерно такое сообщение:
И самое главное, что пакет станет доступен в хабе пакетов http://hub.oscript.io.
Заключение
That’s all, folks! Надеюсь, этот гайд поможет вам влиться в увлекательный мир создания библиотек для OneScript, и количество нового и полезного начнет стремительно расти :)
Финальное содержимое каталога calculation можно найти в моем репозитории на GitHub - https://github.com/nixel2007/infostart-calculation