microsoft/hve-core

Public

mirrored fromhttps://github.com/microsoft/hve-coreAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/a11y-pr1-scripts-validators

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

scripts/tests/linting/Invoke-PythonLint.Tests.ps1

384lines · modecode

1#Requires -Modules Pester
2# Copyright (c) Microsoft Corporation.
3# SPDX-License-Identifier: MIT
4<#
5.SYNOPSIS
6 Pester tests for Invoke-PythonLint.ps1 script
7.DESCRIPTION
8 Tests for Python linting wrapper script:
9 - Parameter validation
10 - Tool availability checks
11 - Skill discovery via pyproject.toml
12 - Ruff execution and result handling
13 - Output file generation
14#>
15
16BeforeAll {
17 $script:ScriptPath = Join-Path $PSScriptRoot '../../linting/Invoke-PythonLint.ps1'
18
19 # Create stub function for ruff so it can be mocked even when not installed
20 function global:ruff { '' }
21
22 . $script:ScriptPath
23}
24
25AfterAll {
26 Remove-Item -Path 'Function:\ruff' -Force -ErrorAction SilentlyContinue
27}
28
29#region Parameter Validation Tests
30
31Describe 'Invoke-PythonLint Parameter Validation' -Tag 'Unit' {
32 Context 'RepoRoot parameter' {
33 BeforeEach {
34 Mock Get-PythonSkill { @() }
35 Mock Get-Command { [PSCustomObject]@{ Source = 'ruff' } } -ParameterFilter { $Name -eq 'ruff' }
36 Mock Push-Location {}
37 Mock Pop-Location {}
38 }
39
40 It 'Accepts custom RepoRoot' {
41 $repoRoot = Join-Path $TestDrive 'test-repo'
42 New-Item -ItemType Directory -Path $repoRoot -Force | Out-Null
43 { Invoke-PythonLint -RepoRoot $repoRoot } | Should -Not -Throw
44 }
45 }
46
47 Context 'OutputPath parameter' {
48 BeforeEach {
49 Mock Get-PythonSkill { @() }
50 Mock Get-Command { [PSCustomObject]@{ Source = 'ruff' } } -ParameterFilter { $Name -eq 'ruff' }
51 Mock Push-Location {}
52 Mock Pop-Location {}
53 }
54
55 It 'Accepts custom OutputPath' {
56 $outputPath = Join-Path $TestDrive 'lint-output.json'
57 { Invoke-PythonLint -RepoRoot $TestDrive -OutputPath $outputPath } | Should -Not -Throw
58 }
59 }
60}
61
62#endregion
63
64#region Tool Availability Tests
65
66Describe 'ruff Tool Availability' -Tag 'Unit' {
67 Context 'Tool not installed' {
68 BeforeEach {
69 Mock Push-Location {}
70 Mock Pop-Location {}
71 Mock Get-PythonSkill { @((Join-Path $TestDrive 'skill1')) }
72 Mock Get-Command { $null } -ParameterFilter { $Name -eq 'ruff' }
73 }
74
75 It 'Returns failure when ruff not available' {
76 $result = Invoke-PythonLint -RepoRoot $TestDrive
77 $result.success | Should -BeFalse
78 }
79
80 It 'Reports skill path in errors' {
81 $result = Invoke-PythonLint -RepoRoot $TestDrive
82 $result.errors | Should -Contain (Join-Path $TestDrive 'skill1')
83 }
84
85 It 'Reports zero skills checked when ruff missing' {
86 $result = Invoke-PythonLint -RepoRoot $TestDrive
87 $result.skillsChecked | Should -Be 0
88 }
89 }
90
91 Context 'Tool installed' {
92 BeforeEach {
93 Mock Push-Location {}
94 Mock Pop-Location {}
95 Mock Get-PythonSkill { @() }
96 Mock Get-Command { [PSCustomObject]@{ Source = 'ruff' } } -ParameterFilter { $Name -eq 'ruff' }
97 }
98
99 It 'Proceeds when ruff available' {
100 { Invoke-PythonLint -RepoRoot $TestDrive } | Should -Not -Throw
101 }
102 }
103}
104
105#endregion
106
107#region Skill Discovery Tests
108
109Describe 'Python Skill Discovery' -Tag 'Unit' {
110 Context 'No Python skills found' {
111 BeforeEach {
112 Mock Push-Location {}
113 Mock Pop-Location {}
114 Mock Get-PythonSkill { @() }
115 Mock Get-Command { [PSCustomObject]@{ Source = 'ruff' } } -ParameterFilter { $Name -eq 'ruff' }
116 }
117
118 It 'Returns success with zero skills when no pyproject.toml found' {
119 $result = Invoke-PythonLint -RepoRoot $TestDrive
120 $result.success | Should -BeTrue
121 $result.skillsChecked | Should -Be 0
122 }
123 }
124
125 Context 'Python skills found' {
126 BeforeEach {
127 Mock Push-Location {}
128 Mock Pop-Location {}
129 Mock Get-Command { [PSCustomObject]@{ Source = 'ruff' } } -ParameterFilter { $Name -eq 'ruff' }
130 Mock ruff { $global:LASTEXITCODE = 0; '' }
131 }
132
133 It 'Discovers skills via pyproject.toml' {
134 $skillDir = Join-Path $TestDrive 'skill1'
135 Mock Get-PythonSkill { @($skillDir) }
136
137 $result = Invoke-PythonLint -RepoRoot $TestDrive
138 $result.skillsChecked | Should -Be 1
139 }
140
141 It 'Discovers multiple skills' {
142 $skill1Dir = Join-Path $TestDrive 'skill1'
143 $skill2Dir = Join-Path $TestDrive 'skill2'
144 Mock Get-PythonSkill { @($skill1Dir, $skill2Dir) }
145
146 $result = Invoke-PythonLint -RepoRoot $TestDrive
147 $result.skillsChecked | Should -Be 2
148 }
149
150 It 'Excludes node_modules from discovery' {
151 # Get-PythonSkill applies the node_modules filter; mock returns post-filter result.
152 Mock Get-PythonSkill { @() }
153
154 $result = Invoke-PythonLint -RepoRoot $TestDrive
155 $result.skillsChecked | Should -Be 0
156 }
157 }
158}
159
160#endregion
161
162#region Lint Execution Tests
163
164Describe 'Ruff Lint Execution' -Tag 'Unit' {
165 BeforeAll {
166 $script:SkillDir = Join-Path $TestDrive 'lint-skill'
167 }
168
169 BeforeEach {
170 Mock Push-Location {}
171 Mock Pop-Location {}
172 Mock Get-Command { [PSCustomObject]@{ Source = 'ruff' } } -ParameterFilter { $Name -eq 'ruff' }
173 Mock Get-PythonSkill { @($script:SkillDir) }
174 }
175
176 Context 'Lint passes' {
177 BeforeEach {
178 Mock ruff { $global:LASTEXITCODE = 0; '' }
179 }
180
181 It 'Returns success when ruff reports no issues' {
182 $result = Invoke-PythonLint -RepoRoot $TestDrive
183 $result.success | Should -BeTrue
184 }
185
186 It 'Marks skill as passed in details' {
187 $result = Invoke-PythonLint -RepoRoot $TestDrive
188 $result.details[0].passed | Should -BeTrue
189 }
190
191 It 'Reports no errors' {
192 $result = Invoke-PythonLint -RepoRoot $TestDrive
193 $result.errors | Should -HaveCount 0
194 }
195 }
196
197 Context 'Lint fails' {
198 BeforeEach {
199 Mock ruff { $global:LASTEXITCODE = 1; 'error: E501 line too long' }
200 }
201
202 It 'Returns failure when ruff reports issues' {
203 $result = Invoke-PythonLint -RepoRoot $TestDrive
204 $result.success | Should -BeFalse
205 }
206
207 It 'Records skill path in errors' {
208 $result = Invoke-PythonLint -RepoRoot $TestDrive
209 $result.errors | Should -Contain $script:SkillDir
210 }
211
212 It 'Marks skill as failed in details' {
213 $result = Invoke-PythonLint -RepoRoot $TestDrive
214 $result.details[0].passed | Should -BeFalse
215 }
216 }
217
218 Context 'Ruff throws exception' {
219 BeforeEach {
220 Mock ruff { throw 'ruff crashed' }
221 }
222
223 It 'Handles ruff exception gracefully' {
224 $result = Invoke-PythonLint -RepoRoot $TestDrive
225 $result.success | Should -BeFalse
226 }
227
228 It 'Records error with skill path' {
229 $result = Invoke-PythonLint -RepoRoot $TestDrive
230 $result.errors | Should -Not -BeNullOrEmpty
231 }
232 }
233
234 Context 'Fix mode with -Fix switch' {
235 BeforeEach {
236 Mock ruff { $global:LASTEXITCODE = 0; '' }
237 }
238
239 It 'Invokes ruff with --fix argument' {
240 Invoke-PythonLint -Fix -RepoRoot $TestDrive
241 Should -Invoke ruff -ParameterFilter { $args -contains '--fix' }
242 }
243
244 It 'Invokes ruff with check subcommand' {
245 Invoke-PythonLint -Fix -RepoRoot $TestDrive
246 Should -Invoke ruff -ParameterFilter { $args -contains 'check' }
247 }
248
249 It 'Invokes ruff with format subcommand' {
250 Invoke-PythonLint -Fix -RepoRoot $TestDrive
251 Should -Invoke ruff -ParameterFilter { $args -contains 'format' }
252 }
253
254 It 'Records formatExitCode in skill detail' {
255 $result = Invoke-PythonLint -Fix -RepoRoot $TestDrive
256 $result.details[0].formatExitCode | Should -Be 0
257 }
258 }
259}
260
261#endregion
262
263#region Output Persistence Tests
264
265Describe 'Output Persistence' -Tag 'Unit' {
266 BeforeAll {
267 $script:OutputSkillDir = Join-Path $TestDrive 'output-skill'
268 }
269
270 BeforeEach {
271 Mock Push-Location {}
272 Mock Pop-Location {}
273 Mock Get-Command { [PSCustomObject]@{ Source = 'ruff' } } -ParameterFilter { $Name -eq 'ruff' }
274 Mock Get-PythonSkill { @($script:OutputSkillDir) }
275 Mock ruff { $global:LASTEXITCODE = 0; '' }
276 }
277
278 Context 'OutputPath specified' {
279 It 'Writes JSON results to OutputPath' {
280 $outputPath = Join-Path $TestDrive 'lint-results.json'
281 Invoke-PythonLint -RepoRoot $TestDrive -OutputPath $outputPath
282 Test-Path $outputPath | Should -BeTrue
283 }
284
285 It 'Produces valid JSON output' {
286 $outputPath = Join-Path $TestDrive 'lint-results2.json'
287 Invoke-PythonLint -RepoRoot $TestDrive -OutputPath $outputPath
288 { Get-Content $outputPath -Raw | ConvertFrom-Json } | Should -Not -Throw
289 }
290 }
291
292 Context 'OutputPath not specified' {
293 It 'Does not throw when OutputPath omitted' {
294 { Invoke-PythonLint -RepoRoot $TestDrive } | Should -Not -Throw
295 }
296 }
297}
298
299#endregion
300
301#region Import-Order Detection (I001) Guard
302
303# Defense-in-depth: ensure that when a Python skill enables the isort rule ('I')
304# and ships an import-order violation, Invoke-PythonLint surfaces it as a
305# failure. Guards against silent regressions where the rule selection is
306# stripped or the lint runner stops invoking ruff against test fixtures.
307# Untagged so it runs in the default Pester suite (which excludes 'Integration'
308# and 'Slow'); skips at runtime when no real ruff binary is available.
309Describe 'Invoke-PythonLint Import-Order Detection (I001 Guard)' {
310 BeforeAll {
311 # Drop the global ruff stub from the file's top-level BeforeAll so we
312 # invoke a real ruff binary, not the no-op function.
313 Remove-Item -Path 'Function:\ruff' -Force -ErrorAction SilentlyContinue
314
315 # Resolve a real ruff binary: prefer one already on PATH, else borrow
316 # one from any existing skill venv in this repo so the test mirrors how
317 # Invoke-PythonLint discovers ruff in CI.
318 $script:RealRuffPath = $null
319 $globalRuff = Get-Command ruff -ErrorAction SilentlyContinue
320 if ($globalRuff) {
321 $script:RealRuffPath = $globalRuff.Source
322 }
323 else {
324 $repoRoot = Resolve-Path (Join-Path $PSScriptRoot '../../..')
325 $candidate = Get-ChildItem -Path (Join-Path $repoRoot '.github/skills') `
326 -Recurse -Force -File -Filter 'ruff' -ErrorAction SilentlyContinue |
327 Where-Object { $_.FullName -match '\.venv/bin/ruff$' } |
328 Select-Object -First 1
329 if ($candidate) {
330 $script:RealRuffPath = $candidate.FullName
331 }
332 }
333 }
334
335 It 'Reports failure when a skill ships an I001 import-order violation' {
336 if (-not $script:RealRuffPath) {
337 Set-ItResult -Skipped -Because 'no ruff binary available on PATH or in any skill .venv'
338 }
339
340 $skillDir = Join-Path $TestDrive 'i001-guard-skill'
341 $venvBinDir = Join-Path $skillDir '.venv/bin'
342 New-Item -ItemType Directory -Path $venvBinDir -Force | Out-Null
343
344 # Plant pyproject.toml that explicitly enables the isort rule.
345 Set-Content -Path (Join-Path $skillDir 'pyproject.toml') -Value @"
346[project]
347name = "i001-guard-skill"
348version = "0.0.0"
349requires-python = ">=3.11"
350
351[tool.ruff]
352line-length = 88
353target-version = "py311"
354
355[tool.ruff.lint]
356select = ["I"]
357"@
358
359 # Plant a Python file whose imports are unsorted (triggers I001).
360 $scriptsDir = Join-Path $skillDir 'scripts'
361 New-Item -ItemType Directory -Path $scriptsDir -Force | Out-Null
362 Set-Content -Path (Join-Path $scriptsDir 'bad_imports.py') -Value @"
363import sys
364import os
365"@
366
367 # Stage a real ruff binary at the skill's expected venv path so
368 # Invoke-PythonLint resolves and invokes it just as in production.
369 $stagedRuff = Join-Path $venvBinDir 'ruff'
370 try {
371 New-Item -ItemType SymbolicLink -Path $stagedRuff -Target $script:RealRuffPath -Force | Out-Null
372 }
373 catch {
374 Copy-Item -Path $script:RealRuffPath -Destination $stagedRuff -Force
375 }
376
377 $result = Invoke-PythonLint -RepoRoot $TestDrive
378 $result.success | Should -BeFalse
379 $result.skillsChecked | Should -Be 1
380 ($result.details | Where-Object { -not $_.passed }).output | Should -Match 'I001'
381 }
382}
383
384#endregion
385