microsoft/hve-core

Public

mirrored from https://github.com/microsoft/hve-coreAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
docs/659-role-based-docs-lifecycle-guides

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/tests/linting/LintingHelpers.Tests.ps1

484lines · modecode

1#Requires -Modules Pester
2# Copyright (c) Microsoft Corporation.
3# SPDX-License-Identifier: MIT
4<#
5.SYNOPSIS
6 Pester tests for LintingHelpers.psm1 module
7.DESCRIPTION
8 Comprehensive tests for all 3 exported functions in the LintingHelpers module:
9 - Get-ChangedFilesFromGit
10 - Get-FilesRecursive
11 - Get-GitIgnorePatterns
12#>
13
14BeforeAll {
15 $modulePath = Join-Path $PSScriptRoot '../../linting/Modules/LintingHelpers.psm1'
16 Import-Module $modulePath -Force
17}
18
19#region Get-ChangedFilesFromGit Tests
20
21Describe 'Get-ChangedFilesFromGit' {
22 Context 'Merge-base succeeds' {
23 BeforeEach {
24 # Mock git commands at module scope with proper LASTEXITCODE handling
25 $changedFiles = @('scripts/test.ps1', 'docs/readme.md', 'config/settings.json')
26
27 Mock git {
28 $global:LASTEXITCODE = 0
29 return 'abc123def456789'
30 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'merge-base' }
31
32 Mock git {
33 $global:LASTEXITCODE = 0
34 return $changedFiles
35 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'diff' }
36
37 Mock Test-Path { return $true } -ModuleName 'LintingHelpers' -ParameterFilter { $PathType -eq 'Leaf' }
38 }
39
40 It 'Returns changed files filtered by extension' {
41 $result = Get-ChangedFilesFromGit -FileExtensions @('*.ps1')
42 $result | Should -Contain 'scripts/test.ps1'
43 $result | Should -Not -Contain 'docs/readme.md'
44 $result | Should -Not -Contain 'config/settings.json'
45 }
46
47 It 'Returns all files with wildcard extension' {
48 $result = Get-ChangedFilesFromGit -FileExtensions @('*')
49 $result.Count | Should -Be 3
50 }
51
52 It 'Returns files matching multiple extension patterns' {
53 $result = Get-ChangedFilesFromGit -FileExtensions @('*.ps1', '*.md')
54 $result | Should -Contain 'scripts/test.ps1'
55 $result | Should -Contain 'docs/readme.md'
56 $result | Should -Not -Contain 'config/settings.json'
57 }
58
59 It 'Uses default extension pattern when not specified' {
60 $result = Get-ChangedFilesFromGit
61 $result.Count | Should -Be 3
62 }
63 }
64
65 Context 'Merge-base fails, HEAD~1 fallback' {
66 BeforeEach {
67 # Merge-base fails
68 Mock git {
69 $global:LASTEXITCODE = 128
70 return $null
71 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'merge-base' }
72
73 # rev-parse succeeds for HEAD~1 check
74 Mock git {
75 $global:LASTEXITCODE = 0
76 return 'HEAD~1-sha'
77 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'rev-parse' }
78
79 # diff returns fallback file
80 Mock git {
81 $global:LASTEXITCODE = 0
82 return @('fallback-file.ps1')
83 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'diff' }
84
85 Mock Test-Path { return $true } -ModuleName 'LintingHelpers' -ParameterFilter { $PathType -eq 'Leaf' }
86 }
87
88 It 'Falls back to HEAD~1 comparison and returns files' {
89 $result = Get-ChangedFilesFromGit -FileExtensions @('*.ps1')
90 $result | Should -Contain 'fallback-file.ps1'
91 }
92 }
93
94 Context 'Empty results' {
95 BeforeEach {
96 Mock git {
97 $global:LASTEXITCODE = 0
98 return 'abc123def456789'
99 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'merge-base' }
100
101 Mock git {
102 $global:LASTEXITCODE = 0
103 return @()
104 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'diff' }
105 }
106
107 It 'Returns empty array when no files changed' {
108 $result = Get-ChangedFilesFromGit
109 $result | Should -BeNullOrEmpty
110 }
111 }
112
113 Context 'File existence filtering' {
114 BeforeEach {
115 Mock git {
116 $global:LASTEXITCODE = 0
117 return 'abc123def456789'
118 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'merge-base' }
119
120 Mock git {
121 $global:LASTEXITCODE = 0
122 return @('exists.ps1', 'deleted.ps1')
123 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'diff' }
124
125 Mock Test-Path {
126 param($Path)
127 return $Path -eq 'exists.ps1'
128 } -ModuleName 'LintingHelpers' -ParameterFilter { $PathType -eq 'Leaf' }
129 }
130
131 It 'Excludes files that no longer exist' {
132 $result = Get-ChangedFilesFromGit -FileExtensions @('*.ps1')
133 $result | Should -Contain 'exists.ps1'
134 $result | Should -Not -Contain 'deleted.ps1'
135 }
136 }
137
138 Context 'Empty and whitespace file entries' {
139 BeforeEach {
140 Mock git {
141 $global:LASTEXITCODE = 0
142 return 'abc123def456789'
143 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'merge-base' }
144
145 Mock git {
146 $global:LASTEXITCODE = 0
147 return @('valid.ps1', '', ' ', 'another.ps1')
148 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'diff' }
149
150 Mock Test-Path { return $true } -ModuleName 'LintingHelpers' -ParameterFilter { $PathType -eq 'Leaf' }
151 }
152
153 It 'Filters out empty and whitespace entries' {
154 $result = Get-ChangedFilesFromGit -FileExtensions @('*.ps1')
155 $result | Should -Contain 'valid.ps1'
156 $result | Should -Contain 'another.ps1'
157 $result | Should -Not -Contain ''
158 $result | Should -Not -Contain ' '
159 }
160 }
161
162 Context 'Both merge-base and HEAD~1 fail, third fallback' {
163 BeforeEach {
164 # Merge-base fails
165 Mock git {
166 $global:LASTEXITCODE = 128
167 return $null
168 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'merge-base' }
169
170 # rev-parse fails for HEAD~1 check
171 Mock git {
172 $global:LASTEXITCODE = 128
173 return $null
174 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'rev-parse' }
175
176 # diff returns files from third fallback (git diff --name-only HEAD)
177 Mock git {
178 $global:LASTEXITCODE = 0
179 return @('unstaged-file.ps1')
180 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'diff' }
181
182 Mock Test-Path { return $true } -ModuleName 'LintingHelpers' -ParameterFilter { $PathType -eq 'Leaf' }
183 }
184
185 It 'Falls back to git diff --name-only HEAD and returns files' {
186 $result = Get-ChangedFilesFromGit -FileExtensions @('*.ps1')
187 $result | Should -Contain 'unstaged-file.ps1'
188 }
189 }
190
191 Context 'Git diff command fails' {
192 BeforeEach {
193 Mock git {
194 $global:LASTEXITCODE = 0
195 return 'abc123def456789'
196 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'merge-base' }
197
198 # Diff fails with non-zero exit code
199 Mock git {
200 $global:LASTEXITCODE = 1
201 return $null
202 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'diff' }
203 }
204
205 It 'Returns empty array when git diff fails' {
206 $result = Get-ChangedFilesFromGit
207 $result | Should -BeNullOrEmpty
208 }
209 }
210
211 Context 'Exception during execution' {
212 BeforeEach {
213 Mock git {
214 throw "Simulated git failure"
215 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'merge-base' }
216 }
217
218 It 'Catches exceptions and returns empty array' {
219 $result = Get-ChangedFilesFromGit
220 $result | Should -BeNullOrEmpty
221 }
222 }
223}
224
225#endregion
226
227#region Get-FilesRecursive Tests
228
229Describe 'Get-FilesRecursive' {
230 Context 'Basic file enumeration' {
231 BeforeEach {
232 New-Item -Path 'TestDrive:/scripts' -ItemType Directory -Force | Out-Null
233 New-Item -Path 'TestDrive:/scripts/test.ps1' -ItemType File -Force | Out-Null
234 New-Item -Path 'TestDrive:/scripts/readme.md' -ItemType File -Force | Out-Null
235 New-Item -Path 'TestDrive:/scripts/sub' -ItemType Directory -Force | Out-Null
236 New-Item -Path 'TestDrive:/scripts/sub/nested.ps1' -ItemType File -Force | Out-Null
237 }
238
239 It 'Finds files matching Include pattern' {
240 $result = Get-FilesRecursive -Path 'TestDrive:/scripts' -Include @('*.ps1')
241 $result.Count | Should -Be 2
242 $result.Name | Should -Contain 'test.ps1'
243 $result.Name | Should -Contain 'nested.ps1'
244 }
245
246 It 'Finds files with multiple Include patterns' {
247 $result = Get-FilesRecursive -Path 'TestDrive:/scripts' -Include @('*.ps1', '*.md')
248 $result.Count | Should -Be 3
249 }
250
251 It 'Does not include directories in results' {
252 $result = Get-FilesRecursive -Path 'TestDrive:/scripts' -Include @('*')
253 $result | ForEach-Object { $_.PSIsContainer | Should -BeFalse }
254 }
255 }
256
257 Context 'Gitignore filtering' {
258 BeforeEach {
259 New-Item -Path 'TestDrive:/project' -ItemType Directory -Force | Out-Null
260 New-Item -Path 'TestDrive:/project/src' -ItemType Directory -Force | Out-Null
261 New-Item -Path 'TestDrive:/project/src/app.ps1' -ItemType File -Force | Out-Null
262 New-Item -Path 'TestDrive:/project/node_modules' -ItemType Directory -Force | Out-Null
263 New-Item -Path 'TestDrive:/project/node_modules/pkg.ps1' -ItemType File -Force | Out-Null
264 'node_modules/' | Set-Content 'TestDrive:/project/.gitignore'
265 }
266
267 It 'Excludes files matching gitignore patterns' {
268 $result = Get-FilesRecursive -Path 'TestDrive:/project' `
269 -Include @('*.ps1') `
270 -GitIgnorePath 'TestDrive:/project/.gitignore'
271 $result.Name | Should -Contain 'app.ps1'
272 $result.Name | Should -Not -Contain 'pkg.ps1'
273 }
274
275 It 'Returns all files when gitignore path not provided' {
276 $result = Get-FilesRecursive -Path 'TestDrive:/project' -Include @('*.ps1')
277 $result.Count | Should -Be 2
278 }
279 }
280
281 Context 'Invalid paths' {
282 It 'Returns empty for non-existent path' {
283 $result = Get-FilesRecursive -Path 'TestDrive:/nonexistent' -Include @('*.ps1')
284 $result | Should -BeNullOrEmpty
285 }
286 }
287
288 Context 'No gitignore file' {
289 BeforeEach {
290 New-Item -Path 'TestDrive:/simple' -ItemType Directory -Force | Out-Null
291 New-Item -Path 'TestDrive:/simple/file.ps1' -ItemType File -Force | Out-Null
292 }
293
294 It 'Returns files when gitignore does not exist' {
295 $result = Get-FilesRecursive -Path 'TestDrive:/simple' `
296 -Include @('*.ps1') `
297 -GitIgnorePath 'TestDrive:/simple/.gitignore'
298 $result.Count | Should -Be 1
299 }
300 }
301
302 Context 'Git ls-files code path at repo root' {
303 BeforeEach {
304 Mock git {
305 $global:LASTEXITCODE = 0
306 return '/mock/repo'
307 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'rev-parse' }
308
309 Mock git {
310 $global:LASTEXITCODE = 0
311 return @('src/app.ps1', 'src/helper.psm1', 'tests/run.ps1')
312 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'ls-files' }
313
314 Mock Resolve-Path {
315 [PSCustomObject]@{ Path = '/mock/repo' }
316 } -ModuleName 'LintingHelpers'
317
318 Mock Test-Path { $true } -ModuleName 'LintingHelpers' -ParameterFilter {
319 $LiteralPath -or ($Path -and $PathType -eq 'Leaf')
320 }
321
322 Mock Get-Item {
323 [PSCustomObject]@{
324 FullName = $LiteralPath
325 Name = [System.IO.Path]::GetFileName($LiteralPath)
326 PSIsContainer = $false
327 }
328 } -ModuleName 'LintingHelpers'
329 }
330
331 It 'Calls git ls-files when path is inside the repository' {
332 Get-FilesRecursive -Path '.' -Include @('*.ps1')
333 Should -Invoke git -ModuleName 'LintingHelpers' -ParameterFilter {
334 $args[0] -eq 'ls-files'
335 }
336 }
337
338 It 'Returns FileInfo objects from git ls-files output' {
339 $result = Get-FilesRecursive -Path '.' -Include @('*.ps1')
340 $result | Should -Not -BeNullOrEmpty
341 $result | ForEach-Object { $_.PSIsContainer | Should -BeFalse }
342 }
343
344 It 'Passes Include patterns as pathspecs at repo root' {
345 Get-FilesRecursive -Path '.' -Include @('*.ps1', '*.psm1')
346 Should -Invoke git -ModuleName 'LintingHelpers' -ParameterFilter {
347 $args -contains '*.ps1' -and $args -contains '*.psm1'
348 }
349 }
350
351 It 'Accepts GitIgnorePath without error on git path' {
352 { Get-FilesRecursive -Path '.' -Include @('*.ps1') -GitIgnorePath '/nonexistent/.gitignore' } |
353 Should -Not -Throw
354 }
355 }
356
357 Context 'Git ls-files subdirectory scoping' {
358 BeforeEach {
359 Mock git {
360 $global:LASTEXITCODE = 0
361 return '/mock/repo'
362 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'rev-parse' }
363
364 Mock git {
365 $global:LASTEXITCODE = 0
366 return @('src/app.ps1', 'src/helper.psm1')
367 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'ls-files' }
368
369 Mock Resolve-Path {
370 [PSCustomObject]@{ Path = '/mock/repo/src' }
371 } -ModuleName 'LintingHelpers'
372
373 Mock Test-Path { $true } -ModuleName 'LintingHelpers' -ParameterFilter {
374 $LiteralPath -or ($Path -and $PathType -eq 'Leaf')
375 }
376
377 Mock Get-Item {
378 [PSCustomObject]@{
379 FullName = $LiteralPath
380 Name = [System.IO.Path]::GetFileName($LiteralPath)
381 PSIsContainer = $false
382 }
383 } -ModuleName 'LintingHelpers'
384 }
385
386 It 'Scopes git ls-files to the specified subdirectory' {
387 Get-FilesRecursive -Path './src' -Include @('*.ps1')
388 Should -Invoke git -ModuleName 'LintingHelpers' -ParameterFilter {
389 $args -contains '--' -and $args -contains 'src/'
390 }
391 }
392
393 It 'Filters subdirectory results by Include patterns' {
394 $result = Get-FilesRecursive -Path './src' -Include @('*.ps1')
395 $result.Name | Should -Contain 'app.ps1'
396 $result.Name | Should -Not -Contain 'helper.psm1'
397 }
398 }
399
400 Context 'Git unavailable' {
401 BeforeEach {
402 Mock git {
403 $global:LASTEXITCODE = 128
404 return $null
405 } -ModuleName 'LintingHelpers' -ParameterFilter { $args[0] -eq 'rev-parse' }
406
407 New-Item -Path 'TestDrive:/nogit' -ItemType Directory -Force | Out-Null
408 New-Item -Path 'TestDrive:/nogit/script.ps1' -ItemType File -Force | Out-Null
409 }
410
411 It 'Falls back to Get-ChildItem when git is unavailable' {
412 $result = Get-FilesRecursive -Path 'TestDrive:/nogit' -Include @('*.ps1')
413 $result.Count | Should -Be 1
414 $result.Name | Should -Contain 'script.ps1'
415 }
416 }
417}
418
419#endregion
420
421#region Get-GitIgnorePatterns Tests
422
423Describe 'Get-GitIgnorePatterns' {
424 Context 'Non-existent file' {
425 It 'Returns empty for non-existent file' {
426 $result = Get-GitIgnorePatterns -GitIgnorePath 'TestDrive:/nonexistent/.gitignore'
427 $result | Should -BeNullOrEmpty
428 }
429 }
430
431 Context 'Empty file' {
432 BeforeEach {
433 New-Item -Path 'TestDrive:/.gitignore-empty' -ItemType File -Force | Out-Null
434 }
435
436 It 'Returns empty for empty file' {
437 $result = Get-GitIgnorePatterns -GitIgnorePath 'TestDrive:/.gitignore-empty'
438 $result | Should -BeNullOrEmpty
439 }
440 }
441
442 Context 'Pattern parsing' {
443 It 'Skips comments and empty lines' {
444 @('# Comment', '', 'node_modules/', ' ', '*.log') | Set-Content 'TestDrive:/.gitignore'
445 $result = Get-GitIgnorePatterns -GitIgnorePath 'TestDrive:/.gitignore'
446 $result.Count | Should -Be 2
447 }
448
449 It 'Converts directory patterns correctly' {
450 $gitignorePath = Join-Path $TestDrive '.gitignore-dir'
451 'node_modules/' | Set-Content $gitignorePath
452 $result = @(Get-GitIgnorePatterns -GitIgnorePath $gitignorePath)
453 $sep = [System.IO.Path]::DirectorySeparatorChar
454 # Function wraps directory patterns with platform separator
455 $result[0] | Should -Be "*${sep}node_modules${sep}*"
456 }
457
458 It 'Converts file patterns with paths correctly' {
459 $gitignorePath = Join-Path $TestDrive '.gitignore-path'
460 'build/output.log' | Set-Content $gitignorePath
461 $result = @(Get-GitIgnorePatterns -GitIgnorePath $gitignorePath)
462 $sep = [System.IO.Path]::DirectorySeparatorChar
463 # Function normalizes paths and wraps with wildcards
464 $result[0] | Should -Be "*${sep}build${sep}output.log*"
465 }
466
467 It 'Handles simple file patterns' {
468 $gitignorePath = Join-Path $TestDrive '.gitignore-simple'
469 '*.log' | Set-Content $gitignorePath
470 $result = @(Get-GitIgnorePatterns -GitIgnorePath $gitignorePath)
471 $sep = [System.IO.Path]::DirectorySeparatorChar
472 # Function wraps simple patterns with wildcards
473 $result[0] | Should -Be "*${sep}*.log${sep}*"
474 }
475
476 It 'Processes multiple patterns' {
477 @('node_modules/', 'dist/', '*.tmp', 'logs/debug.log') | Set-Content 'TestDrive:/.gitignore-multi'
478 $result = Get-GitIgnorePatterns -GitIgnorePath 'TestDrive:/.gitignore-multi'
479 $result.Count | Should -Be 4
480 }
481 }
482}
483
484#endregion
485