diff --git a/.cursor/skills/release-notes/SKILL.md b/.cursor/skills/release-notes/SKILL.md index 7f495ac89..4f4903ab2 100644 --- a/.cursor/skills/release-notes/SKILL.md +++ b/.cursor/skills/release-notes/SKILL.md @@ -32,7 +32,7 @@ git --no-pager log --pretty="* %%s" --no-merges %TAGNAME%..HEAD Те коммиты, которые ссылок на гитхаб не содержат, ты должен просто прочитать и решить - стоит их включать в release-notes или нет, содержат ли они описание конкретной сделанной функциональности, или это промежуточный, технический и не требующий публикации коммит. -Далее, ты должен сделать итоговый дайджест того, что было сделано в версии и записать это в файл install/release-notes.md. Каждая задача это отдельный bullet-point, причем ссылки на github должны быть сделаны в markdown реальными кликабельными ссылками. +Далее, ты должен сделать итоговый дайджест того, что было сделано в версии и записать это в файл install/release-notes.md. Каждая задача это отдельный bullet-point, причем ссылки на github должны быть сделаны в markdown реальными кликабельными ссылками. Формат строго следующий: сначала ссылка на задачу, потом описание. Если ссылку создать невозможно, тогда только текст задачи. ## Формат файла release-notes diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index a53c8d0ab..a10a7feb4 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -17,11 +17,11 @@ jobs: # Setup steps for OneScript development environment steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - run: git config --global core.quotepath false - - uses: actions/setup-dotnet@v5 + - uses: actions/setup-dotnet@v6 with: dotnet-version: '8.0.x' diff --git a/.github/workflows/rebase.yml b/.github/workflows/rebase.yml index 2902fe38c..95fc7d0d8 100644 --- a/.github/workflows/rebase.yml +++ b/.github/workflows/rebase.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the latest code - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: token: ${{ secrets.GITHUB_TOKEN }} fetch-depth: 0 # otherwise, you will fail to push refs to dest repo diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index f41bd6bde..6a56cdb96 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -11,22 +11,22 @@ jobs: if: (github.repository == 'EvilBeaver/OneScript' ) && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.event.repository.full_name) steps: - name: Set up JDK 17 - uses: actions/setup-java@v5 + uses: actions/setup-java@v6.0.0 with: java-version: 17 distribution: temurin - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - name: Cache SonarQube packages - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: ~\sonar\cache key: ${{ runner.os }}-sonar restore-keys: ${{ runner.os }}-sonar - name: Cache SonarQube scanner id: cache-sonar-scanner - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: .\.sonar\scanner key: ${{ runner.os }}-sonar-scanner diff --git a/Jenkinsfile b/Jenkinsfile index 7fa3839c8..53f1395fe 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -4,7 +4,7 @@ pipeline { agent none environment { - VersionPrefix = '2.1.0' + VersionPrefix = '2.2.0' outputEnc = '65001' } diff --git a/README.md b/README.md index 55b917ba8..c852f17bf 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,18 @@ dotnet msbuild Build.csproj /t:BuildDocumentation dotnet build src/oscript/oscript.csproj ``` +### Тесты NativeApi (`native-api.os`) + +Проверяют поддержку внешних компонент NativeApi. На macOS не запускаются. + +Нужен компилятор C++ (см. раздел «Ручная локальная сборка»). Сборка нативных компонент (прокси `ScriptEngine.NativeApi` и тестовая `AddInNative` из `tests/native-api/`) и прогон приёмочных тестов, включая `native-api.os`: + +```bat +dotnet msbuild Build.csproj /t:BuildAll;Test +``` + +Без C++-компилятора нативная часть не собирается (`NoCppCompiler=True`), и эти тесты пройти не смогут. + # Документация для разработчиков Если вы хотите контрибьютить в проект, познакомьтесь с дополнительными документами в каталоге [`docs/`](docs/): diff --git a/install/WizardImage.bmp b/install/WizardImage.bmp new file mode 100644 index 000000000..ad204ec05 Binary files /dev/null and b/install/WizardImage.bmp differ diff --git a/install/install.iss b/install/install.iss new file mode 100644 index 000000000..1ffd621ee --- /dev/null +++ b/install/install.iss @@ -0,0 +1,277 @@ +#include + +#define MyAppName "OneScript" +#define MyAppPublisher "EvilBeaver" +#define MyAppURL "https://oscript.io" + +#define VerMajor +#define VerMinor +#define VerRelease +#define Build + +#define ArtifactRoot "..\built\tmp" +#define OvmExeSource "ovm.exe" + +#define WizardImageSource "WizardImage.bmp" +#define LicenseSource "..\LICENSE" + + +#ifndef Suffix + #define Suffix "x86" +#endif + +#if Suffix == "x64" + #define Binaries="bin" +#else + #define Binaries="bin32" +#endif + +#expr GetVersionComponents(ArtifactRoot + "\" + Binaries + "\ScriptEngine.dll", VerMajor, VerMinor, VerRelease, Build) + +[Setup] +AppName = {#MyAppName} +AppVersion = {#VerMajor}.{#VerMinor}.{#VerRelease} +AppPublisher = {#MyAppPublisher} +AppPublisherURL = {#MyAppURL} +AppSupportURL = {#MyAppURL} +AppUpdatesURL = {#MyAppURL} +DefaultDirName = {localappdata}\OneScript +LicenseFile = {#LicenseSource} +DisableProgramGroupPage = yes +OutputBaseFilename = "OneScript-{#VerMajor}.{#VerMinor}.{#VerRelease}-{#Suffix}" +Compression = lzma +SolidCompression = yes +WizardStyle = modern +DisableWelcomePage = no +WizardImageFile = {#WizardImageSource} +PrivilegesRequired = lowest + +#if Suffix == "x64" + ArchitecturesInstallIn64BitMode="x64" +#endif + +[Languages] +Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl" + +[Files] +Source: "{#ArtifactRoot}\*"; DestDir: "{app}\{#VerMajor}.{#VerMinor}.{#VerRelease}"; Flags: recursesubdirs +Source: "{#OvmExeSource}"; DestDir: "{app}"; DestName: "ovm.exe"; Check: InstallOvmSelected + + +[UninstallDelete] +Type: filesandordirs; Name: "{app}\{#VerMajor}.{#VerMinor}.{#VerRelease}" + +[Code] + +var + OvmPage: TWizardPage; + OvmIntroLabel: TNewStaticText; + InstallOvmCheckBox: TNewCheckBox; + +procedure InitializeWizard; +begin + OvmPage := CreateCustomPage(wpSelectDir, + 'Менеджер версий OVM', + 'Установка менеджера версий OneScript'); + + OvmIntroLabel := TNewStaticText.Create(OvmPage); + OvmIntroLabel.Parent := OvmPage.Surface; + OvmIntroLabel.Left := 0; + OvmIntroLabel.Top := 0; + OvmIntroLabel.Width := OvmPage.SurfaceWidth; + OvmIntroLabel.Height := ScaleY(40); + OvmIntroLabel.AutoSize := False; + OvmIntroLabel.WordWrap := True; + OvmIntroLabel.Caption := + 'OneScript Version Manager (OVM) - утилита, предназначенная для установки, обновления и переключения между различными версиями OneScript'; + + InstallOvmCheckBox := TNewCheckBox.Create(OvmPage); + InstallOvmCheckBox.Parent := OvmPage.Surface; + InstallOvmCheckBox.Left := 0; + InstallOvmCheckBox.Top := OvmIntroLabel.Top + OvmIntroLabel.Height + ScaleY(14); + InstallOvmCheckBox.Width := OvmPage.SurfaceWidth; + InstallOvmCheckBox.Caption := 'Установить OVM'; + InstallOvmCheckBox.Checked := True; +end; + +function InstallOvmSelected(): Boolean; +begin + Result := InstallOvmCheckBox.Checked; +end; + +function RemoveDirectoryLink(const LinkPath: String): Boolean; +var + ResultCode: Integer; +begin + if not DirExists(LinkPath) then + begin + Result := True; + Exit; + end; + Result := Exec('cmd.exe', '/C rmdir "' + LinkPath + '"', + '', SW_HIDE, ewWaitUntilTerminated, ResultCode) and (ResultCode = 0); +end; + +function CreateDirectoryJunction(const LinkPath, TargetPath: String): Boolean; +var + ResultCode: Integer; +begin + if not DirExists(TargetPath) then + begin + MsgBox('Каталог версии OneScript не найден:' + #13#10 + TargetPath, mbError, MB_OK); + Result := False; + Exit; + end; + + if not RemoveDirectoryLink(LinkPath) then + begin + MsgBox('Не удалось удалить существующую ссылку current:' + #13#10 + LinkPath, mbError, MB_OK); + Result := False; + Exit; + end; + + Result := Exec('cmd.exe', '/C mklink /J "' + LinkPath + '" "' + TargetPath + '"', + '', SW_HIDE, ewWaitUntilTerminated, ResultCode) and (ResultCode = 0); + + if not Result then + MsgBox( + 'Не удалось создать junction current -> версия.' + #13#10 + + 'Запустите установщик от имени администратора или включите режим разработчика Windows.' + #13#10#13#10 + + 'Link: ' + LinkPath + #13#10 + + 'Target: ' + TargetPath, + mbError, MB_OK); +end; + +function PathContains(const Path, Entry: String): Boolean; +begin + Result := Pos(';' + Entry + ';', ';' + Path + ';') > 0; +end; + +function NeedsAddPath(const Param: String): Boolean; +var + OrigPath: String; +begin + if not RegQueryStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', OrigPath) then + begin + Result := True; + Exit; + end; + Result := not PathContains(OrigPath, Param); +end; + +procedure PrependUserPath(const Entry: String); +var + OldPath, NewPath: String; +begin + if not NeedsAddPath(Entry) then + Exit; + if RegQueryStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', OldPath) then + begin + if OldPath = '' then + NewPath := Entry + else + NewPath := Entry + ';' + OldPath; + RegWriteExpandStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', NewPath); + end + else + RegWriteExpandStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', Entry); +end; + +procedure ActivateOneScriptLikeOvm(const InstallDir: String); +var + VersionDir, CurrentLink, OscriptBin, OldPath, NewPath: String; +begin + VersionDir := InstallDir + '\{#VerMajor}.{#VerMinor}.{#VerRelease}'; + CurrentLink := InstallDir + '\current'; + OscriptBin := CurrentLink + '\bin'; + + if not CreateDirectoryJunction(CurrentLink, VersionDir) then + Exit; + + if InstallOvmSelected then + begin + RegWriteStringValue(HKEY_CURRENT_USER, 'Environment', 'OVM_INSTALL_PATH', InstallDir); + + RegWriteExpandStringValue(HKEY_CURRENT_USER, 'Environment', 'OVM_OSCRIPTBIN', OscriptBin); + + if RegQueryStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', OldPath) then + begin + if not PathContains(OldPath, '%OVM_OSCRIPTBIN%') then + begin + if OldPath = '' then + NewPath := '%OVM_OSCRIPTBIN%' + else + NewPath := '%OVM_OSCRIPTBIN%;' + OldPath; + RegWriteExpandStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', NewPath); + end; + end + else + RegWriteExpandStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', '%OVM_OSCRIPTBIN%'); + + if NeedsAddPath(InstallDir) then + begin + if RegQueryStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', OldPath) then + begin + if OldPath = '' then + NewPath := InstallDir + else + NewPath := OldPath + ';' + InstallDir; + RegWriteExpandStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', NewPath); + end + else + RegWriteExpandStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', InstallDir); + end; + Exit; + end; + + PrependUserPath(OscriptBin); +end; + +procedure CurStepChanged(CurStep: TSetupStep); +begin + if CurStep = ssPostInstall then + ActivateOneScriptLikeOvm(ExpandConstant('{app}')); +end; + +function RemoveFromPath(const Path, Entry: String): String; +var + ResultPath: String; +begin + ResultPath := Path; + StringChangeEx(ResultPath, Entry + ';', '', True); + StringChangeEx(ResultPath, ';' + Entry, '', True); + if ResultPath = Entry then + ResultPath := ''; + Result := ResultPath; +end; + +procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); +var + InstallDir, ExistingValue, OldPath, NewPath: String; +begin + if CurUninstallStep = usPostUninstall then + begin + InstallDir := ExpandConstant('{app}'); + + RemoveDirectoryLink(InstallDir + '\current'); + + if RegQueryStringValue(HKEY_CURRENT_USER, 'Environment', 'OVM_INSTALL_PATH', ExistingValue) then + if ExistingValue = InstallDir then + RegDeleteValue(HKEY_CURRENT_USER, 'Environment', 'OVM_INSTALL_PATH'); + + if RegQueryStringValue(HKEY_CURRENT_USER, 'Environment', 'OVM_OSCRIPTBIN', ExistingValue) then + if ExistingValue = InstallDir + '\current\bin' then + RegDeleteValue(HKEY_CURRENT_USER, 'Environment', 'OVM_OSCRIPTBIN'); + + if RegQueryStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', OldPath) then + begin + NewPath := RemoveFromPath(OldPath, '%OVM_OSCRIPTBIN%'); + NewPath := RemoveFromPath(NewPath, InstallDir + '\current\bin'); + NewPath := RemoveFromPath(NewPath, InstallDir); + if NewPath = '' then + RegDeleteValue(HKEY_CURRENT_USER, 'Environment', 'Path') + else + RegWriteExpandStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', NewPath); + end; + end; +end; diff --git a/install/opm.ospx b/install/opm.ospx index b30cc6ee3..ecf313429 100644 Binary files a/install/opm.ospx and b/install/opm.ospx differ diff --git a/install/release-notes.md b/install/release-notes.md index 2351dee9c..5be5b1c6c 100644 --- a/install/release-notes.md +++ b/install/release-notes.md @@ -1,23 +1,22 @@ -# Версия 2.1.0 +# Версия 2.2.0 ## Новые возможности -* Реализован конфигурационный параметр `binaryData.inMemoryMaxBytes` для настройки порога хранения двоичных данных в памяти или на диске [#1667](https://github.com/EvilBeaver/OneScript/issues/1667) -* Добавлено свойство `СистемнаяИнформация.ПолнаяКоманднаяСтрока` для получения полной строки запуска процесса: [#1678](https://github.com/EvilBeaver/OneScript/issues/1678). -* Реализована поддержка оператора `Перейти` / `Goto` и меток: [#1468](https://github.com/EvilBeaver/OneScript/issues/1468), [#1329](https://github.com/EvilBeaver/OneScript/issues/1329), [#6](https://github.com/EvilBeaver/OneScript/issues/6). -* В метод `Процесс.Завершить()` добавлен параметр завершения дочерних процессов [#1660](https://github.com/EvilBeaver/OneScript/pull/1660). -* В `ЧтениеJSON` добавлен метод `ОткрытьПоток()`, чтобы объект мог работать с потоками [#1679](https://github.com/EvilBeaver/OneScript/issues/1679). +* [#1725](https://github.com/EvilBeaver/OneScript/pull/1725) Добавлена глобальная функция `ТекущийПоток()`, возвращающая объект `ПотокИсполнения`. +* Добавлен CLI-флаг `-c` для выполнения кода BSL из строки командной строки. +* [#1532](https://github.com/EvilBeaver/OneScript/issues/1532). В `ЗаписьJSON` добавлен метод `ОткрытьПоток()`, чтобы объект мог писать в потоки. Также у `ЗаписьJSON` появилось свойство `Параметры`. Улучшена совместимость API с платформой 1С:Предприятие. +* [#1711](https://github.com/EvilBeaver/OneScript/pull/1711) Добавлена глобальная функция `ЗаписатьДатуJSON` / `WriteJSONDate` и перечисление `ВариантЗаписиДатыJSON` для сериализации даты в JSON в форматах, совместимых с 1С. +* В `СписокЗначений` добавлены свойства `ТипЗначения` и `ДоступныеЗначения`, у `ЭлементСпискаЗначений` добавлен метод `ПолучитьИдентификатор`. +* `СписокЗначений.ЗагрузитьЗначения` приводит значения с учётом `ТипЗначения`; исправлены сортировка по значению с учётом типов, поиск по `ДоступныеЗначения`, преобразование в строку и проверки индекса. ## Исправление ошибок -* Исправлена работа `ЗагрузитьСценарий()` в native-режиме: корректный возвращаемый тип и обработка ошибок компиляции скрипта: [#1675](https://github.com/EvilBeaver/OneScript/issues/1675). -* Исправлена native-компиляция вызовов методов с дополнительным параметром процесса (включая кейс `ТаблицаЗначений.Сортировать()`): [#1673](https://github.com/EvilBeaver/OneScript/issues/1673). -* Исправлено поведение `ХешированиеДанных`: инкрементальное хеширование, освобождение файлов и совместимость обработки параметра `Количество`: [#1683](https://github.com/EvilBeaver/OneScript/issues/1683). -* Исправлено чтение JSON-строк, содержащих ISO-дату: они больше не преобразуются ошибочно в `Дата`: [#1681](https://github.com/EvilBeaver/OneScript/issues/1681). -* Исправлена ошибка компилятора при некорректном "висячем" окончании синтаксического блока (улучшена диагностика ошибок): [#1685](https://github.com/EvilBeaver/OneScript/issues/1685). -* Метод `ТаблицаЗначений.Найти` теперь использует индекс таблицы значений. Ранее индекс использовался только методом `НайтиСтроки` [#1661](https://github.com/EvilBeaver/OneScript/issues/1661) -* В режиме Native некорректно работало сложение даты с числом. - -## Прочее - -* Обновлена документация проекта и разделы установки под актуальную платформу .NET 8. \ No newline at end of file +* [#1359](https://github.com/EvilBeaver/OneScript/issues/1359). Значительно улучшена совместимость Native API с 1С: Поддержаны выходные параметры методов, исправлена ошибка регистрации типов, реализован обмен значениями типа `Дата`, исправлен жизненный цикл библиотек и устойчивость колбэков к сборке мусора. +* [#1718](https://github.com/EvilBeaver/OneScript/issues/1718) Исправлена потокобезопасность `ФоновыеЗадания`: одновременный запуск и опрос списка больше не приводят к `InvalidOperationException`, `ПолучитьТекущее()` ищет задание за O(1). +* `ФоновыеЗадания.ОжидатьЗавершенияЗадач()` больше не удаляет из реестра задания, запущенные во время ожидания. +* [#1656](https://github.com/EvilBeaver/OneScript/issues/1656) Приведена точность типа `Дата` и сериализация дат в JSON к поведению 1С. +* Исправлено округление долей секунды при сложении/вычитании даты с числом (граничные значения вроде `0.00015`). +* [#1696](https://github.com/EvilBeaver/OneScript/issues/1696) Запрещено чередование директив `#native` и `#stack` в одном модуле: режим компиляции фиксируется первой аннотацией. +* [#1705](https://github.com/EvilBeaver/OneScript/issues/1705) Исправлена `ЗаписьXML.Закрыть()`: нет падения без установленного приёмника, метод возвращает строку. +* [#1707](https://github.com/EvilBeaver/OneScript/issues/1707) Исправлена совместимость и обработка ошибок `ЗаписьXML.ЗаписатьТипДокумента`. +* Исправлено экранирование символов при записи JSON. \ No newline at end of file diff --git a/src/Component/SimpleClass.cs b/src/Component/SimpleClass.cs index 54b743579..d9dac2a07 100644 --- a/src/Component/SimpleClass.cs +++ b/src/Component/SimpleClass.cs @@ -6,6 +6,8 @@ This Source Code Form is subject to the terms of the ----------------------------------------------------------*/ using OneScript.Contexts; +using OneScript.Execution; +using OneScript.Types; using ScriptEngine.Machine; using ScriptEngine.Machine.Contexts; @@ -14,6 +16,16 @@ namespace Component [ContextClass("ПростоКласс")] public sealed class SimpleClass : AutoContext, ISimple { + + // Для вызова некоторых операций значений IValue, таких как AsString(), Count(), требуется указание активного процесса. + // Процесс может быть передан как параметр в конструкторе или как первый параметр методов класса + private readonly IBslProcess _bslProcess; + + private SimpleClass(IBslProcess bslProcess) + { + _bslProcess = bslProcess; + } + [ContextProperty("СвойствоПеречисление")] public SimpleEnum EnumProperty { get; set; } @@ -21,18 +33,33 @@ public sealed class SimpleClass : AutoContext, ISimple public int IntProperty { get; set; } [ContextProperty("СвойствоСПроизвольнымЗначением")] - public IValue AnyValueProperty { get; set; } + public IValue AnyValueProperty { get; set; } = ValueFactory.Create(); - [ScriptConstructor] - public static SimpleClass Constructor() + [ContextMethod("МетодСПроцессом")] + public string MethodWithProcess(IBslProcess bslProcess) + { + // Параметр IBslProcess bslProcess не будет виден из скрипта. + // Скрипт будет считать такой метод методом без параметров. + return AnyValueProperty.AsString(bslProcess); + } + + [ScriptConstructor] + public static SimpleClass Constructor(TypeActivationContext ctx) { - return new SimpleClass(); + + // В отличие от методов в конструктор передается TypeActivationContext. + + // Параметр TypeActivationContext не будет виден из скрипта. + // Скрипт будет считать такой конструктор конструктором без параметров. + return new SimpleClass(ctx.CurrentProcess); } [ScriptConstructor] - public static SimpleClass Constructor(int initialProperty) + public static SimpleClass Constructor(TypeActivationContext ctx, int initialProperty) { - var result = new SimpleClass(); + // Параметр TypeActivationContext не будет виден из скрипта. + // Скрипт будет считать такой конструктор конструктором с ОДНИМ параметром. + var result = new SimpleClass(ctx.CurrentProcess); result.IntProperty = initialProperty; return result; } diff --git a/src/Component/UseLibrary.cs b/src/Component/UseLibrary.cs deleted file mode 100644 index 1ca7ff8f5..000000000 --- a/src/Component/UseLibrary.cs +++ /dev/null @@ -1,100 +0,0 @@ -/*---------------------------------------------------------- -This Source Code Form is subject to the terms of the -Mozilla Public License, v.2.0. If a copy of the MPL -was not distributed with this file, You can obtain one -at http://mozilla.org/MPL/2.0/. -----------------------------------------------------------*/ - -using OneScript.StandardLibrary; -using OneScript.StandardLibrary.Binary; -using OneScript.StandardLibrary.Collections; -using OneScript.StandardLibrary.Collections.ValueList; -using OneScript.StandardLibrary.Collections.ValueTable; -using OneScript.StandardLibrary.Collections.ValueTree; -using OneScript.StandardLibrary.Http; -using OneScript.StandardLibrary.Net; -using OneScript.StandardLibrary.Processes; -using OneScript.StandardLibrary.Text; -using OneScript.StandardLibrary.Xml; -using OneScript.StandardLibrary.Zip; -using OneScript.StandardLibrary.TypeDescriptions; -using ScriptEngine.Machine; - -namespace Component -{ - public static class UseLibrary - { - public static void Use() where T : IValue - { - } - - public static void Use() - { - /* Суть следующих выражений в том, что они перестанут компилироваться, - если какой-либо из классов поменяет область видимости - или переедет в иное пространство имён - (https://github.com/EvilBeaver/OneScript/commit/20e48fa7692b5430b819eb4b6982be1b591e536f)*/ - - Use(); - Use(); - Use(); - Use(); - Use(); - Use(); - Use(); - - Use(); - Use(); - Use(); - Use(); - - Use(); - Use(); - Use(); - Use(); - Use(); - Use(); - Use(); - Use(); - Use(); - Use(); - Use(); - Use(); - Use(); - - Use(); - Use(); - Use(); - Use(); - - Use(); - Use(); - - Use(); - Use(); - Use(); - Use(); - - Use(); - Use(); - - Use(); - Use(); - Use(); - Use(); - Use(); - - Use(); - Use(); - Use(); - Use(); - - Use(); - Use(); - Use(); - - Use(); - Use(); - } - } -} \ No newline at end of file diff --git a/src/OneScript.Core/Execution/IBslProcess.cs b/src/OneScript.Core/Execution/IBslProcess.cs index 2ef3b4233..297f11f63 100644 --- a/src/OneScript.Core/Execution/IBslProcess.cs +++ b/src/OneScript.Core/Execution/IBslProcess.cs @@ -7,6 +7,7 @@ This Source Code Form is subject to the terms of the #nullable enable +using System; using OneScript.Contexts; using OneScript.DependencyInjection; using OneScript.Values; @@ -15,7 +16,10 @@ This Source Code Form is subject to the terms of the namespace OneScript.Execution { /// - /// Готовый к исполнению bsl-процесс, с настроенным окружением + /// Готовый к исполнению bsl-процесс, с настроенным окружением. + /// + /// Процесс освобождается тем, кто его создал, когда единица исполнения отработала. + /// Освобождение завершает поток исполнения процесса. /// public interface IBslProcess { @@ -33,7 +37,18 @@ public interface IBslProcess /// Сервисы текущего процесса /// public IServiceContainer Services { get; } - + public int VirtualThreadId { get; } + + /** + * Обертка текущего процесса в терминах BSL. + * Может отсутствовать, полностью определяется хост-приложением процесса. + */ + public IRuntimeContextInstance? BslWrapper + { + get => null; + // ReSharper disable once ValueParameterNotUsed + set { } + } } } \ No newline at end of file diff --git a/src/OneScript.Core/Values/BslDateValue.cs b/src/OneScript.Core/Values/BslDateValue.cs index 91ac459a0..a9376132c 100644 --- a/src/OneScript.Core/Values/BslDateValue.cs +++ b/src/OneScript.Core/Values/BslDateValue.cs @@ -14,6 +14,8 @@ namespace OneScript.Values { public sealed class BslDateValue : BslPrimitiveValue, IBslComparable { + private const long TicksPerStep = TimeSpan.TicksPerSecond / 10000; // 1000 + private readonly DateTime _value; private BslDateValue(DateTime value) @@ -21,7 +23,23 @@ private BslDateValue(DateTime value) _value = value; } - public static BslDateValue Create(DateTime value) => new BslDateValue(value); + public static DateTime Normalize(DateTime value) + { + var ticks = value.Ticks; + var rounded = (long)Math.Round((double)ticks / TicksPerStep, MidpointRounding.AwayFromZero) * TicksPerStep; + return new DateTime(rounded, value.Kind); + } + + public static BslDateValue Create(DateTime value) => new BslDateValue(Normalize(value)); + + public static DateTime AddSeconds(DateTime date, decimal seconds) + { + var rounded = Math.Round(seconds, 4, MidpointRounding.AwayFromZero); + return Normalize(date.AddTicks((long)(rounded * TimeSpan.TicksPerSecond))); + } + + public static DateTime SubtractSeconds(DateTime date, decimal seconds) => + AddSeconds(date, -seconds); public override int CompareTo(BslValue other) { @@ -67,15 +85,11 @@ public override string ToString() #region Date Arithmetics - public static DateTime operator +(BslDateValue left, decimal right) - { - return left._value.AddSeconds((double) right); - } + public static DateTime operator +(BslDateValue left, decimal right) => + AddSeconds(left._value, right); - public static DateTime operator -(BslDateValue left, decimal right) - { - return left._value.AddSeconds(-(double) right); - } + public static DateTime operator -(BslDateValue left, decimal right) => + SubtractSeconds(left._value, right); public static decimal operator -(BslDateValue left, DateTime right) { diff --git a/src/OneScript.Language/SyntaxAnalysis/LocalizedErrors.cs b/src/OneScript.Language/SyntaxAnalysis/LocalizedErrors.cs index 864bd4ede..fca4526e5 100644 --- a/src/OneScript.Language/SyntaxAnalysis/LocalizedErrors.cs +++ b/src/OneScript.Language/SyntaxAnalysis/LocalizedErrors.cs @@ -137,6 +137,10 @@ public static CodeError InvalidRegionName(string name) => public static CodeError DirectiveIsMissing(string directive) => Create($"Пропущена директива #{directive}", $"Directive #{directive} is missing"); + public static CodeError RuntimeDirectiveAlreadyDefined() => + Create("Директива режима выполнения (#native или #stack) может быть указана только один раз", + "Runtime directive (#native or #stack) can be specified only once"); + public static CodeError LibraryNameExpected() => Create("Ожидается имя библиотеки", "Library name expected"); diff --git a/src/OneScript.Native/Compiler/DateOperations.cs b/src/OneScript.Native/Compiler/DateOperations.cs index 4627a2d2f..3b5e7d523 100644 --- a/src/OneScript.Native/Compiler/DateOperations.cs +++ b/src/OneScript.Native/Compiler/DateOperations.cs @@ -8,11 +8,19 @@ This Source Code Form is subject to the terms of the using System; using System.Diagnostics; using System.Linq.Expressions; +using System.Reflection; +using OneScript.Values; namespace OneScript.Native.Compiler { internal static class DateOperations { + private static readonly MethodInfo AddSecondsMethod = + typeof(BslDateValue).GetMethod(nameof(BslDateValue.AddSeconds)); + + private static readonly MethodInfo SubtractSecondsMethod = + typeof(BslDateValue).GetMethod(nameof(BslDateValue.SubtractSeconds)); + /// /// Операция смещения дат (прибавление или удаление секунд /// @@ -25,19 +33,15 @@ public static Expression DateOffsetOperation(Expression left, Expression right, { Debug.Assert(left.Type == typeof(DateTime)); Debug.Assert(right.Type == typeof(decimal)); - - var adder = typeof(DateTime).GetMethod(nameof(DateTime.AddSeconds)); - Debug.Assert(adder != null); - - var toDouble = Expression.Convert(right, typeof(double)); - Expression arg = opCode switch + Debug.Assert(AddSecondsMethod != null); + Debug.Assert(SubtractSecondsMethod != null); + + return opCode switch { - ExpressionType.Add => toDouble, - ExpressionType.Subtract => Expression.Negate(toDouble), + ExpressionType.Add => Expression.Add(left, right, AddSecondsMethod), + ExpressionType.Subtract => Expression.Subtract(left, right, SubtractSecondsMethod), _ => throw NativeCompilerException.OperationNotDefined(opCode, left.Type, right.Type) }; - - return Expression.Call(left, adder, arg); } /// diff --git a/src/OneScript.Native/Compiler/NativeRuntimeAnnotationHandler.cs b/src/OneScript.Native/Compiler/NativeRuntimeAnnotationHandler.cs index 763c36147..1a6c34b88 100644 --- a/src/OneScript.Native/Compiler/NativeRuntimeAnnotationHandler.cs +++ b/src/OneScript.Native/Compiler/NativeRuntimeAnnotationHandler.cs @@ -7,6 +7,7 @@ This Source Code Form is subject to the terms of the using System.Collections.Generic; using OneScript.Language; +using OneScript.Language.LexicalAnalysis; using OneScript.Language.SyntaxAnalysis; namespace OneScript.Native.Compiler @@ -21,9 +22,47 @@ public class NativeRuntimeAnnotationHandler : SingleWordModuleAnnotationHandler NativeDirectiveName, StackRuntimeDirectiveName }; + + private bool _runtimeDirectiveDefined; public NativeRuntimeAnnotationHandler(IErrorSink errorSink) : base(Directives, errorSink) { } + + public override void OnModuleEnter() + { + _runtimeDirectiveDefined = false; + base.OnModuleEnter(); + } + + public override void OnModuleLeave() + { + _runtimeDirectiveDefined = false; + base.OnModuleLeave(); + } + + protected override void ParseAnnotationInternal( + ref Lexem lastExtractedLexem, + ILexer lexer, + ParserContext parserContext) + { + if (_runtimeDirectiveDefined) + { + var err = LocalizedErrors.RuntimeDirectiveAlreadyDefined(); + err.Position = new ErrorPositionInfo + { + LineNumber = lastExtractedLexem.Location.LineNumber, + ColumnNumber = lastExtractedLexem.Location.ColumnNumber, + Code = lexer.Iterator.GetCodeLine(lastExtractedLexem.Location.LineNumber), + ModuleName = lexer.Iterator.Source.Name + }; + ErrorSink.AddError(err); + lastExtractedLexem = lexer.NextLexem(); + return; + } + + _runtimeDirectiveDefined = true; + base.ParseAnnotationInternal(ref lastExtractedLexem, lexer, parserContext); + } } -} \ No newline at end of file +} diff --git a/src/OneScript.StandardLibrary/Binary/BinaryDataBuffer.cs b/src/OneScript.StandardLibrary/Binary/BinaryDataBuffer.cs index 1509059fc..55dd5eb7e 100644 --- a/src/OneScript.StandardLibrary/Binary/BinaryDataBuffer.cs +++ b/src/OneScript.StandardLibrary/Binary/BinaryDataBuffer.cs @@ -355,7 +355,7 @@ public void WriteBitwiseXor(int position, BinaryDataBuffer bytes, int number = 0 [ContextMethod("Перевернуть", "Reverse")] public BinaryDataBuffer Reverse() { - var bytes = Bytes.Reverse().ToArray(); + var bytes = Enumerable.Reverse(Bytes).ToArray(); return new BinaryDataBuffer(bytes, ByteOrder); } diff --git a/src/OneScript.StandardLibrary/Binary/EndianBitConverter.cs b/src/OneScript.StandardLibrary/Binary/EndianBitConverter.cs index 2fa9a373b..a921ca2b5 100644 --- a/src/OneScript.StandardLibrary/Binary/EndianBitConverter.cs +++ b/src/OneScript.StandardLibrary/Binary/EndianBitConverter.cs @@ -51,11 +51,11 @@ public byte[] GetBytes(bool value) { if (IsLittleEndian) { - return System.BitConverter.GetBytes(value); + return BitConverter.GetBytes(value); } else { - return System.BitConverter.GetBytes(value).Reverse().ToArray(); + return Enumerable.Reverse(BitConverter.GetBytes(value)).ToArray(); } } /// @@ -73,11 +73,11 @@ public byte[] GetBytes(char value) { if (IsLittleEndian) { - return System.BitConverter.GetBytes(value); + return BitConverter.GetBytes(value); } else { - return System.BitConverter.GetBytes(value).Reverse().ToArray(); + return Enumerable.Reverse(BitConverter.GetBytes(value)).ToArray(); } } /// @@ -96,11 +96,11 @@ public byte[] GetBytes(double value) { if (IsLittleEndian) { - return System.BitConverter.GetBytes(value); + return BitConverter.GetBytes(value); } else { - return System.BitConverter.GetBytes(value).Reverse().ToArray(); + return Enumerable.Reverse(BitConverter.GetBytes(value)).ToArray(); } } /// @@ -119,11 +119,11 @@ public byte[] GetBytes(float value) { if (IsLittleEndian) { - return System.BitConverter.GetBytes(value); + return BitConverter.GetBytes(value); } else { - return System.BitConverter.GetBytes(value).Reverse().ToArray(); + return Enumerable.Reverse(BitConverter.GetBytes(value)).ToArray(); } } /// @@ -141,11 +141,11 @@ public byte[] GetBytes(int value) { if (IsLittleEndian) { - return System.BitConverter.GetBytes(value); + return BitConverter.GetBytes(value); } else { - return System.BitConverter.GetBytes(value).Reverse().ToArray(); + return Enumerable.Reverse(BitConverter.GetBytes(value)).ToArray(); } } /// @@ -163,11 +163,11 @@ public byte[] GetBytes(long value) { if (IsLittleEndian) { - return System.BitConverter.GetBytes(value); + return BitConverter.GetBytes(value); } else { - return System.BitConverter.GetBytes(value).Reverse().ToArray(); + return Enumerable.Reverse(BitConverter.GetBytes(value)).ToArray(); } } /// @@ -185,11 +185,11 @@ public byte[] GetBytes(short value) { if (IsLittleEndian) { - return System.BitConverter.GetBytes(value); + return BitConverter.GetBytes(value); } else { - return System.BitConverter.GetBytes(value).Reverse().ToArray(); + return Enumerable.Reverse(BitConverter.GetBytes(value)).ToArray(); } } /// @@ -207,11 +207,11 @@ public byte[] GetBytes(uint value) { if (IsLittleEndian) { - return System.BitConverter.GetBytes(value); + return BitConverter.GetBytes(value); } else { - return System.BitConverter.GetBytes(value).Reverse().ToArray(); + return Enumerable.Reverse(BitConverter.GetBytes(value)).ToArray(); } } /// @@ -229,11 +229,11 @@ public byte[] GetBytes(ulong value) { if (IsLittleEndian) { - return System.BitConverter.GetBytes(value); + return BitConverter.GetBytes(value); } else { - return System.BitConverter.GetBytes(value).Reverse().ToArray(); + return Enumerable.Reverse(BitConverter.GetBytes(value)).ToArray(); } } /// @@ -251,11 +251,11 @@ public byte[] GetBytes(ushort value) { if (IsLittleEndian) { - return System.BitConverter.GetBytes(value); + return BitConverter.GetBytes(value); } else { - return System.BitConverter.GetBytes(value).Reverse().ToArray(); + return Enumerable.Reverse(BitConverter.GetBytes(value)).ToArray(); } } /// @@ -377,7 +377,7 @@ public int ToInt16(byte[] value, int startIndex) { if (IsLittleEndian) { - return System.BitConverter.ToInt16(value, startIndex); + return BitConverter.ToInt16(value, startIndex); } else { @@ -415,7 +415,7 @@ public int ToInt32(byte[] value, int startIndex) { if (IsLittleEndian) { - return System.BitConverter.ToInt32(value, startIndex); + return BitConverter.ToInt32(value, startIndex); } else { @@ -453,7 +453,7 @@ public long ToInt64(byte[] value, int startIndex) { if (IsLittleEndian) { - return System.BitConverter.ToInt64(value, startIndex); + return BitConverter.ToInt64(value, startIndex); } else { @@ -500,11 +500,11 @@ public float ToSingle(byte[] value, int startIndex) { if (IsLittleEndian) { - return System.BitConverter.ToSingle(value, startIndex); + return BitConverter.ToSingle(value, startIndex); } else { - return System.BitConverter.ToSingle(value.Reverse().ToArray(), value.Length - sizeof(Single) - startIndex); + return BitConverter.ToSingle(Enumerable.Reverse(value).ToArray(), value.Length - sizeof(Single) - startIndex); } } /// @@ -528,11 +528,11 @@ public string ToString(byte[] value) { if (IsLittleEndian) { - return System.BitConverter.ToString(value); + return BitConverter.ToString(value); } else { - return System.BitConverter.ToString(value.Reverse().ToArray()); + return BitConverter.ToString(Enumerable.Reverse(value).ToArray()); } } /// @@ -563,11 +563,11 @@ public string ToString(byte[] value, int startIndex) { if (IsLittleEndian) { - return System.BitConverter.ToString(value, startIndex); + return BitConverter.ToString(value, startIndex); } else { - return System.BitConverter.ToString(value.Reverse().ToArray(), startIndex); + return BitConverter.ToString(Enumerable.Reverse(value).ToArray(), startIndex); } } /// @@ -607,11 +607,11 @@ public string ToString(byte[] value, int startIndex, int length) { if (IsLittleEndian) { - return System.BitConverter.ToString(value, startIndex, length); + return BitConverter.ToString(value, startIndex, length); } else { - return System.BitConverter.ToString(value.Reverse().ToArray(), startIndex, length); + return BitConverter.ToString(Enumerable.Reverse(value).ToArray(), startIndex, length); } } /// @@ -643,11 +643,11 @@ public ushort ToUInt16(byte[] value, int startIndex) { if (IsLittleEndian) { - return System.BitConverter.ToUInt16(value, startIndex); + return BitConverter.ToUInt16(value, startIndex); } else { - return System.BitConverter.ToUInt16(value.Reverse().ToArray(), value.Length - sizeof(UInt16) - startIndex); + return BitConverter.ToUInt16(Enumerable.Reverse(value).ToArray(), value.Length - sizeof(UInt16) - startIndex); } } /// @@ -680,7 +680,7 @@ public uint ToUInt32(byte[] value, int startIndex) { if (IsLittleEndian) { - return System.BitConverter.ToUInt32(value, startIndex); + return BitConverter.ToUInt32(value, startIndex); } else { @@ -717,7 +717,7 @@ public ulong ToUInt64(byte[] value, int startIndex) { if (IsLittleEndian) { - return System.BitConverter.ToUInt64(value, startIndex); + return BitConverter.ToUInt64(value, startIndex); } else { diff --git a/src/OneScript.StandardLibrary/Collections/ValueList/ValueListImpl.cs b/src/OneScript.StandardLibrary/Collections/ValueList/ValueListImpl.cs index 6f3c1b288..f2f7115e9 100644 --- a/src/OneScript.StandardLibrary/Collections/ValueList/ValueListImpl.cs +++ b/src/OneScript.StandardLibrary/Collections/ValueList/ValueListImpl.cs @@ -13,6 +13,7 @@ This Source Code Form is subject to the terms of the using OneScript.Execution; using OneScript.Types; using OneScript.Values; +using OneScript.StandardLibrary.TypeDescriptions; using ScriptEngine.Machine; using ScriptEngine.Machine.Contexts; @@ -25,43 +26,74 @@ namespace OneScript.StandardLibrary.Collections.ValueList public class ValueListImpl : AutoCollectionContext { readonly List _items; + ulong _maxId; + public ValueListImpl() { _items = new List(); } - public override bool IsIndexed - { - get - { - return true; - } - } + public override bool IsIndexed => true; public override IValue GetIndexedValue(IValue index) { - if (index.SystemType == BasicTypes.Number) - { - return GetValue(index); - } - else - { - return base.GetIndexedValue(index); - } + return index is BslNumericValue ? GetValue(index) : base.GetIndexedValue(index); } public override void SetIndexedValue(IValue index, IValue val) { if (index.SystemType == BasicTypes.Number) + throw IndexedIsReadonlyException(); + + base.SetIndexedValue(index, val); + } + + /// + /// Определяет тип для значений, которые могут храниться в элементах данного списка значений + /// + /// ОписаниеТипов + [ContextProperty("ТипЗначения", "ValueType")] + public TypeDescription ListValueType { get; set; } = new(); + + ValueListImpl _availableValues; + + /// + /// Ограничивает допустимые значения для элементов данного списка значений. + /// Возможные значения: Неопределено - ограничения отсутствуют, + /// СписокЗначений - список допустимых значений + /// + /// + /// Проверка допустимости добавляемых значений производится после приведения к указанным в ТипЗначения (если есть) + /// + /// СписокЗначений или Неопределено + [ContextProperty("ДоступныеЗначения", "AvailableValues")] + public IValue AvailableValues + { + get { - throw new RuntimeException("Индексированное значение доступно только для чтения"); - } - else - { - base.SetIndexedValue(index, val); + if (_availableValues != null) + return _availableValues; + + return BslUndefinedValue.Instance; } - } + set + { + switch (value) + { + case BslUndefinedValue: + _availableValues = null; + break; + + case ValueListImpl vl: + _availableValues = vl; + break; + + default: throw RuntimeException.InvalidArgumentType(); + } + } + } + /// /// Получить элемент по индексу /// @@ -71,6 +103,9 @@ public override void SetIndexedValue(IValue index, IValue val) public ValueListItem GetValue(IValue index) { int numericIndex = (int)index.AsNumber(); + if (numericIndex < 0 || numericIndex >= _items.Count) + throw RuntimeException.IndexOutOfRange(); + return _items[numericIndex]; } @@ -103,22 +138,31 @@ public ValueListItem Add(IValue value = null, string presentation = null, bool c [ContextMethod("Вставить", "Insert")] public ValueListItem Insert(int index, IValue value = null, string presentation = null, bool check = false, IValue picture = null) { + if (index < 0 || index > _items.Count) + throw RuntimeException.IndexOutOfRange(); + var newItem = CreateNewListItem(value, presentation, check, picture); _items.Insert(index, newItem); return newItem; } - private static ValueListItem CreateNewListItem(IValue value, string presentation, bool check, IValue picture) + private ValueListItem CreateNewListItem(IValue value, string presentation, bool check, IValue picture) { - var newItem = new ValueListItem + var newValue = ListValueType.AdjustValue(value); + if (_availableValues is not null) + { + var foundItem = _availableValues.FindByValue(newValue); + newValue = foundItem is ValueListItem li ? li.Value : ListValueType.AdjustValue(); + } + + return new ValueListItem(_maxId++) { - Value = value ?? BslUndefinedValue.Instance, + Value = newValue, Presentation = presentation, Check = check, Picture = picture }; - return newItem; } /// @@ -126,10 +170,7 @@ private static ValueListItem CreateNewListItem(IValue value, string presentation /// /// Массив [ContextMethod("ВыгрузитьЗначения", "UnloadValues")] - public ArrayImpl UnloadValues() - { - return new ArrayImpl(_items.Select(x=>x.Value)); - } + public ArrayImpl UnloadValues() => new ArrayImpl(_items.Select(x => x.Value)); /// /// Загружает значения из массива @@ -138,18 +179,15 @@ public ArrayImpl UnloadValues() [ContextMethod("ЗагрузитьЗначения", "LoadValues")] public void LoadValues(ArrayImpl source) { - Clear(); - _items.AddRange(source.Select(x => new ValueListItem() { Value = x })); + Clear(); + _items.AddRange(source.Select(x => CreateNewListItem(x, null, false, null))); } /// /// Удаляет все элементы из списка. /// [ContextMethod("Очистить", "Clear")] - public void Clear() - { - _items.Clear(); - } + public void Clear() => _items.Clear(); /// /// Устанавливает значение пометки у всех элементов списка значений @@ -170,10 +208,7 @@ public void FillChecks(bool check) /// ЭлементСпискаЗначений - Элемент списка значений, для которого необходимо определить индекс /// Число - Индекс в списке, если не найдено возвращает -1 [ContextMethod("Индекс", "IndexOf")] - public int IndexOf(ValueListItem item) - { - return _items.IndexOf(item); - } + public int IndexOf(ValueListItem item) => _items.IndexOf(item); /// /// Осуществляет поиск значения в списке @@ -194,11 +229,11 @@ private int IndexByValue(BslValue item) { int index; - if (item is ValueListItem) + if (item is ValueListItem listItem) { - index = IndexOf(item as ValueListItem); + index = IndexOf(listItem); if (index == -1) - throw new RuntimeException("Элемент не принадлежит списку значений"); + throw ElementDoesntBelongException(); } else { @@ -211,7 +246,7 @@ private int IndexByValue(BslValue item) throw RuntimeException.InvalidArgumentType(); } - if (index < 0 || index >= _items.Count()) + if (index < 0 || index >= _items.Count) throw RuntimeException.IndexOutOfRange(); } @@ -230,10 +265,9 @@ private int IndexByValue(BslValue item) public void Move(BslValue item, int offset) { int index_source = IndexByValue(item); - int index_dest = index_source + offset; - if (index_dest < 0 || index_dest >= _items.Count()) + if (index_dest < 0 || index_dest >= _items.Count) throw RuntimeException.InvalidNthArgumentValue(2); ValueListItem itemObject = _items[index_source]; @@ -273,26 +307,23 @@ public ValueListImpl Copy() [ContextMethod("СортироватьПоЗначению", "SortByValue")] public void SortByValue(IBslProcess process, SortDirectionEnum? direction = null) { - if (direction == null || direction == SortDirectionEnum.Asc) - { - _items.Sort((x, y) => SafeCompare(process, x.Value, y.Value)); - } - else - { - _items.Sort((x, y) => SafeCompare(process, y.Value, x.Value)); - } + _items.Sort( new ItemComparator(process, direction == null || direction == SortDirectionEnum.Asc) ); } - private int SafeCompare(IBslProcess p, IValue x, IValue y) + private class ItemComparator : IComparer { - try + readonly GenericIValueComparer _comparer; + readonly int _direction; + + public ItemComparator(IBslProcess process, bool ascending = true) { - return x.CompareTo(y); + _comparer = new GenericIValueComparer(process); + _direction = ascending ? 1 : -1; } - catch(RuntimeException) + + public int Compare(ValueListItem x, ValueListItem y) { - // Сравнение типов не поддерживается - return string.Compare(x?.AsString(p), y?.AsString(p), StringComparison.Ordinal); + return _comparer.Compare(x.Value, y.Value) * _direction; } } @@ -321,33 +352,34 @@ public void SortByPresentation(SortDirectionEnum? direction = null) /// Число - Индекс удаляемого элемента /// [ContextMethod("Удалить", "Delete")] - public void Delete(BslValue item) - { - int indexSource = IndexByValue(item); - - _items.RemoveAt(indexSource); - } + public void Delete(BslValue item) => _items.RemoveAt(IndexByValue(item)); #region Collection Context [ContextMethod("Количество", "Count")] - public override int Count() - { - return _items.Count; - } + public override int Count() => _items.Count; - public override IEnumerator GetEnumerator() - { - return _items.GetEnumerator(); - } + public override IEnumerator GetEnumerator() => _items.GetEnumerator(); #endregion + public override string ToString() => string.Join("; ", _items); + [ScriptConstructor] public static ValueListImpl Constructor() { return new ValueListImpl(); + } + + public static RuntimeException IndexedIsReadonlyException() + { + return new("Индексированное значение доступно только для чтения", "Indexed value is read-only"); } + public static RuntimeException ElementDoesntBelongException() + { + return new("Элемент не принадлежит списку значений", "Element does not belong to values list"); + } + } } diff --git a/src/OneScript.StandardLibrary/Collections/ValueList/ValueListItem.cs b/src/OneScript.StandardLibrary/Collections/ValueList/ValueListItem.cs index c015ee277..e394bacec 100644 --- a/src/OneScript.StandardLibrary/Collections/ValueList/ValueListItem.cs +++ b/src/OneScript.StandardLibrary/Collections/ValueList/ValueListItem.cs @@ -7,6 +7,8 @@ This Source Code Form is subject to the terms of the using System; using OneScript.Contexts; +using OneScript.Localization; +using OneScript.Values; using ScriptEngine.Machine; using ScriptEngine.Machine.Contexts; @@ -18,18 +20,29 @@ namespace OneScript.StandardLibrary.Collections.ValueList [ContextClass("ЭлементСпискаЗначений", "ValueListItem")] public class ValueListItem : AutoContext { + static readonly BilingualString EMPTY_VALUE_STRING = new("<Пустое значение>", ""); + private string _presentationHolder; private IValue _pictureHolder; + private readonly ulong _id; - public ValueListItem() + internal ValueListItem(ulong id) { + _id = id; _pictureHolder = ValueFactory.Create(); _presentationHolder = String.Empty; - } - + } + + /// + /// Хранимое элементом списка значение произвольного типа + /// [ContextProperty("Значение", "Value")] public IValue Value { get; set; } + /// + /// Строковое представление элемента списка значений. + /// Если не задано или указана пустая строка, то соответствует представлению значения. + /// [ContextProperty("Представление", "Presentation")] public string Presentation { @@ -37,17 +50,32 @@ public string Presentation set => _presentationHolder = value ?? String.Empty; } + /// + /// Связанное с элементом списка значение пометки. Тип: Булево + /// [ContextProperty("Пометка", "Check")] - public bool Check { get; set; } - + public bool Check { get; set; } + + /// + /// Картинка, связанная с элементом списка значений. + /// + /// Сейчас может содержать произвольное значение, не используется [ContextProperty("Картинка", "Picture")] public IValue Picture { get => _pictureHolder; set => _pictureHolder = value ?? ValueFactory.Create(); - } + } + + /// + /// Получает идентификатор для элемента списка значений, не привязанный к позиции элемента в списке + /// + [ContextMethod("ПолучитьИдентификатор", "GetID")] + public ulong GetID() => _id; public override string ToString() - => !String.IsNullOrEmpty(_presentationHolder) ? _presentationHolder : Value.ToString(); + => !String.IsNullOrEmpty(_presentationHolder) ? _presentationHolder + : Value is BslUndefinedValue or BslNullValue? EMPTY_VALUE_STRING + : Value.ToString(); } } diff --git a/src/OneScript.StandardLibrary/DynamicLoadingFunctions.cs b/src/OneScript.StandardLibrary/DynamicLoadingFunctions.cs index c0a902c34..4349f4357 100644 --- a/src/OneScript.StandardLibrary/DynamicLoadingFunctions.cs +++ b/src/OneScript.StandardLibrary/DynamicLoadingFunctions.cs @@ -1,120 +1,120 @@ -/*---------------------------------------------------------- -This Source Code Form is subject to the terms of the -Mozilla Public License, v.2.0. If a copy of the MPL -was not distributed with this file, You can obtain one -at http://mozilla.org/MPL/2.0/. -----------------------------------------------------------*/ - -using OneScript.Commons; -using OneScript.Compilation; -using OneScript.Contexts; -using OneScript.Exceptions; -using OneScript.Execution; -using OneScript.Language; -using OneScript.Localization; -using OneScript.StandardLibrary.Collections; -using OneScript.StandardLibrary.NativeApi; -using ScriptEngine; -using ScriptEngine.Machine; -using ScriptEngine.Machine.Contexts; -using System; - -namespace OneScript.StandardLibrary -{ - /// - /// Динамическое подключение сценариев - /// - [GlobalContext(ManualRegistration = true, Category = "Динамическое подключение сценариев")] - public class DynamicLoadingFunctions : GlobalContextBase - { - private readonly ScriptingEngine _engine; - - public DynamicLoadingFunctions(ScriptingEngine engine) - { - _engine = engine; - } - - /// - /// Подключает сторонний файл сценария к текущей системе типов. - /// Подключенный сценарий выступает как самостоятельный класс, создаваемый оператором Новый - /// - /// Путь к подключаемому сценарию - /// Имя типа, которое будет иметь новый класс. Экземпляры класса создаются оператором Новый. - /// ПодключитьСценарий("C:\file.os", "МойОбъект"); - /// А = Новый МойОбъект(); - [ContextMethod("ПодключитьСценарий", "AttachScript")] - public void AttachScript(IBslProcess process, string path, string typeName) - { - var compiler = _engine.GetCompilerService(); - try - { - _engine.AttachedScriptsFactory.AttachByPath(compiler, path, typeName, process); - } - catch (Exception e) when (e is SyntaxErrorException || e is CompilerException) +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using OneScript.Commons; +using OneScript.Compilation; +using OneScript.Contexts; +using OneScript.Exceptions; +using OneScript.Execution; +using OneScript.Language; +using OneScript.Localization; +using OneScript.StandardLibrary.Collections; +using OneScript.StandardLibrary.NativeApi; +using ScriptEngine; +using ScriptEngine.Machine; +using ScriptEngine.Machine.Contexts; +using System; + +namespace OneScript.StandardLibrary +{ + /// + /// Динамическое подключение сценариев + /// + [GlobalContext(ManualRegistration = true, Category = "Динамическое подключение сценариев")] + public class DynamicLoadingFunctions : GlobalContextBase + { + private readonly ScriptingEngine _engine; + + public DynamicLoadingFunctions(ScriptingEngine engine) + { + _engine = engine; + } + + /// + /// Подключает сторонний файл сценария к текущей системе типов. + /// Подключенный сценарий выступает как самостоятельный класс, создаваемый оператором Новый + /// + /// Путь к подключаемому сценарию + /// Имя типа, которое будет иметь новый класс. Экземпляры класса создаются оператором Новый. + /// ПодключитьСценарий("C:\file.os", "МойОбъект"); + /// А = Новый МойОбъект(); + [ContextMethod("ПодключитьСценарий", "AttachScript")] + public void AttachScript(IBslProcess process, string path, string typeName) + { + var compiler = _engine.GetCompilerService(); + try + { + _engine.AttachedScriptsFactory.AttachByPath(compiler, path, typeName, process); + } + catch (Exception e) when (e is SyntaxErrorException || e is CompilerException) { throw ScriptCompilingException(e); - } - } - - /// - /// Создает экземпляр объекта на основании стороннего файла сценария. - /// Загруженный сценарий возвращается как самостоятельный объект. - /// Экспортные свойства и методы скрипта доступны для вызова. - /// - /// - /// Текст сценария - /// Структура. Глобальные свойства, которые будут инжектированы в область видимости загружаемого скрипта. (Необязательный) - /// - /// Контекст = Новый Структура("ЧислоПи", 3.1415); // 4 знака хватит всем - /// ЗагрузитьСценарийИзСтроки("Сообщить(ЧислоПи);", Контекст); - [ContextMethod("ЗагрузитьСценарийИзСтроки", "LoadScriptFromString")] - public UserScriptContextInstance LoadScriptFromString(IBslProcess process, - string code, - StructureImpl externalContext = null) - { + } + } + + /// + /// Создает экземпляр объекта на основании стороннего файла сценария. + /// Загруженный сценарий возвращается как самостоятельный объект. + /// Экспортные свойства и методы скрипта доступны для вызова. + /// + /// + /// Текст сценария + /// Структура. Глобальные свойства, которые будут инжектированы в область видимости загружаемого скрипта. (Необязательный) + /// + /// Контекст = Новый Структура("ЧислоПи", 3.1415); // 4 знака хватит всем + /// ЗагрузитьСценарийИзСтроки("Сообщить(ЧислоПи);", Контекст); + [ContextMethod("ЗагрузитьСценарийИзСтроки", "LoadScriptFromString")] + public UserScriptContextInstance LoadScriptFromString(IBslProcess process, + string code, + StructureImpl externalContext = null) + { var compiler = _engine.GetCompilerService(); try { - if (externalContext == null) - return _engine.AttachedScriptsFactory.LoadFromString(compiler, code, process); - + if (externalContext == null) + return _engine.AttachedScriptsFactory.LoadFromString(compiler, code, process); + var extData = CreateContextData(externalContext); - return _engine.AttachedScriptsFactory.LoadFromString(compiler, code, process, extData); + return _engine.AttachedScriptsFactory.LoadFromString(compiler, code, process, extData); } catch (Exception e) when (e is SyntaxErrorException || e is CompilerException) { throw ScriptCompilingException(e); } - } - - /// - /// Создает экземпляр объекта на основании стороннего файла сценария. - /// Загруженный сценарий возвращается как самостоятельный объект. - /// Экспортные свойства и методы скрипта доступны для вызова. - /// - /// Путь к подключаемому сценарию - /// - /// Структура. Глобальные свойства, которые будут инжектированы в область видимости загружаемого скрипта. (Необязательный) - /// - /// Контекст = Новый Структура("ЧислоПи", 3.1415); // 4 знака хватит - /// // В коде скрипта somescript.os будет доступна глобальная переменная "ЧислоПи" - /// Объект = ЗагрузитьСценарий("somescript.os", Контекст); - [ContextMethod("ЗагрузитьСценарий", "LoadScript")] - public UserScriptContextInstance LoadScript(IBslProcess process, string path, StructureImpl externalContext = null) + } + + /// + /// Создает экземпляр объекта на основании стороннего файла сценария. + /// Загруженный сценарий возвращается как самостоятельный объект. + /// Экспортные свойства и методы скрипта доступны для вызова. + /// + /// Путь к подключаемому сценарию + /// + /// Структура. Глобальные свойства, которые будут инжектированы в область видимости загружаемого скрипта. (Необязательный) + /// + /// Контекст = Новый Структура("ЧислоПи", 3.1415); // 4 знака хватит + /// // В коде скрипта somescript.os будет доступна глобальная переменная "ЧислоПи" + /// Объект = ЗагрузитьСценарий("somescript.os", Контекст); + [ContextMethod("ЗагрузитьСценарий", "LoadScript")] + public UserScriptContextInstance LoadScript(IBslProcess process, string path, StructureImpl externalContext = null) { - var compiler = _engine.GetCompilerService(); + var compiler = _engine.GetCompilerService(); try { - if(externalContext == null) - return _engine.AttachedScriptsFactory.LoadFromPath(compiler, path, process); - - var extData = CreateContextData(externalContext); - return _engine.AttachedScriptsFactory.LoadFromPath(compiler, path, extData, process); + if(externalContext == null) + return _engine.AttachedScriptsFactory.LoadFromPath(compiler, path, process); + + var extData = CreateContextData(externalContext); + return _engine.AttachedScriptsFactory.LoadFromPath(compiler, path, extData, process); } catch (Exception e) when (e is SyntaxErrorException || e is CompilerException) { throw ScriptCompilingException(e); - } + } } private static ExternalContextData CreateContextData(StructureImpl externalContext) @@ -132,55 +132,55 @@ private static ExternalContextData CreateContextData(StructureImpl externalConte return extData; } - /// - /// Подключает внешнюю сборку среды .NET (*.dll) и регистрирует классы 1Script, объявленные в этой сборке. - /// Публичные классы, отмеченные в dll атрибутом ContextClass, будут импортированы аналогично встроенным классам 1Script. - /// Загружаемая сборка должна ссылаться на сборку ScriptEngine.dll - /// - /// Также подключает внешние компоненты, разработанные по технологии Native API, - /// поставляемые в виде отдельных DLL или упакованные в ZIP-архив. - /// - /// - /// Путь к внешней компоненте - /// Символическое имя подключаемой внешней компоненты (только для Native API) - /// Тип подключаемой внешней компоненты (для совместимости, необязательно) - /// - /// //Подключает внешнюю сборку среды .NET (*.dll) - /// ПодключитьВнешнююКомпоненту("C:\MyAssembly.dll"); - /// КлассИзКомпоненты = Новый КлассИзКомпоненты(); // тип объявлен внутри компоненты - /// - /// //Подключает вншение компонеты Native API, упакованные в ZIP-архив - /// ПодключитьВнешнююКомпоненту("C:\AddInNative.zip", "AddInNative"); - /// ЭкземплярВнешнейКомпоненты = Новый ("AddIn.AddInNative.NativeComponent", ТипВнешнейКомпоненты.Native); - /// - /// //Подключает вншение компонеты Native API в виде отдельных DLL-файлов - /// ПодключитьВнешнююКомпоненту("C:\AddInNative.dll", "SimpleAddIn", ТипВнешнейКомпоненты.Native); - /// ЭкземплярВнешнейКомпоненты = Новый ("AddIn.SimpleAddIn.SimpleComponent"); - /// - [ContextMethod("ПодключитьВнешнююКомпоненту", "AttachAddIn")] - public bool AttachAddIn(string dllPath, string name = "", NativeApiEnums type = NativeApiEnums.OneScript) - { - if (type == NativeApiEnums.OneScript) - { - var assembly = System.Reflection.Assembly.LoadFrom(dllPath); - _engine.AttachExternalAssembly(assembly); - return true; - } - else - { - if (!Utils.IsValidIdentifier(name)) - { - throw RuntimeException.InvalidArgumentValue(name); - } - return NativeApiFactory.Register(dllPath, name, _engine.TypeManager); - } - } - + /// + /// Подключает внешнюю сборку среды .NET (*.dll) и регистрирует классы 1Script, объявленные в этой сборке. + /// Публичные классы, отмеченные в dll атрибутом ContextClass, будут импортированы аналогично встроенным классам 1Script. + /// Загружаемая сборка должна ссылаться на сборку ScriptEngine.dll + /// + /// Также подключает внешние компоненты, разработанные по технологии Native API, + /// поставляемые в виде отдельных DLL или упакованные в ZIP-архив. + /// + /// + /// Путь к внешней компоненте + /// Символическое имя подключаемой внешней компоненты (только для Native API) + /// Тип подключаемой внешней компоненты (для совместимости, необязательно) + /// + /// //Подключает внешнюю сборку среды .NET (*.dll) + /// ПодключитьВнешнююКомпоненту("C:\MyAssembly.dll"); + /// КлассИзКомпоненты = Новый КлассИзКомпоненты(); // тип объявлен внутри компоненты + /// + /// //Подключает вншение компонеты Native API, упакованные в ZIP-архив + /// ПодключитьВнешнююКомпоненту("C:\AddInNative.zip", "AddInNative"); + /// ЭкземплярВнешнейКомпоненты = Новый ("AddIn.AddInNative.NativeComponent", ТипВнешнейКомпоненты.Native); + /// + /// //Подключает вншение компонеты Native API в виде отдельных DLL-файлов + /// ПодключитьВнешнююКомпоненту("C:\AddInNative.dll", "SimpleAddIn", ТипВнешнейКомпоненты.Native); + /// ЭкземплярВнешнейКомпоненты = Новый ("AddIn.SimpleAddIn.SimpleComponent"); + /// + [ContextMethod("ПодключитьВнешнююКомпоненту", "AttachAddIn")] + public bool AttachAddIn(string dllPath, string name = "", NativeApiEnums type = NativeApiEnums.OneScript) + { + if (type == NativeApiEnums.OneScript) + { + var assembly = System.Reflection.Assembly.LoadFrom(dllPath); + _engine.AttachExternalAssembly(assembly); + return true; + } + else + { + if (!Utils.IsValidIdentifier(name)) + { + throw RuntimeException.InvalidArgumentValue(name); + } + return _engine.Services.Resolve().Register(dllPath, name, _engine.TypeManager); + } + } + private static RuntimeException ScriptCompilingException(Exception e) { - return new RuntimeException(BilingualString.Localize( - "Ошибка компиляции подключаемого сценария:\n", + return new RuntimeException(BilingualString.Localize( + "Ошибка компиляции подключаемого сценария:\n", "Error compiling attached script:\n") + e.Message, e); - } - } + } + } } \ No newline at end of file diff --git a/src/OneScript.StandardLibrary/EngineBuilderExtensions.cs b/src/OneScript.StandardLibrary/EngineBuilderExtensions.cs index 6e3a9875a..30993ee5a 100644 --- a/src/OneScript.StandardLibrary/EngineBuilderExtensions.cs +++ b/src/OneScript.StandardLibrary/EngineBuilderExtensions.cs @@ -7,6 +7,8 @@ This Source Code Form is subject to the terms of the using OneScript.StandardLibrary.Binary; using OneScript.StandardLibrary.Collections; +using OneScript.StandardLibrary.NativeApi; +using ScriptEngine; using ScriptEngine.Hosting; using ScriptEngine.Machine; @@ -19,6 +21,13 @@ public static IEngineBuilder UseBinaryDataOptions(this IEngineBuilder builder) builder.Services.RegisterSingleton(); return builder; } + + public static IEngineBuilder UseNativeApi(this IEngineBuilder builder) + { + builder.Services.RegisterSingleton(); + builder.Services.RegisterSingleton(sp => sp.Resolve()); + return builder; + } public static ExecutionContext AddStandardLibrary(this ExecutionContext env) { diff --git a/src/OneScript.StandardLibrary/ExecutionThreadContext.cs b/src/OneScript.StandardLibrary/ExecutionThreadContext.cs new file mode 100644 index 000000000..0b8f56952 --- /dev/null +++ b/src/OneScript.StandardLibrary/ExecutionThreadContext.cs @@ -0,0 +1,133 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using System; +using System.Linq; +using OneScript.Contexts; +using OneScript.Execution; +using OneScript.StandardLibrary.Collections; +using ScriptEngine; +using ScriptEngine.Machine; +using ScriptEngine.Machine.Contexts; + +namespace OneScript.StandardLibrary +{ + /// + /// Поток исполнения bsl-кода. + /// + /// Потоком исполнения является каждая независимая единица исполнения: основной скрипт, + /// каждое фоновое задание и каждый обрабатываемый запрос веб-сервера. Получить поток + /// исполнения текущего кода можно функцией ТекущийПоток(). + /// + /// Свойство Данные представляет собой хранилище, живущее ровно столько же, сколько живёт + /// сам поток исполнения. Оно предназначено для библиотек, которым нужно хранить состояние + /// в разрезе единицы исполнения (аналог thread-local хранилища). + /// + /// По завершении потока исполнения поднимается событие ПриЗавершении (оно же OnTermination), + /// на которое можно подписаться штатным способом: + /// + /// ДобавитьОбработчик ТекущийПоток().ПриЗавершении, ЭтотОбъект.ВернутьСоединениеВПул; + /// + /// Обработчик вызывается до очистки данных потока, поэтому ещё видит их содержимое. + /// Это позволяет владельцам ресурсов узнавать о конце единицы исполнения вместо того, + /// чтобы опрашивать список фоновых заданий. + /// + [ContextClass("ПотокИсполнения", "ExecutionThread")] + public sealed class ExecutionThreadContext : AutoContext, IDisposable + { + private readonly IBslProcess _process; + + private bool _isDisposed; + + private ExecutionThreadContext(IBslProcess process) + { + _process = process; + } + + /// + /// Идентификатор потока исполнения. Предназначен для диагностики и журналирования. + /// + /// Идентификаторы выдаются последовательно и в пределах запущенного движка не повторяются, + /// пока счётчик не исчерпает диапазон Int32. Для хранения состояния в разрезе потока + /// исполнения используйте свойство Данные, а не идентификатор в качестве ключа. + /// + /// Число. Идентификатор потока исполнения. + [ContextProperty("Идентификатор", "Id")] + public int Identifier => _process.VirtualThreadId; + + /// + /// Хранилище данных потока исполнения, аналог набора thread-local переменных. + /// + /// Соответствие создаётся вместе с потоком исполнения и не разделяется с другими потоками. + /// В конце потока исполнения соответствие очищается, а его значения, поддерживающие + /// интерфейс IDisposable среды CLR, принудительно освобождаются. + /// + /// Соответствие. Данные потока исполнения. + [ContextProperty("Данные", "Data")] + public MapImpl Data { get; } = new MapImpl(); + + /// + /// Возвращает поток исполнения указанного bsl-процесса, создавая его при первом обращении. + /// Для одного процесса всегда возвращается один и тот же экземпляр. + /// + /// Созданный поток остаётся на процессе и освобождается вместе с ним. + /// + internal static ExecutionThreadContext Of(IBslProcess process) + { + if (process.BslWrapper is ExecutionThreadContext wrapper) + return wrapper; + + lock (process) + { + if (process.BslWrapper is ExecutionThreadContext created) + return created; + + if (process.BslWrapper != null) + throw new InvalidOperationException($"BslWrapper for process is not {nameof(ExecutionThreadContext)}: {process.BslWrapper.GetType()}"); + + var thread = new ExecutionThreadContext(process); + process.BslWrapper = thread; + + return thread; + } + } + + public void Dispose() + { + // Пока идёт завершение, поток ещё числится за процессом, и обработчик вправе + // добраться до него через ТекущийПоток(). Если он при этом освободит процесс, + // завершение не должно пойти по второму кругу. + if (_isDisposed) + return; + + _isDisposed = true; + + try + { + foreach (var item in Data.ToArray()) + { + if (item.Value is not IDisposable disposable) + continue; + + try + { + disposable.Dispose(); + } + catch (Exception exception) + { + SystemLogger.Write( + $"WARNING! Error releasing execution thread data '{item.Key}': {exception.Message}"); + } + } + } + finally + { + Data.Clear(); + } + } + } +} diff --git a/src/OneScript.StandardLibrary/Json/GlobalJsonFunctions.cs b/src/OneScript.StandardLibrary/Json/GlobalJsonFunctions.cs index fe19b479e..508cba9d5 100644 --- a/src/OneScript.StandardLibrary/Json/GlobalJsonFunctions.cs +++ b/src/OneScript.StandardLibrary/Json/GlobalJsonFunctions.cs @@ -13,6 +13,9 @@ This Source Code Form is subject to the terms of the using OneScript.Exceptions; using OneScript.Execution; using OneScript.StandardLibrary.Collections; +using OneScript.StandardLibrary.Timezones; +using OneScript.Types; +using OneScript.Values; using ScriptEngine.Machine; using ScriptEngine.Machine.Contexts; @@ -42,9 +45,9 @@ public static IAttachableContext CreateInstance() /// Если установлено Ложь, объекты будут считываться в объект типа Структура. /// Значение по умолчанию: Ложь. /// - /// Значение не обрабатывается в текущей версии. Значение по умолчанию: Неопределено. + /// Имена свойств JSON, значения которых нужно преобразовать в дату. Значение по умолчанию: Неопределено. /// - /// Значение не обрабатывается в текущей версии. Значение по умолчанию: ISO. + /// Формат даты во входных строках. Значение по умолчанию: ISO. /// /// Значение не обрабатывается в текущей версии. Значение по умолчанию: Неопределено. /// @@ -59,15 +62,50 @@ public static IAttachableContext CreateInstance() /// /// [ContextMethod("ПрочитатьJSON", "ReadJSON")] - public IValue ReadJSON(JSONReader Reader, bool ReadToMap = false, IValue PropertiesWithDateValuesNames = null, IValue ExpectedDateFormat = null, string ReviverFunctionName = null, IValue ReviverFunctionModule = null, IValue ReviverFunctionAdditionalParameters = null, IValue RetriverPropertiesNames = null, int MaximumNesting = 500) + public IValue ReadJSON(JSONReader Reader, bool ReadToMap = false, IValue PropertiesWithDateValuesNames = null, JSONDateFormatEnum? ExpectedDateFormat = null, string ReviverFunctionName = null, IValue ReviverFunctionModule = null, IValue ReviverFunctionAdditionalParameters = null, IValue RetriverPropertiesNames = null, int MaximumNesting = 500) { - var jsonReader = new JsonReaderInternal(Reader); + var dateFormat = ExpectedDateFormat ?? JSONDateFormatEnum.ISO; + if (dateFormat == JSONDateFormatEnum.JavaScript) + { + throw new RuntimeException(Locale.NStr( + "ru='Формат даты JavaScript не поддерживается.'; en='JavaScript date format is not supported'")); + } + + var datePropertyNames = ParseDatePropertyNames(PropertiesWithDateValuesNames); + var jsonReader = new JsonReaderInternal(Reader, datePropertyNames, dateFormat); return ReadToMap ? jsonReader.Read() : jsonReader.Read(); } + private static HashSet ParseDatePropertyNames(IValue namesValue) + { + if (namesValue == null || namesValue.SystemType == BasicTypes.Undefined) + return new HashSet(StringComparer.Ordinal); + + if (namesValue.SystemType == BasicTypes.String) + return new HashSet(StringComparer.Ordinal) { namesValue.AsString(ForbiddenBslProcess.Instance) }; + + if (namesValue is IValueArray array) + { + var result = new HashSet(StringComparer.Ordinal); + foreach (var item in array) + { + if (item.SystemType != BasicTypes.String) + throw RuntimeException.InvalidArgumentType(); + + result.Add(item.AsString(ForbiddenBslProcess.Instance)); + } + + return result; + } + + throw RuntimeException.InvalidArgumentType(); + } + internal class JsonReaderInternal { private readonly JSONReader _reader; + private readonly HashSet _datePropertyNames; + private readonly JSONDateFormatEnum _dateFormat; private Func _builder; private Action _inserter; @@ -89,9 +127,11 @@ private void Init() } } - public JsonReaderInternal(JSONReader reader) + public JsonReaderInternal(JSONReader reader, HashSet datePropertyNames, JSONDateFormatEnum dateFormat) { _reader = reader; + _datePropertyNames = datePropertyNames; + _dateFormat = dateFormat; } private IValue Create() => _builder(); @@ -112,6 +152,10 @@ public IValue Read() where T: IEnumerable { throw; } + catch (RuntimeException) + { + throw; + } catch (Exception exc) { throw InvalidJsonException(exc.Message); @@ -145,6 +189,13 @@ private bool ReadJsonValue(out IValue value) if (!ReadJsonValue(out value)) return false; + if (_datePropertyNames.Count > 0 + && _datePropertyNames.Contains(propertyName) + && value is BslStringValue stringValue) + { + value = ParseJsonDate(stringValue.ToString(), _dateFormat); + } + AddProperty(jsonObject, propertyName, value); } @@ -196,7 +247,10 @@ private RuntimeException InvalidJsonException(string message=null) public IValue ReadJSONInMap(JSONReader reader) { - var jsonReader = new JsonReaderInternal(reader); + var jsonReader = new JsonReaderInternal( + reader, + new HashSet(StringComparer.Ordinal), + JSONDateFormatEnum.ISO); return jsonReader.Read(); } @@ -215,13 +269,17 @@ public IValue ReadJSONInMap(JSONReader reader) /// [ContextMethod("ПрочитатьДатуJSON", "ReadJSONDate")] public IValue ReadJSONDate(string String, JSONDateFormatEnum? format) + { + return ParseJsonDate(String, format ?? JSONDateFormatEnum.ISO); + } + + private static IValue ParseJsonDate(string dateString, JSONDateFormatEnum format) { DateFormatHandling dateFormatHandling; switch (format) { case JSONDateFormatEnum.ISO: - case null: dateFormatHandling = DateFormatHandling.IsoDateFormat; break; case JSONDateFormatEnum.Microsoft: @@ -233,22 +291,49 @@ public IValue ReadJSONDate(string String, JSONDateFormatEnum? format) "ru='Формат даты JavaScript не поддерживается.'; en='JavaScript date format is not supported'")); } - string json = @"{""Date"":""" + String + @"""}"; + string json = @"{""Date"":""" + dateString + @"""}"; var settings = new JsonSerializerSettings { DateFormatHandling = dateFormatHandling }; + DateTime dateTime; try { var result = JsonConvert.DeserializeObject(json, settings); - return ValueFactory.Create((DateTime)result.Date); + dateTime = (DateTime)result.Date; } catch (JsonException) { throw new RuntimeException(Locale.NStr("ru='Представление даты имеет неверный формат.'; en='Invalid date presentation format'")); } + + if (dateTime.Kind == DateTimeKind.Utc) + dateTime = TimeZoneConverter.ToLocalTime(dateTime); + + dateTime = new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, dateTime.Hour, dateTime.Minute, dateTime.Second); + return ValueFactory.Create(dateTime); + } + + /// + /// Преобразовывает дату в строку для последующей записи в формат JSON. + /// + /// + /// + /// Дата, которую надо преобразовать в формат JSON. + /// + /// Формат записываемой даты. + /// + /// Вариант записи даты JSON. Значение по умолчанию: ЛокальнаяДата. + /// + /// + /// Значения данного типа содержат строку в формате Unicode произвольной длины. + /// + [ContextMethod("ЗаписатьДатуJSON", "WriteJSONDate")] + public string WriteJSONDate(IValue dateValue, JSONDateFormatEnum format, JSONDateWritingVariantEnum dateWritingVariant = JSONDateWritingVariantEnum.LocalDate) + { + return JSONDateWriter.Write(dateValue, format, dateWritingVariant); } /// diff --git a/src/OneScript.StandardLibrary/Json/JSONDateWriter.cs b/src/OneScript.StandardLibrary/Json/JSONDateWriter.cs new file mode 100644 index 000000000..05205595a --- /dev/null +++ b/src/OneScript.StandardLibrary/Json/JSONDateWriter.cs @@ -0,0 +1,147 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using System; +using System.Globalization; +using OneScript.Commons; +using OneScript.Exceptions; +using OneScript.Types; +using ScriptEngine.Machine; + +namespace OneScript.StandardLibrary.Json +{ + internal static class JSONDateWriter + { + internal static string FormatDateForJson(DateTime date) + { + return FormatISODate(DropSubsecond(date)); + } + + public static string Write(IValue dateValue, JSONDateFormatEnum format, JSONDateWritingVariantEnum dateWritingVariant) + { + var date = DropSubsecond(GetDateArgument(dateValue)); + + switch (format) + { + case JSONDateFormatEnum.ISO: + return WriteISODate(date, dateWritingVariant); + + case JSONDateFormatEnum.JavaScript: + CheckUniversalDateWritingVariant(format, dateWritingVariant); + return $"new Date({UnixMilliseconds(ConvertLocalTimeToUtc(date))})"; + + case JSONDateFormatEnum.Microsoft: + CheckUniversalDateWritingVariant(format, dateWritingVariant); + return $"/Date({UnixMilliseconds(ConvertLocalTimeToUtc(date))})/"; + + default: + throw RuntimeException.InvalidNthArgumentValue(2); + } + } + + private static DateTime GetDateArgument(IValue dateValue) + { + if (dateValue?.SystemType != BasicTypes.Date) + { + throw RuntimeException.InvalidNthArgumentType(1); + } + + return dateValue.AsDate(); + } + + private static string WriteISODate(DateTime date, JSONDateWritingVariantEnum dateWritingVariant) + { + switch (dateWritingVariant) + { + case JSONDateWritingVariantEnum.LocalDate: + return FormatISODate(date); + + case JSONDateWritingVariantEnum.LocalDateWithOffset: + return FormatISODate(date) + FormatJSONDateOffset(GetLocalUtcOffset(date)); + + case JSONDateWritingVariantEnum.UniversalDate: + return FormatISODate(ConvertLocalTimeToUtc(date)) + "Z"; + + default: + throw RuntimeException.InvalidNthArgumentValue(3); + } + } + + private static string FormatISODate(DateTime date) + { + return date.ToString("yyyy-MM-dd'T'HH:mm:ss", CultureInfo.InvariantCulture); + } + + private static DateTime ConvertLocalTimeToUtc(DateTime date) + { + if (date == DateTime.MinValue) + { + return DateTime.MinValue; + } + + return TimeZoneInfo.ConvertTimeToUtc(AsUnspecified(date), TimeZoneInfo.Local); + } + + private static TimeSpan GetLocalUtcOffset(DateTime date) + { + if (date == DateTime.MinValue) + { + return TimeSpan.Zero; + } + + return TimeZoneInfo.Local.GetUtcOffset(AsUnspecified(date)); + } + + private static DateTime AsUnspecified(DateTime date) + { + return DateTime.SpecifyKind(date, DateTimeKind.Unspecified); + } + + private static DateTime DropSubsecond(DateTime date) + { + return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second); + } + + private static long UnixMilliseconds(DateTime utcDate) + { + var utc = DateTime.SpecifyKind(utcDate, DateTimeKind.Utc); + return new DateTimeOffset(utc).ToUnixTimeMilliseconds(); + } + + private static string FormatJSONDateOffset(TimeSpan offset) + { + var sign = offset < TimeSpan.Zero ? "-" : "+"; + var totalMinutes = Math.Abs((int)Math.Truncate(offset.TotalMinutes)); + var hours = totalMinutes / 60; + var minutes = totalMinutes % 60; + + return $"{sign}{hours:00}:{minutes:00}"; + } + + private static void CheckUniversalDateWritingVariant(JSONDateFormatEnum format, JSONDateWritingVariantEnum dateWritingVariant) + { + if (dateWritingVariant == JSONDateWritingVariantEnum.UniversalDate) + { + return; + } + + if (format == JSONDateFormatEnum.JavaScript) + { + throw new RuntimeException(Locale.NStr( + "ru='Невозможно сохранить локальную дату в формате JavaScript'; en='Cannot save local date in JavaScript format'")); + } + + if (format == JSONDateFormatEnum.Microsoft) + { + throw new RuntimeException(Locale.NStr( + "ru='Невозможно сохранить локальную дату в формате Microsoft'; en='Cannot save local date in Microsoft format'")); + } + + throw RuntimeException.InvalidNthArgumentValue(2); + } + } +} diff --git a/src/OneScript.StandardLibrary/Json/JSONDateWritingVariantEnum.cs b/src/OneScript.StandardLibrary/Json/JSONDateWritingVariantEnum.cs new file mode 100644 index 000000000..93d7f853c --- /dev/null +++ b/src/OneScript.StandardLibrary/Json/JSONDateWritingVariantEnum.cs @@ -0,0 +1,26 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using OneScript.Contexts.Enums; + +namespace OneScript.StandardLibrary.Json +{ + [EnumerationType("ВариантЗаписиДатыJSON", "JSONDateWritingVariant", + TypeUUID = "7B1B8BE8-0EA9-47BF-BAAF-8BC6A4756E8B", + ValueTypeUUID = "4DD7E419-8D53-4F4C-A3A5-8F7436A07734")] + public enum JSONDateWritingVariantEnum + { + [EnumValue("ЛокальнаяДата", "LocalDate")] + LocalDate, + + [EnumValue("ЛокальнаяДатаСоСмещением", "LocalDateWithOffset")] + LocalDateWithOffset, + + [EnumValue("УниверсальнаяДата", "UniversalDate")] + UniversalDate + } +} diff --git a/src/OneScript.StandardLibrary/Json/JSONWriter.cs b/src/OneScript.StandardLibrary/Json/JSONWriter.cs index a31842fc2..509ba8ec4 100644 --- a/src/OneScript.StandardLibrary/Json/JSONWriter.cs +++ b/src/OneScript.StandardLibrary/Json/JSONWriter.cs @@ -13,6 +13,7 @@ This Source Code Form is subject to the terms of the using OneScript.Commons; using OneScript.Contexts; using OneScript.Exceptions; +using OneScript.StandardLibrary.Binary; using OneScript.StandardLibrary.Text; using ScriptEngine.Machine; using ScriptEngine.Machine.Contexts; @@ -20,13 +21,13 @@ This Source Code Form is subject to the terms of the namespace OneScript.StandardLibrary.Json { /// - /// /// Предназначен для организации последовательной записи объектов и текстов JSON. /// [ContextClass("ЗаписьJSON", "JSONWriter")] - public class JSONWriter : AutoContext + public class JSONWriter : AutoContext, IDisposable { private const int INDENT_SIZE = 0; + private const string DEFAULT_ENCODING = "UTF-8"; private JSONWriterSettings _settings; private JsonTextWriter _writer; // Объект из библиотеки Newtonsoft для работы с форматом JSON @@ -34,28 +35,24 @@ public class JSONWriter : AutoContext StringWriter _stringWriter; public JSONWriter() - { - + { + _settings = new JSONWriterSettings(); } - private bool _escapeNonAscii; - /// - /// - /// Возвращает true если для объекта чтения json был задан текст для парсинга. + /// Возвращает true если был открыт объект для записи. /// - private bool IsOpen() - { - return _writer != null; - } + private bool IsOpen() => _writer != null; /// - /// - /// Возвращает true если для объекта чтения json был задан текст для парсинга. + /// Возвращает true если текст json выводится в строку. /// - private bool IsOpenForString() + private bool IsOpenForString() => _stringWriter != null; + + private void CheckWriter() { - return _stringWriter != null; + if (!IsOpen()) + throw NotOpenException(); } private void SetDefaultOptions() @@ -63,54 +60,34 @@ private void SetDefaultOptions() _writer.Indentation = INDENT_SIZE; _writer.Formatting = Formatting.Indented; _settings = new JSONWriterSettings(); - _escapeNonAscii = false; } private void SetOptions(JSONWriterSettings settings) - { + { + if (settings == null) + { + SetDefaultOptions(); + return; + } + _settings = settings; - if (_settings.UseDoubleQuotes) - _writer.QuoteChar = '\"'; - else { - _writer.QuoteChar = '\''; - } - _writer.IndentChar = !string.IsNullOrEmpty(_settings.PaddingSymbols) ? _settings.PaddingSymbols[0] : ' '; - _writer.Indentation = !string.IsNullOrEmpty(_settings.PaddingSymbols) ? 1 : INDENT_SIZE; - _writer.Formatting = Formatting.Indented; + _writer.QuoteChar = _settings.UseDoubleQuotes ? '"' : '\''; - if (_settings.EscapeCharacters != JSONCharactersEscapeModeEnum.None) - { - var jsonCharactersEscapeMode = _settings.EscapeCharacters; - if (jsonCharactersEscapeMode == JSONCharactersEscapeModeEnum.NotASCIISymbols) - { - _escapeNonAscii = true; - _writer.QuoteChar = '\"'; - _writer.StringEscapeHandling = StringEscapeHandling.EscapeNonAscii; - } - else if (jsonCharactersEscapeMode == JSONCharactersEscapeModeEnum.SymbolsNotInBMP) - throw new NotImplementedException(); + if (string.IsNullOrEmpty(_settings.PaddingSymbols)) + { + _writer.IndentChar = ' '; + _writer.Indentation = INDENT_SIZE; } - } - - void WriteStringValue(string val) - { - if (_settings.EscapeCharacters != JSONCharactersEscapeModeEnum.None && _escapeNonAscii) + else { - StringWriter wr = new StringWriter(); - var jsonWriter = new JsonTextWriter(wr); - jsonWriter.QuoteChar = '\"'; - jsonWriter.StringEscapeHandling = StringEscapeHandling.EscapeNonAscii; - new JsonSerializer().Serialize(jsonWriter, val); - string str = wr.ToString(); - _writer.WriteRawValue(EscapeCharacters(str.Substring(1, str.Length - 2), false)); - - } - else - _writer.WriteRawValue(EscapeCharacters(val, _settings.EscapeSlash)); + _writer.IndentChar = _settings.PaddingSymbols[0]; + _writer.Indentation = 1; + } + _writer.Formatting = Formatting.Indented; } - string EscapeCharacters(string sval, bool EscapeSlash) + string EscapeCharacters(string sval) { var sb = new StringBuilder(); @@ -120,62 +97,32 @@ string EscapeCharacters(string sval, bool EscapeSlash) for (var i = 0; i < length; i++) { char c = sval[i]; - string? escapedValue = null; - - if (EscapeSlash && c == '/') - { - escapedValue = "\\/"; - } - else if (_settings.EscapeAmpersand && c == '&') - { - escapedValue = "\\&"; - } - else if ((_settings.EscapeSingleQuotes || !_settings.UseDoubleQuotes) && c == '\'') - { - escapedValue = "\\u0027"; - } - else if (_settings.EscapeAngleBrackets && c == '<') - { - escapedValue = "\\u003C"; - } - else if (_settings.EscapeAngleBrackets && c == '>') - { - escapedValue = "\\u003E"; - } - else if (c == '\r') - { - escapedValue = "\\r"; - } - else if (c == '\n') - { - escapedValue = "\\n"; - } - else if (c == '\f') - { - escapedValue = "\\f"; - } - else if (c == '\"') - { - escapedValue = "\\\""; - } - else if (c == '\b') - { - escapedValue = "\\b"; - } - else if (c == '\t') - { - escapedValue = "\\t"; - } - else if (c == '\\') - { - escapedValue = "\\\\"; - } - - // Спец. символы: \u0000, \u0001, \u0002, ... , \u001e, \u001f; - else if ((int)c >= 0 && (int)c <= 31) - { - escapedValue = "\\u" + ((int)c).ToString("x4"); - } + string escapedValue = c switch + { + '/' when _settings.EscapeSlash => "\\/", + '\'' when (_settings.EscapeSingleQuotes || !_settings.UseDoubleQuotes) => "\\u0027", + '&' when _settings.EscapeAmpersand => "\\u0026", + '<' when _settings.EscapeAngleBrackets => "\\u003C", + '>' when _settings.EscapeAngleBrackets => "\\u003E", + '\x2028' when _settings.EscapeLineTerminators => "\\u2028", + '\x2029' when _settings.EscapeLineTerminators => "\\u2029", + '\r' => "\\r", + '\n' => "\\n", + '\f' => "\\f", + '\b' => "\\b", + '\t' => "\\t", + '\"' => "\\\"", + '\\' => "\\\\", + >= '\x00' and <= '\x1F' => "\\u" + ((int)c).ToString("X4"), + + >= '\xD800' and <= '\xDFFF' when _settings.EscapeCharacters == JSONCharactersEscapeModeEnum.SymbolsNotInBMP + => "\\u" + ((int)c).ToString("X4"), + + >= '\xFF' and <= '\xFFFF' when _settings.EscapeCharacters == JSONCharactersEscapeModeEnum.NotASCIISymbols + => "\\u" + ((int)c).ToString("X4"), + + _ => null + }; if (escapedValue != null) { @@ -209,13 +156,14 @@ void SetNewLineChars(TextWriter textWriter) case JSONLineBreakEnum.Auto: textWriter.NewLine = "\r\n"; break; - default: - textWriter.NewLine = ""; //Нет + default: // JSONLineBreakEnum.None + textWriter.NewLine = ""; _writer.Formatting = Formatting.None; break; } } } + [ScriptConstructor] public static JSONWriter Constructor() { @@ -223,86 +171,69 @@ public static JSONWriter Constructor() } /// - /// /// Определяет текущие параметры записи JSON. /// /// ПараметрыЗаписиJSON (JSONWriterSettings) [ContextProperty("Параметры", "Settings")] - public IValue Settings - { - get { throw new NotImplementedException(); } - - } - + public IValue Settings => _settings; /// - /// - /// Показывает, будет ли проводиться проверка правильности структуры записываемого JSON объекта. В случае обнаружение ошибки, будет сгенерировано исключение. Например: при попытке записать значение без имени вне массива или записать окончание объекта без начала. Установка данного свойства не имеет немедленного эффекта. Установленное значение свойства будет использовано только после открытия файла или установки строки. + /// Показывает, будет ли проводиться проверка правильности структуры записываемого JSON объекта. + /// В случае обнаружения ошибки, будет сгенерировано исключение. Например: при попытке записать значение без имени вне массива или записать окончание объекта без начала. + /// Установка данного свойства не имеет немедленного эффекта. Установленное значение свойства будет использовано только после открытия файла или установки строки. /// После создания объекта данное свойство имеет значение Истина. /// + /// Проверка структуры поддерживается частично. Попытка установки свойства в Ложь вызовет исключение /// Булево (Boolean) [ContextProperty("ПроверятьСтруктуру", "ValidateStructure")] public bool ValidateStructure { - get { throw new NotImplementedException(); } - set { throw new NotImplementedException(); } + get { return true; } + set { if(!value) throw new NotImplementedException(); } } /// - /// - /// Завершает запись текста JSON. Если производилась запись в файл, то файл закрывается. - /// Если производилась запись в строку, то результирующая строка будет получена в качестве возвращаемого значения метода. - /// Если производилась запись в файл, то метод вернет пустую строку. + /// Завершает запись текста JSON. + /// Если производилась запись в строку, то метод вернет результирующую строку. + /// Если производилась запись в файл, то файл закрывается, а метод вернет пустую строку. + /// Если производилась запись в поток, то поток остается открытым, а метод вернет пустую строку. /// - /// - /// - /// Значения данного типа содержат строку в формате Unicode произвольной длины. + /// Допускается повторное закрытие. Будет возвращена пустая строка. + /// Строка в формате Unicode [ContextMethod("Закрыть", "Close")] public string Close() { - string res = ""; - - if (IsOpenForString()) - { - res = _stringWriter.ToString(); - _stringWriter.Close(); - _stringWriter = null; - } - - if (_writer != null) - { - _writer.Close(); - _writer = null; - } - - return res; + var result = String.Empty; + if (_writer is not null) + { + _writer.Flush(); + if (IsOpenForString()) + result = _stringWriter.ToString(); + + Dispose(); + } + + return result; } - /// - /// /// Выполняет запись произвольной строки в документ, при этом проверка структуры документа не выполняется. /// Если при использовании метода свойство ПроверятьСтруктуру установлено в значение Истина, то проверка структуры продолжается на следующем элементе. /// - /// /// /// Строка, записываемая в документ JSON. [ContextMethod("ЗаписатьБезОбработки", "WriteRaw")] public void WriteRaw(string stringValue) { - if (!IsOpen()) - throw NotOpenException(); - + CheckWriter(); _writer.WriteRaw(stringValue); } /// - /// /// Записывает значение свойства JSON. /// - /// /// /// Записываемое значение. Типы: Строка (String), Число (Number), Булево (Boolean), Неопределено (Undefined) /// @@ -310,18 +241,17 @@ public void WriteRaw(string stringValue) /// Значение по умолчанию: Ложь. [ContextMethod("ЗаписатьЗначение", "WriteValue")] public void WriteValue(IValue value, bool useFormatWithExponent = false) - { - if (!IsOpen()) - throw NotOpenException(); + { + CheckWriter(); var clrValue = value.UnwrapToClrObject(); switch (clrValue) { - case string v: - WriteStringValue(v); + case string v: + _writer.WriteRawValue(EscapeCharacters(v)); break; case decimal v: - if (v == Math.Round(v)) + if (v == Math.Truncate(v)) { Int64 i = Convert.ToInt64(v); if (useFormatWithExponent) @@ -341,7 +271,7 @@ public void WriteValue(IValue value, bool useFormatWithExponent = false) _writer.WriteValue(v); break; case DateTime v: - _writer.WriteValue(v); + _writer.WriteValue(JSONDateWriter.FormatDateForJson(v)); break; case null: @@ -354,78 +284,70 @@ public void WriteValue(IValue value, bool useFormatWithExponent = false) } /// - /// /// Записывает имя свойства JSON. /// - /// /// /// Имя свойства. [ContextMethod("ЗаписатьИмяСвойства", "WritePropertyName")] public void WritePropertyName(string propertyName) { + CheckWriter(); _writer.WritePropertyName(propertyName); } /// - /// /// Записывает конец массива JSON. /// - /// [ContextMethod("ЗаписатьКонецМассива", "WriteEndArray")] public void WriteEndArray() { + CheckWriter(); _writer.WriteEndArray(); } /// - /// /// Записывает конец объекта JSON. /// - /// [ContextMethod("ЗаписатьКонецОбъекта", "WriteEndObject")] public void WriteEndObject() { + CheckWriter(); _writer.WriteEndObject(); } /// - /// /// Записывает начало массива JSON. /// - /// [ContextMethod("ЗаписатьНачалоМассива", "WriteStartArray")] public void WriteStartArray() { + CheckWriter(); _writer.WriteStartArray(); } /// - /// /// Записывает начало объекта JSON. /// - /// [ContextMethod("ЗаписатьНачалоОбъекта", "WriteStartObject")] public void WriteStartObject() { + CheckWriter(); _writer.WriteStartObject(); } /// - /// /// Открывает файл для записи JSON. Позволяет указать тип кодировки, который будет использован для записи файла JSON, а также использование BOM. /// - /// /// /// Имя файла, в который будет записываться текст JSON. /// /// В качестве типа кодировки может быть указана одна из возможных кодировок текста. В этом случае файл будет записан в соответствующей кодировке. Если же в качестве параметра указана пустая строка или ничего не указано, то для записи файла будет использована кодировка UTF8. /// Поддерживаемые коды кодировок: - /// /// Значение по умолчанию: UTF-8. /// /// Определяет, будет ли добавлен маркер порядка байт (BOM) к результирующему файлу JSON. @@ -437,66 +359,112 @@ public void WriteStartObject() public void OpenFile(string fileName, string encoding = null, bool addBOM = false, JSONWriterSettings settings = null) { StreamWriter streamWriter; - + var textEncoding = TextEncodingEnum.GetEncodingByName(encoding ?? DEFAULT_ENCODING, addBOM); + try { - if (encoding != null) - streamWriter = FileOpener.OpenWriter(fileName, TextEncodingEnum.GetEncodingByName(encoding, addBOM)); - else - streamWriter = FileOpener.OpenWriter(fileName, TextEncodingEnum.GetEncodingByName("UTF-8", addBOM)); + streamWriter = FileOpener.OpenWriter(fileName, textEncoding); } catch (Exception e) { throw new RuntimeException(e.Message, e); } - if (IsOpen()) - Close(); - - _writer = new JsonTextWriter(streamWriter); - if (settings == null) - SetDefaultOptions(); - else - SetOptions(settings); + Close(); + SetWriter(streamWriter, settings); + } - SetNewLineChars(streamWriter); + /// + /// Открывает поток для записи JSON. Позволяет указать тип кодировки, который будет использован для записи файла JSON, а также использование BOM. + /// + /// + /// Поток, в который будет записываться текст JSON. + /// + /// В качестве типа кодировки может быть указана одна из возможных кодировок текста. В этом случае файл будет записан в соответствующей кодировке. Если же в качестве параметра указана пустая строка или ничего не указано, то для записи файла будет использована кодировка UTF8. + /// Поддерживаемые коды кодировок: + /// Значение по умолчанию: UTF-8. + /// + /// Определяет, будет ли добавлен маркер порядка байт (BOM) к результирующему потоку JSON. + /// Внимание. Стандарт RFC7159 настоятельно рекомендует не добавлять маркер порядка байт (BOM) к документу JSON . + /// Значение по умолчанию: Ложь. + /// + /// Параметры, используемые при открытии потока для настройки записи в формате JSON. + [ContextMethod("ОткрытьПоток", "OpenStream")] + public void OpenStream(IStreamWrapper streamContext, string encoding = null, bool addBOM = false, JSONWriterSettings settings = null) + { + if (streamContext is null) + throw new ArgumentNullException(nameof(streamContext)); + + if (streamContext.IsReadOnly) + throw CannotWriteException(); + + Close(); + var textEncoding = TextEncodingEnum.GetEncodingByName(encoding ?? DEFAULT_ENCODING, addBOM); + StreamWriter streamWriter = new(streamContext.GetUnderlyingStream(), textEncoding, leaveOpen: true); + SetWriter(streamWriter, settings); } /// - /// /// Инициализирует объект для вывода результирующего JSON текста в строку. /// - /// /// /// Параметры, используемые при записи объекта JSON. /// По умолчанию, содержит ПараметрыЗаписиJSON, сгенерированные автоматически. [ContextMethod("УстановитьСтроку", "SetString")] public void SetString(JSONWriterSettings settings = null) { - if (IsOpen()) - Close(); + Close(); + _stringWriter = new StringWriter(); - _writer = new JsonTextWriter(_stringWriter); - if (settings == null) - SetDefaultOptions(); - else - SetOptions(settings); + SetWriter(_stringWriter, settings); + } - SetNewLineChars(_stringWriter); + private void SetWriter(TextWriter stream, JSONWriterSettings settings) + { + _writer = new JsonTextWriter(stream); + + SetOptions(settings); + SetNewLineChars(stream); } + #region IDisposable Members + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing && _writer is not null) + { + _stringWriter?.Dispose(); + _stringWriter = null; + + _writer.Close(); + _writer = null; + } + } + + #endregion + RuntimeException NotOpenException() { - return new RuntimeException(Locale.NStr - ("ru='Приемник данных JSON не открыт';en='JSON data target is not opened'")); + return new RuntimeException("Приемник данных JSON не открыт", "JSON data target is not opened"); } - RuntimeException TypeNotSupportedException(Type type) + RuntimeException CannotWriteException() { - return new RuntimeException(Locale.NStr - ($"ru='Запись значения типа {type} не поддерживается.'; en='Can not write value of type {type}'")); + return new("Попытка записи в поток не поддерживающий запись", + "Cannot write to a stream that does not support writing"); } + RuntimeException TypeNotSupportedException(Type type) + { + return new RuntimeException($"Запись значения типа {type} не поддерживается", + $"Cannot write value of type {type}"); + } } } diff --git a/src/OneScript.StandardLibrary/Json/JSONWriterSettings.cs b/src/OneScript.StandardLibrary/Json/JSONWriterSettings.cs index ba7825f2d..292bf3c90 100644 --- a/src/OneScript.StandardLibrary/Json/JSONWriterSettings.cs +++ b/src/OneScript.StandardLibrary/Json/JSONWriterSettings.cs @@ -11,7 +11,6 @@ This Source Code Form is subject to the terms of the namespace OneScript.StandardLibrary.Json { /// - /// /// Определяет набор параметров, используемых при записи JSON. /// [ContextClass("ПараметрыЗаписиJSON", "JSONWriterSettings")] @@ -51,17 +50,15 @@ public JSONWriterSettings(JSONLineBreakEnum NewLines = JSONLineBreakEnum.Auto, s /// - /// /// Создает объект параметров записи JSON. /// - /// /// /// Определяет способ переноса строк, который будет использован при записи данных JSON. /// Значение по умолчанию: Авто. /// /// Определяет символы отступа, используемые при записи данных JSON. /// Применяется только, если значение ПереносСтрокJSON отлично от Нет. - /// Значение по умолчанию: " ". + /// Значение по умолчанию: "". /// /// Определяет, будут ли при записи имена свойств JSON записываться в двойных кавычках. /// Значение по умолчанию: Истина. @@ -86,8 +83,8 @@ public JSONWriterSettings(JSONLineBreakEnum NewLines = JSONLineBreakEnum.Auto, s /// Значение по умолчанию: Ложь. [ScriptConstructor(Name = "По описанию параметров записи")] public static JSONWriterSettings ParametrizedConstructor( - JSONLineBreakEnum newLines = JSONLineBreakEnum.None, - string paddingSymbols = null, + JSONLineBreakEnum newLines = JSONLineBreakEnum.Auto, + string paddingSymbols = "", bool useDoubleQuotes = true, JSONCharactersEscapeModeEnum escapeCharacters = JSONCharactersEscapeModeEnum.None, bool escapeAngleBrackets = false, @@ -107,11 +104,6 @@ public static JSONWriterSettings ParametrizedConstructor( escapeSlash); } - /// - /// - /// - /// - /// [ScriptConstructor] public static JSONWriterSettings Constructor() { @@ -119,7 +111,6 @@ public static JSONWriterSettings Constructor() } /// - /// /// Определяет использование двойных кавычек при записи свойств и значений JSON. /// После создания объекта данное свойство имеет значение Истина. /// @@ -129,7 +120,6 @@ public static JSONWriterSettings Constructor() /// - /// /// Управляет размещением начала и конца объектов и массивов, ключей и значений на новой строке. /// Также установка данного свойства в значение, отличное от Нет, добавляет пробел между именем свойства, двоеточием и значением. /// После создания объекта данное свойство имеет значение Авто. @@ -140,20 +130,18 @@ public static JSONWriterSettings Constructor() /// - /// /// Определяет символы отступа, используемые при записи документа JSON. /// Свойство не используется, если свойство ПереносСтрокJSON установлено в значение Нет. - /// После создания объекта данное свойство имеет значение - один пробельный символ. + /// После создания объекта данное свойство имеет значение - пустая строка. /// /// Строка (String) [ContextProperty("СимволыОтступа", "PaddingSymbols")] - public string PaddingSymbols => _paddingSymbols; + public string PaddingSymbols => _paddingSymbols ?? string.Empty; /// - /// /// Определяет способ экранирования символов при записи документа JSON. - /// После создания объекта данное свойство имеет значение СимволыВнеASCII. + /// После создания объекта данное свойство имеет значение Нет. /// /// ЭкранированиеСимволовJSON (JSONCharactersEscapeMode) [ContextProperty("ЭкранированиеСимволов", "EscapeCharacters")] @@ -161,7 +149,6 @@ public static JSONWriterSettings Constructor() /// - /// /// Определяет, будет ли экранироваться знак амперсанда при записи документа JSON. /// После создания объекта данное свойство имеет значение Ложь. /// @@ -171,7 +158,6 @@ public static JSONWriterSettings Constructor() /// - /// /// Определяет, будет ли экранирован знак одинарной кавычки при записи документа JSON. /// Имеет значение Истина, если ИспользоватьДвойныеКавычки установлен Ложь. /// После создания объекта данное свойство имеет значение Ложь @@ -182,7 +168,6 @@ public static JSONWriterSettings Constructor() /// - /// /// Определяет экранирование символов "U+2028" (разделитель строк) и "U+2029" (разделитель абзацев) для совместимости с JavaScript. /// После создания объекта данное свойство имеет значение Истина. /// @@ -192,7 +177,6 @@ public static JSONWriterSettings Constructor() /// - /// /// Определяет, будет ли экранироваться слеш (косая черта) при записи значения. /// После создания объекта данное свойство имеет значение Ложь. /// @@ -202,7 +186,6 @@ public static JSONWriterSettings Constructor() /// - /// /// Определяет, будут ли экранированы знаки угловых скобок при записи документа JSON. /// После создания объекта данное свойство имеет значение Ложь. /// diff --git a/src/OneScript.StandardLibrary/NativeApi/NativeApiComponent.cs b/src/OneScript.StandardLibrary/NativeApi/NativeApiComponent.cs index dc73f7edf..27f40d36c 100644 --- a/src/OneScript.StandardLibrary/NativeApi/NativeApiComponent.cs +++ b/src/OneScript.StandardLibrary/NativeApi/NativeApiComponent.cs @@ -22,6 +22,11 @@ class NativeApiComponent : BslObjectValue, IRuntimeContextInstance, IDisposable { private IntPtr _object; private TypeDescriptor _type; + // Ссылка на библиотеку Native API, нужна для удержания ссылки и устранения гонки финализаторов. + private readonly NativeApiLibrary _library; + private readonly NativeApiProxy.OnErrorDelegate _onError; + private readonly NativeApiProxy.OnEventDelegate _onEvent; + private readonly NativeApiProxy.OnStatusDelegate _onStatus; public event OnComponentEvent OnComponentEvent; @@ -78,18 +83,21 @@ public NativeApiComponent(object host, NativeApiLibrary library, TypeDescriptor if (!NativeApiProxy.IsAvailable) throw new RuntimeException("Native API Proxy DLL is not loaded"); - _object = NativeApiProxy.GetClassObject(library.Module, componentName, - (wcode, source, descr, scode) => - { - OnComponentError?.Invoke(Status(wcode), scode, S(source), S(descr)); - }, - (source, message, data) => { - OnComponentEvent?.Invoke(S(source), S(message), S(data)); - }, - (status) => { - OnComponentStatusText?.Invoke(S(status)); - } - ); + _library = library; + _onError = (wcode, source, descr, scode) => + { + OnComponentError?.Invoke(Status(wcode), scode, S(source), S(descr)); + }; + _onEvent = (source, message, data) => + { + OnComponentEvent?.Invoke(S(source), S(message), S(data)); + }; + _onStatus = status => + { + OnComponentStatusText?.Invoke(S(status)); + }; + + _object = NativeApiProxy.GetClassObject(library.Module, componentName, _onError, _onEvent, _onStatus); _type = typeDef; } @@ -200,7 +208,7 @@ public BslMethodInfo GetMethodInfo(int methodNumber) { var parameter = method.NewParameter() .Name($"p{i}") - .ByValue(true); + .ByValue(false); if (NativeApiProxy.HasParamDefValue(_object, methodNumber, i)) { @@ -243,6 +251,15 @@ private void SetDefValues(int methodNumber, int paramCount, IValue[] arguments) ); } + private void WriteBackParameters(int paramCount, IValue[] arguments, IntPtr variantPtr) + { + for (int i = 0; i < paramCount; i++) + { + if (arguments[i] is IVariable variable) + variable.Value = NativeApiVariant.Value(variantPtr, i); + } + } + public void CallAsProcedure(int methodNumber, IValue[] arguments, IBslProcess process) { int paramCount = NativeApiProxy.GetNParams(_object, methodNumber); @@ -253,6 +270,7 @@ public void CallAsProcedure(int methodNumber, IValue[] arguments, IBslProcess pr variant.Assign(arguments[i], i); NativeApiProxy.CallAsProc(_object, methodNumber, variant.Ptr); + WriteBackParameters(paramCount, arguments, variant.Ptr); } } @@ -269,6 +287,7 @@ public void CallAsFunction(int methodNumber, IValue[] arguments, out IValue retV NativeApiProxy.CallAsFunc(_object, methodNumber, variant.Ptr, res => result = NativeApiVariant.Value(res) ); + WriteBackParameters(paramCount, arguments, variant.Ptr); } retValue = result; } diff --git a/src/OneScript.StandardLibrary/NativeApi/NativeApiFactory.cs b/src/OneScript.StandardLibrary/NativeApi/NativeApiFactory.cs index d33abce8d..2adf518a8 100644 --- a/src/OneScript.StandardLibrary/NativeApi/NativeApiFactory.cs +++ b/src/OneScript.StandardLibrary/NativeApi/NativeApiFactory.cs @@ -9,6 +9,7 @@ This Source Code Form is subject to the terms of the using System.Collections.Generic; using OneScript.Types; using OneScript.Contexts; +using ScriptEngine; using ScriptEngine.Machine; namespace OneScript.StandardLibrary.NativeApi @@ -17,36 +18,61 @@ namespace OneScript.StandardLibrary.NativeApi /// Фабрика, осуществляющая регистрацию библиотеки внешних /// компонент Native API и создания экземпляров компонент. /// - class NativeApiFactory + class NativeApiFactory : IEngineLifetime { - public static bool Register(string filepath, string identifier, ITypeManager typeManager) + private readonly object _sync = new object(); + private readonly Dictionary _libraries = new Dictionary(); + private bool _disposed; + + public bool Register(string filepath, string identifier, ITypeManager typeManager) { - if (_libraries.ContainsKey(identifier)) - return false; - var library = new NativeApiLibrary(filepath, identifier, typeManager); - if (library.Loaded) - _libraries.Add(identifier, library); - return library.Loaded; - } + lock (_sync) + { + ObjectDisposedException.ThrowIf(_disposed, this); - private static readonly Dictionary _libraries = new Dictionary(); + if (_libraries.ContainsKey(identifier)) + return true; - internal static void Initialize() + var library = new NativeApiLibrary(filepath, identifier, typeManager); + if (library.Loaded) + _libraries.Add(identifier, library); + return library.Loaded; + } + } + + public void Dispose() { - foreach (var item in _libraries) - item.Value.Dispose(); - _libraries.Clear(); + lock (_sync) + { + if (_disposed) + return; + + foreach (var item in _libraries) + item.Value.Dispose(); + + _libraries.Clear(); + _disposed = true; + } } [ScriptConstructor] public static IValue Constructor(TypeActivationContext context) { + var factory = context.Services.Resolve(); var typeName = context.TypeName; var separator = new char[] { '.' }; var names = typeName.Split(separator, StringSplitOptions.RemoveEmptyEntries); - if (names.Length == 3 && _libraries.TryGetValue(names[1], out NativeApiLibrary library)) + if (names.Length == 3 && factory.TryGetLibrary(names[1], out NativeApiLibrary library)) return library.CreateComponent(context.TypeManager, default, typeName, names[2]); throw new NotImplementedException(); } + + private bool TryGetLibrary(string identifier, out NativeApiLibrary library) + { + lock (_sync) + { + return _libraries.TryGetValue(identifier, out library); + } + } } } diff --git a/src/OneScript.StandardLibrary/NativeApi/NativeApiLibrary.cs b/src/OneScript.StandardLibrary/NativeApi/NativeApiLibrary.cs index f5be8a3ee..dfe46573c 100644 --- a/src/OneScript.StandardLibrary/NativeApi/NativeApiLibrary.cs +++ b/src/OneScript.StandardLibrary/NativeApi/NativeApiLibrary.cs @@ -25,6 +25,9 @@ class NativeApiLibrary : IDisposable private readonly List _components = new List(); + private readonly Dictionary _nameToClassName = + new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly String _tempfile; public NativeApiLibrary(string filepath, string identifier, ITypeManager typeManager) @@ -68,51 +71,87 @@ private void RegisterComponents(string identifier, ITypeManager typeManager) throw new RuntimeException("Не удалось получить список компонент в составе библиотеки"); var separator = new char[] { '|' }; var names = NativeApiProxy.Str(namesPtr).Split(separator, StringSplitOptions.RemoveEmptyEntries); - foreach (String name in names) - typeManager.RegisterType($"AddIn.{identifier}.{name}", default, typeof(NativeApiFactory)); + foreach (String className in names) + { + var ptr = NativeApiProxy.GetClassObject(Module, className, null, null, null); + if (ptr == IntPtr.Zero) + { + typeManager.RegisterType($"AddIn.{identifier}.{className}", default, typeof(NativeApiFactory)); + continue; + } + + var extensionName = string.Empty; + NativeApiProxy.GetExtensionName(ptr, n => extensionName = NativeApiProxy.Str(n)); + NativeApiProxy.DestroyObject(ptr); + + if (string.IsNullOrEmpty(extensionName)) + { + typeManager.RegisterType($"AddIn.{identifier}.{className}", default, typeof(NativeApiFactory)); + continue; + } + + _nameToClassName[extensionName] = className; + + if (string.Equals(extensionName, className, StringComparison.OrdinalIgnoreCase)) + typeManager.RegisterType($"AddIn.{identifier}.{extensionName}", default, typeof(NativeApiFactory)); + else + typeManager.RegisterType( + $"AddIn.{identifier}.{extensionName}", + $"AddIn.{identifier}.{className}", + typeof(NativeApiFactory)); + } + } + + internal string ResolveClassName(string name) + { + if (_nameToClassName.TryGetValue(name, out var className)) + return className; + return name; } public IValue CreateComponent(ITypeManager typeManager, object host, String typeName, String componentName) { var typeDef = typeManager.GetTypeByName(typeName); - var component = new NativeApiComponent(host, this, typeDef, componentName); + var resolvedName = ResolveClassName(componentName); + var component = new NativeApiComponent(host, this, typeDef, resolvedName); _components.Add(component); return component; } - private void ReleaseUnmanagedResources(bool isDisposing) + private void DisposeManagedResources() { - try + foreach (var component in _components) { - foreach (var component in _components) - { - component.Dispose(); - } - - if (Loaded && NativeApiKernel.FreeLibrary(Module)) - { - if (!String.IsNullOrEmpty(_tempfile)) - { - File.Delete(_tempfile); - } - } + component.Dispose(); } - catch (Exception) + + _components.Clear(); + } + + private void ReleaseUnmanagedResources() + { + if (!Loaded) + return; + + NativeApiKernel.FreeLibrary(Module); + Module = IntPtr.Zero; + + if (!String.IsNullOrEmpty(_tempfile)) { - if (isDisposing) - throw; + File.Delete(_tempfile); } } public void Dispose() { - ReleaseUnmanagedResources(true); + DisposeManagedResources(); + ReleaseUnmanagedResources(); GC.SuppressFinalize(this); } ~NativeApiLibrary() { - ReleaseUnmanagedResources(false); + ReleaseUnmanagedResources(); } } } diff --git a/src/OneScript.StandardLibrary/NativeApi/NativeApiProxy.cs b/src/OneScript.StandardLibrary/NativeApi/NativeApiProxy.cs index ab7cc697b..0f1eebcce 100644 --- a/src/OneScript.StandardLibrary/NativeApi/NativeApiProxy.cs +++ b/src/OneScript.StandardLibrary/NativeApi/NativeApiProxy.cs @@ -21,11 +21,14 @@ static class NativeApiProxy [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode)] public delegate void TDestroyObject(IntPtr ptr); + [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode)] + public delegate void TGetExtensionName(IntPtr ptr, PointerDelegate response); + [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode)] public delegate IntPtr TCreateVariant(Int32 length); [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode)] - public delegate void TFreeVariant(IntPtr ptr); + public delegate void TFreeVariant(IntPtr ptr, Int32 count); [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode)] public delegate Int32 TGetNProps(IntPtr ptr); @@ -69,12 +72,16 @@ static class NativeApiProxy [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode)] public delegate void TSetVariantBlob(IntPtr ptr, Int32 num, byte[] data, Int32 length); + [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode)] + public delegate void TSetVariantDate(IntPtr ptr, Int32 num, Double value); + [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode)] public delegate void TGetVariant(IntPtr ptr, Int32 num , TSetVariantEmpty e , TSetVariantBool b , TSetVariantInt i , TSetVariantReal r + , TSetVariantDate d , TSetVariantPtr s , TSetVariantPtr x ); @@ -108,6 +115,7 @@ public delegate void TGetVariant(IntPtr ptr, Int32 num public static readonly TGetClassObject GetClassObject; public static readonly TDestroyObject DestroyObject; + public static readonly TGetExtensionName GetExtensionName; public static readonly TCreateVariant CreateVariant; public static readonly TFreeVariant FreeVariant; public static readonly TGetNProps GetNProps; @@ -124,6 +132,7 @@ public delegate void TGetVariant(IntPtr ptr, Int32 num public static readonly TSetVariantBlob SetVariantBlob; public static readonly TSetVariantInt SetVariantInt; public static readonly TSetVariantStr SetVariantStr; + public static readonly TSetVariantDate SetVariantDate; public static readonly TGetNMethods GetNMethods; public static readonly TFindMethod FindMethod; public static readonly TGetMethodName GetMethodName; @@ -154,6 +163,7 @@ static NativeApiProxy() GetClassObject = Marshal.GetDelegateForFunctionPointer(NativeApiKernel.GetProcAddress(module, "GetClassObject")); DestroyObject = Marshal.GetDelegateForFunctionPointer(NativeApiKernel.GetProcAddress(module, "DestroyObject")); + GetExtensionName = Marshal.GetDelegateForFunctionPointer(NativeApiKernel.GetProcAddress(module, "GetExtensionName")); CreateVariant = Marshal.GetDelegateForFunctionPointer(NativeApiKernel.GetProcAddress(module, "CreateVariant")); FreeVariant = Marshal.GetDelegateForFunctionPointer(NativeApiKernel.GetProcAddress(module, "FreeVariant")); GetNProps = Marshal.GetDelegateForFunctionPointer(NativeApiKernel.GetProcAddress(module, "GetNProps")); @@ -169,8 +179,8 @@ static NativeApiProxy() SetVariantBlob = Marshal.GetDelegateForFunctionPointer(NativeApiKernel.GetProcAddress(module, "SetVariantBlob")); SetVariantInt = Marshal.GetDelegateForFunctionPointer(NativeApiKernel.GetProcAddress(module, "SetVariantInt")); SetVariantStr = Marshal.GetDelegateForFunctionPointer(NativeApiKernel.GetProcAddress(module, "SetVariantStr")); + SetVariantDate = Marshal.GetDelegateForFunctionPointer(NativeApiKernel.GetProcAddress(module, "SetVariantDate")); GetVariant = Marshal.GetDelegateForFunctionPointer(NativeApiKernel.GetProcAddress(module, "GetVariant")); - GetPropVal = Marshal.GetDelegateForFunctionPointer(NativeApiKernel.GetProcAddress(module, "GetPropVal")); GetNMethods = Marshal.GetDelegateForFunctionPointer(NativeApiKernel.GetProcAddress(module, "GetNMethods")); FindMethod = Marshal.GetDelegateForFunctionPointer(NativeApiKernel.GetProcAddress(module, "FindMethod")); GetMethodName = Marshal.GetDelegateForFunctionPointer(NativeApiKernel.GetProcAddress(module, "GetMethodName")); diff --git a/src/OneScript.StandardLibrary/NativeApi/NativeApiVariant.cs b/src/OneScript.StandardLibrary/NativeApi/NativeApiVariant.cs index b059638e9..b056217b8 100644 --- a/src/OneScript.StandardLibrary/NativeApi/NativeApiVariant.cs +++ b/src/OneScript.StandardLibrary/NativeApi/NativeApiVariant.cs @@ -7,6 +7,7 @@ This Source Code Form is subject to the terms of the using System; using System.Runtime.InteropServices; +using OneScript.Exceptions; using OneScript.StandardLibrary.Binary; using ScriptEngine.Machine; @@ -19,19 +20,26 @@ namespace OneScript.StandardLibrary.NativeApi /// class NativeApiVariant: IDisposable { - private readonly IntPtr variant = IntPtr.Zero; + private IntPtr variant = IntPtr.Zero; + private readonly Int32 _count; public IntPtr Ptr { get { return variant; } } public NativeApiVariant(Int32 count = 1) { + _count = count; variant = NativeApiProxy.CreateVariant(count); + if (count > 0 && variant == IntPtr.Zero) + throw new RuntimeException("Не удалось выделить память для массива параметров Native API"); } public void Dispose() { if (variant != IntPtr.Zero) - NativeApiProxy.FreeVariant(variant); + { + NativeApiProxy.FreeVariant(variant, _count); + variant = IntPtr.Zero; + } } public void Assign(IValue value, Int32 number = 0) @@ -54,6 +62,9 @@ public void Assign(IValue value, Int32 number = 0) case BinaryDataContext binaryData: NativeApiProxy.SetVariantBlob(variant, number, binaryData.Buffer, binaryData.Buffer.Length); break; + case DateTime dt: + NativeApiProxy.SetVariantDate(variant, number, dt.ToOADate()); + break; default: NativeApiProxy.SetVariantEmpty(variant, number); break; @@ -67,6 +78,7 @@ public static IValue Value(IntPtr variant, Int32 number = 0) (v, n, r) => value = ValueFactory.Create(r), (v, n, r) => value = ValueFactory.Create((Decimal)r), (v, n, r) => value = ValueFactory.Create((Decimal)r), + (v, n, d) => value = ValueFactory.Create(DateTime.FromOADate(d)), (v, n, r, s) => value = ValueFactory.Create(Marshal.PtrToStringUni(r, s)), (v, n, r, s) => { byte[] buffer = new byte[s]; diff --git a/src/OneScript.StandardLibrary/Processes/ArgumentsParser.cs b/src/OneScript.StandardLibrary/Processes/ArgumentsParser.cs index 6000d85ab..60623e28b 100644 --- a/src/OneScript.StandardLibrary/Processes/ArgumentsParser.cs +++ b/src/OneScript.StandardLibrary/Processes/ArgumentsParser.cs @@ -42,10 +42,19 @@ public string[] GetArguments() } _currentArgument.Clear(); - - foreach (var currentChar in _input) + + for (var i = 0; i < _input.Length; i++) { - if (char.IsWhiteSpace(currentChar) && !_isInQuotes) + var currentChar = _input[i]; + + if (_isInQuotes && currentChar == _currentQuote + && i + 1 < _input.Length && _input[i + 1] == _currentQuote) + { + // удвоенная кавычка внутри кавычек - экранированная кавычка + PushChar(currentChar); + i++; + } + else if (char.IsWhiteSpace(currentChar) && !_isInQuotes) { PushArgument(); } diff --git a/src/OneScript.StandardLibrary/Processes/ProcessContext.cs b/src/OneScript.StandardLibrary/Processes/ProcessContext.cs index a1ee50e77..a5948a1bf 100644 --- a/src/OneScript.StandardLibrary/Processes/ProcessContext.cs +++ b/src/OneScript.StandardLibrary/Processes/ProcessContext.cs @@ -29,6 +29,8 @@ public class ProcessContext : AutoContext, IDisposable private StdTextReadStream _stdOutContext; private StdTextReadStream _stdErrContext; private StdTextWriteStream _stdInContext; + private ProcessOutputWrapper _stdOutWrapper; + private ProcessOutputWrapper _stdErrWrapper; private readonly IValue _outputEncoding; @@ -107,15 +109,14 @@ public void Start() if (IsOutputRedirected) { - var stream = new ProcessOutputWrapper(_p, ProcessOutputWrapper.OutputVariant.Stdout); - stream.StartReading(); - _stdOutContext = new StdTextReadStream(stream); + _stdOutWrapper = new ProcessOutputWrapper(_p.StandardOutput); + _stdOutWrapper.StartReading(); + _stdOutContext = new StdTextReadStream(_stdOutWrapper); - stream = new ProcessOutputWrapper(_p, ProcessOutputWrapper.OutputVariant.Stderr); - stream.StartReading(); - - _stdErrContext = new StdTextReadStream(stream); + _stdErrWrapper = new ProcessOutputWrapper(_p.StandardError); + _stdErrWrapper.StartReading(); + _stdErrContext = new StdTextReadStream(_stdErrWrapper); } } @@ -149,7 +150,14 @@ public bool HasExited { get { - return _p.HasExited; + if (!_p.HasExited) + return false; + + // Процесс завершился, но фоновое чтение могло еще не добрать + // хвост вывода из пайпа: цикл "Пока НЕ Завершен ИЛИ ЕстьДанные" + // не должен завершиться раньше, чем вывод станет доступен + return (_stdOutWrapper?.IsDrained ?? true) + && (_stdErrWrapper?.IsDrained ?? true); } } @@ -176,6 +184,12 @@ public bool WaitForExit(IValue timeout = null) if (timeout == null) { _p.WaitForExit(); + + // Прочитать() после ожидания должен видеть весь вывод: + // дожидаемся, пока фоновое чтение доберет хвост потоков + _stdOutWrapper?.WaitSourceDrained(); + _stdErrWrapper?.WaitSourceDrained(); + return true; } diff --git a/src/OneScript.StandardLibrary/Processes/ProcessOutputWrapper.cs b/src/OneScript.StandardLibrary/Processes/ProcessOutputWrapper.cs index afcd9800a..7b800e6a8 100644 --- a/src/OneScript.StandardLibrary/Processes/ProcessOutputWrapper.cs +++ b/src/OneScript.StandardLibrary/Processes/ProcessOutputWrapper.cs @@ -1,39 +1,57 @@ -/*---------------------------------------------------------- -This Source Code Form is subject to the terms of the -Mozilla Public License, v.2.0. If a copy of the MPL -was not distributed with this file, You can obtain one +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. ----------------------------------------------------------*/ using System; using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.ExceptionServices; using System.Text; -using sys = System.Diagnostics; +using System.Threading.Tasks; + +[assembly: InternalsVisibleTo("OneScript.StandardLibrary.Tests")] namespace OneScript.StandardLibrary.Processes { + /// + /// Неблокирующий читатель поверх потока вывода процесса + /// (Process.StandardOutput/StandardError). Фоновая задача перекачивает + /// сырые символы источника в буфер, включая настоящие переводы строк, + /// записанные процессом. + /// Событийный API (BeginOutputReadLine) не используется: он вырезает + /// терминаторы строк, и их обратный синтез порождал гонку (issue #1726), + /// а вывод, не завершенный переводом строки, застревал в .NET до конца процесса. + /// class ProcessOutputWrapper : TextReader { - private readonly sys.Process _process; - private readonly OutputVariant _variant; + private const int CompactionThreshold = 4096; + + private readonly TextReader _source; private readonly StringBuilder _buffer = new StringBuilder(4096); private int _bufferIndex = 0; - private bool AlreadyReading { get; set; } + // Позиция, до которой ReadLine уже искал терминатор и не нашел: + // диапазон [_bufferIndex, _lineScanIndex) заведомо без терминаторов, + // повторные вызовы сканируют только новые символы, а не буфер целиком + private int _lineScanIndex = 0; - private Encoding Encoding { get; set; } + // Пишутся пампом и читаются читателями только под lock(_buffer) + private bool _streamEnded; + private Exception _pumpError; - public enum OutputVariant - { - Stdout, - Stderr - } + private volatile bool _stopRequested; + + private Task _pumpTask; + + private bool AlreadyReading { get; set; } - public ProcessOutputWrapper(sys.Process process, OutputVariant variant) + public ProcessOutputWrapper(TextReader source) { - _process = process; - _variant = variant; + _source = source; } public void StartReading() @@ -41,44 +59,87 @@ public void StartReading() if (AlreadyReading) return; - if (_variant == OutputVariant.Stdout) - { - Encoding = _process.StartInfo.StandardOutputEncoding; - _process.OutputDataReceived += StreamDataReceived; - _process.BeginOutputReadLine(); - } - else - { - Encoding = _process.StartInfo.StandardErrorEncoding; - _process.ErrorDataReceived += StreamDataReceived; - _process.BeginErrorReadLine(); - } + _pumpTask = Task.Run(() => PumpAsync(_source)); AlreadyReading = true; } - private void StopReading() + private async Task PumpAsync(TextReader reader) { - if (_variant == OutputVariant.Stdout) + var chunk = new char[4096]; + try + { + while (true) + { + int read = await reader.ReadAsync(chunk, 0, chunk.Length).ConfigureAwait(false); + if (read == 0) + break; + + lock (_buffer) + { + // После закрытия читателя данные никому не нужны, но пайп + // продолжает дренироваться до конца потока: иначе процесс, + // заполнив пайп, навсегда заблокируется на записи + if (!_stopRequested) + _buffer.Append(chunk, 0, read); + } + } + } + catch (Exception e) { - _process.OutputDataReceived -= StreamDataReceived; + // Ошибка источника перебрасывается читателю, когда он вычитает + // накопленные данные (см. ThrowIfPumpFailed). После остановки + // чтения (Dispose) источник закрыт, его ошибки ожидаемы. + if (!_stopRequested) + { + lock (_buffer) + { + _pumpError = e; + } + } } - else + finally { - _process.ErrorDataReceived -= StreamDataReceived; + lock (_buffer) + { + _streamEnded = true; + } } } - private void StreamDataReceived(object sender, sys.DataReceivedEventArgs e) + // Ошибка чтения источника отдается после уже накопленных данных, + // поэтому перебрасывается только при пустом буфере. + // должна вызываться ТОЛЬКО внутри вышестоящего блока lock. + private void ThrowIfPumpFailed() + { + if (_pumpError != null && _bufferIndex >= _buffer.Length) + ExceptionDispatchInfo.Capture(_pumpError).Throw(); + } + + /// + /// Дождаться, пока весь вывод процесса будет перекачан в буфер. + /// Вызывается после Process.WaitForExit(), чтобы Прочитать() гарантированно + /// видел хвост вывода (аналог гарантии WaitForExit() для событийного чтения). + /// + internal void WaitSourceDrained() + { + _pumpTask?.Wait(); + } + + /// + /// Источник дочитан до конца: после завершения процесса хвост его + /// вывода гарантированно доступен читателям. + /// + internal bool IsDrained => _pumpTask == null || _pumpTask.IsCompleted; + + // Для тестов: физический размер внутреннего буфера + internal int InternalBufferSize { - if (e.Data != null) + get { - lock(_buffer) + lock (_buffer) { - if (_buffer.Length != 0) - _buffer.Append(System.Environment.NewLine); - - _buffer.Append(e.Data); + return _buffer.Length; } } } @@ -88,9 +149,12 @@ public override int Peek() lock (_buffer) { if (_bufferIndex >= _buffer.Length) + { + ThrowIfPumpFailed(); return -1; // no data + } - return _buffer[_bufferIndex]; + return _buffer[_bufferIndex]; } } @@ -98,7 +162,26 @@ public override int Read() { lock (_buffer) { - return ReadInternal(); + int ch = ReadInternal(); + if (ch == -1) + ThrowIfPumpFailed(); + + CompactBuffer(); + return ch; + } + } + + // Вычитанный префикс буфера периодически удаляется, иначе вывод + // долгоживущего процесса накапливается в памяти даже при аккуратном + // читателе. + // должна вызываться ТОЛЬКО внутри вышестоящего блока lock. + private void CompactBuffer() + { + if (_bufferIndex >= CompactionThreshold) + { + _buffer.Remove(0, _bufferIndex); + _lineScanIndex = Math.Max(0, _lineScanIndex - _bufferIndex); + _bufferIndex = 0; } } @@ -123,7 +206,7 @@ public override int Read(char[] destBuffer, int index, int count) if (destBuffer.Length - index < count) throw new ArgumentException("Invalid offset"); - + int n = 0; lock (_buffer) { @@ -133,42 +216,81 @@ public override int Read(char[] destBuffer, int index, int count) if (ch == -1) break; destBuffer[index + n++] = (char)ch; - } while (n < count); + } while (n < count); + + if (n == 0 && count > 0) + ThrowIfPumpFailed(); + + CompactBuffer(); } return n; } + /// + /// Возвращает очередную строку, только когда она гарантированно полна: + /// ее терминатор уже в буфере либо поток источника закончился. + /// Для незавершенной строки возвращает null (данных пока нет) — + /// частично записанная процессом строка не может быть возвращена + /// ни как строка, ни по кускам. + /// public override string ReadLine() { - var sb = new StringBuilder(); lock (_buffer) { - while (true) + for (int i = Math.Max(_lineScanIndex, _bufferIndex); i < _buffer.Length; i++) { - int ch = ReadInternal(); - if (ch == -1) break; - if (ch == '\r' || ch == '\n') + char ch = _buffer[i]; + + if (ch == '\n') + return ConsumeLine(i, i + 1); + + if (ch == '\r') { - if (ch == '\r' && Peek() == '\n') Read(); - return sb.ToString(); + if (i + 1 < _buffer.Length) + return ConsumeLine(i, _buffer[i + 1] == '\n' ? i + 2 : i + 1); + + if (_streamEnded) + return ConsumeLine(i, i + 1); + + // '\r' — последний символ буфера, а поток еще жив: + // парный '\n' может быть в пути, ждем следующей порции + // (сам '\r' при этом остается несканированным) + _lineScanIndex = i; + return null; } - sb.Append((char)ch); - } + } + + _lineScanIndex = _buffer.Length; + + // терминатора нет; после конца потока остаток буфера — последняя строка + if (_streamEnded && _bufferIndex < _buffer.Length) + return ConsumeLine(_buffer.Length, _buffer.Length); + + ThrowIfPumpFailed(); + return null; } - if (sb.Length > 0) - return sb.ToString(); + } - return null; + // должна вызываться ТОЛЬКО внутри вышестоящего блока lock. + private string ConsumeLine(int lineEnd, int nextPosition) + { + var line = _buffer.ToString(_bufferIndex, lineEnd - _bufferIndex); + _bufferIndex = nextPosition; + _lineScanIndex = nextPosition; + CompactBuffer(); + return line; } public override string ReadToEnd() { lock (_buffer) { - string data = base.ReadToEnd(); + ThrowIfPumpFailed(); + + string data = _buffer.ToString(_bufferIndex, _buffer.Length - _bufferIndex); ResetBuffer(); - return data; + return data; } } @@ -176,13 +298,17 @@ private void ResetBuffer() { _buffer.Clear(); _bufferIndex = 0; + _lineScanIndex = 0; } protected override void Dispose(bool disposing) { if (disposing) { - StopReading(); + lock (_buffer) + { + _stopRequested = true; + } } base.Dispose(disposing); diff --git a/src/OneScript.StandardLibrary/StandardGlobalContext.cs b/src/OneScript.StandardLibrary/StandardGlobalContext.cs index f12754f12..26fc2f349 100644 --- a/src/OneScript.StandardLibrary/StandardGlobalContext.cs +++ b/src/OneScript.StandardLibrary/StandardGlobalContext.cs @@ -75,6 +75,28 @@ public void RunGarbageCollection() GC.WaitForPendingFinalizers(); } + /// + /// Возвращает поток исполнения, в котором выполняется текущий код. + /// + /// Отдельным потоком исполнения является каждая независимая единица исполнения bsl-кода: + /// основной скрипт, каждое фоновое задание и каждый обрабатываемый запрос веб-сервера. + /// Для одной единицы исполнения всегда возвращается один и тот же объект, поэтому его + /// свойство Данные можно использовать как набор thread-local переменных. + /// + /// В отличие от идентификатора фонового задания, поток исполнения определён во всех + /// контекстах, в том числе при обработке запросов веб-сервера, где фоновое задание + /// отсутствует. + /// + /// Поток исполнения не наследуется: фоновое задание, запущенное из текущего потока + /// исполнения, получит собственный поток с собственными данными. + /// + /// ПотокИсполнения. Поток исполнения текущего кода. + [ContextMethod("ТекущийПоток", "CurrentThread")] + public ExecutionThreadContext CurrentThread(IBslProcess process) + { + return ExecutionThreadContext.Of(process); + } + /// /// Приостанавливает выполнение скрипта. /// @@ -100,7 +122,9 @@ public string DetailErrorDescription(ExceptionInfoContext errInfo) [ContextMethod("ТекущаяУниверсальнаяДата", "CurrentUniversalDate")] public IValue CurrentUniversalDate() { - return ValueFactory.Create(DateTime.UtcNow); + var date = DateTime.UtcNow; + date = date.AddTicks(-(date.Ticks % TimeSpan.TicksPerSecond)); + return ValueFactory.Create(date); } [ContextMethod("ТекущаяУниверсальнаяДатаВМиллисекундах", "CurrentUniversalDateInMilliseconds")] diff --git a/src/OneScript.StandardLibrary/Tasks/BackgroundTasksManager.cs b/src/OneScript.StandardLibrary/Tasks/BackgroundTasksManager.cs index 98aec99f1..c3d253ce2 100644 --- a/src/OneScript.StandardLibrary/Tasks/BackgroundTasksManager.cs +++ b/src/OneScript.StandardLibrary/Tasks/BackgroundTasksManager.cs @@ -1,4 +1,4 @@ -/*---------------------------------------------------------- +/*---------------------------------------------------------- This Source Code Form is subject to the terms of the Mozilla Public License, v.2.0. If a copy of the MPL was not distributed with this file, You can obtain one @@ -6,6 +6,7 @@ This Source Code Form is subject to the terms of the ----------------------------------------------------------*/ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -27,7 +28,7 @@ namespace OneScript.StandardLibrary.Tasks public class BackgroundTasksManager : AutoContext, IDisposable { private readonly ExecutionContext _runtimeContext; - private List _tasks = new List(); + private readonly ConcurrentDictionary _tasks = new ConcurrentDictionary(); public BackgroundTasksManager(ExecutionContext runtimeContext) { @@ -46,7 +47,6 @@ public BackgroundTasksManager(ExecutionContext runtimeContext) public BackgroundTask Execute(IRuntimeContextInstance target, string methodName, ArrayImpl parameters = null, bool longRunning = false) { var task = new BackgroundTask(target, methodName, parameters); - _tasks.Add(task); var taskCreationOptions = longRunning ? TaskCreationOptions.LongRunning : TaskCreationOptions.None; var worker = new Task(() => @@ -57,6 +57,7 @@ public BackgroundTask Execute(IRuntimeContextInstance target, string methodName, }, taskCreationOptions); task.WorkerTask = worker; + _tasks.TryAdd(task.TaskId, task); worker.Start(); return task; @@ -112,22 +113,22 @@ public int WaitAny(ArrayImpl tasks, int timeout = 0) [ContextMethod("ОжидатьЗавершенияЗадач", "WaitCompletionOfTasks")] public void WaitCompletionOfTasks() { - lock (_tasks) + var snapshot = _tasks.Values.ToArray(); + Task.WaitAll(GetWorkerTasks(snapshot)); + + var failedTasks = snapshot.Where(x => x.State == TaskStateEnum.CompletedWithErrors) + .ToList(); + + if (failedTasks.Count != 0) { - var workers = GetWorkerTasks(); - Task.WaitAll(workers); + throw new ParametrizedRuntimeException( + Locale.NStr("ru = 'Задания завершились с ошибками';en = 'Tasks are completed with errors'"), + new ArrayImpl(failedTasks)); + } - var failedTasks = _tasks.Where(x => x.State == TaskStateEnum.CompletedWithErrors) - .ToList(); - - if (failedTasks.Count != 0) - { - throw new ParametrizedRuntimeException( - Locale.NStr("ru = 'Задания завершились с ошибками';en = 'Tasks are completed with errors'"), - new ArrayImpl(failedTasks)); - } - - _tasks.Clear(); + foreach (var task in snapshot) + { + _tasks.TryRemove(task.TaskId, out _); } } @@ -135,10 +136,10 @@ public void WaitCompletionOfTasks() public ArrayImpl GetBackgroundJobs(StructureImpl filter = default) { if(filter == default) - return new ArrayImpl(_tasks); + return new ArrayImpl(_tasks.Values); var arr = new ArrayImpl(); - foreach (var task in _tasks) + foreach (var task in _tasks.Values) { var result = true; foreach (var filterItem in filter) @@ -185,8 +186,10 @@ public IValue GetCurrent() if (currentId == null) return ValueFactory.Create(); - var task = _tasks.FirstOrDefault(x => x.TaskId == (int) currentId && x.State == TaskStateEnum.Running); - return task ?? ValueFactory.Create(); + if (_tasks.TryGetValue(currentId.Value, out var task) && task.State == TaskStateEnum.Running) + return task; + + return ValueFactory.Create(); } internal static int ConvertTimeout(int timeout) @@ -212,7 +215,7 @@ private static Task[] GetWorkerTasks(IEnumerable tasks) private Task[] GetWorkerTasks() { - return GetWorkerTasks(_tasks); + return GetWorkerTasks(_tasks.Values); } public void Dispose() diff --git a/src/OneScript.StandardLibrary/TypeDescriptions/TypeDescription.cs b/src/OneScript.StandardLibrary/TypeDescriptions/TypeDescription.cs index 9e91465bb..c14c2223c 100644 --- a/src/OneScript.StandardLibrary/TypeDescriptions/TypeDescription.cs +++ b/src/OneScript.StandardLibrary/TypeDescriptions/TypeDescription.cs @@ -5,7 +5,9 @@ This Source Code Form is subject to the terms of the at http://mozilla.org/MPL/2.0/. ----------------------------------------------------------*/ +using System; using System.Collections.Generic; +using System.Text; using OneScript.Contexts; using OneScript.Exceptions; using OneScript.StandardLibrary.Collections; @@ -329,10 +331,13 @@ private static void CheckAndAddOneQualifier(TypeDescriptionBuilder builder, IVal default: throw RuntimeException.InvalidNthArgumentType(nParam); } - } - } - - internal class UndefinedTypeAdjuster : IValueAdjuster + } + + public override string ToString() => string.Join(", ", _types); + } + + + internal class UndefinedTypeAdjuster : IValueAdjuster { public IValue Adjust(IValue value) { diff --git a/src/OneScript.StandardLibrary/Xml/XmlGlobalFunctions.cs b/src/OneScript.StandardLibrary/Xml/XmlGlobalFunctions.cs index 97533cd58..4a6ee7cb6 100644 --- a/src/OneScript.StandardLibrary/Xml/XmlGlobalFunctions.cs +++ b/src/OneScript.StandardLibrary/Xml/XmlGlobalFunctions.cs @@ -8,6 +8,7 @@ This Source Code Form is subject to the terms of the using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.Xml; using OneScript.Contexts; using OneScript.Exceptions; @@ -65,7 +66,10 @@ public string XMLString(BslValue value) else if(value.SystemType == BasicTypes.Boolean) return XmlConvert.ToString(value.AsBoolean()); else if(value.SystemType == BasicTypes.Date) - return XmlConvert.ToString(value.AsDate(), XmlDateTimeSerializationMode.Unspecified); + { + var date = value.AsDate(); + return date.ToString("yyyy-MM-dd'T'HH:mm:ss", CultureInfo.InvariantCulture); + } else if(value.SystemType == BasicTypes.Number) return XmlConvert.ToString(value.AsNumber()); else diff --git a/src/OneScript.StandardLibrary/Xml/XmlWriterImpl.cs b/src/OneScript.StandardLibrary/Xml/XmlWriterImpl.cs index 466cd7b16..e553591fc 100644 --- a/src/OneScript.StandardLibrary/Xml/XmlWriterImpl.cs +++ b/src/OneScript.StandardLibrary/Xml/XmlWriterImpl.cs @@ -25,9 +25,9 @@ public class XmlWriterImpl : AutoContext, IDisposable { private TextWriterWithSettings _internalTextWriter; private XmlTextWriter _writer; - private XmlWriterSettingsImpl _settings = (XmlWriterSettingsImpl)XmlWriterSettingsImpl.Constructor(); + private XmlWriterSettingsImpl _settings = XmlWriterSettingsImpl.Constructor(); private int _depth; - private Stack> _nsmap = new Stack>(); + private readonly Stack> _nsmap = new(); private StringBuilder _stringBuffer; private const string DEFAULT_INDENT_STRING = " "; @@ -51,6 +51,12 @@ private void ExitScope() --_depth; } + private void CheckIfOpen() + { + if (_writer == null) + throw NotOpenException(); + } + #region Properties [ContextProperty("Отступ","Indent")] @@ -77,8 +83,9 @@ public XmlWriterSettingsImpl Settings [ContextMethod("ЗаписатьАтрибут","WriteAttribute")] public void WriteAttribute(string localName, string valueOrNamespace, string value = null) - { - if(value == null) + { + CheckIfOpen(); + if (value == null) { _writer.WriteAttributeString(localName, valueOrNamespace); } @@ -91,30 +98,35 @@ public void WriteAttribute(string localName, string valueOrNamespace, string val [ContextMethod("ЗаписатьБезОбработки","WriteRaw")] public void WriteRaw(string data) { + CheckIfOpen(); _writer.WriteRaw(data); } [ContextMethod("ЗаписатьИнструкциюОбработки","WriteProcessingInstruction")] public void WriteProcessingInstruction(string name, string text) { + CheckIfOpen(); _writer.WriteProcessingInstruction(name, text); } [ContextMethod("ЗаписатьКомментарий","WriteComment")] public void WriteComment(string text) { + CheckIfOpen(); _writer.WriteComment(text); } [ContextMethod("ЗаписатьКонецАтрибута","WriteEndAttribute")] public void WriteEndAttribute() { + CheckIfOpen(); _writer.WriteEndAttribute(); } [ContextMethod("ЗаписатьКонецЭлемента","WriteEndElement")] public void WriteEndElement() { + CheckIfOpen(); _internalTextWriter.TrimEndSlashes = true; _writer.WriteEndElement(); _internalTextWriter.TrimEndSlashes = false; @@ -124,7 +136,8 @@ public void WriteEndElement() [ContextMethod("ЗаписатьНачалоАтрибута","WriteStartAttribute")] public void WriteStartAttribute(string name, string ns = null) { - if(ns == null) + CheckIfOpen(); + if (ns == null) { _writer.WriteStartAttribute(name); } @@ -138,6 +151,7 @@ public void WriteStartAttribute(string name, string ns = null) [ContextMethod("ЗаписатьНачалоЭлемента","WriteStartElement")] public void WriteStartElement(string name, string ns = null) { + CheckIfOpen(); if (ns == null) { _writer.WriteStartElement(name); @@ -152,18 +166,21 @@ public void WriteStartElement(string name, string ns = null) [ContextMethod("ЗаписатьОбъявлениеXML","WriteXMLDeclaration")] public void WriteXMLDeclaration() { + CheckIfOpen(); _writer.WriteStartDocument(); } [ContextMethod("ЗаписатьСекциюCDATA","WriteCDATASection")] public void WriteCDATASection(string data) { + CheckIfOpen(); _writer.WriteCData(data); } [ContextMethod("ЗаписатьСоответствиеПространстваИмен","WriteNamespaceMapping")] public void WriteNamespaceMapping(string prefix, string uri) { + CheckIfOpen(); _writer.WriteAttributeString("xmlns", prefix, null, uri); _nsmap.Peek()[prefix] = uri; } @@ -171,18 +188,21 @@ public void WriteNamespaceMapping(string prefix, string uri) [ContextMethod("ЗаписатьСсылкуНаСущность","WriteEntityReference")] public void WriteEntityReference(string name) { + CheckIfOpen(); _writer.WriteEntityRef(name); } [ContextMethod("ЗаписатьТекст","WriteText")] public void WriteText(string text) { + CheckIfOpen(); _writer.WriteString(text); } [ContextMethod("ЗаписатьТекущий","WriteCurrent")] public void WriteCurrent(XmlReaderImpl reader) { + CheckIfOpen(); var nodeType = reader.NodeType.UnderlyingValue; switch (nodeType) { @@ -225,7 +245,8 @@ public void WriteCurrent(XmlReaderImpl reader) case XmlNodeType.Document: case XmlNodeType.DocumentFragment: case XmlNodeType.Notation: - throw new RuntimeException(new Localization.BilingualString($"Копирование узла {nodeType} не поддерживается")); + throw CopyingNotSupportedException(nodeType); + default: break; } @@ -292,23 +313,58 @@ private static (string prefix, string localName) splitName(string nameOrLocalNam [ContextMethod("ЗаписатьТипДокумента","WriteDocumentType")] public void WriteDocumentType(string name, string varArg2, string varArg3 = null, string varArg4 = null) { - if(varArg4 != null) - { - _writer.WriteDocType(name, varArg2, varArg3, varArg4); + CheckIfOpen(); + + if (string.IsNullOrWhiteSpace(name)) + throw RuntimeException.InvalidNthArgumentValue(1); + + try + { + if (varArg4 != null) + { + if (string.IsNullOrEmpty(varArg2)) + { + varArg2 = null; + if (string.Empty.Equals(varArg3)) + varArg3 = null; + } + else if (string.IsNullOrEmpty(varArg3)) // (A1,A2,,A4) + throw NoSystemIdException(); + + if (string.Empty.Equals(varArg4)) + varArg4 = null; + + _writer.WriteDocType(name, varArg2, varArg3, varArg4); + } + else if (varArg3 != null) + { + if (string.Empty.Equals(varArg2)) + varArg2 = null; + + if (string.Empty.Equals(varArg3)) + varArg3 = null; + + _writer.WriteDocType(name, null, varArg2, varArg3); + } + else + { + _writer.WriteDocType(name, null, null, varArg2); + } } - else if(varArg3 != null) - { - _writer.WriteDocType(name, null, varArg2, varArg3); + catch (ArgumentException) + { + throw RuntimeException.InvalidNthArgumentValue(1); } - else - { - _writer.WriteDocType(name, null, null, varArg2); + catch (InvalidOperationException) + { + throw WrongOrderException(); } } [ContextMethod("НайтиПрефикс","LookupPrefix")] public IValue LookupPrefix(string uri) { + CheckIfOpen(); string prefix = _writer.LookupPrefix(uri); if (prefix == null) return ValueFactory.Create(); @@ -318,26 +374,24 @@ public IValue LookupPrefix(string uri) [ContextMethod("Закрыть","Close")] public IValue Close() { - if(IsOpenForString()) + if (_writer==null) + return ValueFactory.Create(String.Empty); + + _writer.Flush(); + _writer.Close(); + Dispose(); + + if (IsOpenForString()) { - _writer.Flush(); - _writer.Close(); - - Dispose(); - var result = _stringBuffer.ToString(); _stringBuffer = null; + return ValueFactory.Create(result); } else { - _writer.Flush(); - _writer.Close(); - Dispose(); - - return ValueFactory.Create(); + return ValueFactory.Create(String.Empty); } - } private void ApplySettings(BslValue encodingOrSettings) @@ -477,9 +531,7 @@ protected override void Dispose(bool disposing) public void Dispose() { - if (_writer != null) - _writer.Close(); - + _writer?.Close(); _writer = null; } @@ -490,5 +542,33 @@ public static XmlWriterImpl Create() } public XmlWriter GetNativeWriter() => _writer; + + public static RuntimeException NotOpenException() + { + return new RuntimeException + ("Приемник данных XML не открыт", + "XML data target is not opened"); + } + public static RuntimeException CopyingNotSupportedException(XmlNodeType nodeType) + { + return new RuntimeException + ($"Копирование узла типа {nodeType} не поддерживается", + $"Copying a node of type {nodeType} is not supported"); + } + + public static RuntimeException WrongOrderException() + { + return new RuntimeException + ("Ошибочный порядок записи XML", + "Wrong order of XML record"); + } + + public static RuntimeException NoSystemIdException() + { + return new RuntimeException + ("Отсутствует системный идентификатор", + "System ID missing"); + } + } } diff --git a/src/ScriptEngine.HostedScript/DefaultEventProcessor.cs b/src/ScriptEngine.HostedScript/DefaultEventProcessor.cs index 24549e84b..6c283db1e 100644 --- a/src/ScriptEngine.HostedScript/DefaultEventProcessor.cs +++ b/src/ScriptEngine.HostedScript/DefaultEventProcessor.cs @@ -114,6 +114,14 @@ public void RemoveHandler( } } + public void RemoveAllHandlers(IRuntimeContextInstance eventSource) + { + lock (_subscriptionLock) + { + _registeredHandlers.Remove(eventSource); + } + } + public void HandleEvent(IRuntimeContextInstance eventSource, string eventName, IValue[] eventArgs, IBslProcess process) { diff --git a/src/ScriptEngine.HostedScript/Extensions/EngineBuilderExtensions.cs b/src/ScriptEngine.HostedScript/Extensions/EngineBuilderExtensions.cs index 25c7c89b9..8dc92ecbb 100644 --- a/src/ScriptEngine.HostedScript/Extensions/EngineBuilderExtensions.cs +++ b/src/ScriptEngine.HostedScript/Extensions/EngineBuilderExtensions.cs @@ -69,7 +69,8 @@ public static ConfigurationProviders UseEnvironmentVariableConfig(this Configura public static IEngineBuilder UseDefaultHosting(this IEngineBuilder b) { return b.UseFileSystemLibraries() - .UseBinaryDataOptions(); + .UseBinaryDataOptions() + .UseNativeApi(); } public static IEngineBuilder UseFileSystemLibraries(this IEngineBuilder b) diff --git a/src/ScriptEngine.NativeApi/NativeApiProxy.cpp b/src/ScriptEngine.NativeApi/NativeApiProxy.cpp index 0a60a4101..844a3a727 100644 --- a/src/ScriptEngine.NativeApi/NativeApiProxy.cpp +++ b/src/ScriptEngine.NativeApi/NativeApiProxy.cpp @@ -10,6 +10,7 @@ at http://mozilla.org/MPL/2.0/. #include "include/AddInDefBase.h" #include "include/IMemoryManager.h" #include "NativeInterface.h" +#include #ifdef _WINDOWS @@ -46,7 +47,7 @@ typedef void(_stdcall* VariantFuncRespond) (const tVariant* variant); static bool AllocMemory(void** pMemory, unsigned long ulCountByte) { #ifdef _WINDOWS - return *pMemory = LocalAlloc(LMEM_FIXED, ulCountByte); + return *pMemory = LocalAlloc(LMEM_FIXED | LMEM_ZEROINIT, ulCountByte); #else return *pMemory = calloc(1, ulCountByte); #endif//_WINDOWS @@ -64,15 +65,18 @@ void ADDIN_API FreeMemory(void** pMemory) { class ProxyComponent : public IMemoryManager { private: + HMODULE hModule = nullptr; IComponentBase* pComponent = nullptr; NativeInterface mInterface; public: ProxyComponent( + HMODULE hModule, IComponentBase* pComponent, ErrorFuncRespond onError, EventFuncRespond onEvent, StatusFuncRespond onStatus ) : + hModule(hModule), pComponent(pComponent), mInterface(onError, onEvent, onStatus) { @@ -81,7 +85,12 @@ class ProxyComponent : public IMemoryManager { } virtual ~ProxyComponent() override { pComponent->Done(); - delete pComponent; + auto proc = (DestroyObjectPtr)GetProcAddress(hModule, "DestroyObject"); + if (proc) + proc(&pComponent); + else + delete pComponent; + pComponent = nullptr; } virtual bool ADDIN_API AllocMemory(void** pMemory, unsigned long ulCountByte) override { return ::AllocMemory(pMemory, ulCountByte); @@ -106,6 +115,14 @@ static void ClearVariant(tVariant& variant) FreeMemory((void**)&variant.pwstrVal); variant.wstrLen = 0; break; + case VTYPE_VARIANT: + if (variant.pvarVal) { + for (uint32_t i = 0; i < variant.cbElements; i++) + ClearVariant(variant.pvarVal[i]); + FreeMemory((void**)&variant.pvarVal); + variant.cbElements = 0; + } + break; } variant.vt = VTYPE_EMPTY; } @@ -114,14 +131,16 @@ DllExport tVariant* CreateVariant(int32_t lSizeArray) { if (lSizeArray <= 0) return nullptr; void* ptr = nullptr; - ::AllocMemory(&ptr, sizeof(tVariant) * lSizeArray); + if (!::AllocMemory(&ptr, sizeof(tVariant) * lSizeArray)) + return nullptr; return (tVariant*)ptr; } -DllExport void FreeVariant(tVariant* variant) +DllExport void FreeVariant(tVariant* variant, int32_t count) { if (variant == nullptr) return; - ::ClearVariant(*variant); + for (int32_t i = 0; i < count; i++) + ::ClearVariant(variant[i]); ::FreeMemory((void**)&variant); } @@ -138,7 +157,7 @@ DllExport ProxyComponent* GetClassObject( IComponentBase* pComponent = nullptr; auto ok = proc(wsName, &pComponent); if (ok == 0) return nullptr; - return new ProxyComponent(pComponent, onError, onEvent, onStatus); + return new ProxyComponent(hModule, pComponent, onError, onEvent, onStatus); } DllExport void DestroyObject(ProxyComponent* proxy) @@ -146,6 +165,15 @@ DllExport void DestroyObject(ProxyComponent* proxy) if (proxy) delete proxy; } +DllExport void GetExtensionName(ProxyComponent* proxy, StringFuncRespond respond) +{ + CHECK_PROXY(EMPTY_DEF); + WCHAR_T* name = nullptr; + auto ok = proxy->Component().RegisterExtensionAs(&name); + if (ok && name) respond(name); + if (name) proxy->FreeMemory((void**)&name); +} + DllExport int32_t GetNProps(ProxyComponent* proxy) { CHECK_PROXY(0); @@ -188,12 +216,13 @@ DllExport bool SetPropVal(ProxyComponent* proxy, int32_t lPropNum, tVariant* var DllExport void SetVariantEmpty(tVariant* variant, int32_t number) { tVariant* v = variant + number; - TV_VT(v) = VTYPE_EMPTY; + ClearVariant(*v); } DllExport void SetVariantBool(tVariant* variant, int32_t number, bool value) { tVariant* v = variant + number; + ClearVariant(*v); TV_BOOL(v) = value; TV_VT(v) = VTYPE_BOOL; } @@ -201,6 +230,7 @@ DllExport void SetVariantBool(tVariant* variant, int32_t number, bool value) DllExport void SetVariantReal(tVariant* variant, int32_t number, double value) { tVariant* v = variant + number; + ClearVariant(*v); TV_R8(v) = value; TV_VT(v) = VTYPE_R8; } @@ -208,6 +238,7 @@ DllExport void SetVariantReal(tVariant* variant, int32_t number, double value) DllExport void SetVariantInt(tVariant* variant, int32_t number, int32_t value) { tVariant* v = variant + number; + ClearVariant(*v); TV_I4(v) = value; TV_VT(v) = VTYPE_I4; } @@ -215,6 +246,7 @@ DllExport void SetVariantInt(tVariant* variant, int32_t number, int32_t value) DllExport void SetVariantStr(tVariant* variant, int32_t number, const WCHAR_T* value, int32_t length) { tVariant* v = variant + number; + ClearVariant(*v); unsigned long size = sizeof(WCHAR_T) * (length + 1); if (::AllocMemory((void**)&v->pwstrVal, size)) { memcpy(v->pwstrVal, value, size); @@ -227,6 +259,7 @@ DllExport void SetVariantStr(tVariant* variant, int32_t number, const WCHAR_T* v DllExport void SetVariantBlob(tVariant* variant, int32_t number, const char* value, int32_t length) { tVariant* v = variant + number; + ClearVariant(*v); if (::AllocMemory((void**)&v->pstrVal, length)) { memcpy(v->pstrVal, value, length); v->strLen = length; @@ -234,10 +267,35 @@ DllExport void SetVariantBlob(tVariant* variant, int32_t number, const char* val } } +DllExport void SetVariantDate(tVariant* variant, int32_t number, double value) +{ + tVariant* v = variant + number; + ClearVariant(*v); + TV_DATE(v) = value; + TV_VT(v) = VTYPE_DATE; +} + +static double TmToOleDate(const struct tm& tmVal) +{ + struct tm t = tmVal; + time_t time = mktime(&t); + if (time == (time_t)-1) + return 0.0; + + struct tm epoch = {}; + epoch.tm_year = 1899 - 1900; + epoch.tm_mon = 11; + epoch.tm_mday = 30; + time_t epochTime = mktime(&epoch); + + return difftime(time, epochTime) / 86400.0; +} + typedef void(_stdcall* TSetVariantEmpty)(tVariant*, int32_t); typedef void(_stdcall* TSetVariantBool)(tVariant*, int32_t, bool); typedef void(_stdcall* TSetVariantReal)(tVariant*, int32_t, double); typedef void(_stdcall* TSetVariantInt)(tVariant*, int32_t, int32_t); +typedef void(_stdcall* TSetVariantDate)(tVariant*, int32_t, double); typedef void(_stdcall* TSetVariantBlob)(tVariant*, int32_t, void*, int32_t); DllExport void GetVariant(tVariant* variant, int32_t number @@ -245,12 +303,14 @@ DllExport void GetVariant(tVariant* variant, int32_t number , TSetVariantBool b , TSetVariantInt i , TSetVariantReal r + , TSetVariantDate d , TSetVariantBlob s , TSetVariantBlob x ) { if (variant == nullptr) return; - switch (variant->vt) { + tVariant* v = variant + number; + switch (v->vt) { case VTYPE_EMPTY: e(variant, number); break; @@ -258,27 +318,29 @@ DllExport void GetVariant(tVariant* variant, int32_t number case VTYPE_I4: case VTYPE_ERROR: case VTYPE_UI1: - i(variant, number, variant->lVal); + i(variant, number, v->lVal); break; case VTYPE_BOOL: - b(variant, number, variant->bVal); + b(variant, number, v->bVal); break; case VTYPE_R4: case VTYPE_R8: - r(variant, number, variant->dblVal); + r(variant, number, v->dblVal); break; case VTYPE_DATE: + d(variant, number, v->dblVal); + break; case VTYPE_TM: - e(variant, number); + d(variant, number, TmToOleDate(v->tmVal)); break; case VTYPE_PSTR: - e(variant, number); + x(variant, number, v->pstrVal, v->strLen); break; case VTYPE_PWSTR: - s(variant, number, variant->pwstrVal, variant->strLen); + s(variant, number, v->pwstrVal, v->strLen); break; case VTYPE_BLOB: - x(variant, number, variant->pstrVal, variant->strLen); + x(variant, number, v->pstrVal, v->strLen); break; default: e(variant, number); diff --git a/src/ScriptEngine/BslProcess.cs b/src/ScriptEngine/BslProcess.cs index 65b8c9de2..4d7deeb06 100644 --- a/src/ScriptEngine/BslProcess.cs +++ b/src/ScriptEngine/BslProcess.cs @@ -18,10 +18,13 @@ namespace ScriptEngine { internal class BslProcess : IBslProcess { + private static readonly string[] TerminationEventNames = { "ПриЗавершении", "OnTermination" }; + private readonly IExecutorProvider[] _executorProviders; private readonly IDictionary _bslExecutorsByModule; private bool _isRunning; + private bool _disposed; public BslProcess(int id, ExecutionContext context, IEnumerable executorProviders) { @@ -55,10 +58,51 @@ public BslValue Run(BslObjectValue target, IExecutableModule module, BslScriptMe { if (notifyExecutors) { + RaiseTerminationEvent(); + if (BslWrapper is IDisposable disposable) + { + disposable.Dispose(); + } + Array.ForEach(_executorProviders, e => e.AfterProcessExit(this)); + Services.Dispose(); _isRunning = false; } } } + + public IRuntimeContextInstance BslWrapper { get; set; } + + private void RaiseTerminationEvent() + { + if (BslWrapper == null) + return; + + var eventProcessor = Services.TryResolve(); + if (eventProcessor == null) + return; + + try + { + foreach (var eventName in TerminationEventNames) + { + try + { + eventProcessor.HandleEvent(BslWrapper, eventName, Array.Empty(), this); + } + catch (Exception exception) + { + SystemLogger.Write( + $"WARNING! Error in execution thread termination handler '{eventName}': {exception.Message}"); + } + } + } + finally + { + // Процессор событий держит источник, пока подписки не сняты. Потоков исполнения + // много и живут они недолго, поэтому без явного снятия реестр рос бы бесконечно. + eventProcessor.RemoveAllHandlers(BslWrapper); + } + } } } \ No newline at end of file diff --git a/src/ScriptEngine/Hosting/DefaultEngineBuilder.cs b/src/ScriptEngine/Hosting/DefaultEngineBuilder.cs index 948b83b44..e6c5120d1 100644 --- a/src/ScriptEngine/Hosting/DefaultEngineBuilder.cs +++ b/src/ScriptEngine/Hosting/DefaultEngineBuilder.cs @@ -44,8 +44,13 @@ public virtual ScriptingEngine Build() return engine; } + protected virtual void RegisterEngineServices(IServiceDefinitions services) + { + } + protected virtual IServiceContainer GetContainer() { + RegisterEngineServices(Services); return Services.CreateContainer(); } } diff --git a/src/ScriptEngine/IEngineLifetime.cs b/src/ScriptEngine/IEngineLifetime.cs new file mode 100644 index 000000000..cd0809085 --- /dev/null +++ b/src/ScriptEngine/IEngineLifetime.cs @@ -0,0 +1,18 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using System; + +namespace ScriptEngine +{ + /// + /// Служба с жизненным циклом, привязанным к экземпляру . + /// + public interface IEngineLifetime : IDisposable + { + } +} diff --git a/src/ScriptEngine/Machine/Contexts/COMWrapperContext.cs b/src/ScriptEngine/Machine/Contexts/COMWrapperContext.cs index f609daf36..8c1da2747 100644 --- a/src/ScriptEngine/Machine/Contexts/COMWrapperContext.cs +++ b/src/ScriptEngine/Machine/Contexts/COMWrapperContext.cs @@ -39,7 +39,7 @@ protected COMWrapperContext(object instance) private static Type FindTypeByName(string typeName) { - foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies().Reverse()) + foreach (var assembly in Enumerable.Reverse(AppDomain.CurrentDomain.GetAssemblies())) { var tt = assembly.GetType(typeName, throwOnError:false, ignoreCase:true); if (tt != null) diff --git a/src/ScriptEngine/Machine/IEventProcessor.cs b/src/ScriptEngine/Machine/IEventProcessor.cs index 9c667fd79..ab936356f 100644 --- a/src/ScriptEngine/Machine/IEventProcessor.cs +++ b/src/ScriptEngine/Machine/IEventProcessor.cs @@ -25,5 +25,17 @@ void RemoveHandler( string handlerMethod); void HandleEvent(IRuntimeContextInstance eventSource, string eventName, IValue[] eventArgs, IBslProcess process); + + /// + /// Снимает все подписки на события указанного источника. + /// + /// Нужен источникам, которые живут меньше самого процессора событий: без этого реестр + /// подписок удерживает такой источник до конца работы движка. Реализация по умолчанию + /// ничего не делает, чтобы не ломать сторонние процессоры событий. + /// + /// Источник, подписки на который нужно снять. + void RemoveAllHandlers(IRuntimeContextInstance eventSource) + { + } } } \ No newline at end of file diff --git a/src/ScriptEngine/ScriptingEngine.cs b/src/ScriptEngine/ScriptingEngine.cs index 5b8340900..7e97f1061 100644 --- a/src/ScriptEngine/ScriptingEngine.cs +++ b/src/ScriptEngine/ScriptingEngine.cs @@ -22,6 +22,7 @@ public class ScriptingEngine : IDisposable { private AttachedScriptsFactory _attachedScriptsFactory; private IRuntimeEnvironment _runtimeEnvironment; + private bool _disposed; private readonly ILibraryManager _libraryManager; @@ -189,7 +190,17 @@ private void EnableCodeStatistics() public void Dispose() { + if (_disposed) + return; + + _disposed = true; + AttachedScriptsFactory.SetInstance(null); + + foreach (var service in Services.ResolveEnumerable()) + { + service.Dispose(); + } } #endregion diff --git a/src/Tests/DocumenterTests/XmlDocConversionTest.cs b/src/Tests/DocumenterTests/XmlDocConversionTest.cs index 19534c9c0..7f5b5274b 100644 --- a/src/Tests/DocumenterTests/XmlDocConversionTest.cs +++ b/src/Tests/DocumenterTests/XmlDocConversionTest.cs @@ -31,7 +31,7 @@ public void TestConversionOfTextBlock() var expected = @"Для Каждого Переменная Из ПеременныеСреды() Цикл Сообщить(Переменная.Ключ + "" = "" + Переменная.Значение); -КонецЦикла;"; +КонецЦикла;".ReplaceLineEndings("\n"); Assert.Equal(expected, text); } diff --git a/src/Tests/OneScript.Core.Tests/ArgumentParserTests.cs b/src/Tests/OneScript.Core.Tests/ArgumentParserTests.cs index 247713773..b8f36c5b5 100644 --- a/src/Tests/OneScript.Core.Tests/ArgumentParserTests.cs +++ b/src/Tests/OneScript.Core.Tests/ArgumentParserTests.cs @@ -20,6 +20,9 @@ public class ArgumentParserTests [InlineData("-c \"'oscript' -version\"", "-c", "'oscript' -version")] [InlineData("'aaa\"", "aaa\"")] [InlineData(" ")] + [InlineData("-c \"Сообщить(\"\"привет\"\")\"", "-c", "Сообщить(\"привет\")")] + [InlineData("-c 'it''s fine'", "-c", "it's fine")] + [InlineData("\"aaa\"\"\"", "aaa\"")] public void Should_Parse_Arguments(string input, params string[] expected) { var parser = new ArgumentsParser(input); diff --git a/src/Tests/OneScript.Core.Tests/BslProcessTests.cs b/src/Tests/OneScript.Core.Tests/BslProcessTests.cs new file mode 100644 index 000000000..eb022261b --- /dev/null +++ b/src/Tests/OneScript.Core.Tests/BslProcessTests.cs @@ -0,0 +1,73 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using System; +using FluentAssertions; +using ScriptEngine.Hosting; +using Xunit; + +namespace OneScript.Core.Tests +{ + /// + /// Регистрируется как scoped, поэтому живёт ровно столько же, сколько область сервисов процесса. + /// + public sealed class ScopedDisposableProbe : IDisposable + { + public bool IsDisposed { get; private set; } + + public void Dispose() + { + IsDisposed = true; + } + } + + public class BslProcessTests + { + private static ScriptEngine.ScriptingEngine CreateEngineWithScopedProbe() + { + var builder = DefaultEngineBuilder.Create().SetDefaultOptions(); + builder.Services.RegisterScoped(); + + var engine = builder.Build(); + engine.Initialize(); + + return engine; + } + + [Fact] + public void ProcessReleasesItsServiceScope() + { + var engine = CreateEngineWithScopedProbe(); + + var process = engine.NewProcess(); + var scoped = process.Services.Resolve(); + + scoped.IsDisposed.Should().BeFalse("процесс ещё работает"); + + engine.AttachedScriptsFactory.LoadFromString( + engine.GetCompilerService(), "f = 1", process); + + scoped.IsDisposed.Should().BeTrue("процесс владеет своей областью сервисов"); + } + + [Fact] + public void ProcessDoesNotReleaseScopeOfAnotherProcess() + { + var engine = CreateEngineWithScopedProbe(); + + var first = engine.NewProcess(); + var second = engine.NewProcess(); + + var firstScoped = first.Services.Resolve(); + var secondScoped = second.Services.Resolve(); + + firstScoped.Should().NotBeSameAs(secondScoped, "у каждого процесса своя область сервисов"); + + secondScoped.IsDisposed.Should().BeFalse("освобождение одного процесса не трогает другой"); + } + } +} diff --git a/src/Tests/OneScript.Core.Tests/DateValueCompatibilityTests.cs b/src/Tests/OneScript.Core.Tests/DateValueCompatibilityTests.cs new file mode 100644 index 000000000..5ddfe4bd0 --- /dev/null +++ b/src/Tests/OneScript.Core.Tests/DateValueCompatibilityTests.cs @@ -0,0 +1,351 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; +using Moq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OneScript.Commons; +using OneScript.Contexts; +using OneScript.Execution; +using OneScript.Native.Runtime; +using OneScript.StandardLibrary; +using OneScript.StandardLibrary.Collections; +using OneScript.StandardLibrary.Json; +using OneScript.StandardLibrary.Timezones; +using OneScript.StandardLibrary.Xml; +using OneScript.Values; +using ScriptEngine; +using ScriptEngine.Machine; +using ScriptEngine.Machine.Contexts; +using Xunit; + +namespace OneScript.Core.Tests +{ + public class DateValueCompatibilityTests + { + private static readonly BslDateValue Base = + (BslDateValue)ValueFactory.Create(new DateTime(2026, 2, 1, 15, 30, 45)); + + private const string JsonText = + "{\"a\":\"2026-02-01T15:30:45\"," + + "\"b\":\"2026-02-01T15:30:45.123\"," + + "\"c\":\"2026-02-01T15:30:45Z\"," + + "\"d\":\"2026-02-01T15:30:45+03:00\"," + + "\"e\":\"2026-02-01T15:30:45.1234567Z\"}"; + + private static DateTime DropFraction(DateTime dt) => + new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second); + + private static IValue GetStructureProperty(StructureImpl structure, string name) => + structure.GetPropValue(structure.GetPropertyNumber(name)); + + public static IEnumerable DateAdditionRoundingCases => new[] + { + new object[] { 0.00005m, 0.0001m }, + new object[] { 0.00006m, 0.0001m }, + new object[] { 0.00015m, 0.0002m }, + new object[] { 0.00016m, 0.0002m }, + new object[] { 0.00025m, 0.0003m }, + new object[] { -0.00005m, -0.0001m }, + new object[] { -0.00015m, -0.0002m }, + }; + + public static IEnumerable FractionalSubtractionCases => new[] + { + new object[] { 0.5m, 0.5m }, + new object[] { 1.5m, 1.5m }, + new object[] { 0.25m, 0.25m }, + new object[] { 2m, 2m }, + }; + + private static JObject ParseJson(string json) + { + using (var reader = new JsonTextReader(new StringReader(json)) + { + DateParseHandling = DateParseHandling.None + }) + { + return JObject.Load(reader); + } + } + + private static string WriteStructureToJson(StructureImpl structure) + { + var writer = new JSONWriter(); + writer.SetString(); + new GlobalJsonFunctions().WriteJSON(ForbiddenBslProcess.Instance, writer, structure); + return writer.Close(); + } + + [Theory] + [InlineData(0.001)] + [InlineData(0.1)] + [InlineData(0.5)] + [InlineData(0.999)] + [InlineData(0.0001)] + public void Date_Addition_Above_1C_Precision_Changes_Value(double delta) + { + var result = ValueFactory.Add(Base, ValueFactory.Create((decimal)delta), ForbiddenBslProcess.Instance); + + Assert.False(Base.Equals(result)); + } + + [Theory] + [InlineData(0.00001)] + [InlineData(0.0000001)] + [InlineData(0.00004)] + public void Date_Addition_Below_Half_Of_1C_Precision_Is_Ignored(double delta) + { + var result = ValueFactory.Add(Base, ValueFactory.Create((decimal)delta), ForbiddenBslProcess.Instance); + + Assert.True(Base.Equals(result)); + } + + [Theory] + [MemberData(nameof(DateAdditionRoundingCases))] + public void Date_Addition_Is_Rounded_To_1C_Precision(decimal addition, decimal expectedShift) + { + var result = ValueFactory.Add(Base, ValueFactory.Create(addition), ForbiddenBslProcess.Instance); + + Assert.Equal(expectedShift, ValueFactory.Sub(result, Base).AsNumber()); + + var expectedDate = ValueFactory.Add(Base, ValueFactory.Create(expectedShift), ForbiddenBslProcess.Instance); + Assert.True(result.Equals(expectedDate)); + } + + [Fact] + public void Date_Fractional_Addition_Is_Not_Rounded_To_Whole_Second() + { + var plus06 = ValueFactory.Add(Base, ValueFactory.Create(0.6m), ForbiddenBslProcess.Instance); + var plus1 = ValueFactory.Add(Base, ValueFactory.Create(1m), ForbiddenBslProcess.Instance); + var plus04 = ValueFactory.Add(Base, ValueFactory.Create(0.4m), ForbiddenBslProcess.Instance); + var plus0004 = ValueFactory.Add(Base, ValueFactory.Create(0.0004m), ForbiddenBslProcess.Instance); + var plus0006 = ValueFactory.Add(Base, ValueFactory.Create(0.0006m), ForbiddenBslProcess.Instance); + + Assert.False(Base.Equals(plus06)); + Assert.False(plus1.Equals(plus06)); + Assert.False(Base.Equals(plus04)); + Assert.False(Base.Equals(plus0004)); + Assert.False(Base.Equals(plus0006)); + } + + [Theory] + [MemberData(nameof(FractionalSubtractionCases))] + public void Date_Subtraction_Returns_Fractional_Seconds(decimal delta, decimal expectedDiff) + { + var shifted = ValueFactory.Add(Base, ValueFactory.Create(delta), ForbiddenBslProcess.Instance); + var diff = ValueFactory.Sub(shifted, Base).AsNumber(); + + Assert.Equal(expectedDiff, diff); + } + + [Fact] + public void Date_Subtraction_Result_Is_Not_Integer() + { + var diff = ValueFactory.Sub( + ValueFactory.Add(Base, ValueFactory.Create(0.5m), ForbiddenBslProcess.Instance), + Base).AsNumber(); + + Assert.NotEqual(decimal.Truncate(diff), diff); + } + + [Fact] + public void Date_Comparison_Distinguishes_Fractions() + { + var fractional = ValueFactory.Add(Base, ValueFactory.Create(0.001m), ForbiddenBslProcess.Instance); + + Assert.True(fractional.CompareTo(Base) > 0); + Assert.True(fractional.CompareTo(Base) >= 0); + Assert.False(fractional.CompareTo(Base) < 0); + Assert.False(Base.Equals(fractional)); + Assert.NotEqual(fractional, Base); + } + + [Fact] + public void CurrentDate_Has_No_Fractional_Seconds() + { + var current = BuiltInFunctions.CurrentDate(); + + Assert.Equal(DropFraction(current), current); + } + + [Fact] + public void CurrentUniversalDate_Has_No_Fractional_Seconds() + { + var universal = (DateTime)(BslDateValue)new StandardGlobalContext().CurrentUniversalDate(); + + Assert.Equal(DropFraction(universal), universal); + } + + [Fact] + public void ToLocalTime_Of_Whole_Second_Date_Has_No_Fractional_Seconds() + { + var utc = DropFraction(DateTime.UtcNow); + var local = TimeZoneConverter.ToLocalTime(utc); + + Assert.Equal(DropFraction(local), local); + } + + [Fact] + public void Timezone_Roundtrip_Preserves_Fractional_Seconds() + { + var frac = (BslDateValue)ValueFactory.Add(Base, ValueFactory.Create(0.001m), ForbiddenBslProcess.Instance); + var utc = TimeZoneConverter.ToUniversalTime((DateTime)frac); + var back = TimeZoneConverter.ToLocalTime(utc); + + Assert.True(frac.Equals((BslValue)ValueFactory.Create(back))); + } + + [Fact] + public void Date_ToString_Hides_Fractional_Seconds() + { + var shifted = ValueFactory.Add(Base, ValueFactory.Create(0.123m), ForbiddenBslProcess.Instance); + + Assert.Equal(Base.ToString(), shifted.ToString()); + } + + [Fact] + public void Format_Ignores_Fractional_Seconds() + { + var shifted = ValueFactory.Add(Base, ValueFactory.Create(0.123m), ForbiddenBslProcess.Instance); + + Assert.Equal("45", ValueFormatter.Format((BslValue)shifted, "ДФ=сс")); + Assert.Equal("20260201153045", ValueFormatter.Format((BslValue)shifted, "ДФ=ггггММддЧЧммсс")); + } + + [Fact] + public void XmlString_Drops_Fractional_Seconds() + { + var shifted = ValueFactory.Add(Base, ValueFactory.Create(0.123m), ForbiddenBslProcess.Instance); + var mgr = new Mock(); + mgr.Setup(m => m.GetInstance(It.IsAny())).Returns((IAttachableContext)null); + var xml = (XmlGlobalFunctions)XmlGlobalFunctions.CreateInstance(mgr.Object); + + Assert.Equal("2026-02-01T15:30:45", xml.XMLString((BslValue)shifted)); + } + + [Fact] + public void Json_Write_Naive_Date_Uses_Iso_Seconds() + { + var structure = new StructureImpl(); + structure.Insert("Литерал", ValueFactory.Create(new DateTime(2026, 2, 1, 15, 30, 45))); + structure.Insert("ПустаяДата", ValueFactory.Create(DateTime.MinValue)); + + var json = WriteStructureToJson(structure); + var obj = ParseJson(json); + + Assert.Equal("2026-02-01T15:30:45", obj["Литерал"]?.ToString()); + Assert.Equal("0001-01-01T00:00:00", obj["ПустаяДата"]?.ToString()); + } + + [Fact] + public void Json_Write_Drops_Fractional_Seconds() + { + var structure = new StructureImpl(); + structure.Insert("Дробная", ValueFactory.Add(Base, ValueFactory.Create(0.123m), ForbiddenBslProcess.Instance)); + + var json = WriteStructureToJson(structure); + var obj = ParseJson(json); + + Assert.Equal("2026-02-01T15:30:45", obj["Дробная"]?.ToString()); + } + + [Fact] + public void Json_Write_Ignores_DateTime_Kind() + { + var structure = new StructureImpl(); + structure.Insert("Local", ValueFactory.Create(DropFraction(DateTime.Now))); + structure.Insert("Utc", ValueFactory.Create(DropFraction(DateTime.UtcNow))); + + var json = WriteStructureToJson(structure); + var obj = ParseJson(json); + var pattern = new Regex(@"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$"); + + var localValue = obj["Local"]?.ToString(); + var utcValue = obj["Utc"]?.ToString(); + + Assert.Matches(pattern, localValue); + Assert.Matches(pattern, utcValue); + Assert.DoesNotContain("Z", utcValue); + Assert.DoesNotContain("+", utcValue); + Assert.DoesNotContain("-", utcValue.Substring(10)); + } + + [Fact] + public void Json_Read_Without_Date_Property_Names_Keeps_Strings() + { + var reader = new JSONReader(); + reader.SetString(JsonText); + var data = (StructureImpl)new GlobalJsonFunctions().ReadJSON(reader); + + Assert.IsType(GetStructureProperty(data, "a")); + Assert.IsType(GetStructureProperty(data, "b")); + Assert.IsType(GetStructureProperty(data, "c")); + Assert.IsType(GetStructureProperty(data, "d")); + Assert.IsType(GetStructureProperty(data, "e")); + + Assert.Equal("2026-02-01T15:30:45", GetStructureProperty(data, "a").ToString()); + Assert.Equal("2026-02-01T15:30:45.123", GetStructureProperty(data, "b").ToString()); + Assert.Equal("2026-02-01T15:30:45Z", GetStructureProperty(data, "c").ToString()); + Assert.Equal("2026-02-01T15:30:45+03:00", GetStructureProperty(data, "d").ToString()); + Assert.Equal("2026-02-01T15:30:45.1234567Z", GetStructureProperty(data, "e").ToString()); + } + + [Fact] + public void Json_Read_With_Date_Property_Names_Parses_Iso() + { + var names = new ArrayImpl(); + names.Add(ValueFactory.Create("a")); + names.Add(ValueFactory.Create("b")); + names.Add(ValueFactory.Create("c")); + names.Add(ValueFactory.Create("d")); + names.Add(ValueFactory.Create("e")); + + var reader = new JSONReader(); + reader.SetString(JsonText); + var data = (StructureImpl)new GlobalJsonFunctions().ReadJSON( + reader, + ReadToMap: false, + PropertiesWithDateValuesNames: names, + ExpectedDateFormat: null); + + var expectedLocalFromUtc = TimeZoneInfo.ConvertTimeFromUtc( + new DateTime(2026, 2, 1, 15, 30, 45, DateTimeKind.Utc), + TimeZoneInfo.Local); + var expectedLocalFromUtcD = TimeZoneInfo.ConvertTimeFromUtc( + new DateTime(2026, 2, 1, 12, 30, 45, DateTimeKind.Utc), + TimeZoneInfo.Local); + + AssertDateWithoutFraction(GetStructureProperty(data, "a"), new DateTime(2026, 2, 1, 15, 30, 45)); + AssertDateWithoutFraction(GetStructureProperty(data, "b"), new DateTime(2026, 2, 1, 15, 30, 45)); + AssertDateWithoutFraction(GetStructureProperty(data, "c"), expectedLocalFromUtc); + AssertDateWithoutFraction(GetStructureProperty(data, "d"), expectedLocalFromUtcD); + AssertDateWithoutFraction(GetStructureProperty(data, "e"), expectedLocalFromUtc); + } + + [Fact] + public void ReadJsonDate_Drops_Fractional_Seconds() + { + var result = new GlobalJsonFunctions().ReadJSONDate("2026-02-01T15:30:45.123", JSONDateFormatEnum.ISO); + var date = (DateTime)(BslDateValue)result; + + Assert.Equal(new DateTime(2026, 2, 1, 15, 30, 45), date); + Assert.Equal(0, date.Ticks % TimeSpan.TicksPerSecond); + } + + private static void AssertDateWithoutFraction(IValue value, DateTime expected) + { + Assert.IsType(value); + var date = (DateTime)(BslDateValue)value; + Assert.Equal(expected, date); + Assert.Equal(0, date.Ticks % TimeSpan.TicksPerSecond); + } + } +} diff --git a/src/Tests/OneScript.Core.Tests/EventProcessorTests.cs b/src/Tests/OneScript.Core.Tests/EventProcessorTests.cs new file mode 100644 index 000000000..37f78d5d8 --- /dev/null +++ b/src/Tests/OneScript.Core.Tests/EventProcessorTests.cs @@ -0,0 +1,85 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using System; +using FluentAssertions; +using OneScript.Execution; +using OneScript.StandardLibrary.Collections; +using OneScript.Values; +using ScriptEngine.HostedScript; +using ScriptEngine.Hosting; +using ScriptEngine.Machine; +using ScriptEngine.Machine.Contexts; +using Xunit; + +namespace OneScript.Core.Tests +{ + public class EventProcessorTests + { + private const string HandlerScript = @" + Перем Вызовов Экспорт; + + Процедура Обработчик() Экспорт + Вызовов = Вызовов + 1; + КонецПроцедуры + + Вызовов = 0;"; + + private static (UserScriptContextInstance Handler, IBslProcess Process) CreateHandler() + { + var engine = DefaultEngineBuilder.Create().SetDefaultOptions().Build(); + engine.Initialize(); + + var process = engine.NewProcess(); + var handler = engine.AttachedScriptsFactory.LoadFromString( + engine.GetCompilerService(), HandlerScript, process); + + return (handler, process); + } + + private static decimal CallCount(UserScriptContextInstance handler) + { + var propertyIndex = handler.GetPropertyNumber("Вызовов"); + return (decimal)(BslNumericValue)handler.GetPropValue(propertyIndex); + } + + [Fact] + public void RemoveAllHandlers_UnsubscribesEverythingOfTheSource() + { + var (handler, process) = CreateHandler(); + var eventSource = new ArrayImpl(); + IEventProcessor processor = new DefaultEventProcessor(); + + processor.AddHandler(eventSource, "ПриЗавершении", handler, "Обработчик"); + processor.HandleEvent(eventSource, "ПриЗавершении", Array.Empty(), process); + + CallCount(handler).Should().Be(1, "подписка должна работать до её снятия"); + + processor.RemoveAllHandlers(eventSource); + processor.HandleEvent(eventSource, "ПриЗавершении", Array.Empty(), process); + + CallCount(handler).Should().Be(1, "после снятия подписок обработчик вызываться не должен"); + } + + [Fact] + public void RemoveAllHandlers_KeepsSubscriptionsOfOtherSources() + { + var (handler, process) = CreateHandler(); + var releasedSource = new ArrayImpl(); + var aliveSource = new ArrayImpl(); + IEventProcessor processor = new DefaultEventProcessor(); + + processor.AddHandler(releasedSource, "ПриЗавершении", handler, "Обработчик"); + processor.AddHandler(aliveSource, "ПриЗавершении", handler, "Обработчик"); + + processor.RemoveAllHandlers(releasedSource); + processor.HandleEvent(aliveSource, "ПриЗавершении", Array.Empty(), process); + + CallCount(handler).Should().Be(1, "снятие подписок одного источника не трогает другие"); + } + } +} diff --git a/src/Tests/OneScript.Core.Tests/NativeCompilerTest.cs b/src/Tests/OneScript.Core.Tests/NativeCompilerTest.cs index c4029976f..855bf25cf 100644 --- a/src/Tests/OneScript.Core.Tests/NativeCompilerTest.cs +++ b/src/Tests/OneScript.Core.Tests/NativeCompilerTest.cs @@ -61,8 +61,9 @@ public void CanInjectContext_As_Symbols() var context = new GlobalJsonFunctions(); var scope = SymbolScope.FromObject(context); - scope.Methods.Should().HaveCount(3); + scope.Methods.Should().HaveCount(4); scope.Methods.IndexOf("ЗаписатьJSON").Should().BeGreaterOrEqualTo(0); + scope.Methods.IndexOf("ЗаписатьДатуJSON").Should().BeGreaterOrEqualTo(0); } [Fact] diff --git a/src/Tests/OneScript.Core.Tests/OneScript.Core.Tests.csproj b/src/Tests/OneScript.Core.Tests/OneScript.Core.Tests.csproj index f1708dd92..cba21a483 100644 --- a/src/Tests/OneScript.Core.Tests/OneScript.Core.Tests.csproj +++ b/src/Tests/OneScript.Core.Tests/OneScript.Core.Tests.csproj @@ -27,6 +27,7 @@ + diff --git a/src/Tests/OneScript.Dynamic.Tests/NativeExecutableTestProcess.cs b/src/Tests/OneScript.Dynamic.Tests/NativeExecutableTestProcess.cs index f5c18cb9c..848b2a9f1 100644 --- a/src/Tests/OneScript.Dynamic.Tests/NativeExecutableTestProcess.cs +++ b/src/Tests/OneScript.Dynamic.Tests/NativeExecutableTestProcess.cs @@ -26,4 +26,9 @@ public BslValue Run(BslObjectValue target, IExecutableModule module, BslScriptMe public IServiceContainer Services { get; set; } public int VirtualThreadId => 0; + + public void Dispose() + { + Services.Dispose(); + } } \ No newline at end of file diff --git a/src/Tests/OneScript.Dynamic.Tests/NativeRuntimeAnnotationHandlerTests.cs b/src/Tests/OneScript.Dynamic.Tests/NativeRuntimeAnnotationHandlerTests.cs new file mode 100644 index 000000000..71b254893 --- /dev/null +++ b/src/Tests/OneScript.Dynamic.Tests/NativeRuntimeAnnotationHandlerTests.cs @@ -0,0 +1,89 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using FluentAssertions; +using OneScript.Compilation; +using OneScript.Language; +using OneScript.Language.LexicalAnalysis; +using OneScript.Language.SyntaxAnalysis; +using OneScript.Native.Compiler; +using OneScript.Sources; +using Xunit; + +namespace OneScript.Dynamic.Tests +{ + public class NativeRuntimeAnnotationHandlerTests + { + [Theory] + [InlineData("#stack\n#native")] + [InlineData("#native\n#stack")] + [InlineData("#stack\n#stack")] + [InlineData("#native\n#native")] + public void Duplicate_Runtime_Directive_Should_Report_Error(string code) + { + var errors = ParseModule(code); + + errors.Should().ContainSingle(e => e.ErrorId == "RuntimeDirectiveAlreadyDefined"); + } + + [Theory] + [InlineData("#native")] + [InlineData("#stack")] + public void Single_Runtime_Directive_Should_Parse(string code) + { + var errors = ParseModule(code); + + errors.Should().BeEmpty(); + } + + [Fact] + public void Duplicate_Runtime_Directive_Should_Throw_CompilerException() + { + var code = "#stack\n#native"; + + var act = () => ParseModuleWithThrowingSink(code); + + act.Should().Throw() + .Which.LineNumber.Should().Be(2); + } + + private static System.Collections.Generic.IEnumerable ParseModule(string code) + { + var errors = new ListErrorSink(); + Parse(code, errors); + return errors.Errors; + } + + private static void ParseModuleWithThrowingSink(string code) + { + var errors = new ThrowingErrorSink(CompilerException.FromCodeError); + Parse(code, errors); + } + + private static void Parse(string code, IErrorSink errors) + { + var lexer = new DefaultLexer + { + Iterator = SourceCodeBuilder.Create() + .FromString(code) + .WithName("") + .Build() + .CreateIterator() + }; + + var handlers = new PreprocessorHandlers(new[] { new NativeRuntimeAnnotationHandler(errors) }); + var preprocessingLexer = new PreprocessingLexer(lexer) + { + Handlers = handlers, + ErrorSink = errors + }; + + var parser = new DefaultBslParser(preprocessingLexer, errors, handlers); + _ = parser.ParseStatefulModule(); + } + } +} diff --git a/src/Tests/OneScript.Dynamic.Tests/NativeSdoTests.cs b/src/Tests/OneScript.Dynamic.Tests/NativeSdoTests.cs index 531d7fd3b..1bcbe677a 100644 --- a/src/Tests/OneScript.Dynamic.Tests/NativeSdoTests.cs +++ b/src/Tests/OneScript.Dynamic.Tests/NativeSdoTests.cs @@ -94,7 +94,7 @@ private IBslProcess GetProcess(IServiceContainer serviceContainer) { return new NativeExecutableTestProcess { - Services = serviceContainer + Services = serviceContainer.CreateScope() }; } diff --git a/src/Tests/OneScript.StandardLibrary.Tests/JsonReadDatePropertiesTests.cs b/src/Tests/OneScript.StandardLibrary.Tests/JsonReadDatePropertiesTests.cs new file mode 100644 index 000000000..9f3e60789 --- /dev/null +++ b/src/Tests/OneScript.StandardLibrary.Tests/JsonReadDatePropertiesTests.cs @@ -0,0 +1,281 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using System; +using OneScript.Commons; +using OneScript.Exceptions; +using OneScript.Localization; +using OneScript.StandardLibrary.Collections; +using OneScript.StandardLibrary.Json; +using OneScript.Values; +using ScriptEngine.Machine; +using Xunit; + +namespace OneScript.StandardLibrary.Tests +{ + public class JsonReadDatePropertiesTests + { + private const string JsonText = + "{\"a\":\"2026-02-01T15:30:45\"," + + "\"b\":\"2026-02-01T15:30:45.123\"," + + "\"c\":\"2026-02-01T15:30:45Z\"," + + "\"d\":\"2026-02-01T15:30:45+03:00\"," + + "\"e\":\"2026-02-01T15:30:45.1234567Z\"}"; + + private static DateTime LocalOf(int h, int m, int s) => + TimeZoneInfo.ConvertTimeFromUtc( + new DateTime(2026, 2, 1, h, m, s, DateTimeKind.Utc), + TimeZoneInfo.Local); + + private static IValue GetStructureProperty(StructureImpl structure, string name) => + structure.GetPropValue(structure.GetPropertyNumber(name)); + + private static IValue GetMapProperty(MapImpl map, string name) => + map.GetIndexedValue(ValueFactory.Create(name)); + + private static void AssertDateWithoutFraction(IValue value, DateTime expected) + { + Assert.IsType(value); + var date = (DateTime)(BslDateValue)value; + Assert.Equal(expected, date); + Assert.Equal(0, date.Ticks % TimeSpan.TicksPerSecond); + } + + private static ArrayImpl CreateNameArray(params string[] names) + { + var array = new ArrayImpl(); + foreach (var name in names) + array.Add(ValueFactory.Create(name)); + return array; + } + + [Fact] + public void ReadJson_Without_Date_Property_Names_Keeps_Strings() + { + var reader = new JSONReader(); + reader.SetString(JsonText); + var data = (StructureImpl)new GlobalJsonFunctions().ReadJSON(reader); + + Assert.IsType(GetStructureProperty(data, "a")); + Assert.IsType(GetStructureProperty(data, "b")); + Assert.IsType(GetStructureProperty(data, "c")); + Assert.IsType(GetStructureProperty(data, "d")); + Assert.IsType(GetStructureProperty(data, "e")); + + Assert.Equal("2026-02-01T15:30:45", GetStructureProperty(data, "a").ToString()); + Assert.Equal("2026-02-01T15:30:45.123", GetStructureProperty(data, "b").ToString()); + Assert.Equal("2026-02-01T15:30:45Z", GetStructureProperty(data, "c").ToString()); + Assert.Equal("2026-02-01T15:30:45+03:00", GetStructureProperty(data, "d").ToString()); + Assert.Equal("2026-02-01T15:30:45.1234567Z", GetStructureProperty(data, "e").ToString()); + } + + [Fact] + public void ReadJson_With_Single_String_Name_Parses_Only_That_Property() + { + var reader = new JSONReader(); + reader.SetString(JsonText); + var data = (StructureImpl)new GlobalJsonFunctions().ReadJSON( + reader, + PropertiesWithDateValuesNames: ValueFactory.Create("a")); + + AssertDateWithoutFraction(GetStructureProperty(data, "a"), new DateTime(2026, 2, 1, 15, 30, 45)); + Assert.IsType(GetStructureProperty(data, "b")); + Assert.IsType(GetStructureProperty(data, "c")); + Assert.IsType(GetStructureProperty(data, "d")); + Assert.IsType(GetStructureProperty(data, "e")); + } + + [Fact] + public void ReadJson_With_ArrayImpl_Names_Parses_Iso_Dates() + { + var names = CreateNameArray("a", "b", "c", "d", "e"); + + var reader = new JSONReader(); + reader.SetString(JsonText); + var data = (StructureImpl)new GlobalJsonFunctions().ReadJSON( + reader, + PropertiesWithDateValuesNames: names, + ExpectedDateFormat: JSONDateFormatEnum.ISO); + + AssertDateWithoutFraction(GetStructureProperty(data, "a"), new DateTime(2026, 2, 1, 15, 30, 45)); + AssertDateWithoutFraction(GetStructureProperty(data, "b"), new DateTime(2026, 2, 1, 15, 30, 45)); + AssertDateWithoutFraction(GetStructureProperty(data, "c"), LocalOf(15, 30, 45)); + AssertDateWithoutFraction(GetStructureProperty(data, "d"), LocalOf(12, 30, 45)); + AssertDateWithoutFraction(GetStructureProperty(data, "e"), LocalOf(15, 30, 45)); + } + + [Fact] + public void ReadJson_With_FixedArrayImpl_Names_Parses_Iso_Dates() + { + var names = new FixedArrayImpl(CreateNameArray("a", "b", "c", "d", "e")); + + var reader = new JSONReader(); + reader.SetString(JsonText); + var data = (StructureImpl)new GlobalJsonFunctions().ReadJSON( + reader, + PropertiesWithDateValuesNames: names, + ExpectedDateFormat: JSONDateFormatEnum.ISO); + + AssertDateWithoutFraction(GetStructureProperty(data, "a"), new DateTime(2026, 2, 1, 15, 30, 45)); + AssertDateWithoutFraction(GetStructureProperty(data, "b"), new DateTime(2026, 2, 1, 15, 30, 45)); + AssertDateWithoutFraction(GetStructureProperty(data, "c"), LocalOf(15, 30, 45)); + AssertDateWithoutFraction(GetStructureProperty(data, "d"), LocalOf(12, 30, 45)); + AssertDateWithoutFraction(GetStructureProperty(data, "e"), LocalOf(15, 30, 45)); + } + + [Fact] + public void ReadJson_With_Names_Parses_Dates_At_Any_Nesting_Level() + { + const string nestedJson = + "{\"outer\":{\"a\":\"2026-02-01T15:30:45Z\"}," + + "\"items\":[{\"b\":\"2026-02-01T15:30:45Z\"}]}"; + + var names = CreateNameArray("a", "b"); + + var reader = new JSONReader(); + reader.SetString(nestedJson); + var data = (StructureImpl)new GlobalJsonFunctions().ReadJSON( + reader, + PropertiesWithDateValuesNames: names); + + var outer = (StructureImpl)GetStructureProperty(data, "outer"); + var items = (ArrayImpl)GetStructureProperty(data, "items"); + var item = (StructureImpl)items.GetIndexedValue(ValueFactory.Create(0)); + + AssertDateWithoutFraction(GetStructureProperty(outer, "a"), LocalOf(15, 30, 45)); + AssertDateWithoutFraction(GetStructureProperty(item, "b"), LocalOf(15, 30, 45)); + } + + [Fact] + public void ReadJson_ToMap_With_Names_Parses_Dates() + { + var names = CreateNameArray("a"); + + var reader = new JSONReader(); + reader.SetString("{\"a\":\"2026-02-01T15:30:45Z\"}"); + var data = (MapImpl)new GlobalJsonFunctions().ReadJSON( + reader, + ReadToMap: true, + PropertiesWithDateValuesNames: names); + + AssertDateWithoutFraction(GetMapProperty(data, "a"), LocalOf(15, 30, 45)); + } + + [Fact] + public void ReadJson_With_Names_Does_Not_Convert_NonString_Values() + { + const string json = + "{\"a\":123,\"b\":null,\"c\":{\"x\":1},\"d\":[1,2]}"; + + var names = CreateNameArray("a", "b", "c", "d", "missing"); + + var reader = new JSONReader(); + reader.SetString(json); + var data = (StructureImpl)new GlobalJsonFunctions().ReadJSON( + reader, + PropertiesWithDateValuesNames: names); + + Assert.IsType(GetStructureProperty(data, "a")); + Assert.IsType(GetStructureProperty(data, "b")); + Assert.IsType(GetStructureProperty(data, "c")); + Assert.IsType(GetStructureProperty(data, "d")); + } + + [Fact] + public void ReadJsonDate_Drops_Fractional_Seconds_For_Iso() + { + var result = new GlobalJsonFunctions().ReadJSONDate("2026-02-01T15:30:45.123", JSONDateFormatEnum.ISO); + var date = (DateTime)(BslDateValue)result; + + Assert.Equal(new DateTime(2026, 2, 1, 15, 30, 45), date); + Assert.Equal(0, date.Ticks % TimeSpan.TicksPerSecond); + } + + [Fact] + public void ReadJsonDate_Converts_Z_And_Offset_To_Local() + { + var fromZ = (DateTime)(BslDateValue)new GlobalJsonFunctions().ReadJSONDate( + "2026-02-01T15:30:45Z", + JSONDateFormatEnum.ISO); + var fromOffset = (DateTime)(BslDateValue)new GlobalJsonFunctions().ReadJSONDate( + "2026-02-01T15:30:45+03:00", + JSONDateFormatEnum.ISO); + + Assert.Equal(LocalOf(15, 30, 45), fromZ); + Assert.Equal(LocalOf(12, 30, 45), fromOffset); + } + + [Fact] + public void ReadJson_With_Invalid_Date_String_Throws_RuntimeException() + { + var names = CreateNameArray("a"); + + var reader = new JSONReader(); + reader.SetString("{\"a\":\"not-a-date\"}"); + + var exception = Assert.Throws(() => + new GlobalJsonFunctions().ReadJSON(reader, PropertiesWithDateValuesNames: names)); + + Assert.True(exception.ErrorDescription.BilingualEquals( + "Представление даты имеет неверный формат.", + "Invalid date presentation format")); + } + + [Fact] + public void ReadJsonDate_With_Invalid_String_Throws_RuntimeException() + { + var exception = Assert.Throws(() => + new GlobalJsonFunctions().ReadJSONDate("not-a-date", JSONDateFormatEnum.ISO)); + + Assert.True(exception.ErrorDescription.BilingualEquals( + "Представление даты имеет неверный формат.", + "Invalid date presentation format")); + } + + [Fact] + public void ReadJson_With_JavaScript_Format_Throws() + { + var reader = new JSONReader(); + reader.SetString(JsonText); + + Assert.Throws(() => + new GlobalJsonFunctions().ReadJSON( + reader, + PropertiesWithDateValuesNames: CreateNameArray("a"), + ExpectedDateFormat: JSONDateFormatEnum.JavaScript)); + } + + [Fact] + public void ReadJsonDate_With_JavaScript_Format_Throws() + { + Assert.Throws(() => + new GlobalJsonFunctions().ReadJSONDate("2026-02-01T15:30:45", JSONDateFormatEnum.JavaScript)); + } + + [Fact] + public void ReadJson_With_Empty_Names_Keeps_Strings() + { + var reader = new JSONReader(); + reader.SetString(JsonText); + + var withEmptyArray = (StructureImpl)new GlobalJsonFunctions().ReadJSON( + reader, + PropertiesWithDateValuesNames: new ArrayImpl()); + + reader.SetString(JsonText); + var withNull = (StructureImpl)new GlobalJsonFunctions().ReadJSON( + reader, + PropertiesWithDateValuesNames: null); + + foreach (var data in new[] { withEmptyArray, withNull }) + { + Assert.IsType(GetStructureProperty(data, "a")); + Assert.IsType(GetStructureProperty(data, "b")); + } + } + } +} diff --git a/src/Tests/OneScript.StandardLibrary.Tests/ProcessOutputWrapperTest.cs b/src/Tests/OneScript.StandardLibrary.Tests/ProcessOutputWrapperTest.cs new file mode 100644 index 000000000..64b15a4f5 --- /dev/null +++ b/src/Tests/OneScript.StandardLibrary.Tests/ProcessOutputWrapperTest.cs @@ -0,0 +1,296 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.IO; +using System.Threading; +using Xunit; +using FluentAssertions; +using OneScript.StandardLibrary.Processes; + +namespace OneScript.StandardLibrary.Tests +{ + public class ProcessOutputWrapperTest : IDisposable + { + // Источник, отдающий текст порциями, как пайп работающего процесса: + // Read блокируется до появления следующей порции, Complete() дает EOF. + private class ChunkedReader : TextReader + { + private readonly BlockingCollection _chunks = new BlockingCollection(); + private string _current = string.Empty; + private int _pos; + private Exception _error; + + public void Push(string chunk) => _chunks.Add(chunk); + + public void Complete() => _chunks.CompleteAdding(); + + public void Fail(Exception error) + { + _error = error; + _chunks.CompleteAdding(); + } + + public bool AllTaken => _chunks.Count == 0; + + public override int Read(char[] buffer, int index, int count) + { + while (_pos >= _current.Length) + { + if (!_chunks.TryTake(out _current, Timeout.Infinite)) + { + if (_error != null) + throw _error; + + return 0; // EOF + } + + _pos = 0; + } + + int n = Math.Min(count, _current.Length - _pos); + _current.CopyTo(_pos, buffer, index, n); + _pos += n; + return n; + } + } + + private readonly ChunkedReader _source = new ChunkedReader(); + private readonly ProcessOutputWrapper _wrapper; + + public ProcessOutputWrapperTest() + { + _wrapper = new ProcessOutputWrapper(_source); + _wrapper.StartReading(); + } + + public void Dispose() + { + _source.Complete(); + _wrapper.Dispose(); + } + + // Перекачка фоновая, ждем видимого эффекта от отданной порции + private T Eventually(Func read, Func arrived) + { + var sw = Stopwatch.StartNew(); + while (true) + { + var value = read(); + if (arrived(value) || sw.ElapsedMilliseconds > 5000) + return value; + + Thread.Sleep(1); + } + } + + private string EventuallyReadLine() => Eventually(() => _wrapper.ReadLine(), line => line != null); + + [Fact] + public void ReadLine_ReturnsLinesInOrder() + { + _source.Push("L1\nL2\nL3\n"); + + EventuallyReadLine().Should().Be("L1"); + EventuallyReadLine().Should().Be("L2"); + EventuallyReadLine().Should().Be("L3"); + _wrapper.ReadLine().Should().BeNull(); + } + + // Гонка из issue #1726: читатель вычерпал буфер по границе строки, + // затем поступила следующая порция. Лишняя пустая строка не появляется. + [Fact] + public void ReadLine_DoesNotInsertEmptyLine_WhenBufferDrainedBetweenWrites() + { + _source.Push("L1\nL2\n"); + + EventuallyReadLine().Should().Be("L1"); + EventuallyReadLine().Should().Be("L2"); + _wrapper.ReadLine().Should().BeNull(); + + _source.Push("L3\n"); + + EventuallyReadLine().Should().Be("L3"); + _wrapper.ReadLine().Should().BeNull(); + } + + // Незавершенная строка не возвращается ни целиком, ни по кускам, + // пока не придет ее терминатор + [Fact] + public void ReadLine_DoesNotTearLine_SplitBetweenWrites() + { + _source.Push("L1\nПол"); + + EventuallyReadLine().Should().Be("L1"); + _wrapper.ReadLine().Should().BeNull(); + + _source.Push("овина\n"); + + EventuallyReadLine().Should().Be("Половина"); + } + + // Длинная строка, приходящая многими порциями: промежуточные опросы + // возвращают null, итоговая строка собирается целиком + [Fact] + public void ReadLine_AssemblesLine_FromManyChunks() + { + _source.Push("aa"); + Eventually(() => _wrapper.Peek(), c => c != -1); + _wrapper.ReadLine().Should().BeNull(); + + _source.Push("bb"); + _wrapper.ReadLine().Should().BeNull(); + + _source.Push("cc\n"); + EventuallyReadLine().Should().Be("aabbcc"); + _wrapper.ReadLine().Should().BeNull(); + } + + // CRLF, разорванный между порциями, не дает ни фантомной пустой строки, + // ни '\r' в теле строки + [Fact] + public void ReadLine_HandlesCrLfSplitBetweenWrites() + { + _source.Push("L1\r"); + + _wrapper.ReadLine().Should().BeNull(); + + _source.Push("\nL2\r\n"); + + EventuallyReadLine().Should().Be("L1"); + EventuallyReadLine().Should().Be("L2"); + _wrapper.ReadLine().Should().BeNull(); + } + + [Fact] + public void ReadLine_PreservesRealEmptyLines() + { + _source.Push("L1\n\nL2\n"); + + EventuallyReadLine().Should().Be("L1"); + EventuallyReadLine().Should().Be(""); + EventuallyReadLine().Should().Be("L2"); + _wrapper.ReadLine().Should().BeNull(); + } + + // Пустая последняя строка вывода больше не теряется + [Fact] + public void ReadLine_PreservesTrailingEmptyLine() + { + _source.Push("L1\n\n"); + _source.Complete(); + + EventuallyReadLine().Should().Be("L1"); + EventuallyReadLine().Should().Be(""); + _wrapper.ReadLine().Should().BeNull(); + } + + // Вывод, не завершенный переводом строки, возвращается по концу потока + [Fact] + public void ReadLine_ReturnsUnterminatedTail_AfterStreamEnd() + { + _source.Push("L1\nL2"); + + EventuallyReadLine().Should().Be("L1"); + _wrapper.ReadLine().Should().BeNull(); + + _source.Complete(); + + EventuallyReadLine().Should().Be("L2"); + _wrapper.ReadLine().Should().BeNull(); + } + + // Прочитать() возвращает подлинные символы вывода процесса, + // без синтеза и без подрезки + [Fact] + public void ReadToEnd_ReturnsRawOutput() + { + _source.Push("L1\nL2\n"); + + Eventually(() => _wrapper.Peek(), c => c != -1); + EventuallyReadLine().Should().Be("L1"); + + _wrapper.ReadToEnd().Should().Be("L2\n"); + } + + // Ошибка чтения источника не глотается: она перебрасывается читателю, + // но только после того, как он вычитает накопленные данные + [Fact] + public void SourceFailure_SurfacesToReader_AfterBufferedDataIsConsumed() + { + _source.Push("L1\n"); + _source.Fail(new IOException("pipe failure")); + + EventuallyReadLine().Should().Be("L1"); + + var error = Eventually(ReadLineError, e => e != null); + error.Should().BeOfType().Which.Message.Should().Be("pipe failure"); + + // ошибка не одноразовая: последующие обращения тоже ее видят + ReadLineError().Should().BeOfType(); + } + + private Exception ReadLineError() + { + try + { + _wrapper.ReadLine(); + return null; + } + catch (Exception e) + { + return e; + } + } + + // После закрытия читателя источник продолжает дренироваться + // (иначе процесс заблокируется на записи в переполненный пайп), + // но данные больше не накапливаются + [Fact] + public void AfterDispose_SourceIsStillDrained_ButDataIsDiscarded() + { + _source.Push("L1\n"); + EventuallyReadLine().Should().Be("L1"); + + _wrapper.Dispose(); + _source.Push("L2\n"); + + Eventually(() => _source.AllTaken, taken => taken).Should().BeTrue(); + _wrapper.ReadLine().Should().BeNull(); + } + + // Вычитанный префикс буфера освобождается: вывод долгоживущего + // процесса не накапливается в памяти при аккуратном читателе + [Fact] + public void ConsumedPrefix_IsCompacted() + { + var line = new string('x', 1000); + for (int i = 0; i < 20; i++) + { + _source.Push(line + "\n"); + EventuallyReadLine().Should().Be(line); + } + + _wrapper.InternalBufferSize.Should().BeLessThan(2 * 4096); + } + + [Fact] + public void IsDrained_BecomesTrue_OnlyAfterSourceEnd() + { + _source.Push("L1\n"); + EventuallyReadLine().Should().Be("L1"); + + _wrapper.IsDrained.Should().BeFalse(); + + _source.Complete(); + + Eventually(() => _wrapper.IsDrained, drained => drained).Should().BeTrue(); + } + } +} diff --git a/src/oscommon.targets b/src/oscommon.targets index 39d0e4266..2fc560484 100644 --- a/src/oscommon.targets +++ b/src/oscommon.targets @@ -6,7 +6,7 @@ 0 Release x86 - default + 12 net8.0 $(NoWarn);CS1591 False diff --git a/src/oscript/BehaviorSelector.cs b/src/oscript/BehaviorSelector.cs index 264cfb64f..be49228eb 100644 --- a/src/oscript/BehaviorSelector.cs +++ b/src/oscript/BehaviorSelector.cs @@ -44,6 +44,7 @@ private static AppBehavior SelectParametrized(CmdLineHelper helper) initializers.Add("-cgi", h => new CgiBehavior()); initializers.Add("-version", h => new ShowVersionBehavior()); initializers.Add("-v", h => new ShowVersionBehavior()); + initializers.Add("-c", ExecuteCodeBehavior.Create); initializers.Add("-encoding", ProcessEncodingKey); initializers.Add("-codestat", EnableCodeStatistics); initializers.Add("-debug", DebugBehavior.Create); diff --git a/src/oscript/ConsoleApplicationHost.cs b/src/oscript/ConsoleApplicationHost.cs new file mode 100644 index 000000000..45b2bc11c --- /dev/null +++ b/src/oscript/ConsoleApplicationHost.cs @@ -0,0 +1,51 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using System; +using OneScript.Sources; +using OneScript.StandardLibrary; +using ScriptEngine; +using ScriptEngine.HostedScript; + +namespace oscript +{ + internal class ConsoleApplicationHost(string[] args) : IHostApplication, ISystemLogWriter + { + public void Echo(string text, MessageStatusEnum status = MessageStatusEnum.Ordinary) + => ConsoleHostImpl.Echo(text, status); + + public void ShowExceptionInfo(Exception exc) + => ConsoleHostImpl.ShowExceptionInfo(exc); + + public bool InputString(out string result, string prompt, int maxLen, bool multiline) + => ConsoleHostImpl.InputString(out result, prompt, maxLen, multiline); + + public string[] GetCommandLineArguments() + => args; + + public void Write(string text) + => Console.Error.WriteLine(text); + + public int RunProcess(HostedScriptEngine engine, SourceCode source) + { + SystemLogger.SetWriter(this); + + Process process; + try + { + process = engine.CreateProcess(this, source); + } + catch (Exception e) + { + ShowExceptionInfo(e); + return 1; + } + + return process.Start(); + } + } +} diff --git a/src/oscript/ExecuteCodeBehavior.cs b/src/oscript/ExecuteCodeBehavior.cs new file mode 100644 index 000000000..0e943e5e2 --- /dev/null +++ b/src/oscript/ExecuteCodeBehavior.cs @@ -0,0 +1,37 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using System; +using System.IO; +using ScriptEngine.HostedScript; + +namespace oscript +{ + internal class ExecuteCodeBehavior(string code, string[] args) : AppBehavior + { + private readonly ConsoleApplicationHost _host = new(args); + + public static AppBehavior Create(CmdLineHelper helper) + { + var code = helper.Next(); + if (string.IsNullOrEmpty(code)) + return null; + + return new ExecuteCodeBehavior(code, helper.Tail()); + } + + public override int Execute() + { + var configPath = Path.Combine(Environment.CurrentDirectory, CfgFileConfigProvider.CONFIG_FILE_NAME); + var builder = ConsoleHostBuilder.Create(configPath); + using var hostedScript = ConsoleHostBuilder.Build(builder); + var source = hostedScript.Loader.FromString(code); + + return _host.RunProcess(hostedScript, source); + } + } +} diff --git a/src/oscript/ExecuteScriptBehavior.cs b/src/oscript/ExecuteScriptBehavior.cs index 49cbc29c4..0d77883db 100644 --- a/src/oscript/ExecuteScriptBehavior.cs +++ b/src/oscript/ExecuteScriptBehavior.cs @@ -4,25 +4,23 @@ This Source Code Form is subject to the terms of the was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. ----------------------------------------------------------*/ -using System; -using OneScript.StandardLibrary; -using ScriptEngine; -using ScriptEngine.HostedScript; + +using System.IO; using ScriptEngine.Hosting; using ScriptEngine.Machine; using ScriptEngine.Machine.Debugger; namespace oscript { - class ExecuteScriptBehavior : AppBehavior, IHostApplication, ISystemLogWriter + class ExecuteScriptBehavior : AppBehavior { - protected string[] _scriptArgs; protected string _path; + private readonly ConsoleApplicationHost _host; public ExecuteScriptBehavior(string path, string[] args) { - _scriptArgs = args; _path = path; + _host = new ConsoleApplicationHost(args); } public IDebugger DebugController { get; set; } = new DisabledDebugger(); @@ -33,14 +31,12 @@ public ExecuteScriptBehavior(string path, string[] args) public override int Execute() { - if (!System.IO.File.Exists(_path)) + if (!File.Exists(_path)) { - Echo($"Script file is not found '{_path}'"); + _host.Echo($"Script file is not found '{_path}'"); return 2; } - SystemLogger.SetWriter(this); - var builder = ConsoleHostBuilder.Create(_path); builder.WithDebugger(DebugController); CodeStatProcessor codeStatProcessor = null; @@ -50,22 +46,9 @@ public override int Execute() builder.Services.RegisterSingleton(codeStatProcessor); } - var hostedScript = ConsoleHostBuilder.Build(builder); - + using var hostedScript = ConsoleHostBuilder.Build(builder); var source = hostedScript.Loader.FromFile(_path); - Process process; - try - { - process = hostedScript.CreateProcess(this, source); - } - catch (Exception e) - { - ShowExceptionInfo(e); - return 1; - } - - var result = process.Start(); - hostedScript.Dispose(); + var result = _host.RunProcess(hostedScript, source); if (codeStatProcessor != null) { @@ -77,34 +60,5 @@ public override int Execute() return result; } - - #region IHostApplication Members - - public void Echo(string text, MessageStatusEnum status = MessageStatusEnum.Ordinary) - { - ConsoleHostImpl.Echo(text, status); - } - - public void ShowExceptionInfo(Exception exc) - { - ConsoleHostImpl.ShowExceptionInfo(exc); - } - - public bool InputString(out string result, string prompt, int maxLen, bool multiline) - { - return ConsoleHostImpl.InputString(out result, prompt, maxLen, multiline); - } - - public string[] GetCommandLineArguments() - { - return _scriptArgs; - } - - #endregion - - public void Write(string text) - { - Console.Error.WriteLine(text); - } } } diff --git a/src/oscript/ShowUsageBehavior.cs b/src/oscript/ShowUsageBehavior.cs index ee1c26a57..d3bf00470 100644 --- a/src/oscript/ShowUsageBehavior.cs +++ b/src/oscript/ShowUsageBehavior.cs @@ -15,6 +15,7 @@ public override int Execute() Output.WriteLine(); Output.WriteLine("Usage:"); Output.WriteLine(" oscript.exe [options] [script_arguments...]"); + Output.WriteLine(" oscript.exe [options] -c [script_arguments...]"); Output.WriteLine(" oscript.exe [mode_options] [script_arguments...]"); Output.WriteLine(); @@ -34,6 +35,7 @@ public override int Execute() Output.WriteLine($" {"",modeWidth} {"-port=",subOptionWidth} Debugger port (default is 2801)."); Output.WriteLine($" {"",modeWidth} {"-noWait",subOptionWidth} Do not wait for debugger connection."); + Output.WriteLine($" {"-c",modeWidth} Execute code from command line."); Output.WriteLine($" {"-version, -v",modeWidth} Output version string."); Output.WriteLine(); diff --git a/tests/cli-eval.os b/tests/cli-eval.os new file mode 100644 index 000000000..2d0d92903 --- /dev/null +++ b/tests/cli-eval.os @@ -0,0 +1,73 @@ +Перем юТест; + +Функция ПолучитьСписокТестов(ЮнитТестирование) Экспорт + + юТест = ЮнитТестирование; + + ВсеТесты = Новый Массив; + ВсеТесты.Добавить("ТестДолжен_ВыполнитьКодЧерезФлагC"); + ВсеТесты.Добавить("ТестДолжен_ПередатьАргументыПослеКода"); + ВсеТесты.Добавить("ТестДолжен_ПоказатьUsageЕслиНетКода"); + ВсеТесты.Добавить("ТестДолжен_РаботатьСEncoding"); + + Возврат ВсеТесты; + +КонецФункции + +Функция ПутьОСкрипт() + Возврат "dotnet """ + ОбъединитьПути(КаталогПрограммы(), "oscript.dll") + """"; +КонецФункции + +Функция НормализоватьВывод(Знач Текст) + Текст = СтрЗаменить(Текст, Символы.ВК, ""); + Возврат СокрЛП(Текст); +КонецФункции + +Функция ЗапуститьОскрипт(Знач Аргументы) + + Процесс = СоздатьПроцесс(ПутьОСкрипт() + " " + Аргументы, , Истина, , КодировкаТекста.UTF8); + Процесс.Запустить(); + Процесс.ОжидатьЗавершения(); + + Результат = Новый Структура; + Результат.Вставить("КодВозврата", Процесс.КодВозврата); + Результат.Вставить("Вывод", НормализоватьВывод(Процесс.ПотокВывода.Прочитать())); + Возврат Результат; + +КонецФункции + +Процедура ТестДолжен_ВыполнитьКодЧерезФлагC() Экспорт + + Результат = ЗапуститьОскрипт("-c ""Сообщить(1+2)"""); + + юТест.ПроверитьРавенство(0, Результат.КодВозврата, "Код возврата"); + юТест.ПроверитьРавенство("3", Результат.Вывод, "Вывод выражения"); + +КонецПроцедуры + +Процедура ТестДолжен_ПередатьАргументыПослеКода() Экспорт + + Результат = ЗапуститьОскрипт("-c ""Сообщить(АргументыКоманднойСтроки[0])"" hello"); + + юТест.ПроверитьРавенство(0, Результат.КодВозврата, "Код возврата"); + юТест.ПроверитьРавенство("hello", Результат.Вывод, "Аргумент скрипта"); + +КонецПроцедуры + +Процедура ТестДолжен_ПоказатьUsageЕслиНетКода() Экспорт + + Результат = ЗапуститьОскрипт("-c"); + + юТест.ПроверитьРавенство(0, Результат.КодВозврата, "Код возврата usage"); + юТест.ПроверитьИстину(СтрНайти(Результат.Вывод, "-c") > 0, "В usage должен быть флаг -c"); + +КонецПроцедуры + +Процедура ТестДолжен_РаботатьСEncoding() Экспорт + + Результат = ЗапуститьОскрипт("-encoding=utf-8 -c ""Сообщить(""""привет"""")"""); + + юТест.ПроверитьРавенство(0, Результат.КодВозврата, "Код возврата"); + юТест.ПроверитьРавенство("привет", Результат.Вывод, "Вывод с -encoding"); + +КонецПроцедуры diff --git a/tests/date-behavior.os b/tests/date-behavior.os new file mode 100644 index 000000000..160fcdcf7 --- /dev/null +++ b/tests/date-behavior.os @@ -0,0 +1,341 @@ +Перем юТест; + +Функция ПолучитьСписокТестов(ЮнитТестирование) Экспорт + + юТест = ЮнитТестирование; + + ВсеТесты = Новый Массив; + + ВсеТесты.Добавить("Тест_ДолженПроверить_ПрибавкаВышеТочности1СМеняетЗначение"); + + ВсеТесты.Добавить("Тест_ДолженПроверить_ПрибавкаНижеПоловиныШага1СИгнорируется"); + ВсеТесты.Добавить("Тест_ДолженПроверить_ПрибавкаОкругляетсяДоШага1С"); + + ВсеТесты.Добавить("Тест_ДолженПроверить_ДробнаяЧастьНеОкругляетсяДоСекунды"); + ВсеТесты.Добавить("Тест_ДолженПроверить_РазностьДатВозвращаетДробныеСекунды"); + ВсеТесты.Добавить("Тест_ДолженПроверить_РазностьДатНеЦелая"); + ВсеТесты.Добавить("Тест_ДолженПроверить_СравнениеРазличаетДробнуюЧасть"); + + ВсеТесты.Добавить("Тест_ДолженПроверить_ТекущаяДатаБезДолейСекунды"); + + ВсеТесты.Добавить("Тест_ДолженПроверить_ТекущаяУниверсальнаяДатаБезДолейСекунды"); + + ВсеТесты.Добавить("Тест_ДолженПроверить_МестноеВремяБезДолейСекунды"); + ВсеТесты.Добавить("Тест_ДолженПроверить_КонвертацияПоясаСохраняетДробь"); + + ВсеТесты.Добавить("Тест_ДолженПроверить_СтрокаСкрываетДробнуюЧасть"); + ВсеТесты.Добавить("Тест_ДолженПроверить_ФорматИгнорируетДробнуюЧасть"); + + ВсеТесты.Добавить("Тест_ДолженПроверить_XMLСтрокаОтбрасываетДробнуюЧасть"); + + ВсеТесты.Добавить("Тест_ДолженПроверить_ЗаписьJsonНаивнойДаты"); + + ВсеТесты.Добавить("Тест_ДолженПроверить_ЗаписьJsonОтбрасываетДробнуюЧасть"); + ВсеТесты.Добавить("Тест_ДолженПроверить_ЗаписьJsonИгнорируетKind"); + + ВсеТесты.Добавить("Тест_ДолженПроверить_ЧтениеJsonБезИменСвойствДат"); + + ВсеТесты.Добавить("Тест_ДолженПроверить_ЧтениеJsonСИменамиСвойствДат"); + + ВсеТесты.Добавить("Тест_ДолженПроверить_ПрочитатьДатуJsonОтбрасываетДробнуюЧасть"); + + Возврат ВсеТесты; + +КонецФункции + +Функция БазоваяДата() + + Возврат Дата(2026, 2, 1, 15, 30, 45); + +КонецФункции + +Функция БезДробнойЧасти(Знач Дата) + + Возврат Дата(Год(Дата), Месяц(Дата), День(Дата), Час(Дата), Минута(Дата), Секунда(Дата)); + +КонецФункции + +Функция ТекстJsonДляЧтенияДат() + + Возврат "{""a"":""2026-02-01T15:30:45"", + |""b"":""2026-02-01T15:30:45.123"", + |""c"":""2026-02-01T15:30:45Z"", + |""d"":""2026-02-01T15:30:45+03:00"", + |""e"":""2026-02-01T15:30:45.1234567Z""}"; + +КонецФункции + +Функция ОжидаемаяЛокальнаяДатаИзUtc(Год, Месяц, День, Час, Минута, Секунда) + + Возврат БезДробнойЧасти(МестноеВремя(Дата(Год, Месяц, День, Час, Минута, Секунда))); + +КонецФункции + +Процедура Тест_ДолженПроверить_ПрибавкаВышеТочности1СМеняетЗначение() Экспорт + + База = БазоваяДата(); + Дельты = Новый Массив; + Дельты.Добавить(0.001); + Дельты.Добавить(0.1); + Дельты.Добавить(0.5); + Дельты.Добавить(0.999); + Дельты.Добавить(0.0001); + + Для Каждого Дельта Из Дельты Цикл + юТест.ПроверитьЛожь(База = (База + Дельта), "Прибавка " + Дельта + " должна менять значение"); + КонецЦикла; + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_ПрибавкаНижеПоловиныШага1СИгнорируется() Экспорт + + База = БазоваяДата(); + Дельты = Новый Массив; + Дельты.Добавить(0.00001); + Дельты.Добавить(0.0000001); + Дельты.Добавить(0.00004); + + Для Каждого Дельта Из Дельты Цикл + юТест.ПроверитьИстину(База = (База + Дельта), "Прибавка " + Дельта + " должна игнорироваться"); + КонецЦикла; + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_ПрибавкаОкругляетсяДоШага1С() Экспорт + + База = БазоваяДата(); + + юТест.ПроверитьРавенство(0.0001, (База + 0.00005) - База); + юТест.ПроверитьРавенство(0.0001, (База + 0.00006) - База); + юТест.ПроверитьРавенство(0.0002, (База + 0.00015) - База); + юТест.ПроверитьРавенство(0.0002, (База + 0.00016) - База); + юТест.ПроверитьРавенство(0.0003, (База + 0.00025) - База); + юТест.ПроверитьРавенство(-0.0001, (База - 0.00005) - База); + юТест.ПроверитьРавенство(-0.0002, (База - 0.00015) - База); + + юТест.ПроверитьИстину((База + 0.00006) = (База + 0.0001)); + юТест.ПроверитьЛожь((База + 0.00016) = (База + 0.0001)); + юТест.ПроверитьИстину((База + 0.00016) = (База + 0.0002)); + юТест.ПроверитьИстину((База + 0.00015) = (База + 0.0002)); + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_ДробнаяЧастьНеОкругляетсяДоСекунды() Экспорт + + База = БазоваяДата(); + + юТест.ПроверитьЛожь((База + 0.6) = База); + юТест.ПроверитьЛожь((База + 0.6) = (База + 1)); + юТест.ПроверитьЛожь((База + 0.4) = База); + юТест.ПроверитьЛожь((База + 0.0004) = База); + юТест.ПроверитьЛожь((База + 0.0006) = База); + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_РазностьДатВозвращаетДробныеСекунды() Экспорт + + База = БазоваяДата(); + + юТест.ПроверитьРавенство(0.5, (База + 0.5) - База); + юТест.ПроверитьРавенство(1.5, (База + 1.5) - База); + юТест.ПроверитьРавенство(0.25, (База + 0.25) - База); + юТест.ПроверитьРавенство(2, (База + 2) - База); + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_РазностьДатНеЦелая() Экспорт + + База = БазоваяДата(); + Разность = (База + 0.5) - База; + + юТест.ПроверитьИстину(Разность <> Цел(Разность)); + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_СравнениеРазличаетДробнуюЧасть() Экспорт + + База = БазоваяДата(); + Дробная = База + 0.001; + + юТест.ПроверитьИстину(Дробная > База); + юТест.ПроверитьИстину(Дробная >= База); + юТест.ПроверитьЛожь(Дробная < База); + юТест.ПроверитьЛожь(База = Дробная); + юТест.ПроверитьИстину(База <> Дробная); + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_ТекущаяДатаБезДолейСекунды() Экспорт + + ТекДата = ТекущаяДата(); + + юТест.ПроверитьРавенство(ТекДата, БезДробнойЧасти(ТекДата)); + юТест.ПроверитьРавенство(0, ТекДата - БезДробнойЧасти(ТекДата)); + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_ТекущаяУниверсальнаяДатаБезДолейСекунды() Экспорт + + УнивДата = ТекущаяУниверсальнаяДата(); + + юТест.ПроверитьРавенство(УнивДата, БезДробнойЧасти(УнивДата)); + юТест.ПроверитьРавенство(0, УнивДата - БезДробнойЧасти(УнивДата)); + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_МестноеВремяБезДолейСекунды() Экспорт + + Унив = БезДробнойЧасти(ТекущаяУниверсальнаяДата()); + Местн = МестноеВремя(Унив); + + юТест.ПроверитьРавенство(Местн, БезДробнойЧасти(Местн)); + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_КонвертацияПоясаСохраняетДробь() Экспорт + + Дробная = БазоваяДата() + 0.001; + + юТест.ПроверитьРавенство(Дробная, МестноеВремя(УниверсальноеВремя(Дробная))); + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_СтрокаСкрываетДробнуюЧасть() Экспорт + + База = БазоваяДата(); + Дробная = База + 0.123; + + юТест.ПроверитьРавенство(Строка(База), Строка(Дробная)); + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_ФорматИгнорируетДробнуюЧасть() Экспорт + + Дробная = БазоваяДата() + 0.123; + + юТест.ПроверитьРавенство("45", Формат(Дробная, "ДФ=сс")); + юТест.ПроверитьРавенство("20260201153045", Формат(Дробная, "ДФ=ггггММддЧЧммсс")); + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_XMLСтрокаОтбрасываетДробнуюЧасть() Экспорт + + Дробная = БазоваяДата() + 0.123; + + юТест.ПроверитьРавенство("2026-02-01T15:30:45", XMLСтрока(Дробная)); + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_ЗаписьJsonНаивнойДаты() Экспорт + + Данные = Новый Структура; + Данные.Вставить("Литерал", БазоваяДата()); + Данные.Вставить("ПустаяДата", Дата(1, 1, 1)); + + Запись = Новый ЗаписьJSON(); + Запись.УстановитьСтроку(); + ЗаписатьJSON(Запись, Данные); + Текст = Запись.Закрыть(); + + юТест.ПроверитьВхождение(Текст, """Литерал"": ""2026-02-01T15:30:45"""); + юТест.ПроверитьВхождение(Текст, """ПустаяДата"": ""0001-01-01T00:00:00"""); + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_ЗаписьJsonОтбрасываетДробнуюЧасть() Экспорт + + Данные = Новый Структура; + Данные.Вставить("Дробная", БазоваяДата() + 0.123); + + Запись = Новый ЗаписьJSON(); + Запись.УстановитьСтроку(); + ЗаписатьJSON(Запись, Данные); + Текст = Запись.Закрыть(); + + юТест.ПроверитьВхождение(Текст, """Дробная"": ""2026-02-01T15:30:45"""); + юТест.ПроверитьЛожь(СтрНайти(Текст, ".123") > 0); + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_ЗаписьJsonИгнорируетKind() Экспорт + + Данные = Новый Структура; + Данные.Вставить("ТекущаяДата", БезДробнойЧасти(ТекущаяДата())); + Данные.Вставить("УниверсальнаяДата", БезДробнойЧасти(ТекущаяУниверсальнаяДата())); + + Запись = Новый ЗаписьJSON(); + Запись.УстановитьСтроку(); + ЗаписатьJSON(Запись, Данные); + Текст = Запись.Закрыть(); + + юТест.ПроверитьЛожь(СтрНайти(Текст, "Z") > 0); + юТест.ПроверитьЛожь(СтрНайти(Текст, "+") > 0); + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_ЧтениеJsonБезИменСвойствДат() Экспорт + + Чтение = Новый ЧтениеJSON(); + Чтение.УстановитьСтроку(ТекстJsonДляЧтенияДат()); + Данные = ПрочитатьJSON(Чтение); + Чтение.Закрыть(); + + юТест.ПроверитьРавенство(Тип("Строка"), ТипЗнч(Данные.a)); + юТест.ПроверитьРавенство(Тип("Строка"), ТипЗнч(Данные.b)); + юТест.ПроверитьРавенство(Тип("Строка"), ТипЗнч(Данные.c)); + юТест.ПроверитьРавенство(Тип("Строка"), ТипЗнч(Данные.d)); + юТест.ПроверитьРавенство(Тип("Строка"), ТипЗнч(Данные.e)); + + юТест.ПроверитьРавенство("2026-02-01T15:30:45", Данные.a); + юТест.ПроверитьРавенство("2026-02-01T15:30:45.123", Данные.b); + юТест.ПроверитьРавенство("2026-02-01T15:30:45Z", Данные.c); + юТест.ПроверитьРавенство("2026-02-01T15:30:45+03:00", Данные.d); + юТест.ПроверитьРавенство("2026-02-01T15:30:45.1234567Z", Данные.e); + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_ЧтениеJsonСИменамиСвойствДат() Экспорт + + ИменаСвойств = Новый Массив; + ИменаСвойств.Добавить("a"); + ИменаСвойств.Добавить("b"); + ИменаСвойств.Добавить("c"); + ИменаСвойств.Добавить("d"); + ИменаСвойств.Добавить("e"); + + Чтение = Новый ЧтениеJSON(); + Чтение.УстановитьСтроку(ТекстJsonДляЧтенияДат()); + Данные = ПрочитатьJSON(Чтение, Ложь, ИменаСвойств, ФорматДатыJSON.ISO); + Чтение.Закрыть(); + + ОжиданиеC = ОжидаемаяЛокальнаяДатаИзUtc(2026, 2, 1, 15, 30, 45); + ОжиданиеD = ОжидаемаяЛокальнаяДатаИзUtc(2026, 2, 1, 12, 30, 45); + + юТест.ПроверитьРавенство(Тип("Дата"), ТипЗнч(Данные.a)); + юТест.ПроверитьРавенство(Тип("Дата"), ТипЗнч(Данные.b)); + юТест.ПроверитьРавенство(Тип("Дата"), ТипЗнч(Данные.c)); + юТест.ПроверитьРавенство(Тип("Дата"), ТипЗнч(Данные.d)); + юТест.ПроверитьРавенство(Тип("Дата"), ТипЗнч(Данные.e)); + + юТест.ПроверитьРавенство(Дата(2026, 2, 1, 15, 30, 45), Данные.a); + юТест.ПроверитьРавенство(Дата(2026, 2, 1, 15, 30, 45), Данные.b); + юТест.ПроверитьРавенство(ОжиданиеC, Данные.c); + юТест.ПроверитьРавенство(ОжиданиеD, Данные.d); + юТест.ПроверитьРавенство(ОжиданиеC, Данные.e); + + юТест.ПроверитьИстину(Данные.a = БезДробнойЧасти(Данные.a)); + юТест.ПроверитьИстину(Данные.b = БезДробнойЧасти(Данные.b)); + юТест.ПроверитьИстину(Данные.c = БезДробнойЧасти(Данные.c)); + юТест.ПроверитьИстину(Данные.d = БезДробнойЧасти(Данные.d)); + юТест.ПроверитьИстину(Данные.e = БезДробнойЧасти(Данные.e)); + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_ПрочитатьДатуJsonОтбрасываетДробнуюЧасть() Экспорт + + Результат = ПрочитатьДатуJSON("2026-02-01T15:30:45.123", ФорматДатыJSON.ISO); + + юТест.ПроверитьРавенство(Дата(2026, 2, 1, 15, 30, 45), Результат); + юТест.ПроверитьИстину(Результат = БезДробнойЧасти(Результат)); + +КонецПроцедуры diff --git a/tests/json/test-json_date.os b/tests/json/test-json_date.os new file mode 100644 index 000000000..ddd005e67 --- /dev/null +++ b/tests/json/test-json_date.os @@ -0,0 +1,443 @@ +#Использовать asserts + +Перем юТест; + +Функция ПолучитьСписокТестов(Тестирование) Экспорт + + юТест = Тестирование; + + СписокТестов = Новый Массив; + СписокТестов.Добавить("Тест_ДолженЗаписатьДатуJSONВФорматеISOКакЛокальнуюДатуПоУмолчанию"); + СписокТестов.Добавить("Тест_ДолженЗаписатьДатуJSONВФорматеISOКакЯвнуюЛокальнуюДату"); + СписокТестов.Добавить("Тест_ДолженЗаписатьДатуJSONВФорматеISOКакЛокальнуюДатуСоСмещением"); + СписокТестов.Добавить("Тест_ДолженЗаписатьДатуJSONВФорматеISOКакУниверсальнуюДату"); + СписокТестов.Добавить("Тест_ДолженЗаписатьДатуJSONВФорматеJavaScriptКакУниверсальнуюДату"); + СписокТестов.Добавить("Тест_ДолженЗаписатьОбычнуюДатуJSONВФорматеJavaScriptКакУниверсальнуюДату"); + СписокТестов.Добавить("Тест_ДолженЗаписатьДатуJSONВФорматеMicrosoftКакУниверсальнуюДату"); + СписокТестов.Добавить("Тест_ДолженЗаписатьОбычнуюДатуJSONВФорматеMicrosoftКакУниверсальнуюДату"); + СписокТестов.Добавить("Тест_ДолженЗаписатьUnixEpochJSON"); + СписокТестов.Добавить("Тест_ДолженЗаписатьМинимальнуюДатуJSONКакВ1С"); + СписокТестов.Добавить("Тест_ДолженЗаписатьДатыРядомСМинимальнойJSON"); + СписокТестов.Добавить("Тест_ДолженЗаписатьМаксимальнуюДатуJSONКакВ1С"); + СписокТестов.Добавить("Тест_ДолженВызватьИсключениеЕслиДатаИмеетНеверныйТип"); + СписокТестов.Добавить("Тест_ДолженВызватьИсключениеЕслиФорматИмеетНеверныйТип"); + СписокТестов.Добавить("Тест_ДолженВызватьИсключениеЕслиВариантЗаписиИмеетНеверныйТип"); + СписокТестов.Добавить("Тест_ДолженВызватьИсключениеДляФорматаJavaScriptПоУмолчанию"); + СписокТестов.Добавить("Тест_ДолженВызватьИсключениеДляФорматаJavaScriptБезУниверсальнойДаты"); + СписокТестов.Добавить("Тест_ДолженВызватьИсключениеДляФорматаMicrosoftПоУмолчанию"); + СписокТестов.Добавить("Тест_ДолженВызватьИсключениеДляФорматаMicrosoftБезУниверсальнойДаты"); + + Возврат СписокТестов; + +КонецФункции + +Процедура Тест_ДолженЗаписатьДатуJSONВФорматеISOКакЛокальнуюДатуПоУмолчанию() Экспорт + + // Дано + ЗначениеДаты = Дата("20140510131415"); + + // Когда + Результат = ЗаписатьДатуJSON(ЗначениеДаты, ФорматДатыJSON.ISO); + + // Тогда + юТест.ПроверитьРавенство("2014-05-10T13:14:15", Результат); + +КонецПроцедуры + +Процедура Тест_ДолженЗаписатьДатуJSONВФорматеISOКакЯвнуюЛокальнуюДату() Экспорт + + // Дано + ЗначениеДаты = Дата("20140510131415"); + + // Когда + Результат = ЗаписатьДатуJSON(ЗначениеДаты, ФорматДатыJSON.ISO, ВариантЗаписиДатыJSON.ЛокальнаяДата); + + // Тогда + юТест.ПроверитьРавенство("2014-05-10T13:14:15", Результат); + +КонецПроцедуры + +Процедура Тест_ДолженЗаписатьДатуJSONВФорматеISOКакЛокальнуюДатуСоСмещением() Экспорт + + // Дано + ЗначениеДаты = Дата("20140510131415"); + ОжидаемыйРезультат = "2014-05-10T13:14:15" + СмещениеТекущейЗоныДляДаты(ЗначениеДаты); + + // Когда + Результат = ЗаписатьДатуJSON(ЗначениеДаты, ФорматДатыJSON.ISO, ВариантЗаписиДатыJSON.ЛокальнаяДатаСоСмещением); + + // Тогда + юТест.ПроверитьРавенство(ОжидаемыйРезультат, Результат); + +КонецПроцедуры + +Процедура Тест_ДолженЗаписатьДатуJSONВФорматеISOКакУниверсальнуюДату() Экспорт + + // Дано + ДатаUTC = Дата("20140510091415"); + ЗначениеДаты = МестноеВремя(ДатаUTC); + + // Когда + Результат = ЗаписатьДатуJSON(ЗначениеДаты, ФорматДатыJSON.ISO, ВариантЗаписиДатыJSON.УниверсальнаяДата); + + // Тогда + юТест.ПроверитьРавенство("2014-05-10T09:14:15Z", Результат); + +КонецПроцедуры + +Процедура Тест_ДолженЗаписатьДатуJSONВФорматеJavaScriptКакУниверсальнуюДату() Экспорт + + // Дано + ЗначениеДаты = МестноеВремя(Дата("19700101000001")); + + // Когда + Результат = ЗаписатьДатуJSON(ЗначениеДаты, ФорматДатыJSON.JavaScript, ВариантЗаписиДатыJSON.УниверсальнаяДата); + + // Тогда + юТест.ПроверитьРавенство("new Date(1000)", Результат); + +КонецПроцедуры + +Процедура Тест_ДолженЗаписатьОбычнуюДатуJSONВФорматеJavaScriptКакУниверсальнуюДату() Экспорт + + // Дано + ЗначениеДаты = МестноеВремя(Дата("20140510091415")); + + // Когда + Результат = ЗаписатьДатуJSON(ЗначениеДаты, ФорматДатыJSON.JavaScript, ВариантЗаписиДатыJSON.УниверсальнаяДата); + + // Тогда + юТест.ПроверитьРавенство("new Date(1399713255000)", Результат); + +КонецПроцедуры + +Процедура Тест_ДолженЗаписатьДатуJSONВФорматеMicrosoftКакУниверсальнуюДату() Экспорт + + // Дано + ЗначениеДаты = МестноеВремя(Дата("19700101000001")); + + // Когда + Результат = ЗаписатьДатуJSON(ЗначениеДаты, ФорматДатыJSON.Microsoft, ВариантЗаписиДатыJSON.УниверсальнаяДата); + + // Тогда + юТест.ПроверитьРавенство("/Date(1000)/", Результат); + +КонецПроцедуры + +Процедура Тест_ДолженЗаписатьОбычнуюДатуJSONВФорматеMicrosoftКакУниверсальнуюДату() Экспорт + + // Дано + ЗначениеДаты = МестноеВремя(Дата("20140510091415")); + + // Когда + Результат = ЗаписатьДатуJSON(ЗначениеДаты, ФорматДатыJSON.Microsoft, ВариантЗаписиДатыJSON.УниверсальнаяДата); + + // Тогда + юТест.ПроверитьРавенство("/Date(1399713255000)/", Результат); + +КонецПроцедуры + +Процедура Тест_ДолженЗаписатьUnixEpochJSON() Экспорт + + // Дано + ЗначениеДаты = МестноеВремя(Дата("19700101000000")); + + // Когда + РезультатJavaScript = ЗаписатьДатуJSON(ЗначениеДаты, ФорматДатыJSON.JavaScript, ВариантЗаписиДатыJSON.УниверсальнаяДата); + РезультатMicrosoft = ЗаписатьДатуJSON(ЗначениеДаты, ФорматДатыJSON.Microsoft, ВариантЗаписиДатыJSON.УниверсальнаяДата); + + // Тогда + юТест.ПроверитьРавенство("new Date(0)", РезультатJavaScript); + юТест.ПроверитьРавенство("/Date(0)/", РезультатMicrosoft); + +КонецПроцедуры + +Процедура Тест_ДолженЗаписатьМинимальнуюДатуJSONКакВ1С() Экспорт + + // Дано + МинимальнаяДата = Дата(1, 1, 1, 0, 0, 0); + + // Когда + РезультатISOКакЛокальнаяДата = ЗаписатьДатуJSON( + МинимальнаяДата, ФорматДатыJSON.ISO, ВариантЗаписиДатыJSON.ЛокальнаяДата); + РезультатISOСоСмещением = ЗаписатьДатуJSON( + МинимальнаяДата, ФорматДатыJSON.ISO, ВариантЗаписиДатыJSON.ЛокальнаяДатаСоСмещением); + РезультатISOКакУниверсальнаяДата = ЗаписатьДатуJSON( + МинимальнаяДата, ФорматДатыJSON.ISO, ВариантЗаписиДатыJSON.УниверсальнаяДата); + РезультатJavaScript = ЗаписатьДатуJSON( + МинимальнаяДата, ФорматДатыJSON.JavaScript, ВариантЗаписиДатыJSON.УниверсальнаяДата); + РезультатMicrosoft = ЗаписатьДатуJSON( + МинимальнаяДата, ФорматДатыJSON.Microsoft, ВариантЗаписиДатыJSON.УниверсальнаяДата); + + // Тогда + юТест.ПроверитьРавенство("0001-01-01T00:00:00", РезультатISOКакЛокальнаяДата); + юТест.ПроверитьРавенство("0001-01-01T00:00:00+00:00", РезультатISOСоСмещением); + юТест.ПроверитьРавенство("0001-01-01T00:00:00Z", РезультатISOКакУниверсальнаяДата); + юТест.ПроверитьРавенство("new Date(-62135596800000)", РезультатJavaScript); + юТест.ПроверитьРавенство("/Date(-62135596800000)/", РезультатMicrosoft); + +КонецПроцедуры + +Процедура Тест_ДолженЗаписатьДатыРядомСМинимальнойJSON() Экспорт + + // Дано + ДатаСОднойСекундой = Дата(1, 1, 1, 0, 0, 1); + ДатаСОднимЧасом = Дата(1, 1, 1, 1, 0, 0); + ДатаСТремяЧасами = Дата(1, 1, 1, 3, 0, 0); + СмещениеОколоМинимальнойДаты = СмещениеТекущейЗоныДляДаты(Дата(1, 1, 2, 0, 0, 0)); + + // Когда + ЛокальнаяДатаСОднойСекундой = ЗаписатьДатуJSON( + ДатаСОднойСекундой, ФорматДатыJSON.ISO, ВариантЗаписиДатыJSON.ЛокальнаяДата); + ЛокальнаяДатаСОднимЧасом = ЗаписатьДатуJSON( + ДатаСОднимЧасом, ФорматДатыJSON.ISO, ВариантЗаписиДатыJSON.ЛокальнаяДата); + ЛокальнаяДатаСТремяЧасами = ЗаписатьДатуJSON( + ДатаСТремяЧасами, ФорматДатыJSON.ISO, ВариантЗаписиДатыJSON.ЛокальнаяДата); + ДатаСоСмещениемСОднойСекундой = ЗаписатьДатуJSON( + ДатаСОднойСекундой, ФорматДатыJSON.ISO, ВариантЗаписиДатыJSON.ЛокальнаяДатаСоСмещением); + ДатаСоСмещениемСОднимЧасом = ЗаписатьДатуJSON( + ДатаСОднимЧасом, ФорматДатыJSON.ISO, ВариантЗаписиДатыJSON.ЛокальнаяДатаСоСмещением); + ДатаСоСмещениемСТремяЧасами = ЗаписатьДатуJSON( + ДатаСТремяЧасами, ФорматДатыJSON.ISO, ВариантЗаписиДатыJSON.ЛокальнаяДатаСоСмещением); + УниверсальнаяДатаСОднойСекундой = ЗаписатьДатуJSON( + ДатаСОднойСекундой, ФорматДатыJSON.ISO, ВариантЗаписиДатыJSON.УниверсальнаяДата); + УниверсальнаяДатаСОднимЧасом = ЗаписатьДатуJSON( + ДатаСОднимЧасом, ФорматДатыJSON.ISO, ВариантЗаписиДатыJSON.УниверсальнаяДата); + УниверсальнаяДатаСТремяЧасами = ЗаписатьДатуJSON( + ДатаСТремяЧасами, ФорматДатыJSON.ISO, ВариантЗаписиДатыJSON.УниверсальнаяДата); + + // Тогда + юТест.ПроверитьРавенство("0001-01-01T00:00:01", ЛокальнаяДатаСОднойСекундой); + юТест.ПроверитьРавенство("0001-01-01T01:00:00", ЛокальнаяДатаСОднимЧасом); + юТест.ПроверитьРавенство("0001-01-01T03:00:00", ЛокальнаяДатаСТремяЧасами); + юТест.ПроверитьРавенство("0001-01-01T00:00:01" + СмещениеОколоМинимальнойДаты, + ДатаСоСмещениемСОднойСекундой); + юТест.ПроверитьРавенство("0001-01-01T01:00:00" + СмещениеОколоМинимальнойДаты, + ДатаСоСмещениемСОднимЧасом); + юТест.ПроверитьРавенство("0001-01-01T03:00:00" + СмещениеОколоМинимальнойДаты, + ДатаСоСмещениемСТремяЧасами); + юТест.ПроверитьРавенство(ФорматISO(УниверсальноеВремя(ДатаСОднойСекундой)) + "Z", + УниверсальнаяДатаСОднойСекундой); + юТест.ПроверитьРавенство(ФорматISO(УниверсальноеВремя(ДатаСОднимЧасом)) + "Z", + УниверсальнаяДатаСОднимЧасом); + юТест.ПроверитьРавенство(ФорматISO(УниверсальноеВремя(ДатаСТремяЧасами)) + "Z", + УниверсальнаяДатаСТремяЧасами); + +КонецПроцедуры + +Процедура Тест_ДолженЗаписатьМаксимальнуюДатуJSONКакВ1С() Экспорт + + // Дано + МаксимальнаяДата = Дата(3999, 12, 31, 23, 59, 59); + ОжидаемаяДатаUTC = УниверсальноеВремя(МаксимальнаяДата); + + // Когда + Результат = ЗаписатьДатуJSON(МаксимальнаяДата, ФорматДатыJSON.ISO, ВариантЗаписиДатыJSON.УниверсальнаяДата); + + // Тогда + юТест.ПроверитьРавенство(ФорматISO(ОжидаемаяДатаUTC) + "Z", Результат); + +КонецПроцедуры + +Процедура Тест_ДолженВызватьИсключениеЕслиДатаИмеетНеверныйТип() Экспорт + + // Дано + Значения = Новый Массив; + Значения.Добавить("20140510131415"); + Значения.Добавить(1); + Значения.Добавить(Истина); + Значения.Добавить(Неопределено); + Значения.Добавить(Null); + Значения.Добавить(Новый Массив); + Значения.Добавить(Новый Структура); + + // Когда + ОписанияОшибок = Новый Массив; + Для Каждого Значение Из Значения Цикл + Попытка + ЗаписатьДатуJSON(Значение, ФорматДатыJSON.ISO); + Исключение + ОписанияОшибок.Добавить(ОписаниеОшибки()); + Продолжить; + КонецПопытки; + + ВызватьИсключение "Для первого параметра недопустимого типа должно быть вызвано исключение"; + КонецЦикла; + + // Тогда + ПроверитьОписанияОшибокСодержат(ОписанияОшибок, "Неверный тип аргумента номер 1"); + +КонецПроцедуры + +Процедура Тест_ДолженВызватьИсключениеЕслиФорматИмеетНеверныйТип() Экспорт + + // Дано + Значения = ЗначенияНеприводимыеКПеречислению(); + + // Когда + ОписанияОшибок = Новый Массив; + Для Каждого Значение Из Значения Цикл + Попытка + ЗаписатьДатуJSON(Дата("20140510131415"), Значение); + Исключение + ОписанияОшибок.Добавить(ОписаниеОшибки()); + Продолжить; + КонецПопытки; + + ВызватьИсключение "Для второго параметра недопустимого типа должно быть вызвано исключение"; + КонецЦикла; + + // Тогда + ПроверитьОписанияОшибокСодержат(ОписанияОшибок, "Неверный тип аргумента"); + +КонецПроцедуры + +Процедура Тест_ДолженВызватьИсключениеЕслиВариантЗаписиИмеетНеверныйТип() Экспорт + + // Дано + Значения = ЗначенияНеприводимыеКПеречислению(); + + // Когда + ОписанияОшибок = Новый Массив; + Для Каждого Значение Из Значения Цикл + Попытка + ЗаписатьДатуJSON(Дата("20140510131415"), ФорматДатыJSON.ISO, Значение); + Исключение + ОписанияОшибок.Добавить(ОписаниеОшибки()); + Продолжить; + КонецПопытки; + + ВызватьИсключение "Для третьего параметра недопустимого типа должно быть вызвано исключение"; + КонецЦикла; + + // Тогда + ПроверитьОписанияОшибокСодержат(ОписанияОшибок, "Неверный тип аргумента"); + +КонецПроцедуры + +Процедура Тест_ДолженВызватьИсключениеДляФорматаJavaScriptПоУмолчанию() Экспорт + + // Дано + Формат = ФорматДатыJSON.JavaScript; + + // Когда + ОписаниеОшибки = ОписаниеИсключенияЗаписиДатыJSON(Формат); + + // Тогда + юТест.ПроверитьВхождение(ОписаниеОшибки, "Невозможно сохранить локальную дату в формате JavaScript"); + +КонецПроцедуры + +Процедура Тест_ДолженВызватьИсключениеДляФорматаJavaScriptБезУниверсальнойДаты() Экспорт + + // Дано + Формат = ФорматДатыJSON.JavaScript; + Варианты = ВариантыЗаписиЛокальнойДатыJSON(); + + // Когда + ОписанияОшибок = ОписанияИсключенийЗаписиДатыJSON(Формат, Варианты); + + // Тогда + ПроверитьОписанияОшибокСодержат(ОписанияОшибок, "Невозможно сохранить локальную дату в формате JavaScript"); + +КонецПроцедуры + +Процедура Тест_ДолженВызватьИсключениеДляФорматаMicrosoftПоУмолчанию() Экспорт + + // Дано + Формат = ФорматДатыJSON.Microsoft; + + // Когда + ОписаниеОшибки = ОписаниеИсключенияЗаписиДатыJSON(Формат); + + // Тогда + юТест.ПроверитьВхождение(ОписаниеОшибки, "Невозможно сохранить локальную дату в формате Microsoft"); + +КонецПроцедуры + +Процедура Тест_ДолженВызватьИсключениеДляФорматаMicrosoftБезУниверсальнойДаты() Экспорт + + // Дано + Формат = ФорматДатыJSON.Microsoft; + Варианты = ВариантыЗаписиЛокальнойДатыJSON(); + + // Когда + ОписанияОшибок = ОписанияИсключенийЗаписиДатыJSON(Формат, Варианты); + + // Тогда + ПроверитьОписанияОшибокСодержат(ОписанияОшибок, "Невозможно сохранить локальную дату в формате Microsoft"); + +КонецПроцедуры + +Функция ФорматISO(Дата) + Если Дата = Дата(1, 1, 1, 0, 0, 0) Тогда + Возврат "0001-01-01T00:00:00"; + КонецЕсли; + + Возврат Формат(Дата, "ДФ=yyyy-MM-ddTHH:mm:ss"); +КонецФункции + +Функция СмещениеТекущейЗоныДляДаты(Дата) + СмещениеВСекундах = Дата - УниверсальноеВремя(Дата); + Знак = ?(СмещениеВСекундах < 0, "-", "+"); + ВсегоСекунд = ?(СмещениеВСекундах < 0, -СмещениеВСекундах, СмещениеВСекундах); + Часы = Цел(ВсегоСекунд / 3600); + Минуты = Цел((ВсегоСекунд - Часы * 3600) / 60); + ЧасыСтрокой = Формат(Часы, "ЧГ=0; ЧН=0"); + МинутыСтрокой = Формат(Минуты, "ЧГ=0; ЧН=0"); + + Если СтрДлина(ЧасыСтрокой) = 1 Тогда + ЧасыСтрокой = "0" + ЧасыСтрокой; + КонецЕсли; + + Если СтрДлина(МинутыСтрокой) = 1 Тогда + МинутыСтрокой = "0" + МинутыСтрокой; + КонецЕсли; + + Возврат Знак + ЧасыСтрокой + ":" + МинутыСтрокой; +КонецФункции + +Функция ЗначенияНеприводимыеКПеречислению() + Значения = Новый Массив; + Значения.Добавить(Дата("20140510131415")); + Значения.Добавить(Новый Массив); + Значения.Добавить(Новый Структура); + Возврат Значения; +КонецФункции + +Функция ВариантыЗаписиЛокальнойДатыJSON() + Варианты = Новый Массив; + Варианты.Добавить(ВариантЗаписиДатыJSON.ЛокальнаяДата); + Варианты.Добавить(ВариантЗаписиДатыJSON.ЛокальнаяДатаСоСмещением); + Возврат Варианты; +КонецФункции + +Функция ОписанияИсключенийЗаписиДатыJSON(Формат, Варианты) + ОписанияОшибок = Новый Массив; + Для Каждого Вариант Из Варианты Цикл + ОписанияОшибок.Добавить(ОписаниеИсключенияЗаписиДатыJSON(Формат, Вариант)); + КонецЦикла; + Возврат ОписанияОшибок; +КонецФункции + +Функция ОписаниеИсключенияЗаписиДатыJSON(Формат, Вариант = Неопределено) + Попытка + Если Вариант = Неопределено Тогда + ЗаписатьДатуJSON(Дата("20140510131415"), Формат); + Иначе + ЗаписатьДатуJSON(Дата("20140510131415"), Формат, Вариант); + КонецЕсли; + Исключение + Возврат ОписаниеОшибки(); + КонецПопытки; + + ВызватьИсключение "Для указанного формата и варианта записи даты должно быть вызвано исключение"; +КонецФункции + +Процедура ПроверитьОписанияОшибокСодержат(ОписанияОшибок, ТекстОшибки) + + Для Каждого ОписаниеОшибки Из ОписанияОшибок Цикл + юТест.ПроверитьВхождение(ОписаниеОшибки, ТекстОшибки); + КонецЦикла; + +КонецПроцедуры diff --git a/tests/json/test-json_reader.os b/tests/json/test-json_reader.os index 3a49b33ab..61c1a8166 100644 --- a/tests/json/test-json_reader.os +++ b/tests/json/test-json_reader.os @@ -33,7 +33,8 @@ СписокТестов.Добавить("Тест_Должен_ЧитатьСтрокуСДатойВФорматеISOКакСтроку"); СписокТестов.Добавить("Тест_Должен_ОткрытьПотокЧитаетСтрокуСДатойВФорматеISOКакСтроку"); СписокТестов.Добавить("Тест_Должен_ОткрытьФайлЧитаетСтрокуСДатойВФорматеISOКакСтроку"); - СписокТестов.Добавить("Тест_Должен_ЧитатьДатуВФорматеISOКакСтрокуПриИспользованииПрочитатьJSON"); + СписокТестов.Добавить("Тест_Должен_ЧитатьДатуВФорматеISOКакСтрокуПриИспользованииПрочитатьJSON"); + СписокТестов.Добавить("Тест_Должен_ЧитатьДатуВФорматеISOПриИспользованииПрочитатьJSONСИменамиСвойствДат"); Возврат СписокТестов; @@ -333,6 +334,26 @@ КонецПроцедуры +Процедура Тест_Должен_ЧитатьДатуВФорматеISOПриИспользованииПрочитатьJSONСИменамиСвойствДат() Экспорт + + СтрокаJSON = "{ ""lastSignInTime"": ""2026-05-13T22:51:46.230Z"" }"; + + ИменаСвойств = Новый Массив; + ИменаСвойств.Добавить("lastSignInTime"); + + ЧтениеJSON = Новый ЧтениеJSON; + ЧтениеJSON.УстановитьСтроку(СтрокаJSON); + Объект = ПрочитатьJSON(ЧтениеJSON, Ложь, ИменаСвойств, ФорматДатыJSON.ISO); + ЧтениеJSON.Закрыть(); + + Ожидание = МестноеВремя(Дата(2026, 5, 13, 22, 51, 46)); + + юТест.ПроверитьРавенство(Тип("Дата"), ТипЗнч(Объект.lastSignInTime)); + юТест.ПроверитьРавенство(Ожидание, Объект.lastSignInTime); + юТест.ПроверитьРавенство(0, Объект.lastSignInTime - Дата(Год(Объект.lastSignInTime), Месяц(Объект.lastSignInTime), День(Объект.lastSignInTime), Час(Объект.lastSignInTime), Минута(Объект.lastSignInTime), Секунда(Объект.lastSignInTime))); + +КонецПроцедуры + Процедура Тест_Должен_ОткрытьПотокЧитаетСтрокуСДатойВФорматеISOКакСтроку() Экспорт diff --git a/tests/json/test-json_writer.os b/tests/json/test-json_writer.os index 927a7c186..d5c6bb1fd 100644 --- a/tests/json/test-json_writer.os +++ b/tests/json/test-json_writer.os @@ -9,8 +9,13 @@ СписокТестов = Новый Массив; + СписокТестов.Добавить("Тест_Должен_ПроверитьПараметрыЗаписиJSONПоУмолчанию"); + СписокТестов.Добавить("Тест_Должен_ПроверитьУстановленныеПараметрыПоУмолчанию"); СписокТестов.Добавить("Тест_Должен_СверитьСтрокуСоСпецСимволами"); - СписокТестов.Добавить("Тест_Должен_СверитьСтрокуЭкранированнымиКириллическимиСимволами"); + СписокТестов.Добавить("Тест_Должен_СверитьСтрокуССимволамиВнеBMP"); + СписокТестов.Добавить("Тест_Должен_СверитьСтрокуСЭкранированнымиКириллическимиСимволами"); + СписокТестов.Добавить("Тест_Должен_СверитьСтрокуСЭкранированнымиСимволами_БезПараметров"); + СписокТестов.Добавить("Тест_Должен_СверитьСтрокуСЭкранированнымиСимволами_СПараметрами"); СписокТестов.Добавить("Тест_Должен_ПроверитьИсключенияДляНедопустимыхТипов"); СписокТестов.Добавить("Тест_Должен_СверитьХешСуммуСгенерированнойСтрокиJSON"); @@ -18,6 +23,9 @@ СписокТестов.Добавить("Тест_Должен_СверитьХешСуммуСгенерированнойМетодомЗаписатьJSONСтруктурыСФиксированнымиКоллекциями"); СписокТестов.Добавить("Тест_Должен_ПроверитьПустуюСтрокуПриЗакрытии"); СписокТестов.Добавить("Тест_Должен_ПроверитьСохранениеУстановленнойСтроки"); + СписокТестов.Добавить("Тест_Должен_ПроверитьЗаписьВПоток"); + СписокТестов.Добавить("Тест_Должен_ПроверитьСохранениеПотокаПослеЗакрытия"); + СписокТестов.Добавить("Тест_Должен_ПроверитьИсключениеНаПотокеТолькоДляЧтения"); Возврат СписокТестов; КонецФункции @@ -25,7 +33,10 @@ Процедура Тест_Должен_СверитьХешСуммуСгенерированнойСтрокиJSON() Экспорт Запись = Новый ЗаписьJson(); - Запись.УстановитьСтроку(Новый ПараметрыЗаписиJSON(ПереносСтрокJSON.Windows)); + Параметры = Новый ПараметрыЗаписиJSON(ПереносСтрокJSON.Windows); + юТест.ПроверитьРавенство(ПереносСтрокJSON.Windows, Параметры.ПереносСтрок, "ПереносСтрок.Windows"); + + Запись.УстановитьСтроку(Параметры); Запись.ЗаписатьНачалоОбъекта(); Запись.ЗаписатьИмяСвойства("firstName"); Запись.ЗаписатьЗначение("Иван"); @@ -55,7 +66,7 @@ Запись.ЗаписатьКонецОбъекта(); Текст = Запись.Закрыть(); - + Хеширование = Новый ХешированиеДанных(ХешФункция.CRC32); Хеширование.Добавить(Текст); @@ -63,29 +74,107 @@ КонецПроцедуры -Процедура Тест_Должен_СверитьСтрокуЭкранированнымиКириллическимиСимволами() Экспорт +Процедура Тест_Должен_СверитьСтрокуСЭкранированнымиКириллическимиСимволами() Экспорт -ЗаписьJSON = Новый ЗаписьJSON; -Параметры = Новый ПараметрыЗаписиJSON(ПереносСтрокJSON.Windows, , , ЭкранированиеСимволовJSON.СимволыВнеASCII); - -ЗаписьJSON.УстановитьСтроку(Параметры); + ЗаписьJSON = Новый ЗаписьJSON; + Параметры = Новый ПараметрыЗаписиJSON(ПереносСтрокJSON.Windows, , , ЭкранированиеСимволовJSON.СимволыВнеASCII); + + ЗаписьJSON.УстановитьСтроку(Параметры); -ЗаписатьJSON(ЗаписьJSON, "Символы кириллицы"); + ЗаписатьJSON(ЗаписьJSON, "Символы кириллицы"); -юТест.ПроверитьРавенство(ЗаписьJSON.Закрыть(), """\\u0421\\u0438\\u043c\\u0432\\u043e\\u043b\\u044b \\u043a\\u0438\\u0440\\u0438\\u043b\\u043b\\u0438\\u0446\\u044b"""); + юТест.ПроверитьРавенство(ЗаписьJSON.Закрыть(), """\u0421\u0438\u043C\u0432\u043E\u043B\u044B \u043A\u0438\u0440\u0438\u043B\u043B\u0438\u0446\u044B"""); КонецПроцедуры Процедура Тест_Должен_СверитьСтрокуСоСпецСимволами() Экспорт + + ЗаписьJSON = Новый ЗаписьJSON; + ЗаписьJSON.УстановитьСтроку(); + + ЗаписатьJSON(ЗаписьJSON, Символ(1) + Символ(31)); + юТест.ПроверитьРавенство(ЗаписьJSON.Закрыть(), """\u0001\u001F"""); + +КонецПроцедуры + +Процедура Тест_Должен_СверитьСтрокуССимволамиВнеBMP() Экспорт + + ЗаписьJSON = Новый ЗаписьJSON; + Параметры = Новый ПараметрыЗаписиJSON(, , , ЭкранированиеСимволовJSON.СимволыВнеBMP); + ЗаписьJSON.УстановитьСтроку(Параметры); + + ЗаписатьJSON(ЗаписьJSON, "Ключ: "+Символ(55348)+Символ(56606)); + юТест.ПроверитьРавенство(ЗаписьJSON.Закрыть(), """Ключ: \uD834\uDD1E"""); + +КонецПроцедуры + +Процедура Тест_Должен_ПроверитьПараметрыЗаписиJSONПоУмолчанию() Экспорт + Параметры = Новый ПараметрыЗаписиJSON; + + юТест.ПроверитьРавенство(ПереносСтрокJSON.Авто, Параметры.ПереносСтрок, "ПереносСтрок"); + юТест.ПроверитьРавенство("", Параметры.СимволыОтступа, "СимволыОтступа"); + юТест.ПроверитьИстину(Параметры.ИспользоватьДвойныеКавычки, "ИспользоватьДвойныеКавычки"); + юТест.ПроверитьРавенство( ЭкранированиеСимволовJSON.Нет, Параметры.ЭкранированиеСимволов, "ЭкранированиеСимволов"); + юТест.ПроверитьЛожь(Параметры.ЭкранироватьУгловыеСкобки, "ЭкранироватьУгловыеСкобки"); + юТест.ПроверитьИстину(Параметры.ЭкранироватьРазделителиСтрок, "ЭкранироватьРазделителиСтрок"); + юТест.ПроверитьЛожь(Параметры.ЭкранироватьАмперсанд, "ЭкранироватьАмперсанд"); + юТест.ПроверитьЛожь(Параметры.ЭкранироватьОдинарныеКавычки, "ЭкранироватьОдинарныеКавычки"); + юТест.ПроверитьЛожь(Параметры.ЭкранироватьСлеш, "ЭкранироватьСлеш"); + +КонецПроцедуры + +Процедура Тест_Должен_ПроверитьУстановленныеПараметрыПоУмолчанию() Экспорт + ЗаписьJSON = Новый ЗаписьJSON; + Параметры = ЗаписьJSON.Параметры; + ЗаписьJSON.Закрыть(); + + юТест.ПроверитьРавенство(ПереносСтрокJSON.Авто, Параметры.ПереносСтрок, "ПереносСтрок"); + юТест.ПроверитьРавенство("", Параметры.СимволыОтступа, "СимволыОтступа"); + юТест.ПроверитьИстину(Параметры.ИспользоватьДвойныеКавычки, "ИспользоватьДвойныеКавычки"); + юТест.ПроверитьРавенство( ЭкранированиеСимволовJSON.Нет, Параметры.ЭкранированиеСимволов, "ЭкранированиеСимволов"); + юТест.ПроверитьЛожь(Параметры.ЭкранироватьУгловыеСкобки, "ЭкранироватьУгловыеСкобки"); + юТест.ПроверитьИстину(Параметры.ЭкранироватьРазделителиСтрок, "ЭкранироватьРазделителиСтрок"); + юТест.ПроверитьЛожь(Параметры.ЭкранироватьАмперсанд, "ЭкранироватьАмперсанд"); + юТест.ПроверитьЛожь(Параметры.ЭкранироватьОдинарныеКавычки, "ЭкранироватьОдинарныеКавычки"); + юТест.ПроверитьЛожь(Параметры.ЭкранироватьСлеш, "ЭкранироватьСлеш"); + +КонецПроцедуры + +Процедура Тест_Должен_СверитьСтрокуСЭкранированнымиСимволами_БезПараметров() Экспорт + ЗаписьJSON = Новый ЗаписьJSON; + ЗаписьJSON.УстановитьСтроку(); -ЗаписьJSON = Новый ЗаписьJSON; -ЗаписьJSON.УстановитьСтроку(); + ЗаписьJSON.ЗаписатьЗначение("""\<&>/'"+Символ(13)+Символ(10)+Символ(12)+Символ(8)+Символ(9)); + Рез = ЗаписьJSON.Закрыть(); -ЗаписатьJSON(ЗаписьJSON, Символ(1) + Символ(31)); -юТест.ПроверитьРавенство(ЗаписьJSON.Закрыть(), """\u0001\u001f"""); + юТест.ПроверитьРавенство("""\""\\<&>/'\r\n\f\b\t""", Рез); КонецПроцедуры +Процедура Тест_Должен_СверитьСтрокуСЭкранированнымиСимволами_СПараметрами() Экспорт + Параметры = Новый ПараметрыЗаписиJSON( + , // ПереносСтрок + , // СимволыОтступа + Ложь, // ИспользоватьДвойныеКавычки + ЭкранированиеСимволовJSON.Нет, // ЭкранированиеСимволов + Истина, // ЭкранироватьУгловыеСкобки + Истина, // ЭкранироватьРазделителиСтрок + Истина, // ЭкранироватьАмперсанд + Истина, // ЭкранироватьОдинарныеКавычки + Истина // ЭкранироватьСлеш + ); + + ЗаписьJSON = Новый ЗаписьJSON; + ЗаписьJSON.УстановитьСтроку(Параметры); + + ЗаписьJSON.ЗаписатьЗначение("""\<&>/'"+Символ(8232)+Символ(8233)); + Рез = ЗаписьJSON.Закрыть(); + + юТест.ПроверитьРавенство("'\""\\\u003C\u0026\u003E\/\u0027\u2028\u2029'", Рез); + +КонецПроцедуры + + Процедура Тест_Должен_СверитьХешСуммуСгенерированнойМетодомЗаписатьJSONСтруктурыСФиксированнымиКоллекциями() Экспорт Запись = Новый ЗаписьJson(); @@ -207,3 +296,54 @@ Структура = Новый Структура(); ПопыткаЗаписатьЗначение(Структура); КонецПроцедуры + +Процедура Тест_Должен_ПроверитьЗаписьВПоток() Экспорт + Поток = Новый ПотокВПамяти(64); + + ЗаписьJSON = Новый ЗаписьJSON; + ЗаписьJSON.ОткрытьПоток(Поток); + ЗаписьJSON.ЗаписатьБезОбработки("123"); + РезЗаписи = ЗаписьJSON.Закрыть(); + РезПотока = Поток.ЗакрытьИПолучитьДвоичныеДанные(); + + юТест.ПроверитьРавенство("", РезЗаписи); + юТест.ПроверитьРавенство("31 32 33", Строка(РезПотока)); +КонецПроцедуры + +Процедура Тест_Должен_ПроверитьСохранениеПотокаПослеЗакрытия() Экспорт + Поток = Новый ПотокВПамяти(64); + Поток.Записать(ПолучитьБуферДвоичныхДанныхИзСтроки("01"),0,2); + + ЗаписьJSON = Новый ЗаписьJSON; + ЗаписьJSON.ОткрытьПоток(Поток); + ЗаписьJSON.ЗаписатьБезОбработки("23"); + ЗаписьJSON.УстановитьСтроку(); + ЗаписьJSON.ЗаписатьБезОбработки("45"); + РезЗаписи = ЗаписьJSON.Закрыть(); + + Поток.Записать(ПолучитьБуферДвоичныхДанныхИзСтроки("67"),0,2); + РезПотока = Поток.ЗакрытьИПолучитьДвоичныеДанные(); + + юТест.ПроверитьРавенство("45", РезЗаписи); + юТест.ПроверитьРавенство("30 31 32 33 36 37", Строка(РезПотока)); +КонецПроцедуры + +// отличается от 1С +Процедура Тест_Должен_ПроверитьИсключениеНаПотокеТолькоДляЧтения() Экспорт + Поток = Новый ПотокВПамяти(64); + ПотокЧтения = Поток.ПолучитьПотокТолькоДляЧтения(); + + ЗаписьJSON = Новый ЗаписьJSON; + Попытка + ЗаписьJSON.ОткрытьПоток(ПотокЧтения); // OneScript падает здесь + Исключение + Возврат; + КонецПопытки; + + ЗаписьJSON.ЗаписатьБезОбработки("123"); // 1C падает здесь + ЗаписьJSON.Закрыть(); + + ВызватьИсключение "Должно было быть выдано исключение, но его не было"; +КонецПроцедуры + + diff --git a/tests/native-api.os b/tests/native-api.os index a422d2b40..70f1955db 100644 --- a/tests/native-api.os +++ b/tests/native-api.os @@ -17,6 +17,8 @@ ВсеТесты.Добавить("ТестДолжен_ПроверитьПодключениеВнешнейКомпонентыDLL"); ВсеТесты.Добавить("ТестДолжен_ПроверитьСвойстваВнешнейКомпоненты"); ВсеТесты.Добавить("ТестДолжен_ПроверитьМетодыВнешнейКомпоненты"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьОбменПараметров"); + ВсеТесты.Добавить("ТестДолжен_ВыдержатьСборкуМусораПриКолбэкеОшибки"); #Если Windows Тогда ЭтоWindows = Истина; @@ -251,4 +253,65 @@ ЧтениеТекста = Новый ЧтениеТекста(Поток, КодировкаТекста.UTF8); юТест.ПроверитьРавенство(ЧтениеТекста.Прочитать(), СодержимоеФайла); +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьОбменПараметров() Экспорт + + Идентификатор = "AddinNativeDLL"; + КомпонентаПодключена = ПодключитьКомпонентуDLL(); + юТест.ПроверитьИстину(КомпонентаПодключена, "Компонента должна подключиться"); + + ИмяВнешнейКомпоненты = "AddIn." + Идентификатор + ".CAddInNative"; + ВнешняяКомпонента = Новый(ИмяВнешнейКомпоненты); + + // Обмен строковых out-параметров + А = "первый"; + Б = "второй"; + ВнешняяКомпонента.ОбменПараметров(А, Б); + юТест.ПроверитьРавенство(А, "второй"); + юТест.ПроверитьРавенство(Б, "первый"); + + // Обмен числовых out-параметров + ЧислоА = 1; + ЧислоБ = 2; + ВнешняяКомпонента.ОбменПараметров(ЧислоА, ЧислоБ); + юТест.ПроверитьРавенство(ЧислоА, 2); + юТест.ПроверитьРавенство(ЧислоБ, 1); + + // Возвращаемое значение функции не ломается обратным чтением параметров + Параметр = Ложь; + Результат = ВнешняяКомпонента.ПараметрПоУмолчанию(Параметр); + юТест.ПроверитьРавенство(Результат, Ложь); + юТест.ПроверитьРавенство(Параметр, Ложь); + + // Пропущенный параметр по умолчанию — без исключений + юТест.ПроверитьРавенство(ВнешняяКомпонента.ПараметрПоУмолчанию(), Истина); + +КонецПроцедуры + +Процедура ТестДолжен_ВыдержатьСборкуМусораПриКолбэкеОшибки() Экспорт + + // Регрессия issue #1359: нативный код хранит указатели на managed-делегаты колбэков. + // После GC без закрепления делегатов следующий AddError из компоненты роняет процесс. + Идентификатор = "AddinNativeDLL"; + КомпонентаПодключена = ПодключитьКомпонентуDLL(); + юТест.ПроверитьИстину(КомпонентаПодключена, "Компонента должна подключиться"); + + ИмяВнешнейКомпоненты = "AddIn." + Идентификатор + ".CAddInNative"; + ВнешняяКомпонента = Новый(ИмяВнешнейКомпоненты); + + КоличествоИтераций = 100; + Счетчик = 0; + Для Номер = 1 По КоличествоИтераций Цикл + ВыполнитьСборкуМусора(); + Попытка + // Неверный тип параметра → addError → колбэк OnError из нативного кода + ВнешняяКомпонента.Петля("строка вместо двоичных данных"); + Исключение + КонецПопытки; + Счетчик = Счетчик + 1; + КонецЦикла; + + юТест.ПроверитьРавенство(Счетчик, КоличествоИтераций); + КонецПроцедуры \ No newline at end of file diff --git a/tests/native-api/AddInNative.cpp b/tests/native-api/AddInNative.cpp index 32fb90e09..a5375b77a 100644 --- a/tests/native-api/AddInNative.cpp +++ b/tests/native-api/AddInNative.cpp @@ -30,6 +30,8 @@ static const wchar_t* g_PropNames[] = { L"StringRW", L"StringRO", L"StringWO", + L"DateRW", + L"FixedDate", }; static const wchar_t* g_PropNamesRu[] = { @@ -37,6 +39,8 @@ static const wchar_t* g_PropNamesRu[] = { L"СтрокаЧтениеЗапись", L"СтрокаТолькоЧтение", L"СтрокаТолькоЗапись", + L"ДатаЧтениеЗапись", + L"ФиксированнаяДата", }; static const wchar_t* g_MethodNames[] = { @@ -48,7 +52,9 @@ static const wchar_t* g_MethodNames[] = { L"ShowMessageBox", L"Exchange", L"Concatenate", - L"Loopback" + L"Loopback", + L"EchoDate", + L"SetDateOut" }; static const wchar_t* g_MethodNamesRu[] = { @@ -61,9 +67,11 @@ static const wchar_t* g_MethodNamesRu[] = { L"ОбменПараметров", L"КонкатенацияСтрок", L"Петля", + L"ЭхоДата", + L"УстановитьДатуИсходящий", }; -static const wchar_t g_kClassNames[] = L"CAddInNative"; //"|OtherClass1|OtherClass2"; +static const wchar_t g_kClassNames[] = L"CAddInNative|Alias2"; static IAddInDefBase* pAsyncEvent = NULL; uint32_t convToShortWchar(WCHAR_T** Dest, const wchar_t* Source, uint32_t len = 0); @@ -76,7 +84,13 @@ long GetClassObject(const WCHAR_T* wsName, IComponentBase** pInterface) { if (!*pInterface) { - *pInterface = new CAddInNative; + wchar_t* name = 0; + ::convFromShortWchar(&name, wsName); + if (wcscmp(name, L"Alias2") == 0) + *pInterface = new CAddInNativeSecond; + else + *pInterface = new CAddInNative; + delete[] name; return (long)*pInterface; } return 0; @@ -149,6 +163,22 @@ bool CAddInNative::RegisterExtensionAs(WCHAR_T** wsExtensionName) return false; } //---------------------------------------------------------------------------// +bool CAddInNativeSecond::RegisterExtensionAs(WCHAR_T** wsExtensionName) +{ + const wchar_t* wsExtension = L"CAddInNativeSecond"; + int iActualSize = ::wcslen(wsExtension) + 1; + WCHAR_T* dest = 0; + + if (m_iMemory) + { + if (m_iMemory->AllocMemory((void**)wsExtensionName, iActualSize * sizeof(WCHAR_T))) + ::convToShortWchar(wsExtensionName, wsExtension, iActualSize); + return true; + } + + return false; +} +//---------------------------------------------------------------------------// long CAddInNative::GetNProps() { // You may delete next lines and add your own implementation code here @@ -223,6 +253,14 @@ bool CAddInNative::GetPropVal(const long lPropNum, tVariant* pvarPropVal) } return false; } + case ePropDateRW: + TV_VT(pvarPropVal) = VTYPE_DATE; + TV_DATE(pvarPropVal) = m_Date; + break; + case ePropFixedDate: + TV_VT(pvarPropVal) = VTYPE_DATE; + TV_DATE(pvarPropVal) = 46037.5208333333; // 2026-01-15 12:30:00 + break; default: return false; } @@ -245,6 +283,11 @@ bool CAddInNative::SetPropVal(const long lPropNum, tVariant* pvarPropVal) return false; m_String = WcharWrapper(TV_WSTR(pvarPropVal)); break; + case ePropDateRW: + if (TV_VT(pvarPropVal) != VTYPE_DATE) + return false; + m_Date = TV_DATE(pvarPropVal); + break; default: return false; } @@ -259,6 +302,8 @@ bool CAddInNative::IsPropReadable(const long lPropNum) case ePropIsEnabled: case ePropStringRW: case ePropStringRO: + case ePropDateRW: + case ePropFixedDate: return true; default: return false; @@ -274,6 +319,7 @@ bool CAddInNative::IsPropWritable(const long lPropNum) case ePropIsEnabled: case ePropStringRW: case ePropStringWO: + case ePropDateRW: return true; default: return false; @@ -351,6 +397,10 @@ long CAddInNative::GetNParams(const long lMethodNum) return 2; case eMethLoopback: return 1; + case eMethEchoDate: + return 1; + case eMethSetDateOut: + return 1; default: return 0; } @@ -393,6 +443,7 @@ bool CAddInNative::HasRetVal(const long lMethodNum) case eMethLoadPicture: case eMethConcatenate: case eMethLoopback: + case eMethEchoDate: return true; default: return false; @@ -425,6 +476,11 @@ bool CAddInNative::CallAsProc(const long lMethodNum, memcpy(paParams, paParams + 1, sizeof(tVariant)); memcpy(paParams + 1, &variant, sizeof(tVariant)); break; + case eMethSetDateOut: + if (lSizeArray != 1 || TV_VT(paParams) != VTYPE_DATE) + return false; + TV_DATE(paParams) += 1.0; + break; case eMethShowMsgBox: { if (eAppCapabilities1 <= g_capabilities) @@ -524,6 +580,12 @@ bool CAddInNative::CallAsFunc(const long lMethodNum, pvarRetValue->strLen = paParams->strLen; return true; } + case eMethEchoDate: + if (lSizeArray != 1 || TV_VT(paParams) != VTYPE_DATE) + return false; + TV_VT(pvarRetValue) = VTYPE_DATE; + TV_DATE(pvarRetValue) = TV_DATE(paParams) + 1.0; + return true; break; case eMethLoadPicture: diff --git a/tests/native-api/AddInNative.h b/tests/native-api/AddInNative.h index a84cf6561..2ecbca300 100644 --- a/tests/native-api/AddInNative.h +++ b/tests/native-api/AddInNative.h @@ -17,6 +17,8 @@ class CAddInNative : public IComponentBase ePropStringRW, ePropStringRO, ePropStringWO, + ePropDateRW, + ePropFixedDate, ePropLast // Always last }; @@ -31,6 +33,8 @@ class CAddInNative : public IComponentBase eMethExchange, eMethConcatenate, eMethLoopback, + eMethEchoDate, + eMethSetDateOut, eMethLast // Always last }; @@ -64,10 +68,19 @@ class CAddInNative : public IComponentBase private: long findName(const wchar_t* names[], const wchar_t* name, const uint32_t size) const; void addError(uint32_t wcode, const wchar_t* source, const wchar_t* descriptor, long code); +protected: IAddInDefBase* m_iConnect; IMemoryManager* m_iMemory; bool m_Enabled = false; std::wstring m_String; + double m_Date = 0.0; +private: +}; + +class CAddInNativeSecond : public CAddInNative +{ +public: + bool ADDIN_API RegisterExtensionAs(WCHAR_T** wsExtensionName) override; }; class WcharWrapper diff --git a/tests/process.os b/tests/process.os index 44da651b5..722ef93e5 100644 --- a/tests/process.os +++ b/tests/process.os @@ -11,6 +11,7 @@ Если Найти(СИ.ВерсияОС, "Windows") > 0 Тогда ВсеТесты.Добавить("ТестДолжен_ПрочитатьВыводOscriptСразу"); ВсеТесты.Добавить("ТестДолжен_ПрочитатьВыводOscriptПострочно"); + ВсеТесты.Добавить("ТестДолжен_ПрочитатьВыводБезКонцевогоПереводаСтроки"); ВсеТесты.Добавить("ТестДолжен_ПроверитьЧтоМетодПрочитатьНаПустомПотокеВернетНеопределено"); ВсеТесты.Добавить("ТестДолжен_ЗаписатьВоВходнойПотокПроцесса"); ВсеТесты.Добавить("ТестДолжен_НайтиПроцессПоИмени"); @@ -33,11 +34,12 @@ Процесс.Запустить(); Поток = Процесс.ПотокВывода; Процесс.ОжидатьЗавершения(); - + + // Прочитать() возвращает вывод как есть, включая завершающий перевод строки Стр = НормализоватьПереводыСтрок(Поток.Прочитать()); - + юТест.ПроверитьРавенство(ШтатныйВыводOscript(), Стр); - + КонецПроцедуры Процедура ТестДолжен_ПрочитатьВыводOscriptПострочно() Экспорт @@ -59,13 +61,29 @@ Стр = Стр + СтрокаПотока + " |"; КонецЦикла; - - Стр = СокрП(Стр); - + юТест.ПроверитьРавенство(ШтатныйВыводOscript(), НормализоватьПереводыСтрок(Стр)); КонецПроцедуры +Процедура ТестДолжен_ПрочитатьВыводБезКонцевогоПереводаСтроки() Экспорт + + ИмяСкрипта = юТест.ИмяВременногоФайла("os"); + ЗаписьТекста = Новый ЗаписьТекста(ИмяСкрипта, КодировкаТекста.UTF8); + ЗаписьТекста.ЗаписатьСтроку("Консоль.ВывестиСтроку(""line1"");"); + ЗаписьТекста.Записать("Консоль.Вывести(""line2"");"); + ЗаписьТекста.Закрыть(); + + Процесс = СоздатьПроцесс(ПутьОСкрипт() + " """ + ИмяСкрипта + """",, Истина); + Процесс.Запустить(); + Процесс.ОжидатьЗавершения(); + + Стр = НормализоватьПереводыСтрок(Процесс.ПотокВывода.Прочитать()); + + юТест.ПроверитьРавенство("line1" + Символы.ПС + "line2", Стр); + +КонецПроцедуры + Процедура ТестДолжен_ЗаписатьВоВходнойПотокПроцесса() Экспорт КодировкаПоУмолчанию = Консоль.КодировкаВходногоПотока; @@ -88,7 +106,8 @@ Эталон = НормализоватьПереводыСтрок("1 |2 - |3"); + |3 + |"); юТест.ПроверитьРавенство(Эталон, НормализоватьПереводыСтрок(Вывод)); КонецПроцедуры @@ -145,6 +164,7 @@ | |Usage: | oscript.exe [options] [script_arguments...] + | oscript.exe [options] -c [script_arguments...] | oscript.exe [mode_options] [script_arguments...] | |Modes: @@ -158,6 +178,7 @@ | Options: | -port= Debugger port (default is 2801). | -noWait Do not wait for debugger connection. + | -c Execute code from command line. | -version, -v Output version string. | |Options: @@ -166,8 +187,9 @@ | |CGI Mode: | oscript.exe -cgi [script_arguments...] - | Runs as CGI application under HTTP-server."; - + | Runs as CGI application under HTTP-server. + |"; + Возврат НормализоватьПереводыСтрок(Текст); КонецФункции @@ -233,9 +255,9 @@ Процесс.Запустить(); Поток = Процесс.ПотокВывода; Процесс.ОжидатьЗавершения(); - + Стр = НормализоватьПереводыСтрок(Поток.Прочитать()); - + юТест.ПроверитьРавенство(ШтатныйВыводOscript(), Стр); КонецПроцедуры @@ -248,9 +270,9 @@ Процесс.Запустить(); Поток = Процесс.ПотокВывода; Процесс.ОжидатьЗавершения(); - + Стр = НормализоватьПереводыСтрок(Поток.Прочитать()); - + юТест.ПроверитьРавенство(ШтатныйВыводOscript(), Стр); КонецПроцедуры diff --git a/tests/showversion.os b/tests/showversion.os index 4beaf301c..f71a71554 100644 --- a/tests/showversion.os +++ b/tests/showversion.os @@ -34,9 +34,10 @@ Процесс.Запустить(); Процесс.ОжидатьЗавершения(); - ДанныеВывода = Процесс.ПотокВывода.Прочитать(); + // Прочитать() возвращает вывод как есть, включая завершающий перевод строки + ДанныеВывода = СокрЛП(Процесс.ПотокВывода.Прочитать()); - юТест.ПроверитьРавенство(ДанныеВывода, СистемнаяИнформация.Версия, "Версия вывода не совпадает с системной информацией!"); + юТест.ПроверитьРавенство(СистемнаяИнформация.Версия, ДанныеВывода, "Версия вывода не совпадает с системной информацией!"); КонецЦикла; diff --git a/tests/tasks.os b/tests/tasks.os index 1cc7f878b..d39f48ed4 100644 --- a/tests/tasks.os +++ b/tests/tasks.os @@ -5,6 +5,9 @@ Перем СобытиеВызвано; +Перем ЗавершениеПотокаЗафиксировано; +Перем ЭлементовВДанныхПриЗавершении; + //////////////////////////////////////////////////////////////////// // Программный интерфейс @@ -24,6 +27,17 @@ ВсеТесты.Добавить("ТестДолжен_ПроверитьЧтоВозвращаетсяРезультатДелегата"); ВсеТесты.Добавить("ТестДолжен_ПроверитьЧтоРаботаетБлокировка"); ВсеТесты.Добавить("ТестДолжен_ПроверитьЧтоКодМожетОпределитьИДЗадания"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьУникальностьПотокаИсполнения"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьИзоляциюДанныхПотокаИсполнения"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьОсвобождениеДанныхПотокаИсполнения"); + ВсеТесты.Добавить("ТестДолжен_ВызватьОбработчикЗавершенияПотокаИсполнения"); + ВсеТесты.Добавить("ТестДолжен_НеРонятьЗаданиеИзЗаОшибкиВОбработчикеЗавершения"); + ВсеТесты.Добавить("ТестДолжен_ОсвободитьОстальныеДанныеПриОшибкеОсвобождения"); + ВсеТесты.Добавить("ТестДолжен_ОсвободитьДанныеПотокаОсновногоСкрипта"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьПотокобезопасностьПолучитьТекущее"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьПотокобезопасностьПолучитьФоновыеЗадания"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьПоискТекущегоСредиМножестваЗавершенных"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьЧтоОжиданиеНеСтираетНовыеЗадания"); ВсеТесты.Добавить("ТестДолжен_ПроверитьЧтоВИнформацииОбОшибкеЕстьСтекВызовов"); ВсеТесты.Добавить("ТестДолжен_ПроверитьЧтоОбработчикиСобытийВызываютсяВФоновомЗадании"); @@ -272,6 +286,328 @@ КонецПроцедуры +Функция ВернутьИдентификаторПотокаИсполнения() Экспорт + + Приостановить(500); + Возврат ТекущийПоток().Идентификатор; + +КонецФункции + +Процедура ТестДолжен_ПроверитьУникальностьПотокаИсполнения() Экспорт + + ИдОсновногоПотока = ТекущийПоток().Идентификатор; + юТест.ПроверитьРавенство(ИдОсновногоПотока, ТекущийПоток().Идентификатор, + "Идентификатор потока исполнения должен быть одинаковым при повторном вызове"); + + МассивЗаданий = Новый Массив; + Для Сч = 1 По 4 Цикл + МассивЗаданий.Добавить(ФоновыеЗадания.Выполнить(ЭтотОбъект, "ВернутьИдентификаторПотокаИсполнения")); + КонецЦикла; + + ФоновыеЗадания.ОжидатьВсе(МассивЗаданий); + + УникальныеИдентификаторы = Новый Соответствие; + Для Каждого Задание Из МассивЗаданий Цикл + юТест.ПроверитьНеРавенство(ИдОсновногоПотока, Задание.Результат, + "Фоновое задание должно получить собственный поток исполнения"); + УникальныеИдентификаторы.Вставить(Задание.Результат, Истина); + КонецЦикла; + + юТест.ПроверитьРавенство(МассивЗаданий.Количество(), УникальныеИдентификаторы.Количество(), + "Идентификаторы одновременных фоновых заданий должны различаться"); + +КонецПроцедуры + +Функция ЗаписатьВДанныеПотокаИЗабрать(Знач Метка) Экспорт + + ТекущийПоток().Данные.Вставить("метка", Метка); + Приостановить(500); + + // Данные основного потока не должны быть видны, а соседние задания не должны затирать наши + Результат = Новый Структура("Метка, ЕстьЧужое"); + Результат.Метка = ТекущийПоток().Данные.Получить("метка"); + Результат.ЕстьЧужое = (ТекущийПоток().Данные.Получить("изОсновногоПотока") <> Неопределено); + + Возврат Результат; + +КонецФункции + +Процедура ТестДолжен_ПроверитьИзоляциюДанныхПотокаИсполнения() Экспорт + + ТекущийПоток().Данные.Вставить("изОсновногоПотока", Истина); + + юТест.ПроверитьРавенство(Истина, ТекущийПоток().Данные.Получить("изОсновногоПотока"), + "ТекущийПоток() должен возвращать один и тот же экземпляр в пределах потока исполнения"); + + МассивЗаданий = Новый Массив; + Для Сч = 1 По 4 Цикл + Параметры = Новый Массив; + Параметры.Добавить(Сч); + МассивЗаданий.Добавить(ФоновыеЗадания.Выполнить(ЭтотОбъект, "ЗаписатьВДанныеПотокаИЗабрать", Параметры)); + КонецЦикла; + + ФоновыеЗадания.ОжидатьВсе(МассивЗаданий); + + Для Сч = 1 По МассивЗаданий.Количество() Цикл + Результат = МассивЗаданий[Сч - 1].Результат; + юТест.ПроверитьРавенство(Сч, Результат.Метка, + "Данные потока исполнения не должны затираться соседними фоновыми заданиями"); + юТест.ПроверитьРавенство(Ложь, Результат.ЕстьЧужое, + "Фоновое задание не должно видеть данные основного потока исполнения"); + КонецЦикла; + + ТекущийПоток().Данные.Удалить("изОсновногоПотока"); + +КонецПроцедуры + +Функция ПоложитьВДанныеПотокаОсвобождаемыйОбъект() Экспорт + + ИмяФайла = ПолучитьИмяВременногоФайла("tmp"); + ФайловыйПоток = Новый ФайловыйПоток(ИмяФайла, РежимОткрытияФайла.Создать, ДоступКФайлу.Запись); + + ТекущийПоток().Данные.Вставить("файл", ФайловыйПоток); + + // Возвращаем и сам поток исполнения, и объект - чтобы проверить их состояние после завершения задания + Возврат Новый Структура("ПотокИсполнения, ФайловыйПоток, ИмяФайла", + ТекущийПоток(), ФайловыйПоток, ИмяФайла); + +КонецФункции + +Процедура ТестДолжен_ПроверитьОсвобождениеДанныхПотокаИсполнения() Экспорт + + Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ПоложитьВДанныеПотокаОсвобождаемыйОбъект"); + Задание.ОжидатьЗавершения(); + + Результат = Задание.Результат; + юТест.ПроверитьНеРавенство(Неопределено, Результат, "Задание должно вернуть результат"); + + юТест.ПроверитьРавенство(0, Результат.ПотокИсполнения.Данные.Количество(), + "Данные потока исполнения должны очищаться по завершении фонового задания"); + + ИсключениеБыло = Ложь; + Попытка + Результат.ФайловыйПоток.ТекущаяПозиция(); + Исключение + ИсключениеБыло = Истина; + КонецПопытки; + + юТест.ПроверитьРавенство(Истина, ИсключениеБыло, + "Значения данных потока исполнения должны принудительно освобождаться"); + + УдалитьФайлы(Результат.ИмяФайла); + +КонецПроцедуры + +Процедура ПриЗавершенииПотокаЗадания() Экспорт + + ЗавершениеПотокаЗафиксировано = Истина; + // Обработчик должен успевать увидеть данные потока до их очистки + ЭлементовВДанныхПриЗавершении = ТекущийПоток().Данные.Количество(); + +КонецПроцедуры + +Функция ПодписатьсяНаЗавершениеПотока() Экспорт + + ТекущийПоток().Данные.Вставить("ресурс", "значение"); + ДобавитьОбработчик ТекущийПоток().ПриЗавершении, ЭтотОбъект.ПриЗавершенииПотокаЗадания; + Возврат ТекущийПоток(); + +КонецФункции + +Процедура ТестДолжен_ВызватьОбработчикЗавершенияПотокаИсполнения() Экспорт + + ЗавершениеПотокаЗафиксировано = Ложь; + ЭлементовВДанныхПриЗавершении = -1; + + Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ПодписатьсяНаЗавершениеПотока"); + Задание.ОжидатьЗавершения(); + + юТест.ПроверитьРавенство(Истина, ЗавершениеПотокаЗафиксировано, + "Обработчик ПриЗавершении должен вызываться по завершении потока исполнения"); + + юТест.ПроверитьРавенство(1, ЭлементовВДанныхПриЗавершении, + "Обработчик должен видеть данные потока до их очистки"); + + юТест.ПроверитьРавенство(0, Задание.Результат.Данные.Количество(), + "После обработчика данные потока должны быть очищены"); + +КонецПроцедуры + +Процедура ПриЗавершенииПотокаСИсключением() Экспорт + ВызватьИсключение "Ошибка внутри обработчика завершения"; +КонецПроцедуры + +Функция ПодписатьсяНаПадающийОбработчик() Экспорт + ДобавитьОбработчик ТекущийПоток().ПриЗавершении, ЭтотОбъект.ПриЗавершенииПотокаСИсключением; + Возврат "задание отработало"; +КонецФункции + +Процедура ТестДолжен_НеРонятьЗаданиеИзЗаОшибкиВОбработчикеЗавершения() Экспорт + + Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ПодписатьсяНаПадающийОбработчик"); + Задание.ОжидатьЗавершения(); + + юТест.ПроверитьРавенство("задание отработало", Задание.Результат, + "Ошибка в обработчике завершения не должна ломать результат задания"); + + юТест.ПроверитьРавенство(СостояниеФоновогоЗадания.Завершено, Задание.Состояние, + "Ошибка в обработчике завершения не должна менять состояние задания"); + +КонецПроцедуры + +// Создаёт значение, освобождение которого завершается ошибкой. +// +// ЗаписьДанных при освобождении сбрасывает буфер в нижележащий поток, а он уже закрыт. +// +// Параметры: +// ИмяФайла - Строка - Файл, поверх которого строится значение. +// +Функция ПадающийПриОсвобожденииРесурс(Знач ИмяФайла) + + ФайловыйПоток = Новый ФайловыйПоток(ИмяФайла, РежимОткрытияФайла.Создать, ДоступКФайлу.Запись); + Писатель = Новый ЗаписьДанных(ФайловыйПоток); + Писатель.ЗаписатьСтроку("данные, которые придётся сбрасывать при закрытии"); + ФайловыйПоток.Закрыть(); + + Возврат Писатель; + +КонецФункции + +Функция ПоложитьВДанныеПадающийИИсправныйРесурсы() Экспорт + + ФайлПадающего = ПолучитьИмяВременногоФайла("tmp"); + ФайлИсправного = ПолучитьИмяВременногоФайла("tmp"); + + Исправный = Новый ФайловыйПоток(ФайлИсправного, РежимОткрытияФайла.Создать, ДоступКФайлу.Запись); + + ТекущийПоток().Данные.Вставить("падающий", ПадающийПриОсвобожденииРесурс(ФайлПадающего)); + ТекущийПоток().Данные.Вставить("исправный", Исправный); + + // Пути возвращаются вызывающему: закрытие потоков файлы не удаляет + Возврат Новый Структура("Поток, Исправный, Файлы", ТекущийПоток(), Исправный, + ОбщегоНазначенияМассивИз(ФайлПадающего, ФайлИсправного)); + +КонецФункции + +// Собирает массив из переданных значений. +// +Функция ОбщегоНазначенияМассивИз(Знач Первое, Знач Второе) + + Результат = Новый Массив; + Результат.Добавить(Первое); + Результат.Добавить(Второе); + + Возврат Результат; + +КонецФункции + +// Ошибка освобождения одного значения не должна ни ронять задание, ни оставлять +// неосвобождёнными остальные значения. +// +Процедура ТестДолжен_ОсвободитьОстальныеДанныеПриОшибкеОсвобождения() Экспорт + + Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ПоложитьВДанныеПадающийИИсправныйРесурсы"); + Задание.ОжидатьЗавершения(); + + Результат = Задание.Результат; + + Попытка + + юТест.ПроверитьРавенство(СостояниеФоновогоЗадания.Завершено, Задание.Состояние, + "Ошибка освобождения значения не должна менять состояние задания"); + + юТест.ПроверитьНеРавенство(Неопределено, Результат, "Задание должно вернуть результат"); + + юТест.ПроверитьРавенство(0, Результат.Поток.Данные.Количество(), + "Данные потока должны очищаться даже при ошибке освобождения одного из значений"); + + ИсправныйОсвобожден = Ложь; + Попытка + Результат.Исправный.ТекущаяПозиция(); + Исключение + ИсправныйОсвобожден = Истина; + КонецПопытки; + + юТест.ПроверитьРавенство(Истина, ИсправныйОсвобожден, + "Исправное значение должно освобождаться независимо от ошибки на соседнем"); + + Исключение + УдалитьВременныеФайлы(Результат); + ВызватьИсключение; + КонецПопытки; + + УдалитьВременныеФайлы(Результат); + +КонецПроцедуры + +// Удаляет временные файлы, созданные заданием: закрытие потоков их не удаляет. +// +Процедура УдалитьВременныеФайлы(Знач Результат) + + Если Результат = Неопределено Тогда + Возврат; + КонецЕсли; + + Для Каждого ИмяФайла Из Результат.Файлы Цикл + Если Новый Файл(ИмяФайла).Существует() Тогда + УдалитьФайлы(ИмяФайла); + КонецЕсли; + КонецЦикла; + +КонецПроцедуры + +// Поток исполнения основного скрипта тоже завершается: обработчик ПриЗавершении должен +// сработать по окончании скрипта, а не остаться невызванным до конца процесса. +// +// Проверяется дочерним oscript: изнутри самого скрипта момент его завершения не наблюдаем. +// Дочерний процесс подписывается на завершение своего потока и пишет из обработчика файл-метку, +// родитель дожидается завершения и смотрит на метку. +// +Процедура ТестДолжен_ОсвободитьДанныеПотокаОсновногоСкрипта() Экспорт + + ФайлМетки = ПолучитьИмяВременногоФайла("txt"); + ФайлСкрипта = ПолучитьИмяВременногоФайла("os"); + + ТекстСкрипта = Новый ЗаписьТекста(ФайлСкрипта, КодировкаТекста.UTF8); + ТекстСкрипта.ЗаписатьСтроку("Процедура ПриЗавершении() Экспорт"); + ТекстСкрипта.ЗаписатьСтроку(" Запись = Новый ЗаписьТекста(""" + СтрЗаменить(ФайлМетки, "\", "\\") + """);"); + ТекстСкрипта.ЗаписатьСтроку(" Запись.ЗаписатьСтроку(""элементов="" + ТекущийПоток().Данные.Количество());"); + ТекстСкрипта.ЗаписатьСтроку(" Запись.Закрыть();"); + ТекстСкрипта.ЗаписатьСтроку("КонецПроцедуры"); + ТекстСкрипта.ЗаписатьСтроку("ТекущийПоток().Данные.Вставить(""ключ"", ""значение"");"); + ТекстСкрипта.ЗаписатьСтроку("ДобавитьОбработчик ТекущийПоток().ПриЗавершении, ЭтотОбъект.ПриЗавершении;"); + ТекстСкрипта.Закрыть(); + + Попытка + + Процесс = СоздатьПроцесс("dotnet """ + КаталогПрограммы() + "/oscript.dll"" """ + ФайлСкрипта + """"); + Процесс.Запустить(); + Процесс.ОжидатьЗавершения(); + + юТест.ПроверитьРавенство(0, Процесс.КодВозврата, "Дочерний скрипт должен отработать без ошибки"); + + юТест.ПроверитьРавенство(Истина, Новый Файл(ФайлМетки).Существует(), + "Обработчик ПриЗавершении должен вызываться и для потока основного скрипта"); + + Чтение = Новый ЧтениеТекста(ФайлМетки); + Метка = СокрЛП(Чтение.ПрочитатьСтроку()); + Чтение.Закрыть(); + + юТест.ПроверитьРавенство("элементов=1", Метка, + "Обработчик должен видеть данные потока основного скрипта до их очистки"); + + Исключение + УдалитьФайлы(ФайлСкрипта); + ВызватьИсключение; + КонецПопытки; + + УдалитьФайлы(ФайлСкрипта); + Если Новый Файл(ФайлМетки).Существует() Тогда + УдалитьФайлы(ФайлМетки); + КонецЕсли; + +КонецПроцедуры + Процедура ТестДолжен_ПроверитьЧтоВИнформацииОбОшибкеЕстьСтекВызовов() Экспорт Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ПроцедураСИсключением"); @@ -310,3 +646,104 @@ ); КонецПроцедуры + +Процедура Пустышка() Экспорт +КонецПроцедуры + +Процедура ДергатьТекущее() Экспорт + Для Счетчик = 1 По 20000 Цикл + Текущее = ФоновыеЗадания.ПолучитьТекущее(); + Если Текущее = Неопределено Тогда + ВызватьИсключение "Не найдено текущее задание"; + КонецЕсли; + КонецЦикла; +КонецПроцедуры + +Процедура ДергатьСписокЗаданий() Экспорт + Для Счетчик = 1 По 20000 Цикл + ФоновыеЗадания.ПолучитьФоновыеЗадания(); + КонецЦикла; +КонецПроцедуры + +Процедура ПроверитьОтсутствиеОшибокВЗаданиях(Задания) + + Для Каждого Задание Из Задания Цикл + Задание.ОжидатьЗавершения(60000); + Если Задание.ИнформацияОбОшибке <> Неопределено Тогда + ВызватьИсключение Задание.ИнформацияОбОшибке.Описание; + КонецЕсли; + КонецЦикла; + +КонецПроцедуры + +Процедура НаполнитьРеестрЗаданиями() + + Для Номер = 1 По 300 Цикл + ФоновыеЗадания.Выполнить(ЭтотОбъект, "Пустышка"); + КонецЦикла; + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьПотокобезопасностьПолучитьТекущее() Экспорт + + Задания = Новый Массив; + Для Номер = 1 По 4 Цикл + Задания.Добавить(ФоновыеЗадания.Выполнить(ЭтотОбъект, "ДергатьТекущее", Новый Массив, Истина)); + КонецЦикла; + + НаполнитьРеестрЗаданиями(); + ПроверитьОтсутствиеОшибокВЗаданиях(Задания); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьПотокобезопасностьПолучитьФоновыеЗадания() Экспорт + + Задания = Новый Массив; + Для Номер = 1 По 4 Цикл + Задания.Добавить(ФоновыеЗадания.Выполнить(ЭтотОбъект, "ДергатьСписокЗаданий", Новый Массив, Истина)); + КонецЦикла; + + НаполнитьРеестрЗаданиями(); + ПроверитьОтсутствиеОшибокВЗаданиях(Задания); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьПоискТекущегоСредиМножестваЗавершенных() Экспорт + + Для Номер = 1 По 200 Цикл + Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "Пустышка"); + Задание.ОжидатьЗавершения(); + КонецЦикла; + + Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ПроверитьИдентификаторЗадания"); + Задание.ОжидатьЗавершения(); + + юТест.ПроверитьРавенство(Задание.УникальныйИдентификатор, Задание.Результат); + юТест.ПроверитьНеРавенство(Неопределено, Задание.Результат); + +КонецПроцедуры + +Функция ЗапуститьОтложенноеЗадание() Экспорт + Приостановить(300); + Возврат ФоновыеЗадания.Выполнить(ЭтотОбъект, "ДолгаяПустышка"); +КонецФункции + +Процедура ДолгаяПустышка() Экспорт + Приостановить(2000); +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьЧтоОжиданиеНеСтираетНовыеЗадания() Экспорт + + Стартовое = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ЗапуститьОтложенноеЗадание"); + ФоновыеЗадания.ОжидатьЗавершенияЗадач(); + + ПозднееЗадание = Стартовое.Результат; + юТест.ПроверитьНеРавенство(Неопределено, ПозднееЗадание); + + Оставшиеся = ФоновыеЗадания.ПолучитьФоновыеЗадания(); + юТест.ПроверитьРавенство(1, Оставшиеся.Количество()); + юТест.ПроверитьРавенство(ПозднееЗадание.УникальныйИдентификатор, Оставшиеся[0].УникальныйИдентификатор); + + ПозднееЗадание.ОжидатьЗавершения(); + +КонецПроцедуры diff --git a/tests/value-list.os b/tests/value-list.os index 2dbf03562..dd1812e6c 100644 --- a/tests/value-list.os +++ b/tests/value-list.os @@ -17,9 +17,20 @@ ВсеТесты.Добавить("ТестДолжен_ПроверитьМетодСдвинутьСпискаЗначений"); ВсеТесты.Добавить("ТестДолжен_ПроверитьМетодСортироватьПоЗначениюСпискаЗначений"); ВсеТесты.Добавить("ТестДолжен_ПроверитьМетодСортироватьПоПредставлениюСпискаЗначений"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьСортировкуПоЗначениюРазныхТипов"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьСортировкуНеупорядочиваемыхТипов"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьМетодНайтиСпискаЗначений"); ВсеТесты.Добавить("ТестДолжен_ПроверитьМетодУдалитьСпискаЗначений"); ВсеТесты.Добавить("ТестДолжен_ПроверитьПреобразованиеЭлементаСпискаВСтроку"); + + ВсеТесты.Добавить("ТестДолжен_ПроверитьОшибкуМетодаПолучитьСпискаЗначений"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьСвойствоТипЗначения"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьСвойствоДоступныеЗначения_Без_ТипЗначения"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьСвойствоДоступныеЗначения_С_ТипЗначения"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьПреобразованиеСпискаВСтроку"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьМетодПолучитьИдентификатор"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьМетодЗагрузитьЗначенияСПриведениемЗначений"); Возврат ВсеТесты; КонецФункции @@ -148,6 +159,32 @@ юТест.ПроверитьРавенство(СЗ.Индекс(Э3), 2, "Метод Вставить() списка значений"); юТест.ПроверитьРавенство(СЗ.Индекс(Э4), 3, "Метод Вставить() списка значений"); + // Проверим ошибки + Ошибка = "Вставить с неверным индексом"; + Попытка + СЗ.Вставить(10, 9); + Исключение + Ошибка = ИнформацияОбОшибке().Описание; + КонецПопытки; + юТест.ПроверитьРавенство(Ошибка, "Значение индекса выходит за пределы диапазона"); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьОшибкуМетодаПолучитьСпискаЗначений() Экспорт + + СЗ = Новый СписокЗначений; + СЗ.Добавить("Один"); + СЗ.Добавить("Два"); + + // Проверим ошибки + Ошибка = "Получить с неверным индексом"; + Попытка + СЗ.Получить(10); + Исключение + Ошибка = ИнформацияОбОшибке().Описание; + КонецПопытки; + юТест.ПроверитьРавенство(Ошибка, "Значение индекса выходит за пределы диапазона"); + КонецПроцедуры Процедура ТестДолжен_ПроверитьМетодУдалитьСпискаЗначений() Экспорт @@ -354,6 +391,50 @@ КонецПроцедуры +Процедура ТестДолжен_ПроверитьСортировкуПоЗначениюРазныхТипов() Экспорт + СЗ = Новый СписокЗначений; + + СЗ.Добавить(Тип("Строка")); + СЗ.Добавить("Стр2"); + СЗ.Добавить("СтрЪ"); + СЗ.Добавить(); + СЗ.Добавить(Новый Структура); + СЗ.Добавить(Новый Массив); + СЗ.Добавить(null); + СЗ.Добавить(Тип("Булево")); + СЗ.Добавить(Тип("Массив")); + СЗ.Добавить(Тип("ЗаписьXML")); + СЗ.Добавить(Новый ЧтениеJSON); + СЗ.Добавить(Новый ЗаписьXML); + + СЗ.СортироватьПоЗначению(); + + юТест.ПроверитьРавенство(СЗ[0].Значение, Неопределено); + юТест.ПроверитьРавенство(СЗ[1].Значение, null); + юТест.ПроверитьРавенство(СЗ[2].Значение, "Стр2"); + юТест.ПроверитьРавенство(СЗ[3].Значение, "СтрЪ"); + юТест.ПроверитьТип(СЗ[4].Значение, Тип("ЗаписьXML")); + юТест.ПроверитьТип(СЗ[5].Значение, Тип("Массив")); + юТест.ПроверитьТип(СЗ[6].Значение, Тип("Структура")); + юТест.ПроверитьТип(СЗ[7].Значение, Тип("ЧтениеJSON")); + юТест.ПроверитьРавенство(СЗ[8].Значение, Тип("Булево")); + юТест.ПроверитьРавенство(СЗ[9].Значение, Тип("ЗаписьXML")); + юТест.ПроверитьРавенство(СЗ[10].Значение, Тип("Массив")); + юТест.ПроверитьРавенство(СЗ[11].Значение, Тип("Строка")); +КонецПроцедуры + +// см. issue #1577 +Процедура ТестДолжен_ПроверитьСортировкуНеупорядочиваемыхТипов() Экспорт + СЗ = Новый СписокЗначений; + + СЗ.Добавить(Неопределено); + СЗ.Добавить(Неопределено); + + СЗ.СортироватьПоЗначению(); // не должно упасть + + юТест.ПроверитьРавенство(СЗ[0].Значение, Неопределено); + юТест.ПроверитьРавенство(СЗ[1].Значение, Неопределено); +КонецПроцедуры Процедура ТестДолжен_ПроверитьМетодСортироватьПоПредставлениюСпискаЗначений() Экспорт @@ -400,6 +481,10 @@ СЗ.Добавить(1, ""); СЗ.Добавить(2, Неопределено); СЗ.Добавить(3, " "); + СЗ.Добавить(Неопределено); + СЗ.Добавить(Неопределено,"(неопр.)"); + СЗ.Добавить(NULL); + СЗ.Добавить(NULL,"(null)"); юТест.ПроверитьРавенство("0", Строка(СЗ[0]), "Строка, без указания"); юТест.ПроверитьРавенство("", СЗ[0].Представление, "Представление, без указания"); @@ -409,5 +494,138 @@ юТест.ПроверитьРавенство("", СЗ[2].Представление, "Представление, Неопределено"); юТест.ПроверитьРавенство(" ", Строка(СЗ[3]), "Строка, пробел"); юТест.ПроверитьРавенство(" ", СЗ[3].Представление, "Представление, пробел"); + + юТест.ПроверитьРавенство("<Пустое значение>", Строка(СЗ[4]), "Неопределено, без указания"); + юТест.ПроверитьРавенство("", СЗ[4].Представление, "Неопределено, без представления"); + юТест.ПроверитьРавенство("(неопр.)", Строка(СЗ[5]), "Неопределено, с указанием"); + юТест.ПроверитьРавенство("(неопр.)", СЗ[5].Представление, "Неопределено, с представлением"); -КонецПроцедуры \ No newline at end of file + юТест.ПроверитьРавенство("<Пустое значение>", Строка(СЗ[6]), "NULL, без указания"); + юТест.ПроверитьРавенство("", СЗ[6].Представление, "NULL, без представления"); + юТест.ПроверитьРавенство("(null)", Строка(СЗ[7]), "NULL, с указанием"); + юТест.ПроверитьРавенство("(null)", СЗ[7].Представление, "NULL, с представлением"); + + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьСвойствоТипЗначения() Экспорт + Описание = Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(2)); + СЗ = Новый СписокЗначений; + СЗ.ТипЗначения = Описание; + СЗ.Добавить(12); + СЗ.Добавить(22); + СЗ.Добавить(122); + СЗ.Добавить("23"); + СЗ.Добавить("*3"); + + юТест.ПроверитьРавенство(12, СЗ[0].Значение); + юТест.ПроверитьРавенство(22, СЗ[1].Значение); + юТест.ПроверитьРавенство(99, СЗ[2].Значение); + юТест.ПроверитьТип(СЗ[3].Значение,"Число"); + юТест.ПроверитьРавенство(23, СЗ[3].Значение); + юТест.ПроверитьРавенство(0, СЗ[4].Значение); +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьСвойствоДоступныеЗначения_Без_ТипЗначения() Экспорт + ДЗ = Новый СписокЗначений; + ДЗ.Добавить(11); + ДЗ.Добавить(12); + ДЗ.Добавить(99); + + СЗ = Новый СписокЗначений; + СЗ.ДоступныеЗначения = ДЗ; + СЗ.Добавить(12); + СЗ.Добавить(22); + СЗ.Добавить("11"); + + юТест.ПроверитьТип(СЗ[0].Значение,"Число"); + юТест.ПроверитьРавенство(12, СЗ[0].Значение, "Допустимое значение"); + юТест.ПроверитьРавенство( Неопределено, СЗ[1].Значение, "Недопустимое значение"); + юТест.ПроверитьРавенство( Неопределено, СЗ[2].Значение, "Без приведения типа"); +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьСвойствоДоступныеЗначения_С_ТипЗначения() Экспорт + ДЗ = Новый СписокЗначений; + ДЗ.Добавить(11); + ДЗ.Добавить(12); + ДЗ.Добавить(99); + + Описание = Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(2)); + + СЗ = Новый СписокЗначений; + СЗ.ТипЗначения = Описание; + СЗ.ДоступныеЗначения = ДЗ; + СЗ.Добавить(12); + СЗ.Добавить(22); + СЗ.Добавить("11"); // приведение типа + СЗ.Добавить(123); // переполнение + + юТест.ПроверитьТип(СЗ[0].Значение,"Число"); + юТест.ПроверитьРавенство(12, СЗ[0].Значение, "Допустимое значение"); + юТест.ПроверитьРавенство( 0, СЗ[1].Значение, "Недопустимое значение с приведением типа"); + юТест.ПроверитьТип(СЗ[2].Значение,"Число"); + юТест.ПроверитьРавенство( 11, СЗ[2].Значение, "Допустимое значение с приведением типа"); + юТест.ПроверитьРавенство( 99, СЗ[3].Значение, "Приведение с переполнением и допустимое значение"); +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьПреобразованиеСпискаВСтроку() Экспорт + + СЗ = Новый СписокЗначений; + СЗ.Добавить(11); + СЗ.Добавить(12,"двенадцать"); + СЗ.Добавить("абв"); + СЗ.Добавить("фыв",""); + СЗ.Добавить("пробел"," "); + СЗ.Добавить("йцу","Йцу!"); + СЗ.Добавить(Неопределено); + СЗ.Добавить(Неопределено,"(неопр.)"); + + Строка = Строка(СЗ); + + юТест.ПроверитьРавенство(Строка, "11; двенадцать; абв; фыв; ; Йцу!; <Пустое значение>; (неопр.)"); +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьМетодПолучитьИдентификатор() Экспорт + СЗ = Новый СписокЗначений; + + СЗ.Добавить(1); + СЗ.Добавить(-2); + СЗ.Добавить(3); + СЗ.Добавить(-4); + + СЗ.Удалить(2); + СЗ.Добавить(0); + + СЗ.СортироватьПоЗначению(); + + юТест.ПроверитьРавенство(-4, СЗ[0].Значение); + юТест.ПроверитьРавенство(3, СЗ[0].ПолучитьИдентификатор()); + + юТест.ПроверитьРавенство(-2, СЗ[1].Значение); + юТест.ПроверитьРавенство(1, СЗ[1].ПолучитьИдентификатор()); + + юТест.ПроверитьРавенство(0, СЗ[2].Значение); + юТест.ПроверитьРавенство(4, СЗ[2].ПолучитьИдентификатор()); + + юТест.ПроверитьРавенство(1, СЗ[3].Значение); + юТест.ПроверитьРавенство(0, СЗ[3].ПолучитьИдентификатор()); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьМетодЗагрузитьЗначенияСПриведениемЗначений() Экспорт + Массив = Новый Массив; + Массив.Добавить(12); + Массив.Добавить(123); + + Описание = Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(2)); + + СЗ = Новый СписокЗначений; + СЗ.ТипЗначения = Описание; + + СЗ.Добавить(1); + СЗ.ЗагрузитьЗначения(Массив); + + юТест.ПроверитьРавенство(2, СЗ.Количество()); + юТест.ПроверитьРавенство(12, СЗ[0].Значение); + юТест.ПроверитьРавенство(99, СЗ[1].Значение); +КонецПроцедуры diff --git a/tests/xmlwrite.os b/tests/xmlwrite.os index 5b3e4665f..b978d332a 100644 --- a/tests/xmlwrite.os +++ b/tests/xmlwrite.os @@ -43,6 +43,15 @@ ВсеТесты.Добавить("ТестДолжен_ЗаписатьТекущий_ИнструкцииОбработки"); ВсеТесты.Добавить("ТестДолжен_ЗаписатьТекущий_ОбъявлениеИТипДокумента"); + + ВсеТесты.Добавить("ТестДолжен_ПроверитьЗакрытиеПустого"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьТипВозвратаЗакрыть_ИзФайла"); + + ВсеТесты.Добавить("ТестДолжен_ПроверитьЗаписьТипаДокумента_4Параметра"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьЗаписьТипаДокумента_3Параметра"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьЗаписьТипаДокумента_4ПараметраСОшибкой"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьЗаписьТипаДокумента_ОшибкаПослеНачала"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьЗаписьТипаДокумента_ОшибкаИмени"); Возврат ВсеТесты; @@ -759,3 +768,119 @@ ЧтениеXML.Прочитать(); юТест.ПроверитьРавенство(ЧтениеXML.ТипУзла, ТипУзла, Сообщение); КонецПроцедуры + +Процедура ТестДолжен_ПроверитьЗакрытиеПустого() Экспорт + Запись = Новый ЗаписьXML; + Результат = Запись.Закрыть(); + + юТест.ПроверитьРавенство(ТипЗнч(Результат), Тип("Строка")); + юТест.ПроверитьРавенство(Результат, ""); +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьТипВозвратаЗакрыть_ИзФайла() Экспорт + Запись = Новый ЗаписьXML; + ИмяФайла = юТест.ИмяВременногоФайла("xml"); + Запись.ОткрытьФайл(ИмяФайла); + Результат = Запись.Закрыть(); + + юТест.ПроверитьРавенство(ТипЗнч(Результат), Тип("Строка")); + юТест.ПроверитьРавенство(Результат, ""); +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьЗаписьТипаДокумента_4Параметра() Экспорт + Варианты = Новый Массив(); + Варианты.Добавить("А,,,:"); + //Варианты.Добавить("А,B,,:"); + Варианты.Добавить("А,,C,:"); + Варианты.Добавить("А,B,C,:"); + Варианты.Добавить("А,,,D:"); + //Варианты.Добавить("А,B,,D:"); + Варианты.Добавить("А,,C,D:"); + Варианты.Добавить("А,B,C,D:"); + + ЗаписьXML = Новый ЗаписьXML; + Для Каждого Пара Из Варианты Цикл + АргРез = СтрРазделить(Пара,":"); + Арг = СтрРазделить(АргРез[0],","); + + ЗаписьXML.УстановитьСтроку(); + ЗаписьXML.ЗаписатьТипДокумента(Арг[0],Арг[1],Арг[2],Арг[3]); + Результат = ЗаписьXML.Закрыть(); + юТест.ПроверитьРавенство(АргРез[1], Результат, "Ошибка для парамеров "+СтрСоединить(Арг,",")); + КонецЦикла; + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьЗаписьТипаДокумента_3Параметра() Экспорт + Варианты = Новый Массив(); + Варианты.Добавить("А,,:"); + Варианты.Добавить("А,B,:"); + Варианты.Добавить("А,,C:"); + Варианты.Добавить("А,B,C:"); + + ЗаписьXML = Новый ЗаписьXML; + Для Каждого Пара Из Варианты Цикл + АргРез = СтрРазделить(Пара,":"); + Арг = СтрРазделить(АргРез[0],","); + + ЗаписьXML.УстановитьСтроку(); + ЗаписьXML.ЗаписатьТипДокумента(Арг[0],Арг[1],Арг[2]); + Результат = ЗаписьXML.Закрыть(); + юТест.ПроверитьРавенство(АргРез[1], Результат, "Ошибка для парамеров "+СтрСоединить(Арг,",")); + КонецЦикла; + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьЗаписьТипаДокумента_4ПараметраСОшибкой() Экспорт + ЗаписьXML = Новый ЗаписьXML; + ЗаписьXML.УстановитьСтроку(); + + БылоИсключение = Ложь; + Попытка + ЗаписьXML.ЗаписатьТипДокумента("A","B",,"D"); + Исключение + Ошибка = ИнформацияОбОшибке().Описание; + юТест.ПроверитьНеРавенство(Найти(Ошибка,"Отсутствует системный идентификатор"), 0, "Неверный вид ошибки: "+Ошибка); + БылоИсключение = Истина; + КонецПопытки; + ЗаписьXML.Закрыть(); + + юТест.ПроверитьИстину(БылоИсключение, "ЗаписьТипаДокумента_4ПараметраСОшибкой"); +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьЗаписьТипаДокумента_ОшибкаПослеНачала() Экспорт + + ЗаписьXML = Новый ЗаписьXML; + ЗаписьXML.УстановитьСтроку(); + ЗаписьXML.ЗаписатьНачалоЭлемента("a"); + + БылоИсключение = Ложь; + Попытка + ЗаписьXML.ЗаписатьТипДокумента("A","B"); + Исключение + Ошибка = ИнформацияОбОшибке().Описание; + юТест.ПроверитьНеРавенство(Найти(Ошибка,"Ошибочный порядок записи XML"), 0, "Неверный вид ошибки: "+Ошибка); + БылоИсключение = Истина; + КонецПопытки; + ЗаписьXML.Закрыть(); + + юТест.ПроверитьИстину(БылоИсключение, "ЗаписьТипаДокумента_ОшибкаПослеНачала"); +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьЗаписьТипаДокумента_ОшибкаИмени() Экспорт + + ЗаписьXML = Новый ЗаписьXML; + ЗаписьXML.УстановитьСтроку(); + + БылоИсключение = Ложь; + Попытка + ЗаписьXML.ЗаписатьТипДокумента("%$#A","B"); + Исключение + Ошибка = ИнформацияОбОшибке().Описание; + юТест.ПроверитьНеРавенство(Найти(Ошибка,"Неверное значение аргумента номер 1"), 0, "Неверный вид ошибки: "+Ошибка); + БылоИсключение = Истина; + КонецПопытки; + ЗаписьXML.Закрыть(); + + юТест.ПроверитьИстину(БылоИсключение, "ТестДолжен_ПроверитьЗаписьТипаДокумента_ОшибкаИмени"); +КонецПроцедуры