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-PythonTests.Tests.ps1

464lines · modecode

1#Requires -Modules Pester
2# Copyright (c) Microsoft Corporation.
3# SPDX-License-Identifier: MIT
4<#
5.SYNOPSIS
6 Pester tests for Invoke-PythonTests.ps1 script
7.DESCRIPTION
8 Tests for Python testing wrapper script:
9 - Parameter validation
10 - Tool availability checks
11 - Skill discovery via pyproject.toml
12 - Tests directory detection
13 - Pytest execution and result handling
14 - Output file generation
15 - Summary counters
16#>
17
18BeforeAll {
19 $script:ScriptPath = Join-Path $PSScriptRoot '../../linting/Invoke-PythonTests.ps1'
20
21 # Create stub functions so they can be mocked even when not installed
22 function global:pytest { '' }
23 function global:uv { '' }
24
25 . $script:ScriptPath
26}
27
28AfterAll {
29 Remove-Item -Path 'Function:\pytest' -Force -ErrorAction SilentlyContinue
30 Remove-Item -Path 'Function:\uv' -Force -ErrorAction SilentlyContinue
31}
32
33#region Parameter Validation Tests
34
35Describe 'Invoke-PythonTests Parameter Validation' -Tag 'Unit' {
36 Context 'RepoRoot parameter' {
37 BeforeEach {
38 Mock Get-ChildItem { @() }
39 Mock Get-Command { [PSCustomObject]@{ Source = 'pytest' } } -ParameterFilter { $Name -eq 'pytest' }
40 Mock Get-Command { $null } -ParameterFilter { $Name -eq 'uv' }
41 Mock Push-Location {}
42 Mock Pop-Location {}
43 }
44
45 It 'Accepts custom RepoRoot' {
46 $repoRoot = Join-Path $TestDrive 'test-repo'
47 New-Item -ItemType Directory -Path $repoRoot -Force | Out-Null
48 { Invoke-PythonTests -RepoRoot $repoRoot } | Should -Not -Throw
49 }
50 }
51
52 Context 'OutputPath parameter' {
53 BeforeEach {
54 Mock Get-ChildItem { @() }
55 Mock Get-Command { [PSCustomObject]@{ Source = 'pytest' } } -ParameterFilter { $Name -eq 'pytest' }
56 Mock Get-Command { $null } -ParameterFilter { $Name -eq 'uv' }
57 Mock Push-Location {}
58 Mock Pop-Location {}
59 }
60
61 It 'Accepts custom OutputPath' {
62 $outputPath = Join-Path $TestDrive 'test-output.json'
63 { Invoke-PythonTests -RepoRoot $TestDrive -OutputPath $outputPath } | Should -Not -Throw
64 }
65 }
66
67 Context 'Verbosity parameter' {
68 BeforeEach {
69 Mock Get-ChildItem { @() }
70 Mock Get-Command { [PSCustomObject]@{ Source = 'pytest' } } -ParameterFilter { $Name -eq 'pytest' }
71 Mock Get-Command { $null } -ParameterFilter { $Name -eq 'uv' }
72 Mock Push-Location {}
73 Mock Pop-Location {}
74 }
75
76 It 'Defaults to -v verbosity' {
77 { Invoke-PythonTests -RepoRoot $TestDrive } | Should -Not -Throw
78 }
79
80 It 'Accepts custom verbosity' {
81 { Invoke-PythonTests -RepoRoot $TestDrive -Verbosity '-vv' } | Should -Not -Throw
82 }
83 }
84}
85
86#endregion
87
88#region Tool Availability Tests
89
90Describe 'pytest Tool Availability' -Tag 'Unit' {
91 Context 'Tool not installed' {
92 BeforeEach {
93 Mock Push-Location {}
94 Mock Pop-Location {}
95 Mock Get-ChildItem {
96 @([PSCustomObject]@{
97 FullName = (Join-Path $TestDrive 'skill1/pyproject.toml')
98 Directory = [PSCustomObject]@{ FullName = (Join-Path $TestDrive 'skill1') }
99 })
100 }
101 Mock Get-Command { $null } -ParameterFilter { $Name -eq 'pytest' }
102 Mock Get-Command { $null } -ParameterFilter { $Name -eq 'uv' }
103 Mock Test-Path { $true } -ParameterFilter { $Path -like '*tests' }
104 Mock Test-Path { $false } -ParameterFilter { $Path -like '*uv.lock' }
105 }
106
107 It 'Returns failure when pytest not available' {
108 $result = Invoke-PythonTests -RepoRoot $TestDrive
109 $result.success | Should -BeFalse
110 }
111
112 It 'Reports skill path in errors' {
113 $result = Invoke-PythonTests -RepoRoot $TestDrive
114 $result.errors | Should -Contain (Join-Path $TestDrive 'skill1')
115 }
116
117 It 'Reports zero skills tested when pytest missing' {
118 $result = Invoke-PythonTests -RepoRoot $TestDrive
119 $result.skillsTested | Should -Be 0
120 }
121
122 It 'Reports zero passed' {
123 $result = Invoke-PythonTests -RepoRoot $TestDrive
124 $result.passed | Should -Be 0
125 }
126 }
127
128 Context 'Tool installed' {
129 BeforeEach {
130 Mock Push-Location {}
131 Mock Pop-Location {}
132 Mock Get-ChildItem { @() }
133 Mock Get-Command { [PSCustomObject]@{ Source = 'pytest' } } -ParameterFilter { $Name -eq 'pytest' }
134 Mock Get-Command { $null } -ParameterFilter { $Name -eq 'uv' }
135 }
136
137 It 'Proceeds when pytest available' {
138 { Invoke-PythonTests -RepoRoot $TestDrive } | Should -Not -Throw
139 }
140 }
141}
142
143#endregion
144
145#region Skill Discovery Tests
146
147Describe 'Python Skill Discovery for Testing' -Tag 'Unit' {
148 Context 'No Python skills found' {
149 BeforeEach {
150 Mock Push-Location {}
151 Mock Pop-Location {}
152 Mock Get-ChildItem { @() }
153 Mock Get-Command { [PSCustomObject]@{ Source = 'pytest' } } -ParameterFilter { $Name -eq 'pytest' }
154 Mock Get-Command { $null } -ParameterFilter { $Name -eq 'uv' }
155 }
156
157 It 'Returns success with zero skills' {
158 $result = Invoke-PythonTests -RepoRoot $TestDrive
159 $result.success | Should -BeTrue
160 $result.skillsTested | Should -Be 0
161 }
162
163 It 'Reports zero passed and failed' {
164 $result = Invoke-PythonTests -RepoRoot $TestDrive
165 $result.passed | Should -Be 0
166 $result.failed | Should -Be 0
167 }
168 }
169
170 Context 'Skill without tests directory' {
171 BeforeEach {
172 $script:NoTestsSkillDir = Join-Path $TestDrive 'no-tests-skill'
173 Mock Push-Location {}
174 Mock Pop-Location {}
175 Mock Get-Command { [PSCustomObject]@{ Source = 'pytest' } } -ParameterFilter { $Name -eq 'pytest' }
176 Mock Get-Command { $null } -ParameterFilter { $Name -eq 'uv' }
177 Mock Get-ChildItem {
178 @([PSCustomObject]@{
179 FullName = (Join-Path $script:NoTestsSkillDir 'pyproject.toml')
180 Directory = [PSCustomObject]@{ FullName = $script:NoTestsSkillDir }
181 })
182 }
183 Mock Test-Path { $false } -ParameterFilter { $Path -like '*tests' }
184 Mock Test-Path { $false } -ParameterFilter { $Path -like '*uv.lock' }
185 Mock pytest { $global:LASTEXITCODE = 0; '' }
186 }
187
188 It 'Skips skill without tests directory' {
189 $result = Invoke-PythonTests -RepoRoot $TestDrive
190 $result.skillsTested | Should -Be 0
191 }
192
193 It 'Does not call pytest for skill without tests' {
194 Invoke-PythonTests -RepoRoot $TestDrive
195 Should -Invoke -CommandName pytest -Times 0
196 }
197 }
198
199 Context 'Excludes node_modules from discovery' {
200 BeforeEach {
201 Mock Push-Location {}
202 Mock Pop-Location {}
203 Mock Get-Command { [PSCustomObject]@{ Source = 'pytest' } } -ParameterFilter { $Name -eq 'pytest' }
204 Mock Get-Command { $null } -ParameterFilter { $Name -eq 'uv' }
205 Mock Get-ChildItem {
206 @([PSCustomObject]@{
207 FullName = (Join-Path $TestDrive 'node_modules/pkg/pyproject.toml')
208 Directory = [PSCustomObject]@{ FullName = (Join-Path $TestDrive 'node_modules/pkg') }
209 })
210 }
211 }
212
213 It 'Filters out node_modules paths' {
214 $result = Invoke-PythonTests -RepoRoot $TestDrive
215 $result.skillsTested | Should -Be 0
216 }
217 }
218}
219
220#endregion
221
222#region Test Execution Tests
223
224Describe 'Pytest Execution' -Tag 'Unit' {
225 BeforeAll {
226 $script:SkillDir = Join-Path $TestDrive 'test-skill'
227 }
228
229 BeforeEach {
230 Mock Push-Location {}
231 Mock Pop-Location {}
232 Mock Get-Command { [PSCustomObject]@{ Source = 'pytest' } } -ParameterFilter { $Name -eq 'pytest' }
233 Mock Get-Command { $null } -ParameterFilter { $Name -eq 'uv' }
234 Mock Get-ChildItem {
235 @([PSCustomObject]@{
236 FullName = (Join-Path $script:SkillDir 'pyproject.toml')
237 Directory = [PSCustomObject]@{ FullName = $script:SkillDir }
238 })
239 }
240 Mock Test-Path { $true } -ParameterFilter { $Path -like '*tests' }
241 Mock Test-Path { $false } -ParameterFilter { $Path -like '*uv.lock' }
242 }
243
244 Context 'Tests pass' {
245 BeforeEach {
246 Mock pytest { $global:LASTEXITCODE = 0; '3 passed' }
247 }
248
249 It 'Returns success when pytest passes' {
250 $result = Invoke-PythonTests -RepoRoot $TestDrive
251 $result.success | Should -BeTrue
252 }
253
254 It 'Increments passed counter' {
255 $result = Invoke-PythonTests -RepoRoot $TestDrive
256 $result.passed | Should -Be 1
257 }
258
259 It 'Reports one skill tested' {
260 $result = Invoke-PythonTests -RepoRoot $TestDrive
261 $result.skillsTested | Should -Be 1
262 }
263
264 It 'Reports zero failures' {
265 $result = Invoke-PythonTests -RepoRoot $TestDrive
266 $result.failed | Should -Be 0
267 }
268
269 It 'Marks skill as passed in details' {
270 $result = Invoke-PythonTests -RepoRoot $TestDrive
271 $result.details[0].passed | Should -BeTrue
272 }
273 }
274
275 Context 'Tests fail' {
276 BeforeEach {
277 Mock pytest { $global:LASTEXITCODE = 1; '1 failed, 2 passed' }
278 }
279
280 It 'Returns failure when pytest fails' {
281 $result = Invoke-PythonTests -RepoRoot $TestDrive
282 $result.success | Should -BeFalse
283 }
284
285 It 'Increments failed counter' {
286 $result = Invoke-PythonTests -RepoRoot $TestDrive
287 $result.failed | Should -Be 1
288 }
289
290 It 'Records skill path in errors' {
291 $result = Invoke-PythonTests -RepoRoot $TestDrive
292 $result.errors | Should -Contain $script:SkillDir
293 }
294
295 It 'Marks skill as failed in details' {
296 $result = Invoke-PythonTests -RepoRoot $TestDrive
297 $result.details[0].passed | Should -BeFalse
298 }
299 }
300
301 Context 'Pytest throws exception' {
302 BeforeEach {
303 Mock pytest { throw 'pytest crashed' }
304 }
305
306 It 'Handles pytest exception gracefully' {
307 $result = Invoke-PythonTests -RepoRoot $TestDrive
308 $result.success | Should -BeFalse
309 }
310
311 It 'Increments failed counter on exception' {
312 $result = Invoke-PythonTests -RepoRoot $TestDrive
313 $result.failed | Should -Be 1
314 }
315
316 It 'Records error with skill path' {
317 $result = Invoke-PythonTests -RepoRoot $TestDrive
318 $result.errors | Should -Not -BeNullOrEmpty
319 }
320 }
321
322 Context 'Multiple skills' {
323 BeforeEach {
324 $script:Skill1Dir = Join-Path $TestDrive 'skill-a'
325 $script:Skill2Dir = Join-Path $TestDrive 'skill-b'
326 Mock Get-ChildItem {
327 @(
328 [PSCustomObject]@{
329 FullName = (Join-Path $script:Skill1Dir 'pyproject.toml')
330 Directory = [PSCustomObject]@{ FullName = $script:Skill1Dir }
331 },
332 [PSCustomObject]@{
333 FullName = (Join-Path $script:Skill2Dir 'pyproject.toml')
334 Directory = [PSCustomObject]@{ FullName = $script:Skill2Dir }
335 }
336 )
337 }
338 Mock pytest { $global:LASTEXITCODE = 0; 'passed' }
339 }
340
341 It 'Tests all discovered skills' {
342 $result = Invoke-PythonTests -RepoRoot $TestDrive
343 $result.skillsTested | Should -Be 2
344 }
345
346 It 'Counts all passing skills' {
347 $result = Invoke-PythonTests -RepoRoot $TestDrive
348 $result.passed | Should -Be 2
349 }
350 }
351
352 Context 'Locked uv project' {
353 BeforeEach {
354 Mock Get-Command { [PSCustomObject]@{ Source = 'uv' } } -ParameterFilter { $Name -eq 'uv' }
355 Mock Test-Path { $true } -ParameterFilter { $Path -like '*uv.lock' }
356 Mock uv {
357 if ($args[0] -eq 'sync') {
358 $global:LASTEXITCODE = 0
359 return 'synced'
360 }
361
362 $global:LASTEXITCODE = 0
363 return '3 passed'
364 }
365 }
366
367 It 'Uses uv runner for locked project' {
368 $result = Invoke-PythonTests -RepoRoot $TestDrive
369 $result.details[0].runner | Should -Be 'uv'
370 }
371
372 It 'Returns success when uv pytest passes' {
373 $result = Invoke-PythonTests -RepoRoot $TestDrive
374 $result.success | Should -BeTrue
375 $result.passed | Should -Be 1
376 }
377
378 It 'Runs uv sync before pytest' {
379 Invoke-PythonTests -RepoRoot $TestDrive
380 Should -Invoke -CommandName uv -ParameterFilter { $args[0] -eq 'sync' -and $args[1] -eq '--locked' -and $args[2] -eq '--dev' } -Times 1
381 }
382
383 It 'Runs pytest through uv' {
384 Invoke-PythonTests -RepoRoot $TestDrive
385 Should -Invoke -CommandName uv -ParameterFilter { $args[0] -eq 'run' -and $args[1] -eq 'pytest' -and $args[2] -eq 'tests/' } -Times 1
386 }
387 }
388
389 Context 'Locked uv project sync failure' {
390 BeforeEach {
391 Mock Get-Command { [PSCustomObject]@{ Source = 'uv' } } -ParameterFilter { $Name -eq 'uv' }
392 Mock Test-Path { $true } -ParameterFilter { $Path -like '*uv.lock' }
393 Mock uv {
394 $global:LASTEXITCODE = 1
395 'sync failed'
396 }
397 }
398
399 It 'Returns failure when uv sync fails' {
400 $result = Invoke-PythonTests -RepoRoot $TestDrive
401 $result.success | Should -BeFalse
402 }
403
404 It 'Records sync phase failure details' {
405 $result = Invoke-PythonTests -RepoRoot $TestDrive
406 $result.details[0].runner | Should -Be 'uv'
407 $result.details[0].phase | Should -Be 'sync'
408 $result.details[0].passed | Should -BeFalse
409 }
410
411 It 'Does not run pytest after failed sync' {
412 Invoke-PythonTests -RepoRoot $TestDrive
413 Should -Invoke -CommandName uv -ParameterFilter { $args[0] -eq 'run' } -Times 0
414 }
415 }
416}
417
418#endregion
419
420#region Output Persistence Tests
421
422Describe 'Output Persistence' -Tag 'Unit' {
423 BeforeAll {
424 $script:OutputSkillDir = Join-Path $TestDrive 'output-skill'
425 }
426
427 BeforeEach {
428 Mock Push-Location {}
429 Mock Pop-Location {}
430 Mock Get-Command { [PSCustomObject]@{ Source = 'pytest' } } -ParameterFilter { $Name -eq 'pytest' }
431 Mock Get-Command { $null } -ParameterFilter { $Name -eq 'uv' }
432 Mock Get-ChildItem {
433 @([PSCustomObject]@{
434 FullName = (Join-Path $script:OutputSkillDir 'pyproject.toml')
435 Directory = [PSCustomObject]@{ FullName = $script:OutputSkillDir }
436 })
437 }
438 Mock Test-Path { $true } -ParameterFilter { $Path -like '*tests' }
439 Mock Test-Path { $false } -ParameterFilter { $Path -like '*uv.lock' }
440 Mock pytest { $global:LASTEXITCODE = 0; 'passed' }
441 }
442
443 Context 'OutputPath specified' {
444 It 'Writes JSON results to OutputPath' {
445 $outputPath = Join-Path $TestDrive 'test-results.json'
446 Invoke-PythonTests -RepoRoot $TestDrive -OutputPath $outputPath
447 Test-Path $outputPath | Should -BeTrue
448 }
449
450 It 'Produces valid JSON output' {
451 $outputPath = Join-Path $TestDrive 'test-results2.json'
452 Invoke-PythonTests -RepoRoot $TestDrive -OutputPath $outputPath
453 { Get-Content $outputPath -Raw | ConvertFrom-Json } | Should -Not -Throw
454 }
455 }
456
457 Context 'OutputPath not specified' {
458 It 'Does not throw when OutputPath omitted' {
459 { Invoke-PythonTests -RepoRoot $TestDrive } | Should -Not -Throw
460 }
461 }
462}
463
464#endregion
465