diff --git a/build.data.psd1 b/build.data.psd1 index 25cd55930..9e0b958e1 100644 --- a/build.data.psd1 +++ b/build.data.psd1 @@ -1,6 +1,6 @@ @{ PackageFiles = @{ - Linux = @( + Linux = @( 'bicep.dsc.extension.json', 'dsc', 'dsc_default.settings.json', @@ -16,6 +16,8 @@ 'osinfo', 'osinfo.dsc.resource.json', 'powershell.dsc.resource.json', + 'powershell.discover.ps1', + 'powershell.dsc.extension.json', 'psDscAdapter/', 'psscript.ps1', 'psscript.dsc.resource.json', @@ -24,7 +26,7 @@ 'sshdconfig', 'sshd_config.dsc.resource.json' ) - MacOS = @( + MacOS = @( 'bicep.dsc.extension.json', 'dsc', 'dsc_default.settings.json', @@ -40,6 +42,8 @@ 'osinfo', 'osinfo.dsc.resource.json', 'powershell.dsc.resource.json', + 'powershell.discover.ps1', + 'powershell.dsc.extension.json', 'psDscAdapter/', 'psscript.ps1', 'psscript.dsc.resource.json', @@ -48,7 +52,7 @@ 'sshdconfig', 'sshd_config.dsc.resource.json' ) - Windows = @( + Windows = @( 'appx.dsc.extension.json', 'appx-discover.ps1', 'bicep.dsc.extension.json', @@ -64,6 +68,8 @@ 'osinfo.exe', 'osinfo.dsc.resource.json', 'powershell.dsc.resource.json', + 'powershell.discover.ps1', + 'powershell.dsc.extension.json', 'psDscAdapter/', 'psscript.ps1', 'psscript.dsc.resource.json', @@ -90,23 +96,23 @@ 'brew.dsc.resource.sh' ) } - Projects = @( + Projects = @( @{ - Name = 'root' + Name = 'root' RelativePath = '.' - Kind = 'Root' - CopyFiles = @{ + Kind = 'Root' + CopyFiles = @{ All = @( 'NOTICE.txt' ) } } @{ - Name = 'resources/brew' - RelativePath = 'resources/brew' - Kind = 'Resource' + Name = 'resources/brew' + RelativePath = 'resources/brew' + Kind = 'Resource' SupportedPlatformOS = 'MacOS' - CopyFiles = @{ + CopyFiles = @{ macOS = @( 'brew.dsc.resource.sh' 'brew.dsc.resource.json' @@ -114,11 +120,11 @@ } } @{ - Name = 'resources/apt' - RelativePath = 'resources/apt' - Kind = 'Resource' + Name = 'resources/apt' + RelativePath = 'resources/apt' + Kind = 'Resource' SupportedPlatformOS = 'Linux' - CopyFiles = @{ + CopyFiles = @{ Linux = @( 'apt.dsc.resource.sh' 'apt.dsc.resource.json' @@ -126,38 +132,38 @@ } } @{ - Name = 'dsc-lib-pal' - RelativePath = 'lib/dsc-lib-pal' - Kind = 'Library' - IsRust = $true + Name = 'dsc-lib-pal' + RelativePath = 'lib/dsc-lib-pal' + Kind = 'Library' + IsRust = $true SupportedPlatformOS = 'Windows' } @{ - Name = 'dsc-lib-registry' - RelativePath = 'lib/dsc-lib-registry' - Kind = 'Library' - IsRust = $true + Name = 'dsc-lib-registry' + RelativePath = 'lib/dsc-lib-registry' + Kind = 'Library' + IsRust = $true SupportedPlatformOS = 'Windows' } @{ - Name = 'registry' - RelativePath = 'resources/registry' - Kind = 'Resource' - IsRust = $true + Name = 'registry' + RelativePath = 'resources/registry' + Kind = 'Resource' + IsRust = $true SupportedPlatformOS = 'Windows' - Binaries = @('registry') - CopyFiles = @{ + Binaries = @('registry') + CopyFiles = @{ Windows = @( 'registry.dsc.resource.json' ) } } @{ - Name = 'reboot_pending' - RelativePath = 'resources/reboot_pending' - Kind = 'Resource' + Name = 'reboot_pending' + RelativePath = 'resources/reboot_pending' + Kind = 'Resource' SupportedPlatformOS = 'Windows' - CopyFiles = @{ + CopyFiles = @{ Windows = @( 'reboot_pending.resource.ps1' 'reboot_pending.dsc.resource.json' @@ -165,11 +171,11 @@ } } @{ - Name = 'wmi' - RelativePath = 'adapters/wmi' - Kind = 'Adapter' + Name = 'wmi' + RelativePath = 'adapters/wmi' + Kind = 'Adapter' SupportedPlatformOS = 'Windows' - CopyFIles = @{ + CopyFIles = @{ WIndows = @( 'wmi.resource.ps1' 'wmiAdapter.psd1' @@ -179,11 +185,11 @@ } } @{ - Name = 'configurations/windows' - RelativePath = 'configurations/windows' - Kind = 'Configuration' + Name = 'configurations/windows' + RelativePath = 'configurations/windows' + Kind = 'Configuration' SupportedPlatformOS = 'Windows' - CopyFiles = @{ + CopyFiles = @{ Windows = @( 'windows_baseline.dsc.yaml' 'windows_inventory.dsc.yaml' @@ -191,11 +197,11 @@ } } @{ - Name = 'extensions/appx' - RelativePath = 'extensions/appx' - Kind = 'Extension' + Name = 'extensions/appx' + RelativePath = 'extensions/appx' + Kind = 'Extension' SupportedPlatformOS = 'Windows' - CopyFiles = @{ + CopyFiles = @{ Windows = @( 'appx-discover.ps1' 'appx.dsc.extension.json' @@ -203,60 +209,71 @@ } } @{ - Name = 'tree-sitter-dscexpression' - RelativePath = 'grammars/tree-sitter-dscexpression' - Kind = 'Grammar' - IsRust = $true + Name = 'extensions/powershell' + RelativePath = 'extensions/powershell' + Kind = 'Extension' + CopyFiles = @{ + All = @( + 'powershell.dsc.extension.json' + 'powershell.discover.ps1' + ) + } + } + @{ + Name = 'tree-sitter-dscexpression' + RelativePath = 'grammars/tree-sitter-dscexpression' + Kind = 'Grammar' + IsRust = $true ClippyUnclean = $true - SkipTest = @{ + SkipTest = @{ Windows = $true } } - @{ - Name = 'tree-sitter-ssh-server-config' - RelativePath = 'grammars/tree-sitter-ssh-server-config' - Kind = 'Grammar' - IsRust = $true + @{ + Name = 'tree-sitter-ssh-server-config' + RelativePath = 'grammars/tree-sitter-ssh-server-config' + Kind = 'Grammar' + IsRust = $true ClippyUnclean = $true # SKipTestProject = $IsWindows } @{ - Name = 'dsc-lib-security_context' + Name = 'dsc-lib-security_context' RelativePath = 'lib/dsc-lib-security_context' - Kind = 'Library' - IsRust = $true + Kind = 'Library' + IsRust = $true } @{ - Name = 'dsc-lib-osinfo' + Name = 'dsc-lib-osinfo' RelativePath = 'lib/dsc-lib-osinfo' - Kind = 'Library' - IsRust = $true + Kind = 'Library' + IsRust = $true } @{ - Name = 'osinfo' + Name = 'osinfo' RelativePath = 'resources/osinfo' - Kind = 'Resource' - IsRust = $true - Binaries = @('osinfo') - CopyFiles = @{ + Kind = 'Resource' + IsRust = $true + Binaries = @('osinfo') + CopyFiles = @{ All = @( 'osinfo.dsc.resource.json' ) } } @{ - Name = 'dsc-lib' + Name = 'dsc-lib' RelativePath = 'lib/dsc-lib' - Kind = 'Library' - IsRust = $true + Kind = 'Library' + IsRust = $true } @{ - Name = 'dsc' + Name = 'dsc' RelativePath = 'dsc' - Kind = 'CLI' - IsRust = $true - Binaries = @('dsc') - CopyFiles = @{ + Kind = 'CLI' + IsRust = $true + Binaries = @('dsc') + CopyFiles = @{ All = @( 'dsc.settings.json' 'dsc_default.settings.json' @@ -267,36 +284,36 @@ } } @{ - Name = 'dscecho' + Name = 'dscecho' RelativePath = 'resources/dscecho' - Kind = 'Resource' - IsRust = $true - Binaries = @('dscecho') - CopyFiles = @{ + Kind = 'Resource' + IsRust = $true + Binaries = @('dscecho') + CopyFiles = @{ All = @( 'echo.dsc.resource.json' ) } } @{ - Name = 'bicep' + Name = 'bicep' RelativePath = 'extensions/bicep' - Kind = 'Extension' - CopyFIles = @{ + Kind = 'Extension' + CopyFiles = @{ All = 'bicep.dsc.extension.json' } } @{ - Name = 'powershell-adapter' + Name = 'powershell-adapter' RelativePath = 'adapters/powershell' - Kind = 'Adapter' - CopyFiles = @{ - All = @( + Kind = 'Adapter' + CopyFiles = @{ + All = @( 'psDscAdapter/powershell.resource.ps1' 'psDscAdapter/psDscAdapter.psd1' 'psDscAdapter/psDscAdapter.psm1' 'powershell.dsc.resource.json' - ) + ) Windows = @( 'psDscAdapter/win_psDscAdapter.psd1' 'psDscAdapter/win_psDscAdapter.psm1' @@ -305,11 +322,11 @@ } } @{ - Name = 'PSScript' + Name = 'PSScript' RelativePath = 'resources/PSScript' - Kind = 'Resource' - CopyFiles = @{ - All = @( + Kind = 'Resource' + CopyFiles = @{ + All = @( 'psscript.ps1' 'psscript.dsc.resource.json' ) @@ -319,37 +336,37 @@ } } @{ - Name = 'process' + Name = 'process' RelativePath = 'resources/process' - Kind = 'Resource' - IsRust = $true - Binaries = @('process') - CopyFiles = @{ + Kind = 'Resource' + IsRust = $true + Binaries = @('process') + CopyFiles = @{ All = @( 'process.dsc.resource.json' ) } } @{ - Name = 'runcommandonset' + Name = 'runcommandonset' RelativePath = 'resources/runcommandonset' - Kind = 'Resource' - IsRust = $true - Binaries = @('runcommandonset') - CopyFiles = @{ + Kind = 'Resource' + IsRust = $true + Binaries = @('runcommandonset') + CopyFiles = @{ All = @( 'RunCommandOnSet.dsc.resource.json' ) } } @{ - Name = 'sshdconfig' + Name = 'sshdconfig' RelativePath = 'resources/sshdconfig' - Kind = 'Resource' - IsRust = $true - Binaries = @('sshdconfig') - CopyFiles = @{ - All = @( + Kind = 'Resource' + IsRust = $true + Binaries = @('sshdconfig') + CopyFiles = @{ + All = @( 'sshd_config.dsc.resource.json' ) Windows = @( @@ -358,13 +375,13 @@ } } @{ - Name = 'dsctest' + Name = 'dsctest' RelativePath = 'tools/dsctest' - Kind = 'Resource' - IsRust = $true - TestOnly = $true - Binaries = @('dsctest') - CopyFiles = @{ + Kind = 'Resource' + IsRust = $true + TestOnly = $true + Binaries = @('dsctest') + CopyFiles = @{ All = @( 'dscdelete.dsc.resource.json' 'dscexist.dsc.resource.json' @@ -389,24 +406,24 @@ } } @{ - Name = 'test_group_resource' + Name = 'test_group_resource' RelativePath = 'tools/test_group_resource' - Kind = 'Resource' - IsRust = $true - TestOnly = $true - Binaries = @('test_group_resource') - CopyFIles = @{ + Kind = 'Resource' + IsRust = $true + TestOnly = $true + Binaries = @('test_group_resource') + CopyFIles = @{ All = @( 'testGroup.dsc.resource.json' ) } } @{ - Name = 'y2j' + Name = 'y2j' RelativePath = 'y2j' - Kind = 'CLI' - IsRust = $true - Binaries = @('y2j') + Kind = 'CLI' + IsRust = $true + Binaries = @('y2j') } ) } \ No newline at end of file diff --git a/build.ps1 b/build.ps1 index ebe922238..a83db83c4 100755 --- a/build.ps1 +++ b/build.ps1 @@ -63,6 +63,8 @@ $filesForWindowsPackage = @( 'NOTICE.txt', 'osinfo.exe', 'osinfo.dsc.resource.json', + 'powershell.discover.ps1', + 'powershell.dsc.extension.json', 'powershell.dsc.resource.json', 'psDscAdapter/', 'psscript.ps1', @@ -101,6 +103,8 @@ $filesForLinuxPackage = @( 'NOTICE.txt', 'osinfo', 'osinfo.dsc.resource.json', + 'powershell.discover.ps1', + 'powershell.dsc.extension.json', 'powershell.dsc.resource.json', 'psDscAdapter/', 'psscript.ps1', @@ -126,6 +130,8 @@ $filesForMacPackage = @( 'NOTICE.txt', 'osinfo', 'osinfo.dsc.resource.json', + 'powershell.discover.ps1', + 'powershell.dsc.extension.json', 'powershell.dsc.resource.json', 'psDscAdapter/', 'psscript.ps1', @@ -347,9 +353,9 @@ if (!$SkipBuild) { New-Item -ItemType Directory $target -ErrorAction Ignore > $null # make sure dependencies are built first so clippy runs correctly - $windows_projects = @("lib/dsc-lib-pal", "lib/dsc-lib-registry", "resources/registry", "resources/reboot_pending", "adapters/wmi", "configurations/windows", 'extensions/appx') - $macOS_projects = @("resources/brew") - $linux_projects = @("resources/apt") + $windows_projects = @("lib/dsc-lib-pal", "lib/dsc-lib-registry", "resources/registry", "resources/reboot_pending", "adapters/wmi", "configurations/windows", "extensions/appx", "extensions/powershell") + $macOS_projects = @("resources/brew", "extensions/powershell") + $linux_projects = @("resources/apt", "extensions/powershell") # projects are in dependency order $projects = @( diff --git a/docs/reference/schemas/extension/stdout/discover.md b/docs/reference/schemas/extension/stdout/discover.md index fc33b89d6..07744f919 100644 --- a/docs/reference/schemas/extension/stdout/discover.md +++ b/docs/reference/schemas/extension/stdout/discover.md @@ -22,13 +22,16 @@ Type: object ## Description -Represents the actual state of a resource instance in DSCpath to a discovered DSC resource or +Represents the actual state of a resource instance in DSC path to a discovered DSC resource or extension manifest on the system. DSC expects every JSON Line emitted to stdout for the **Discover** operation to adhere to this schema. The output must be a JSON object. The object must define the full path to the discovered manifest. If an extension returns JSON that is invalid against this schema, DSC raises an error. +If the extension doesn't discover any manifests, it must return nothing to stdout. An empty output +indicates no resources were found. + ## Required Properties The output for the `discover` operation must include these properties: @@ -43,6 +46,9 @@ The value for this property must be the absolute path to a manifest file on the manifest can be for a DSC resource or extension. If the returned path doesn't exist, DSC raises an error. +Each discovered manifest must be emitted as a separate JSON Line to stdout. If no manifests are +discovered, the extension must not emit any output to stdout. + ```yaml Type: string Required: true diff --git a/dsc/tests/dsc_extension_discover.tests.ps1 b/dsc/tests/dsc_extension_discover.tests.ps1 index 043713536..51fde8e13 100644 --- a/dsc/tests/dsc_extension_discover.tests.ps1 +++ b/dsc/tests/dsc_extension_discover.tests.ps1 @@ -23,30 +23,29 @@ Describe 'Discover extension tests' { It 'Discover extensions' { $out = dsc extension list | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 - if ($IsWindows) { - $out.Count | Should -Be 3 -Because ($out | Out-String) - $out[0].type | Should -Be 'Microsoft.DSC.Extension/Bicep' - $out[0].version | Should -Be '0.1.0' - $out[0].capabilities | Should -BeExactly @('import') - $out[0].manifest | Should -Not -BeNullOrEmpty - $out[1].type | Should -Be 'Microsoft.Windows.Appx/Discover' - $out[1].version | Should -Be '0.1.0' - $out[1].capabilities | Should -BeExactly @('discover') - $out[1].manifest | Should -Not -BeNullOrEmpty - $out[2].type | Should -BeExactly 'Test/Discover' - $out[2].version | Should -BeExactly '0.1.0' - $out[2].capabilities | Should -BeExactly @('discover') - $out[2].manifest | Should -Not -BeNullOrEmpty + $expectedExtensions = if ($IsWindows) { + @( + @{ type = 'Microsoft.DSC.Extension/Bicep'; version = '0.1.0'; capabilities = @('import') } + @{ type = 'Microsoft.Windows.Appx/Discover'; version = '0.1.0'; capabilities = @('discover') } + @{ type = 'Microsoft.PowerShell/Discover'; version = '0.1.0'; capabilities = @('discover') } + @{ type = 'Test/Discover'; version = '0.1.0'; capabilities = @('discover') } + ) } else { - $out.Count | Should -Be 2 -Because ($out | Out-String) - $out[0].type | Should -Be 'Microsoft.DSC.Extension/Bicep' - $out[0].version | Should -Be '0.1.0' - $out[0].capabilities | Should -BeExactly @('import') - $out[0].manifest | Should -Not -BeNullOrEmpty - $out[1].type | Should -BeExactly 'Test/Discover' - $out[1].version | Should -BeExactly '0.1.0' - $out[1].capabilities | Should -BeExactly @('discover') - $out[1].manifest | Should -Not -BeNullOrEmpty + @( + @{ type = 'Microsoft.DSC.Extension/Bicep'; version = '0.1.0'; capabilities = @('import') } + @{ type = 'Microsoft.PowerShell/Discover'; version = '0.1.0'; capabilities = @('discover') } + @{ type = 'Test/Discover'; version = '0.1.0'; capabilities = @('discover') } + ) + } + + $out.Count | Should -Be $expectedExtensions.Count -Because ($out | Out-String) + + foreach ($expected in $expectedExtensions) { + $extension = $out | Where-Object { $_.type -eq $expected.type } + $extension | Should -Not -BeNullOrEmpty -Because "Extension $($expected.type) should exist" + $extension.version | Should -BeExactly $expected.version + $extension.capabilities | Should -BeExactly $expected.capabilities + $extension.manifest | Should -Not -BeNullOrEmpty } } diff --git a/extensions/powershell/copy_files.txt b/extensions/powershell/copy_files.txt new file mode 100644 index 000000000..e3b12dc13 --- /dev/null +++ b/extensions/powershell/copy_files.txt @@ -0,0 +1,2 @@ +powershell.discover.ps1 +powershell.dsc.extension.json diff --git a/extensions/powershell/powershell.discover.ps1 b/extensions/powershell/powershell.discover.ps1 new file mode 100644 index 000000000..e94678c8e --- /dev/null +++ b/extensions/powershell/powershell.discover.ps1 @@ -0,0 +1,113 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +[CmdletBinding()] +param () + +function Get-CacheFilePath { + if ($IsWindows) { + Join-Path $env:LocalAppData "dsc\PowerShellDiscoverCache.json" + } else { + Join-Path $env:HOME ".dsc" "PowerShellDiscoverCache.json" + } +} + +function Test-CacheValid { + param([string]$CacheFilePath, [string[]]$PSPaths) + + if (-not (Test-Path $CacheFilePath)) { + return $false + } + + try { + $cache = Get-Content -Raw $CacheFilePath | ConvertFrom-Json + + foreach ($entry in $cache.PathInfo.PSObject.Properties) { + $path = $entry.Name + if (-not (Test-Path $path)) { + return $false + } + + $currentLastWrite = (Get-Item $path).LastWriteTimeUtc + $cachedLastWrite = [DateTime]$entry.Value + + if ($currentLastWrite -ne $cachedLastWrite) { + return $false + } + } + + $cachedPaths = [string[]]$cache.PSModulePaths + if ($cachedPaths.Count -ne $PSPaths.Count) { + return $false + } + + $diff = Compare-Object $cachedPaths $PSPaths + if ($null -ne $diff) { + return $false + } + + return $true + } catch { + return $false + } +} + +function Invoke-DscResourceDiscovery { + [CmdletBinding()] + param() + + begin { + $psPaths = $env:PSModulePath -split [System.IO.Path]::PathSeparator | Where-Object { $_ -notmatch 'WindowsPowerShell' } + + $cacheFilePath = Get-CacheFilePath + $useCache = Test-CacheValid -CacheFilePath $cacheFilePath -PSPaths $psPaths + } + process { + if ($useCache) { + $cache = Get-Content -Raw $cacheFilePath | ConvertFrom-Json + $manifests = $cache.Manifests + } else { + $manifests = $psPaths | ForEach-Object -Parallel { + $searchPatterns = @('*.dsc.resource.json', '*.dsc.resource.yaml', '*.dsc.resource.yml') + $enumOptions = [System.IO.EnumerationOptions]@{ IgnoreInaccessible = $false; RecurseSubdirectories = $true } + foreach ($pattern in $searchPatterns) { + try { + [System.IO.Directory]::EnumerateFiles($_, $pattern, $enumOptions) | ForEach-Object { + @{ manifestPath = $_ } + } + } catch { } + } + } -ThrottleLimit 10 + + $pathInfo = @{} + foreach ($path in $psPaths) { + if (Test-Path $path) { + $pathInfo[$path] = (Get-Item $path).LastWriteTimeUtc + } + } + + $cacheObject = @{ + PSModulePaths = $psPaths + PathInfo = $pathInfo + Manifests = $manifests + } + + $cacheDir = Split-Path $cacheFilePath -Parent + if (-not (Test-Path $cacheDir)) { + New-Item -ItemType Directory -Path $cacheDir -Force | Out-Null + } + + $cacheObject | ConvertTo-Json -Depth 10 | Set-Content -Path $cacheFilePath -Force + } + } + end { + if ($null -eq $manifests -or [string]::IsNullOrEmpty($manifests)) { + # Return nothing + } else { + $manifests | ForEach-Object { $_ | ConvertTo-Json -Compress } + } + } +} + +Invoke-DscResourceDiscovery + diff --git a/extensions/powershell/powershell.discover.tests.ps1 b/extensions/powershell/powershell.discover.tests.ps1 new file mode 100644 index 000000000..66203e8fb --- /dev/null +++ b/extensions/powershell/powershell.discover.tests.ps1 @@ -0,0 +1,125 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +BeforeAll { + $fakeManifest = @{ + '$schema' = "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json" + type = "Test/FakeResource" + version = "0.1.0" + get = @{ + executable = "fakeResource" + args = @( + "get", + @{ + jsonInputArg = "--input" + mandatory = $true + } + ) + } + } + + $manifestPath = Join-Path $TestDrive "fake.dsc.resource.json" + $fakeManifest | ConvertTo-Json -Depth 10 | Set-Content -Path $manifestPath + $env:PSModulePath += [System.IO.Path]::PathSeparator + $TestDrive + + $script:discoverScript = Join-Path $PSScriptRoot "powershell.discover.ps1" + + $cacheFilePath = if ($IsWindows) { + Join-Path $env:LocalAppData "dsc\PowerShellDiscoverCache.json" + } else { + Join-Path $env:HOME ".dsc" "PowerShellDiscoverCache.json" + } + $script:cacheFilePath = $cacheFilePath + + Remove-Item -Force -ErrorAction SilentlyContinue -Path $script:cacheFilePath +} + +Describe 'Tests for PowerShell resource discovery' { + It 'Should create cache file on first run' { + $script:cacheFilePath | Should -Not -Exist + $null = & $script:discoverScript 2>&1 + $script:cacheFilePath | Should -Exist + + $cache = Get-Content -Raw $script:cacheFilePath | ConvertFrom-Json + $cache.PSModulePaths | Should -Not -BeNullOrEmpty + $cache.PathInfo | Should -Not -BeNullOrEmpty + $cache.Manifests | Should -Not -BeNullOrEmpty + } + + It 'Should find DSC PowerShell resources' { + $out = dsc resource list | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $out.manifest.type | Should -Contain 'Test/FakeResource' + } + + It 'Should use cache on subsequent runs' { + $null = & $script:discoverScript 2>&1 + $script:cacheFilePath | Should -Exist + + $cacheLastWriteTime = (Get-Item $script:cacheFilePath).LastWriteTimeUtc + + Start-Sleep -Milliseconds 100 + + $null = & $script:discoverScript 2>&1 + + $newLastWriteTime = (Get-Item $script:cacheFilePath).LastWriteTimeUtc + $newLastWriteTime | Should -Be $cacheLastWriteTime + } + + It 'Should invalidate cache when PSModulePath changes' { + $null = & $script:discoverScript 2>&1 + $script:cacheFilePath | Should -Exist + + $cache = Get-Content -Raw $script:cacheFilePath | ConvertFrom-Json + $originalPaths = $cache.PSModulePaths + $cache.PSModulePaths = @($originalPaths[0]) # Remove some paths + $cache | ConvertTo-Json -Depth 10 | Set-Content -Path $script:cacheFilePath -Force + + $cacheLastWriteTime = (Get-Item $script:cacheFilePath).LastWriteTimeUtc + Start-Sleep -Milliseconds 100 + + $null = & $script:discoverScript 2>&1 + + $newLastWriteTime = (Get-Item $script:cacheFilePath).LastWriteTimeUtc + $newLastWriteTime | Should -Not -Be $cacheLastWriteTime + } + + It 'Should invalidate cache when module directory is modified' { + $null = & $script:discoverScript 2>&1 + $script:cacheFilePath | Should -Exist + + $cache = Get-Content -Raw $script:cacheFilePath | ConvertFrom-Json + + $firstPath = $cache.PathInfo.PSObject.Properties | Select-Object -First 1 + if ($firstPath) { + $oldTimestamp = [DateTime]$firstPath.Value + $newTimestamp = $oldTimestamp.AddDays(-1) + $cache.PathInfo.($firstPath.Name) = $newTimestamp + $cache | ConvertTo-Json -Depth 10 | Set-Content -Path $script:cacheFilePath -Force + + $cacheLastWriteTime = (Get-Item $script:cacheFilePath).LastWriteTimeUtc + Start-Sleep -Milliseconds 100 + + $null = & $script:discoverScript 2>&1 + + $newLastWriteTime = (Get-Item $script:cacheFilePath).LastWriteTimeUtc + $newLastWriteTime | Should -Not -Be $cacheLastWriteTime + } + } + + It 'Should rebuild cache if cache file is corrupted' { + "{ invalid json }" | Set-Content -Path $script:cacheFilePath -Force + $script:cacheFilePath | Should -Exist + + $null = & $script:discoverScript 2>&1 + + $cache = Get-Content -Raw $script:cacheFilePath | ConvertFrom-Json + $cache.PSModulePaths | Should -Not -BeNullOrEmpty + $cache.PathInfo | Should -Not -BeNullOrEmpty + } + + It 'Should include test manifest in discovery results' { + $out = & $script:discoverScript | ConvertFrom-Json + $out.manifestPath | Should -Contain $manifestPath + } +} diff --git a/extensions/powershell/powershell.dsc.extension.json b/extensions/powershell/powershell.dsc.extension.json new file mode 100644 index 000000000..9096e4fa1 --- /dev/null +++ b/extensions/powershell/powershell.dsc.extension.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "type": "Microsoft.PowerShell/Discover", + "version": "0.1.0", + "description": "Discovers DSC resources packaged in PowerShell 7 modules.", + "discover": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-ExecutionPolicy", + "Bypass", + "-NoProfile", + "-Command", + "./powershell.discover.ps1" + ] + } +}