diff --git a/.cursor/skills/release-notes/SKILL.md b/.cursor/skills/release-notes/SKILL.md index 4f4903ab2..1dc3a911d 100644 --- a/.cursor/skills/release-notes/SKILL.md +++ b/.cursor/skills/release-notes/SKILL.md @@ -36,14 +36,14 @@ git --no-pager log --pretty="* %%s" --no-merges %TAGNAME%..HEAD ## Формат файла release-notes -В начале записывается заголовок "Версия <НомерВерсии>. Какая сейчас конкретно версия ты должен посмотреть в файле Jenkinsfile в параметре VersionPrefix и взять его. +В начале записывается заголовок "Версия <НомерВерсии>". Какая сейчас конкретно версия ты должен посмотреть в файле .github/workflows/dist.yml в параметре VersionPrefix и взять его. Далее идет перечень сделанных задач, как описано выше. Задачи разбиваются на две группы: исправление ошибок и новые возможности. Соответственно в release notes добавляются два раздела уровня "заголовок 2" для "Новые возможности" и "Исправление ошибок". Если задач по какой-то категории нет (например, были только исправления, а новых фич не было), то соответствующий раздел не добавляется в файл совсем. ## Instructions -1. Определи текущую выпускаемую версию, прочитав файл Jenkinsfile и его параметр VersionPrefix в самом начале +1. Определи текущую выпускаемую версию, прочитав файл .github/workflows/dist.yml и его параметр VersionPrefix в самом начале 2. Определи тег предыдущей версии. Для этого запроси его у пользователя, если тег не был сразу задан в промпте 3. Выполни команду gitlog, как показано выше, для получения истории коммитов между версиями 4. Составь дайджест изменений и запиши их в install/release-notes.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..c1c6dfd92 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,196 @@ +# Сборка и тесты OneScript для веток и PR. +# Сборка дистрибутивов - в dist.yml, публикация - в release.yml. + +name: Build + +on: + push: + branches: ['**'] + pull_request: + workflow_dispatch: + +concurrency: + group: build-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +permissions: + contents: read + +env: + DOTNET_NOLOGO: 'true' + DOTNET_CLI_TELEMETRY_OPTOUT: 'true' + # Тесты рассчитаны на русскую Windows с форматами дат из NLS, + # а .NET на новых Windows по умолчанию берет форматы из ICU + DOTNET_SYSTEM_GLOBALIZATION_USENLS: 'true' + # На develop и release-ветках упавшие тесты не валят сборку + TESTS_FAIL_BUILD: ${{ github.event_name == 'pull_request' || !contains(fromJSON('["refs/heads/develop", "refs/heads/release/preview", "refs/heads/release/latest", "refs/heads/release/lts"]'), github.ref) }} + +jobs: + dist: + name: Distribution + uses: ./.github/workflows/dist.yml + + windows-testing: + name: Windows testing + needs: dist + runs-on: windows-latest + timeout-minutes: 60 + env: + OSCRIPT_CONFIG: systemlanguage=ru + MSBUILDDEBUGPATH: ${{ github.workspace }}\msbuild-debug + defaults: + run: + shell: cmd + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-dotnet@v6 + with: + dotnet-version: '8.0.x' + - uses: microsoft/setup-msbuild@v3 + - name: Russian locale + shell: pwsh + run: Set-Culture ru-RU + - uses: actions/download-artifact@v8 + with: + name: buildResults + path: built + - uses: actions/download-artifact@v8 + with: + name: nativeApiTestsDll + path: tests/native-api + - uses: actions/download-artifact@v8 + with: + name: testComponent + path: tests/component + + - name: Test + run: | + chcp 65001 > nul + msbuild Build.csproj /t:Test + + - name: Test results + if: ${{ !cancelled() }} + uses: EnricoMi/publish-unit-test-result-action/windows@v2 + with: + check_name: Windows testing + files: tests/*.xml + check_run: false + comment_mode: off + action_fail: ${{ env.TESTS_FAIL_BUILD }} + action_fail_on_inconclusive: true + - if: ${{ !cancelled() }} + uses: actions/upload-artifact@v7 + with: + name: wintests + path: tests/*.xml + - if: failure() + uses: actions/upload-artifact@v7 + with: + name: msbuild-debug-windows-testing + path: msbuild-debug/ + if-no-files-found: ignore + + linux-testing: + name: Linux testing + needs: dist + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + # Событие запуска для отчета о тестах в PR (test-results.yml) + - uses: actions/upload-artifact@v7 + with: + name: event-file + path: ${{ github.event_path }} + retention-days: 3 + - uses: actions/checkout@v7 + - uses: actions/download-artifact@v8 + with: + name: buildResults + path: built + - uses: actions/download-artifact@v8 + with: + name: nativeApiSo + path: output/na-proxy + - uses: actions/download-artifact@v8 + with: + name: nativeApiTestsSo + path: output/na-tests + - uses: actions/download-artifact@v8 + with: + name: testComponent + path: tests/component + + - name: Test + run: | + cp output/na-proxy/*.so built/linux-x64/bin/ + mkdir -p tests/native-api/build64 lintests + cp output/na-tests/*.so tests/native-api/build64/ + docker run --rm -u "$(id -u):$(id -g)" -e HOME=/tmp -e OSCRIPT_CONFIG=systemlanguage=ru \ + -v "$PWD:/w" -w /w/tests mcr.microsoft.com/dotnet/sdk:6.0 \ + dotnet ../built/linux-x64/bin/oscript.dll testrunner.os -runall . xddReportPath ../lintests || true + + - name: Test results + if: ${{ !cancelled() }} + uses: EnricoMi/publish-unit-test-result-action@v2 + with: + check_name: Linux testing + files: lintests/*.xml + check_run: false + comment_mode: off + action_fail: ${{ env.TESTS_FAIL_BUILD }} + action_fail_on_inconclusive: true + - if: ${{ !cancelled() }} + uses: actions/upload-artifact@v7 + with: + name: lintests + path: lintests/*.xml + + macos-testing: + name: macOS testing + needs: dist + runs-on: macos-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@v7 + - uses: actions/download-artifact@v8 + with: + name: buildResults + path: built + - uses: actions/download-artifact@v8 + with: + name: nativeApiDylib + path: built/osx-arm64/bin + - uses: actions/download-artifact@v8 + with: + name: nativeApiTestsDylib + path: tests/native-api/build64 + - uses: actions/download-artifact@v8 + with: + name: testComponent + path: tests/component + + # Тот же oscript, что попадет в дистрибутив osx-arm64 + - name: Test + env: + OSCRIPT_CONFIG: systemlanguage=ru + run: | + chmod +x built/osx-arm64/bin/oscript + mkdir -p mactests + cd tests + ../built/osx-arm64/bin/oscript testrunner.os -runall . xddReportPath ../mactests || true + + - name: Test results + if: ${{ !cancelled() }} + uses: EnricoMi/publish-unit-test-result-action/macos@v2 + with: + check_name: macOS testing + files: mactests/*.xml + check_run: false + comment_mode: off + action_fail: ${{ env.TESTS_FAIL_BUILD }} + action_fail_on_inconclusive: true + - if: ${{ !cancelled() }} + uses: actions/upload-artifact@v7 + with: + name: mactests + path: mactests/*.xml diff --git a/.github/workflows/dist.yml b/.github/workflows/dist.yml new file mode 100644 index 000000000..a21a1e982 --- /dev/null +++ b/.github/workflows/dist.yml @@ -0,0 +1,235 @@ +# Сборка дистрибутивов OneScript без тестов: вызывается из build.yml (с тестами) и release.yml (публикация) + +name: Distribution + +on: + workflow_call: + outputs: + version: + value: ${{ jobs.windows-build.outputs.version }} + +permissions: + contents: read + +env: + VersionPrefix: '2.3.0' + VersionSuffix: 'dev+${{ github.run_number }}' + BUILD_NUMBER: ${{ github.run_number }} + DOTNET_NOLOGO: 'true' + DOTNET_CLI_TELEMETRY_OPTOUT: 'true' + # Документация рассчитана на русскую Windows с форматами дат из NLS, + # а .NET на новых Windows по умолчанию берет форматы из ICU + DOTNET_SYSTEM_GLOBALIZATION_USENLS: 'true' + +jobs: + windows-build: + name: Windows Build + runs-on: windows-latest + timeout-minutes: 60 + env: + # MSBuild кладет сюда отчеты о падении своих процессов (MSBuild_*.failure.txt, например при MSB4166) + MSBUILDDEBUGPATH: ${{ github.workspace }}\msbuild-debug + defaults: + run: + shell: cmd + outputs: + version: ${{ steps.version.outputs.version }} + steps: + - name: Check release version + if: github.event_name == 'release' + shell: bash + run: | + FULL_VERSION="$VersionPrefix${VersionSuffix:+-$VersionSuffix}" + if [ "$GITHUB_REF_NAME" != "v$FULL_VERSION" ]; then + echo "::error::Тег $GITHUB_REF_NAME не совпадает с версией сборки $FULL_VERSION (VersionPrefix и VersionSuffix в dist.yml)" + exit 1 + fi + + - uses: actions/checkout@v7 + - uses: actions/setup-dotnet@v6 + with: + dotnet-version: '8.0.x' + - uses: microsoft/setup-msbuild@v3 + - name: Russian locale + shell: pwsh + run: Set-Culture ru-RU + + - name: Restore + run: | + chcp 65001 > nul + msbuild src/1Script.sln /t:restore + - name: Publish documenter + run: | + chcp 65001 > nul + dotnet publish src/OneScriptDocumenter/OneScriptDocumenter.csproj -c Release -o doctool + - name: Build + run: | + chcp 65001 > nul + msbuild Build.csproj /t:CleanAll;PrepareDistributionFiles;CreateNuget + # Тестовая .NET-компонента для addin.os и enum.os: Build.csproj ее не собирает, а post-build кладет ее в tests/component + - name: Build test component + run: | + chcp 65001 > nul + dotnet build src/Component/Component.csproj -c Release -p:SolutionDir=%GITHUB_WORKSPACE%\src\ + + # Версия едет вместе с дистрибутивами: по ней release.yml публикует сборку develop + - name: Version + id: version + shell: bash + run: | + FULL_VERSION="$VersionPrefix${VersionSuffix:+-$VersionSuffix}" + echo "$FULL_VERSION" > built/version.txt + echo "version=$FULL_VERSION" >> "$GITHUB_OUTPUT" + + - uses: actions/upload-artifact@v7 + with: + name: buildResults + path: built/ + include-hidden-files: true + retention-days: 3 + - uses: actions/upload-artifact@v7 + with: + name: vscode + path: built/vscode/ + retention-days: 3 + - uses: actions/upload-artifact@v7 + with: + name: nativeApiTestsDll + path: tests/native-api/bin*/*.dll + retention-days: 3 + - uses: actions/upload-artifact@v7 + with: + name: testComponent + path: tests/component/Component.dll + retention-days: 3 + - if: failure() + uses: actions/upload-artifact@v7 + with: + name: msbuild-debug-windows-build + path: msbuild-debug/ + if-no-files-found: ignore + + linux-build: + name: Linux Build + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v7 + # Сборка в контейнере вручную, а не через container: в ubuntu 18.04 не запускается node из actions + - name: Build native API + run: | + mkdir -p output + docker run --rm -u "$(id -u):$(id -g)" -v "$PWD:/w" -w /w oscript/onescript-builder:gcc sh -ec ' + mkdir -p built/tmp/na-proxy built/tmp/na-tests + cd src/ScriptEngine.NativeApi && ./build.sh && cp *.so ../../built/tmp/na-proxy && cd ../.. + cd tests/native-api && ./build.sh && cp *.so ../../built/tmp/na-tests && cd ../.. + cp -Rv built/tmp/* output/ + ' + - uses: actions/upload-artifact@v7 + with: + name: nativeApiSo + path: output/na-proxy/*.so + retention-days: 3 + - uses: actions/upload-artifact@v7 + with: + name: nativeApiTestsSo + path: output/na-tests/*.so + retention-days: 3 + + macos-build: + name: macOS Build + runs-on: macos-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v7 + # Универсальные библиотеки, сразу arm64 и x86_64. Политика 3.5 - для CMake 4 со старым cmake_minimum_required + - name: Build native API + run: | + for project in src/ScriptEngine.NativeApi tests/native-api; do + cmake -S $project -B build/$project -D CMAKE_BUILD_TYPE=Release -D CMAKE_POLICY_VERSION_MINIMUM=3.5 + cmake --build build/$project + done + mkdir -p output/na-proxy output/na-tests + cp build/src/ScriptEngine.NativeApi/*.dylib output/na-proxy/ + cp build/tests/native-api/*.dylib output/na-tests/ + lipo -info output/*/*.dylib + # Apple Silicon не загружает неподписанные библиотеки, а линковщик подписывает не все части универсальной. + # На раннере SIP выключен, поэтому подпись проверяется явно + codesign -f -s - output/*/*.dylib + codesign -v output/*/*.dylib + - uses: actions/upload-artifact@v7 + with: + name: nativeApiDylib + path: output/na-proxy/*.dylib + retention-days: 3 + - uses: actions/upload-artifact@v7 + with: + name: nativeApiTestsDylib + path: output/na-tests/*.dylib + retention-days: 3 + + vscode-debugger: + name: VSCode debugger Build + needs: windows-build + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/download-artifact@v8 + with: + name: vscode + path: built/vscode + - uses: actions/setup-node@v7 + with: + node-version: '22' + - name: Package + run: | + npm install @vscode/vsce@4.0.0 + cd built/vscode && ../../node_modules/.bin/vsce package + - uses: actions/upload-artifact@v7 + with: + name: vsix + path: built/vscode/*.vsix + + packaging: + name: Packaging + needs: [windows-build, linux-build, macos-build] + runs-on: windows-latest + timeout-minutes: 30 + defaults: + run: + shell: cmd + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-dotnet@v6 + with: + dotnet-version: '8.0.x' + - uses: microsoft/setup-msbuild@v3 + - name: Russian locale + shell: pwsh + run: Set-Culture ru-RU + - uses: actions/download-artifact@v8 + with: + name: buildResults + path: built + - uses: actions/download-artifact@v8 + with: + name: nativeApiSo + path: output/na-proxy + - uses: actions/download-artifact@v8 + with: + name: nativeApiDylib + path: output/na-mac + + - name: Pack + run: | + chcp 65001 > nul + xcopy output\na-proxy\*64.so built\linux-x64\bin\ /F || exit /b + xcopy output\na-mac\*.dylib built\osx-x64\bin\ /F || exit /b + xcopy output\na-mac\*.dylib built\osx-arm64\bin\ /F || exit /b + msbuild Build.csproj /t:PackDistributions + + - uses: actions/upload-artifact@v7 + with: + name: dist + path: built/ + include-hidden-files: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..9cbcefd37 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,295 @@ +# Публикация OneScript. +# Релиз на GitHub: сборка без тестов (dist.yml), дистрибутивы кладутся в релиз, +# сайт забирает их по вебхуку, затем NuGet и Docker. +# develop: после успешной сборки Build ее дистрибутивы выкладываются в пре-релиз night-build. +# Публикация на сайт, в NuGet и Docker Hub идет только в EvilBeaver/OneScript и требует секретов. + +name: Release + +on: + release: + types: [published] + workflow_run: + workflows: [Build] + types: [completed] + branches: [develop, release/lts] + +# Публикации одного канала не идут параллельно, чтобы файлы night-build не перемешивались +concurrency: + group: release-${{ github.event_name == 'release' && github.ref || github.event.workflow_run.head_branch }} + +permissions: + contents: read + +env: + # Сборка develop публикуется из прогона Build, релиз - из своего + BUILD_RUN_ID: ${{ github.event.workflow_run.id || github.run_id }} + BUILD_SHA: ${{ github.event.workflow_run.head_sha || github.sha }} + +jobs: + build: + name: Build + if: github.event_name == 'release' + uses: ./.github/workflows/dist.yml + + # Дистрибутивы кладутся в релиз на GitHub: при публикации релиза — в него, + # при сборке develop — в постоянный пре-релиз night-build, который каждая сборка перезаписывает + github-release: + name: Publishing to GitHub release + needs: build + if: >- + !cancelled() && ( + (github.event_name == 'release' && needs.build.result == 'success') || + (github.repository == 'EvilBeaver/OneScript' && github.event_name == 'workflow_run' && + github.event.workflow_run.event == 'push' && github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.head_branch == 'develop')) + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: write + actions: read + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + outputs: + channel: ${{ steps.release.outputs.channel }} + version: ${{ steps.release.outputs.version }} + steps: + - uses: actions/download-artifact@v8 + with: + name: dist + path: built + run-id: ${{ env.BUILD_RUN_ID }} + github-token: ${{ github.token }} + - uses: actions/download-artifact@v8 + with: + name: vsix + path: built/vscode + run-id: ${{ env.BUILD_RUN_ID }} + github-token: ${{ github.token }} + + - name: Release + id: release + env: + PRERELEASE: ${{ github.event.release.prerelease }} + run: | + FULL_VERSION=$(cat built/version.txt) + if [ "$GITHUB_EVENT_NAME" = release ]; then + TAG="$GITHUB_REF_NAME" + case "$TAG" in + v1.*) CHANNEL= ;; + *) if [ "$PRERELEASE" = true ]; then CHANNEL=preview; else CHANNEL=latest; fi ;; + esac + else + TAG=night-build + CHANNEL=night-build + # Тег переносится на собранный коммит, файлы прошлой сборки удаляются + gh api "repos/$GH_REPO/git/refs/tags/$TAG" -X PATCH -f sha="$BUILD_SHA" -F force=true > /dev/null \ + || gh api "repos/$GH_REPO/git/refs" -f ref="refs/tags/$TAG" -f sha="$BUILD_SHA" > /dev/null + NOTES="Сборка develop $FULL_VERSION, коммит $BUILD_SHA" + if gh release view "$TAG" > /dev/null 2>&1; then + gh release edit "$TAG" --notes "$NOTES" + gh release view "$TAG" --json assets --jq '.assets[].name' | while read -r name; do + gh release delete-asset "$TAG" "$name" --yes + done + else + gh release create "$TAG" --prerelease --title "Night build" --notes "$NOTES" --verify-tag + fi + fi + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + echo "channel=$CHANNEL" >> "$GITHUB_OUTPUT" + echo "version=$FULL_VERSION" >> "$GITHUB_OUTPUT" + + # Все файлы дистрибутива: сайт забирает их отсюда + - name: Upload + env: + TAG: ${{ steps.release.outputs.tag }} + run: | + mkdir release + for f in built/*.zip built/vscode/*.vsix; do + case "$(basename "$f")" in + mddoc*.zip) ;; + *) cp "$f" release/ ;; + esac + done + gh release upload "$TAG" release/* --clobber + + - name: Site payload + env: + TAG: ${{ steps.release.outputs.tag }} + CHANNEL: ${{ steps.release.outputs.channel }} + FULL_VERSION: ${{ steps.release.outputs.version }} + run: | + REPO_URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY" + gh api "repos/$GH_REPO/releases/tags/$TAG" --jq '.assets[] | [.name, .browser_download_url] | @tsv' > assets.tsv + for f in release/*; do + name=$(basename "$f") + url=$(awk -F '\t' -v n="$name" '$1 == n { print $2 }' assets.tsv) + if [ -z "$url" ]; then + echo "::error::Файла $name нет в релизе $TAG" + exit 1 + fi + size=$(stat -c %s "$f") + sha=$(sha256sum "$f" | cut -d' ' -f1) + rid=${name#OneScript-"$FULL_VERSION"-} + rid=${rid%.zip} + case "$name" in + *.vsix) kind=vsix; os=; arch= ;; + OneScript-*.zip) + case "$rid" in + fdd-*) kind=fdd; os= ;; + *) kind=scd; os=${rid%-*} ;; + esac + arch=${rid##*-} ;; + *) kind=other; os=; arch= ;; + esac + jq -n --arg name "$name" --arg kind "$kind" --arg os "$os" --arg arch "$arch" \ + --arg url "$url" --argjson size "$size" --arg sha256 "$sha" \ + '{name: $name, kind: $kind, os: (if $os == "" then null else $os end), + arch: (if $arch == "" then null else $arch end), url: $url, size: $size, sha256: $sha256}' + done | jq -s \ + --arg channel "$CHANNEL" --arg version "$FULL_VERSION" --arg commit "$BUILD_SHA" \ + --arg run "$REPO_URL/actions/runs/$BUILD_RUN_ID" --arg release "$REPO_URL/releases/tag/$TAG" \ + --arg notes "https://raw.githubusercontent.com/$GITHUB_REPOSITORY/$BUILD_SHA/install/release-notes.md" \ + '{channel: $channel, version: $version, + versionDir: (if $channel == "night-build" then null else ($version | gsub("\\."; "_")) end), + commit: $commit, run: $run, release: $release, + releaseNotes: (if $channel == "night-build" then null else $notes end), + files: .}' > site-payload.json + cat site-payload.json + - uses: actions/upload-artifact@v7 + with: + name: site-payload + path: site-payload.json + retention-days: 30 + + # Сайт сам забирает файлы из релиза по ссылкам из вебхука + publish-site: + name: Publishing to oscript.io + needs: github-release + if: github.repository == 'EvilBeaver/OneScript' && needs.github-release.outputs.channel != '' + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/download-artifact@v8 + with: + name: site-payload + - name: Webhook + env: + SITE_WEBHOOK_URL: ${{ secrets.SITE_WEBHOOK_URL }} + SITE_WEBHOOK_SECRET: ${{ secrets.SITE_WEBHOOK_SECRET }} + run: | + SIGNATURE=$(openssl dgst -sha256 -hmac "$SITE_WEBHOOK_SECRET" -r site-payload.json | cut -d' ' -f1) + # Сайт отвечает, когда файлы уже на месте: следом Build v2 ставит движок с сайта через ovm + curl --fail-with-body -sS --max-time 1200 \ + -H 'Content-Type: application/json' \ + -H "X-OneScript-Signature-256: sha256=$SIGNATURE" \ + --data-binary @site-payload.json "$SITE_WEBHOOK_URL" + + publish-nuget: + name: Publishing artifacts to clouds + needs: publish-site + if: >- + github.repository == 'EvilBeaver/OneScript' && github.event_name == 'release' && + !startsWith(github.ref, 'refs/tags/v1.') + runs-on: windows-latest + timeout-minutes: 15 + defaults: + run: + shell: cmd + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-dotnet@v6 + with: + dotnet-version: '8.0.x' + - uses: microsoft/setup-msbuild@v3 + - uses: actions/download-artifact@v8 + with: + name: buildResults + path: built + - name: Push + env: + NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }} + run: | + chcp 65001 > nul + msbuild Build.csproj /t:PublishNuget /p:NugetToken=%NUGET_TOKEN% + + docker-v1: + name: Build v1 + needs: build + if: >- + !cancelled() && github.repository == 'EvilBeaver/OneScript' && ( + (github.event_name == 'release' && startsWith(github.ref, 'refs/tags/v1.') && needs.build.result == 'success') || + (github.event_name == 'workflow_run' && github.event.workflow_run.event == 'push' && + github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'release/lts')) + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ env.BUILD_SHA }} + - name: Version + id: version + run: | + if [ "$GITHUB_EVENT_NAME" = release ]; then + # тег v1.9.4 -> версия для ovm 1.9.4 + echo "codename=$GITHUB_REF_NAME" >> "$GITHUB_OUTPUT" + echo "engine=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + else + echo "codename=lts" >> "$GITHUB_OUTPUT" + echo "engine=lts" >> "$GITHUB_OUTPUT" + fi + - uses: docker/setup-buildx-action@v4 + - uses: docker/login-action@v4 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - uses: docker/build-push-action@v7 + with: + context: . + file: install/builders/base-image/Dockerfile_v1 + build-args: VERSION=${{ steps.version.outputs.engine }} + tags: evilbeaver/onescript:${{ steps.version.outputs.codename }} + # --pull и --no-cache: обновлять базовые образы и заново ставить движок через ovm + pull: true + no-cache: true + push: true + + docker-v2: + name: Build v2 + # Образ ставит движок через ovm с сайта, поэтому после публикации + needs: [github-release, publish-site] + if: >- + github.repository == 'EvilBeaver/OneScript' && + (github.event_name == 'workflow_run' || + (github.event_name == 'release' && !github.event.release.prerelease && !startsWith(github.ref, 'refs/tags/v1.'))) + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ env.BUILD_SHA }} + - name: Version + id: version + env: + FULL_VERSION: ${{ needs.github-release.outputs.version }} + # для v2 тег образа совпадает с версией для ovm: dev или 2.2.0 + run: | + case "$FULL_VERSION" in + *-*) echo "codename=dev" >> "$GITHUB_OUTPUT" ;; + *) echo "codename=$FULL_VERSION" >> "$GITHUB_OUTPUT" ;; + esac + - uses: docker/setup-buildx-action@v4 + - uses: docker/login-action@v4 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - uses: docker/build-push-action@v7 + with: + context: . + file: install/builders/base-image/Dockerfile_v2 + build-args: VERSION=${{ steps.version.outputs.codename }} + tags: evilbeaver/onescript:${{ steps.version.outputs.codename }} + pull: true + no-cache: true + push: true diff --git a/.github/workflows/test-results.yml b/.github/workflows/test-results.yml new file mode 100644 index 000000000..30767dea2 --- /dev/null +++ b/.github/workflows/test-results.yml @@ -0,0 +1,48 @@ +# Отчет о тестах после Build: комментарий в PR со сводкой и проверка с упавшими тестами. +# Отдельный workflow, потому что у PR из форков токен в Build только на чтение. + +name: Test results + +on: + workflow_run: + workflows: [Build] + types: [completed] + +permissions: {} + +jobs: + test-results: + name: Test results + if: github.event.workflow_run.conclusion != 'skipped' && github.event.workflow_run.conclusion != 'cancelled' + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + actions: read + checks: write + contents: read + issues: read + pull-requests: write + steps: + - uses: actions/download-artifact@v8 + with: + pattern: '*tests' + path: artifacts + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + - uses: actions/download-artifact@v8 + with: + name: event-file + path: artifacts/event-file + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + + - uses: EnricoMi/publish-unit-test-result-action@v2 + with: + commit: ${{ github.event.workflow_run.head_sha }} + event_file: artifacts/event-file/event.json + event_name: ${{ github.event.workflow_run.event }} + files: artifacts/*tests/*.xml + check_name: Test results + comment_title: Результаты тестов + # Только упавшие тесты, без списков всех и пропущенных + check_run_annotations: none diff --git a/Jenkinsfile b/Jenkinsfile deleted file mode 100644 index 717207d9e..000000000 --- a/Jenkinsfile +++ /dev/null @@ -1,460 +0,0 @@ - -pipeline { - - agent none - - // Сборки одной ветки идут по очереди: параллельные публикации night-build - // через rsync --delete оставляли в папке файлы двух сборок, и сайт отдавал 404 - options { disableConcurrentBuilds() } - - environment { - VersionPrefix = '2.3.0' - VersionSuffix = 'dev'+"+${BUILD_NUMBER}" - outputEnc = '65001' - } - - stages { - stage('Build'){ - parallel { - stage('Windows Build') { - agent { label 'windows' } - - options { skipDefaultCheckout() } - - // пути к инструментам доступны только когда - // нода уже определена - environment { - NugetPath = "${tool 'nuget'}" - StandardLibraryPacks = "${tool 'os_stdlib'}" - // MSBuild кладет сюда отчеты о падении своих процессов (MSBuild_*.failure.txt, например при MSB4166), - // по умолчанию они остаются во временной папке пользователя на агенте и в сборку не попадают - MSBUILDDEBUGPATH = "${env.WORKSPACE}/msbuild-debug" - } - - steps { - dir('msbuild-debug') { deleteDir() } - - // в среде Multibranch Pipeline Jenkins первращает имена веток в папки - // а для веток Gitflow вида release/* экранирует в слэш в %2F - // При этом MSBuild, видя urlEncoding, разэкранирует его обратно, ломая путь (появляется слэш, где не надо) - // - // Поэтому, применяем костыль с кастомным workspace - // см. https://issues.jenkins-ci.org/browse/JENKINS-34564 - // - // А еще Jenkins под Windows постоянно добавляет в конец папки какую-то мусорную строку. - // Для этого отсекаем все, что находится после последнего дефиса - // см. https://issues.jenkins-ci.org/browse/JENKINS-40072 - - ws(env.WORKSPACE.replaceAll("%", "_").replaceAll(/(-[^-]+$)/, "")) - { - step([$class: 'WsCleanup']) - checkout scm - - bat "chcp $outputEnc > nul\r\n\"${tool 'MSBuild'}\" src/1Script.sln /t:restore && mkdir doctool" - bat "chcp $outputEnc > nul\r\n dotnet publish src/OneScriptDocumenter/OneScriptDocumenter.csproj -c Release -o doctool" - bat "chcp $outputEnc > nul\r\n\"${tool 'MSBuild'}\" Build.csproj /t:CleanAll;PrepareDistributionFiles;CreateNuget" - - stash includes: 'built/**', name: 'buildResults' - stash includes: 'tests/native-api/bin*/*.dll', name: 'nativeApiTestsDll' - } - } - post { - failure { - archiveArtifacts artifacts: 'msbuild-debug/**', allowEmptyArchive: true - } - } - } - - stage('Linux Build') { - agent { - docker { - image 'oscript/onescript-builder:gcc' - label 'linux' - } - } - - steps { - sh 'mkdir -p built/tmp/na-proxy && mkdir -p built/tmp/na-tests' - dir('src/ScriptEngine.NativeApi') { - sh './build.sh' - sh 'cp *.so ../../built/tmp/na-proxy' - } - dir('tests/native-api') { - sh './build.sh' - sh 'cp *.so ../../built/tmp/na-tests' - } - dir('output') { - sh 'cp -Rv ../built/tmp/* .' - } - stash includes: 'output/na-proxy/*.so', name: 'nativeApiSo' - stash includes: 'output/na-tests/*.so', name: 'nativeApiTestsSo' - } - } - } - } - stage('VSCode debugger Build') { - agent { - docker { - image 'node:22-alpine3.20' - label 'linux' - } - } - - steps { - unstash 'buildResults' - sh 'npm install @vscode/vsce' - script { - def vsceBin = pwd() + "/node_modules/.bin/vsce" - sh "cd built/vscode && ${vsceBin} package" - archiveArtifacts artifacts: 'built/vscode/*.vsix', fingerprint: true - stash includes: 'built/vscode/*.vsix', name: 'vsix' - } - } - } - - stage('Testing'){ - parallel{ - stage('Windows testing') { - agent { label 'windows' } - options { skipDefaultCheckout() } - environment { - OSCRIPT_CONFIG = 'systemlanguage=ru' - // MSBuild кладет сюда отчеты о падении своих процессов (MSBuild_*.failure.txt, например при MSB4166), - // по умолчанию они остаются во временной папке пользователя на агенте и в сборку не попадают - MSBUILDDEBUGPATH = "${env.WORKSPACE}/msbuild-debug" - } - steps { - dir('msbuild-debug') { deleteDir() } - ws(env.WORKSPACE.replaceAll("%", "_").replaceAll(/(-[^-]+$)/, "")) - { - step([$class: 'WsCleanup']) - checkout scm - - dir('install/build'){ - deleteDir() - } - unstash 'buildResults' - unstash 'nativeApiTestsDll' - bat "chcp $outputEnc > nul\r\n\"${tool 'MSBuild'}\" Build.csproj /t:Test" - - publishTestResults('tests/*.xml') - } - } - post { - failure { - archiveArtifacts artifacts: 'msbuild-debug/**', allowEmptyArchive: true - } - } - } - - stage('Linux testing') { - agent{ - docker { - image 'mcr.microsoft.com/dotnet/sdk:6.0' - label 'linux' - } - } - environment { - OSCRIPT_CONFIG = 'systemlanguage=ru' - } - - steps { - - dir('built'){ - deleteDir() - } - - unstash 'buildResults' - unstash 'nativeApiSo' - unstash 'nativeApiTestsSo' - - sh 'cp output/na-proxy/*.so ./built/linux-x64/bin/' - sh 'mkdir -p tests/native-api/build64 && cp output/na-tests/*.so ./tests/native-api/build64/' - - sh '''\ - if [ ! -d lintests ]; then - mkdir lintests - fi - rm lintests/*.xml -f - cd tests - dotnet ../built/linux-x64/bin/oscript.dll testrunner.os -runall . xddReportPath ../lintests || true - exit 0 - '''.stripIndent() - - publishTestResults('lintests/*.xml') - archiveArtifacts artifacts: 'lintests/*.xml', fingerprint: true - } - } - } - } - - stage('Packaging') { - agent { label 'windows' } - - options { skipDefaultCheckout() } - - steps { - ws(env.WORKSPACE.replaceAll("%", "_").replaceAll(/(-[^-]+$)/, "")) - { - step([$class: 'WsCleanup']) - checkout scm - - dir('built'){ - deleteDir() - } - - unstash 'buildResults' - unstash 'nativeApiSo' - - bat ''' - chcp 65001 > nul - dir output\\na-proxy - xcopy output\\na-proxy\\*64.so built\\linux-x64\\bin\\ /F - '''.stripIndent() - - bat "chcp $outputEnc > nul\r\n\"${tool 'MSBuild'}\" Build.csproj /t:PackDistributions" - - archiveArtifacts artifacts: 'built/**', fingerprint: true - stash includes: 'built/**', name: 'dist' - } - } - } - - stage ('Publishing night-build') { - when { - beforeAgent true - anyOf { - branch 'develop'; - } - } - agent { label 'master' } - options { skipDefaultCheckout() } - - steps { - cleanWs() - - unstash 'dist' - unstash 'vsix' - - publishRelease('night-build', false) - } - } - - stage ('Publishing preview') { - when { - beforeAgent true - anyOf { - branch 'release/preview'; - } - } - agent { label 'master' } - options { skipDefaultCheckout() } - - steps { - cleanWs() - checkout scm // чтобы получить файл release-notes - unstash 'dist' - unstash 'vsix' - - // Положит описание для сайта - publishReleaseNotes('preview') - - // Положит файлы дистрибутива в целевую папку - publishRelease('preview', true) - } - } - - stage ('Publishing latest') { - when { - beforeAgent true - anyOf { - branch 'release/latest'; - } - } - agent { label 'master' } - options { skipDefaultCheckout() } - - steps { - cleanWs() - checkout scm // чтобы получить файл release-notes - unstash 'dist' - unstash 'vsix' - - // Положит описание для сайта - publishReleaseNotes('latest') - - // Положит файлы дистрибутива в целевую папку - publishRelease('latest', true) - } - } - - stage ('Publishing artifacts to clouds') { - when { - beforeAgent true - anyOf { - branch 'release/latest'; - branch 'release/preview'; - } - } - - agent { label 'windows' } - - steps{ - - unstash 'buildResults' - - withCredentials([string(credentialsId: 'NuGetToken', variable: 'NUGET_TOKEN')]) { - bat "chcp $outputEnc > nul\r\n\"${tool 'MSBuild'}\" Build.csproj /t:PublishNuget /p:NugetToken=$NUGET_TOKEN" - } - } - } - - stage ('Publishing docker-images') { - parallel { - stage('Build v1') { - agent { label 'linux' } - when { - beforeAgent true - anyOf { - branch 'release/lts' - expression { - return env.TAG_NAME && env.TAG_NAME.startsWith('v1.') - } - } - } - steps { - script { - def codename = env.TAG_NAME ? env.TAG_NAME : 'lts' - // тег v1.9.4 -> версия для ovm 1.9.4 - def engineVersion = env.TAG_NAME ? env.TAG_NAME.replaceFirst(/^v/, '') : 'lts' - publishDockerImage('v1', codename, engineVersion) - } - } - } - - stage('Build v2') { - agent { label 'linux' } - when { - beforeAgent true - anyOf { - branch 'develop' - branch 'release/latest' - } - } - steps { - script { - def codename = '' - if (env.VersionSuffix != null && !env.VersionSuffix.isEmpty()) { - codename = 'dev' - } - else - { - codename = fullVersionNumber() - } - - // для v2 тег образа совпадает с версией для ovm: dev или 2.2.0 - publishDockerImage('v2', codename, codename) - } - } - } - } - } - } -} - -def testsShouldFailBuild() { - if (env.CHANGE_ID) { - return true - } - - switch (env.BRANCH_NAME) { - case 'develop': - case 'release/preview': - case 'release/latest': - case 'release/lts': - return false - default: - return true - } -} - -def publishTestResults(String pattern) { - def failBuild = testsShouldFailBuild() - def results = junit testResults: pattern, skipMarkingBuildUnstable: failBuild - if (failBuild && results.failCount > 0) { - error("${results.failCount} test(s) failed") - } -} - -def fullVersionNumber() { - def version = env.VersionPrefix - if (env.VersionSuffix != null && !env.VersionSuffix.isEmpty()) - { - version = version + "-${env.VersionSuffix}" - } - - return version -} - -def underscoredVersion() { - return fullVersionNumber().replaceAll("\\.", "_") -} - -def publishRelease(codename, isNumbered) { - dir('targetContent') { - sh """ - ZIPS=../built - NUGET=../built/nuget - VSIX=../built/vscode - mv \$ZIPS/*.zip ./ - mv \$VSIX/*.vsix ./ - - TARGET="/var/www/oscript.io/download/versions/${codename}/" - mkdir -p \$TARGET - sudo rsync -rv --delete --exclude mddoc*.zip --exclude *.src.rpm . \$TARGET - """.stripIndent() - - if (isNumbered) { - - def version = underscoredVersion() - - sh """ - TARGET="/var/www/oscript.io/download/versions/${version}/" - sudo mkdir -p \$TARGET - sudo rsync -rv --delete --exclude mddoc*.zip --exclude *.src.rpm . \$TARGET - """.stripIndent() - } - } -} - -def publishReleaseNotes(codename) { - dir('markdownContent') { - def version=underscoredVersion() - def targetDir='/var/www/oscript.io/markdown/versions' - - sh """ - cp ../install/release-notes.md "./${codename}.md" - cp ../install/release-notes.md "./${version}.md" - - sudo rsync -rv . ${targetDir} - """.stripIndent() - } -} - -def publishDockerImage(flavour, codename, engineVersion) { - def imageName = "evilbeaver/onescript:${codename}" - - // Временно, см. #1752: в buildkit с runc 1.4.3 на ядре сервера не запускается ни один RUN. - // v0.30.0 собран с runc 1.3.5. Убрать, когда выйдет buildkit с runc 1.5.1+ - def buildkitVersion = 'v0.30.0' - def builder = "onescript-buildkit-${buildkitVersion}" - sh "docker buildx inspect ${builder} > /dev/null 2>&1 || docker buildx create --name ${builder} --driver docker-container --driver-opt image=moby/buildkit:${buildkitVersion}" - - // --no-cache: слой с ovm install не меняется между сборками и иначе берется из кэша агента - // --pull: обновлять базовые образы (ovm, aspnet, mono) - docker.build( - imageName, - "--builder ${builder} --load --pull --no-cache --build-arg VERSION=${engineVersion} -f install/builders/base-image/Dockerfile_${flavour} ." - ).push() -} - diff --git a/README-EN.md b/README-EN.md index 36c6e80cb..cf5c6df4c 100644 --- a/README-EN.md +++ b/README-EN.md @@ -1,6 +1,6 @@ # OneScript # -[![Join telegram chat](https://img.shields.io/badge/chat-telegram-blue?style=flat&logo=telegram)](https://t.me/oscript_library) [![DEV Build Status](https://build.oscript.io/buildStatus/icon?job=1Script%2Fdevelop&style=flat-square&subject=dev)](https://build.oscript.io/job/1Script/job/develop/) [![STABLE Build Status](https://build.oscript.io/buildStatus/icon?job=1Script%2Fmaster&style=flat-square&subject=stable)](https://build.oscript.io/job/1Script/job/master/) +[![Join telegram chat](https://img.shields.io/badge/chat-telegram-blue?style=flat&logo=telegram)](https://t.me/oscript_library) [![DEV Build Status](https://github.com/EvilBeaver/OneScript/actions/workflows/build.yml/badge.svg?branch=develop)](https://github.com/EvilBeaver/OneScript/actions/workflows/build.yml?query=branch%3Adevelop) [![STABLE Build Status](https://github.com/EvilBeaver/OneScript/actions/workflows/build.yml/badge.svg?branch=release%2Flatest)](https://github.com/EvilBeaver/OneScript/actions/workflows/build.yml?query=branch%3Arelease%2Flatest) ## The project is an independent cross-platform implementation of a virtual machine that executes scripts written in the 1C:Enterprise language ## diff --git a/README.md b/README.md index 55b917ba8..f105ccb71 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # OneScript # -[![Join telegram chat](https://img.shields.io/badge/chat-telegram-blue?style=flat&logo=telegram)](https://t.me/oscript_library) [![DEV Build Status](https://build.oscript.io/buildStatus/icon?job=1Script%2Fdevelop&style=flat-square&subject=dev)](https://build.oscript.io/job/1Script/job/develop/) [![STABLE Build Status](https://build.oscript.io/buildStatus/icon?job=1Script%2Fmaster&style=flat-square&subject=stable)](https://build.oscript.io/job/1Script/job/master/) +[![Join telegram chat](https://img.shields.io/badge/chat-telegram-blue?style=flat&logo=telegram)](https://t.me/oscript_library) [![DEV Build Status](https://github.com/EvilBeaver/OneScript/actions/workflows/build.yml/badge.svg?branch=develop)](https://github.com/EvilBeaver/OneScript/actions/workflows/build.yml?query=branch%3Adevelop) [![STABLE Build Status](https://github.com/EvilBeaver/OneScript/actions/workflows/build.yml/badge.svg?branch=release%2Flatest)](https://github.com/EvilBeaver/OneScript/actions/workflows/build.yml?query=branch%3Arelease%2Flatest) ## Проект является независимой кросс-платформенной реализацией виртуальной машины, исполняющей скрипты на языке 1С:Предприятие ## diff --git a/src/1Script.sln b/src/1Script.sln index 86f35f9b1..1d67e4097 100644 --- a/src/1Script.sln +++ b/src/1Script.sln @@ -20,7 +20,10 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D14BF321-348C-46B8-B96A-43A22BA7AC10}" ProjectSection(SolutionItems) = preProject ..\Build.csproj = ..\Build.csproj - ..\Jenkinsfile = ..\Jenkinsfile + ..\.github\workflows\build.yml = ..\.github\workflows\build.yml + ..\.github\workflows\release.yml = ..\.github\workflows\release.yml + ..\.github\workflows\dist.yml = ..\.github\workflows\dist.yml + ..\.github\workflows\test-results.yml = ..\.github\workflows\test-results.yml oscommon.targets = oscommon.targets EndProjectSection EndProject diff --git a/src/OneScript.StandardLibrary/NativeApi/NativeApiKernel.cs b/src/OneScript.StandardLibrary/NativeApi/NativeApiKernel.cs index 7af4c3998..a04b62b61 100644 --- a/src/OneScript.StandardLibrary/NativeApi/NativeApiKernel.cs +++ b/src/OneScript.StandardLibrary/NativeApi/NativeApiKernel.cs @@ -15,31 +15,36 @@ namespace OneScript.StandardLibrary.NativeApi /// public class NativeApiKernel { - private const string KernelDll = "kernel32.dll"; public static bool IsLinux { get => System.Environment.OSVersion.Platform == PlatformID.Unix; } - private const String KernelWin = "kernel32.dll"; - private const String KernelLin = "libdl.so.2"; + internal static bool IsMacOS => RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + + /// + /// Операционная система в манифесте библиотеки внешних компонент + /// + internal static string OsName => IsMacOS ? "MacOS" : IsLinux ? "Linux" : "Windows"; + + internal static string LibraryExtension => IsMacOS ? ".dylib" : IsLinux ? ".so" : ".dll"; public static IntPtr LoadLibrary(string filename) { - return IsLinux ? LinuxLoad(filename, 1) : WindowsLoad(filename); + return NativeLibrary.TryLoad(filename, out var module) ? module : IntPtr.Zero; } public static IntPtr GetProcAddress(IntPtr module, string procName) { - var pointer = IsLinux ? LinuxProc(module, procName) : WindowsProc(module, procName); - return pointer != IntPtr.Zero + return NativeLibrary.TryGetExport(module, procName, out var pointer) ? pointer : throw new ApplicationException($"Function pointer for {procName} not obtained."); } public static bool FreeLibrary(IntPtr module) { - return IsLinux ? LinuxFree(module) == 0 : WindowsFree(module); + NativeLibrary.Free(module); + return true; } /// @@ -50,23 +55,5 @@ public static void Shutdown() { NativeApiFactory.Shutdown(); } - - [DllImport(KernelWin, SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "LoadLibrary")] - protected static extern IntPtr WindowsLoad(string lpLibFileName); - - [DllImport(KernelWin, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Ansi, EntryPoint = "GetProcAddress")] - protected static extern IntPtr WindowsProc(IntPtr module, string procName); - - [DllImport(KernelWin, SetLastError = true, EntryPoint = "FreeLibrary")] - protected static extern bool WindowsFree(IntPtr module); - - [DllImport(KernelLin, EntryPoint = "dlopen")] - protected static extern IntPtr LinuxLoad(string filename, int flags); - - [DllImport(KernelLin, EntryPoint = "dlsym")] - protected static extern IntPtr LinuxProc(IntPtr handle, string symbol); - - [DllImport(KernelLin, EntryPoint = "dlclose")] - protected static extern int LinuxFree(IntPtr handle); } } diff --git a/src/OneScript.StandardLibrary/NativeApi/NativeApiPackage.cs b/src/OneScript.StandardLibrary/NativeApi/NativeApiPackage.cs index 8f6c9580f..cce090d93 100644 --- a/src/OneScript.StandardLibrary/NativeApi/NativeApiPackage.cs +++ b/src/OneScript.StandardLibrary/NativeApi/NativeApiPackage.cs @@ -7,6 +7,7 @@ This Source Code Form is subject to the terms of the using System; using System.IO; +using System.Runtime.InteropServices; using System.Xml; using Ionic.Zip; using OneScript.Exceptions; @@ -27,9 +28,20 @@ public static bool IsZip(Stream stream) return (BitConverter.ToInt32(bytes, 0) == ZIP_LEAD_BYTES); } - private static bool Is64BitProcess() + // Архитектура процесса в обозначениях манифеста: по размеру указателя arm64 не отличить от x86_64 + private static string ProcessArch() { - return IntPtr.Size == 8; + if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64) + return "ARM64"; + + return IntPtr.Size == 8 ? "x86_64" : "i386"; + } + + // Универсальная библиотека под macOS подходит и для arm64, и для x86_64 + private static bool IsSuitableArch(string arch, string thisArch) + { + return string.Equals(arch, thisArch, StringComparison.OrdinalIgnoreCase) + || NativeApiKernel.IsMacOS && string.Equals(arch, "Universal", StringComparison.OrdinalIgnoreCase); } public static void Extract(Stream stream, String tempfile) @@ -54,13 +66,13 @@ private static String GetEntryName(ZipFile zip) stream.Seek(0, 0); using (var reader = XmlReader.Create(stream)) { - var thisOs = NativeApiProxy.IsLinux ? "Linux" : "Windows"; - var thisArch = Is64BitProcess() ? "x86_64" : "i386"; + var thisOs = NativeApiKernel.OsName; + var thisArch = ProcessArch(); while (reader.ReadToFollowing("component")) { var attrOs = reader.GetAttribute("os"); var attrArch = reader.GetAttribute("arch"); - if (string.Equals(attrArch, thisArch, StringComparison.OrdinalIgnoreCase) + if (IsSuitableArch(attrArch, thisArch) && string.Equals(attrOs, thisOs, StringComparison.OrdinalIgnoreCase)) return reader.GetAttribute("path"); } diff --git a/src/OneScript.StandardLibrary/NativeApi/NativeApiProxy.cs b/src/OneScript.StandardLibrary/NativeApi/NativeApiProxy.cs index f3871766d..8e0b36795 100644 --- a/src/OneScript.StandardLibrary/NativeApi/NativeApiProxy.cs +++ b/src/OneScript.StandardLibrary/NativeApi/NativeApiProxy.cs @@ -190,7 +190,7 @@ static NativeApiProxy() string filename = System.IO.Path.GetDirectoryName(location) + System.IO.Path.DirectorySeparatorChar + "ScriptEngine.NativeApi" + (IntPtr.Size == 8 ? "64" : "32") - + (NativeApiKernel.IsLinux ? ".so" : ".dll"); + + NativeApiKernel.LibraryExtension; IntPtr module = NativeApiKernel.LoadLibrary(filename); if (module == IntPtr.Zero) { diff --git a/src/ScriptEngine.NativeApi/CMakeLists.txt b/src/ScriptEngine.NativeApi/CMakeLists.txt index 50d45b1d0..d236b74af 100644 --- a/src/ScriptEngine.NativeApi/CMakeLists.txt +++ b/src/ScriptEngine.NativeApi/CMakeLists.txt @@ -11,7 +11,11 @@ add_library(${TARGET} SHARED target_compile_definitions(${TARGET} PRIVATE UNICODE _UNICODE) target_include_directories(${TARGET} PRIVATE include) -if (UNIX) +if (APPLE) + # Одна библиотека и для osx-arm64, и для osx-x64 + set(MySuffix "64") + set_target_properties(${TARGET} PROPERTIES PREFIX "" OSX_ARCHITECTURES "arm64;x86_64") +elseif (UNIX) if (TARGET_PLATFORM_32) set(MySuffix "32") set(CMAKE_LIBRARY_PATH "/usr/lib/i386-linux-gnu" CACHE PATH "") diff --git a/src/ScriptEngine/ScriptingEngine.cs b/src/ScriptEngine/ScriptingEngine.cs index 5b8340900..ec7a7e079 100644 --- a/src/ScriptEngine/ScriptingEngine.cs +++ b/src/ScriptEngine/ScriptingEngine.cs @@ -5,6 +5,7 @@ This Source Code Form is subject to the terms of the at http://mozilla.org/MPL/2.0/. ----------------------------------------------------------*/ using System; +using System.Runtime.InteropServices; using OneScript.Compilation; using OneScript.Contexts; using OneScript.DependencyInjection; @@ -125,18 +126,13 @@ public ICompilerFrontend GetCompilerService() var compiler = scope.Resolve(); compiler.SharedSymbols = _runtimeEnvironment.GetSymbolTable(); - switch (System.Environment.OSVersion.Platform) - { - case PlatformID.Unix: - compiler.PreprocessorDefinitions.Add("Linux"); - break; - case PlatformID.MacOSX: - compiler.PreprocessorDefinitions.Add("MacOS"); - break; - case PlatformID.Win32NT: - compiler.PreprocessorDefinitions.Add("Windows"); - break; - } + // Environment.OSVersion.Platform на macOS возвращает Unix, а не MacOSX + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + compiler.PreprocessorDefinitions.Add("Windows"); + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + compiler.PreprocessorDefinitions.Add("MacOS"); + else + compiler.PreprocessorDefinitions.Add("Linux"); compiler.GenerateDebugCode = ProduceExtraCode.HasFlag(CodeGenerationFlags.DebugCode); compiler.GenerateCodeStat = ProduceExtraCode.HasFlag(CodeGenerationFlags.CodeStatistics); diff --git a/tests/directives.os b/tests/directives.os index 619e1c9d0..db5669579 100644 --- a/tests/directives.os +++ b/tests/directives.os @@ -10,6 +10,7 @@ ВсеТесты.Добавить("ТестДолжен_ПроверитьЧтоИспользоватьНеКомпилируетсяЕслиНачалсяКод"); ВсеТесты.Добавить("ТестДолжен_ПроверитьЧтоМногострочныеДирективыНеКомпилируются"); ВсеТесты.Добавить("ТестДолжен_ПроверитьЧтоИсключенныеСтрокиПропускаются"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьСимволОперационнойСистемы"); Возврат ВсеТесты; @@ -26,6 +27,28 @@ КонецФункции +Функция ТестДолжен_ПроверитьСимволОперационнойСистемы() Экспорт + + Определены = Новый Массив; + #Если Windows Тогда + Определены.Добавить("Windows"); + #КонецЕсли + #Если Linux Тогда + Определены.Добавить("Linux"); + #КонецЕсли + #Если MacOS Тогда + Определены.Добавить("MacOS"); + #КонецЕсли + + // ТипПлатформы определяет систему сам, по ядру: Windows_x86_64, Linux_x86_64, MacOS_x86_64 + Платформа = Строка(Новый СистемнаяИнформация().ТипПлатформы); + Ожидаемый = Лев(Платформа, СтрНайти(Платформа, "_") - 1); + + юТест.ПроверитьРавенство(1, Определены.Количество(), "Определен один символ операционной системы"); + юТест.ПроверитьРавенство(Ожидаемый, Определены[0]); + +КонецФункции + Функция ФайлКомпилируется(Знач ПутьКФайлу, Знач ИмяПодключения = "НеПодключится") Попытка diff --git a/tests/formatting.os b/tests/formatting.os index 62744d923..768182804 100644 --- a/tests/formatting.os +++ b/tests/formatting.os @@ -18,7 +18,7 @@ ВсеТесты.Добавить("ТестДолжен_ПроверитьФорматированиеБольшойРазрядности"); ВсеТесты.Добавить("ТестДолжен_ПроверитьФорматированиеПустаяДата"); ВсеТесты.Добавить("ТестДолжен_ПроверитьФорматированиеЛокальнаяДата"); - #Если Не Linux Тогда + #Если Windows Тогда //TODO: https://github.com/EvilBeaver/OneScript/issues/1228 ВсеТесты.Добавить("ТестДолжен_ПроверитьФорматированиеЛокальнаяДатаВремя"); ВсеТесты.Добавить("ТестДолжен_ПроверитьФорматированиеДлинныйФорматЛокальнойДаты"); diff --git a/tests/native-api.os b/tests/native-api.os index f360f4434..5a4a36653 100644 --- a/tests/native-api.os +++ b/tests/native-api.os @@ -32,10 +32,8 @@ #Если Windows Тогда ЭтоWindows = Истина; - #ИначеЕсли Linux Тогда - ЭтоWindows = Ложь; #Иначе - Возврат Новый Массив; // под макос не тестируем + ЭтоWindows = Ложь; #КонецЕсли Возврат ВсеТесты; @@ -55,6 +53,8 @@ ПутьБиблиотеки = ОбъединитьПути(Каталог, "ScriptEngine.NativeApi32.so"); ИначеЕсли СистемнаяИнформация.ТипПлатформы = ТипПлатформы.Linux_x86_64 Тогда ПутьБиблиотеки = ОбъединитьПути(Каталог, "ScriptEngine.NativeApi64.so"); + ИначеЕсли СистемнаяИнформация.ТипПлатформы = ТипПлатформы.MacOS_x86_64 Тогда + ПутьБиблиотеки = ОбъединитьПути(Каталог, "ScriptEngine.NativeApi64.dylib"); Иначе ПутьБиблиотеки = ""; КонецЕсли; @@ -85,6 +85,8 @@ ПутьБиблиотеки = ОбъединитьПути(КаталогСборок, "build32", "AddInNativeLin32.so"); ИначеЕсли СистемнаяИнформация.ТипПлатформы = ТипПлатформы.Linux_x86_64 Тогда ПутьБиблиотеки = ОбъединитьПути(КаталогСборок, "build64", "AddInNativeLin64.so"); + ИначеЕсли СистемнаяИнформация.ТипПлатформы = ТипПлатформы.MacOS_x86_64 Тогда + ПутьБиблиотеки = ОбъединитьПути(КаталогСборок, "build64", "AddInNativeMac.dylib"); Иначе ВызватьИсключение "Неподдерживаемый тип платформы: " + СистемнаяИнформация.ТипПлатформы; КонецЕсли; @@ -115,6 +117,7 @@ Каталог = ТекущийСценарий().Каталог + "/native-api/"; МасивФайлов.Добавить("build32/AddInNativeLin32.so"); МасивФайлов.Добавить("build64/AddInNativeLin64.so"); + МасивФайлов.Добавить("build64/AddInNativeMac.dylib"); КонецЕсли; ЗаписьZIP.Добавить(Каталог + "MANIFEST.XML", РежимСохраненияПутейZIP.НеСохранятьПути); diff --git a/tests/native-api/AddInNative.cpp b/tests/native-api/AddInNative.cpp index 5ecbe870a..460efa2ad 100644 --- a/tests/native-api/AddInNative.cpp +++ b/tests/native-api/AddInNative.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #endif #include diff --git a/tests/native-api/CMakeLists.txt b/tests/native-api/CMakeLists.txt index b1bc69220..112d32a68 100644 --- a/tests/native-api/CMakeLists.txt +++ b/tests/native-api/CMakeLists.txt @@ -21,7 +21,11 @@ include_directories(${CMAKE_SOURCE_DIR}/include) SET (CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} ${CMAKE_SOURCE_DIR}) SET(AddInDef_SRC) -if (UNIX) +if (APPLE) + # Одна библиотека и для arm64, и для x86_64 + set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64") + set(MySuffix "Mac") +elseif (UNIX) if (TARGET_PLATFORM_32) set(CMAKE_CXX_FLAGS "-m32 ${CMAKE_CXX_FLAGS}") set(MySuffix "Lin32") diff --git a/tests/native-api/MANIFEST.XML b/tests/native-api/MANIFEST.XML index 03bb40552..c47eac462 100644 --- a/tests/native-api/MANIFEST.XML +++ b/tests/native-api/MANIFEST.XML @@ -4,4 +4,5 @@ + diff --git a/tests/preprocessor/excluded.os b/tests/preprocessor/excluded.os index 2674374db..a8021b516 100644 --- a/tests/preprocessor/excluded.os +++ b/tests/preprocessor/excluded.os @@ -2,7 +2,7 @@ !! #КонецЕсли -#Если Windows ИЛИ Linux Тогда //!! ok +#Если Windows ИЛИ Linux ИЛИ MacOS Тогда //!! ok #ИначеЕсли НЕ MacOS Тогда //?? !! diff --git a/tests/sysinfo.os b/tests/sysinfo.os index 0e63bf5a3..51f759d07 100644 --- a/tests/sysinfo.os +++ b/tests/sysinfo.os @@ -103,11 +103,19 @@ юТест.ПроверитьРавенство(СИ.ТипПлатформы, ТипПлатформы.Windows_x86); КонецЕсли; Иначе + #Если MacOS Тогда + Если Си.Это64БитнаяОперационнаяСистема Тогда + юТест.ПроверитьРавенство(СИ.ТипПлатформы, ТипПлатформы.MacOS_x86_64); + Иначе + юТест.ПроверитьРавенство(СИ.ТипПлатформы, ТипПлатформы.MacOS_x86); + КонецЕсли; + #Иначе Если Си.Это64БитнаяОперационнаяСистема Тогда юТест.ПроверитьРавенство(СИ.ТипПлатформы, ТипПлатформы.Linux_x86_64); Иначе юТест.ПроверитьРавенство(СИ.ТипПлатформы, ТипПлатформы.Linux_x86); КонецЕсли; + #КонецЕсли КонецЕсли; КонецПроцедуры diff --git a/tests/testrunner.os b/tests/testrunner.os index 7214f48e7..70e1b6a52 100644 --- a/tests/testrunner.os +++ b/tests/testrunner.os @@ -486,13 +486,19 @@ КонецЕсли; Сообщить("Не удалось загрузить тест "+ПолноеИмяТестовогоСлучая+Символы.ПС+ текстОшибки); + ДобавитьНеЗагруженныйНабор(ПолноеИмяТестовогоСлучая, ИмяКлассаТеста, текстОшибки, СтрокаОшибкиВФайле(ИнфоОшибки, ПолноеИмяТестовогоСлучая)); Рез = Ложь; РезультатТестирования = ЗначенияСостоянияТестов.Сломался; Продолжить; КонецПопытки; - МассивТестовыхСлучаев = ПолучитьТестовыеСлучаи(Тест, ПолноеИмяТестовогоСлучая); - Если МассивТестовыхСлучаев = Неопределено Тогда + ОшибкаЗагрузки = Неопределено; + МассивТестовыхСлучаев = ПолучитьТестовыеСлучаи(Тест, ПолноеИмяТестовогоСлучая, ОшибкаЗагрузки); + Если ОшибкаЗагрузки <> Неопределено Тогда + ДобавитьНеЗагруженныйНабор(ПолноеИмяТестовогоСлучая, ИмяКлассаТеста, ОшибкаЗагрузки); + Рез = Ложь; + Продолжить; + ИначеЕсли МассивТестовыхСлучаев = Неопределено Тогда Сообщить("Нет тестовых случаев в файле " + Файл.ПолноеИмя); Продолжить; КонецЕсли; @@ -548,6 +554,23 @@ Возврат Рез; КонецФункции +// Незагруженный набор - упавший тест, а не молча пропущенный файл: попадает в итог и в отчет +Процедура ДобавитьНеЗагруженныйНабор(ПолноеИмя, ИмяКласса, ТекстОшибки, СтрокаОшибки = Неопределено) + + ОписаниеТеста = Новый Структура; + ОписаниеТеста.Вставить("ТестОбъект", Неопределено); + ОписаниеТеста.Вставить("ИмяКласса", ИмяКласса); + ОписаниеТеста.Вставить("ПолноеИмя", ПолноеИмя); + ОписаниеТеста.Вставить("Параметры", Неопределено); + ОписаниеТеста.Вставить("ИмяМетода", "ЗагрузкаНабораТестов"); + ОписаниеТеста.Вставить("ОшибкаЗагрузки", ТекстОшибки); + ОписаниеТеста.Вставить("СтрокаОшибкиЗагрузки", СтрокаОшибки); + + НаборТестов.Добавить(ОписаниеТеста); + РезультатТестирования = ЗначенияСостоянияТестов.Сломался; + +КонецПроцедуры + Функция ВыполнитьВсеТесты() Если НаборТестов.Количество() > 0 Тогда НаборОшибок = Новый Соответствие; @@ -562,7 +585,9 @@ Для Сч = 0 По НаборТестов.Количество() - 1 Цикл ОписаниеТеста = НаборТестов[Сч]; + НачалоТеста = ТекущаяУниверсальнаяДатаВМиллисекундах(); НовыйРезультатТестирования = ВыполнитьТест(ОписаниеТеста, Сч); + ОписаниеТеста.Вставить("Длительность", (ТекущаяУниверсальнаяДатаВМиллисекундах() - НачалоТеста) / 1000); РезультатТестирования = ЗапомнитьСамоеХудшееСостояние(РезультатТестирования, НовыйРезультатТестирования); КонецЦикла; @@ -649,6 +674,14 @@ ФайлТеста = Новый Файл(ПутьНабора); ЗаписьXML.ЗаписатьАтрибут("name", ИмяНабора); + + ДлительностьНабора = 0; + Для Каждого ОписаниеТеста Из НаборТестов Цикл + Если ОписаниеТеста.Свойство("Длительность") Тогда + ДлительностьНабора = ДлительностьНабора + ОписаниеТеста.Длительность; + КонецЕсли; + КонецЦикла; + ЗаписьXML.ЗаписатьАтрибут("time", XMLСтрока(ДлительностьНабора)); ЗаписьXML.ЗаписатьНачалоЭлемента("properties"); ЗаписьXML.ЗаписатьКонецЭлемента(); @@ -677,6 +710,10 @@ ЗаписьXML.ЗаписатьНачалоЭлемента("testcase"); ЗаписьXML.ЗаписатьАтрибут("classname", ИмяТекущегоТеста(ОписаниеТеста.ПолноеИмя)); ЗаписьXML.ЗаписатьАтрибут("name", ОписаниеТеста.ИмяМетода); + ЗаписьXML.ЗаписатьАтрибут("file", ПутьОтКорняРепозитория(ОписаниеТеста.ПолноеИмя)); + Если ОписаниеТеста.Свойство("Длительность") Тогда + ЗаписьXML.ЗаписатьАтрибут("time", XMLСтрока(ОписаниеТеста.Длительность)); + КонецЕсли; СтруктураОшибки = НаборОшибок.Получить(ОписаниеТеста); @@ -685,6 +722,10 @@ КонецЕсли; Если СтруктураОшибки <> Неопределено Тогда + Если СтруктураОшибки.Свойство("СтрокаВФайле") И СтруктураОшибки.СтрокаВФайле <> Неопределено Тогда + ЗаписьXML.ЗаписатьАтрибут("line", XMLСтрока(СтруктураОшибки.СтрокаВФайле)); + КонецЕсли; + СтрокаРезультат = ?(СтруктураОшибки.СостояниеВыполнения = ЗначенияСостоянияТестов.Сломался, "failure", "skipped"); ЗаписьXML.ЗаписатьАтрибут("status", СтрокаРезультат); @@ -704,6 +745,43 @@ КонецПроцедуры +// Путь теста от каталога над testrunner, то есть от корня репозитория: по нему отчет показывает ошибку у файла +Функция ПутьОтКорняРепозитория(Знач ПолноеИмя) + Корень = Новый Файл(ТекущийСценарий().Каталог).Путь; + Если Не СтрНачинаетсяС(ПолноеИмя, Корень) Тогда + Возврат ПолноеИмя; + КонецЕсли; + Возврат СтрЗаменить(Сред(ПолноеИмя, СтрДлина(Корень) + 1), "\", "/"); +КонецФункции + +// Строка файла теста, на которой случилась ошибка +Функция СтрокаОшибкиВФайле(Знач ИнфоОшибки, Знач ПутьФайла) + + Если ИнфоОшибки = Неопределено Тогда + Возврат Неопределено; + КонецЕсли; + + Стек = ИнфоОшибки.ПолучитьСтекВызовов(); + Если Стек <> Неопределено Тогда + Для Каждого Кадр Из Стек Цикл + Если Кадр.ИмяМодуля = ПутьФайла Тогда + Возврат Кадр.НомерСтроки; + КонецЕсли; + КонецЦикла; + КонецЕсли; + + // У ошибки компиляции стека нет: место - в самой ошибке или в ее причине + Пока ИнфоОшибки <> Неопределено Цикл + Если ИнфоОшибки.ИмяМодуля = ПутьФайла И ИнфоОшибки.НомерСтроки > 0 Тогда + Возврат ИнфоОшибки.НомерСтроки; + КонецЕсли; + ИнфоОшибки = ИнфоОшибки.Причина; + КонецЦикла; + + Возврат Неопределено; + +КонецФункции + Функция ПутьФайлаОтчетаТестированияВФорматеJUnitXML() Возврат ?(ЗначениеЗаполнено(ПутьЛогФайлаJUnit), ПутьЛогФайлаJUnit, ТекущийКаталог()); КонецФункции @@ -719,6 +797,11 @@ Тест = ОписаниеТеста.ТестОбъект; ИмяМетода = ОписаниеТеста.ИмяМетода; + Если ОписаниеТеста.Свойство("ОшибкаЗагрузки") Тогда + ПоказатьИнформациюПоТесту(ОписаниеТеста, Сч, ИмяМетода); + Возврат ВывестиОшибкуВыполненияТеста(ЗначенияСостоянияТестов.Сломался, "Набор тестов не загружен", ОписаниеТеста, ОписаниеТеста.ОшибкаЗагрузки, Неопределено); + КонецЕсли; + Успешно = ВыполнитьПроцедуруТестовогоСлучая(Тест, "ПередЗапускомТеста", ИмяМетода, ОписаниеТеста); Если Не Успешно Тогда Рез = ЗначенияСостоянияТестов.Сломался; @@ -777,6 +860,10 @@ Функция ВывестиОшибкуВыполненияТеста(СостояниеВыполнения, ПредставлениеОшибки, ОписаниеТеста, текстОшибки, ИнфоОшибки) ИмяМетода = ОписаниеТеста.ИмяМетода; + СтрокаВФайле = СтрокаОшибкиВФайле(ИнфоОшибки, ОписаниеТеста.ПолноеИмя); + Если СтрокаВФайле = Неопределено И ОписаниеТеста.Свойство("СтрокаОшибкиЗагрузки") Тогда + СтрокаВФайле = ОписаниеТеста.СтрокаОшибкиЗагрузки; + КонецЕсли; сообщение = ?(ПредставлениеОшибки="", "", ПредставлениеОшибки + Символы.ПС) + "Тест: <" + ИмяМетода + ">" + Символы.ПС + @@ -819,6 +906,7 @@ СтруктураОшибки.Вставить("Описание", текстОшибки); СтруктураОшибки.Вставить("ИнфоОшибки", стИнфоОшибки); СтруктураОшибки.Вставить("ПолныйПуть", ОписаниеТеста.ПолноеИмя); + СтруктураОшибки.Вставить("СтрокаВФайле", СтрокаВФайле); Если СостояниеВыполнения = ЗначенияСостоянияТестов.Сломался Тогда НаборОшибок.Вставить(ОписаниеТеста, СтруктураОшибки); @@ -851,7 +939,7 @@ Возврат Успешно; КонецФункции -Функция ПолучитьТестовыеСлучаи(ТестОбъект, ПолноеИмяОбъекта) +Функция ПолучитьТестовыеСлучаи(ТестОбъект, ПолноеИмяОбъекта, ОшибкаЗагрузки = Неопределено) Попытка @@ -866,7 +954,7 @@ текстОшибки = ?(ВыводитьОшибкиПодробно, ИнформацияОбОшибке().ПодробноеОписаниеОшибки(), ОписаниеОшибки()); - ВывестиОшибку("Набор тестов не загружен: " + ПолноеИмяОбъекта + " + ОшибкаЗагрузки = ВывестиОшибку("Набор тестов не загружен: " + ПолноеИмяОбъекта + " | Ошибка получения списка тестовых случаев: " + текстОшибки); ТестОбъект = Неопределено; @@ -878,14 +966,14 @@ Если ТипЗнч(МассивТестовыхСлучаев) <> Тип("Массив") Тогда - ВывестиОшибку("Набор тестов не загружен: " + ПолноеИмяОбъекта + " + ОшибкаЗагрузки = ВывестиОшибку("Набор тестов не загружен: " + ПолноеИмяОбъекта + " | Ошибка получения списка тестовых случаев: вместо массива имен тестовых случаев получен объект <" + Строка(ТипЗнч(МассивТестовыхСлучаев)) + ">"); ТестОбъект = Неопределено; Возврат Неопределено; КонецЕсли; - Если НЕ ПроверитьМассивТестовыхСлучаев(МассивТестовыхСлучаев, ТестОбъект, ПолноеИмяОбъекта) Тогда + Если НЕ ПроверитьМассивТестовыхСлучаев(МассивТестовыхСлучаев, ТестОбъект, ПолноеИмяОбъекта, ОшибкаЗагрузки) Тогда Возврат Неопределено; КонецЕсли; @@ -893,19 +981,19 @@ КонецФункции -Функция ПроверитьМассивТестовыхСлучаев(МассивТестовыхСлучаев, ТестОбъект, ПолноеИмяОбъекта) +Функция ПроверитьМассивТестовыхСлучаев(МассивТестовыхСлучаев, ТестОбъект, ПолноеИмяОбъекта, ОшибкаЗагрузки) Для каждого данныеТеста из МассивТестовыхСлучаев Цикл Если ТипЗнч(данныеТеста) = Тип("Строка") Тогда Продолжить; КонецЕсли; Если ТипЗнч(данныеТеста) <> Тип("Структура") Тогда - ВывестиОшибку("Набор тестов не загружен: " + ПолноеИмяОбъекта + " + ОшибкаЗагрузки = ВывестиОшибку("Набор тестов не загружен: " + ПолноеИмяОбъекта + " | Ошибка получения структуры описания тестового случая: " + ОписаниеОшибки()); Возврат Ложь; КонецЕсли; Если НЕ данныеТеста.Свойство("ИмяТеста") Тогда - ВывестиОшибку("Набор тестов не загружен: " + ПолноеИмяОбъекта + " + ОшибкаЗагрузки = ВывестиОшибку("Набор тестов не загружен: " + ПолноеИмяОбъекта + " | Не задано имя теста в структуре описания тестового случая: " + ОписаниеОшибки()); Возврат Ложь; КонецЕсли;