diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..96c2e0d --- /dev/null +++ b/.gitattributes @@ -0,0 +1,15 @@ +# Needed for publishing of examples, build worker defaults to core.autocrlf=input. +* text eol=autocrlf + +*.mof text eol=crlf +*.sh text eol=lf +*.svg eol=lf + +# Ensure any exe files are treated as binary +*.exe binary +*.jpg binary +*.xl* binary +*.pfx binary +*.png binary +*.dll binary +*.so binary diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..68cfdd4 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,9 @@ +# cSpell: ignore Viscalyx.Assert +# Each line is a file pattern followed by one or more owners. + +# These owners will be the default owners for everything in +# the repo. Unless a later match takes precedence, +# @viscalyx/Viscalyx.Assert will be requested for review when +# someone opens a pull request. +* @viscalyx/Viscalyx.Assert +/* @johlju diff --git a/.github/ISSUE_TEMPLATE/01_general.md b/.github/ISSUE_TEMPLATE/01_general.md new file mode 100644 index 0000000..fbcdf24 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01_general.md @@ -0,0 +1,7 @@ +--- +name: General question or documentation update +about: If you have a general question or documentation update suggestion around the resource module. +--- + diff --git a/.github/ISSUE_TEMPLATE/02_command_proposal.yml b/.github/ISSUE_TEMPLATE/02_command_proposal.yml new file mode 100644 index 0000000..362cd21 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02_command_proposal.yml @@ -0,0 +1,39 @@ +name: New command proposal +description: If you have a proposal for a new public command that you think should be added to this module. The new command that is proposed shall be able to be used by a new or existing resource. +title: "NewCommandName: New command proposal" +labels: [] +assignees: [] +body: + - type: markdown + attributes: + value: | + Please replace `NewCommandName` in the issue title (above) with your proposed command name. + + Thank you for contributing and making this module better! + - type: textarea + id: description + attributes: + label: Command proposal + description: Provide information how this command will/should work and how it will help users. + validations: + required: true + - type: textarea + id: proposedParameters + attributes: + label: Proposed parameters + description: | + List all the proposed parameters and any parameter sets that the command should have. For each parameter provide a detailed description, the data type, if a default value should be used, and if the property is limited to a set of values. + value: | + Parameter | Mandatory | Data type | Description | Default value | Allowed values + --- | --- | --- | --- | --- | --- + ParameterName | Yes | String | Detailed description | None | None + validations: + required: true + - type: textarea + id: considerations + attributes: + label: Special considerations or limitations + description: | + Provide any considerations or limitations you can think of that a contributor should take in account when coding the proposed command, and or what limitations a user will encounter or should consider when using the proposed command. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/03_problem_with_command.yml b/.github/ISSUE_TEMPLATE/03_problem_with_command.yml new file mode 100644 index 0000000..6cebc98 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/03_problem_with_command.yml @@ -0,0 +1,101 @@ +name: Problem with a command +description: If you want to report a bug or suggest an enhancement to a public command in this module. +labels: [] +assignees: [] +body: + - type: markdown + attributes: + value: | + TITLE: Please be descriptive not sensationalist. + + Your feedback and support is greatly appreciated, thanks for contributing! + + Please provide information regarding your issue under each section below. + **Write N/A in sections that do not apply, or if the information is not available.** + - type: textarea + id: description + attributes: + label: Problem description + description: Details of the scenario you tried and the problem that is occurring, or the enhancement you are suggesting. + validations: + required: true + - type: textarea + id: logs + attributes: + label: Verbose logs + description: | + Verbose logs showing the problem. **NOTE! Sensitive information should be obfuscated.** _Will be automatically formatted as plain text._ + placeholder: | + Paste verbose logs here + render: text + validations: + required: true + - type: textarea + id: reproducible + attributes: + label: How to reproduce + description: Provide the steps to reproduce the problem. + validations: + required: true + - type: textarea + id: expectedBehavior + attributes: + label: Expected behavior + description: Describe what you expected to happen. + validations: + required: true + - type: textarea + id: currentBehavior + attributes: + label: Current behavior + description: Describe what actually happens. + validations: + required: true + - type: textarea + id: suggestedSolution + attributes: + label: Suggested solution + description: Do you have any suggestions how to solve the issue? + validations: + required: true + - type: textarea + id: targetNodeOS + attributes: + label: Operating system the target node is running + description: | + Please provide as much as possible about the node running the command. _Will be automatically formatted as plain text._ + + To help with this information: + - On a Linux distribution, please provide the distribution name, version, and release. The following command can help get this information: `cat /etc/*-release && cat /proc/version` + - On a Windows OS please provide edition, version, build, and language. The following command can help get this information: `Get-ComputerInfo -Property @('OsName','OsOperatingSystemSKU','OSArchitecture','WindowsVersion','WindowsBuildLabEx','OsLanguage','OsMuiLanguages')` + placeholder: | + Add operating system information here + render: text + validations: + required: true + - type: textarea + id: targetNodePS + attributes: + label: PowerShell version and build the target node is running + description: | + Please provide the version and build of PowerShell the target node is running. _Will be automatically formatted as plain text._ + + To help with this information, please run this command: `$PSVersionTable` + placeholder: | + Add PowerShell information here + render: text + validations: + required: true + - type: textarea + id: moduleVersion + attributes: + label: Module version used + description: | + Please provide the version of the module that was used. _Will be automatically formatted as plain text._ + + To help with this information, please run this command where you encountered the problem: `Get-Module -Name 'Viscalyx.Assert' -ListAvailable | ft Name,Version,Path` + placeholder: | + Add module information here + render: text + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..8df8340 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: "Virtual PowerShell User Group #testing channel" + url: https://poshcode.org/ + about: "To talk to the community and maintainers of Viscalyx.Assert, please visit the #testing channel." diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..c74a127 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,52 @@ + +#### Pull Request (PR) description + + +#### This Pull Request (PR) fixes the following issues + + +#### Task list + +- [ ] Added an entry to the change log under the Unreleased section of the + file CHANGELOG.md. Entry should say what was changed and how that + affects users (if applicable), and reference the issue being resolved + (if applicable). +- [ ] Documentation added/updated in README.md and source/WikiSource. +- [ ] Comment-based help added/updated for all new/changed functions. +- [ ] Localization strings added/updated in all localization files as appropriate. +- [ ] Examples appropriately added/updated. +- [ ] Unit tests added/updated. See [DSC Community Testing Guidelines](https://dsccommunity.org/guidelines/testing-guidelines). +- [ ] Integration tests added/updated (where applicable). See + [DSC Community Testing Guidelines](https://dsccommunity.org/guidelines/testing-guidelines). +- [ ] New/changed code adheres to [DSC Community Style Guidelines](https://dsccommunity.org/styleguidelines). diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..501c31c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/code-analysis-built-module.yml b/.github/workflows/code-analysis-built-module.yml new file mode 100644 index 0000000..9063198 --- /dev/null +++ b/.github/workflows/code-analysis-built-module.yml @@ -0,0 +1,64 @@ +name: Code analysis (built module) + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + workflow_dispatch: + +# cSpell: ignore potatoqualitee codeql SARIF +jobs: + pssa: + name: PSScriptAnalyzer + runs-on: windows-latest + permissions: + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/upload-sarif to upload SARIF results + #actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install GitVersion + shell: pwsh + run: | + dotnet tool install --global GitVersion.Tool --version 5.* + - name: Run GitVersion + shell: pwsh + run: | + dotnet-gitversion | ConvertFrom-Json + - name: Build Module + shell: pwsh + run: | + Write-Information -MessageData 'Module is being built.' -InformationAction 'Continue' + .\build.ps1 -ResolveDependency -Tasks 'build' + - name: Run PSScriptAnalyzer + shell: pwsh + run: | + Write-Information -MessageData 'Prepare the test pipeline.' -InformationAction 'Continue' + .\build.ps1 -Tasks 'noop' + + Write-Information -MessageData 'Import module ConvertToSARIF into the session.' -InformationAction 'Continue' + Import-Module -Name 'ConvertToSARIF' -Force + + Write-Information -MessageData 'Import module PSScriptAnalyzer into the session.' -InformationAction 'Continue' + Import-Module -Name 'PSScriptAnalyzer' -Force + + $filesToScan = Get-ChildItem -Path './output/builtModule/Viscalyx.Assert/**/Viscalyx.Assert.psm1' -File + Write-Information -MessageData ("Will scan the file:`t{0}." -f $filesToScan.FullName) -InformationAction 'Continue' + + Write-Information -MessageData 'Running PSScriptAnalyzer on built module.' -InformationAction 'Continue' + $pssaError = $filesToScan | + Invoke-ScriptAnalyzer -Settings './.vscode/analyzersettings.psd1' + + Write-Information -MessageData 'Converting PSScriptAnalyzer result to SARIF.' -InformationAction 'Continue' + $pssaError | + ConvertTo-SARIF -FilePath 'results.sarif' + + Write-Information -MessageData 'Analyzing done.' -InformationAction 'Continue' + - name: Upload SARIF results + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: results.sarif diff --git a/.github/workflows/code-analysis.yml b/.github/workflows/code-analysis.yml new file mode 100644 index 0000000..9042539 --- /dev/null +++ b/.github/workflows/code-analysis.yml @@ -0,0 +1,74 @@ +name: Code analysis (source) + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + workflow_dispatch: + +# cSpell: ignore codeql SARIF +jobs: + pssa: + name: PSScriptAnalyzer + runs-on: windows-latest + permissions: + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/upload-sarif to upload SARIF results + #actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install GitVersion + shell: pwsh + run: | + dotnet tool install --global GitVersion.Tool --version 5.* + - name: Run GitVersion + shell: pwsh + run: | + dotnet-gitversion | ConvertFrom-Json + - name: Build Module + shell: pwsh + run: | + Write-Information -MessageData 'Module is being built so that examples can be scanned.' -InformationAction 'Continue' + .\build.ps1 -ResolveDependency -Tasks 'build' + - name: Run PSScriptAnalyzer + shell: pwsh + run: | + Write-Information -MessageData 'Prepare the test pipeline.' -InformationAction 'Continue' + .\build.ps1 -Tasks 'noop' + + Write-Information -MessageData 'Import module ConvertToSARIF into the session.' -InformationAction 'Continue' + Import-Module -Name 'ConvertToSARIF' -Force + + Write-Information -MessageData 'Import module PSScriptAnalyzer into the session.' -InformationAction 'Continue' + Import-Module -Name 'PSScriptAnalyzer' -Force + + $filesToScan = Get-ChildItem -Path './source/' -Recurse -Include @('*.psm1', '*.ps1') -File + Write-Information -MessageData ("Will scan the files:`n`r`t{0}." -f ($filesToScan.FullName -join "`n`r`t")) -InformationAction 'Continue' + + Write-Information -MessageData 'Running PSScriptAnalyzer.' -InformationAction 'Continue' + $pssaError = $filesToScan | + Invoke-ScriptAnalyzer -Settings './.vscode/analyzersettings.psd1' + + $parseErrorTypes = @( + 'TypeNotFound' + 'RequiresModuleInvalid' + ) + Write-Information -MessageData ('Filter out reported parse errors that is unable to be resolved in source files: {0}' -f ($parseErrorTypes -join ', ')) -InformationAction 'Continue' + $pssaError = $pssaError | + Where-Object -FilterScript { + $_.RuleName -notin $parseErrorTypes + } + + Write-Information -MessageData 'Converting PSScriptAnalyzer result to SARIF.' -InformationAction 'Continue' + $pssaError | + ConvertTo-SARIF -FilePath 'results.sarif' + + Write-Information -MessageData 'Analyzing done.' -InformationAction 'Continue' + - name: Upload SARIF results + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: results.sarif diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..6ec739c --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,25 @@ +name: Mark stale issues and pull requests + +on: + schedule: + - cron: "30 1 * * *" + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: 'This issue has been automatically marked as stale because it has not had activity from the community in the last 30 days. It will be closed if no further activity occurs within 40 days. If the issue is labelled with any of the work labels (e.g bug, enhancement, documentation, or tests) then the issue will not auto-close.' + close-issue-message: 'This issue has been automatically closed because it is has not had activity from the community in the last 40 days. If this issue was wrongly closed, for a issue author please comment and re-open it, if you are not the issue author comment with a reason for it to be reopened and tag a maintainer in the comment.' + days-before-issue-stale: 30 + days-before-issue-close: 40 + exempt-issue-labels: 'bug,enhancement,tests,documentation,resource proposal,command proposal,on hold,resolved' + stale-issue-label: 'stale' + stale-pr-message: 'Labeling this pull request (PR) as abandoned since it has gone 14 days or more since the last update. An abandoned PR can be continued by another contributor. The abandoned label will be removed if work on this PR is taken up again.' + days-before-pr-stale: 14 + days-before-pr-close: -1 + exempt-pr-labels: 'needs review,on hold,ready for merge' + stale-pr-label: 'abandoned' + remove-stale-when-updated: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..67ad94f --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +output/ + +**.bak +*.local.* +!**/README.md +.kitchen/ + +*.nupkg +*.suo +*.user +*.coverage +.vs +.psproj +.sln +markdownissues.txt +node_modules +package-lock.json +.DS_Store diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc new file mode 100644 index 0000000..3c54d91 --- /dev/null +++ b/.markdownlint-cli2.jsonc @@ -0,0 +1,18 @@ +{ + // This is used for VS Code extension. Documented here: https://github.com/DavidAnson/markdownlint-cli2?tab=readme-ov-file#configuration + // Example: https://github.com/DavidAnson/markdownlint/blob/v0.32.1/schema/.markdownlint.jsonc + "config": { + "default": true, + "MD029": { + "style": "one" + }, + "MD013": true, + "MD024": false, + "MD034": false, + "no-hard-tabs": true + }, + "ignores": [ + ".github/CODEOWNERS" + ], + "gitignore": true +} diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..87b7da5 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,10 @@ +{ + "default": true, + "MD029": { + "style": "one" + }, + "MD013": true, + "MD024": false, + "MD034": false, + "no-hard-tabs": true +} diff --git a/.vscode/analyzersettings.psd1 b/.vscode/analyzersettings.psd1 new file mode 100644 index 0000000..84b661c --- /dev/null +++ b/.vscode/analyzersettings.psd1 @@ -0,0 +1,115 @@ +@{ + CustomRulePath = @( + './output/RequiredModules/DscResource.AnalyzerRules' + './output/RequiredModules/Indented.ScriptAnalyzerRules' + ) + IncludeDefaultRules = $true + IncludeRules = @( + # DSC Community style guideline rules from the module ScriptAnalyzer. + 'PSAvoidDefaultValueForMandatoryParameter' + 'PSAvoidDefaultValueSwitchParameter' + 'PSAvoidInvokingEmptyMembers' + 'PSAvoidNullOrEmptyHelpMessageAttribute' + 'PSAvoidUsingCmdletAliases' + 'PSAvoidUsingComputerNameHardcoded' + 'PSAvoidUsingDeprecatedManifestFields' + 'PSAvoidUsingEmptyCatchBlock' + 'PSAvoidUsingInvokeExpression' + 'PSAvoidUsingPositionalParameters' + 'PSAvoidShouldContinueWithoutForce' + 'PSAvoidUsingWMICmdlet' + 'PSAvoidUsingWriteHost' + 'PSDSCReturnCorrectTypesForDSCFunctions' + 'PSDSCStandardDSCFunctionsInResource' + 'PSDSCUseIdenticalMandatoryParametersForDSC' + 'PSDSCUseIdenticalParametersForDSC' + 'PSMisleadingBacktick' + 'PSMissingModuleManifestField' + 'PSPossibleIncorrectComparisonWithNull' + 'PSProvideCommentHelp' + 'PSReservedCmdletChar' + 'PSReservedParams' + 'PSUseApprovedVerbs' + 'PSUseCmdletCorrectly' + 'PSUseOutputTypeCorrectly' + 'PSAvoidGlobalVars' + 'PSAvoidUsingConvertToSecureStringWithPlainText' + 'PSAvoidUsingPlainTextForPassword' + 'PSAvoidUsingUsernameAndPasswordParams' + 'PSDSCUseVerboseMessageInDSCResource' + 'PSShouldProcess' + 'PSUseDeclaredVarsMoreThanAssignments' + 'PSUsePSCredentialType' + + # Additional rules from the module ScriptAnalyzer + 'PSUseConsistentWhitespace' + 'UseCorrectCasing' + 'PSPlaceOpenBrace' + 'PSPlaceCloseBrace' + 'AlignAssignmentStatement' + 'AvoidUsingDoubleQuotesForConstantString' + 'UseShouldProcessForStateChangingFunctions' + + # Rules from the modules DscResource.AnalyzerRules + 'Measure-*' + + # Rules from the module Indented.ScriptAnalyzerRules + 'AvoidCreatingObjectsFromAnEmptyString' + 'AvoidDashCharacters' + 'AvoidEmptyNamedBlocks' + 'AvoidFilter' + 'AvoidHelpMessage' + 'AvoidNestedFunctions' + 'AvoidNewObjectToCreatePSObject' + 'AvoidParameterAttributeDefaultValues' + 'AvoidProcessWithoutPipeline' + 'AvoidSmartQuotes' + 'AvoidThrowOutsideOfTry' + 'AvoidWriteErrorStop' + 'AvoidWriteOutput' + 'UseSyntacticallyCorrectExamples' + ) + + <# + The following types are not rules but parse errors reported by PSScriptAnalyzer + so they cannot be excluded. They need to be filtered out from the result of + Invoke-ScriptAnalyzer. + + TypeNotFound - Because classes in the project cannot be found unless built. + RequiresModuleInvalid - Because 'using module' in prefix.ps1 cannot be resolved as source file. + #> + ExcludeRules = @() + + Rules = @{ + PSUseConsistentWhitespace = @{ + Enable = $true + CheckOpenBrace = $true + CheckInnerBrace = $true + CheckOpenParen = $true + CheckOperator = $false + CheckSeparator = $true + CheckPipe = $true + CheckPipeForRedundantWhitespace = $true + CheckParameter = $false + } + + PSPlaceOpenBrace = @{ + Enable = $true + OnSameLine = $false + NewLineAfter = $true + IgnoreOneLineBlock = $false + } + + PSPlaceCloseBrace = @{ + Enable = $true + NoEmptyLineBefore = $true + IgnoreOneLineBlock = $false + NewLineAfter = $true + } + + PSAlignAssignmentStatement = @{ + Enable = $true + CheckHashtable = $true + } + } +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..1792540 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "davidanson.vscode-markdownlint", + "pspester.pester-test", + "ms-vscode.powershell", + "streetsidesoftware.code-spell-checker" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..07e77f1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,59 @@ + +{ + "powershell.codeFormatting.openBraceOnSameLine": false, + "powershell.codeFormatting.newLineAfterOpenBrace": true, + "powershell.codeFormatting.newLineAfterCloseBrace": true, + "powershell.codeFormatting.whitespaceBeforeOpenBrace": true, + "powershell.codeFormatting.whitespaceBeforeOpenParen": true, + "powershell.codeFormatting.whitespaceAroundOperator": true, + "powershell.codeFormatting.whitespaceAfterSeparator": true, + "powershell.codeFormatting.ignoreOneLineBlock": false, + "powershell.codeFormatting.pipelineIndentationStyle": "IncreaseIndentationForFirstPipeline", + "powershell.codeFormatting.preset": "Custom", + "powershell.codeFormatting.alignPropertyValuePairs": true, + "powershell.codeFormatting.useConstantStrings": true, + "powershell.developer.bundledModulesPath": "${cwd}/output/RequiredModules", + "powershell.scriptAnalysis.settingsPath": "/.vscode/analyzersettings.psd1", + "powershell.scriptAnalysis.enable": true, + "files.trimTrailingWhitespace": true, + "files.trimFinalNewlines": true, + "files.insertFinalNewline": true, + "files.associations": { + "*.ps1xml": "xml" + }, + "cSpell.dictionaries": [ + "powershell" + ], + "cSpell.words": [ + "COMPANYNAME", + "ICONURI", + "LICENSEURI", + "PROJECTURI", + "RELEASENOTES", + "PSDSC", + "Viscalyx", + "analyzersettings", + "CODEOWNERS" + ], + "cSpell.ignorePaths": [ + ".git" + ], + "cSpell.ignoreRegExpList": [ + "/HKCU:/g", + "/analyzersettings\\.psd1/g" + ], + "[markdown]": { + "files.trimTrailingWhitespace": true, + "files.encoding": "utf8" + }, + "powershell.pester.useLegacyCodeLens": false, + "pester.testFilePath": [ + "[tT]ests/[qQ][aA]/*.[tT]ests.[pP][sS]1", + "[tT]ests/[uU]nit/**/*.[tT]ests.[pP][sS]1", + "[tT]ests/[uU]nit/*.[tT]ests.[pP][sS]1" + ], + "pester.runTestsInNewProcess": false, + "pester.pesterModulePath": "./output/RequiredModules/Pester", + "powershell.pester.codeLens": true, + "pester.suppressCodeLensNotice": true +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..2991140 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,125 @@ +{ + "version": "2.0.0", + "_runner": "terminal", + "windows": { + "options": { + "shell": { + "executable": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", + "args": [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command" + ] + } + } + }, + "linux": { + "options": { + "shell": { + "executable": "/usr/bin/pwsh", + "args": [ + "-NoProfile", + "-Command" + ] + } + } + }, + "osx": { + "options": { + "shell": { + "executable": "/usr/local/bin/pwsh", + "args": [ + "-NoProfile", + "-Command" + ] + } + } + }, + "tasks": [ + { + "label": "build", + "type": "shell", + "command": "&${cwd}/build.ps1", + "args": [], + "presentation": { + "echo": true, + "reveal": "always", + "focus": true, + "panel": "new", + "clear": false + }, + "runOptions": { + "runOn": "default" + }, + "problemMatcher": [ + { + "owner": "powershell", + "fileLocation": [ + "absolute" + ], + "severity": "error", + "pattern": [ + { + "regexp": "^\\s*(\\[-\\]\\s*.*?)(\\d+)ms\\s*$", + "message": 1 + }, + { + "regexp": "(.*)", + "code": 1 + }, + { + "regexp": "" + }, + { + "regexp": "^.*,\\s*(.*):\\s*line\\s*(\\d+).*", + "file": 1, + "line": 2 + } + ] + } + ] + }, + { + "label": "test", + "type": "shell", + "command": "&${cwd}/build.ps1", + "args": ["-AutoRestore","-Tasks","test"], + "presentation": { + "echo": true, + "reveal": "always", + "focus": true, + "panel": "dedicated", + "showReuseMessage": true, + "clear": false + }, + "problemMatcher": [ + { + "owner": "powershell", + "fileLocation": [ + "absolute" + ], + "severity": "error", + "pattern": [ + { + "regexp": "^\\s*(\\[-\\]\\s*.*?)(\\d+)ms\\s*$", + "message": 1 + }, + { + "regexp": "(.*)", + "code": 1 + }, + { + "regexp": "" + }, + { + "regexp": "^.*,\\s*(.*):\\s*line\\s*(\\d+).*", + "file": 1, + "line": 2 + } + ] + } + ] + } + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4bb238a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog for Viscalyx.Assert + +The format is based on and uses the types of changes according to [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- Asserts: + - `Assert-BlockString` (alias `Should-BeBlockString`) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..d7589dd --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,3 @@ +# Code of Conduct + +This project has adopted the [DSC Community Code of Conduct](https://dsccommunity.org/code_of_conduct). diff --git a/GitVersion.yml b/GitVersion.yml new file mode 100644 index 0000000..74197c2 --- /dev/null +++ b/GitVersion.yml @@ -0,0 +1,27 @@ +mode: ContinuousDelivery +next-version: 0.0.1 +major-version-bump-message: '(breaking\schange|breaking)\b' +minor-version-bump-message: '(adds?|minor)\b' +patch-version-bump-message: '\s?(fix|patch)' +no-bump-message: '\+semver:\s?(none|skip)' +assembly-informational-format: '{NuGetVersionV2}+Sha.{Sha}.Date.{CommitDate}' +branches: + master: + tag: preview + regex: ^main$ + pull-request: + tag: PR + feature: + tag: useBranchName + increment: Minor + regex: f(eature(s)?)?[\/-] + source-branches: ['master'] + hotfix: + tag: fix + increment: Patch + regex: (hot)?fix(es)?[\/-] + source-branches: ['master'] + +ignore: + sha: [] +merge-message-formats: {} diff --git a/LICENSE b/LICENSE index 35a6381..94c420e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Viscalyx +Copyright (c) Viscalyx.Assert contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 8c3cabb..4920d54 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,35 @@ # Viscalyx.Assert -Assertions for Pester that can improve your tests + +Commands to convert Pester tests. + +[![Build Status](https://dev.azure.com/viscalyx/Viscalyx.Assert/_apis/build/status/viscalyx.Viscalyx.Assert?branchName=main)](https://dev.azure.com/viscalyx/Viscalyx.Assert/_build/latest?definitionId=34&branchName=main) +![Azure DevOps coverage (branch)](https://img.shields.io/azure-devops/coverage/viscalyx/Viscalyx.Assert/34/main) +[![codecov](https://codecov.io/gh/viscalyx/Viscalyx.Assert/branch/main/graph/badge.svg)](https://codecov.io/gh/viscalyx/Viscalyx.Assert) +[![Azure DevOps tests](https://img.shields.io/azure-devops/tests/viscalyx/Viscalyx.Assert/34/main)](https://viscalyx.visualstudio.com/Viscalyx.Assert/_test/analytics?definitionId=34&contextType=build) +[![PowerShell Gallery (with prerelease)](https://img.shields.io/powershellgallery/vpre/Viscalyx.Assert?label=Viscalyx.Assert%20Preview)](https://www.powershellgallery.com/packages/Viscalyx.Assert/) +[![PowerShell Gallery](https://img.shields.io/powershellgallery/v/Viscalyx.Assert?label=Viscalyx.Assert)](https://www.powershellgallery.com/packages/Viscalyx.Assert/) + +## Code of Conduct + +This project has adopted this [Code of Conduct](CODE_OF_CONDUCT.md). + +## Releases + +For each merge to the branch `main` a preview release will be +deployed to [PowerShell Gallery](https://www.powershellgallery.com/). +Periodically a release version tag will be pushed which will deploy a +full release to [PowerShell Gallery](https://www.powershellgallery.com/). + +## Contributing + +Please check out common DSC Community [contributing guidelines](https://dsccommunity.org/guidelines/contributing) +and the specific [Contributing to Viscalyx.Assert](https://github.com/viscalyx/Viscalyx.Assert/blob/main/CONTRIBUTING.md) +guidelines. + +## Change log + +A full list of changes in each version can be found in the [change log](CHANGELOG.md). + +## Documentation + +The documentation can be found in the [Viscalyx.Assert Wiki](https://github.com/viscalyx/Viscalyx.Assert/wiki). diff --git a/RequiredModules.psd1 b/RequiredModules.psd1 new file mode 100644 index 0000000..add0cad --- /dev/null +++ b/RequiredModules.psd1 @@ -0,0 +1,43 @@ +@{ + InvokeBuild = 'latest' + PSScriptAnalyzer = 'latest' + ConvertToSARIF = 'latest' # cSpell: disable-line + + <# + If preview release of Pester prevents release we should temporary shift + back to stable. + #> + Pester = @{ + Version = 'latest' + Parameters = @{ + AllowPrerelease = $true + } + } + + Plaster = 'latest' + ModuleBuilder = 'latest' + ChangelogManagement = 'latest' + Sampler = 'latest' + 'Sampler.GitHubTasks' = 'latest' + MarkdownLinkCheck = 'latest' + 'DscResource.Test' = 'latest' + xDscResourceDesigner = 'latest' + + # Build dependencies needed for using the module + 'DscResource.Common' = 'latest' + + # Analyzer rules + 'DscResource.AnalyzerRules' = 'latest' + 'Indented.ScriptAnalyzerRules' = 'latest' + + # Prerequisite modules for documentation. + 'DscResource.DocGenerator' = 'latest' + PlatyPS = 'latest' + + 'Viscalyx.Common' = @{ + Version = 'latest' + Parameters = @{ + AllowPrerelease = $true + } + } +} diff --git a/Resolve-Dependency.ps1 b/Resolve-Dependency.ps1 new file mode 100644 index 0000000..d400c0d --- /dev/null +++ b/Resolve-Dependency.ps1 @@ -0,0 +1,1058 @@ +<# + .DESCRIPTION + Bootstrap script for PSDepend. + + .PARAMETER DependencyFile + Specifies the configuration file for the this script. The default value is + 'RequiredModules.psd1' relative to this script's path. + + .PARAMETER PSDependTarget + Path for PSDepend to be bootstrapped and save other dependencies. + Can also be CurrentUser or AllUsers if you wish to install the modules in + such scope. The default value is 'output/RequiredModules' relative to + this script's path. + + .PARAMETER Proxy + Specifies the URI to use for Proxy when attempting to bootstrap + PackageProvider and PowerShellGet. + + .PARAMETER ProxyCredential + Specifies the credential to contact the Proxy when provided. + + .PARAMETER Scope + Specifies the scope to bootstrap the PackageProvider and PSGet if not available. + THe default value is 'CurrentUser'. + + .PARAMETER Gallery + Specifies the gallery to use when bootstrapping PackageProvider, PSGet and + when calling PSDepend (can be overridden in Dependency files). The default + value is 'PSGallery'. + + .PARAMETER GalleryCredential + Specifies the credentials to use with the Gallery specified above. + + .PARAMETER AllowOldPowerShellGetModule + Allow you to use a locally installed version of PowerShellGet older than + 1.6.0 (not recommended). Default it will install the latest PowerShellGet + if an older version than 2.0 is detected. + + .PARAMETER MinimumPSDependVersion + Allow you to specify a minimum version fo PSDepend, if you're after specific + features. + + .PARAMETER AllowPrerelease + Not yet written. + + .PARAMETER WithYAML + Not yet written. + + .PARAMETER UseModuleFast + Specifies to use ModuleFast instead of PowerShellGet to resolve dependencies + faster. + + .PARAMETER ModuleFastBleedingEdge + Specifies to use ModuleFast code that is in the ModuleFast's main branch + in its GitHub repository. The parameter UseModuleFast must also be set to + true. + + .PARAMETER UsePSResourceGet + Specifies to use the new PSResourceGet module instead of the (now legacy) PowerShellGet module. + + .PARAMETER PSResourceGetVersion + String specifying the module version for PSResourceGet if the `UsePSResourceGet` switch is utilized. + + .NOTES + Load defaults for parameters values from Resolve-Dependency.psd1 if not + provided as parameter. +#> +[CmdletBinding()] +param +( + [Parameter()] + [System.String] + $DependencyFile = 'RequiredModules.psd1', + + [Parameter()] + [System.String] + $PSDependTarget = (Join-Path -Path $PSScriptRoot -ChildPath 'output/RequiredModules'), + + [Parameter()] + [System.Uri] + $Proxy, + + [Parameter()] + [System.Management.Automation.PSCredential] + $ProxyCredential, + + [Parameter()] + [ValidateSet('CurrentUser', 'AllUsers')] + [System.String] + $Scope = 'CurrentUser', + + [Parameter()] + [System.String] + $Gallery = 'PSGallery', + + [Parameter()] + [System.Management.Automation.PSCredential] + $GalleryCredential, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $AllowOldPowerShellGetModule, + + [Parameter()] + [System.String] + $MinimumPSDependVersion, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $AllowPrerelease, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $WithYAML, + + [Parameter()] + [System.Collections.Hashtable] + $RegisterGallery, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $UseModuleFast, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $ModuleFastBleedingEdge, + + [Parameter()] + [System.String] + $ModuleFastVersion, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $UsePSResourceGet, + + [Parameter()] + [System.String] + $PSResourceGetVersion, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $UsePowerShellGetCompatibilityModule, + + [Parameter()] + [System.String] + $UsePowerShellGetCompatibilityModuleVersion +) + +try +{ + if ($PSVersionTable.PSVersion.Major -le 5) + { + if (-not (Get-Command -Name 'Import-PowerShellDataFile' -ErrorAction 'SilentlyContinue')) + { + Import-Module -Name Microsoft.PowerShell.Utility -RequiredVersion '3.1.0.0' + } + } + + Write-Verbose -Message 'Importing Bootstrap default parameters from ''$PSScriptRoot/Resolve-Dependency.psd1''.' + + $resolveDependencyConfigPath = Join-Path -Path $PSScriptRoot -ChildPath '.\Resolve-Dependency.psd1' -Resolve -ErrorAction 'Stop' + + $resolveDependencyDefaults = Import-PowerShellDataFile -Path $resolveDependencyConfigPath + + $parameterToDefault = $MyInvocation.MyCommand.ParameterSets.Where{ $_.Name -eq $PSCmdlet.ParameterSetName }.Parameters.Keys + + if ($parameterToDefault.Count -eq 0) + { + $parameterToDefault = $MyInvocation.MyCommand.Parameters.Keys + } + + # Set the parameters available in the Parameter Set, or it's not possible to choose yet, so all parameters are an option. + foreach ($parameterName in $parameterToDefault) + { + if (-not $PSBoundParameters.Keys.Contains($parameterName) -and $resolveDependencyDefaults.ContainsKey($parameterName)) + { + Write-Verbose -Message "Setting parameter '$parameterName' to value '$($resolveDependencyDefaults[$parameterName])'." + + try + { + $variableValue = $resolveDependencyDefaults[$parameterName] + + if ($variableValue -is [System.String]) + { + $variableValue = $ExecutionContext.InvokeCommand.ExpandString($variableValue) + } + + $PSBoundParameters.Add($parameterName, $variableValue) + + Set-Variable -Name $parameterName -Value $variableValue -Force -ErrorAction 'SilentlyContinue' + } + catch + { + Write-Verbose -Message "Error adding default for $parameterName : $($_.Exception.Message)." + } + } + } +} +catch +{ + Write-Warning -Message "Error attempting to import Bootstrap's default parameters from '$resolveDependencyConfigPath': $($_.Exception.Message)." +} + +# Handle when both ModuleFast and PSResourceGet is configured or/and passed as parameter. +if ($UseModuleFast -and $UsePSResourceGet) +{ + Write-Information -MessageData 'Both ModuleFast and PSResourceGet is configured or/and passed as parameter.' -InformationAction 'Continue' + + if ($PSVersionTable.PSVersion -ge '7.2') + { + $UsePSResourceGet = $false + + Write-Information -MessageData 'PowerShell 7.2 or higher being used, prefer ModuleFast over PSResourceGet.' -InformationAction 'Continue' + } + else + { + $UseModuleFast = $false + + Write-Information -MessageData 'Windows PowerShell or PowerShell <=7.1 is being used, prefer PSResourceGet since ModuleFast is not supported on this version of PowerShell.' -InformationAction 'Continue' + } +} + +# Only bootstrap ModuleFast if it is not already imported. +if ($UseModuleFast -and -not (Get-Module -Name 'ModuleFast')) +{ + try + { + $moduleFastBootstrapScriptBlockParameters = @{} + + if ($ModuleFastBleedingEdge) + { + Write-Information -MessageData 'ModuleFast is configured to use Bleeding Edge (directly from ModuleFast''s main branch).' -InformationAction 'Continue' + + $moduleFastBootstrapScriptBlockParameters.UseMain = $true + } + elseif($ModuleFastVersion) + { + if ($ModuleFastVersion -notmatch 'v') + { + $ModuleFastVersion = 'v{0}' -f $ModuleFastVersion + } + + Write-Information -MessageData ('ModuleFast is configured to use version {0}.' -f $ModuleFastVersion) -InformationAction 'Continue' + + $moduleFastBootstrapScriptBlockParameters.Release = $ModuleFastVersion + } + else + { + Write-Information -MessageData 'ModuleFast is configured to use latest released version.' -InformationAction 'Continue' + } + + $moduleFastBootstrapUri = 'bit.ly/modulefast' # cSpell: disable-line + + Write-Debug -Message ('Using bootstrap script at {0}' -f $moduleFastBootstrapUri) + + $invokeWebRequestParameters = @{ + Uri = $moduleFastBootstrapUri + ErrorAction = 'Stop' + } + + $moduleFastBootstrapScript = Invoke-WebRequest @invokeWebRequestParameters + + $moduleFastBootstrapScriptBlock = [ScriptBlock]::Create($moduleFastBootstrapScript) + + & $moduleFastBootstrapScriptBlock @moduleFastBootstrapScriptBlockParameters + } + catch + { + Write-Warning -Message ('ModuleFast could not be bootstrapped. Reverting to PSResourceGet. Error: {0}' -f $_.Exception.Message) + + $UseModuleFast = $false + $UsePSResourceGet = $true + } +} + +if ($UsePSResourceGet) +{ + $psResourceGetModuleName = 'Microsoft.PowerShell.PSResourceGet' + + # If PSResourceGet was used prior it will be locked and we can't replace it. + if ((Test-Path -Path "$PSDependTarget/$psResourceGetModuleName" -PathType 'Container') -and (Get-Module -Name $psResourceGetModuleName)) + { + Write-Information -MessageData ('{0} is already bootstrapped and imported into the session. If there is a need to refresh the module, open a new session and resolve dependencies again.' -f $psResourceGetModuleName) -InformationAction 'Continue' + } + else + { + Write-Debug -Message ('{0} do not exist, saving the module to RequiredModules.' -f $psResourceGetModuleName) + + $psResourceGetDownloaded = $false + + try + { + if (-not $PSResourceGetVersion) + { + # Default to latest version if no version is passed in parameter or specified in configuration. + $psResourceGetUri = "https://www.powershellgallery.com/api/v2/package/$psResourceGetModuleName" + } + else + { + $psResourceGetUri = "https://www.powershellgallery.com/api/v2/package/$psResourceGetModuleName/$PSResourceGetVersion" + } + + $invokeWebRequestParameters = @{ + Uri = $psResourceGetUri + OutFile = "$PSDependTarget/$psResourceGetModuleName.nupkg" # cSpell: ignore nupkg + ErrorAction = 'Stop' + } + + $previousProgressPreference = $ProgressPreference + $ProgressPreference = 'SilentlyContinue' + + # Bootstrapping Microsoft.PowerShell.PSResourceGet. + Invoke-WebRequest @invokeWebRequestParameters + + $ProgressPreference = $previousProgressPreference + + $psResourceGetDownloaded = $true + } + catch + { + Write-Warning -Message ('{0} could not be bootstrapped. Reverting to PowerShellGet. Error: {1}' -f $psResourceGetModuleName, $_.Exception.Message) + } + + $UsePSResourceGet = $false + + if ($psResourceGetDownloaded) + { + # On Windows PowerShell the command Expand-Archive do not like .nupkg as a zip archive extension. + $zipFileName = ((Split-Path -Path $invokeWebRequestParameters.OutFile -Leaf) -replace 'nupkg', 'zip') + + $renameItemParameters = @{ + Path = $invokeWebRequestParameters.OutFile + NewName = $zipFileName + Force = $true + } + + Rename-Item @renameItemParameters + + $psResourceGetZipArchivePath = Join-Path -Path (Split-Path -Path $invokeWebRequestParameters.OutFile -Parent) -ChildPath $zipFileName + + $expandArchiveParameters = @{ + Path = $psResourceGetZipArchivePath + DestinationPath = "$PSDependTarget/$psResourceGetModuleName" + Force = $true + } + + Expand-Archive @expandArchiveParameters + + Remove-Item -Path $psResourceGetZipArchivePath + + Import-Module -Name $expandArchiveParameters.DestinationPath -Force + + # Successfully bootstrapped PSResourceGet, so let's use it. + $UsePSResourceGet = $true + } + } + + if ($UsePSResourceGet) + { + $psResourceGetModule = Get-Module -Name $psResourceGetModuleName + + $psResourceGetModuleVersion = $psResourceGetModule.Version.ToString() + + if ($psResourceGetModule.PrivateData.PSData.Prerelease) + { + $psResourceGetModuleVersion += '-{0}' -f $psResourceGetModule.PrivateData.PSData.Prerelease + } + + Write-Information -MessageData ('Using {0} v{1}.' -f $psResourceGetModuleName, $psResourceGetModuleVersion) -InformationAction 'Continue' + + if ($UsePowerShellGetCompatibilityModule) + { + $savePowerShellGetParameters = @{ + Name = 'PowerShellGet' + Path = $PSDependTarget + Repository = 'PSGallery' + TrustRepository = $true + } + + if ($UsePowerShellGetCompatibilityModuleVersion) + { + $savePowerShellGetParameters.Version = $UsePowerShellGetCompatibilityModuleVersion + + # Check if the version is a prerelease. + if ($UsePowerShellGetCompatibilityModuleVersion -match '\d+\.\d+\.\d+-.*') + { + $savePowerShellGetParameters.Prerelease = $true + } + } + + Save-PSResource @savePowerShellGetParameters + + Import-Module -Name "$PSDependTarget/PowerShellGet" + } + } +} + +# Check if legacy PowerShellGet and PSDepend must be bootstrapped. +if (-not ($UseModuleFast -or $UsePSResourceGet)) +{ + if ($PSVersionTable.PSVersion.Major -le 5) + { + <# + Making sure the imported PackageManagement module is not from PS7 module + path. The VSCode PS extension is changing the $env:PSModulePath and + prioritize the PS7 path. This is an issue with PowerShellGet because + it loads an old version if available (or fail to load latest). + #> + Get-Module -ListAvailable PackageManagement | + Where-Object -Property 'ModuleBase' -NotMatch 'powershell.7' | + Select-Object -First 1 | + Import-Module -Force + } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 0 -CurrentOperation 'NuGet Bootstrap' + + $importModuleParameters = @{ + Name = 'PowerShellGet' + MinimumVersion = '2.0' + MaximumVersion = '2.8.999' + ErrorAction = 'SilentlyContinue' + PassThru = $true + } + + if ($AllowOldPowerShellGetModule) + { + $importModuleParameters.Remove('MinimumVersion') + } + + $powerShellGetModule = Import-Module @importModuleParameters + + # Install the package provider if it is not available. + $nuGetProvider = Get-PackageProvider -Name 'NuGet' -ListAvailable -ErrorAction 'SilentlyContinue' | + Select-Object -First 1 + + if (-not $powerShellGetModule -and -not $nuGetProvider) + { + $providerBootstrapParameters = @{ + Name = 'NuGet' + Force = $true + ForceBootstrap = $true + ErrorAction = 'Stop' + Scope = $Scope + } + + switch ($PSBoundParameters.Keys) + { + 'Proxy' + { + $providerBootstrapParameters.Add('Proxy', $Proxy) + } + + 'ProxyCredential' + { + $providerBootstrapParameters.Add('ProxyCredential', $ProxyCredential) + } + + 'AllowPrerelease' + { + $providerBootstrapParameters.Add('AllowPrerelease', $AllowPrerelease) + } + } + + Write-Information -MessageData 'Bootstrap: Installing NuGet Package Provider from the web (Make sure Microsoft addresses/ranges are allowed).' + + $null = Install-PackageProvider @providerBootstrapParameters + + $nuGetProvider = Get-PackageProvider -Name 'NuGet' -ListAvailable | Select-Object -First 1 + + $nuGetProviderVersion = $nuGetProvider.Version.ToString() + + Write-Information -MessageData "Bootstrap: Importing NuGet Package Provider version $nuGetProviderVersion to current session." + + $Null = Import-PackageProvider -Name 'NuGet' -RequiredVersion $nuGetProviderVersion -Force + } + + if ($RegisterGallery) + { + if ($RegisterGallery.ContainsKey('Name') -and -not [System.String]::IsNullOrEmpty($RegisterGallery.Name)) + { + $Gallery = $RegisterGallery.Name + } + else + { + $RegisterGallery.Name = $Gallery + } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 7 -CurrentOperation "Verifying private package repository '$Gallery'" -Completed + + $previousRegisteredRepository = Get-PSRepository -Name $Gallery -ErrorAction 'SilentlyContinue' + + if ($previousRegisteredRepository.SourceLocation -ne $RegisterGallery.SourceLocation) + { + if ($previousRegisteredRepository) + { + Write-Progress -Activity 'Bootstrap:' -PercentComplete 9 -CurrentOperation "Re-registrering private package repository '$Gallery'" -Completed + + Unregister-PSRepository -Name $Gallery + + $unregisteredPreviousRepository = $true + } + else + { + Write-Progress -Activity 'Bootstrap:' -PercentComplete 9 -CurrentOperation "Registering private package repository '$Gallery'" -Completed + } + + Register-PSRepository @RegisterGallery + } + } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 10 -CurrentOperation "Ensuring Gallery $Gallery is trusted" + + # Fail if the given PSGallery is not registered. + $previousGalleryInstallationPolicy = (Get-PSRepository -Name $Gallery -ErrorAction 'Stop').Trusted + + $updatedGalleryInstallationPolicy = $false + + if ($previousGalleryInstallationPolicy -ne $true) + { + $updatedGalleryInstallationPolicy = $true + + # Only change policy if the repository is not trusted + Set-PSRepository -Name $Gallery -InstallationPolicy 'Trusted' -ErrorAction 'Ignore' + } +} + +try +{ + # Check if legacy PowerShellGet and PSDepend must be used. + if (-not ($UseModuleFast -or $UsePSResourceGet)) + { + Write-Progress -Activity 'Bootstrap:' -PercentComplete 25 -CurrentOperation 'Checking PowerShellGet' + + # Ensure the module is loaded and retrieve the version you have. + $powerShellGetVersion = (Import-Module -Name 'PowerShellGet' -PassThru -ErrorAction 'SilentlyContinue').Version + + Write-Verbose -Message "Bootstrap: The PowerShellGet version is $powerShellGetVersion" + + # Versions below 2.0 are considered old, unreliable & not recommended + if (-not $powerShellGetVersion -or ($powerShellGetVersion -lt [System.Version] '2.0' -and -not $AllowOldPowerShellGetModule)) + { + Write-Progress -Activity 'Bootstrap:' -PercentComplete 40 -CurrentOperation 'Fetching newer version of PowerShellGet' + + # PowerShellGet module not found, installing or saving it. + if ($PSDependTarget -in 'CurrentUser', 'AllUsers') + { + Write-Debug -Message "PowerShellGet module not found. Attempting to install from Gallery $Gallery." + + Write-Warning -Message "Installing PowerShellGet in $PSDependTarget Scope." + + $installPowerShellGetParameters = @{ + Name = 'PowerShellGet' + Force = $true + SkipPublisherCheck = $true + AllowClobber = $true + Scope = $Scope + Repository = $Gallery + MaximumVersion = '2.8.999' + } + + switch ($PSBoundParameters.Keys) + { + 'Proxy' + { + $installPowerShellGetParameters.Add('Proxy', $Proxy) + } + + 'ProxyCredential' + { + $installPowerShellGetParameters.Add('ProxyCredential', $ProxyCredential) + } + + 'GalleryCredential' + { + $installPowerShellGetParameters.Add('Credential', $GalleryCredential) + } + } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 60 -CurrentOperation 'Installing newer version of PowerShellGet' + + Install-Module @installPowerShellGetParameters + } + else + { + Write-Debug -Message "PowerShellGet module not found. Attempting to Save from Gallery $Gallery to $PSDependTarget" + + $saveModuleParameters = @{ + Name = 'PowerShellGet' + Repository = $Gallery + Path = $PSDependTarget + Force = $true + MaximumVersion = '2.8.999' + } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 60 -CurrentOperation "Saving PowerShellGet from $Gallery to $Scope" + + Save-Module @saveModuleParameters + } + + Write-Debug -Message 'Removing previous versions of PowerShellGet and PackageManagement from session' + + Get-Module -Name 'PowerShellGet' -All | Remove-Module -Force -ErrorAction 'SilentlyContinue' + Get-Module -Name 'PackageManagement' -All | Remove-Module -Force + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 65 -CurrentOperation 'Loading latest version of PowerShellGet' + + Write-Debug -Message 'Importing latest PowerShellGet and PackageManagement versions into session' + + if ($AllowOldPowerShellGetModule) + { + $powerShellGetModule = Import-Module -Name 'PowerShellGet' -Force -PassThru + } + else + { + Import-Module -Name 'PackageManagement' -MinimumVersion '1.4.8.1' -Force + + $powerShellGetModule = Import-Module -Name 'PowerShellGet' -MinimumVersion '2.2.5' -Force -PassThru + } + + $powerShellGetVersion = $powerShellGetModule.Version.ToString() + + Write-Information -MessageData "Bootstrap: PowerShellGet version loaded is $powerShellGetVersion" + } + + # Try to import the PSDepend module from the available modules. + $getModuleParameters = @{ + Name = 'PSDepend' + ListAvailable = $true + } + + $psDependModule = Get-Module @getModuleParameters + + if ($PSBoundParameters.ContainsKey('MinimumPSDependVersion')) + { + try + { + $psDependModule = $psDependModule | Where-Object -FilterScript { $_.Version -ge $MinimumPSDependVersion } + } + catch + { + throw ('There was a problem finding the minimum version of PSDepend. Error: {0}' -f $_) + } + } + + if (-not $psDependModule) + { + Write-Debug -Message 'PSDepend module not found.' + + # PSDepend module not found, installing or saving it. + if ($PSDependTarget -in 'CurrentUser', 'AllUsers') + { + Write-Debug -Message "Attempting to install from Gallery '$Gallery'." + + Write-Warning -Message "Installing PSDepend in $PSDependTarget Scope." + + $installPSDependParameters = @{ + Name = 'PSDepend' + Repository = $Gallery + Force = $true + Scope = $PSDependTarget + SkipPublisherCheck = $true + AllowClobber = $true + } + + if ($MinimumPSDependVersion) + { + $installPSDependParameters.Add('MinimumVersion', $MinimumPSDependVersion) + } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 75 -CurrentOperation "Installing PSDepend from $Gallery" + + Install-Module @installPSDependParameters + } + else + { + Write-Debug -Message "Attempting to Save from Gallery $Gallery to $PSDependTarget" + + $saveModuleParameters = @{ + Name = 'PSDepend' + Repository = $Gallery + Path = $PSDependTarget + Force = $true + } + + if ($MinimumPSDependVersion) + { + $saveModuleParameters.add('MinimumVersion', $MinimumPSDependVersion) + } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 75 -CurrentOperation "Saving PSDepend from $Gallery to $PSDependTarget" + + Save-Module @saveModuleParameters + } + } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 80 -CurrentOperation 'Importing PSDepend' + + $importModulePSDependParameters = @{ + Name = 'PSDepend' + ErrorAction = 'Stop' + Force = $true + } + + if ($PSBoundParameters.ContainsKey('MinimumPSDependVersion')) + { + $importModulePSDependParameters.Add('MinimumVersion', $MinimumPSDependVersion) + } + + # We should have successfully bootstrapped PSDepend. Fail if not available. + $null = Import-Module @importModulePSDependParameters + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 81 -CurrentOperation 'Invoke PSDepend' + + if ($WithYAML) + { + Write-Progress -Activity 'Bootstrap:' -PercentComplete 82 -CurrentOperation 'Verifying PowerShell module PowerShell-Yaml' + + if (-not (Get-Module -ListAvailable -Name 'PowerShell-Yaml')) + { + Write-Progress -Activity 'Bootstrap:' -PercentComplete 85 -CurrentOperation 'Installing PowerShell module PowerShell-Yaml' + + Write-Verbose -Message "PowerShell-Yaml module not found. Attempting to Save from Gallery '$Gallery' to '$PSDependTarget'." + + $SaveModuleParam = @{ + Name = 'PowerShell-Yaml' + Repository = $Gallery + Path = $PSDependTarget + Force = $true + } + + Save-Module @SaveModuleParam + } + else + { + Write-Verbose -Message 'PowerShell-Yaml is already available' + } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 88 -CurrentOperation 'Importing PowerShell module PowerShell-Yaml' + } + } + + if (Test-Path -Path $DependencyFile) + { + if ($UseModuleFast -or $UsePSResourceGet) + { + $requiredModules = Import-PowerShellDataFile -Path $DependencyFile + + $requiredModules = $requiredModules.GetEnumerator() | + Where-Object -FilterScript { $_.Name -ne 'PSDependOptions' } + + if ($UseModuleFast) + { + Write-Progress -Activity 'Bootstrap:' -PercentComplete 90 -CurrentOperation 'Invoking ModuleFast' + + Write-Progress -Activity 'ModuleFast:' -PercentComplete 0 -CurrentOperation 'Restoring Build Dependencies' + + $modulesToSave = @( + 'PSDepend' # Always include PSDepend for backward compatibility. + ) + + if ($WithYAML) + { + $modulesToSave += 'PowerShell-Yaml' + } + + if ($UsePowerShellGetCompatibilityModule) + { + Write-Debug -Message 'PowerShellGet compatibility module is configured to be used.' + + # This is needed to ensure that the PowerShellGet compatibility module works. + $psResourceGetModuleName = 'Microsoft.PowerShell.PSResourceGet' + + if ($PSResourceGetVersion) + { + $modulesToSave += ('{0}:[{1}]' -f $psResourceGetModuleName, $PSResourceGetVersion) + } + else + { + $modulesToSave += $psResourceGetModuleName + } + + $powerShellGetCompatibilityModuleName = 'PowerShellGet' + + if ($UsePowerShellGetCompatibilityModuleVersion) + { + $modulesToSave += ('{0}:[{1}]' -f $powerShellGetCompatibilityModuleName, $UsePowerShellGetCompatibilityModuleVersion) + } + else + { + $modulesToSave += $powerShellGetCompatibilityModuleName + } + } + + foreach ($requiredModule in $requiredModules) + { + # If the RequiredModules.psd1 entry is an Hashtable then special handling is needed. + if ($requiredModule.Value -is [System.Collections.Hashtable]) + { + if (-not $requiredModule.Value.Version) + { + $requiredModuleVersion = 'latest' + } + else + { + $requiredModuleVersion = $requiredModule.Value.Version + } + + if ($requiredModuleVersion -eq 'latest') + { + $moduleNameSuffix = '' + + if ($requiredModule.Value.Parameters.AllowPrerelease -eq $true) + { + <# + Adding '!' to the module name indicate to ModuleFast + that is should also evaluate pre-releases. + #> + $moduleNameSuffix = '!' + } + + $modulesToSave += ('{0}{1}' -f $requiredModule.Name, $moduleNameSuffix) + } + else + { + $modulesToSave += ('{0}:[{1}]' -f $requiredModule.Name, $requiredModuleVersion) + } + } + else + { + if ($requiredModule.Value -eq 'latest') + { + $modulesToSave += $requiredModule.Name + } + else + { + # Handle different nuget version operators already present. + if ($requiredModule.Value -match '[!|:|[|(|,|>|<|=]') + { + $modulesToSave += ('{0}{1}' -f $requiredModule.Name, $requiredModule.Value) + } + else + { + # Assuming the version is a fixed version. + $modulesToSave += ('{0}:[{1}]' -f $requiredModule.Name, $requiredModule.Value) + } + } + } + } + + Write-Debug -Message ("Required modules to retrieve plan for:`n{0}" -f ($modulesToSave | Out-String)) + + $installModuleFastParameters = @{ + Destination = $PSDependTarget + DestinationOnly = $true + NoPSModulePathUpdate = $true + NoProfileUpdate = $true + Update = $true + Confirm = $false + } + + $moduleFastPlan = Install-ModuleFast -Specification $modulesToSave -Plan @installModuleFastParameters + + Write-Debug -Message ("Missing modules that need to be saved:`n{0}" -f ($moduleFastPlan | Out-String)) + + if ($moduleFastPlan) + { + # Clear all modules in plan from the current session so they can be fetched again. + $moduleFastPlan.Name | Get-Module | Remove-Module -Force + + $moduleFastPlan | Install-ModuleFast @installModuleFastParameters + } + else + { + Write-Verbose -Message 'All required modules were already up to date' + } + + Write-Progress -Activity 'ModuleFast:' -PercentComplete 100 -CurrentOperation 'Dependencies restored' -Completed + } + + if ($UsePSResourceGet) + { + Write-Progress -Activity 'Bootstrap:' -PercentComplete 90 -CurrentOperation 'Invoking PSResourceGet' + + $modulesToSave = @( + @{ + Name = 'PSDepend' # Always include PSDepend for backward compatibility. + } + ) + + if ($WithYAML) + { + $modulesToSave += @{ + Name = 'PowerShell-Yaml' + } + } + + # Prepare hashtable that can be concatenated to the Save-PSResource parameters. + foreach ($requiredModule in $requiredModules) + { + # If the RequiredModules.psd1 entry is an Hashtable then special handling is needed. + if ($requiredModule.Value -is [System.Collections.Hashtable]) + { + $saveModuleHashtable = @{ + Name = $requiredModule.Name + } + + if ($requiredModule.Value.Version -and $requiredModule.Value.Version -ne 'latest') + { + $saveModuleHashtable.Version = $requiredModule.Value.Version + } + + if ($requiredModule.Value.Parameters.AllowPrerelease -eq $true) + { + $saveModuleHashtable.Prerelease = $true + } + + $modulesToSave += $saveModuleHashtable + } + else + { + if ($requiredModule.Value -eq 'latest') + { + $modulesToSave += @{ + Name = $requiredModule.Name + } + } + else + { + $modulesToSave += @{ + Name = $requiredModule.Name + Version = $requiredModule.Value + } + } + } + } + + $percentagePerModule = [System.Math]::Floor(100 / $modulesToSave.Length) + + $progressPercentage = 0 + + Write-Progress -Activity 'PSResourceGet:' -PercentComplete $progressPercentage -CurrentOperation 'Restoring Build Dependencies' + + foreach ($currentModule in $modulesToSave) + { + Write-Progress -Activity 'PSResourceGet:' -PercentComplete $progressPercentage -CurrentOperation 'Restoring Build Dependencies' -Status ('Saving module {0}' -f $savePSResourceParameters.Name) + + $savePSResourceParameters = @{ + Path = $PSDependTarget + TrustRepository = $true + Confirm = $false + } + + # Concatenate the module parameters to the Save-PSResource parameters. + $savePSResourceParameters += $currentModule + + # Modules that Sampler depend on that cannot be refreshed without a new session. + $skipModule = @('PowerShell-Yaml') + + if ($savePSResourceParameters.Name -in $skipModule -and (Get-Module -Name $savePSResourceParameters.Name)) + { + Write-Progress -Activity 'PSResourceGet:' -PercentComplete $progressPercentage -CurrentOperation 'Restoring Build Dependencies' -Status ('Skipping module {0}' -f $savePSResourceParameters.Name) + + Write-Information -MessageData ('Skipping the module {0} since it cannot be refresh while loaded into the session. To refresh the module open a new session and resolve dependencies again.' -f $savePSResourceParameters.Name) -InformationAction 'Continue' + } + else + { + # Clear all module from the current session so any new version fetched will be re-imported. + Get-Module -Name $savePSResourceParameters.Name | Remove-Module -Force + + Save-PSResource @savePSResourceParameters -ErrorVariable 'savePSResourceError' + + if ($savePSResourceError) + { + Write-Warning -Message 'Save-PSResource could not save (replace) one or more dependencies. This can be due to the module is loaded into the session (and referencing assemblies). Close the current session and open a new session and try again.' + } + } + + $progressPercentage += $percentagePerModule + } + + Write-Progress -Activity 'PSResourceGet:' -PercentComplete 100 -CurrentOperation 'Dependencies restored' -Completed + } + } + else + { + Write-Progress -Activity 'Bootstrap:' -PercentComplete 90 -CurrentOperation 'Invoking PSDepend' + + Write-Progress -Activity 'PSDepend:' -PercentComplete 0 -CurrentOperation 'Restoring Build Dependencies' + + $psDependParameters = @{ + Force = $true + Path = $DependencyFile + } + + Invoke-PSDepend @psDependParameters + + Write-Progress -Activity 'PSDepend:' -PercentComplete 100 -CurrentOperation 'Dependencies restored' -Completed + } + } + else + { + Write-Warning -Message "The dependency file '$DependencyFile' could not be found." + } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 100 -CurrentOperation 'Bootstrap complete' -Completed +} +finally +{ + if ($RegisterGallery) + { + Write-Verbose -Message "Removing private package repository '$Gallery'." + Unregister-PSRepository -Name $Gallery + } + + if ($unregisteredPreviousRepository) + { + Write-Verbose -Message "Reverting private package repository '$Gallery' to previous location URI:s." + + $registerPSRepositoryParameters = @{ + Name = $previousRegisteredRepository.Name + InstallationPolicy = $previousRegisteredRepository.InstallationPolicy + } + + if ($previousRegisteredRepository.SourceLocation) + { + $registerPSRepositoryParameters.SourceLocation = $previousRegisteredRepository.SourceLocation + } + + if ($previousRegisteredRepository.PublishLocation) + { + $registerPSRepositoryParameters.PublishLocation = $previousRegisteredRepository.PublishLocation + } + + if ($previousRegisteredRepository.ScriptSourceLocation) + { + $registerPSRepositoryParameters.ScriptSourceLocation = $previousRegisteredRepository.ScriptSourceLocation + } + + if ($previousRegisteredRepository.ScriptPublishLocation) + { + $registerPSRepositoryParameters.ScriptPublishLocation = $previousRegisteredRepository.ScriptPublishLocation + } + + Register-PSRepository @registerPSRepositoryParameters + } + + if ($updatedGalleryInstallationPolicy -eq $true -and $previousGalleryInstallationPolicy -ne $true) + { + # Only try to revert installation policy if the repository exist + if ((Get-PSRepository -Name $Gallery -ErrorAction 'SilentlyContinue')) + { + # Reverting the Installation Policy for the given gallery if it was not already trusted + Set-PSRepository -Name $Gallery -InstallationPolicy 'Untrusted' + } + } + + Write-Verbose -Message 'Project Bootstrapped, returning to Invoke-Build.' +} diff --git a/Resolve-Dependency.psd1 b/Resolve-Dependency.psd1 new file mode 100644 index 0000000..7e4c67f --- /dev/null +++ b/Resolve-Dependency.psd1 @@ -0,0 +1,8 @@ +@{ + Gallery = 'PSGallery' + AllowPrerelease = $false + WithYAML = $true + + UseModuleFast = $true +} + diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 0000000..a27f8a8 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,364 @@ +trigger: + branches: + include: + - main + paths: + include: + - source/* + tags: + include: + - "v*" + exclude: + - "*-*" + +variables: + buildFolderName: output + buildArtifactName: output + testResultFolderName: testResults + testArtifactName: 'testResults_$(System.JobAttempt)' + sourceFolderName: source + defaultBranch: main + Agent.Source.Git.ShallowFetchDepth: 0 + +# cSpell: ignore setvariable updatebuildnumber hqrmtest quickconfig viscalyx psresourceget +stages: + - stage: Build + jobs: + - job: Package_Module + displayName: 'Package Module' + pool: + vmImage: 'windows-latest' + steps: + - pwsh: | + dotnet tool install --global GitVersion.Tool --version 5.* + $gitVersionObject = dotnet-gitversion | ConvertFrom-Json + $gitVersionObject.PSObject.Properties.ForEach{ + Write-Host -Object "Setting Task Variable '$($_.Name)' with value '$($_.Value)'." + Write-Host -Object "##vso[task.setvariable variable=$($_.Name);]$($_.Value)" + } + Write-Host -Object "##vso[build.updatebuildnumber]$($gitVersionObject.FullSemVer)" + displayName: Calculate ModuleVersion (GitVersion) + - task: PowerShell@2 + name: package + displayName: 'Build & Package Module' + inputs: + filePath: './build.ps1' + arguments: '-ResolveDependency -tasks pack' + pwsh: true + env: + ModuleVersion: $(NuGetVersionV2) + - task: PublishPipelineArtifact@1 + displayName: 'Publish Build Artifact' + inputs: + targetPath: '$(buildFolderName)/' + artifact: $(buildArtifactName) + publishLocation: 'pipeline' + parallel: true + + - stage: Test + dependsOn: Build + jobs: + - job: Test_HQRM + displayName: 'HQRM' + pool: + vmImage: 'windows-2022' + timeoutInMinutes: 0 + steps: + - task: DownloadPipelineArtifact@2 + displayName: 'Download Build Artifact' + inputs: + buildType: 'current' + artifactName: $(buildArtifactName) + targetPath: '$(Build.SourcesDirectory)/$(buildFolderName)' + - pwsh: | + # Workaround for issue https://github.com/dsccommunity/DscResource.Test/issues/100 + ./build.ps1 -Task noop + + $pesterConfig = New-PesterConfiguration + $pesterConfig.Run.Path = '.\tests\QA' + $pesterConfig.Run.Throw = $true + $pesterConfig.Output.Verbosity = 'Detailed' + + Invoke-Pester -Configuration $pesterConfig + name: qualityTest + displayName: 'Run QA Test' + - task: PowerShell@2 + name: test + displayName: 'Run HQRM Test' + condition: succeededOrFailed() + inputs: + filePath: './build.ps1' + arguments: '-Tasks hqrmtest' + pwsh: true + - task: PublishTestResults@2 + displayName: 'Publish Test Results' + condition: succeededOrFailed() + inputs: + testResultsFormat: 'NUnit' + testResultsFiles: '$(buildFolderName)/$(testResultFolderName)/NUnit*.xml' + testRunTitle: 'HQRM' + + - job: test_linux + displayName: 'Unit Linux' + timeoutInMinutes: 0 + pool: + vmImage: 'ubuntu-latest' + steps: + - task: DownloadPipelineArtifact@2 + displayName: 'Download Build Artifact' + inputs: + buildType: 'current' + artifactName: $(buildArtifactName) + targetPath: '$(Build.SourcesDirectory)/$(buildFolderName)' + - task: PowerShell@2 + name: test + displayName: 'Run Tests' + inputs: + filePath: './build.ps1' + arguments: '-tasks test' + - task: PublishTestResults@2 + displayName: 'Publish Test Results' + condition: succeededOrFailed() + inputs: + testResultsFormat: 'NUnit' + testResultsFiles: '$(buildFolderName)/$(testResultFolderName)/NUnit*.xml' + testRunTitle: 'Linux' + - task: PublishPipelineArtifact@1 + displayName: 'Publish Test Artifact' + condition: succeededOrFailed() + inputs: + targetPath: '$(buildFolderName)/$(testResultFolderName)/' + artifactName: 'CodeCoverageLinux_$(System.JobAttempt)' + parallel: true + + - job: test_windows_core + displayName: 'Unit Windows (PowerShell Core)' + timeoutInMinutes: 0 + pool: + vmImage: 'windows-latest' + steps: + - task: DownloadPipelineArtifact@2 + displayName: 'Download Build Artifact' + inputs: + buildType: 'current' + artifactName: $(buildArtifactName) + targetPath: '$(Build.SourcesDirectory)/$(buildFolderName)' + - task: PowerShell@2 + name: test + displayName: 'Run Tests' + inputs: + filePath: './build.ps1' + arguments: '-tasks test' + pwsh: true + - task: PublishTestResults@2 + displayName: 'Publish Test Results' + condition: succeededOrFailed() + inputs: + testResultsFormat: 'NUnit' + testResultsFiles: '$(buildFolderName)/$(testResultFolderName)/NUnit*.xml' + testRunTitle: 'Windows Server Core (PowerShell Core)' + - task: PublishPipelineArtifact@1 + displayName: 'Publish Test Artifact' + condition: succeededOrFailed() + inputs: + targetPath: '$(buildFolderName)/$(testResultFolderName)/' + artifactName: 'CodeCoverageWinPS7_$(System.JobAttempt)' + parallel: true + + - job: test_macos + displayName: 'Unit macOS' + timeoutInMinutes: 0 + pool: + vmImage: 'macos-latest' + steps: + - task: DownloadPipelineArtifact@2 + displayName: 'Download Build Artifact' + inputs: + buildType: 'current' + artifactName: $(buildArtifactName) + targetPath: '$(Build.SourcesDirectory)/$(buildFolderName)' + - task: PowerShell@2 + name: test + displayName: 'Run Tests' + inputs: + filePath: './build.ps1' + arguments: '-tasks test' + pwsh: true + - task: PublishTestResults@2 + displayName: 'Publish Test Results' + condition: succeededOrFailed() + inputs: + testResultsFormat: 'NUnit' + testResultsFiles: '$(buildFolderName)/$(testResultFolderName)/NUnit*.xml' + testRunTitle: 'MacOS' + - task: PublishPipelineArtifact@1 + displayName: 'Publish Test Artifact' + condition: succeededOrFailed() + inputs: + targetPath: '$(buildFolderName)/$(testResultFolderName)/' + artifactName: 'CodeCoverageMacOS_$(System.JobAttempt)' + parallel: true + + - job: Code_Coverage + displayName: 'Publish Code Coverage' + dependsOn: + - test_macos + - test_linux + - test_windows_core + condition: succeededOrFailed() + pool: + vmImage: 'ubuntu-latest' + timeoutInMinutes: 0 + steps: + - task: DownloadPipelineArtifact@2 + displayName: 'Download Build Artifact' + inputs: + buildType: 'current' + artifactName: $(buildArtifactName) + targetPath: '$(Build.SourcesDirectory)/$(buildFolderName)' + - task: DownloadPipelineArtifact@2 + displayName: 'Download Test Artifact macOS' + inputs: + buildType: 'current' + artifactName: 'CodeCoverageMacOS_$(System.JobAttempt)' + targetPath: '$(Build.SourcesDirectory)/$(buildFolderName)/$(testResultFolderName)' + - task: DownloadPipelineArtifact@2 + displayName: 'Download Test Artifact Linux' + inputs: + buildType: 'current' + artifactName: 'CodeCoverageLinux_$(System.JobAttempt)' + targetPath: '$(Build.SourcesDirectory)/$(buildFolderName)/$(testResultFolderName)' + - task: DownloadPipelineArtifact@2 + displayName: 'Download Test Artifact Windows (PS7)' + inputs: + buildType: 'current' + artifactName: 'CodeCoverageWinPS7_$(System.JobAttempt)' + targetPath: '$(Build.SourcesDirectory)/$(buildFolderName)/$(testResultFolderName)' + - task: PowerShell@2 + name: merge + displayName: 'Merge Code Coverage files' + inputs: + filePath: './build.ps1' + arguments: '-tasks merge' + pwsh: true + - task: PublishCodeCoverageResults@1 + displayName: 'Publish Code Coverage to Azure DevOps' + inputs: + codeCoverageTool: 'JaCoCo' + summaryFileLocation: '$(Build.SourcesDirectory)/$(buildFolderName)/$(testResultFolderName)/JaCoCo_coverage.xml' + pathToSources: '$(Build.SourcesDirectory)/$(sourceFolderName)/' + - script: | + bash <(curl -s https://codecov.io/bash) -f "./$(buildFolderName)/$(testResultFolderName)/JaCoCo_coverage.xml" -F unit + displayName: 'Publish Code Coverage to Codecov.io' + + # - job: Test_Integration + # displayName: 'Integration' + # strategy: + # matrix: + # WINDOWS: + # JOB_VMIMAGE: 'windows-latest' + # UBUNTU: + # JOB_VMIMAGE: 'ubuntu-latest' + # MACOS: + # JOB_VMIMAGE: 'macos-latest' + # pool: + # vmImage: $(JOB_VMIMAGE) + # timeoutInMinutes: 0 + # steps: + # - task: DownloadPipelineArtifact@2 + # displayName: 'Download Build Artifact' + # inputs: + # buildType: 'current' + # artifactName: $(buildArtifactName) + # targetPath: '$(Build.SourcesDirectory)/$(buildFolderName)' + # - pwsh: | + # ./build.ps1 -Tasks noop + # New-Item -ItemType Directory -Path (Get-PSModulePath -Scope CurrentUser) -Force + # name: currentUserModulePath + # displayName: 'Validate CurrentUser Module Path' + # - pwsh: | + # & ([ScriptBlock]::Create((iwr 'bit.ly/psresourceget'))) -Force -Verbose + # Get-Module -Name Microsoft.PowerShell.PSResourceGet -ListAvailable + # name: bootstrapPSResourceGet + # displayName: 'Bootstrap PSResourceGet' + # - pwsh: | + # Install-PSResource -Name 'Pester' -Version '[5.0,6.0)' -TrustRepository -ErrorAction 'Stop' -Confirm:$false -PassThru + # Import-Module -Name Pester -MaximumVersion '5.*' -ErrorAction 'Stop' -PassThru + # Invoke-Pester -Path '.\tests\Integration\Syntax\v5' -Output Detailed + # name: validatePesterSyntax_v5 + # displayName: 'Validate Pester 5 Syntax' + # - pwsh: | + # ./build.ps1 -Tasks noop + # Import-Module -Name Viscalyx.Assert -PassThru + # New-Item -ItemType Directory -Path './ConversionTest/v6' -Force + # Convert-PesterSyntax -Path (Get-ChildItem -Path './tests/Integration/Syntax/v5') -OutputPath './ConversionTest/v6' -Force -Verbose + # name: convert_v5_to_v6 + # displayName: 'Convert v5 syntax to v6' + # - task: PublishPipelineArtifact@1 + # displayName: 'Publish Converted v6 Artifact' + # inputs: + # targetPath: './ConversionTest/v6' + # artifact: 'ConvertedTests_v6_$(System.JobName)_Attempt_$(System.JobAttempt)' + # publishLocation: 'pipeline' + # parallel: true + # - pwsh: | + # Install-PSResource -Name 'Pester' -Prerelease -TrustRepository -ErrorAction 'Stop' -Confirm:$false -PassThru + # Import-Module -Name Pester -MaximumVersion '6.*' -ErrorAction 'Stop' -PassThru + # Invoke-Pester -Path './ConversionTest/v6' -Output Detailed + # name: validatePesterSyntax_v6 + # displayName: 'Validate Pester 6 Syntax' + ## Currently there are no test result to upload since we are not using the normal test pipeline. + # - task: PublishTestResults@2 + # displayName: 'Publish Test Results' + # condition: succeededOrFailed() + # inputs: + # testResultsFormat: 'NUnit' + # testResultsFiles: '$(buildFolderName)/$(testResultFolderName)/NUnit*.xml' + # testRunTitle: 'Integration ($(Agent.JobName))' + + - stage: Deploy + dependsOn: Test + condition: | + and( + succeeded(), + or( + eq(variables['Build.SourceBranch'], 'refs/heads/main'), + startsWith(variables['Build.SourceBranch'], 'refs/tags/') + ), + contains(variables['System.TeamFoundationCollectionUri'], 'viscalyx') + ) + jobs: + - job: Deploy_Module + displayName: 'Deploy Module' + pool: + vmImage: 'ubuntu-latest' + steps: + - task: DownloadPipelineArtifact@2 + displayName: 'Download Build Artifact' + inputs: + buildType: 'current' + artifactName: $(buildArtifactName) + targetPath: '$(Build.SourcesDirectory)/$(buildFolderName)' + - task: PowerShell@2 + name: publishRelease + displayName: 'Publish Release' + inputs: + filePath: './build.ps1' + arguments: '-tasks publish' + pwsh: true + env: + GitHubToken: $(GitHubToken) + GalleryApiToken: $(GalleryApiToken) + ReleaseBranch: $(defaultBranch) + MainGitBranch: $(defaultBranch) + - task: PowerShell@2 + name: sendChangelogPR + displayName: 'Send Changelog PR' + inputs: + filePath: './build.ps1' + arguments: '-tasks Create_ChangeLog_GitHub_PR' + pwsh: true + env: + GitHubToken: $(GitHubToken) + ReleaseBranch: $(defaultBranch) + MainGitBranch: $(defaultBranch) diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..f4a0fae --- /dev/null +++ b/build.ps1 @@ -0,0 +1,538 @@ +<# + .DESCRIPTION + Bootstrap and build script for PowerShell module CI/CD pipeline. + + .PARAMETER Tasks + The task or tasks to run. The default value is '.' (runs the default task). + + .PARAMETER CodeCoverageThreshold + The code coverage target threshold to uphold. Set to 0 to disable. + The default value is '' (empty string). + + .PARAMETER BuildConfig + Not yet written. + + .PARAMETER OutputDirectory + Specifies the folder to build the artefact into. The default value is 'output'. + + .PARAMETER BuiltModuleSubdirectory + Subdirectory name to build the module (under $OutputDirectory). The default + value is '' (empty string). + + .PARAMETER RequiredModulesDirectory + Can be a path (relative to $PSScriptRoot or absolute) to tell Resolve-Dependency + and PSDepend where to save the required modules. It is also possible to use + 'CurrentUser' och 'AllUsers' to install missing dependencies. You can override + the value for PSDepend in the Build.psd1 build manifest. The default value is + 'output/RequiredModules'. + + .PARAMETER PesterScript + One or more paths that will override the Pester configuration in build + configuration file when running the build task Invoke_Pester_Tests. + + If running Pester 5 test, use the alias PesterPath to be future-proof. + + .PARAMETER PesterTag + Filter which tags to run when invoking Pester tests. This is used in the + Invoke-Pester.pester.build.ps1 tasks. + + .PARAMETER PesterExcludeTag + Filter which tags to exclude when invoking Pester tests. This is used in + the Invoke-Pester.pester.build.ps1 tasks. + + .PARAMETER DscTestTag + Filter which tags to run when invoking DSC Resource tests. This is used + in the DscResource.Test.build.ps1 tasks. + + .PARAMETER DscTestExcludeTag + Filter which tags to exclude when invoking DSC Resource tests. This is + used in the DscResource.Test.build.ps1 tasks. + + .PARAMETER ResolveDependency + Not yet written. + + .PARAMETER BuildInfo + The build info object from ModuleBuilder. Defaults to an empty hashtable. + + .PARAMETER AutoRestore + Not yet written. + + .PARAMETER UseModuleFast + Specifies to use ModuleFast instead of PowerShellGet to resolve dependencies + faster. + + .PARAMETER UsePSResourceGet + Specifies to use PSResourceGet instead of PowerShellGet to resolve dependencies + faster. This can also be configured in Resolve-Dependency.psd1. + + .PARAMETER UsePowerShellGetCompatibilityModule + Specifies to use the compatibility module PowerShellGet. This parameter + only works then the method of downloading dependencies is PSResourceGet. + This can also be configured in Resolve-Dependency.psd1. +#> +[CmdletBinding()] +param +( + [Parameter(Position = 0)] + [System.String[]] + $Tasks = '.', + + [Parameter()] + [System.String] + $CodeCoverageThreshold = '', + + [Parameter()] + [System.String] + [ValidateScript( + { Test-Path -Path $_ } + )] + $BuildConfig, + + [Parameter()] + [System.String] + $OutputDirectory = 'output', + + [Parameter()] + [System.String] + $BuiltModuleSubdirectory = '', + + [Parameter()] + [System.String] + $RequiredModulesDirectory = $(Join-Path 'output' 'RequiredModules'), + + [Parameter()] + # This alias is to prepare for the rename of this parameter to PesterPath when Pester 4 support is removed + [Alias('PesterPath')] + [System.Object[]] + $PesterScript, + + [Parameter()] + [System.String[]] + $PesterTag, + + [Parameter()] + [System.String[]] + $PesterExcludeTag, + + [Parameter()] + [System.String[]] + $DscTestTag, + + [Parameter()] + [System.String[]] + $DscTestExcludeTag, + + [Parameter()] + [Alias('bootstrap')] + [System.Management.Automation.SwitchParameter] + $ResolveDependency, + + [Parameter(DontShow)] + [AllowNull()] + [System.Collections.Hashtable] + $BuildInfo, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $AutoRestore, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $UseModuleFast, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $UsePSResourceGet, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $UsePowerShellGetCompatibilityModule +) + +<# + The BEGIN block (at the end of this file) handles the Bootstrap of the Environment + before Invoke-Build can run the tasks if the parameter ResolveDependency (or + parameter alias Bootstrap) is specified. +#> + +process +{ + if ($MyInvocation.ScriptName -notLike '*Invoke-Build.ps1') + { + # Only run the process block through InvokeBuild (look at the Begin block at the bottom of this script). + return + } + + # Execute the Build process from the .build.ps1 path. + Push-Location -Path $PSScriptRoot -StackName 'BeforeBuild' + + try + { + Write-Host -Object "[build] Parsing defined tasks" -ForeGroundColor Magenta + + # Load the default BuildInfo if the parameter BuildInfo is not set. + if (-not $PSBoundParameters.ContainsKey('BuildInfo')) + { + try + { + if (Test-Path -Path $BuildConfig) + { + $configFile = Get-Item -Path $BuildConfig + + Write-Host -Object "[build] Loading Configuration from $configFile" + + $BuildInfo = switch -Regex ($configFile.Extension) + { + # Native Support for PSD1 + '\.psd1' + { + if (-not (Get-Command -Name Import-PowerShellDataFile -ErrorAction SilentlyContinue)) + { + Import-Module -Name Microsoft.PowerShell.Utility -RequiredVersion 3.1.0.0 + } + + Import-PowerShellDataFile -Path $BuildConfig + } + + # Support for yaml when module PowerShell-Yaml is available + '\.[yaml|yml]' + { + Import-Module -Name 'powershell-yaml' -ErrorAction Stop + + ConvertFrom-Yaml -Yaml (Get-Content -Raw $configFile) + } + + # Support for JSON and JSONC (by Removing comments) when module PowerShell-Yaml is available + '\.[json|jsonc]' + { + $jsonFile = Get-Content -Raw -Path $configFile + + $jsonContent = $jsonFile -replace '(?m)\s*//.*?$' -replace '(?ms)/\*.*?\*/' + + # Yaml is superset of JSON. + ConvertFrom-Yaml -Yaml $jsonContent + } + + # Unknown extension, return empty hashtable. + default + { + Write-Error -Message "Extension '$_' not supported. using @{}" + + @{ } + } + } + } + else + { + Write-Host -Object "Configuration file '$($BuildConfig.FullName)' not found" -ForegroundColor Red + + # No config file was found, return empty hashtable. + $BuildInfo = @{ } + } + } + catch + { + $logMessage = "Error loading Config '$($BuildConfig.FullName)'.`r`nAre you missing dependencies?`r`nMake sure you run './build.ps1 -ResolveDependency -tasks noop' before running build to restore the required modules." + + Write-Host -Object $logMessage -ForegroundColor Yellow + + $BuildInfo = @{ } + + Write-Error -Message $_.Exception.Message + } + } + + # If the Invoke-Build Task Header is specified in the Build Info, set it. + if ($BuildInfo.TaskHeader) + { + Set-BuildHeader -Script ([scriptblock]::Create($BuildInfo.TaskHeader)) + } + + <# + Add BuildModuleOutput to PSModule Path environment variable. + Moved here (not in begin block) because build file can contains BuiltSubModuleDirectory value. + #> + if ($BuiltModuleSubdirectory) + { + if (-not (Split-Path -IsAbsolute -Path $BuiltModuleSubdirectory)) + { + $BuildModuleOutput = Join-Path -Path $OutputDirectory -ChildPath $BuiltModuleSubdirectory + } + else + { + $BuildModuleOutput = $BuiltModuleSubdirectory + } + } # test if BuiltModuleSubDirectory set in build config file + elseif ($BuildInfo.ContainsKey('BuiltModuleSubDirectory')) + { + $BuildModuleOutput = Join-Path -Path $OutputDirectory -ChildPath $BuildInfo['BuiltModuleSubdirectory'] + } + else + { + $BuildModuleOutput = $OutputDirectory + } + + # Pre-pending $BuildModuleOutput folder to PSModulePath to resolve built module from this folder. + if ($powerShellModulePaths -notcontains $BuildModuleOutput) + { + Write-Host -Object "[build] Pre-pending '$BuildModuleOutput' folder to PSModulePath" -ForegroundColor Green + + $env:PSModulePath = $BuildModuleOutput + [System.IO.Path]::PathSeparator + $env:PSModulePath + } + + <# + Import Tasks from modules via their exported aliases when defined in Build Manifest. + https://github.com/nightroman/Invoke-Build/tree/master/Tasks/Import#example-2-import-from-a-module-with-tasks + #> + if ($BuildInfo.ContainsKey('ModuleBuildTasks')) + { + foreach ($module in $BuildInfo['ModuleBuildTasks'].Keys) + { + try + { + Write-Host -Object "Importing tasks from module $module" -ForegroundColor DarkGray + + $loadedModule = Import-Module -Name $module -PassThru -ErrorAction Stop + + foreach ($TaskToExport in $BuildInfo['ModuleBuildTasks'].($module)) + { + $loadedModule.ExportedAliases.GetEnumerator().Where{ + Write-Host -Object "`t Loading $($_.Key)..." -ForegroundColor DarkGray + + # Using -like to support wildcard. + $_.Key -like $TaskToExport + }.ForEach{ + # Dot-sourcing the Tasks via their exported aliases. + . (Get-Alias $_.Key) + } + } + } + catch + { + Write-Host -Object "Could not load tasks for module $module." -ForegroundColor Red + + Write-Error -Message $_ + } + } + } + + # Loading Build Tasks defined in the .build/ folder (will override the ones imported above if same task name). + Get-ChildItem -Path '.build/' -Recurse -Include '*.ps1' -ErrorAction Ignore | + ForEach-Object { + "Importing file $($_.BaseName)" | Write-Verbose + + . $_.FullName + } + + # Synopsis: Empty task, useful to test the bootstrap process. + task noop { } + + # Define default task sequence ("."), can be overridden in the $BuildInfo. + task . { + Write-Build -Object 'No sequence currently defined for the default task' -ForegroundColor Yellow + } + + Write-Host -Object 'Adding Workflow from configuration:' -ForegroundColor DarkGray + + # Load Invoke-Build task sequences/workflows from $BuildInfo. + foreach ($workflow in $BuildInfo.BuildWorkflow.keys) + { + Write-Verbose -Message "Creating Build Workflow '$Workflow' with tasks $($BuildInfo.BuildWorkflow.($Workflow) -join ', ')." + + $workflowItem = $BuildInfo.BuildWorkflow.($workflow) + + if ($workflowItem.Trim() -match '^\{(?[\w\W]*)\}$') + { + $workflowItem = [ScriptBlock]::Create($Matches['sb']) + } + + Write-Host -Object " +-> $workflow" -ForegroundColor DarkGray + + task $workflow $workflowItem + } + + Write-Host -Object "[build] Executing requested workflow: $($Tasks -join ', ')" -ForeGroundColor Magenta + + } + finally + { + Pop-Location -StackName 'BeforeBuild' + } +} + +begin +{ + # Find build config if not specified. + if (-not $BuildConfig) + { + $config = Get-ChildItem -Path "$PSScriptRoot\*" -Include 'build.y*ml', 'build.psd1', 'build.json*' -ErrorAction Ignore + + if (-not $config -or ($config -is [System.Array] -and $config.Length -le 0)) + { + throw 'No build configuration found. Specify path via parameter BuildConfig.' + } + elseif ($config -is [System.Array]) + { + if ($config.Length -gt 1) + { + throw 'More than one build configuration found. Specify which path to use via parameter BuildConfig.' + } + + $BuildConfig = $config[0] + } + else + { + $BuildConfig = $config + } + } + + # Bootstrapping the environment before using Invoke-Build as task runner + + if ($MyInvocation.ScriptName -notlike '*Invoke-Build.ps1') + { + Write-Host -Object "[pre-build] Starting Build Init" -ForegroundColor Green + + Push-Location $PSScriptRoot -StackName 'BuildModule' + } + + if ($RequiredModulesDirectory -in @('CurrentUser', 'AllUsers')) + { + # Installing modules instead of saving them. + Write-Host -Object "[pre-build] Required Modules will be installed to the PowerShell module path that is used for $RequiredModulesDirectory." -ForegroundColor Green + + <# + The variable $PSDependTarget will be used below when building the splatting + variable before calling Resolve-Dependency.ps1, unless overridden in the + file Resolve-Dependency.psd1. + #> + $PSDependTarget = $RequiredModulesDirectory + } + else + { + if (-not (Split-Path -IsAbsolute -Path $OutputDirectory)) + { + $OutputDirectory = Join-Path -Path $PSScriptRoot -ChildPath $OutputDirectory + } + + # Resolving the absolute path to save the required modules to. + if (-not (Split-Path -IsAbsolute -Path $RequiredModulesDirectory)) + { + $RequiredModulesDirectory = Join-Path -Path $PSScriptRoot -ChildPath $RequiredModulesDirectory + } + + # Create the output/modules folder if not exists, or resolve the Absolute path otherwise. + if (Resolve-Path -Path $RequiredModulesDirectory -ErrorAction SilentlyContinue) + { + Write-Debug -Message "[pre-build] Required Modules path already exist at $RequiredModulesDirectory" + + $requiredModulesPath = Convert-Path -Path $RequiredModulesDirectory + } + else + { + Write-Host -Object "[pre-build] Creating required modules directory $RequiredModulesDirectory." -ForegroundColor Green + + $requiredModulesPath = (New-Item -ItemType Directory -Force -Path $RequiredModulesDirectory).FullName + } + + $powerShellModulePaths = $env:PSModulePath -split [System.IO.Path]::PathSeparator + + # Pre-pending $requiredModulesPath folder to PSModulePath to resolve from this folder FIRST. + if ($RequiredModulesDirectory -notin @('CurrentUser', 'AllUsers') -and + ($powerShellModulePaths -notcontains $RequiredModulesDirectory)) + { + Write-Host -Object "[pre-build] Pre-pending '$RequiredModulesDirectory' folder to PSModulePath" -ForegroundColor Green + + $env:PSModulePath = $RequiredModulesDirectory + [System.IO.Path]::PathSeparator + $env:PSModulePath + } + + $powerShellYamlModule = Get-Module -Name 'powershell-yaml' -ListAvailable + $invokeBuildModule = Get-Module -Name 'InvokeBuild' -ListAvailable + $psDependModule = Get-Module -Name 'PSDepend' -ListAvailable + + # Checking if the user should -ResolveDependency. + if (-not ($powerShellYamlModule -and $invokeBuildModule -and $psDependModule) -and -not $ResolveDependency) + { + if ($AutoRestore -or -not $PSBoundParameters.ContainsKey('Tasks') -or $Tasks -contains 'build') + { + Write-Host -Object "[pre-build] Dependency missing, running './build.ps1 -ResolveDependency -Tasks noop' for you `r`n" -ForegroundColor Yellow + + $ResolveDependency = $true + } + else + { + Write-Warning -Message "Some required Modules are missing, make sure you first run with the '-ResolveDependency' parameter. Running 'build.ps1 -ResolveDependency -Tasks noop' will pull required modules without running the build task." + } + } + + <# + The variable $PSDependTarget will be used below when building the splatting + variable before calling Resolve-Dependency.ps1, unless overridden in the + file Resolve-Dependency.psd1. + #> + $PSDependTarget = $requiredModulesPath + } + + if ($ResolveDependency) + { + Write-Host -Object "[pre-build] Resolving dependencies using preferred method." -ForegroundColor Green + + $resolveDependencyParams = @{ } + + # If BuildConfig is a Yaml file, bootstrap powershell-yaml via ResolveDependency. + if ($BuildConfig -match '\.[yaml|yml]$') + { + $resolveDependencyParams.Add('WithYaml', $true) + } + + $resolveDependencyAvailableParams = (Get-Command -Name '.\Resolve-Dependency.ps1').Parameters.Keys + + foreach ($cmdParameter in $resolveDependencyAvailableParams) + { + # The parameter has been explicitly used for calling the .build.ps1 + if ($MyInvocation.BoundParameters.ContainsKey($cmdParameter)) + { + $paramValue = $MyInvocation.BoundParameters.Item($cmdParameter) + + Write-Debug " adding $cmdParameter :: $paramValue [from user-provided parameters to Build.ps1]" + + $resolveDependencyParams.Add($cmdParameter, $paramValue) + } + # Use defaults parameter value from Build.ps1, if any + else + { + $paramValue = Get-Variable -Name $cmdParameter -ValueOnly -ErrorAction Ignore + + if ($paramValue) + { + Write-Debug " adding $cmdParameter :: $paramValue [from default Build.ps1 variable]" + + $resolveDependencyParams.Add($cmdParameter, $paramValue) + } + } + } + + Write-Host -Object "[pre-build] Starting bootstrap process." -ForegroundColor Green + + .\Resolve-Dependency.ps1 @resolveDependencyParams + } + + if ($MyInvocation.ScriptName -notlike '*Invoke-Build.ps1') + { + Write-Verbose -Message "Bootstrap completed. Handing back to InvokeBuild." + + if ($PSBoundParameters.ContainsKey('ResolveDependency')) + { + Write-Verbose -Message "Dependency already resolved. Removing task." + + $null = $PSBoundParameters.Remove('ResolveDependency') + } + + Write-Host -Object "[build] Starting build with InvokeBuild." -ForegroundColor Green + + Invoke-Build @PSBoundParameters -Task $Tasks -File $MyInvocation.MyCommand.Path + + Pop-Location -StackName 'BuildModule' + + return + } +} diff --git a/build.yaml b/build.yaml new file mode 100644 index 0000000..8ffde41 --- /dev/null +++ b/build.yaml @@ -0,0 +1,203 @@ +--- +#################################################### +# Pipeline Build Task Configuration (Invoke-Build) # +#################################################### +BuildWorkflow: + '.': + - build + - test + + build: + - Remove_BuiltModule_From_Session + - Clean + - Build_Module_ModuleBuilder + - Build_NestedModules_ModuleBuilder + - Create_Changelog_Release_Output + + docs: + - Generate_Conceptual_Help + - Generate_Wiki_Content + - Generate_Wiki_Sidebar + - Clean_Markdown_Metadata + - Package_Wiki_Content + + pack: + - build + - docs + - package_module_nupkg # cSpell: disable-line + + hqrmtest: # cSpell: disable-line + - Invoke_HQRM_Tests_Stop_On_Fail + + test: + - Pester_Tests_Stop_On_Fail + - Convert_Pester_Coverage + - Pester_If_Code_Coverage_Under_Threshold + + merge: + - Merge_CodeCoverage_Files + + publish: + - Publish_Release_To_GitHub + - Publish_Module_To_gallery + - Publish_GitHub_Wiki_Content + + Remove_BuiltModule_From_Session: | + { + Remove-Module -Name (Get-SamplerProjectName -BuildRoot .) -ErrorAction SilentlyContinue + } + +#################################################### +# ModuleBuilder Configuration # +#################################################### +CopyPaths: + - en-US +Prefix: prefix.ps1 +Encoding: UTF8 +VersionedOutputDirectory: true +BuiltModuleSubdirectory: builtModule + +ModuleBuildTasks: + Sampler: + - '*.build.Sampler.ib.tasks' + Sampler.GitHubTasks: + - '*.ib.tasks' + DscResource.DocGenerator: + - 'Task.*' + DscResource.Test: + - 'Task.*' + +TaskHeader: | + param($Path) + "" + "=" * 79 + Write-Build Cyan "`t`t`t$($Task.Name.replace("_"," ").ToUpper())" + Write-Build DarkGray "$(Get-BuildSynopsis $Task)" + "-" * 79 + Write-Build DarkGray " $Path" + Write-Build DarkGray " $($Task.InvocationInfo.ScriptName):$($Task.InvocationInfo.ScriptLineNumber)" + "" + +#################################################### +# Dependent Modules Configuration (Sampler) # +#################################################### +NestedModule: + DscResource.Common: + CopyOnly: true + Path: ./output/RequiredModules/DscResource.Common + AddToManifest: false + Exclude: PSGetModuleInfo.xml + + Viscalyx.Common: + CopyOnly: true + Path: ./output/RequiredModules/Viscalyx.Common + AddToManifest: false + Exclude: PSGetModuleInfo.xml + +#################################################### +# Pester Configuration (Sampler) # +#################################################### +Pester: + Configuration: + Run: + Path: + - tests/Unit + Output: + Verbosity: Detailed + StackTraceVerbosity: Full + CIFormat: Auto + CodeCoverage: + CoveragePercentTarget: 80 + OutputEncoding: ascii + UseBreakpoints: false + TestResult: + OutputFormat: NUnitXML + OutputEncoding: ascii + ExcludeFromCodeCoverage: + - Modules/DscResource.Common + - Modules/Viscalyx.Common + +#################################################### +# Pester Configuration (DscResource.Test) # +#################################################### +DscTest: + Pester: + Configuration: + Filter: + ExcludeTag: + - "Common Tests - New Error-Level Script Analyzer Rules" + - "Common Tests - Validate Localization" # TODO: Remove this when tests have been converted to v6 + Output: + Verbosity: Detailed + CIFormat: Auto + TestResult: + Enabled: true + OutputFormat: NUnitXML + OutputEncoding: ascii + OutputPath: ./output/testResults/NUnitXml_HQRM_Tests.xml + Script: + ExcludeSourceFile: + - output + ExcludeModuleFile: + - Modules/DscResource.Common + - Modules/Viscalyx.Common + MainGitBranch: main + +#################################################### +# Code Coverage Configuration # +#################################################### + +CodeCoverage: + # Filename of the file that will be outputted by the task Merge_CodeCoverage_Files. + CodeCoverageMergedOutputFile: JaCoCo_coverage.xml + # File pattern used to search for files under the ./output/testResults folder + # by task Merge_CodeCoverage_Files. + CodeCoverageFilePattern: Codecov*.xml + +#################################################### +# PSDepend Configuration # +#################################################### +Resolve-Dependency: + Gallery: 'PSGallery' + AllowPrerelease: false + Verbose: false + +#################################################### +# GitHub Configuration # +#################################################### +GitHubConfig: + GitHubFilesToAdd: + - 'CHANGELOG.md' + ReleaseAssets: + - output/WikiContent.zip + GitHubConfigUserName: viscalyxbot # cSpell: disable-line + GitHubConfigUserEmail: viscalyxbot@viscalyx.se + UpdateChangelogOnPrerelease: false + +#################################################### +# DscResource.DocGenerator Configuration # +#################################################### +DscResource.DocGenerator: + Generate_Conceptual_Help: + MarkdownCodeRegularExpression: + - '\`(.+?)\`' # Match inline code-block + - '\\(\\)' # Match escaped backslash + - '\[[^\[]+\]\((.+?)\)' # Match markdown URL + - '_(.+?)_' # Match Italic (underscore) + - '\*\*(.+?)\*\*' # Match bold + - '\*(.+?)\*' # Match Italic (asterisk) + Publish_GitHub_Wiki_Content: + Debug: false + Generate_Wiki_Content: + MofResourceMetadata: + Type: MofResource + Category: MOF-based Resources + ClassResourceMetadata: + Type: ClassResource + Category: Class-based resources + CompositeResourceMetadata: + Type: CompositeResource + Category: Composite Resources + Generate_Wiki_Sidebar: + Debug: false + AlwaysOverwrite: true diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..ae1a63c --- /dev/null +++ b/codecov.yml @@ -0,0 +1,25 @@ +codecov: + require_ci_to_pass: no + # master should be the baseline for reporting + branch: main + +comment: + layout: "reach, diff, flags, files" + behavior: default + +coverage: + range: 50..80 + round: down + precision: 0 + + status: + project: + default: + # Set the overall project code coverage requirement to 70% + target: 70 + patch: + default: + # Set the pull request requirement to not regress overall coverage by more than 5% + # and let codecov.io set the goal for the code changed in the patch. + target: auto + threshold: 5 diff --git a/source/Public/Assert-BlockString.ps1 b/source/Public/Assert-BlockString.ps1 new file mode 100644 index 0000000..167e003 --- /dev/null +++ b/source/Public/Assert-BlockString.ps1 @@ -0,0 +1,107 @@ +<# + .SYNOPSIS + Asserts that a string, here-string or array of strings matches the expected + string, here-string or array of strings. + + .DESCRIPTION + The `Assert-BlockString` command compares a string, here-string or array + of strings with the expected string, here-string or array of strings and + throws an error that includes the hex output if they are not equal. The + comparison is case sensitive. It is commonly used in unit testing scenarios + to verify the correctness of string outputs on byte level. + + .PARAMETER Actual + The actual string, here-string or array of strings to be compared with the + expected value. This parameter accepts pipeline input. + + .PARAMETER Expected + The expected string, here-string or array of strings that the actual value + should match. + + .PARAMETER Because + An optional reason or explanation for the assertion. + + .PARAMETER DifferenceAnsi + An optional ANSI color code to highlight the difference between the expected + and actual strings. The default value is '31m' (red text). + + .EXAMPLE + PS> Assert-BlockString -Actual 'hello', 'world' -Expected 'Hello', 'World' + + This example asserts that the array of strings 'hello' and 'world' matches + the expected array of strings 'Hello' and 'World'. If the assertion fails, + an error is thrown. + + .EXAMPLE + PS> 'hello', 'world' | Assert-BlockString -Expected 'Hello', 'World' + + This example demonstrates the usage of pipeline input. The block of strings + 'Hello' and 'World' is piped to `Assert-BlockString` and compared with the + expected strings 'Hello' and 'World'. If the assertion fails, an error is + thrown. + + .NOTES + TODO: Is it possible to rename command to `Should-BeBlockString`. Pester handles Should verb with, not sure it possible to resolve here: + https://github.com/pester/Pester/commit/c8bc9679bed19c8fbc4229caa01dd083f2d03d4f#diff-b7592dd925696de2521c9b12b966d65519d502045462f002c343caa7c0986936 + and + https://github.com/pester/Pester/commit/c8bc9679bed19c8fbc4229caa01dd083f2d03d4f#diff-460f64eafc16facefbed201eb00fb151c75eadf7cc58a504a01527015fb1c7cdR17 +#> +function Assert-BlockString +{ + [Alias('Should-BeBlockString')] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseProcessBlockForPipelineCommand', '')] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('AvoidThrowOutsideOfTry', '')] + param + ( + [Parameter(Position = 1, ValueFromPipeline = $true)] + $Actual, + + [Parameter(Position = 0, Mandatory = $true)] + [System.String[]] + $Expected, + + [Parameter()] + [System.String] + $Because, + + [Parameter()] + [System.String] + $DifferenceAnsi = '31m' + ) + + $hasPipelineInput = $MyInvocation.ExpectingInput + + if ($hasPipelineInput) + { + $Actual = @($local:Input) + } + + # Verify if $Actual is a string or string array + $isStringType = $Actual -is [System.String] -or ($Actual -is [System.Array] -and $Actual[0] -is [System.String]) + + $stringsAreEqual = $isStringType -and (-join $Actual) -ceq (-join $Expected) + + if (-not $stringsAreEqual) + { + if (-not $isStringType) + { + $message = 'Expected to actual value to be of type string or string[], but it was not.' + } + else + { + $message = 'Expect the strings to be equal' + + if ($Because) + { + $message += " because $Because" + } + + $message += ", but they were not. Difference is highlighted:`r`n" + + $message += Out-Diff -Reference $Expected -Difference $Actual -ReferenceLabel 'Expected' -DifferenceLabel 'Actual' -DifferenceAnsi:$DifferenceAnsi -PassThru | + ForEach-Object { "`e[0m$_`r`n" } + } + + throw [Pester.Factory]::CreateShouldErrorRecord($Message, $MyInvocation.ScriptName, $MyInvocation.ScriptLineNumber, $MyInvocation.Line.TrimEnd([System.Environment]::NewLine), $true) + } +} diff --git a/source/Viscalyx.Assert.psd1 b/source/Viscalyx.Assert.psd1 new file mode 100644 index 0000000..8bf2ec3 --- /dev/null +++ b/source/Viscalyx.Assert.psd1 @@ -0,0 +1,66 @@ +@{ + # Script module or binary module file associated with this manifest. + RootModule = 'Viscalyx.Assert.psm1' + + # Version number of this module. + ModuleVersion = '0.0.1' + + # ID used to uniquely identify this module + GUID = 'aba638ad-a584-4234-8eaa-48691b21be2f' + + # Author of this module + Author = 'Viscalyx' # cSpell: ignore Viscalyx + + # Company or vendor of this module + CompanyName = 'Viscalyx' + + # Copyright statement for this module + Copyright = 'Copyright the Viscalyx.Assert contributors. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'Assertions for Pester that can improve your tests.' + + # Minimum version of the PowerShell engine required by this module + PowerShellVersion = '5.1' + + # Modules that must be imported into the global environment prior to importing this module + RequiredModules = @() + + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = @() + + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = @() + + # Variables to export from this module + VariablesToExport = @() + + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + AliasesToExport = @() + + # DSC resources to export from this module + DscResourcesToExport = @() + + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + PSData = @{ + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('Pester', 'Assertion', 'Testing', 'TDD', 'BDD', 'Assert', 'PesterAssertion') + + # A URL to the license for this module. + LicenseUri = 'https://github.com/viscalyx/Viscalyx.Assert/blob/main/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/viscalyx/Viscalyx.Assert' + + # A URL to an icon representing this module. + IconUri = 'https://avatars.githubusercontent.com/u/53994072' + + # ReleaseNotes of this module + ReleaseNotes = '' + + # Prerelease string of this module + Prerelease = '' + } # End of PSData hashtable + } # End of PrivateData hashtable +} diff --git a/source/Viscalyx.Assert.psm1 b/source/Viscalyx.Assert.psm1 new file mode 100644 index 0000000..81d556e --- /dev/null +++ b/source/Viscalyx.Assert.psm1 @@ -0,0 +1,4 @@ +<# + This file is intentionally left empty. It is must be left here for the module + manifest to refer to. It is recreated during the build process. +#> diff --git a/source/WikiSource/Home.md b/source/WikiSource/Home.md new file mode 100644 index 0000000..8b29459 --- /dev/null +++ b/source/WikiSource/Home.md @@ -0,0 +1,28 @@ +# Welcome to the Viscalyx.Assert wiki + +*Viscalyx.Assert v#.#.#* + +Here you will find all the information you need to make use of the Viscalyx.Assert. + +Please leave comments, feature requests, and bug reports for this module in +the [issues section](https://github.com/viscalyx/Viscalyx.Assert/issues) +for this repository. + +## Getting started + +To get started either: + +- Install from the PowerShell Gallery using PowerShellGet by running the + following command: + +```powershell +Install-Module -Name Viscalyx.Assert -Repository PSGallery +``` + +## Prerequisites + +- Powershell 5.1 or higher + +## Change log + +A full list of changes in each version can be found in the [change log](https://github.com/viscalyx/Viscalyx.Assert/blob/main/CHANGELOG.md). diff --git a/source/en-US/Viscalyx.Common.strings.psd1 b/source/en-US/Viscalyx.Common.strings.psd1 new file mode 100644 index 0000000..a398da8 --- /dev/null +++ b/source/en-US/Viscalyx.Common.strings.psd1 @@ -0,0 +1,35 @@ +<# + .SYNOPSIS + The localized resource strings in English (en-US). This file should only + contain localized strings for private functions, public command, and + classes (that are not a DSC resource). +#> + +ConvertFrom-StringData @' + ## Remove-History + Convert_PesterSyntax_ShouldProcessVerboseDescription = Converting the script file '{0}'. + Convert_PesterSyntax_ShouldProcessVerboseWarning = Are you sure you want to convert the script file '{0}'? + # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. + Convert_PesterSyntax_ShouldProcessCaption = Convert script file + + New_SamplerGitHubReleaseTag_Rebase_ShouldProcessVerboseDescription = Switching to and rebasing the local default branch '{0}' from the upstream default branch '{0}'. + New_SamplerGitHubReleaseTag_Rebase_ShouldProcessVerboseWarning = Are you sure you want switch to and rebase the local branch '{0}'? + # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. + New_SamplerGitHubReleaseTag_Rebase_ShouldProcessCaption = Rebase the local default branch + + New_SamplerGitHubReleaseTag_UpstreamTags_ShouldProcessVerboseDescription = Fetching the tags from (upstream) remote '{0}'. + New_SamplerGitHubReleaseTag_UpstreamTags_ShouldProcessVerboseWarning = Are you sure you want to fetch tags from remote '{0}'? + # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. + New_SamplerGitHubReleaseTag_UpstreamTags_ShouldProcessCaption = Fetch tags from remote + + New_SamplerGitHubReleaseTag_NewTag_ShouldProcessVerboseDescription = Creating tag '{0}' for commit '{2}' in the local branch '{1}'. + New_SamplerGitHubReleaseTag_NewTagWhatIf_ShouldProcessVerboseDescription = Creating tag for commit in the local branch '{1}'. Note: Actual tag name and commit id cannot be determine during -WhatIf. + New_SamplerGitHubReleaseTag_NewTag_ShouldProcessVerboseWarning = Are you sure you want to create tag '{0}'? + # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. + New_SamplerGitHubReleaseTag_NewTag_ShouldProcessCaption = Create tag + + New_SamplerGitHubReleaseTag_SwitchBack_ShouldProcessVerboseDescription = Switching back to previous local branch '{0}'. + New_SamplerGitHubReleaseTag_SwitchBack_ShouldProcessVerboseWarning = Are you sure you want to switch back to previous local branch '{0}'? + # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. + New_SamplerGitHubReleaseTag_SwitchBack_ShouldProcessCaption = Switch to previous branch +'@ diff --git a/source/en-US/about_Viscalyx.Common.help.txt b/source/en-US/about_Viscalyx.Common.help.txt new file mode 100644 index 0000000..9f5bac9 --- /dev/null +++ b/source/en-US/about_Viscalyx.Common.help.txt @@ -0,0 +1,24 @@ +TOPIC + about_Viscalyx.Assert + +SHORT DESCRIPTION + Assertions for Pester that can improve your tests. + +LONG DESCRIPTION + Assertions for Pester that can improve your tests. + +EXAMPLES + PS C:\> Get-Commands -Module Viscalyx.Assert + +NOTE: + Thank you to all those who contributed to this module, by writing code, sharing opinions, and provided feedback. + +TROUBLESHOOTING NOTE: + Go to the Github repository for read about issues, submit a new issue, and read + about new releases. https://github.com/viscalyx/Viscalyx.Assert + +SEE ALSO + - https://github.com/viscalyx/Viscalyx.Assert + +KEYWORDS + Pester, Converter diff --git a/source/prefix.ps1 b/source/prefix.ps1 new file mode 100644 index 0000000..1a1f20d --- /dev/null +++ b/source/prefix.ps1 @@ -0,0 +1,7 @@ +$script:dscResourceCommonModulePath = Join-Path -Path $PSScriptRoot -ChildPath 'Modules/DscResource.Common' +Import-Module -Name $script:dscResourceCommonModulePath + +$script:viscalyxCommonModulePath = Join-Path -Path $PSScriptRoot -ChildPath 'Modules/Viscalyx.Common' +Import-Module -Name $script:viscalyxCommonModulePath + +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' diff --git a/tests/QA/module.tests.ps1 b/tests/QA/module.tests.ps1 new file mode 100644 index 0000000..439914c --- /dev/null +++ b/tests/QA/module.tests.ps1 @@ -0,0 +1,243 @@ +BeforeDiscovery { + $projectPath = "$($PSScriptRoot)\..\.." | Convert-Path + + <# + If the QA tests are run outside of the build script (e.g with Invoke-Pester) + the parent scope has not set the variable $ProjectName. + #> + if (-not $ProjectName) + { + # Assuming project folder name is project name. + $ProjectName = Get-SamplerProjectName -BuildRoot $projectPath + } + + $script:moduleName = $ProjectName + + Remove-Module -Name $script:moduleName -Force -ErrorAction SilentlyContinue + + $mut = Get-Module -Name $script:moduleName -ListAvailable | + Select-Object -First 1 | + Import-Module -Force -ErrorAction Stop -PassThru +} + +BeforeAll { + # Convert-Path required for PS7 or Join-Path fails + $projectPath = "$($PSScriptRoot)\..\.." | Convert-Path + + <# + If the QA tests are run outside of the build script (e.g with Invoke-Pester) + the parent scope has not set the variable $ProjectName. + #> + if (-not $ProjectName) + { + # Assuming project folder name is project name. + $ProjectName = Get-SamplerProjectName -BuildRoot $projectPath + } + + $script:moduleName = $ProjectName + + $sourcePath = ( + Get-ChildItem -Path $projectPath\*\*.psd1 | + Where-Object -FilterScript { + ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) ` + -and $( + try + { + Test-ModuleManifest -Path $_.FullName -ErrorAction Stop + } + catch + { + $false + } + ) + } + ).Directory.FullName +} + +Describe 'Changelog Management' -Tag 'Changelog' { + It 'Changelog has been updated' -Skip:( + -not ([bool](Get-Command git -ErrorAction SilentlyContinue) -and + [bool](&(Get-Process -Id $PID).Path -NoProfile -Command 'git rev-parse --is-inside-work-tree 2>$null')) + ) { + <# + Get the list of changed files compared with branch main to verify + that required files are changed. + #> + + # Only run if there is a remote called origin + if (((git remote) -match 'origin')) + { + $headCommit = &git rev-parse HEAD + $defaultBranchCommit = &git rev-parse origin/main + $filesChanged = &git @('diff', "$defaultBranchCommit...$headCommit", '--name-only') + } + + $filesStagedAndUnstaged = &git @('diff', 'HEAD', '--name-only') 2>&1 + + $filesChanged += $filesStagedAndUnstaged + + # Only check if there are any changed files. + if ($filesChanged) + { + $filesChanged | Should -Contain 'CHANGELOG.md' -Because 'the CHANGELOG.md must be updated with at least one entry in the Unreleased section for each PR' + } + } + + It 'Changelog format compliant with keepachangelog format' -Skip:(![bool](Get-Command git -EA SilentlyContinue)) { + { Get-ChangelogData -Path (Join-Path $ProjectPath 'CHANGELOG.md') -ErrorAction Stop } | Should -Not -Throw + } + + It 'Changelog should have an Unreleased header' -Skip:$skipTest { + (Get-ChangelogData -Path (Join-Path -Path $ProjectPath -ChildPath 'CHANGELOG.md') -ErrorAction Stop).Unreleased | Should -Not -BeNullOrEmpty + } +} + +Describe 'General module control' -Tags 'FunctionalQuality' { + It 'Should import without errors' { + { Import-Module -Name $script:moduleName -Force -ErrorAction Stop } | Should -Not -Throw + + Get-Module -Name $script:moduleName | Should -Not -BeNullOrEmpty + } + + It 'Should remove without error' { + { Remove-Module -Name $script:moduleName -ErrorAction Stop } | Should -Not -Throw + + Get-Module $script:moduleName | Should -BeNullOrEmpty + } +} + +BeforeDiscovery { + # Must use the imported module to build test cases. + $allModuleFunctions = & $mut { Get-Command -Module $args[0] -CommandType Function } $script:moduleName + + # Build test cases. + $testCases = @() + + foreach ($function in $allModuleFunctions) + { + $testCases += @{ + Name = $function.Name + } + } +} + +Describe 'Quality for module' -Tags 'TestQuality' { + BeforeDiscovery { + if (Get-Command -Name Invoke-ScriptAnalyzer -ErrorAction SilentlyContinue) + { + $scriptAnalyzerRules = Get-ScriptAnalyzerRule + } + else + { + if ($ErrorActionPreference -ne 'Stop') + { + Write-Warning -Message 'ScriptAnalyzer not found!' + } + else + { + throw 'ScriptAnalyzer not found!' + } + } + } + + It 'Should have a unit test for ' -ForEach $testCases { + Get-ChildItem -Path 'tests\' -Recurse -Include "$Name.Tests.ps1" | Should -Not -BeNullOrEmpty + } + + It 'Should pass Script Analyzer for ' -ForEach $testCases -Skip:(-not $scriptAnalyzerRules) { + $functionFile = Get-ChildItem -Path $sourcePath -Recurse -Include "$Name.ps1" + + $pssaResult = (Invoke-ScriptAnalyzer -Path $functionFile.FullName) + $report = $pssaResult | Format-Table -AutoSize | Out-String -Width 110 + $pssaResult | Should -BeNullOrEmpty -Because ` + "some rule triggered.`r`n`r`n $report" + } +} + +Describe 'Help for module' -Tags 'helpQuality' { + It 'Should have .SYNOPSIS for ' -ForEach $testCases { + $functionFile = Get-ChildItem -Path $sourcePath -Recurse -Include "$Name.ps1" + + $scriptFileRawContent = Get-Content -Raw -Path $functionFile.FullName + + $abstractSyntaxTree = [System.Management.Automation.Language.Parser]::ParseInput($scriptFileRawContent, [ref] $null, [ref] $null) + + $astSearchDelegate = { $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] } + + $parsedFunction = $abstractSyntaxTree.FindAll( $astSearchDelegate, $true ) | + Where-Object -FilterScript { + $_.Name -eq $Name + } + + $functionHelp = $parsedFunction.GetHelpContent() + + $functionHelp.Synopsis | Should -Not -BeNullOrEmpty + } + + It 'Should have a .DESCRIPTION with length greater than 40 characters for ' -ForEach $testCases { + $functionFile = Get-ChildItem -Path $sourcePath -Recurse -Include "$Name.ps1" + + $scriptFileRawContent = Get-Content -Raw -Path $functionFile.FullName + + $abstractSyntaxTree = [System.Management.Automation.Language.Parser]::ParseInput($scriptFileRawContent, [ref] $null, [ref] $null) + + $astSearchDelegate = { $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] } + + $parsedFunction = $abstractSyntaxTree.FindAll($astSearchDelegate, $true) | + Where-Object -FilterScript { + $_.Name -eq $Name + } + + $functionHelp = $parsedFunction.GetHelpContent() + + $functionHelp.Description.Length | Should -BeGreaterThan 40 + } + + It 'Should have at least one (1) example for ' -ForEach $testCases { + $functionFile = Get-ChildItem -Path $sourcePath -Recurse -Include "$Name.ps1" + + $scriptFileRawContent = Get-Content -Raw -Path $functionFile.FullName + + $abstractSyntaxTree = [System.Management.Automation.Language.Parser]::ParseInput($scriptFileRawContent, [ref] $null, [ref] $null) + + $astSearchDelegate = { $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] } + + $parsedFunction = $abstractSyntaxTree.FindAll( $astSearchDelegate, $true ) | + Where-Object -FilterScript { + $_.Name -eq $Name + } + + $functionHelp = $parsedFunction.GetHelpContent() + + $functionHelp.Examples.Count | Should -BeGreaterThan 0 + $functionHelp.Examples[0] | Should -Match ([regex]::Escape($function.Name)) + $functionHelp.Examples[0].Length | Should -BeGreaterThan ($function.Name.Length + 10) + + } + + It 'Should have described all parameters for ' -ForEach $testCases { + $functionFile = Get-ChildItem -Path $sourcePath -Recurse -Include "$Name.ps1" + + $scriptFileRawContent = Get-Content -Raw -Path $functionFile.FullName + + $abstractSyntaxTree = [System.Management.Automation.Language.Parser]::ParseInput($scriptFileRawContent, [ref] $null, [ref] $null) + + $astSearchDelegate = { $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] } + + $parsedFunction = $abstractSyntaxTree.FindAll( $astSearchDelegate, $true ) | + Where-Object -FilterScript { + $_.Name -eq $Name + } + + $functionHelp = $parsedFunction.GetHelpContent() + + $parameters = $parsedFunction.Body.ParamBlock.Parameters.Name.VariablePath.ForEach({ $_.ToString() }) + + foreach ($parameter in $parameters) + { + $functionHelp.Parameters.($parameter.ToUpper()) | Should -Not -BeNullOrEmpty -Because ('the parameter {0} must have a description' -f $parameter) + $functionHelp.Parameters.($parameter.ToUpper()).Length | Should -BeGreaterThan 25 -Because ('the parameter {0} must have descriptive description' -f $parameter) + } + } +} + diff --git a/tests/Unit/Public/Assert-BlockString.tests.ps1 b/tests/Unit/Public/Assert-BlockString.tests.ps1 new file mode 100644 index 0000000..bbcfe2b --- /dev/null +++ b/tests/Unit/Public/Assert-BlockString.tests.ps1 @@ -0,0 +1,103 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies has been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 2>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies has not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks build" first.' + } +} + +BeforeAll { + $script:dscModuleName = 'Viscalyx.Assert' + + Import-Module -Name $script:dscModuleName + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +Describe 'Assert-BlockString' { + It 'Should pass when Actual and Expected are equal strings' { + $mockActual = 'Test string' + $mockExpected = 'Test string' + + { Assert-BlockString -Actual $mockActual -Expected $mockExpected } | Should -Not -Throw + } + + It 'Should throw when Actual and Expected are different strings' { + $mockActual = 'Test string' + $mockExpected = 'Different string' + + { Assert-BlockString -Actual $mockActual -Expected $mockExpected } | Should -Throw + } + + It 'Should throw when Actual is not a string' { + $mockActual = 12345 + $mockExpected = 'Test string' + + { Assert-BlockString -Actual $mockActual -Expected $mockExpected } | Should -Throw + } + + It 'Should throw when Actual is string array' { + $mockActual = @('1','2') + $mockExpected = 'Test string' + + { Assert-BlockString -Actual $mockActual -Expected $mockExpected } | Should -Throw + } + + It 'Should include Because message in the error' { + $mockActual = 'Test string' + $mockExpected = 'Different string' + $Because = 'this is a test' + + { + Assert-BlockString -Actual $mockActual -Expected $mockExpected -Because $Because + } | Should -Throw -ExpectedMessage '*because this is a test*' + } + + It 'Should handle pipeline input' { + $mockExpected = 'Test string' + + $scriptBlock = { + 'Test string' | Assert-BlockString -Expected $mockExpected + } + + { & $scriptBlock } | Should -Not -Throw + } + + It 'Should be able to be called using its alias' { + $mockExpected = 'Test string' + + $scriptBlock = { + 'Test string' | Should-BeBlockString -Expected $mockExpected + } + + { & $scriptBlock } | Should -Not -Throw + } +}