microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/context-working

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

592lines · 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 It 'Writes Timestamp using Get-StandardTimestamp in summary JSON' {
411 Mock Get-StandardTimestamp { return '2025-01-15T18:30:00.0000000Z' }
412
413 Invoke-YamlLintCore -OutputPath $script:OutputFile
414 $summary = Get-Content 'logs/yaml-lint-summary.json' | ConvertFrom-Json
415 $summary.Timestamp.ToString('o') | Should -Match (Get-StandardTimestampPattern)
416 Should -Invoke Get-StandardTimestamp -Times 1 -Exactly
417 }
418 }
419
420 Context 'Directory creation' {
421 BeforeEach {
422 Mock Get-Command { [PSCustomObject]@{ Source = 'actionlint' } } -ParameterFilter { $Name -eq 'actionlint' }
423 Mock Test-Path { $true } -ParameterFilter { $Path -eq '.github/workflows' }
424 Mock Get-ChildItem {
425 @([PSCustomObject]@{ FullName = '.github/workflows/ci.yml'; Extension = '.yml' })
426 } -ParameterFilter { $Path -eq '.github/workflows' }
427 Mock actionlint { '[]' }
428 Mock Set-CIOutput {}
429 Mock Set-CIEnv {}
430 Mock Write-CIStepSummary {}
431 Mock Write-CIAnnotation {}
432 }
433
434 It 'Creates logs directory if missing' {
435 $newDir = Join-Path $script:TempDir 'newlogs'
436 $outputPath = Join-Path $newDir 'results.json'
437
438 Invoke-YamlLintCore -OutputPath $outputPath
439 Test-Path $newDir | Should -BeTrue
440 }
441 }
442}
443
444#endregion
445
446#region CI Integration Tests
447
448Describe 'CI Integration' -Tag 'Unit' {
449 BeforeEach {
450 Mock Get-Command { [PSCustomObject]@{ Source = 'actionlint' } } -ParameterFilter { $Name -eq 'actionlint' }
451 Mock Test-Path { $true } -ParameterFilter { $Path -eq '.github/workflows' }
452 Mock Get-ChildItem {
453 @([PSCustomObject]@{ FullName = '.github/workflows/ci.yml'; Extension = '.yml' })
454 } -ParameterFilter { $Path -eq '.github/workflows' }
455 Mock Set-CIOutput {}
456 Mock Set-CIEnv {}
457 Mock Write-CIStepSummary {}
458 Mock Write-CIAnnotation {}
459 Mock New-Item {}
460 Mock Out-File {}
461 }
462
463 Context 'CI outputs' {
464 It 'Sets count output with file count' {
465 Mock actionlint { '[]' }
466
467 Invoke-YamlLintCore
468 Should -Invoke Set-CIOutput -Times 1 -ParameterFilter { $Name -eq 'count' }
469 }
470
471 It 'Sets issues output with issue count' {
472 Mock actionlint { '[]' }
473
474 Invoke-YamlLintCore
475 Should -Invoke Set-CIOutput -Times 1 -ParameterFilter { $Name -eq 'issues' }
476 }
477
478 It 'Sets errors output with error count' {
479 Mock actionlint { '[]' }
480
481 Invoke-YamlLintCore
482 Should -Invoke Set-CIOutput -Times 1 -ParameterFilter { $Name -eq 'errors' }
483 }
484 }
485
486 Context 'CI environment variables' {
487 It 'Sets YAML_LINT_FAILED when issues found' {
488 Mock actionlint {
489 '{"message":"error","filepath":"ci.yml","line":1,"column":1}'
490 }
491
492 try { Invoke-YamlLintCore } catch { Write-Verbose 'Expected error' }
493 Should -Invoke Set-CIEnv -Times 1 -ParameterFilter {
494 $Name -eq 'YAML_LINT_FAILED' -and $Value -eq 'true'
495 }
496 }
497
498 It 'Does not set YAML_LINT_FAILED when no issues' {
499 Mock actionlint { '[]' }
500
501 Invoke-YamlLintCore
502 Should -Invoke Set-CIEnv -Times 0 -ParameterFilter {
503 $Name -eq 'YAML_LINT_FAILED'
504 }
505 }
506 }
507
508 Context 'CI step summary' {
509 It 'Writes success summary when no issues' {
510 Mock actionlint { '[]' }
511
512 Invoke-YamlLintCore
513 Should -Invoke Write-CIStepSummary -Times 2
514 }
515
516 It 'Writes failure summary with table when issues found' {
517 Mock actionlint {
518 '{"message":"error","filepath":"ci.yml","line":1,"column":1}'
519 }
520
521 try { Invoke-YamlLintCore } catch { Write-Verbose 'Expected error' }
522 Should -Invoke Write-CIStepSummary -Times 2
523 }
524 }
525}
526
527#endregion
528
529#region Exit Code Tests
530
531Describe 'Exit Code Handling' -Tag 'Unit' {
532 Context 'Success scenarios (exit 0)' {
533 BeforeEach {
534 Mock Get-Command { [PSCustomObject]@{ Source = 'actionlint' } } -ParameterFilter { $Name -eq 'actionlint' }
535 Mock Set-CIOutput {}
536 Mock Set-CIEnv {}
537 Mock Write-CIStepSummary {}
538 Mock Write-CIAnnotation {}
539 Mock New-Item {}
540 Mock Out-File {}
541 }
542
543 It 'Returns success when no files to analyze' {
544 Mock Test-Path { $false } -ParameterFilter { $Path -eq '.github/workflows' }
545
546 { Invoke-YamlLintCore } | Should -Not -Throw
547 }
548
549 It 'Returns success when files have no issues' {
550 Mock Test-Path { $true } -ParameterFilter { $Path -eq '.github/workflows' }
551 Mock Get-ChildItem {
552 @([PSCustomObject]@{ FullName = '.github/workflows/ci.yml'; Extension = '.yml' })
553 } -ParameterFilter { $Path -eq '.github/workflows' }
554 Mock actionlint { '[]' }
555
556 { Invoke-YamlLintCore } | Should -Not -Throw
557 }
558 }
559
560 Context 'Failure scenarios (exit 1)' {
561 BeforeEach {
562 Mock Set-CIOutput {}
563 Mock Set-CIEnv {}
564 Mock Write-CIStepSummary {}
565 Mock Write-CIAnnotation {}
566 }
567
568 It 'Exits with error when actionlint not installed' {
569 Mock Get-Command { $null } -ParameterFilter { $Name -eq 'actionlint' }
570
571 { Invoke-YamlLintCore } | Should -Throw '*actionlint is not installed*'
572 }
573
574 It 'Exits with error when issues found' {
575 Mock Get-Command { [PSCustomObject]@{ Source = 'actionlint' } } -ParameterFilter { $Name -eq 'actionlint' }
576 Mock Test-Path { $true } -ParameterFilter { $Path -eq '.github/workflows' }
577 Mock Get-ChildItem {
578 @([PSCustomObject]@{ FullName = '.github/workflows/ci.yml'; Extension = '.yml' })
579 } -ParameterFilter { $Path -eq '.github/workflows' }
580 Mock actionlint {
581 '{"message":"error found","filepath":"ci.yml","line":1,"column":1}'
582 }
583 Mock New-Item {}
584 Mock Out-File {}
585
586 try { Invoke-YamlLintCore } catch { $null = $_ }
587 Should -Invoke Write-CIAnnotation -Times 1
588 }
589 }
590}
591
592#endregion
593