microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/add-pester-code-coverage

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/tests/linting/LintingHelpers.Tests.ps1

587lines · modecode

1#Requires -Modules Pester
2<#
3.SYNOPSIS
4 Pester tests for LintingHelpers.psm1 module
5.DESCRIPTION
6 Comprehensive tests for all 7 exported functions in the LintingHelpers module:
7 - Get-ChangedFilesFromGit
8 - Get-FilesRecursive
9 - Get-GitIgnorePatterns
10 - Write-GitHubAnnotation
11 - Set-GitHubOutput
12 - Set-GitHubEnv
13 - Write-GitHubStepSummary
14#>
15
16BeforeAll {
17 $modulePath = Join-Path $PSScriptRoot '../../linting/Modules/LintingHelpers.psm1'
18 $mockPath = Join-Path $PSScriptRoot '../Mocks/GitMocks.psm1'
19
20 Import-Module $modulePath -Force
21 Import-Module $mockPath -Force
22}
23
24#region Get-ChangedFilesFromGit Tests
25
26Describe 'Get-ChangedFilesFromGit' {
27 Context 'Merge-base succeeds' {
28 BeforeEach {
29 # Mock git commands at module scope with proper LASTEXITCODE handling
30 $changedFiles = @('scripts/test.ps1', 'docs/readme.md', 'config/settings.json')
31
32 Mock git {
33 $global:LASTEXITCODE = 0
34 return 'abc123def456789'
35 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'merge-base' }
36
37 Mock git {
38 $global:LASTEXITCODE = 0
39 return $changedFiles
40 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'diff' }
41
42 Mock Test-Path { return $true } -ModuleName 'LintingHelpers' -ParameterFilter { $PathType -eq 'Leaf' }
43 }
44
45 It 'Returns changed files filtered by extension' {
46 $result = Get-ChangedFilesFromGit -FileExtensions @('*.ps1')
47 $result | Should -Contain 'scripts/test.ps1'
48 $result | Should -Not -Contain 'docs/readme.md'
49 $result | Should -Not -Contain 'config/settings.json'
50 }
51
52 It 'Returns all files with wildcard extension' {
53 $result = Get-ChangedFilesFromGit -FileExtensions @('*')
54 $result.Count | Should -Be 3
55 }
56
57 It 'Returns files matching multiple extension patterns' {
58 $result = Get-ChangedFilesFromGit -FileExtensions @('*.ps1', '*.md')
59 $result | Should -Contain 'scripts/test.ps1'
60 $result | Should -Contain 'docs/readme.md'
61 $result | Should -Not -Contain 'config/settings.json'
62 }
63
64 It 'Uses default extension pattern when not specified' {
65 $result = Get-ChangedFilesFromGit
66 $result.Count | Should -Be 3
67 }
68 }
69
70 Context 'Merge-base fails, HEAD~1 fallback' {
71 BeforeEach {
72 # Merge-base fails
73 Mock git {
74 $global:LASTEXITCODE = 128
75 return $null
76 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'merge-base' }
77
78 # rev-parse succeeds for HEAD~1 check
79 Mock git {
80 $global:LASTEXITCODE = 0
81 return 'HEAD~1-sha'
82 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'rev-parse' }
83
84 # diff returns fallback file
85 Mock git {
86 $global:LASTEXITCODE = 0
87 return @('fallback-file.ps1')
88 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'diff' }
89
90 Mock Test-Path { return $true } -ModuleName 'LintingHelpers' -ParameterFilter { $PathType -eq 'Leaf' }
91 }
92
93 It 'Falls back to HEAD~1 comparison and returns files' {
94 $result = Get-ChangedFilesFromGit -FileExtensions @('*.ps1')
95 $result | Should -Contain 'fallback-file.ps1'
96 }
97 }
98
99 Context 'Empty results' {
100 BeforeEach {
101 Mock git {
102 $global:LASTEXITCODE = 0
103 return 'abc123def456789'
104 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'merge-base' }
105
106 Mock git {
107 $global:LASTEXITCODE = 0
108 return @()
109 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'diff' }
110 }
111
112 It 'Returns empty array when no files changed' {
113 $result = Get-ChangedFilesFromGit
114 $result | Should -BeNullOrEmpty
115 }
116 }
117
118 Context 'File existence filtering' {
119 BeforeEach {
120 Mock git {
121 $global:LASTEXITCODE = 0
122 return 'abc123def456789'
123 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'merge-base' }
124
125 Mock git {
126 $global:LASTEXITCODE = 0
127 return @('exists.ps1', 'deleted.ps1')
128 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'diff' }
129
130 Mock Test-Path {
131 param($Path)
132 return $Path -eq 'exists.ps1'
133 } -ModuleName 'LintingHelpers' -ParameterFilter { $PathType -eq 'Leaf' }
134 }
135
136 It 'Excludes files that no longer exist' {
137 $result = Get-ChangedFilesFromGit -FileExtensions @('*.ps1')
138 $result | Should -Contain 'exists.ps1'
139 $result | Should -Not -Contain 'deleted.ps1'
140 }
141 }
142
143 Context 'Empty and whitespace file entries' {
144 BeforeEach {
145 Mock git {
146 $global:LASTEXITCODE = 0
147 return 'abc123def456789'
148 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'merge-base' }
149
150 Mock git {
151 $global:LASTEXITCODE = 0
152 return @('valid.ps1', '', ' ', 'another.ps1')
153 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'diff' }
154
155 Mock Test-Path { return $true } -ModuleName 'LintingHelpers' -ParameterFilter { $PathType -eq 'Leaf' }
156 }
157
158 It 'Filters out empty and whitespace entries' {
159 $result = Get-ChangedFilesFromGit -FileExtensions @('*.ps1')
160 $result | Should -Contain 'valid.ps1'
161 $result | Should -Contain 'another.ps1'
162 $result | Should -Not -Contain ''
163 $result | Should -Not -Contain ' '
164 }
165 }
166
167 Context 'Both merge-base and HEAD~1 fail, third fallback' {
168 BeforeEach {
169 # Merge-base fails
170 Mock git {
171 $global:LASTEXITCODE = 128
172 return $null
173 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'merge-base' }
174
175 # rev-parse fails for HEAD~1 check
176 Mock git {
177 $global:LASTEXITCODE = 128
178 return $null
179 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'rev-parse' }
180
181 # diff returns files from third fallback (git diff --name-only HEAD)
182 Mock git {
183 $global:LASTEXITCODE = 0
184 return @('unstaged-file.ps1')
185 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'diff' }
186
187 Mock Test-Path { return $true } -ModuleName 'LintingHelpers' -ParameterFilter { $PathType -eq 'Leaf' }
188 }
189
190 It 'Falls back to git diff --name-only HEAD and returns files' {
191 $result = Get-ChangedFilesFromGit -FileExtensions @('*.ps1')
192 $result | Should -Contain 'unstaged-file.ps1'
193 }
194 }
195
196 Context 'Git diff command fails' {
197 BeforeEach {
198 Mock git {
199 $global:LASTEXITCODE = 0
200 return 'abc123def456789'
201 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'merge-base' }
202
203 # Diff fails with non-zero exit code
204 Mock git {
205 $global:LASTEXITCODE = 1
206 return $null
207 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'diff' }
208 }
209
210 It 'Returns empty array when git diff fails' {
211 $result = Get-ChangedFilesFromGit
212 $result | Should -BeNullOrEmpty
213 }
214 }
215
216 Context 'Exception during execution' {
217 BeforeEach {
218 Mock git {
219 throw "Simulated git failure"
220 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'merge-base' }
221 }
222
223 It 'Catches exceptions and returns empty array' {
224 $result = Get-ChangedFilesFromGit
225 $result | Should -BeNullOrEmpty
226 }
227 }
228}
229
230#endregion
231
232#region Get-FilesRecursive Tests
233
234Describe 'Get-FilesRecursive' {
235 Context 'Basic file enumeration' {
236 BeforeEach {
237 New-Item -Path 'TestDrive:/scripts' -ItemType Directory -Force | Out-Null
238 New-Item -Path 'TestDrive:/scripts/test.ps1' -ItemType File -Force | Out-Null
239 New-Item -Path 'TestDrive:/scripts/readme.md' -ItemType File -Force | Out-Null
240 New-Item -Path 'TestDrive:/scripts/sub' -ItemType Directory -Force | Out-Null
241 New-Item -Path 'TestDrive:/scripts/sub/nested.ps1' -ItemType File -Force | Out-Null
242 }
243
244 It 'Finds files matching Include pattern' {
245 $result = Get-FilesRecursive -Path 'TestDrive:/scripts' -Include @('*.ps1')
246 $result.Count | Should -Be 2
247 $result.Name | Should -Contain 'test.ps1'
248 $result.Name | Should -Contain 'nested.ps1'
249 }
250
251 It 'Finds files with multiple Include patterns' {
252 $result = Get-FilesRecursive -Path 'TestDrive:/scripts' -Include @('*.ps1', '*.md')
253 $result.Count | Should -Be 3
254 }
255
256 It 'Does not include directories in results' {
257 $result = Get-FilesRecursive -Path 'TestDrive:/scripts' -Include @('*')
258 $result | ForEach-Object { $_.PSIsContainer | Should -BeFalse }
259 }
260 }
261
262 Context 'Gitignore filtering' {
263 BeforeEach {
264 New-Item -Path 'TestDrive:/project' -ItemType Directory -Force | Out-Null
265 New-Item -Path 'TestDrive:/project/src' -ItemType Directory -Force | Out-Null
266 New-Item -Path 'TestDrive:/project/src/app.ps1' -ItemType File -Force | Out-Null
267 New-Item -Path 'TestDrive:/project/node_modules' -ItemType Directory -Force | Out-Null
268 New-Item -Path 'TestDrive:/project/node_modules/pkg.ps1' -ItemType File -Force | Out-Null
269 'node_modules/' | Set-Content 'TestDrive:/project/.gitignore'
270 }
271
272 It 'Excludes files matching gitignore patterns' {
273 $result = Get-FilesRecursive -Path 'TestDrive:/project' `
274 -Include @('*.ps1') `
275 -GitIgnorePath 'TestDrive:/project/.gitignore'
276 $result.Name | Should -Contain 'app.ps1'
277 $result.Name | Should -Not -Contain 'pkg.ps1'
278 }
279
280 It 'Returns all files when gitignore path not provided' {
281 $result = Get-FilesRecursive -Path 'TestDrive:/project' -Include @('*.ps1')
282 $result.Count | Should -Be 2
283 }
284 }
285
286 Context 'Invalid paths' {
287 It 'Returns empty for non-existent path' {
288 $result = Get-FilesRecursive -Path 'TestDrive:/nonexistent' -Include @('*.ps1')
289 $result | Should -BeNullOrEmpty
290 }
291 }
292
293 Context 'No gitignore file' {
294 BeforeEach {
295 New-Item -Path 'TestDrive:/simple' -ItemType Directory -Force | Out-Null
296 New-Item -Path 'TestDrive:/simple/file.ps1' -ItemType File -Force | Out-Null
297 }
298
299 It 'Returns files when gitignore does not exist' {
300 $result = Get-FilesRecursive -Path 'TestDrive:/simple' `
301 -Include @('*.ps1') `
302 -GitIgnorePath 'TestDrive:/simple/.gitignore'
303 $result.Count | Should -Be 1
304 }
305 }
306}
307
308#endregion
309
310#region Get-GitIgnorePatterns Tests
311
312Describe 'Get-GitIgnorePatterns' {
313 Context 'Non-existent file' {
314 It 'Returns empty for non-existent file' {
315 $result = Get-GitIgnorePatterns -GitIgnorePath 'TestDrive:/nonexistent/.gitignore'
316 $result | Should -BeNullOrEmpty
317 }
318 }
319
320 Context 'Empty file' {
321 BeforeEach {
322 New-Item -Path 'TestDrive:/.gitignore-empty' -ItemType File -Force | Out-Null
323 }
324
325 It 'Returns empty for empty file' {
326 $result = Get-GitIgnorePatterns -GitIgnorePath 'TestDrive:/.gitignore-empty'
327 $result | Should -BeNullOrEmpty
328 }
329 }
330
331 Context 'Pattern parsing' {
332 It 'Skips comments and empty lines' {
333 @('# Comment', '', 'node_modules/', ' ', '*.log') | Set-Content 'TestDrive:/.gitignore'
334 $result = Get-GitIgnorePatterns -GitIgnorePath 'TestDrive:/.gitignore'
335 $result.Count | Should -Be 2
336 }
337
338 It 'Converts directory patterns correctly' {
339 $gitignorePath = Join-Path $TestDrive '.gitignore-dir'
340 'node_modules/' | Set-Content $gitignorePath
341 $result = @(Get-GitIgnorePatterns -GitIgnorePath $gitignorePath)
342 $sep = [System.IO.Path]::DirectorySeparatorChar
343 # Function wraps directory patterns with platform separator
344 $result[0] | Should -Be "*${sep}node_modules${sep}*"
345 }
346
347 It 'Converts file patterns with paths correctly' {
348 $gitignorePath = Join-Path $TestDrive '.gitignore-path'
349 'build/output.log' | Set-Content $gitignorePath
350 $result = @(Get-GitIgnorePatterns -GitIgnorePath $gitignorePath)
351 $sep = [System.IO.Path]::DirectorySeparatorChar
352 # Function normalizes paths and wraps with wildcards
353 $result[0] | Should -Be "*${sep}build${sep}output.log*"
354 }
355
356 It 'Handles simple file patterns' {
357 $gitignorePath = Join-Path $TestDrive '.gitignore-simple'
358 '*.log' | Set-Content $gitignorePath
359 $result = @(Get-GitIgnorePatterns -GitIgnorePath $gitignorePath)
360 $sep = [System.IO.Path]::DirectorySeparatorChar
361 # Function wraps simple patterns with wildcards
362 $result[0] | Should -Be "*${sep}*.log${sep}*"
363 }
364
365 It 'Processes multiple patterns' {
366 @('node_modules/', 'dist/', '*.tmp', 'logs/debug.log') | Set-Content 'TestDrive:/.gitignore-multi'
367 $result = Get-GitIgnorePatterns -GitIgnorePath 'TestDrive:/.gitignore-multi'
368 $result.Count | Should -Be 4
369 }
370 }
371}
372
373#endregion
374
375#region Write-GitHubAnnotation Tests
376
377Describe 'Write-GitHubAnnotation' {
378 Context 'Basic annotation types' {
379 It 'Writes error annotation without properties' {
380 $output = Write-GitHubAnnotation -Type 'error' -Message 'Test error' 6>&1
381 $output | Should -Be '::error::Test error'
382 }
383
384 It 'Writes warning annotation without properties' {
385 $output = Write-GitHubAnnotation -Type 'warning' -Message 'Test warning' 6>&1
386 $output | Should -Be '::warning::Test warning'
387 }
388
389 It 'Writes notice annotation without properties' {
390 $output = Write-GitHubAnnotation -Type 'notice' -Message 'Test notice' 6>&1
391 $output | Should -Be '::notice::Test notice'
392 }
393 }
394
395 Context 'Annotation with file property' {
396 It 'Includes file property when specified' {
397 $output = Write-GitHubAnnotation -Type 'warning' -Message 'File warning' -File 'test.ps1' 6>&1
398 $output | Should -Be '::warning file=test.ps1::File warning'
399 }
400 }
401
402 Context 'Annotation with line and column' {
403 It 'Includes file and line properties' {
404 $output = Write-GitHubAnnotation -Type 'notice' -Message 'Line notice' -File 'test.ps1' -Line 10 6>&1
405 $output | Should -Be '::notice file=test.ps1,line=10::Line notice'
406 }
407
408 It 'Includes all properties when specified' {
409 $output = Write-GitHubAnnotation -Type 'error' -Message 'Full error' -File 'test.ps1' -Line 10 -Column 5 6>&1
410 $output | Should -Be '::error file=test.ps1,line=10,col=5::Full error'
411 }
412
413 It 'Omits line when zero' {
414 $output = Write-GitHubAnnotation -Type 'error' -Message 'Zero line' -File 'test.ps1' -Line 0 6>&1
415 $output | Should -Be '::error file=test.ps1::Zero line'
416 }
417
418 It 'Omits column when zero' {
419 $output = Write-GitHubAnnotation -Type 'error' -Message 'Zero col' -File 'test.ps1' -Line 10 -Column 0 6>&1
420 $output | Should -Be '::error file=test.ps1,line=10::Zero col'
421 }
422 }
423}
424
425#endregion
426
427#region Set-GitHubOutput Tests
428
429Describe 'Set-GitHubOutput' {
430 BeforeAll {
431 Save-GitHubEnvironment
432 }
433
434 AfterAll {
435 Restore-GitHubEnvironment
436 }
437
438 Context 'In GitHub Actions environment' {
439 BeforeEach {
440 $script:mockFiles = Initialize-MockGitHubEnvironment
441 }
442
443 AfterEach {
444 Remove-MockGitHubFiles -MockFiles $script:mockFiles
445 }
446
447 It 'Writes output to GITHUB_OUTPUT file' {
448 Set-GitHubOutput -Name 'result' -Value 'success'
449 $content = Get-Content $script:mockFiles.Output
450 $content | Should -Contain 'result=success'
451 }
452
453 It 'Appends multiple outputs to file' {
454 Set-GitHubOutput -Name 'first' -Value 'one'
455 Set-GitHubOutput -Name 'second' -Value 'two'
456 $content = Get-Content $script:mockFiles.Output
457 $content | Should -Contain 'first=one'
458 $content | Should -Contain 'second=two'
459 }
460
461 It 'Handles values with special characters' {
462 Set-GitHubOutput -Name 'path' -Value 'C:\Users\test'
463 $content = Get-Content $script:mockFiles.Output
464 $content | Should -Contain 'path=C:\Users\test'
465 }
466 }
467
468 Context 'Outside GitHub Actions environment' {
469 BeforeEach {
470 Clear-MockGitHubEnvironment
471 }
472
473 It 'Does not throw when GITHUB_OUTPUT is not set' {
474 { Set-GitHubOutput -Name 'test' -Value 'value' } | Should -Not -Throw
475 }
476 }
477}
478
479#endregion
480
481#region Set-GitHubEnv Tests
482
483Describe 'Set-GitHubEnv' {
484 BeforeAll {
485 Save-GitHubEnvironment
486 }
487
488 AfterAll {
489 Restore-GitHubEnvironment
490 }
491
492 Context 'In GitHub Actions environment' {
493 BeforeEach {
494 $script:mockFiles = Initialize-MockGitHubEnvironment
495 }
496
497 AfterEach {
498 Remove-MockGitHubFiles -MockFiles $script:mockFiles
499 }
500
501 It 'Writes environment variable to GITHUB_ENV file' {
502 Set-GitHubEnv -Name 'MY_VAR' -Value 'my_value'
503 $content = Get-Content $script:mockFiles.Env
504 $content | Should -Contain 'MY_VAR=my_value'
505 }
506
507 It 'Appends multiple environment variables' {
508 Set-GitHubEnv -Name 'VAR1' -Value 'value1'
509 Set-GitHubEnv -Name 'VAR2' -Value 'value2'
510 $content = Get-Content $script:mockFiles.Env
511 $content | Should -Contain 'VAR1=value1'
512 $content | Should -Contain 'VAR2=value2'
513 }
514 }
515
516 Context 'Outside GitHub Actions environment' {
517 BeforeEach {
518 Clear-MockGitHubEnvironment
519 }
520
521 It 'Does not throw when GITHUB_ENV is not set' {
522 { Set-GitHubEnv -Name 'test' -Value 'value' } | Should -Not -Throw
523 }
524 }
525}
526
527#endregion
528
529#region Write-GitHubStepSummary Tests
530
531Describe 'Write-GitHubStepSummary' {
532 BeforeAll {
533 Save-GitHubEnvironment
534 }
535
536 AfterAll {
537 Restore-GitHubEnvironment
538 }
539
540 Context 'In GitHub Actions environment' {
541 BeforeEach {
542 $script:mockFiles = Initialize-MockGitHubEnvironment
543 }
544
545 AfterEach {
546 Remove-MockGitHubFiles -MockFiles $script:mockFiles
547 }
548
549 It 'Writes content to GITHUB_STEP_SUMMARY file' {
550 Write-GitHubStepSummary -Content '# Test Summary'
551 $content = Get-Content $script:mockFiles.Summary -Raw
552 $content | Should -Match '# Test Summary'
553 }
554
555 It 'Appends multiple summary entries' {
556 Write-GitHubStepSummary -Content '## Section 1'
557 Write-GitHubStepSummary -Content '## Section 2'
558 $content = Get-Content $script:mockFiles.Summary -Raw
559 $content | Should -Match '## Section 1'
560 $content | Should -Match '## Section 2'
561 }
562
563 It 'Handles markdown table content' {
564 $table = @"
565| Column 1 | Column 2 |
566|----------|----------|
567| Value 1 | Value 2 |
568"@
569 Write-GitHubStepSummary -Content $table
570 $content = Get-Content $script:mockFiles.Summary -Raw
571 $content | Should -Match 'Column 1'
572 $content | Should -Match 'Value 1'
573 }
574 }
575
576 Context 'Outside GitHub Actions environment' {
577 BeforeEach {
578 Clear-MockGitHubEnvironment
579 }
580
581 It 'Does not throw when GITHUB_STEP_SUMMARY is not set' {
582 { Write-GitHubStepSummary -Content 'Test content' } | Should -Not -Throw
583 }
584 }
585}
586
587#endregion
588