diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..e5dec5b4 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "powershell.codeFormatting.preset": "Allman", + "editor.formatOnSave": true +} \ No newline at end of file diff --git a/pipelines/azure-pipelines.yml b/pipelines/azure-pipelines.yml index c668c057..8e619f41 100644 --- a/pipelines/azure-pipelines.yml +++ b/pipelines/azure-pipelines.yml @@ -1,30 +1,30 @@ # winget-dsc pipeline to publish artifacts -name: '$(Build.DefinitionName)-$(Build.DefinitionVersion)-$(Date:yyyyMMdd)-$(Rev:r)' +name: "$(Build.DefinitionName)-$(Build.DefinitionVersion)-$(Date:yyyyMMdd)-$(Rev:r)" # Commit triggers trigger: -- main + - main # PR triggers pr: branches: include: - - main + - main paths: include: - - pipelines/azure-pipelines.yml - - resources/* - - tests/* - + - pipelines/azure-pipelines.yml + - resources/* + - tests/* + resources: repositories: - - repository: self - type: git - ref: refs/heads/main - - repository: 1ESPipelineTemplates - type: git - name: 1ESPipelineTemplates/1ESPipelineTemplates - ref: refs/tags/release + - repository: self + type: git + ref: refs/heads/main + - repository: 1ESPipelineTemplates + type: git + name: 1ESPipelineTemplates/1ESPipelineTemplates + ref: refs/tags/release extends: template: v1/1ES.Official.PipelineTemplate.yml@1ESPipelineTemplates parameters: @@ -33,68 +33,72 @@ extends: image: windows-2022 os: windows customBuildTags: - - ES365AIMigrationTooling + - ES365AIMigrationTooling settings: skipBuildTagsForGitHubPullRequests: true stages: - - stage: WinGet_DSC_Artifacts_Publish - jobs: - - job: Publish_WinGet_DSC_Resources - displayName: "Publish WinGet DSC Resources" - templateContext: - outputs: - - output: pipelineArtifact - displayName: 'Publish Pipeline Microsoft.Windows.Developer' - targetPath: $(Build.SourcesDirectory)\resources\Microsoft.Windows.Developer\ - artifactName: Microsoft.Windows.Developer - - output: pipelineArtifact - displayName: 'Publish Pipeline Microsoft.Windows.Setting.Accessibility' - targetPath: $(Build.SourcesDirectory)\resources\Microsoft.Windows.Setting.Accessibility\ - artifactName: Microsoft.Windows.Setting.Accessibility - - output: pipelineArtifact - displayName: 'Publish Pipeline PythonPip3Dsc' - targetPath: $(Build.SourcesDirectory)\resources\PythonPip3Dsc\ - artifactName: PythonPip3Dsc - - output: pipelineArtifact - displayName: 'Publish Pipeline YarnDsc' - targetPath: $(Build.SourcesDirectory)\resources\YarnDsc\ - artifactName: YarnDsc - - output: pipelineArtifact - displayName: 'Publish Pipeline NpmDsc' - targetPath: $(Build.SourcesDirectory)\resources\NpmDsc\ - artifactName: NpmDsc - - output: pipelineArtifact - displayName: 'Publish Pipeline Microsoft.WindowsSandbox.DSC' - targetPath: $(Build.SourcesDirectory)\resources\Microsoft.WindowsSandbox.DSC\ - artifactName: Microsoft.WindowsSandbox.DSC - - output: pipelineArtifact - displayName: 'Publish Pipeline GitDsc' - targetPath: $(Build.SourcesDirectory)\resources\GitDsc\ - artifactName: GitDsc - - output: pipelineArtifact - displayName: 'Publish Pipeline Microsoft.VSCode.Dsc' - targetPath: $(Build.SourcesDirectory)\resources\Microsoft.VSCode.Dsc\ - artifactName: Microsoft.VSCode.Dsc + - stage: WinGet_DSC_Artifacts_Publish + jobs: + - job: Publish_WinGet_DSC_Resources + displayName: "Publish WinGet DSC Resources" + templateContext: + outputs: + - output: pipelineArtifact + displayName: "Publish Pipeline Microsoft.Windows.Developer" + targetPath: $(Build.SourcesDirectory)\resources\Microsoft.Windows.Developer\ + artifactName: Microsoft.Windows.Developer + - output: pipelineArtifact + displayName: "Publish Pipeline Microsoft.Windows.Setting.Accessibility" + targetPath: $(Build.SourcesDirectory)\resources\Microsoft.Windows.Setting.Accessibility\ + artifactName: Microsoft.Windows.Setting.Accessibility + - output: pipelineArtifact + displayName: "Publish Pipeline PythonPip3Dsc" + targetPath: $(Build.SourcesDirectory)\resources\PythonPip3Dsc\ + artifactName: PythonPip3Dsc + - output: pipelineArtifact + displayName: "Publish Pipeline YarnDsc" + targetPath: $(Build.SourcesDirectory)\resources\YarnDsc\ + artifactName: YarnDsc + - output: pipelineArtifact + displayName: "Publish Pipeline NpmDsc" + targetPath: $(Build.SourcesDirectory)\resources\NpmDsc\ + artifactName: NpmDsc + - output: pipelineArtifact + displayName: "Publish Pipeline Microsoft.WindowsSandbox.DSC" + targetPath: $(Build.SourcesDirectory)\resources\Microsoft.WindowsSandbox.DSC\ + artifactName: Microsoft.WindowsSandbox.DSC + - output: pipelineArtifact + displayName: "Publish Pipeline GitDsc" + targetPath: $(Build.SourcesDirectory)\resources\GitDsc\ + artifactName: GitDsc + - output: pipelineArtifact + displayName: "Publish Pipeline Microsoft.VSCode.Dsc" + targetPath: $(Build.SourcesDirectory)\resources\Microsoft.VSCode.Dsc\ + artifactName: Microsoft.VSCode.Dsc + - output: pipelineArtifact + displayName: "Publish Pipeline Microsoft.DotNet.Dsc" + targetPath: $(Build.SourcesDirectory)\resources\Microsoft.DotNet.Dsc\ + artifactName: Microsoft.DotNet.Dsc + + steps: + - checkout: self + clean: true + fetchTags: false - steps: - - checkout: self - clean: true - fetchTags: false - - - task: PowerShell@2 - displayName: "Run Pester tests for DSC modules" - inputs: - pwsh: true - targetType: "inline" - script: | - $env:PSModulePath += ";$(Build.SourcesDirectory)\resources" - Invoke-Pester -CI - workingDirectory: $(Build.SourcesDirectory)\tests\ - ignoreLASTEXITCODE: true + - task: PowerShell@2 + displayName: "Run Pester tests for DSC modules" + inputs: + pwsh: true + targetType: "inline" + script: | + $env:PSModulePath += ";$(Build.SourcesDirectory)\resources" + Invoke-Pester -CI + workingDirectory: $(Build.SourcesDirectory)\tests\ + ignoreLASTEXITCODE: true - - task: PublishTestResults@2 - inputs: - testResultsFormat: "NUnit" - testResultsFiles: "**/Test*.xml" - failTaskOnFailedTests: true \ No newline at end of file + - task: PublishTestResults@2 + inputs: + testResultsFormat: "NUnit" + testResultsFiles: "**/Test*.xml" + failTaskOnFailedTests: true diff --git a/resources/Microsoft.DotNet.Dsc/Microsoft.DotNet.Dsc.psd1 b/resources/Microsoft.DotNet.Dsc/Microsoft.DotNet.Dsc.psd1 new file mode 100644 index 00000000..9255b722 --- /dev/null +++ b/resources/Microsoft.DotNet.Dsc/Microsoft.DotNet.Dsc.psd1 @@ -0,0 +1,31 @@ +@{ + RootModule = 'Microsoft.DotNet.Dsc.psm1' + ModuleVersion = '0.1.0' + GUID = '2e883e78-1d91-4d08-9fc1-2a968e31009d' + Author = 'Microsoft Corporation' + CompanyName = 'Microsoft Corporation' + Copyright = '(c) Microsoft Corporation. All rights reserved.' + Description = 'DSC Resource for .NET SDK tool installer' + PowerShellVersion = '7.2' + DscResourcesToExport = @( + 'DotNetToolPackage' + ) + PrivateData = @{ + PSData = @{ + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @( + 'PSDscResource_DotNetToolPackage' + ) + + # Prerelease string of this module + Prerelease = 'alpha' + + # A URL to the license for this module. + LicenseUri = 'https://github.com/microsoft/winget-dsc/blob/main/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/microsoft/winget-dsc' + + } + } +} diff --git a/resources/Microsoft.DotNet.Dsc/Microsoft.DotNet.Dsc.psm1 b/resources/Microsoft.DotNet.Dsc/Microsoft.DotNet.Dsc.psm1 new file mode 100644 index 00000000..04f5f137 --- /dev/null +++ b/resources/Microsoft.DotNet.Dsc/Microsoft.DotNet.Dsc.psm1 @@ -0,0 +1,575 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +$ErrorActionPreference = "Stop" +$PSNativeCommandUseErrorActionPreference = $true +Set-StrictMode -Version Latest + +#region Functions +function Get-DotNetPath +{ + if ($IsWindows) + { + $dotNetPath = "$env:ProgramFiles\dotnet\dotnet.exe" + if (-not (Test-Path $dotNetPath)) + { + $dotNetPath = "${env:ProgramFiles(x86)}\dotnet\dotnet.exe" + if (-not (Test-Path $dotNetPath)) + { + throw "dotnet.exe not found in Program Files or Program Files (x86)" + } + } + } + elseif ($IsMacOS) + { + $dotNetPath = "/usr/local/share/dotnet/dotnet" + if (-not (Test-Path $dotNetPath)) + { + $dotNetPath = "/usr/local/bin/dotnet" + if (-not (Test-Path $dotNetPath)) + { + throw "dotnet not found in /usr/local/share/dotnet or /usr/local/bin" + } + } + } + elseif ($IsLinux) + { + $dotNetPath = "/usr/share/dotnet/dotnet" + if (-not (Test-Path $dotNetPath)) + { + $dotNetPath = "/usr/bin/dotnet" + if (-not (Test-Path $dotNetPath)) + { + throw "dotnet not found in /usr/share/dotnet or /usr/bin" + } + } + } + else + { + throw "Unsupported operating system" + } + + Write-Verbose -Message "'dotnet' found at $dotNetPath" + return $dotNetPath +} + +function Get-DotNetToolArguments +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] $PackageId, + [Parameter(Mandatory = $false)] + [string] $Version, + [Parameter(Mandatory = $false)] + [bool] $PreRelease, + [Parameter(Mandatory = $false)] + [string] $ToolPathDirectory, + [bool] $Exist, + [switch] $Downgrade + ) + + $arguments = @($PackageId) + + if (-not ($PSBoundParameters.ContainsKey("ToolPathDirectory"))) + { + $arguments += "--global" + } + + if ($PSBoundParameters.ContainsKey("Prerelease") -and $PSBoundParameters.ContainsKey("Version")) + { + # do it with version instead of pre + $null = $PSBoundParameters.Remove("Prerelease") + } + + # mapping table of command line arguments + $mappingTable = @{ + Version = "--version {0}" + PreRelease = "--prerelease" + ToolPathDirectory = "--tool-path {0}" + Downgrade = '--allow-downgrade' + } + + $PSBoundParameters.GetEnumerator() | ForEach-Object { + if ($mappingTable.ContainsKey($_.Key)) + { + if ($_.Value -ne $false -and -not (([string]::IsNullOrEmpty($_.Value)))) + { + $arguments += ($mappingTable[$_.Key] -f $_.Value) + } + } + } + + return ($arguments -join " ") +} + +# TODO: when https://github.com/dotnet/sdk/pull/37394 is documented and version is released with option simple use --format=JSON + +function Convert-DotNetToolOutput +{ + [CmdletBinding()] + [OutputType([PSCustomObject[]])] + param ( + [string[]] $Output + ) + + process + { + # Split the output into lines + $lines = $Output | Select-Object -Skip 2 + + # Initialize an array to hold the custom objects + $inputObject = @() + + # Skip the header lines and process each line + foreach ($line in $lines) + { + # Split the line into columns + $columns = $line -split '\s{2,}' + + # Create a custom object for each line + $customObject = [PSCustomObject]@{ + PackageId = $columns[0] + Version = $columns[1] + Commands = $columns[2] + } + + # Add the custom object to the array + $inputObject += $customObject + } + + return $inputObject + } +} + +function Get-InstalledDotNetToolPackages +{ + [CmdletBinding()] + param ( + [string] $PackageId, + [string] $Version, + [bool] $PreRelease, + [Parameter(Mandatory = $false)] + [ValidateScript({ + if (-Not ($_ | Test-Path -PathType Container) ) + { + throw "Directory does not exist" + } + return $true + })] + [string] $ToolPathDirectory, + [bool] $Exist + ) + + $resultSet = [System.Collections.Generic.List[DotNetToolPackage]]::new() + $listCommand = "tool list --global" + $installDir = Join-Path -Path $env:USERPROFILE '.dotnet' 'tools' + + if ($PSBoundParameters.ContainsKey('ToolPathDirectory')) + { + $listCommand = "tool list --tool-path $ToolPathDirectory" + $installDir = $ToolPathDirectory + } + + $result = Invoke-DotNet -Command $listCommand + $packages = Convert-DotNetToolOutput -Output $result + + if ($null -eq $packages) + { + Write-Debug -Message "No packages found." + return + } + + if (-not [string]::IsNullOrEmpty($PackageId)) + { + $packages = $packages | Where-Object { $_.PackageId -eq $PackageId } + } + + foreach ($package in $packages) + { + # flags to determine the existence of the package + $isPrerelease = $false + $preReleasePackage = $package.Version -Split "-" + if ($preReleasePackage.Count -gt 1) + { + # set the pre-release flag to true to build the object + $isPrerelease = $true + } + + $resultSet.Add([DotNetToolPackage]::new( + $package.PackageId, $package.Version, $package.Commands, $isPrerelease, $installDir, $true + )) + } + + return $resultSet +} + +function Get-SemVer($version) +{ + $version -match "^(?\d+)(\.(?\d+))?(\.(?\d+))?(\-(?
[0-9A-Za-z\-\.]+))?(\+(?[0-9A-Za-z\-\.]+))?$" | Out-Null
+    $major = [int]$matches['major']
+    $minor = [int]$matches['minor']
+    $patch = [int]$matches['patch']
+    
+    if ($null -eq $matches['pre']) { $pre = @() }
+    else { $pre = $matches['pre'].Split(".") }
+
+    $revision = 0
+    if ($pre.Length -gt 1)
+    {
+        $revision = Get-HighestRevision -InputArray $pre
+    }
+
+    return [version]$version = "$major.$minor.$patch.$revision" 
+}
+
+function Get-HighestRevision
+{
+    param (
+        [Parameter(Mandatory = $true)]
+        [array]$InputArray
+    )
+
+    # Filter the array to keep only integers
+    $integers = $InputArray | ForEach-Object {
+        $_ -as [int]
+    }
+
+    # Return the highest integer
+    if ($integers.Count -gt 0)
+    {
+        return ($integers | Measure-Object -Maximum).Maximum
+    }
+    else
+    {
+        return $null
+    }
+}
+
+function Install-DotNetToolPackage
+{
+    [CmdletBinding()]
+    param (
+        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
+        [string] $PackageId,
+        [string] $Version,
+        [bool]   $PreRelease,
+        [string] $ToolPathDirectory,
+        [bool]   $Exist
+    )
+
+    $installArgument = Get-DotNetToolArguments @PSBoundParameters
+    $arguments = "tool install $installArgument --ignore-failed-sources"
+    Write-Verbose -Message "Installing dotnet tool package with arguments: $arguments"
+
+    Invoke-DotNet -Command $arguments
+}
+
+function Update-DotNetToolPackage
+{
+    [CmdletBinding()]
+    param (
+        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
+        [string] $PackageId,
+        [string] $Version,
+        [bool]   $PreRelease,
+        [string] $ToolPathDirectory,
+        [bool]   $Exist
+    )
+
+    $installArgument = Get-DotNetToolArguments @PSBoundParameters
+    $arguments = "tool update $installArgument --ignore-failed-sources"
+    Write-Verbose -Message "update dotnet tool package with arguments: $arguments"
+
+    Invoke-DotNet -Command $arguments
+}
+
+function Assert-DotNetToolDowngrade
+{
+    [version]$version = Invoke-DotNet -Command '--version'
+
+    if ($version.Build -lt 200)
+    {
+        return $false
+    }
+
+    return $true
+}
+
+function Uninstall-DotNetToolPackage
+{
+    [CmdletBinding()]
+    param (
+        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
+        [string] $PackageId,
+        [string] $ToolPathDirectory
+    )
+
+    $installArgument = Get-DotNetToolArguments @PSBoundParameters
+    $arguments = "tool uninstall $installArgument" 
+    Write-Verbose -Message "Uninstalling dotnet tool package with arguments: $arguments"
+        
+    Invoke-DotNet -Command $arguments
+}
+
+function Invoke-DotNet
+{
+    param (
+        [Parameter(Mandatory = $true)]
+        [string] $Command
+    )
+
+    try
+    {
+        Invoke-Expression "& `"$DotNetCliPath`" $Command"
+    }
+    catch
+    {
+        throw "Executing dotnet.exe with {$Command} failed."
+    }
+}
+
+# Keeps the path of the code.exe CLI path.
+$DotNetCliPath = Get-DotNetPath
+
+#endregion Functions
+
+#region Classes
+<#
+.SYNOPSIS
+    This class is used to install and uninstall .NET SDK tools globally or use the tool path directory.
+#>
+[DSCResource()]
+class DotNetToolPackage
+{
+    [DscProperty(Key)]
+    [string] $PackageId
+
+    [DscProperty()]
+    [string] $Version
+
+    [DscProperty()]
+    [string[]] $Commands
+
+    [DscProperty()]
+    [bool] $Prerelease = $false
+
+    [DscProperty()]
+    [string] $ToolPathDirectory
+
+    [DscProperty()]
+    [bool] $Exist = $true
+
+    static [hashtable] $InstalledPackages
+
+    DotNetToolPackage()
+    {
+        [DotNetToolPackage]::GetInstalledPackages()
+    }
+
+    DotNetToolPackage([string] $PackageId, [string] $Version, [string[]] $Commands, [bool] $PreRelease, [string] $ToolPathDirectory, [bool] $Exist)
+    {
+        $this.PackageId = $PackageId
+        $this.Version = $Version
+        $this.Commands = $Commands
+        $this.PreRelease = $PreRelease
+        $this.ToolPathDirectory = $ToolPathDirectory
+        $this.Exist = $Exist
+    }
+
+    [DotNetToolPackage] Get()
+    {
+        # get the properties of the object currently set
+        $properties = $this.ToHashTable()
+
+        # refresh installed packages
+        [DotNetToolPackage]::GetInstalledPackages($properties)
+
+        # current state
+        $currentState = [DotNetToolPackage]::InstalledPackages[$this.PackageId]
+
+        if ($null -ne $currentState)
+        {
+            if ($this.Version -and ($this.Version -ne $currentState.Version))
+            {
+                # See treatment: https://learn.microsoft.com/en-us/nuget/concepts/package-versioning?tabs=semver20sort#normalized-version-numbers
+                # in this case, we misuse revision if beta,alpha, rc are present and grab the highest revision
+                $installedVersion = Get-Semver -Version $currentState.Version
+                $currentVersion = Get-Semver -Version $this.Version
+                if ($currentVersion -ne $installedVersion)
+                {
+                    $currentState.Exist = $false
+                }
+            }
+
+            return $currentState
+        }
+        
+        return [DotNetToolPackage]@{
+            PackageId         = $this.PackageId
+            Version           = $this.Version
+            Commands          = $this.Commands
+            PreRelease        = $this.PreRelease
+            ToolPathDirectory = $this.ToolPathDirectory
+            Exist             = $false
+        }
+    }
+
+    Set()
+    {
+        if ($this.Test())
+        {
+            return
+        }
+
+        $currentPackage = [DotNetToolPackage]::InstalledPackages[$this.PackageId]
+        if ($currentPackage -and $this.Exist)
+        {
+            if ($this.Version -lt $currentPackage.Version)
+            {
+                $this.ReInstall($false)
+            }
+            else
+            {
+                $this.Upgrade($false)
+            }
+        }
+        elseif ($this.Exist)
+        {
+            $this.Install($false)
+        }
+        else
+        {
+            $this.Uninstall($false)
+        }
+    }
+
+    [bool] Test()
+    {
+        $currentState = $this.Get()
+        if ($currentState.Exist -ne $this.Exist)
+        {
+            return $false
+        }
+
+        if ($null -ne $this.Version -or $this.Version -ne $currentState.Version -and $this.PreRelease -ne $currentState.PreRelease)
+        {
+            return $false
+        }
+        return $true
+    }
+
+    static [DotNetToolPackage[]] Export()
+    {
+        return [DotNetToolPackage]::Export(@{})
+    }
+
+    static [DotNetToolPackage[]] Export([hashtable] $filterProperties)
+    {
+        $packages = Get-InstalledDotNetToolPackages @filterProperties
+
+        return $packages
+    }
+
+    #region DotNetToolPackage helper functions
+    static [void] GetInstalledPackages()
+    {   
+        [DotNetToolPackage]::InstalledPackages = @{}
+
+        foreach ($extension in [DotNetToolPackage]::Export())
+        {
+            [DotNetToolPackage]::InstalledPackages[$extension.PackageId] = $extension
+        }
+    }
+
+    static [void] GetInstalledPackages([hashtable] $filterProperties)
+    {   
+        [DotNetToolPackage]::InstalledPackages = @{}
+
+        foreach ($extension in [DotNetToolPackage]::Export($filterProperties))
+        {
+            [DotNetToolPackage]::InstalledPackages[$extension.PackageId] = $extension
+        }
+    }
+
+    [void] Upgrade([bool] $preTest)
+    {
+        if ($preTest -and $this.Test())
+        {
+            return
+        }
+
+        $params = $this.ToHashTable()   
+
+        Update-DotNetToolpackage @params
+        [DotNetToolPackage]::GetInstalledPackages()
+    }
+
+    [void] ReInstall([bool] $preTest)
+    {
+        if ($preTest -and $this.Test())
+        {
+            return
+        }
+        
+        $this.Uninstall($false)
+        $this.Install($false)
+        [DotNetToolPackage]::GetInstalledPackages()
+    }
+
+    [void] Install([bool] $preTest)
+    {
+        if ($preTest -and $this.Test())
+        {
+            return
+        }
+
+        $params = $this.ToHashTable()   
+
+        Install-DotNetToolpackage @params
+        [DotNetToolPackage]::GetInstalledPackages()
+    }
+
+    [void] Install()
+    {
+        $this.Install($true)
+    }
+
+    [void] Uninstall([bool] $preTest)
+    {
+        $params = $this.ToHashTable()
+
+        $uninstallParams = @{
+            PackageId = $this.PackageId
+        }
+
+        if ($params.ContainsKey('ToolPathDirectory'))
+        {
+            $uninstallParams.Add('ToolPathDirectory', $params['ToolPathDirectory'])
+        }
+
+        Uninstall-DotNetToolpackage @uninstallParams
+        [DotNetToolPackage]::GetInstalledPackages()
+    }
+
+    [void] Uninstall()
+    {
+        $this.Uninstall($true)
+    }
+
+    [hashtable] ToHashTable()
+    {
+        $parameters = @{}
+        foreach ($property in $this.PSObject.Properties)
+        {
+            if (-not ([string]::IsNullOrEmpty($property.Value)))
+            {
+                $parameters[$property.Name] = $property.Value
+            }
+        }
+
+        return $parameters
+    }
+    #endregion DotNetToolPackage helper functions
+}
+#endregion Classes
\ No newline at end of file
diff --git a/tests/Microsoft.DotNet.Dsc/Microsoft.DotNet.Dsc.Tests.ps1 b/tests/Microsoft.DotNet.Dsc/Microsoft.DotNet.Dsc.Tests.ps1
new file mode 100644
index 00000000..1b9efd82
--- /dev/null
+++ b/tests/Microsoft.DotNet.Dsc/Microsoft.DotNet.Dsc.Tests.ps1
@@ -0,0 +1,239 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+using module Microsoft.DotNet.Dsc
+
+$ErrorActionPreference = "Stop"
+Set-StrictMode -Version Latest
+
+<#
+.Synopsis
+   Pester tests related to the Microsoft.DotNet.Dsc PowerShell module.
+#>
+
+BeforeAll {
+    Install-Module -Name PSDesiredStateConfiguration -Force -SkipPublisherCheck
+    Import-Module Microsoft.DotNet.Dsc
+
+    $script:toolsDir = Join-Path $env:USERPROFILE 'tools'
+
+    if (-not (Test-Path $toolsDir))
+    {
+        $null = New-Item -ItemType Directory -Path $toolsDir -Force -ErrorAction SilentlyContinue
+    }
+}
+
+Describe 'List available DSC resources' {
+    It 'Shows DSC Resources' {
+        $expectedDSCResources = "DotNetToolPackage"
+        $availableDSCResources = (Get-DscResource -Module Microsoft.DotNet.Dsc).Name
+        $availableDSCResources.count | Should -Be 1
+        $availableDSCResources | Where-Object { $expectedDSCResources -notcontains $_ } | Should -BeNullOrEmpty -ErrorAction Stop
+    }
+}
+
+Describe 'DSC operation capabilities' {
+    It 'Sets desired package' -Skip:(!$IsWindows) {
+        $parameters = @{
+            PackageId = 'gitversion.tool'
+        }
+        
+        Invoke-DscResource -Name DotNetToolPackage -ModuleName Microsoft.DotNet.Dsc -Method Set -Property $parameters
+     
+        $finalState = Invoke-DscResource -Name DotNetToolPackage -ModuleName Microsoft.DotNet.Dsc -Method Get -Property $parameters
+        $finalState.Exist | Should -BeTrue
+        $finalState.Version | Should -Not -BeNullOrEmpty
+    }
+
+    It 'Sets desired package with prerelease' -Skip:(!$IsWindows) {
+        $parameters = @{
+            PackageId  = 'dotnet-ef'
+            PreRelease = $true
+        }
+        
+        Invoke-DscResource -Name DotNetToolPackage -ModuleName Microsoft.DotNet.Dsc -Method Set -Property $parameters
+     
+        $finalState = Invoke-DscResource -Name DotNetToolPackage -ModuleName Microsoft.DotNet.Dsc -Method Get -Property $parameters
+        $finalState.PackageId | Should -Be $parameters.PackageId
+        $finalState.PreRelease | Should -BeTrue
+    }
+
+    It 'Sets desired package with version' -Skip:(!$IsWindows) {
+        $parameters = @{
+            PackageId = 'dotnet-reportgenerator-globaltool'
+            Version   = '5.3.9'
+        }
+        
+        Invoke-DscResource -Name DotNetToolPackage -ModuleName Microsoft.DotNet.Dsc -Method Set -Property $parameters
+     
+        $finalState = Invoke-DscResource -Name DotNetToolPackage -ModuleName Microsoft.DotNet.Dsc -Method Get -Property $parameters
+        $finalState.PackageId | Should -Be $parameters.PackageId
+        $finalState.Version | Should -Be $parameters.Version
+    }
+
+    It 'Updates desired package with latest version' -Skip:(!$IsWindows) {
+        $parameters = @{
+            PackageId = 'dotnet-reportgenerator-globaltool'
+            Version   = '5.3.10'
+        }
+        
+        Invoke-DscResource -Name DotNetToolPackage -ModuleName Microsoft.DotNet.Dsc -Method Set -Property $parameters
+     
+        $finalState = Invoke-DscResource -Name DotNetToolPackage -ModuleName Microsoft.DotNet.Dsc -Method Get -Property $parameters
+        $finalState.PackageId | Should -Be $parameters.PackageId
+        $finalState.Version | Should -Be $parameters.Version
+    }
+
+    It 'Sets desired package with prerelease version' -Skip:(!$IsWindows) {
+        $parameters = @{
+            PackageId = 'PowerShell'
+            Version   = '7.2.0-preview.5'
+        }
+        
+        Invoke-DscResource -Name DotNetToolPackage -ModuleName Microsoft.DotNet.Dsc -Method Set -Property $parameters
+     
+        $finalState = Invoke-DscResource -Name DotNetToolPackage -ModuleName Microsoft.DotNet.Dsc -Method Get -Property $parameters
+        $finalState.PackageId | Should -Be $parameters.PackageId
+        $finalState.Version | Should -Be $parameters.Version
+        $finalState.PreRelease | Should -BeTrue
+    }
+
+    It 'Exports resources' -Skip:(!$IsWindows) {
+        $obj = [DotNetToolPackage]::Export()
+        
+        $obj.PackageId.Contains('dotnet-ef') | Should -Be $true
+        $obj.PackageId.Contains('dotnet-reportgenerator-globaltool') | Should -Be $true
+    }
+
+    It 'Throws error when resource is not a tool' -Skip:(!$IsWindows) {
+        $parameters = @{
+            PackageId = 'Azure-Core' # not a tool
+        }
+        
+        { Invoke-DscResource -Name DotNetToolPackage -ModuleName Microsoft.DotNet.Dsc -Method Set -Property $parameters } | Should -Throw -ExpectedMessage "Executing dotnet.exe with {tool install Azure-Core --global --ignore-failed-sources} failed."
+    }
+
+    It 'Installs in tool path location with version' -Skip:(!$IsWindows) {
+        $parameters = @{
+            PackageId         = 'dotnet-dump'
+            ToolPathDirectory = $toolsDir
+            Version           = '8.0.532401'
+        }
+
+        Invoke-DscResource -Name DotNetToolPackage -ModuleName Microsoft.DotNet.Dsc -Method Set -Property $parameters
+
+        $state = Invoke-DscResource -Name DotNetToolPackage -ModuleName Microsoft.DotNet.Dsc -Method Get -Property $parameters
+        $state.Exist | Should -BeTrue
+        $state.ToolPathDirectory | Should -Be $parameters.ToolPathDirectory
+        $state::InstalledPackages[$parameters.PackageId].ToolPathDirectory | Should -Be $parameters.ToolPathDirectory # It should reflect updated export()
+    }
+
+    # TODO: Work on update scenario
+    It 'Update in tool path location' -Skip:(!$IsWindows) {
+        $parameters = @{
+            PackageId         = 'dotnet-dump'
+            ToolPathDirectory = $toolsDir
+            Version           = '8.0.547301'
+        }
+
+        Invoke-DscResource -Name DotNetToolPackage -ModuleName Microsoft.DotNet.Dsc -Method Set -Property $parameters
+
+        $state = Invoke-DscResource -Name DotNetToolPackage -ModuleName Microsoft.DotNet.Dsc -Method Get -Property $parameters
+        $state.Exist | Should -BeTrue
+        $state.ToolPathDirectory | Should -Be $parameters.ToolPathDirectory
+        $state::InstalledPackages[$parameters.PackageId].ToolPathDirectory | Should -Be $parameters.ToolPathDirectory # It should reflect updated export()
+    }
+
+    It 'Uninstall a tool' -Skip:(!$IsWindows) {
+        $parameters = @{
+            PackageId = 'gitversion.tool'
+            Exist     = $false
+        }
+
+        Invoke-DscResource -Name DotNetToolPackage -ModuleName Microsoft.DotNet.Dsc -Method Set -Property $parameters
+
+        $state = Invoke-DscResource -Name DotNetToolPackage -ModuleName Microsoft.DotNet.Dsc -Method Get -Property $parameters
+        $state.Exist | Should -BeFalse
+    }
+
+    It 'Uninstall a tool from tool path location' -Skip:(!$IsWindows) {
+        $parameters = @{
+            PackageId         = 'dotnet-dump'
+            ToolPathDirectory = $toolsDir
+            Exist             = $false
+        }
+
+        Invoke-DscResource -Name DotNetToolPackage -ModuleName Microsoft.DotNet.Dsc -Method Set -Property $parameters
+
+        $state = Invoke-DscResource -Name DotNetToolPackage -ModuleName Microsoft.DotNet.Dsc -Method Get -Property $parameters
+        $state.Exist | Should -BeFalse
+    }
+
+    It 'Downgrades a tool' -Skip:(!$IsWindows) {
+        $parameters = @{
+            PackageId = 'gitversion.tool' # should install latest version
+        }
+
+        Invoke-DscResource -Name DotNetToolPackage -ModuleName Microsoft.DotNet.Dsc -Method Set -Property $parameters
+
+        $parameters = @{
+            PackageId = 'gitversion.tool'
+            Version   = '6.0.2'
+        }
+
+        Invoke-DscResource -Name DotNetToolPackage -ModuleName Microsoft.DotNet.Dsc -Method Set -Property $parameters
+        $state = Invoke-DscResource -Name DotNetToolPackage -ModuleName Microsoft.DotNet.Dsc -Method Get -Property $parameters
+        $state.Version | Should -Be $parameters.Version
+    }
+}
+
+Describe 'DSC helper functions' {
+    Context "Semantic Versioning" {
+        It 'Parses valid semantic version' {
+            $version = '1.2.3'
+            $result = Get-Semver -version $version
+            $result.Major | Should -Be 1
+            $result.Minor | Should -Be 2
+            $result.Build | Should -Be 3
+        }
+
+        It 'Parses semantic version with alpha' {
+            $version = '1.2.3-alpha'
+            $result = Get-Semver -version $version
+            $result.Major | Should -Be 1
+            $result.Minor | Should -Be 2
+            $result.Build | Should -Be 3
+            $result.Revision | Should -Be 0 # Because pre-release is not a number according the handling
+        }
+
+        It 'Parses semantic version with alpha tag and version' {
+            $version = '1.2.3-alpha.123'
+            $result = Get-Semver -version $version
+            $result.Major | Should -Be 1
+            $result.Minor | Should -Be 2
+            $result.Build | Should -Be 3
+            $result.Revision | Should -Be '123'
+        }
+
+        It 'Parses semantic version with beta tag and version' {
+            $version = '1.2.3-beta.11'
+            $result = Get-Semver -version $version
+            $result.Major | Should -Be 1
+            $result.Minor | Should -Be 2
+            $result.Build | Should -Be 3
+            $result.Revision | Should -Be '11'
+        }
+
+        It 'Parses semantic version with rc and version' {
+            $version = '1.2.3-rc.1'
+            $result = Get-Semver -version $version
+            $result.Major | Should -Be 1
+            $result.Minor | Should -Be 2
+            $result.Build | Should -Be 3
+            $result.Revision | Should -Be '1'
+        }
+    }
+}
+
+AfterAll {
+    Remove-Item -Path $toolsDir -Recurse -Force -ErrorAction SilentlyContinue
+}
\ No newline at end of file
diff --git a/tests/Microsoft.Windows.Setting.Accessibility/Microsoft.Windows.Setting.Accessibility.Tests.ps1 b/tests/Microsoft.Windows.Setting.Accessibility/Microsoft.Windows.Setting.Accessibility.Tests.ps1
index 50d641b6..da84f88a 100644
--- a/tests/Microsoft.Windows.Setting.Accessibility/Microsoft.Windows.Setting.Accessibility.Tests.ps1
+++ b/tests/Microsoft.Windows.Setting.Accessibility/Microsoft.Windows.Setting.Accessibility.Tests.ps1
@@ -281,4 +281,4 @@ Describe 'TextCursor'{
 
 AfterAll {
     $env:TestRegistryPath = ""
-}
+}
\ No newline at end of file