microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
copilot/update-workflow-file-and-script

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

583lines · modecode

1#Requires -Modules Pester
2# Copyright (c) Microsoft Corporation.
3# SPDX-License-Identifier: MIT
4<#
5.SYNOPSIS
6 Pester tests for Invoke-YamlLint.ps1 script
7.DESCRIPTION
8 Tests for actionlint wrapper script:
9 - Parameter validation
10 - Tool availability checks
11 - ChangedFilesOnly filtering
12 - JSON parsing edge cases
13 - CI integration
14#>
15
16BeforeAll {
17 $script:ScriptPath = Join-Path $PSScriptRoot '../../linting/Invoke-YamlLint.ps1'
18 $script:ModulePath = Join-Path $PSScriptRoot '../../linting/Modules/LintingHelpers.psm1'
19 $script:CIHelpersPath = Join-Path $PSScriptRoot '../../lib/Modules/CIHelpers.psm1'
20
21 # Import modules for mocking
22 Import-Module $script:ModulePath -Force
23 Import-Module $script:CIHelpersPath -Force
24
25 # Create stub function for actionlint so it can be mocked even when not installed
26 function global:actionlint { '[]' }
27
28 . $script:ScriptPath
29}
30
31AfterAll {
32 Remove-Module LintingHelpers -Force -ErrorAction SilentlyContinue
33 Remove-Module CIHelpers -Force -ErrorAction SilentlyContinue
34 # Remove the actionlint stub function
35 Remove-Item -Path 'Function:\actionlint' -Force -ErrorAction SilentlyContinue
36}
37
38#region Parameter Validation Tests
39
40Describe 'Invoke-YamlLint Parameter Validation' -Tag 'Unit' {
41 Context 'ChangedFilesOnly parameter' {
42 BeforeEach {
43 Mock Get-Command { [PSCustomObject]@{ Source = 'actionlint' } } -ParameterFilter { $Name -eq 'actionlint' }
44 Mock actionlint { '[]' }
45 Mock Get-ChangedFilesFromGit { @() }
46 Mock Test-Path { $false } -ParameterFilter { $Path -eq '.github/workflows' }
47 Mock Set-CIOutput {}
48 Mock Set-CIEnv {}
49 Mock Write-CIStepSummary {}
50 Mock Write-CIAnnotation {}
51 }
52
53 It 'Accepts ChangedFilesOnly switch' {
54 { Invoke-YamlLintCore -ChangedFilesOnly } | Should -Not -Throw
55 }
56
57 It 'Accepts BaseBranch with ChangedFilesOnly' {
58 { Invoke-YamlLintCore -ChangedFilesOnly -BaseBranch 'develop' } | Should -Not -Throw
59 }
60 }
61
62 Context 'OutputPath parameter' {
63 BeforeEach {
64 Mock Get-Command { [PSCustomObject]@{ Source = 'actionlint' } } -ParameterFilter { $Name -eq 'actionlint' }
65 Mock actionlint { '[]' }
66 Mock Test-Path { $false } -ParameterFilter { $Path -eq '.github/workflows' }
67 Mock Set-CIOutput {}
68 Mock Set-CIEnv {}
69 Mock Write-CIStepSummary {}
70 Mock Write-CIAnnotation {}
71 }
72
73 It 'Accepts custom output path' {
74 $outputPath = Join-Path ([System.IO.Path]::GetTempPath()) 'test-yaml-lint.json'
75 { Invoke-YamlLintCore -OutputPath $outputPath } | Should -Not -Throw
76 }
77 }
78}
79
80#endregion
81
82#region Tool Availability Tests
83
84Describe 'actionlint Tool Availability' -Tag 'Unit' {
85 Context 'Tool not installed' {
86 BeforeEach {
87 Mock Get-Command { $null } -ParameterFilter { $Name -eq 'actionlint' }
88 }
89
90 It 'Reports error when actionlint not installed' {
91 { Invoke-YamlLintCore } | Should -Throw '*actionlint is not installed*'
92 }
93
94 It 'Writes appropriate error message' {
95 { Invoke-YamlLintCore } | Should -Throw '*actionlint is not installed*'
96 }
97 }
98
99 Context 'Tool installed' {
100 BeforeEach {
101 Mock Get-Command { [PSCustomObject]@{ Source = 'C:\tools\actionlint.exe' } } -ParameterFilter { $Name -eq 'actionlint' }
102 Mock actionlint { '[]' }
103 Mock Test-Path { $false } -ParameterFilter { $Path -eq '.github/workflows' }
104 Mock Set-CIOutput {}
105 Mock Set-CIEnv {}
106 Mock Write-CIStepSummary {}
107 Mock Write-CIAnnotation {}
108 }
109
110 It 'Proceeds when actionlint available' {
111 { Invoke-YamlLintCore } | Should -Not -Throw
112 }
113 }
114}
115
116#endregion
117
118#region File Discovery Tests
119
120Describe 'File Discovery' -Tag 'Unit' {
121 Context 'All files mode' {
122 BeforeEach {
123 Mock Get-Command { [PSCustomObject]@{ Source = 'actionlint' } } -ParameterFilter { $Name -eq 'actionlint' }
124 Mock actionlint { '[]' }
125 Mock Set-CIOutput {}
126 Mock Set-CIEnv {}
127 Mock Write-CIStepSummary {}
128 Mock Write-CIAnnotation {}
129 }
130
131 It 'Uses Get-ChildItem when workflows directory exists' {
132 Mock Test-Path { $true } -ParameterFilter { $Path -eq '.github/workflows' }
133 Mock Get-ChildItem {
134 @(
135 [PSCustomObject]@{ FullName = '.github/workflows/ci.yml'; Extension = '.yml' },
136 [PSCustomObject]@{ FullName = '.github/workflows/release.yaml'; Extension = '.yaml' }
137 )
138 } -ParameterFilter { $Path -eq '.github/workflows' }
139
140 Invoke-YamlLintCore
141 Should -Invoke Get-ChildItem -Times 1 -ParameterFilter { $Path -eq '.github/workflows' }
142 }
143
144 It 'Returns no files when workflows directory missing' {
145 Mock Test-Path { $false } -ParameterFilter { $Path -eq '.github/workflows' }
146
147 Invoke-YamlLintCore
148 Should -Invoke Set-CIOutput -Times 1 -ParameterFilter { $Name -eq 'count' -and $Value -eq '0' }
149 }
150
151 It 'Filters to only .yml and .yaml extensions' {
152 Mock Test-Path { $true } -ParameterFilter { $Path -eq '.github/workflows' }
153 Mock Get-ChildItem {
154 @(
155 [PSCustomObject]@{ FullName = '.github/workflows/ci.yml'; Extension = '.yml' },
156 [PSCustomObject]@{ FullName = '.github/workflows/config.json'; Extension = '.json' },
157 [PSCustomObject]@{ FullName = '.github/workflows/release.yaml'; Extension = '.yaml' }
158 )
159 } -ParameterFilter { $Path -eq '.github/workflows' }
160
161 Invoke-YamlLintCore
162 # Should only count 2 files (yml and yaml, not json)
163 Should -Invoke Set-CIOutput -Times 1 -ParameterFilter { $Name -eq 'count' -and $Value -eq '2' }
164 }
165 }
166
167 Context 'Changed files only mode' {
168 BeforeEach {
169 Mock Get-Command { [PSCustomObject]@{ Source = 'actionlint' } } -ParameterFilter { $Name -eq 'actionlint' }
170 Mock actionlint { '[]' }
171 Mock Set-CIOutput {}
172 Mock Set-CIEnv {}
173 Mock Write-CIStepSummary {}
174 Mock Write-CIAnnotation {}
175 }
176
177 It 'Uses Get-ChangedFilesFromGit when ChangedFilesOnly specified' {
178 Mock Get-ChangedFilesFromGit { @('.github/workflows/ci.yml') }
179
180 Invoke-YamlLintCore -ChangedFilesOnly
181 Should -Invoke Get-ChangedFilesFromGit -Times 1
182 }
183
184 It 'Passes BaseBranch to Get-ChangedFilesFromGit' {
185 Mock Get-ChangedFilesFromGit { @() }
186
187 Invoke-YamlLintCore -ChangedFilesOnly -BaseBranch 'develop'
188 Should -Invoke Get-ChangedFilesFromGit -Times 1 -ParameterFilter {
189 $BaseBranch -eq 'develop'
190 }
191 }
192
193 It 'Filters changed files to workflows directory only' {
194 Mock Get-ChangedFilesFromGit {
195 @(
196 '.github/workflows/ci.yml',
197 'scripts/test.yml',
198 '.github/workflows/build.yaml'
199 )
200 }
201
202 Invoke-YamlLintCore -ChangedFilesOnly
203 # Should only count 2 workflow files
204 Should -Invoke Set-CIOutput -Times 1 -ParameterFilter { $Name -eq 'count' -and $Value -eq '2' }
205 }
206 }
207
208 Context 'No files found' {
209 BeforeEach {
210 Mock Get-Command { [PSCustomObject]@{ Source = 'actionlint' } } -ParameterFilter { $Name -eq 'actionlint' }
211 Mock Test-Path { $false } -ParameterFilter { $Path -eq '.github/workflows' }
212 Mock Set-CIOutput {}
213 Mock Set-CIEnv {}
214 Mock Write-CIStepSummary {}
215 Mock Write-CIAnnotation {}
216 }
217
218 It 'Sets count and issues to 0 when no files found' {
219 Invoke-YamlLintCore
220 Should -Invoke Set-CIOutput -Times 1 -ParameterFilter { $Name -eq 'count' -and $Value -eq '0' }
221 Should -Invoke Set-CIOutput -Times 1 -ParameterFilter { $Name -eq 'issues' -and $Value -eq '0' }
222 }
223
224 It 'Exits with code 0 when no files found' {
225 { Invoke-YamlLintCore } | Should -Not -Throw
226 }
227 }
228}
229
230#endregion
231
232#region JSON Parsing Tests
233
234Describe 'actionlint Output Parsing' -Tag 'Unit' {
235 BeforeEach {
236 Mock Get-Command { [PSCustomObject]@{ Source = 'actionlint' } } -ParameterFilter { $Name -eq 'actionlint' }
237 Mock Test-Path { $true } -ParameterFilter { $Path -eq '.github/workflows' }
238 Mock Get-ChildItem {
239 @([PSCustomObject]@{ FullName = '.github/workflows/ci.yml'; Extension = '.yml' })
240 } -ParameterFilter { $Path -eq '.github/workflows' }
241 Mock Set-CIOutput {}
242 Mock Set-CIEnv {}
243 Mock Write-CIStepSummary {}
244 Mock Write-CIAnnotation {}
245 Mock New-Item {}
246 Mock Out-File {}
247 }
248
249 Context 'Empty output scenarios' {
250 It 'Handles null output gracefully' {
251 Mock actionlint { $null }
252
253 Invoke-YamlLintCore
254 Should -Invoke Set-CIOutput -Times 1 -ParameterFilter { $Name -eq 'issues' -and $Value -eq '0' }
255 }
256
257 It 'Handles "null" string output' {
258 Mock actionlint { 'null' }
259
260 Invoke-YamlLintCore
261 Should -Invoke Set-CIOutput -Times 1 -ParameterFilter { $Name -eq 'issues' -and $Value -eq '0' }
262 }
263
264 It 'Handles empty array output' {
265 Mock actionlint { '[]' }
266
267 Invoke-YamlLintCore
268 Should -Invoke Set-CIOutput -Times 1 -ParameterFilter { $Name -eq 'issues' -and $Value -eq '0' }
269 }
270 }
271
272 Context 'Single issue output' {
273 It 'Converts single object to array' {
274 Mock actionlint {
275 '{"message":"test error","filepath":".github/workflows/ci.yml","line":10,"column":5}'
276 }
277
278 try { Invoke-YamlLintCore } catch { $null = $_ }
279 Should -Invoke Write-CIAnnotation -Times 1
280 Should -Invoke Set-CIOutput -Times 1 -ParameterFilter { $Name -eq 'issues' -and $Value -eq '1' }
281 }
282 }
283
284 Context 'Multiple issues output' {
285 It 'Parses array of issues correctly' {
286 Mock actionlint {
287 '[{"message":"error 1","filepath":".github/workflows/ci.yml","line":10,"column":5},{"message":"error 2","filepath":".github/workflows/ci.yml","line":20,"column":3}]'
288 }
289
290 try { Invoke-YamlLintCore } catch { $null = $_ }
291 Should -Invoke Write-CIAnnotation -Times 2
292 Should -Invoke Set-CIOutput -Times 1 -ParameterFilter { $Name -eq 'issues' -and $Value -eq '2' }
293 }
294 }
295
296 Context 'Invalid JSON output' {
297 It 'Handles malformed JSON gracefully' {
298 Mock actionlint { 'not valid json {{{' }
299 Mock Write-Warning {}
300
301 Invoke-YamlLintCore
302 Should -Invoke Write-Warning -Times 1
303 Should -Invoke Set-CIOutput -Times 1 -ParameterFilter { $Name -eq 'issues' -and $Value -eq '0' }
304 }
305 }
306}
307
308#endregion
309
310#region Issue Processing Tests
311
312Describe 'Issue Processing' -Tag 'Unit' {
313 BeforeEach {
314 Mock Get-Command { [PSCustomObject]@{ Source = 'actionlint' } } -ParameterFilter { $Name -eq 'actionlint' }
315 Mock Test-Path { $true } -ParameterFilter { $Path -eq '.github/workflows' }
316 Mock Get-ChildItem {
317 @([PSCustomObject]@{ FullName = '.github/workflows/ci.yml'; Extension = '.yml' })
318 } -ParameterFilter { $Path -eq '.github/workflows' }
319 Mock Set-CIOutput {}
320 Mock Set-CIEnv {}
321 Mock Write-CIStepSummary {}
322 Mock Write-CIAnnotation {}
323 Mock New-Item {}
324 Mock Out-File {}
325 }
326
327 Context 'Annotation creation' {
328 It 'Creates annotation with correct parameters for each issue' {
329 Mock actionlint {
330 '{"message":"property runs-on is required","filepath":".github/workflows/ci.yml","line":15,"column":5}'
331 }
332
333 try { Invoke-YamlLintCore } catch { $null = $_ }
334 Should -Invoke Write-CIAnnotation -Times 1 -ParameterFilter {
335 $Level -eq 'Error' -and
336 $Message -eq 'property runs-on is required' -and
337 $File -eq '.github/workflows/ci.yml' -and
338 $Line -eq 15 -and
339 $Column -eq 5
340 }
341 }
342
343 It 'Creates annotation for each issue in array' {
344 Mock actionlint {
345 '[{"message":"error 1","filepath":"file1.yml","line":1,"column":1},{"message":"error 2","filepath":"file2.yml","line":2,"column":2}]'
346 }
347
348 try { Invoke-YamlLintCore } catch { $null = $_ }
349 Should -Invoke Write-CIAnnotation -Times 2
350 }
351 }
352
353 Context 'Host output' {
354 It 'Writes formatted error message to host' {
355 Mock actionlint {
356 '{"message":"test message","filepath":".github/workflows/ci.yml","line":10,"column":5}'
357 }
358 Mock Write-Host {}
359
360 try { Invoke-YamlLintCore } catch { $null = $_ }
361 # Verify error output format includes file:line:column: message
362 Should -Invoke Write-Host -ParameterFilter {
363 $Object -like '*ci.yml:10:5*test message*'
364 }
365 }
366 }
367}
368
369#endregion
370
371#region Output Generation Tests
372
373Describe 'Output Generation' -Tag 'Unit' {
374 BeforeAll {
375 $script:TempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
376 New-Item -ItemType Directory -Path $script:TempDir -Force | Out-Null
377 }
378
379 AfterAll {
380 Remove-Item -Path $script:TempDir -Recurse -Force -ErrorAction SilentlyContinue
381 }
382
383 Context 'JSON output file' {
384 BeforeEach {
385 Mock Get-Command { [PSCustomObject]@{ Source = 'actionlint' } } -ParameterFilter { $Name -eq 'actionlint' }
386 Mock Test-Path { $true } -ParameterFilter { $Path -eq '.github/workflows' }
387 Mock Get-ChildItem {
388 @([PSCustomObject]@{ FullName = '.github/workflows/ci.yml'; Extension = '.yml' })
389 } -ParameterFilter { $Path -eq '.github/workflows' }
390 Mock actionlint { '[]' }
391 Mock Set-CIOutput {}
392 Mock Set-CIEnv {}
393 Mock Write-CIStepSummary {}
394 Mock Write-CIAnnotation {}
395
396 $script:OutputFile = Join-Path $script:TempDir 'yaml-lint-results.json'
397 }
398
399 It 'Creates JSON output file at specified path' {
400 # Use real filesystem for this test
401 Invoke-YamlLintCore -OutputPath $script:OutputFile
402 Test-Path $script:OutputFile | Should -BeTrue
403 }
404
405 It 'Output file contains valid JSON' {
406 Invoke-YamlLintCore -OutputPath $script:OutputFile
407 { Get-Content $script:OutputFile | ConvertFrom-Json } | Should -Not -Throw
408 }
409 }
410
411 Context 'Directory creation' {
412 BeforeEach {
413 Mock Get-Command { [PSCustomObject]@{ Source = 'actionlint' } } -ParameterFilter { $Name -eq 'actionlint' }
414 Mock Test-Path { $true } -ParameterFilter { $Path -eq '.github/workflows' }
415 Mock Get-ChildItem {
416 @([PSCustomObject]@{ FullName = '.github/workflows/ci.yml'; Extension = '.yml' })
417 } -ParameterFilter { $Path -eq '.github/workflows' }
418 Mock actionlint { '[]' }
419 Mock Set-CIOutput {}
420 Mock Set-CIEnv {}
421 Mock Write-CIStepSummary {}
422 Mock Write-CIAnnotation {}
423 }
424
425 It 'Creates logs directory if missing' {
426 $newDir = Join-Path $script:TempDir 'newlogs'
427 $outputPath = Join-Path $newDir 'results.json'
428
429 Invoke-YamlLintCore -OutputPath $outputPath
430 Test-Path $newDir | Should -BeTrue
431 }
432 }
433}
434
435#endregion
436
437#region CI Integration Tests
438
439Describe 'CI Integration' -Tag 'Unit' {
440 BeforeEach {
441 Mock Get-Command { [PSCustomObject]@{ Source = 'actionlint' } } -ParameterFilter { $Name -eq 'actionlint' }
442 Mock Test-Path { $true } -ParameterFilter { $Path -eq '.github/workflows' }
443 Mock Get-ChildItem {
444 @([PSCustomObject]@{ FullName = '.github/workflows/ci.yml'; Extension = '.yml' })
445 } -ParameterFilter { $Path -eq '.github/workflows' }
446 Mock Set-CIOutput {}
447 Mock Set-CIEnv {}
448 Mock Write-CIStepSummary {}
449 Mock Write-CIAnnotation {}
450 Mock New-Item {}
451 Mock Out-File {}
452 }
453
454 Context 'CI outputs' {
455 It 'Sets count output with file count' {
456 Mock actionlint { '[]' }
457
458 Invoke-YamlLintCore
459 Should -Invoke Set-CIOutput -Times 1 -ParameterFilter { $Name -eq 'count' }
460 }
461
462 It 'Sets issues output with issue count' {
463 Mock actionlint { '[]' }
464
465 Invoke-YamlLintCore
466 Should -Invoke Set-CIOutput -Times 1 -ParameterFilter { $Name -eq 'issues' }
467 }
468
469 It 'Sets errors output with error count' {
470 Mock actionlint { '[]' }
471
472 Invoke-YamlLintCore
473 Should -Invoke Set-CIOutput -Times 1 -ParameterFilter { $Name -eq 'errors' }
474 }
475 }
476
477 Context 'CI environment variables' {
478 It 'Sets YAML_LINT_FAILED when issues found' {
479 Mock actionlint {
480 '{"message":"error","filepath":"ci.yml","line":1,"column":1}'
481 }
482
483 try { Invoke-YamlLintCore } catch { Write-Verbose 'Expected error' }
484 Should -Invoke Set-CIEnv -Times 1 -ParameterFilter {
485 $Name -eq 'YAML_LINT_FAILED' -and $Value -eq 'true'
486 }
487 }
488
489 It 'Does not set YAML_LINT_FAILED when no issues' {
490 Mock actionlint { '[]' }
491
492 Invoke-YamlLintCore
493 Should -Invoke Set-CIEnv -Times 0 -ParameterFilter {
494 $Name -eq 'YAML_LINT_FAILED'
495 }
496 }
497 }
498
499 Context 'CI step summary' {
500 It 'Writes success summary when no issues' {
501 Mock actionlint { '[]' }
502
503 Invoke-YamlLintCore
504 Should -Invoke Write-CIStepSummary -Times 2
505 }
506
507 It 'Writes failure summary with table when issues found' {
508 Mock actionlint {
509 '{"message":"error","filepath":"ci.yml","line":1,"column":1}'
510 }
511
512 try { Invoke-YamlLintCore } catch { Write-Verbose 'Expected error' }
513 Should -Invoke Write-CIStepSummary -Times 2
514 }
515 }
516}
517
518#endregion
519
520#region Exit Code Tests
521
522Describe 'Exit Code Handling' -Tag 'Unit' {
523 Context 'Success scenarios (exit 0)' {
524 BeforeEach {
525 Mock Get-Command { [PSCustomObject]@{ Source = 'actionlint' } } -ParameterFilter { $Name -eq 'actionlint' }
526 Mock Set-CIOutput {}
527 Mock Set-CIEnv {}
528 Mock Write-CIStepSummary {}
529 Mock Write-CIAnnotation {}
530 Mock New-Item {}
531 Mock Out-File {}
532 }
533
534 It 'Returns success when no files to analyze' {
535 Mock Test-Path { $false } -ParameterFilter { $Path -eq '.github/workflows' }
536
537 { Invoke-YamlLintCore } | Should -Not -Throw
538 }
539
540 It 'Returns success when files have no issues' {
541 Mock Test-Path { $true } -ParameterFilter { $Path -eq '.github/workflows' }
542 Mock Get-ChildItem {
543 @([PSCustomObject]@{ FullName = '.github/workflows/ci.yml'; Extension = '.yml' })
544 } -ParameterFilter { $Path -eq '.github/workflows' }
545 Mock actionlint { '[]' }
546
547 { Invoke-YamlLintCore } | Should -Not -Throw
548 }
549 }
550
551 Context 'Failure scenarios (exit 1)' {
552 BeforeEach {
553 Mock Set-CIOutput {}
554 Mock Set-CIEnv {}
555 Mock Write-CIStepSummary {}
556 Mock Write-CIAnnotation {}
557 }
558
559 It 'Exits with error when actionlint not installed' {
560 Mock Get-Command { $null } -ParameterFilter { $Name -eq 'actionlint' }
561
562 { Invoke-YamlLintCore } | Should -Throw '*actionlint is not installed*'
563 }
564
565 It 'Exits with error when issues found' {
566 Mock Get-Command { [PSCustomObject]@{ Source = 'actionlint' } } -ParameterFilter { $Name -eq 'actionlint' }
567 Mock Test-Path { $true } -ParameterFilter { $Path -eq '.github/workflows' }
568 Mock Get-ChildItem {
569 @([PSCustomObject]@{ FullName = '.github/workflows/ci.yml'; Extension = '.yml' })
570 } -ParameterFilter { $Path -eq '.github/workflows' }
571 Mock actionlint {
572 '{"message":"error found","filepath":"ci.yml","line":1,"column":1}'
573 }
574 Mock New-Item {}
575 Mock Out-File {}
576
577 try { Invoke-YamlLintCore } catch { $null = $_ }
578 Should -Invoke Write-CIAnnotation -Times 1
579 }
580 }
581}
582
583#endregion