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/workflows/Action-Test.yml b/.github/workflows/Action-Test.yml index 6ff2f13..ae765ba 100644 --- a/.github/workflows/Action-Test.yml +++ b/.github/workflows/Action-Test.yml @@ -1,32 +1,45 @@ -name: Action-Test - -run-name: "Action-Test - [${{ github.event.pull_request.title }} #${{ github.event.pull_request.number }}] by @${{ github.actor }}" - -on: - workflow_dispatch: - pull_request: - schedule: - - cron: '0 0 * * *' - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: read - pull-requests: read - -jobs: - ActionTestBasic: - name: Action-Test - [Basic] - runs-on: ubuntu-latest - steps: - # Need to check out as part of the test, as its a local action - - name: Checkout repo - uses: actions/checkout@v4 - - - name: Action-Test - uses: ./ - with: - working-directory: ./tests - subject: PSModule +name: Action-Test + +run-name: "Action-Test - [${{ github.event.pull_request.title }} #${{ github.event.pull_request.number }}] by @${{ github.actor }}" + +on: + workflow_dispatch: + pull_request: + schedule: + - cron: '0 0 * * *' + +env: + GH_TOKEN: ${{ github.token }} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: {} + +jobs: + ActionTestDefault: + name: Action-Test - [Default] + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Initialize environment + uses: PSModule/Initialize-PSModule@main + + - name: Upload module artifact + uses: actions/upload-artifact@v4 + with: + name: module + path: tests/outputs/modules + if-no-files-found: error + retention-days: 1 + + - name: Action-Test + uses: ./ + with: + Name: PSModuleTest + Path: tests/src + ModulesOutputPath: tests/outputs/modules + DocsOutputPath: tests/outputs/docs diff --git a/.github/workflows/Auto-Configure.yml b/.github/workflows/Auto-Configure.yml deleted file mode 100644 index e2321e4..0000000 --- a/.github/workflows/Auto-Configure.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Auto-Configure - -run-name: "Auto-Configure - [${{ github.event.pull_request.title }} #${{ github.event.pull_request.number }}] by @${{ github.actor }}" - -on: - pull_request_target: - branches: - - main - types: - - closed - - opened - - reopened - - synchronize - - labeled - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: write # Required to create releases - pull-requests: write # Required to create comments on the PRs - -jobs: - Auto-Configure: - runs-on: ubuntu-latest - steps: - - name: Checkout Code - uses: actions/checkout@v4 - - - name: Auto-Configure - uses: PSModule/Auto-Configure@v1 - env: - GITHUB_TOKEN: ${{ github.token }} diff --git a/.github/workflows/Auto-Document.yml b/.github/workflows/Auto-Document.yml deleted file mode 100644 index 6a62053..0000000 --- a/.github/workflows/Auto-Document.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Auto-Document - -run-name: "Auto-Document - [${{ github.event.pull_request.title }} #${{ github.event.pull_request.number }}] by @${{ github.actor }}" - -on: - pull_request_target: - branches: - - main - types: - - opened - - reopened - - synchronize - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: write # Required to push to the branch - -jobs: - Auto-Document: - runs-on: ubuntu-latest - steps: - - name: Checkout Code - uses: actions/checkout@v4 - - - name: Auto-Document - uses: PSModule/Auto-Document@v1 - env: - GITHUB_TOKEN: ${{ github.token }} diff --git a/.github/workflows/Auto-Release.yml b/.github/workflows/Auto-Release.yml index d6c477b..1a580b8 100644 --- a/.github/workflows/Auto-Release.yml +++ b/.github/workflows/Auto-Release.yml @@ -1,34 +1,34 @@ -name: Auto-Release - -run-name: "Auto-Release - [${{ github.event.pull_request.title }} #${{ github.event.pull_request.number }}] by @${{ github.actor }}" - -on: - pull_request_target: - branches: - - main - types: - - closed - - opened - - reopened - - synchronize - - labeled - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: write # Required to create releases - pull-requests: write # Required to create comments on the PRs - -jobs: - Auto-Release: - runs-on: ubuntu-latest - steps: - - name: Checkout Code - uses: actions/checkout@v4 - - - name: Auto-Release - uses: PSModule/Auto-Release@v1 - env: - GITHUB_TOKEN: ${{ github.token }} +name: Auto-Release + +run-name: "Auto-Release - [${{ github.event.pull_request.title }} #${{ github.event.pull_request.number }}] by @${{ github.actor }}" + +on: + pull_request_target: + branches: + - main + types: + - closed + - opened + - reopened + - synchronize + - labeled + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: write # Required to create releases + pull-requests: write # Required to create comments on the PRs + +jobs: + Auto-Release: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Auto-Release + uses: PSModule/Auto-Release@v1 + env: + GITHUB_TOKEN: ${{ github.token }} diff --git a/.github/workflows/Linter.yml b/.github/workflows/Linter.yml index 187c17e..bb47b67 100644 --- a/.github/workflows/Linter.yml +++ b/.github/workflows/Linter.yml @@ -1,29 +1,33 @@ -name: Linter - -run-name: "Linter - [${{ github.event.pull_request.title }} #${{ github.event.pull_request.number }}] by @${{ github.actor }}" - -on: [pull_request] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: read - packages: read - statuses: write - -jobs: - Lint: - name: Lint code base - runs-on: ubuntu-latest - steps: - - name: Checkout repo - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Lint code base - uses: super-linter/super-linter/slim@latest - env: - GITHUB_TOKEN: ${{ github.token }} +name: Linter + +run-name: "Linter - [${{ github.event.pull_request.title }} #${{ github.event.pull_request.number }}] by @${{ github.actor }}" + +on: [pull_request] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + packages: read + statuses: write + +jobs: + Lint: + name: Lint code base + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Lint code base + uses: super-linter/super-linter/slim@latest + env: + GITHUB_TOKEN: ${{ github.token }} + VALIDATE_MARKDOWN_PRETTIER: false + VALIDATE_JSON_PRETTIER: false + VALIDATE_YAML_PRETTIER: false + VALIDATE_JSCPD: false diff --git a/.gitignore b/.gitignore index 93352b0..af4061f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,11 @@ -## Ignore Visual Studio Code temporary files, build results, and -## files generated by popular Visual Studio Code add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore -.vscode/* -!.vscode/settings.json -!.vscode/extensions.json -*.code-workspace - -# Local History for Visual Studio Code -.history/ +## Ignore Visual Studio Code temporary files, build results, and +## files generated by popular Visual Studio Code add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore +.vscode/* +!.vscode/settings.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ diff --git a/LICENSE b/LICENSE index 9a9cbf4..75789b6 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2025 PSModule - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT License + +Copyright (c) 2025 PSModule + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/action.yml b/action.yml index 0aea80e..a890336 100644 --- a/action.yml +++ b/action.yml @@ -1,27 +1,79 @@ -name: '{{ NAME }}' -description: '{{ DESCRIPTION }}' -author: PSModule -branding: - icon: upload-cloud - color: white - -inputs: - working-directory: - description: The working directory where Terraform will be executed - required: false - subject: - description: The subject to greet - required: false - default: World - -runs: - using: composite - steps: - - name: '{{ NAME }}' - uses: PSModule/GitHub-Script@v1 - env: - GITHUB_ACTION_INPUT_subject: ${{ inputs.subject }} - with: - Script: | - # '{{ NAME }}' - ${{ github.action_path }}\scripts\main.ps1 +name: Build-PSModuleDocumentation (by PSModule) +description: Build documentation for a PowerShell module. +author: PSModule +branding: + icon: package + color: gray-dark + +inputs: + Name: + description: Name of the module to process. + required: false + Path: + description: Path to the folder where the modules are located. + required: false + default: src + ModulesOutputPath: + description: Path to the folder where the built modules are outputted. + required: false + default: outputs/modules + DocsOutputPath: + description: Path to the folder where the built docs are outputted. + required: false + default: outputs/docs + ModuleArtifactName: + description: Name of the module artifact to upload. + required: false + default: module + DocsArtifactName: + description: Name of the docs artifact to upload. + required: false + default: docs + Debug: + description: Enable debug output. + required: false + default: 'false' + Verbose: + description: Enable verbose output. + required: false + default: 'false' + Version: + description: Specifies the version of the GitHub module to be installed. The value must be an exact version. + required: false + Prerelease: + description: Allow prerelease versions if available. + required: false + default: 'false' + +runs: + using: composite + steps: + - name: Download module artifact + uses: actions/download-artifact@v4 + with: + name: ${{ inputs.ModuleArtifactName }} + path: ${{ inputs.ModulesOutputPath }} + + - name: Run Build-PSModuleDocumentation + uses: PSModule/GitHub-Script@v1 + env: + GITHUB_ACTION_INPUT_Name: ${{ inputs.Name }} + GITHUB_ACTION_INPUT_Path: ${{ inputs.Path }} + GITHUB_ACTION_INPUT_ModulesOutputPath: ${{ inputs.ModulesOutputPath }} + GITHUB_ACTION_INPUT_DocsOutputPath: ${{ inputs.DocsOutputPath }} + with: + Debug: ${{ inputs.Debug }} + Prerelease: ${{ inputs.Prerelease }} + Verbose: ${{ inputs.Verbose }} + Version: ${{ inputs.Version }} + Script: | + # Build-PSModuleDocumentation + ${{ github.action_path }}\scripts\main.ps1 + + - name: Upload docs artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.DocsArtifactName }} + path: ${{ inputs.DocsOutputPath }} + if-no-files-found: error + retention-days: 1 diff --git a/scripts/helpers/Build-PSModuleDocumentation.ps1 b/scripts/helpers/Build-PSModuleDocumentation.ps1 new file mode 100644 index 0000000..09ef6cd --- /dev/null +++ b/scripts/helpers/Build-PSModuleDocumentation.ps1 @@ -0,0 +1,126 @@ +#Requires -Modules @{ ModuleName = 'GitHub'; ModuleVersion = '0.13.2' } +#Requires -Modules @{ ModuleName = 'Utilities'; ModuleVersion = '0.3.0' } + +function Build-PSModuleDocumentation { + <# + .SYNOPSIS + Builds a module. + + .DESCRIPTION + Builds a module. + #> + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', Scope = 'Function', + Justification = 'LogGroup - Scoping affects the variables line of sight.' + )] + param( + # Name of the module. + [Parameter(Mandatory)] + [string] $ModuleName, + + # Path to the folder where the modules are located. + [Parameter(Mandatory)] + [string] $ModuleSourceFolderPath, + + # Path to the folder where the built modules are outputted. + [Parameter(Mandatory)] + [string] $ModulesOutputFolderPath, + + # Path to the folder where the documentation is outputted. + [Parameter(Mandatory)] + [string] $DocsOutputFolderPath + ) + + LogGroup "Documenting module [$ModuleName]" { + Write-Host "Source path: [$ModuleSourceFolderPath]" + if (-not (Test-Path -Path $ModuleSourceFolderPath)) { + Write-Error "Source folder not found at [$ModuleSourceFolderPath]" + exit 1 + } + $moduleSourceFolder = Get-Item -Path $ModuleSourceFolderPath + Write-Host "Module source folder: [$moduleSourceFolder]" + + $moduleOutputFolder = New-Item -Path $ModulesOutputFolderPath -Name $ModuleName -ItemType Directory -Force + Write-Host "Module output folder: [$moduleOutputFolder]" + + $docsOutputFolder = New-Item -Path $DocsOutputFolderPath -ItemType Directory -Force + Write-Host "Docs output folder: [$docsOutputFolder]" + } + + LogGroup 'Build docs - Generate markdown help' { + Add-PSModulePath -Path (Split-Path -Path $ModuleOutputFolder -Parent) + $ModuleName | Remove-Module -Force -ErrorAction SilentlyContinue + Import-PSModule -Path $ModuleOutputFolder -ModuleName $ModuleName + Write-Host ($ModuleName | Get-Module) + $null = New-MarkdownHelp -Module $ModuleName -OutputFolder $DocsOutputFolder -Force -Verbose + } + + LogGroup 'Build docs - Fix markdown code blocks' { + Get-ChildItem -Path $DocsOutputFolder -Recurse -Force -Include '*.md' | ForEach-Object { + $content = Get-Content -Path $_.FullName + $fixedOpening = $false + $newContent = @() + foreach ($line in $content) { + if ($line -match '^```$' -and -not $fixedOpening) { + $line = $line -replace '^```$', '```powershell' + $fixedOpening = $true + } elseif ($line -match '^```.+$') { + $fixedOpening = $true + } elseif ($line -match '^```$') { + $fixedOpening = $false + } + $newContent += $line + } + $newContent | Set-Content -Path $_.FullName + } + } + + LogGroup 'Build docs - Fix markdown escape characters' { + Get-ChildItem -Path $DocsOutputFolder -Recurse -Force -Include '*.md' | ForEach-Object { + $content = Get-Content -Path $_.FullName -Raw + $content = $content -replace '\\`', '`' + $content = $content -replace '\\\[', '[' + $content = $content -replace '\\\]', ']' + $content = $content -replace '\\\<', '<' + $content = $content -replace '\\\>', '>' + $content = $content -replace '\\\\', '\' + $content | Set-Content -Path $_.FullName + } + } + + LogGroup 'Build docs - Structure markdown files to match source files' { + $PublicFunctionsFolder = Join-Path $ModuleSourceFolder.FullName 'functions\public' | Get-Item + Get-ChildItem -Path $DocsOutputFolder -Recurse -Force -Include '*.md' | ForEach-Object { + $file = $_ + Write-Host "Processing: $file" + + # find the source code file that matches the markdown file + $scriptPath = Get-ChildItem -Path $PublicFunctionsFolder -Recurse -Force | Where-Object { $_.Name -eq ($file.BaseName + '.ps1') } + Write-Host "Found script path: $scriptPath" + $docsFilePath = ($scriptPath.FullName).Replace($PublicFunctionsFolder.FullName, $DocsOutputFolder.FullName).Replace('.ps1', '.md') + Write-Host "Doc file path: $docsFilePath" + $docsFolderPath = Split-Path -Path $docsFilePath -Parent + New-Item -Path $docsFolderPath -ItemType Directory -Force + Move-Item -Path $file.FullName -Destination $docsFilePath -Force + } + # Get the MD files that are in the public functions folder and move them to the same place in the docs folder + Get-ChildItem -Path $PublicFunctionsFolder -Recurse -Force -Include '*.md' | ForEach-Object { + $file = $_ + Write-Host "Processing: $file" + $docsFilePath = ($file.FullName).Replace($PublicFunctionsFolder.FullName, $DocsOutputFolder.FullName) + Write-Host "Doc file path: $docsFilePath" + $docsFolderPath = Split-Path -Path $docsFilePath -Parent + New-Item -Path $docsFolderPath -ItemType Directory -Force + Move-Item -Path $file.FullName -Destination $docsFilePath -Force + } + } + + Get-ChildItem -Path $DocsOutputFolder -Recurse -Force -Include '*.md' | ForEach-Object { + $fileName = $_.Name + $hash = (Get-FileHash -Path $_.FullName -Algorithm SHA256).Hash + LogGroup " - [$fileName] - [$hash]" { + Show-FileContent -Path $_ + } + } +} diff --git a/scripts/helpers/Import-PSModule.ps1 b/scripts/helpers/Import-PSModule.ps1 new file mode 100644 index 0000000..2797069 --- /dev/null +++ b/scripts/helpers/Import-PSModule.ps1 @@ -0,0 +1,50 @@ +#Requires -Modules @{ ModuleName = 'Utilities'; ModuleVersion = '0.3.0' } + +function Import-PSModule { + <# + .SYNOPSIS + Imports a build PS module. + + .DESCRIPTION + Imports a build PS module. + + .EXAMPLE + Import-PSModule -SourceFolderPath $ModuleFolderPath -ModuleName $ModuleName + + Imports a module located at $ModuleFolderPath with the name $ModuleName. + #> + [CmdletBinding()] + param( + # Path to the folder where the module source code is located. + [Parameter(Mandatory)] + [string] $Path, + + # Name of the module. + [Parameter(Mandatory)] + [string] $ModuleName + ) + + $moduleName = Split-Path -Path $Path -Leaf + $manifestFileName = "$moduleName.psd1" + $manifestFilePath = Join-Path -Path $Path $manifestFileName + $manifestFile = Get-ModuleManifest -Path $manifestFilePath -As FileInfo -Verbose + + Write-Host "Manifest file path: [$($manifestFile.FullName)]" -Verbose + $existingModule = Get-Module -Name $ModuleName -ListAvailable + $existingModule | Remove-Module -Force -Verbose + $existingModule.RequiredModules | ForEach-Object { $_ | Remove-Module -Force -Verbose -ErrorAction SilentlyContinue } + $existingModule.NestedModules | ForEach-Object { $_ | Remove-Module -Force -Verbose -ErrorAction SilentlyContinue } + # Get-InstalledPSResource | Where-Object Name -EQ $ModuleName | Uninstall-PSResource -SkipDependencyCheck -Verbose:$false + Resolve-PSModuleDependency -ManifestFilePath $manifestFile + Import-Module -Name $ModuleName -RequiredVersion '999.0.0' + + Write-Host 'List loaded modules' + $availableModules = Get-Module -ListAvailable -Refresh -Verbose:$false + $availableModules | Select-Object Name, Version, Path | Sort-Object Name | Format-Table -AutoSize + Write-Host 'List commands' + Write-Host (Get-Command -Module $moduleName | Format-Table -AutoSize | Out-String) + + if ($ModuleName -notin $availableModules.Name) { + throw 'Module not found' + } +} diff --git a/scripts/helpers/Resolve-PSModuleDependency.ps1 b/scripts/helpers/Resolve-PSModuleDependency.ps1 new file mode 100644 index 0000000..4924d3e --- /dev/null +++ b/scripts/helpers/Resolve-PSModuleDependency.ps1 @@ -0,0 +1,64 @@ +#Requires -Modules @{ ModuleName = 'Retry'; ModuleVersion = '0.1.3' } + +function Resolve-PSModuleDependency { + <# + .SYNOPSIS + Resolve dependencies for a module based on the manifest file. + + .DESCRIPTION + Resolve dependencies for a module based on the manifest file, following PSModuleInfo structure + + .EXAMPLE + Resolve-PSModuleDependency -Path 'C:\MyModule\MyModule.psd1' + + Installs all modules defined in the manifest file, following PSModuleInfo structure. + + .NOTES + Should later be adapted to support both pre-reqs, and dependencies. + Should later be adapted to take 4 parameters sets: specific version ("requiredVersion" | "GUID"), latest version ModuleVersion, + and latest version within a range MinimumVersion - MaximumVersion. + #> + [Alias('Resolve-PSModuleDependencies')] + [CmdletBinding()] + param( + # The path to the manifest file. + [Parameter(Mandatory)] + [string] $ManifestFilePath + ) + + Write-Host 'Resolving dependencies' + + $manifest = Import-PowerShellDataFile -Path $ManifestFilePath + Write-Host "Reading [$ManifestFilePath]" + Write-Host "Found [$($manifest.RequiredModules.Count)] modules to install" + + foreach ($requiredModule in $manifest.RequiredModules) { + $installParams = @{} + + if ($requiredModule -is [string]) { + $installParams.Name = $requiredModule + } else { + $installParams.Name = $requiredModule.ModuleName + $installParams.MinimumVersion = $requiredModule.ModuleVersion + $installParams.RequiredVersion = $requiredModule.RequiredVersion + $installParams.MaximumVersion = $requiredModule.MaximumVersion + } + $installParams.Force = $true + $installParams.Verbose = $false + + Write-Host "[$($installParams.Name)] - Installing module" + $VerbosePreferenceOriginal = $VerbosePreference + $VerbosePreference = 'SilentlyContinue' + Retry -Count 5 -Delay 10 { + Install-Module @installParams -AllowPrerelease:$false + } + $VerbosePreference = $VerbosePreferenceOriginal + Write-Host "[$($installParams.Name)] - Importing module" + $VerbosePreferenceOriginal = $VerbosePreference + $VerbosePreference = 'SilentlyContinue' + Import-Module @installParams + $VerbosePreference = $VerbosePreferenceOriginal + Write-Host "[$($installParams.Name)] - Done" + } + Write-Host 'Resolving dependencies - Done' +} diff --git a/scripts/main.ps1 b/scripts/main.ps1 index 2779f38..20ef6a1 100644 --- a/scripts/main.ps1 +++ b/scripts/main.ps1 @@ -1,24 +1,53 @@ -#Requires -Modules GitHub +#Requires -Modules Utilities [CmdletBinding()] -param( - [Parameter()] - [string] $Subject = $env:GITHUB_ACTION_INPUT_subject -) - -begin { - $scriptName = $MyInvocation.MyCommand.Name - Write-Debug "[$scriptName] - Start" +param() + +$path = (Join-Path -Path $PSScriptRoot -ChildPath 'helpers') | Get-Item | Resolve-Path -Relative +LogGroup "Loading helper scripts from [$path]" { + Get-ChildItem -Path $path -Filter '*.ps1' -Recurse | Resolve-Path -Relative | ForEach-Object { + Write-Host "$_" + . $_ + } +} + +LogGroup 'Loading inputs' { + $moduleName = ($env:GITHUB_ACTION_INPUT_Name | IsNullOrEmpty) ? $env:GITHUB_REPOSITORY_NAME : $env:GITHUB_ACTION_INPUT_Name + Write-Host "Module name: [$moduleName]" + + $moduleSourceFolderPath = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath $env:GITHUB_ACTION_INPUT_Path/$moduleName + if (-not (Test-Path -Path $moduleSourceFolderPath)) { + $moduleSourceFolderPath = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath $env:GITHUB_ACTION_INPUT_Path + } + Write-Host "Source module path: [$moduleSourceFolderPath]" + if (-not (Test-Path -Path $moduleSourceFolderPath)) { + throw "Module path [$moduleSourceFolderPath] does not exist." + } + + $modulesOutputFolderPath = Join-Path $env:GITHUB_WORKSPACE $env:GITHUB_ACTION_INPUT_ModulesOutputPath + Write-Host "Modules output path: [$modulesOutputFolderPath]" + $docsOutputFolderPath = Join-Path $env:GITHUB_WORKSPACE $env:GITHUB_ACTION_INPUT_DocsOutputPath + Write-Host "Docs output path: [$docsOutputFolderPath]" } -process { - try { - Write-Output "Hello, $Subject!" - } catch { - throw $_ +LogGroup 'Build local scripts' { + Write-Host 'Execution order:' + $scripts = Get-ChildItem -Filter '*build.ps1' -Recurse | Sort-Object -Property Name | Resolve-Path -Relative + $scripts | ForEach-Object { + Write-Host " - $_" + } + $scripts | ForEach-Object { + LogGroup "Build local scripts - [$_]" { + . $_ + } } } -end { - Write-Debug "[$scriptName] - End" +$params = @{ + ModuleName = $moduleName + ModuleSourceFolderPath = $moduleSourceFolderPath + ModulesOutputFolderPath = $modulesOutputFolderPath + DocsOutputFolderPath = $docsOutputFolderPath } + +Build-PSModuleDocumentation @params diff --git a/tests/README.md b/tests/README.md deleted file mode 100644 index a570e4d..0000000 --- a/tests/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Placeholder for tests - -Location for tests of the action. diff --git a/tests/outputs/modules/PSModuleTest/PSModuleTest.psd1 b/tests/outputs/modules/PSModuleTest/PSModuleTest.psd1 new file mode 100644 index 0000000..e09d34e --- /dev/null +++ b/tests/outputs/modules/PSModuleTest/PSModuleTest.psd1 @@ -0,0 +1,75 @@ +@{ + RootModule = 'PSModuleTest.psm1' + ModuleVersion = '999.0.0' + CompatiblePSEditions = @( + 'Core' + 'Desktop' + ) + GUID = '20b37221-db1c-43db-9cca-f22b33123548' + Author = 'PSModule' + CompanyName = 'PSModule' + Copyright = '(c) 2024 PSModule. All rights reserved.' + Description = 'Process a module from source code to published module.' + PowerShellVersion = '5.1' + ProcessorArchitecture = 'None' + RequiredModules = @( + @{ + ModuleVersion = '1.1.4' + ModuleName = 'PSSemVer' + } + 'Utilities' + ) + RequiredAssemblies = 'assemblies/LsonLib.dll' + ScriptsToProcess = 'scripts/loader.ps1' + TypesToProcess = @( + 'types/DirectoryInfo.Types.ps1xml' + 'types/FileInfo.Types.ps1xml' + ) + FormatsToProcess = @( + 'formats/CultureInfo.Format.ps1xml' + 'formats/Mygciview.Format.ps1xml' + ) + NestedModules = @( + 'modules/OtherPSModule.psm1' + ) + FunctionsToExport = @( + 'Get-PSModuleTest' + 'New-PSModuleTest' + 'Set-PSModuleTest' + 'Test-PSModuleTest' + ) + CmdletsToExport = @() + VariablesToExport = @() + AliasesToExport = '*' + ModuleList = @( + 'modules/OtherPSModule.psm1' + ) + FileList = @( + 'PSModuleTest.psd1' + 'PSModuleTest.psm1' + 'assemblies/LsonLib.dll' + 'data/Config.psd1' + 'data/Settings.psd1' + 'formats/CultureInfo.Format.ps1xml' + 'formats/Mygciview.Format.ps1xml' + 'modules/OtherPSModule.psm1' + 'scripts/loader.ps1' + 'types/DirectoryInfo.Types.ps1xml' + 'types/FileInfo.Types.ps1xml' + ) + PrivateData = @{ + PSData = @{ + Tags = @( + 'workflow' + 'powershell' + 'powershell-module' + 'PSEdition_Desktop' + 'PSEdition_Core' + ) + LicenseUri = 'https://github.com/PSModule/Process-PSModule/blob/main/LICENSE' + ProjectUri = 'https://github.com/PSModule/Process-PSModule' + IconUri = 'https://raw.githubusercontent.com/PSModule/Process-PSModule/main/icon/icon.png' + } + } +} + diff --git a/tests/outputs/modules/PSModuleTest/PSModuleTest.psm1 b/tests/outputs/modules/PSModuleTest/PSModuleTest.psm1 new file mode 100644 index 0000000..dfe05df --- /dev/null +++ b/tests/outputs/modules/PSModuleTest/PSModuleTest.psm1 @@ -0,0 +1,382 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidLongLines', '', Justification = 'Contains long links.')] +[CmdletBinding()] +param() + +$scriptName = $MyInvocation.MyCommand.Name +Write-Verbose "[$scriptName] Importing module" + +#region - Data import +Write-Verbose "[$scriptName] - [data] - Processing folder" +$dataFolder = (Join-Path $PSScriptRoot 'data') +Write-Verbose "[$scriptName] - [data] - [$dataFolder]" +Get-ChildItem -Path "$dataFolder" -Recurse -Force -Include '*.psd1' -ErrorAction SilentlyContinue | ForEach-Object { + Write-Verbose "[$scriptName] - [data] - [$($_.Name)] - Importing" + New-Variable -Name $_.BaseName -Value (Import-PowerShellDataFile -Path $_.FullName) -Force + Write-Verbose "[$scriptName] - [data] - [$($_.Name)] - Done" +} + +Write-Verbose "[$scriptName] - [data] - Done" +#endregion - Data import + +#region - From /init +Write-Verbose "[$scriptName] - [/init] - Processing folder" + +#region - From /init/initializer.ps1 +Write-Verbose "[$scriptName] - [/init/initializer.ps1] - Importing" + +Write-Verbose '-------------------------------' -Verbose +Write-Verbose '--- THIS IS AN INITIALIZER ---' -Verbose +Write-Verbose '-------------------------------' -Verbose + +Write-Verbose "[$scriptName] - [/init/initializer.ps1] - Done" +#endregion - From /init/initializer.ps1 + +Write-Verbose "[$scriptName] - [/init] - Done" +#endregion - From /init + +#region - From /classes +Write-Verbose "[$scriptName] - [/classes] - Processing folder" + +#region - From /classes/Book.ps1 +Write-Verbose "[$scriptName] - [/classes/Book.ps1] - Importing" + +class Book { + # Class properties + [string] $Title + [string] $Author + [string] $Synopsis + [string] $Publisher + [datetime] $PublishDate + [int] $PageCount + [string[]] $Tags + # Default constructor + Book() { $this.Init(@{}) } + # Convenience constructor from hashtable + Book([hashtable]$Properties) { $this.Init($Properties) } + # Common constructor for title and author + Book([string]$Title, [string]$Author) { + $this.Init(@{Title = $Title; Author = $Author }) + } + # Shared initializer method + [void] Init([hashtable]$Properties) { + foreach ($Property in $Properties.Keys) { + $this.$Property = $Properties.$Property + } + } + # Method to calculate reading time as 2 minutes per page + [timespan] GetReadingTime() { + if ($this.PageCount -le 0) { + throw 'Unable to determine reading time from page count.' + } + $Minutes = $this.PageCount * 2 + return [timespan]::new(0, $Minutes, 0) + } + # Method to calculate how long ago a book was published + [timespan] GetPublishedAge() { + if ( + $null -eq $this.PublishDate -or + $this.PublishDate -eq [datetime]::MinValue + ) { throw 'PublishDate not defined' } + + return (Get-Date) - $this.PublishDate + } + # Method to return a string representation of the book + [string] ToString() { + return "$($this.Title) by $($this.Author) ($($this.PublishDate.Year))" + } +} + +Write-Verbose "[$scriptName] - [/classes/Book.ps1] - Done" +#endregion - From /classes/Book.ps1 +#region - From /classes/BookList.ps1 +Write-Verbose "[$scriptName] - [/classes/BookList.ps1] - Importing" + +class BookList { + # Static property to hold the list of books + static [System.Collections.Generic.List[Book]] $Books + # Static method to initialize the list of books. Called in the other + # static methods to avoid needing to explicit initialize the value. + static [void] Initialize() { [BookList]::Initialize($false) } + static [bool] Initialize([bool]$force) { + if ([BookList]::Books.Count -gt 0 -and -not $force) { + return $false + } + + [BookList]::Books = [System.Collections.Generic.List[Book]]::new() + + return $true + } + # Ensure a book is valid for the list. + static [void] Validate([book]$Book) { + $Prefix = @( + 'Book validation failed: Book must be defined with the Title,' + 'Author, and PublishDate properties, but' + ) -join ' ' + if ($null -eq $Book) { throw "$Prefix was null" } + if ([string]::IsNullOrEmpty($Book.Title)) { + throw "$Prefix Title wasn't defined" + } + if ([string]::IsNullOrEmpty($Book.Author)) { + throw "$Prefix Author wasn't defined" + } + if ([datetime]::MinValue -eq $Book.PublishDate) { + throw "$Prefix PublishDate wasn't defined" + } + } + # Static methods to manage the list of books. + # Add a book if it's not already in the list. + static [void] Add([Book]$Book) { + [BookList]::Initialize() + [BookList]::Validate($Book) + if ([BookList]::Books.Contains($Book)) { + throw "Book '$Book' already in list" + } + + $FindPredicate = { + param([Book]$b) + + $b.Title -eq $Book.Title -and + $b.Author -eq $Book.Author -and + $b.PublishDate -eq $Book.PublishDate + }.GetNewClosure() + if ([BookList]::Books.Find($FindPredicate)) { + throw "Book '$Book' already in list" + } + + [BookList]::Books.Add($Book) + } + # Clear the list of books. + static [void] Clear() { + [BookList]::Initialize() + [BookList]::Books.Clear() + } + # Find a specific book using a filtering scriptblock. + static [Book] Find([scriptblock]$Predicate) { + [BookList]::Initialize() + return [BookList]::Books.Find($Predicate) + } + # Find every book matching the filtering scriptblock. + static [Book[]] FindAll([scriptblock]$Predicate) { + [BookList]::Initialize() + return [BookList]::Books.FindAll($Predicate) + } + # Remove a specific book. + static [void] Remove([Book]$Book) { + [BookList]::Initialize() + [BookList]::Books.Remove($Book) + } + # Remove a book by property value. + static [void] RemoveBy([string]$Property, [string]$Value) { + [BookList]::Initialize() + $Index = [BookList]::Books.FindIndex({ + param($b) + $b.$Property -eq $Value + }.GetNewClosure()) + if ($Index -ge 0) { + [BookList]::Books.RemoveAt($Index) + } + } +} + +Write-Verbose "[$scriptName] - [/classes/BookList.ps1] - Done" +#endregion - From /classes/BookList.ps1 + +Write-Verbose "[$scriptName] - [/classes] - Done" +#endregion - From /classes + +#region - From /private +Write-Verbose "[$scriptName] - [/private] - Processing folder" + +#region - From /private/Get-InternalPSModule.ps1 +Write-Verbose "[$scriptName] - [/private/Get-InternalPSModule.ps1] - Importing" + +Function Get-InternalPSModule { + <# + .SYNOPSIS + Performs tests on a module. + + .EXAMPLE + Test-PSModule -Name 'World' + + "Hello, World!" + #> + [CmdletBinding()] + param ( + # Name of the person to greet. + [Parameter(Mandatory)] + [string] $Name + ) + Write-Output "Hello, $Name!" +} + +Write-Verbose "[$scriptName] - [/private/Get-InternalPSModule.ps1] - Done" +#endregion - From /private/Get-InternalPSModule.ps1 +#region - From /private/Set-InternalPSModule.ps1 +Write-Verbose "[$scriptName] - [/private/Set-InternalPSModule.ps1] - Importing" + +Function Set-InternalPSModule { + <# + .SYNOPSIS + Performs tests on a module. + + .EXAMPLE + Test-PSModule -Name 'World' + + "Hello, World!" + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', '', Scope = 'Function', + Justification = 'Reason for suppressing' + )] + [CmdletBinding()] + param ( + # Name of the person to greet. + [Parameter(Mandatory)] + [string] $Name + ) + Write-Output "Hello, $Name!" +} + +Write-Verbose "[$scriptName] - [/private/Set-InternalPSModule.ps1] - Done" +#endregion - From /private/Set-InternalPSModule.ps1 + +Write-Verbose "[$scriptName] - [/private] - Done" +#endregion - From /private + +#region - From /public +Write-Verbose "[$scriptName] - [/public] - Processing folder" + +#region - From /public/Get-PSModuleTest.ps1 +Write-Verbose "[$scriptName] - [/public/Get-PSModuleTest.ps1] - Importing" + +#Requires -Modules Utilities + +function Get-PSModuleTest { + <# + .SYNOPSIS + Performs tests on a module. + + .EXAMPLE + Test-PSModule -Name 'World' + + "Hello, World!" + #> + [CmdletBinding()] + param ( + # Name of the person to greet. + [Parameter(Mandatory)] + [string] $Name + ) + Write-Output "Hello, $Name!" +} + +Write-Verbose "[$scriptName] - [/public/Get-PSModuleTest.ps1] - Done" +#endregion - From /public/Get-PSModuleTest.ps1 +#region - From /public/New-PSModuleTest.ps1 +Write-Verbose "[$scriptName] - [/public/New-PSModuleTest.ps1] - Importing" + +#Requires -Modules @{ModuleName='PSSemVer'; ModuleVersion='1.1.4'} + +function New-PSModuleTest { + <# + .SYNOPSIS + Performs tests on a module. + + .EXAMPLE + Test-PSModule -Name 'World' + + "Hello, World!" + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', '', Scope = 'Function', + Justification = 'Reason for suppressing' + )] + [CmdletBinding()] + param ( + # Name of the person to greet. + [Parameter(Mandatory)] + [string] $Name + ) + Write-Output "Hello, $Name!" +} + +Write-Verbose "[$scriptName] - [/public/New-PSModuleTest.ps1] - Done" +#endregion - From /public/New-PSModuleTest.ps1 +#region - From /public/Set-PSModuleTest.ps1 +Write-Verbose "[$scriptName] - [/public/Set-PSModuleTest.ps1] - Importing" + +function Set-PSModuleTest { + <# + .SYNOPSIS + Performs tests on a module. + + .EXAMPLE + Test-PSModule -Name 'World' + + "Hello, World!" + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', '', Scope = 'Function', + Justification = 'Reason for suppressing' + )] + [CmdletBinding()] + param ( + # Name of the person to greet. + [Parameter(Mandatory)] + [string] $Name + ) + Write-Output "Hello, $Name!" +} + +Write-Verbose "[$scriptName] - [/public/Set-PSModuleTest.ps1] - Done" +#endregion - From /public/Set-PSModuleTest.ps1 +#region - From /public/Test-PSModuleTest.ps1 +Write-Verbose "[$scriptName] - [/public/Test-PSModuleTest.ps1] - Importing" + +function Test-PSModuleTest { + <# + .SYNOPSIS + Performs tests on a module. + + .EXAMPLE + Test-PSModule -Name 'World' + + "Hello, World!" + #> + [CmdletBinding()] + param ( + # Name of the person to greet. + [Parameter(Mandatory)] + [string] $Name + ) + Write-Output "Hello, $Name!" +} + +Write-Verbose "[$scriptName] - [/public/Test-PSModuleTest.ps1] - Done" +#endregion - From /public/Test-PSModuleTest.ps1 + +Write-Verbose "[$scriptName] - [/public] - Done" +#endregion - From /public + +#region - From /finally.ps1 +Write-Verbose "[$scriptName] - [/finally.ps1] - Importing" + +Write-Verbose '------------------------------' -Verbose +Write-Verbose '--- THIS IS A LAST LOADER ---' -Verbose +Write-Verbose '------------------------------' -Verbose +Write-Verbose "[$scriptName] - [/finally.ps1] - Done" +#endregion - From /finally.ps1 + +$exports = @{ + Cmdlet = '' + Alias = '*' + Variable = '' + Function = @( + 'Get-PSModuleTest' + 'New-PSModuleTest' + 'Set-PSModuleTest' + 'Test-PSModuleTest' + ) +} +Export-ModuleMember @exports + diff --git a/tests/outputs/modules/PSModuleTest/assemblies/LsonLib.dll b/tests/outputs/modules/PSModuleTest/assemblies/LsonLib.dll new file mode 100644 index 0000000..3661807 Binary files /dev/null and b/tests/outputs/modules/PSModuleTest/assemblies/LsonLib.dll differ diff --git a/tests/outputs/modules/PSModuleTest/data/Config.psd1 b/tests/outputs/modules/PSModuleTest/data/Config.psd1 new file mode 100644 index 0000000..fea4466 --- /dev/null +++ b/tests/outputs/modules/PSModuleTest/data/Config.psd1 @@ -0,0 +1,3 @@ +@{ + RandomKey = 'RandomValue' +} diff --git a/tests/outputs/modules/PSModuleTest/data/Settings.psd1 b/tests/outputs/modules/PSModuleTest/data/Settings.psd1 new file mode 100644 index 0000000..bcfa7b4 --- /dev/null +++ b/tests/outputs/modules/PSModuleTest/data/Settings.psd1 @@ -0,0 +1,3 @@ +@{ + RandomSetting = 'RandomSettingValue' +} diff --git a/tests/outputs/modules/PSModuleTest/formats/CultureInfo.Format.ps1xml b/tests/outputs/modules/PSModuleTest/formats/CultureInfo.Format.ps1xml new file mode 100644 index 0000000..a715e08 --- /dev/null +++ b/tests/outputs/modules/PSModuleTest/formats/CultureInfo.Format.ps1xml @@ -0,0 +1,37 @@ + + + + + System.Globalization.CultureInfo + + System.Globalization.CultureInfo + + + + + 16 + + + 16 + + + + + + + + LCID + + + Name + + + DisplayName + + + + + + + + diff --git a/tests/outputs/modules/PSModuleTest/formats/Mygciview.Format.ps1xml b/tests/outputs/modules/PSModuleTest/formats/Mygciview.Format.ps1xml new file mode 100644 index 0000000..4c972c2 --- /dev/null +++ b/tests/outputs/modules/PSModuleTest/formats/Mygciview.Format.ps1xml @@ -0,0 +1,65 @@ + + + + + mygciview + + System.IO.DirectoryInfo + System.IO.FileInfo + + + PSParentPath + + + + + + 7 + Left + + + + 26 + Right + + + + 26 + Right + + + + 14 + Right + + + + Left + + + + + + + + ModeWithoutHardLink + + + LastWriteTime + + + CreationTime + + + Length + + + Name + + + + + + + + diff --git a/tests/outputs/modules/PSModuleTest/modules/OtherPSModule.psm1 b/tests/outputs/modules/PSModuleTest/modules/OtherPSModule.psm1 new file mode 100644 index 0000000..9e4353b --- /dev/null +++ b/tests/outputs/modules/PSModuleTest/modules/OtherPSModule.psm1 @@ -0,0 +1,19 @@ +Function Get-OtherPSModule { + <# + .SYNOPSIS + Performs tests on a module. + + .DESCRIPTION + A longer description of the function. + + .EXAMPLE + Get-OtherPSModule -Name 'World' + #> + [CmdletBinding()] + param( + # Name of the person to greet. + [Parameter(Mandatory)] + [string] $Name + ) + Write-Output "Hello, $Name!" +} diff --git a/tests/outputs/modules/PSModuleTest/scripts/loader.ps1 b/tests/outputs/modules/PSModuleTest/scripts/loader.ps1 new file mode 100644 index 0000000..29ad42f --- /dev/null +++ b/tests/outputs/modules/PSModuleTest/scripts/loader.ps1 @@ -0,0 +1,3 @@ +Write-Verbose '-------------------------' -Verbose +Write-Verbose '--- THIS IS A LOADER ---' -Verbose +Write-Verbose '-------------------------' -Verbose diff --git a/tests/outputs/modules/PSModuleTest/types/DirectoryInfo.Types.ps1xml b/tests/outputs/modules/PSModuleTest/types/DirectoryInfo.Types.ps1xml new file mode 100644 index 0000000..aef538b --- /dev/null +++ b/tests/outputs/modules/PSModuleTest/types/DirectoryInfo.Types.ps1xml @@ -0,0 +1,21 @@ + + + + System.IO.FileInfo + + + Status + Success + + + + + System.IO.DirectoryInfo + + + Status + Success + + + + diff --git a/tests/outputs/modules/PSModuleTest/types/FileInfo.Types.ps1xml b/tests/outputs/modules/PSModuleTest/types/FileInfo.Types.ps1xml new file mode 100644 index 0000000..4cfaf6b --- /dev/null +++ b/tests/outputs/modules/PSModuleTest/types/FileInfo.Types.ps1xml @@ -0,0 +1,14 @@ + + + + System.IO.FileInfo + + + Age + + ((Get-Date) - ($this.CreationTime)).Days + + + + + diff --git a/tests/src/assemblies/LsonLib.dll b/tests/src/assemblies/LsonLib.dll new file mode 100644 index 0000000..3661807 Binary files /dev/null and b/tests/src/assemblies/LsonLib.dll differ diff --git a/tests/src/classes/private/SecretWriter.ps1 b/tests/src/classes/private/SecretWriter.ps1 new file mode 100644 index 0000000..1b1732a --- /dev/null +++ b/tests/src/classes/private/SecretWriter.ps1 @@ -0,0 +1,15 @@ +class SecretWriter { + [string] $Alias + [string] $Name + [string] $Secret + + SecretWriter([string] $alias, [string] $name, [string] $secret) { + $this.Alias = $alias + $this.Name = $name + $this.Secret = $secret + } + + [string] GetAlias() { + return $this.Alias + } +} diff --git a/tests/src/classes/public/Book.ps1 b/tests/src/classes/public/Book.ps1 new file mode 100644 index 0000000..8917d9a --- /dev/null +++ b/tests/src/classes/public/Book.ps1 @@ -0,0 +1,147 @@ +class Book { + # Class properties + [string] $Title + [string] $Author + [string] $Synopsis + [string] $Publisher + [datetime] $PublishDate + [int] $PageCount + [string[]] $Tags + # Default constructor + Book() { $this.Init(@{}) } + # Convenience constructor from hashtable + Book([hashtable]$Properties) { $this.Init($Properties) } + # Common constructor for title and author + Book([string]$Title, [string]$Author) { + $this.Init(@{Title = $Title; Author = $Author }) + } + # Shared initializer method + [void] Init([hashtable]$Properties) { + foreach ($Property in $Properties.Keys) { + $this.$Property = $Properties.$Property + } + } + # Method to calculate reading time as 2 minutes per page + [timespan] GetReadingTime() { + if ($this.PageCount -le 0) { + throw 'Unable to determine reading time from page count.' + } + $Minutes = $this.PageCount * 2 + return [timespan]::new(0, $Minutes, 0) + } + # Method to calculate how long ago a book was published + [timespan] GetPublishedAge() { + if ( + $null -eq $this.PublishDate -or + $this.PublishDate -eq [datetime]::MinValue + ) { throw 'PublishDate not defined' } + + return (Get-Date) - $this.PublishDate + } + # Method to return a string representation of the book + [string] ToString() { + return "$($this.Title) by $($this.Author) ($($this.PublishDate.Year))" + } +} + +class BookList { + # Static property to hold the list of books + static [System.Collections.Generic.List[Book]] $Books + # Static method to initialize the list of books. Called in the other + # static methods to avoid needing to explicit initialize the value. + static [void] Initialize() { [BookList]::Initialize($false) } + static [bool] Initialize([bool]$force) { + if ([BookList]::Books.Count -gt 0 -and -not $force) { + return $false + } + + [BookList]::Books = [System.Collections.Generic.List[Book]]::new() + + return $true + } + # Ensure a book is valid for the list. + static [void] Validate([book]$Book) { + $Prefix = @( + 'Book validation failed: Book must be defined with the Title,' + 'Author, and PublishDate properties, but' + ) -join ' ' + if ($null -eq $Book) { throw "$Prefix was null" } + if ([string]::IsNullOrEmpty($Book.Title)) { + throw "$Prefix Title wasn't defined" + } + if ([string]::IsNullOrEmpty($Book.Author)) { + throw "$Prefix Author wasn't defined" + } + if ([datetime]::MinValue -eq $Book.PublishDate) { + throw "$Prefix PublishDate wasn't defined" + } + } + # Static methods to manage the list of books. + # Add a book if it's not already in the list. + static [void] Add([Book]$Book) { + [BookList]::Initialize() + [BookList]::Validate($Book) + if ([BookList]::Books.Contains($Book)) { + throw "Book '$Book' already in list" + } + + $FindPredicate = { + param([Book]$b) + + $b.Title -eq $Book.Title -and + $b.Author -eq $Book.Author -and + $b.PublishDate -eq $Book.PublishDate + }.GetNewClosure() + if ([BookList]::Books.Find($FindPredicate)) { + throw "Book '$Book' already in list" + } + + [BookList]::Books.Add($Book) + } + # Clear the list of books. + static [void] Clear() { + [BookList]::Initialize() + [BookList]::Books.Clear() + } + # Find a specific book using a filtering scriptblock. + static [Book] Find([scriptblock]$Predicate) { + [BookList]::Initialize() + return [BookList]::Books.Find($Predicate) + } + # Find every book matching the filtering scriptblock. + static [Book[]] FindAll([scriptblock]$Predicate) { + [BookList]::Initialize() + return [BookList]::Books.FindAll($Predicate) + } + # Remove a specific book. + static [void] Remove([Book]$Book) { + [BookList]::Initialize() + [BookList]::Books.Remove($Book) + } + # Remove a book by property value. + static [void] RemoveBy([string]$Property, [string]$Value) { + [BookList]::Initialize() + $Index = [BookList]::Books.FindIndex({ + param($b) + $b.$Property -eq $Value + }.GetNewClosure()) + if ($Index -ge 0) { + [BookList]::Books.RemoveAt($Index) + } + } +} + +enum Binding { + Hardcover + Paperback + EBook +} + +enum Genre { + Mystery + Thriller + Romance + ScienceFiction + Fantasy + Horror +} diff --git a/tests/src/data/Config.psd1 b/tests/src/data/Config.psd1 new file mode 100644 index 0000000..fea4466 --- /dev/null +++ b/tests/src/data/Config.psd1 @@ -0,0 +1,3 @@ +@{ + RandomKey = 'RandomValue' +} diff --git a/tests/src/data/Settings.psd1 b/tests/src/data/Settings.psd1 new file mode 100644 index 0000000..bcfa7b4 --- /dev/null +++ b/tests/src/data/Settings.psd1 @@ -0,0 +1,3 @@ +@{ + RandomSetting = 'RandomSettingValue' +} diff --git a/tests/src/finally.ps1 b/tests/src/finally.ps1 new file mode 100644 index 0000000..d8fc207 --- /dev/null +++ b/tests/src/finally.ps1 @@ -0,0 +1,3 @@ +Write-Verbose '------------------------------' +Write-Verbose '--- THIS IS A LAST LOADER ---' +Write-Verbose '------------------------------' diff --git a/tests/src/formats/CultureInfo.Format.ps1xml b/tests/src/formats/CultureInfo.Format.ps1xml new file mode 100644 index 0000000..a715e08 --- /dev/null +++ b/tests/src/formats/CultureInfo.Format.ps1xml @@ -0,0 +1,37 @@ + + + + + System.Globalization.CultureInfo + + System.Globalization.CultureInfo + + + + + 16 + + + 16 + + + + + + + + LCID + + + Name + + + DisplayName + + + + + + + + diff --git a/tests/src/formats/Mygciview.Format.ps1xml b/tests/src/formats/Mygciview.Format.ps1xml new file mode 100644 index 0000000..4c972c2 --- /dev/null +++ b/tests/src/formats/Mygciview.Format.ps1xml @@ -0,0 +1,65 @@ + + + + + mygciview + + System.IO.DirectoryInfo + System.IO.FileInfo + + + PSParentPath + + + + + + 7 + Left + + + + 26 + Right + + + + 26 + Right + + + + 14 + Right + + + + Left + + + + + + + + ModeWithoutHardLink + + + LastWriteTime + + + CreationTime + + + Length + + + Name + + + + + + + + diff --git a/tests/src/functions/private/Get-InternalPSModule.ps1 b/tests/src/functions/private/Get-InternalPSModule.ps1 new file mode 100644 index 0000000..89f053c --- /dev/null +++ b/tests/src/functions/private/Get-InternalPSModule.ps1 @@ -0,0 +1,18 @@ +function Get-InternalPSModule { + <# + .SYNOPSIS + Performs tests on a module. + + .EXAMPLE + Test-PSModule -Name 'World' + + "Hello, World!" + #> + [CmdletBinding()] + param ( + # Name of the person to greet. + [Parameter(Mandatory)] + [string] $Name + ) + Write-Output "Hello, $Name!" +} diff --git a/tests/src/functions/private/Set-InternalPSModule.ps1 b/tests/src/functions/private/Set-InternalPSModule.ps1 new file mode 100644 index 0000000..cf870ba --- /dev/null +++ b/tests/src/functions/private/Set-InternalPSModule.ps1 @@ -0,0 +1,22 @@ +function Set-InternalPSModule { + <# + .SYNOPSIS + Performs tests on a module. + + .EXAMPLE + Test-PSModule -Name 'World' + + "Hello, World!" + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', '', Scope = 'Function', + Justification = 'Reason for suppressing' + )] + [CmdletBinding()] + param ( + # Name of the person to greet. + [Parameter(Mandatory)] + [string] $Name + ) + Write-Output "Hello, $Name!" +} diff --git a/tests/src/functions/public/PSModule/Get-PSModuleTest.ps1 b/tests/src/functions/public/PSModule/Get-PSModuleTest.ps1 new file mode 100644 index 0000000..be5afc0 --- /dev/null +++ b/tests/src/functions/public/PSModule/Get-PSModuleTest.ps1 @@ -0,0 +1,22 @@ +#Requires -Modules Store +#Requires -Modules @{ ModuleName = 'PSSemVer'; RequiredVersion = '1.1.4' } +#Requires -Modules @{ ModuleName = 'DynamicParams'; ModuleVersion = '1.1.8' } + +function Get-PSModuleTest { + <# + .SYNOPSIS + Performs tests on a module. + + .EXAMPLE + Test-PSModule -Name 'World' + + "Hello, World!" + #> + [CmdletBinding()] + param ( + # Name of the person to greet. + [Parameter(Mandatory)] + [string] $Name + ) + Write-Output "Hello, $Name!" +} diff --git a/tests/src/functions/public/PSModule/New-PSModuleTest.ps1 b/tests/src/functions/public/PSModule/New-PSModuleTest.ps1 new file mode 100644 index 0000000..5fa16bc --- /dev/null +++ b/tests/src/functions/public/PSModule/New-PSModuleTest.ps1 @@ -0,0 +1,37 @@ +#Requires -Modules @{ModuleName='PSSemVer'; ModuleVersion='1.1.4'} + +function New-PSModuleTest { + <# + .SYNOPSIS + Performs tests on a module. + + .EXAMPLE + Test-PSModule -Name 'World' + + "Hello, World!" + + .NOTES + Testing if a module can have a [Markdown based link](https://example.com). + !"#¤%&/()=?`´^¨*'-_+§½{[]}<>|@£$€¥¢:;.," + \[This is a test\] + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', '', Scope = 'Function', + Justification = 'Reason for suppressing' + )] + [Alias('New-PSModuleTestAlias1')] + [Alias('New-PSModuleTestAlias2')] + [CmdletBinding()] + param ( + # Name of the person to greet. + [Parameter(Mandatory)] + [string] $Name + ) + Write-Output "Hello, $Name!" +} + +New-Alias New-PSModuleTestAlias3 New-PSModuleTest +New-Alias -Name New-PSModuleTestAlias4 -Value New-PSModuleTest + + +Set-Alias New-PSModuleTestAlias5 New-PSModuleTest diff --git a/tests/src/functions/public/PSModule/PSModule.md b/tests/src/functions/public/PSModule/PSModule.md new file mode 100644 index 0000000..79741cf --- /dev/null +++ b/tests/src/functions/public/PSModule/PSModule.md @@ -0,0 +1 @@ +# This is PSModule diff --git a/tests/src/functions/public/SomethingElse/Set-PSModuleTest.ps1 b/tests/src/functions/public/SomethingElse/Set-PSModuleTest.ps1 new file mode 100644 index 0000000..a87ac11 --- /dev/null +++ b/tests/src/functions/public/SomethingElse/Set-PSModuleTest.ps1 @@ -0,0 +1,22 @@ +function Set-PSModuleTest { + <# + .SYNOPSIS + Performs tests on a module. + + .EXAMPLE + Test-PSModule -Name 'World' + + "Hello, World!" + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', '', Scope = 'Function', + Justification = 'Reason for suppressing' + )] + [CmdletBinding()] + param ( + # Name of the person to greet. + [Parameter(Mandatory)] + [string] $Name + ) + Write-Output "Hello, $Name!" +} diff --git a/tests/src/functions/public/SomethingElse/SomethingElse.md b/tests/src/functions/public/SomethingElse/SomethingElse.md new file mode 100644 index 0000000..d9f7e9e --- /dev/null +++ b/tests/src/functions/public/SomethingElse/SomethingElse.md @@ -0,0 +1 @@ +# This is SomethingElse diff --git a/tests/src/functions/public/Test-PSModuleTest.ps1 b/tests/src/functions/public/Test-PSModuleTest.ps1 new file mode 100644 index 0000000..26be2b9 --- /dev/null +++ b/tests/src/functions/public/Test-PSModuleTest.ps1 @@ -0,0 +1,18 @@ +function Test-PSModuleTest { + <# + .SYNOPSIS + Performs tests on a module. + + .EXAMPLE + Test-PSModule -Name 'World' + + "Hello, World!" + #> + [CmdletBinding()] + param ( + # Name of the person to greet. + [Parameter(Mandatory)] + [string] $Name + ) + Write-Output "Hello, $Name!" +} diff --git a/tests/src/header.ps1 b/tests/src/header.ps1 new file mode 100644 index 0000000..cc1fde9 --- /dev/null +++ b/tests/src/header.ps1 @@ -0,0 +1,3 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidLongLines', '', Justification = 'Contains long links.')] +[CmdletBinding()] +param() diff --git a/tests/src/init/initializer.ps1 b/tests/src/init/initializer.ps1 new file mode 100644 index 0000000..28396fb --- /dev/null +++ b/tests/src/init/initializer.ps1 @@ -0,0 +1,3 @@ +Write-Verbose '-------------------------------' +Write-Verbose '--- THIS IS AN INITIALIZER ---' +Write-Verbose '-------------------------------' diff --git a/tests/src/modules/OtherPSModule.psm1 b/tests/src/modules/OtherPSModule.psm1 new file mode 100644 index 0000000..5d6af8e --- /dev/null +++ b/tests/src/modules/OtherPSModule.psm1 @@ -0,0 +1,19 @@ +function Get-OtherPSModule { + <# + .SYNOPSIS + Performs tests on a module. + + .DESCRIPTION + A longer description of the function. + + .EXAMPLE + Get-OtherPSModule -Name 'World' + #> + [CmdletBinding()] + param( + # Name of the person to greet. + [Parameter(Mandatory)] + [string] $Name + ) + Write-Output "Hello, $Name!" +} diff --git a/tests/src/scripts/loader.ps1 b/tests/src/scripts/loader.ps1 new file mode 100644 index 0000000..973735a --- /dev/null +++ b/tests/src/scripts/loader.ps1 @@ -0,0 +1,3 @@ +Write-Verbose '-------------------------' +Write-Verbose '--- THIS IS A LOADER ---' +Write-Verbose '-------------------------' diff --git a/tests/src/types/DirectoryInfo.Types.ps1xml b/tests/src/types/DirectoryInfo.Types.ps1xml new file mode 100644 index 0000000..aef538b --- /dev/null +++ b/tests/src/types/DirectoryInfo.Types.ps1xml @@ -0,0 +1,21 @@ + + + + System.IO.FileInfo + + + Status + Success + + + + + System.IO.DirectoryInfo + + + Status + Success + + + + diff --git a/tests/src/types/FileInfo.Types.ps1xml b/tests/src/types/FileInfo.Types.ps1xml new file mode 100644 index 0000000..4cfaf6b --- /dev/null +++ b/tests/src/types/FileInfo.Types.ps1xml @@ -0,0 +1,14 @@ + + + + System.IO.FileInfo + + + Age + + ((Get-Date) - ($this.CreationTime)).Days + + + + + diff --git a/tests/src/variables/private/PrivateVariables.ps1 b/tests/src/variables/private/PrivateVariables.ps1 new file mode 100644 index 0000000..f1fc2c3 --- /dev/null +++ b/tests/src/variables/private/PrivateVariables.ps1 @@ -0,0 +1,47 @@ +$script:HabitablePlanets = @( + @{ + Name = 'Earth' + Mass = 5.97 + Diameter = 12756 + DayLength = 24.0 + }, + @{ + Name = 'Mars' + Mass = 0.642 + Diameter = 6792 + DayLength = 24.7 + }, + @{ + Name = 'Proxima Centauri b' + Mass = 1.17 + Diameter = 11449 + DayLength = 5.15 + }, + @{ + Name = 'Kepler-442b' + Mass = 2.34 + Diameter = 11349 + DayLength = 5.7 + }, + @{ + Name = 'Kepler-452b' + Mass = 5.0 + Diameter = 17340 + DayLength = 20.0 + } +) + +$script:InhabitedPlanets = @( + @{ + Name = 'Earth' + Mass = 5.97 + Diameter = 12756 + DayLength = 24.0 + }, + @{ + Name = 'Mars' + Mass = 0.642 + Diameter = 6792 + DayLength = 24.7 + } +) diff --git a/tests/src/variables/public/Moons.ps1 b/tests/src/variables/public/Moons.ps1 new file mode 100644 index 0000000..dd0f33c --- /dev/null +++ b/tests/src/variables/public/Moons.ps1 @@ -0,0 +1,6 @@ +$script:Moons = @( + @{ + Planet = 'Earth' + Name = 'Moon' + } +) diff --git a/tests/src/variables/public/Planets.ps1 b/tests/src/variables/public/Planets.ps1 new file mode 100644 index 0000000..736584b --- /dev/null +++ b/tests/src/variables/public/Planets.ps1 @@ -0,0 +1,20 @@ +$script:Planets = @( + @{ + Name = 'Mercury' + Mass = 0.330 + Diameter = 4879 + DayLength = 4222.6 + }, + @{ + Name = 'Venus' + Mass = 4.87 + Diameter = 12104 + DayLength = 2802.0 + }, + @{ + Name = 'Earth' + Mass = 5.97 + Diameter = 12756 + DayLength = 24.0 + } +) diff --git a/tests/src/variables/public/SolarSystems.ps1 b/tests/src/variables/public/SolarSystems.ps1 new file mode 100644 index 0000000..acbcedf --- /dev/null +++ b/tests/src/variables/public/SolarSystems.ps1 @@ -0,0 +1,17 @@ +$script:SolarSystems = @( + @{ + Name = 'Solar System' + Planets = $script:Planets + Moons = $script:Moons + }, + @{ + Name = 'Alpha Centauri' + Planets = @() + Moons = @() + }, + @{ + Name = 'Sirius' + Planets = @() + Moons = @() + } +)