Контроль кода Powershell
Необязательное вступление
Разработка на любом языке программирования неразрывно связана с проблематикой управления накапливаемой кодовой базой. Чем больше самого́ кода, участников разработки, тем более важно соблюдать общие стандарты кодирования. С ростом числа потребителей разработанных решений, покрытием процессов автоматизацией, растёт и потребность в обеспечении стабильности работы этих решений. Отсюда приходим к необходимости внедрения общеизвестных практик постоянной интеграции и непрерывной поставки (CI/CD).
Язык Powershell нередко применяется в коллективах исключительно для выполнения одноразовых около-админских ad hoc сценариев — в такой ситуации проблематика разработки ПО, очевидно, не актуальна. Открыл терминал, выполнил и забыл. Но так сложилось, что в нашем коллективе пайплайны CI/CD нормальных проектов построены как раз на скриптах Powershell. Да, крутится всё на TeamCity, оно умеет клонировать исходники из наблюдаемого репозитория и даже поддерживает некоторый набор шаблонных шагов, не требующих программирования при конфигурировании. Удобные шаблоны действительно заметно помогают собрать базовый минимальный пайплайн, однако ничем не облегчают построение развесистых сценариев, характе́рных для больших коллективов с развитой фантазией. Особенно если билдить приходится что-то не совсем стандартное. Так, у нас билд-конфиг TeamCity для сборки PR по проекту на T-SQL насчитывает 70 шагов, 180 параметров — столько шаблонов не бывает. Бо́льшая часть этих шагов, конечно же, кастомные наработки.
Изначально этим занимательным скриптописательством развлекался только я, со временем скриптов накопилось несколько десятков. «Внезапно» оказалось, что якобы имеющийся в голове стандарт оформления соблюдать получается только в пределах одного скрипта и лишь приблизительно. Соседний скрипт, написанный в нечётный день недели и без предварительного употребления двойной порции кофе, почему-то оказывался весь в PascalCase вместо camelCase. Со всеми прочими оформительскими вариациями происходила аналогичная история. IDE под названием Notepad++ не сказать, что предлагала какую-либо помощь в этих вопросах. А периодическое дёрганье скриптов в терминале навряд ли позволяло называть это тестированием.
Сто́ит отметить, что к реализации первой задачи по автоматизации работы с некоторым API на павершеле я подошёл в состоянии «окей, гугл, как там выглядит апишка, с которой нужно работать» и «кстати, гугл, дай-ка мне что-нибудь на тему powershell basic syntax, powershell quickstart». В ближайшем окружении трудились похожие специалисты. Более того, интенсивное гугление показало, что в мире powershell скорее одноразовые скрипты и преобладают, сколь-нибудь взрослый SDLC примерно никто не пытается построить вообще. Или не спешит делиться опытом. Поэтому построение процесса двигалось поступательно, какие-то решения могут показаться неожиданными. Сравнивать всё ещё особо не с чем.
Когда нас, активных скриптописателей, стало несколько, проблемы, тесно связанные с отсутствием нормального процесса разработки, скажем так, заиграли более яркими красками, и мы дружно пошли искать варианты решения накопившихся вопросов.
Среда разработки
Довольно быстро выяснилось, что самый нормальный вариант — VS Code. К студии есть повершельный плагин, который подсвечивает синтаксис и предоставляет функцию code completion.
Отчасти похожий функционал предоставляет среда Powershell ISE. Однако, оно изначально было чем-то отдельно стоящим, плюс коллеги, кому нравилось пользоваться этой средой, наткнулись на несколько непоправимых ситуаций: ничем не примечательный код работал где угодно кроме ISE. Тем, кто привык, пришлось отказываться. Сегодня главная страница описания ISE содержит объявление, что нужно пользоваться VS Code с экстеншеном Powershell.
Этот и другие экстеншены можно добавить в параметры workspace, закоммитить этот файл. Разработчик после клонирования репозитория сможет открыть в VS Code этот workspace и сразу получит рекомендации по расширениям, которые нужно установить для комфортной работы.
"extensions": {
"recommendations": [
"ms-vscode.powershell",
"pspester.pester-test",
"ms-vscode.test-adapter-converter",
"hbenl.vscode-test-explorer"
]
}
Форматирование и линтинг
Экстеншен к VS Code включает в себя PSScriptAnalyzer, код не только подсвечивается, но также форматируется автоматически и линтится. Правил в PSScriptAnayzer не много, но проект живой, изредка что-то да добавляется.
Подсветка замечаний от PSScriptAnalyzer в IDE VS Code
Из забавного: в этом линтере есть понятие «опасных глаголов» и если вы, например, даёте имя методу Delete-Something
, то линтер тут же начинает настойчиво рекомендовать навесить атрибут ShouldProcess
, чтобы опасное действие при отладке можно было гонять вхолостую, ничего фактически не удаляя. Без влияния на окружение.
PSScriptAnalyzer’ом можно пользоваться отдельно от студии, то есть получится его применять не только на стороне разработчика, но и в пайплайне CI — контролировать соблюдение включённых правил. Файл настроек подойдёт тот же, что применяется в VS Code.
@{
Rules = @{
PSAvoidUsingCmdletAliases = @{
Whitelist = @('%', '?')
}
PSAvoidSemicolonsAsLineTerminators = @{
Enable = $true
}
PSUseCorrectCasing = @{
Enable = $false # too slow
}
}
ExcludeRules = @(
'PSAvoidUsingWriteHost',
'PSAvoidUsingInvokeExpression',
'PSUseDeclaredVarsMoreThanAssignments',
'PSUseApprovedVerbs',
'PSReviewUnusedParameter',
'PSAvoidUsingPlainTextForPassword',
'PSAvoidUsingConvertToSecureStringWithPlainText')
}
Путь к этому файлу конфигурации и другие параметры расширения настраиваются довольно прозрачно:
"[powershell]": {
"editor.tabSize": 4,
"editor.defaultFormatter": "ms-vscode.powershell"
},
"powershell.codeFormatting.useCorrectCasing": true,
"powershell.codeFormatting.whitespaceBetweenParameters": true,
"powershell.integratedConsole.suppressStartupBanner": true,
"powershell.integratedConsole.showOnStartup": false,
"powershell.scriptAnalysis.settingsPath": "./.vscode/PSScriptAnalyzerSettings.psd1",
"powershell.codeFormatting.pipelineIndentationStyle": "IncreaseIndentationForFirstPipeline",
"powershell.developer.editorServicesLogLevel": "Warning",
Это фрагмент из всё того же *.code-workspace
файла, который коммитится, и таким образом базовые настройки у всех разработчиков синхронизируются.
Соглашения
Разработчики языка Powershell и активные евангелисты дают некоторые рекомендации по кодированию, в частности:
богато описывать параметры функций;
указывать возвращаемый тип;
использовать Pascal-Kebab-Case для именования функций;
для первого слова в таких именах выбирать глаголы из справочника;
входящие параметры именовать в PascalCase, а локальные переменные — в camelCase;
оформлять тело метода в begin-process-end стиле, даже если использование функции в пайпе не предполагается;
не завершать выражения точкой с запятой.
Много хорошего советуют, однако и неоднозначных идей хватает.
Дополнительно к этим рекомендациям в нашем коллективе зафиксированы такие соглашения:
основное тело скрипта, предназначенного для вызова из command line, всегда заключаем в блок (метод), который именуем
Main
сразу после объявления параметров скрипта добавляем блок установки базовых опций:
повышенный режим Strict, который приводит к возникновению ошибок при обращении, например, к необъявленной переменной;
ошибка должна приводить к остановке выполнения;
кодировка выхлопа — utf8; иначе кириллица ломается;
при сохранении файлов всегда явно указываем кодировку;
параметры передаём по имени, не по позиции (кроме самых базовых функций, адаптированных к работе в пайпе);
для скриптов и их параметров пишем описание; где упустили — стараемся дописывать;
объявляя параметры, каждый атрибут пишем на отдельной строке, само имя параметра — тоже, иначе имена разъезжаются по ширине;
в скриптах, вызываемых напрямую из шагов сборки или CLI, первым делом выводим через
Write-Verbose
все значения входных параметров — это сильно помогает в отладке CI-CD-шных скриптов.
И некоторые другие вещи. Линтер подобное контролировать не умеет, оставляем на ревью. Не забыть основное помогают сниппеты.
Сниппет для нового Powershell-скрипта
{
"Init new script": {
"scope": "ps1,powershell",
"prefix": "posh-snippet-script",
"description": "Init cmdlet script",
"body": [
"<#",
".SYNOPSIS",
" tbd",
"",
".PARAMETER $1",
" tbd",
"",
".PARAMETER $2",
" tbd",
"",
".EXAMPLE",
" ./$TM_FILENAME -$1 foo -$2 bar",
"",
" Description",
" -----------",
" tbd",
"",
".NOTES",
"Version: 1.0",
"Author: ?",
"Creation Date: $CURRENT_YEAR-$CURRENT_MONTH-$CURRENT_DATE",
"Original name: $TM_FILENAME",
"#>",
"#Requires -Version 5.1",
"[CmdletBinding()]",
"param (",
" [Parameter(Mandatory)]",
" [string]",
" $$1,",
"",
" [Parameter(Mandatory)]",
" [string]",
" $$2",
")",
"",
"Set-StrictMode -Version 3.0",
"\\$ErrorActionPreference = 'Stop'",
"\\$PSDefaultParameterValues = @{ '*:Encoding' = 'utf8' }",
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8",
"",
"",
"function Main {",
" [CmdletBinding()]",
" param (",
" [Parameter(Mandatory)]",
" [string]",
" $$1,",
"",
" [Parameter(Mandatory)]",
" [string]",
" $$2",
" )",
"",
" begin {",
" . \"$$PSScriptRoot/../modules/env/output_lib.ps1\"",
"",
" Write-VerboseParam -invocation $MyInvocation",
"",
" }",
"",
" process {",
" # tbd",
" }",
"",
" end {",
" # tbd",
" }",
"",
"}",
"",
"Main `",
" -$1 $$1 `",
" -$2 $$2",
""
]
},
Юнит-тесты
Posh-код вполне очень даже возможно покрывать тестами — для этого есть Pester. Его можно запускать в пайплайне CI скриптом, написанным всё так же на Powershell, а можно подключить к VS Code.
В студию тесты интегрируются великолепно: тест-кейсы обнаруживаются автоматически, пиктограммы прогресса выполнения крутятся как чумачечие, ошибки выдаются во вкладку с терминалом.
Работа с тестами в VS Code, написанными на фреймворке Pester для Powershell
Возможности для дебага такие же, как в других IDE. Можно ставить точки прерывания как в тесте, так и в тестируемом коде. Есть отображение локальных переменных с их значениями, работают Step Into, Step Over — как в лучших домах Парижа.
Отладка кода Powershell в VS Code
Тесты на Pester состоят из типовых блоков. Для облегчения написания базовой болванки можно подготовить сниппет.
Сниппет для нового скрипта с тестами на фреймворке Pester
"Unit-test file sceleton": {
"scope": "ps1,powershell",
"prefix": "posh-snippet-test",
"body": [
"BeforeAll {",
" . \"$$PSScriptRoot/../../src/testing/expand_testdrive.ps1\"",
" . \"$$PSScriptRoot/../../src/testing/mocks.ps1\"",
"",
" $$cmd = \"$$PSScriptRoot/../../src/${TM_DIRECTORY/^.+[\\\\/\\\\]+(.+)$/$1/gi}/${TM_FILENAME_BASE/^(.+)[.]tests$/$1/gi}.ps1\"",
"}",
"",
"",
"Describe 'tbd' {",
" BeforeAll {",
" #tbd",
" $$params = @{",
" arg1 = val1",
" }",
" }",
"",
" It 'tbd' {",
" #tbd",
" & $$cmd @params",
" $$false | Should -BeTrue",
" }",
"",
" AfterEach {",
" if (Test-Path $$params.outputFile -PathType Leaf) {",
" Remove-Item $$params.outputFile -Force | Out-Null",
" }",
" }",
"}",
""
]
}
}
Код в репозитории распределён по смысловым папкам, а тесты лежат в отдельной папке tests
и ниже — в одноимённой смысловой папке, такой же, в которой лежит тестируемый скрипт. Поэтому в импортах сниппета присутствует вот этот /../../
двойной переход наверх по директориям с подстановкой имени самой последней директории из пути текущего файла.
Файл со сниппетами вполне можно закоммитить и расшарить на всех. Для сниппетов и для настроек PSScriptAnalyzer подходящее место — папка .vs-code
в корне репозитория.
Непрерывная интеграция
Линтинг с помощью PSScriptAnalyzer и запуск тестов на фреймворке Pester у нас встроен в пайплайн CI. В каждом пулл-реквесте код линтится, гоняются тесты. Выхлоп обоих инструментов возможно конвертировать в формат, понятный TeamCity, как и для иного инструмента. Так, в каждой сборке видим число выполненных тестов, кто из них упал, рассчитанный Coverage. Если PSScriptAnalyzer на что-то ругнулся, то получим так же, как с упавшими тестами, сломанный билд.
Статистика по покрытию кода тестами в сборке TeamCity
Pester сохраняет информацию о выполнении тестов в формате JaCoCo. Чтобы помочь TeamCity понять, что ему говорят, нужен такой минимум:
Import-Module Pester
$cfg = New-PesterConfiguration
# здесь настраиваем $cfg
Invoke-Pester -Configuration $cfg | ConvertTo-NUnitReport
При этом проценты Coverage для подсветки как на скриншоте всё равно придётся извлечь вручную. Чтобы не совсем вручную это делать, можно выбрать из готовых конвертеров, например, ReportGenerator от Daniel Palme. Он умеет и в формат для сонара, и саммари для тимсити сделать из исходного JaCoCo. PSScriptAnalyzer также возвращает «что попало», поэтому и там придётся поконвертить.
Объектный выхлоп PSScriptAnalyzer, который ещё предстоит превратить в формат, понятный TeamCity и SonarQube
Итак, сконвертировали для SonarQube, можем туда загружать полученные данные как thirdparty-отчёты. Таким образом, минимальный пайплайн включает четыре шага:
линтинг с применением PSScriptAnalyzer
прогон тестов с применением Pester
конвертация одного и другого в форматы для TeamCity и SonarQube
доставка в SonarQube
привет, сонар, на связи шаг тимсити
%SONAR_SCANNER_PATH% ^
-Dsonar.sources=. ^
-Dsonar.tests=tests/ ^
-Dsonar.projectKey=my_proj ^
-Dsonar.host.url=%SONAR_ROOT_URL% ^
-Dsonar.login=%SONAR_TOKEN% ^
-Dsonar.sourceEncoding=UTF-8 ^
-Dsonar.inclusions=**/*.ps1,config/**/*.json ^
-Dsonar.exclusions=**/*.sql,**/*.sln,**/*.sqlproj,**/*.xml,**/*.txt,**/*.md,tests/**/*.* ^
-Dsonar.cpd.exclusions=tests/**/*.*,rest/*_api.ps1 ^
-Dsonar.coverage.exclusions=rest/*_api.ps1 ^
-Dsonar.coverageReportPaths=%teamcity.build.checkoutDir%\%ARTIFACT_FOLDER_NAME%\SonarQube.xml ^
-Dsonar.projectVersion=%SONAR_VERSION_NUMBER% %SONAR_SCAN_EXCLUSIONS% %SONAR_SCAN_PR_PARAMS%
как-то так…
На стороне сонара настроен Quality Profile, в котором можно включить или выключить нужное правило или изменить уровень серьёзности его нарушения. И Quality Gate, в котором определён набор условий для признания пулл-ревеста прошедшим шлюз контроля качества, либо провалившим испытания.
Пулл-реквест не прошёл по критериям: низкое покрытие кода тестами
По умолчанию QG содержит такие условия: внесённые изменения обязаны быть покрыты тестами на 80%, дублирования кода не должно превышать 3%, сборка и линтинг не должны были найти ничего серьёзного, а несерьёзного не должно быть слишком много. Значений по умолчанию вполне достаточно.
Сонар не только контролирует прохождение QG, но также очень удобен в качестве общего места накопления информации о кодовой базе.
Общие показатели по кодовой базе Poweshell-скриптов
Здесь видно, что покрывать нужно 15 тысяч строк кода на Powershell, текущее покрытие составляет 76%. Всего строк 60 тысяч, из которых похожи на полную копипасту совсем немного. А так изменялся процент покрытия за последний год:
График изменения объёма кода в репозитории (верхняя область) и его покрытия тестами (нижняя область).
Подводные камни
Интеграция с VS Code полноценно заработала не у всех, кто пробовал.
У меня спустя какое-то время отвалилась возможность запуска всех тестов сразу или большого числа тестов, и совершенно непонятно, как это вернуть.
В VS Code не видно Coverage, узнаём только после сборки на CI.
Запуск тестов с собиранием Coverage работает намного дольше, чем без Coverage. В целом время терпимое, но как будто не очень. В соседнем проекте на C# тестов примерно две тысячи штук и они отрабатывают за полторы минуты. В Powershell-репозитории тестов в два раза меньше, но время выполнения уже подбирается к двадцати минутам.
В PSScriptAnalyzer правил очень мало, интеграция с процессом CI и загрузка в SonarQube хоть и, очевидно, небесполезны, однако не дают утверждать, что код действительно под контролем; многое приходится оставлять на ручное ревью.
То ли Pester, то ли его обёртка для VS Code, то ли сам Powershell что-то где-то кеширует и начинает «путаться в показаниях»: интенсивная работа над тестами вынужденно сопровождается периодическими рестартами VS Code, потому что какая-то внутренняя механика ломается. Запуск тестов в такой ситуации может не привести ни к чему, результат выполнения может отличаться в дебаге и без дебага. Это проявляется, например, в том что локально якобы всё отработало, а на CI выясняется, что тесты сломаны.
Нужны дополнительные конвертеры замечаний к коду и покрытия тестами в формат для TeamCity, GitLab, SonarQube, как минимум часть из которых придётся писать руками.
В целом, с приведённым набором инструментов работать можно. Таким образом, процесс разработки на Powershell становится чуть более похож на то, что принято считать нормальной и приличной организацией процесса. В нашем скриптовом репозитории на сегодня накоплено 60K строк кода на Powershell, включая код тестов. Без дополнительных инструментов, без автоматизации, в расчёте только на глаза и руки, держать этот объём под контролем было бы невозможно.