Sampler.psm1
#Region './Private/Convert-SamplerHashtableToString.ps1' 0 <# .SYNOPSIS Converts an Hashtable to its string representation, recursively. .DESCRIPTION Convert an Hashtable to a string representation. For instance, this hashtable: @{a=1;b=2; c=3; d=@{dd='abcd'}} Becomes: a=1; b=2; c=3; d={dd=abcd} .PARAMETER Hashtable Hashtable to convert to string. .EXAMPLE Convert-SamplerhashtableToString -Hashtable @{a=1;b=2; c=3; d=@{dd='abcd'}} .NOTES This command is not specific to Sampler projects, but is named that way to avoid conflict with other modules. #> function Convert-SamplerHashtableToString { param ( [Parameter()] [System.Collections.Hashtable] $Hashtable ) $values = @() foreach ($pair in $Hashtable.GetEnumerator()) { if ($pair.Value -is [System.Array]) { $str = "$($pair.Key)=($($pair.Value -join ","))" } elseif ($pair.Value -is [System.Collections.Hashtable]) { $str = "$($pair.Key)={$(Convert-SamplerHashtableToString -Hashtable $pair.Value)}" } else { $str = "$($pair.Key)=$($pair.Value)" } $values += $str } [array]::Sort($values) return ($values -join "; ") } #EndRegion './Private/Convert-SamplerHashtableToString.ps1' 51 #Region './Private/Get-SamplerProjectModuleManifest.ps1' 0 <# .SYNOPSIS Gets the path to the Module manifest in the source folder. .DESCRIPTION This command finds the Module Manifest of the current Sampler project, regardless of the name of the source folder (src, source, or MyProjectName). It looks for psd1 that are not build.psd1 or analyzersettings, 1 folder under the $BuildRoot, and where a property ModuleVersion is set. This allows to deduct the Module name's from that module Manifest. .PARAMETER BuildRoot Root folder where the build is called, usually the root of the repository. .EXAMPLE Get-SamplerProjectModuleManifest -BuildRoot . #> function Get-SamplerProjectModuleManifest { [CmdletBinding()] [OutputType([System.IO.FileInfo])] param ( [Parameter(Mandatory = $true)] [System.String] $BuildRoot ) $excludeFiles = @( 'build.psd1' 'analyzersettings.psd1' ) $moduleManifestItem = Get-ChildItem -Path "$BuildRoot\*\*.psd1" -Exclude $excludeFiles | Where-Object -FilterScript { ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and $(Test-ModuleManifest -Path $_.FullName -ErrorAction 'SilentlyContinue' ).Version } if ($moduleManifestItem.Count -gt 1) { throw ("Found more than one project folder containing a module manifest, please make sure there are only one; `n Manifest: {0}" -f ($moduleManifestItem.FullName -join "`n Manifest: ")) } else { return $moduleManifestItem } } #EndRegion './Private/Get-SamplerProjectModuleManifest.ps1' 51 #Region './Public/Add-Sample.ps1' 0 <# .SYNOPSIS Adding code elements (function, enum, class, DSC Resource, tests...) to a module's source. .DESCRIPTION Add-Sample is an helper function to invoke a plaster template built-in the Sampler module. With this function you can bootstrap your module project by adding classes, functions and associated tests, examples and configuration elements. .PARAMETER Sample Specifies a sample component based on the Plaster templates embedded with this module. The available types of module elements are: - Classes: A sample of 4 classes with inheritence and how to manage the orders to avoid parsing errors. - ClassResource: A Class-Based DSC Resources showing some best practices including tests, Reasons, localized strings. - Composite: A DSC Composite Resource (a configuration block) packaged the right way to make sure it's visible by Get-DscResource. - Enum: An example of a simple Enum. - MofResource: A sample of a MOF-Based DSC Resource following the DSC Community practices. - PrivateFunction: A sample of a Private function (not exported from the module) and its test. - PublicCallPrivateFunctions: A sample of 2 functions where the exported one (public) calls the private one, with the tests. - PublicFunction: A sample public function and its test. .PARAMETER DestinationPath Destination of your module source root folder, defaults to the current directory ".". We assume that your current location is the module folder, and within this folder we will find the source folder, the tests folder and other supporting files. .EXAMPLE C:\src\MyModule> Add-Sample -Sample PublicFunction -PublicFunctionName Get-MyStuff .NOTES This module requires and uses Plaster. #> function Add-Sample { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] [CmdletBinding()] [OutputType()] param ( [Parameter()] # Add a sample component based on the Plaster templates embedded with this module. [ValidateSet('Classes', 'ClassFolderResource', 'ClassResource', 'Composite', 'Enum', 'Examples', 'GithubConfig', 'GCPackage', 'HelperSubModules', 'MofResource', 'PrivateFunction', 'PublicCallPrivateFunctions', 'PublicFunction', 'VscodeConfig')] [string] $Sample, [Parameter()] [System.String] $DestinationPath = '.' ) dynamicparam { $paramDictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary if ($null -eq $Sample) { return } $sampleTemplateFolder = Join-Path -Path 'Templates' -ChildPath $Sample $templatePath = Join-Path -Path $MyInvocation.MyCommand.Module.ModuleBase -ChildPath $sampleTemplateFolder $previousErrorActionPreference = $ErrorActionPreference try { <# Let's convert non-terminating errors in this function to terminating so we catch and format the error message as a warning. #> $ErrorActionPreference = 'Stop' <# The constrained runspace is not available in the dynamicparam block. Shouldn't be needed since we are only evaluating the parameters in the manifest - no need for EvaluateConditionAttribute as we are not building up multiple parameter sets. And no need for EvaluateAttributeValue since we are only grabbing the parameter's value which is static. #> $templateAbsolutePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($TemplatePath) if (-not (Test-Path -LiteralPath $templateAbsolutePath -PathType 'Container')) { throw ("Can't find plaster template at {0}." -f $templateAbsolutePath) } $plasterModule = Get-Module -Name 'Plaster' <# Load manifest file using culture lookup (using Plaster module private function GetPlasterManifestPathForCulture). This is the current function that is called: https://github.com/PowerShellOrg/Plaster/blob/0506a26ffb532a335a4e62a8da31d9ca0177ae2a/src/InvokePlaster.ps1#L1478 #> $manifestPath = & $plasterModule { param ( [Parameter()] [System.String] $templateAbsolutePath, [Parameter()] [System.String] $Culture ) GetPlasterManifestPathForCulture -TemplatePath $templateAbsolutePath -Culture $Culture } $templateAbsolutePath $PSCulture if (($null -eq $manifestPath) -or (-not (Test-Path -Path $manifestPath))) { return } $manifest = Plaster\Test-PlasterManifest -Path $manifestPath -ErrorAction Stop 3>$null <# The user-defined parameters in the Plaster manifest are converted to dynamic parameters which allows the user to provide the parameters via the command line. This enables non-interactive use cases. #> foreach ($node in $manifest.plasterManifest.Parameters.ChildNodes) { if ($node -isnot [System.Xml.XmlElement]) { continue } $name = $node.name $type = $node.type if ($node.prompt) { $prompt = $node.prompt } else { $prompt = "Missing Parameter $name" } if (-not $name -or -not $type) { continue } # Configure ParameterAttribute and add to attr collection. $attributeCollection = New-Object -TypeName System.Collections.ObjectModel.Collection[System.Attribute] $paramAttribute = New-Object -TypeName System.Management.Automation.ParameterAttribute $paramAttribute.HelpMessage = $prompt $attributeCollection.Add($paramAttribute) switch -regex ($type) { 'text|user-fullname|user-email' { $param = [System.Management.Automation.RuntimeDefinedParameter]::new($name, [System.String], $attributeCollection) break } 'choice|multichoice' { $choiceNodes = $node.ChildNodes $setValues = New-Object -TypeName System.String[] -ArgumentList $choiceNodes.Count $i = 0 foreach ($choiceNode in $choiceNodes) { $setValues[$i++] = $choiceNode.value } $validateSetAttr = New-Object -TypeName System.Management.Automation.ValidateSetAttribute -ArgumentList $setValues $attributeCollection.Add($validateSetAttr) if ($type -eq 'multichoice') { $type = [System.String[]] } else { $type = [System.String] } $param = [System.Management.Automation.RuntimeDefinedParameter]::new($name, $type, $attributeCollection) break } default { throw "Unrecognized Parameter Type $type for attribute $name." } } $paramDictionary.Add($name, $param) } } catch { Write-Warning "Error processing Dynamic Parameters. $($_.Exception.Message)" } finally { $ErrorActionPreference = $previousErrorActionPreference } $paramDictionary } end { # Clone the the bound parameters. $plasterParameter = @{} + $PSBoundParameters $null = $plasterParameter.Remove('Sample') $sampleTemplateFolder = Join-Path -Path 'Templates' -ChildPath $Sample $templatePath = Join-Path -Path $MyInvocation.MyCommand.Module.ModuleBase -ChildPath $sampleTemplateFolder $plasterParameter.Add('TemplatePath', $templatePath) if (-not $plasterParameter.ContainsKey('DestinationPath')) { $plasterParameter['DestinationPath'] = $DestinationPath } Invoke-Plaster @plasterParameter } } #EndRegion './Public/Add-Sample.ps1' 229 #Region './Public/Get-BuildVersion.ps1' 0 <# .SYNOPSIS Calculates or retrieves the version of the Repository. .DESCRIPTION Attempts to retrieve the version associated with the repository or the module within the repository. If the Version is not provided, the preferred way is to use GitVersion if available, but alternatively it will locate a module manifest in the source folder and read its version. .PARAMETER ModuleManifestPath Path to the Module Manifest that should determine the version if GitVersion is not available. .PARAMETER ModuleVersion Provide the Version to be splitted and do not rely on GitVersion or the Module's manifest. .EXAMPLE Get-BuildVersion -ModuleManifestPath source\MyModule.psd1 #> function Get-BuildVersion { [CmdletBinding()] [OutputType([System.String])] param ( [Parameter(Mandatory = $true)] [System.String] $ModuleManifestPath, [Parameter()] [System.String] $ModuleVersion ) if ([System.String]::IsNullOrEmpty($ModuleVersion)) { Write-Verbose -Message 'Module version is not determined yet. Evaluating methods to get module version.' if ((Get-Command -Name 'gitversion' -ErrorAction 'SilentlyContinue')) { Write-Verbose -Message 'Using the version from GitVersion.' $ModuleVersion = (gitversion | ConvertFrom-Json -ErrorAction 'Stop').NuGetVersionV2 } else { Write-Verbose -Message ( "GitVersion is not installed. Trying to use the version from module manifest in path '{0}'." -f $ModuleManifestPath ) $moduleInfo = Import-PowerShellDataFile -Path $ModuleManifestPath -ErrorAction 'Stop' $ModuleVersion = $moduleInfo.ModuleVersion if ($moduleInfo.PrivateData.PSData.Prerelease) { $ModuleVersion = $ModuleVersion + '-' + $moduleInfo.PrivateData.PSData.Prerelease } } } $moduleVersionParts = Split-ModuleVersion -ModuleVersion $ModuleVersion Write-Verbose -Message ( "Current module version is '{0}'." -f $moduleVersionParts.ModuleVersion ) return $moduleVersionParts.ModuleVersion } #EndRegion './Public/Get-BuildVersion.ps1' 71 #Region './Public/Get-BuiltModuleVersion.ps1' 0 <# .SYNOPSIS Get the module version from the module built by Sampler. .DESCRIPTION Will read the ModuleVersion and PrivateData.PSData.Prerelease tag of the Module Manifest that has been built by Sampler, by looking into the OutputDirectory where the Project's Module should have been built. .PARAMETER OutputDirectory Output directory as defined by the Project. By default it is set to 'output' in a Sampler project. .PARAMETER ProjectName Name of the current project (See Get-SamplerProjectName). .EXAMPLE Get-BuiltModuleVersion -OutputDirectory 'output' -ProjectName Sampler #> function Get-BuiltModuleVersion { [CmdletBinding()] [OutputType([System.String])] param ( [Parameter()] [System.String] $OutputDirectory, [Parameter()] [System.String] $ProjectName ) $ModuleManifestPath = "$OutputDirectory/$ProjectName/*/$ProjectName.psd1" Write-Verbose -Message ( "Get the module version from module manifest in path '{0}'." -f $ModuleManifestPath ) $moduleInfo = Import-PowerShellDataFile -Path $ModuleManifestPath -ErrorAction 'Stop' $ModuleVersion = $moduleInfo.ModuleVersion if ($moduleInfo.PrivateData.PSData.Prerelease) { $ModuleVersion = $ModuleVersion + '-' + $moduleInfo.PrivateData.PSData.Prerelease } $moduleVersionParts = Split-ModuleVersion -ModuleVersion $ModuleVersion Write-Verbose -Message ( "Current module version is '{0}'." -f $moduleVersionParts.ModuleVersion ) return $moduleVersionParts.ModuleVersion } #EndRegion './Public/Get-BuiltModuleVersion.ps1' 59 #Region './Public/Get-ClassBasedResourceName.ps1' 0 <# .SYNOPSIS Get the Names of the Class-based DSC Resources defined in a file using AST. .DESCRIPTION This command returns all Class-based Resource Names in a file, by parsing the file and looking for classes with the [DscResource()] attribute. For MOF-based DSC Resources, look at the `Get-MofSchemaName` function. .PARAMETER Path Path of the file to parse and search the Class-Based DSC Resources. .EXAMPLE Get-ClassBasedResourceName -Path source/Classes/MyDscResource.ps1 Get-ClassBasedResourceName -Path (Join-Path -Path (Get-Module MyResourceModule).ModuleBase -ChildPath (Get-Module MyResourceModule).RootModule) #> function Get-ClassBasedResourceName { [CmdletBinding()] [OutputType([String[]])] param ( [Parameter(Mandatory = $true)] [Alias('FilePath')] [System.String] $Path ) $ast = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$null, [ref]$null) $classDefinition = $ast.FindAll( { ($args[0].GetType().Name -like "TypeDefinitionAst") -and ` ($args[0].Attributes.TypeName.Name -contains 'DscResource') }, $true ) return $classDefinition.Name } #EndRegion './Public/Get-ClassBasedResourceName.ps1' 45 #Region './Public/Get-CodeCoverageThreshold.ps1' 0 <# .SYNOPSIS Gets the CodeCoverageThreshod from Runtime parameter or from BuildInfo. .DESCRIPTION This function will override the CodeCoverageThreshold by the value provided at runtime if any. .PARAMETER RuntimeCodeCoverageThreshold Runtime value for the Pester CodeCoverageThreshold (can be $null). .PARAMETER BuildInfo BuildInfo object as defined by the Build.yml. .EXAMPLE Get-CodeCoverageThreshold -RuntimeCodeCoverageThreshold 0 #> function Get-CodeCoverageThreshold { [CmdletBinding()] [OutputType([System.String])] param ( [Parameter()] [System.String] [AllowNull()] $RuntimeCodeCoverageThreshold, [Parameter()] [PSObject] $BuildInfo ) # If no codeCoverageThreshold configured at runtime, look for BuildInfo settings. if ([String]::IsNullOrEmpty($RuntimeCodeCoverageThreshold)) { if ($BuildInfo.ContainsKey('Pester') -and $BuildInfo.Pester.ContainsKey('CodeCoverageThreshold')) { $codeCoverageThreshold = $BuildInfo.Pester.CodeCoverageThreshold Write-Debug -Message "Loaded Code Coverage Threshold from Config file: $codeCoverageThreshold %." } else { $codeCoverageThreshold = 0 Write-Debug -Message "No code coverage threshold value found (param nor config), using the default value." } } else { $codeCoverageThreshold = [int] $RuntimeCodeCoverageThreshold Write-Debug -Message "Loading CodeCoverage Threshold from Parameter ($codeCoverageThreshold %)." } return $codeCoverageThreshold } #EndRegion './Public/Get-CodeCoverageThreshold.ps1' 57 #Region './Public/Get-MofSchemaName.ps1' 0 <# .SYNOPSIS Gets the Name and Friendly Name of MOF-Based resources from their Schemas. .DESCRIPTION This function looks within a DSC resource's .MOF schema to find the name and friendly name of the class. .PARAMETER Path Path to the DSC Resource Schema MOF. .EXAMPLE Get-MofSchemaName -Path Source/DSCResources/MyResource/MyResource.schema.mof #> function Get-MofSchemaName { [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( [Parameter( Mandatory = $true, ValueFromPipeline = $true )] [System.String] $Path ) begin { $temporaryPath = $null # Determine the correct $env:TEMP drive switch ($true) { (-not (Test-Path -Path variable:IsWindows) -or $IsWindows) { # Windows PowerShell or PowerShell 6+ $temporaryPath = $env:TEMP } $IsMacOS { $temporaryPath = $env:TMPDIR throw 'NotImplemented: Currently there is an issue using the type [Microsoft.PowerShell.DesiredStateConfiguration.Internal.DscClassCache] on macOS. See issue https://github.com/PowerShell/PowerShell/issues/5970 and issue https://github.com/PowerShell/MMI/issues/33.' } $IsLinux { $temporaryPath = '/tmp' } default { throw 'Cannot set the temporary path. Unknown operating system.' } } $tempFilePath = Join-Path -Path $temporaryPath -ChildPath "DscMofHelper_$((New-Guid).Guid).tmp" } process { #region Workaround for OMI_BaseResource inheritance not resolving. $rawContent = (Get-Content -Path $Path -Raw) -replace '\s*:\s*OMI_BaseResource' Set-Content -LiteralPath $tempFilePath -Value $rawContent -ErrorAction 'Stop' # .NET methods don't like PowerShell drives $tempFilePath = Convert-Path -Path $tempFilePath #endregion try { $exceptionCollection = [System.Collections.ObjectModel.Collection[System.Exception]]::new() $moduleInfo = [System.Tuple]::Create('Module', [System.Version] '1.0.0') $class = [Microsoft.PowerShell.DesiredStateConfiguration.Internal.DscClassCache]::ImportClasses( $tempFilePath, $moduleInfo, $exceptionCollection ) } catch { Remove-Item -LiteralPath $tempFilePath -Force throw "Failed to import classes from file $Path. Error $_" } <# For most efficiency, we re-use the same temp file. We need to be sure that the file is empty before the next import. If no, we risk to import the same class twice. #> Set-Content -LiteralPath $tempFilePath -Value '' return @{ Name = $class.CimClassName FriendlyName = ($class.Cimclassqualifiers | Where-Object -FilterScript { $_.Name -eq 'FriendlyName' }).Value } } end { Remove-Item -LiteralPath $tempFilePath -Force } } #EndRegion './Public/Get-MofSchemaName.ps1' 108 #Region './Public/Get-OperatingSystemShortName.ps1' 0 <# .SYNOPSIS Returns the Platform name. .DESCRIPTION Gets whether the platform is Windows, Linux or MacOS. .EXAMPLE Get-OperatingSystemShortName # no Parameter needed .NOTES General notes #> function Get-OperatingSystemShortName { [CmdletBinding()] param () $osShortName = if ($IsWindows -or $PSVersionTable.PSVersion.Major -le 5) { 'Windows' } elseif ($IsMacOS) { 'MacOS' } else { 'Linux' } return $osShortName } #EndRegion './Public/Get-OperatingSystemShortName.ps1' 34 #Region './Public/Get-PesterOutputFileFileName.ps1' 0 <# .SYNOPSIS Gets a descriptive file name to be used as Pester Output file name. .DESCRIPTION Creates a file name to be used as Pester Output xml file composed like so: "${ProjectName}_v${ModuleVersion}.${OsShortName}.${PowerShellVersion}.xml" .PARAMETER ProjectName Name of the Project or module being built. .PARAMETER ModuleVersion Module Version currently defined (including pre-release but without the metadata). .PARAMETER OsShortName Platform name either Windows, Linux, or MacOS. .PARAMETER PowerShellVersion Version of PowerShell the tests have been running on. .EXAMPLE Get-PesterOutputFileFileName -ProjectName 'Sampler' -ModuleVersion 0.110.4-preview001 -OsShortName Windows -PowerShellVersion 5.1 .NOTES General notes #> function Get-PesterOutputFileFileName { param ( [Parameter(Mandatory = $true)] [System.String] $ProjectName, [Parameter(Mandatory = $true)] [System.String] $ModuleVersion, [Parameter(Mandatory = $true)] [System.String] $OsShortName, [Parameter(Mandatory = $true)] [System.String] $PowerShellVersion ) return '{0}_v{1}.{2}.{3}.xml' -f $ProjectName, $ModuleVersion, $OsShortName, $PowerShellVersion } #EndRegion './Public/Get-PesterOutputFileFileName.ps1' 50 #Region './Public/Get-SamplerCodeCoverageOutputFile.ps1' 0 <# .SYNOPSIS Resolves the CodeCoverage output file path from the project's BuildInfo. .DESCRIPTION When the Pester CodeCoverageOutputFile is configured in the buildinfo (aka Build.yml), this function will expand the path (if it contains variables), and resolve to it's absolute path if needed. .PARAMETER BuildInfo The BuildInfo object represented in the Build.yml. .PARAMETER PesterOutputFolder The Pester output folder (that can be overridden at runtime). .EXAMPLE Get-SamplerCodeCoverageOutputFile -BuildInfo $buildInfo -PesterOuputFolder 'C:\src\MyModule\Output\testResults #> function Get-SamplerCodeCoverageOutputFile { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [PSObject] $BuildInfo, [Parameter(Mandatory = $true)] [System.String] $PesterOutputFolder ) if ($BuildInfo.ContainsKey('Pester') -and $BuildInfo.Pester.ContainsKey('CodeCoverageOutputFile')) { $codeCoverageOutputFile = $executioncontext.invokecommand.expandstring($BuildInfo.Pester.CodeCoverageOutputFile) if (-not (Split-Path -IsAbsolute $codeCoverageOutputFile)) { $codeCoverageOutputFile = Join-Path -Path $PesterOutputFolder -ChildPath $codeCoverageOutputFile Write-Debug -Message "Absolute path to code coverage output file is $codeCoverageOutputFile." } } else { $codeCoverageOutputFile = $null } return $codeCoverageOutputFile } #EndRegion './Public/Get-SamplerCodeCoverageOutputFile.ps1' 52 #Region './Public/Get-SamplerCodeCoverageOutputFileEncoding.ps1' 0 <# .SYNOPSIS Returns the Configured encoding for Pester code coverage file from BuildInfo. .DESCRIPTION This function returns the CodeCoverageOutputFileEncoding (Pester v5+) as configured in the BuildInfo (build.yml). .PARAMETER BuildInfo Build Configuration object as defined in the Build.yml. .EXAMPLE Get-SamplerCodeCoverageOutputFileEncoding -BuildInfo $buildInfo #> function Get-SamplerCodeCoverageOutputFileEncoding { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [PSObject] $BuildInfo ) if ($BuildInfo.ContainsKey('Pester') -and $BuildInfo.Pester.ContainsKey('CodeCoverageOutputFileEncoding')) { $codeCoverageOutputFileEncoding = $BuildInfo.Pester.CodeCoverageOutputFileEncoding } else { $codeCoverageOutputFileEncoding = $null } return $codeCoverageOutputFileEncoding } #EndRegion './Public/Get-SamplerCodeCoverageOutputFileEncoding.ps1' 37 #Region './Public/Get-SamplerProjectName.ps1' 0 <# .SYNOPSIS Gets the Project Name based on the ModuleManifest if Available. .DESCRIPTION Finds the Module Manifest through `Get-SamplerProjectModuleManifest` and deduce ProjectName based on the BaseName of that manifest. .PARAMETER BuildRoot BuildRoot of the Sampler project to search the Module manifest from. .EXAMPLE Get-SamplerProjectName -BuildRoot 'C:\src\MyModule' #> function Get-SamplerProjectName { [CmdletBinding()] [OutputType([System.String])] param ( [Parameter(Mandatory = $true)] [System.String] $BuildRoot ) return (Get-SamplerProjectModuleManifest -BuildRoot $BuildRoot).BaseName } #EndRegion './Public/Get-SamplerProjectName.ps1' 30 #Region './Public/Get-SamplerSourcePath.ps1' 0 <# .SYNOPSIS Gets the project's source Path based on the ModuleManifest location. .DESCRIPTION By finding the ModuleManifest of the project using `Get-SamplerProjectModuleManifest` this function assumes that the source folder is the parent folder of that module manifest. This allows the source folder to be src, source, or the Module name's, without hardcoding the name. .PARAMETER BuildRoot BuildRoot of the Sampler project to search the Module manifest from. .EXAMPLE Get-SamplerSourcePath -BuildRoot 'C:\src\MyModule' #> function Get-SamplerSourcePath { [CmdletBinding()] [OutputType([System.String])] param ( [Parameter(Mandatory = $true)] [System.String] $BuildRoot ) return (Get-SamplerProjectModuleManifest -BuildRoot $BuildRoot).Directory.FullName } #EndRegion './Public/Get-SamplerSourcePath.ps1' 32 #Region './Public/Merge-JaCoCoReport.ps1' 0 <# .SYNOPSIS Merge two JaCoCoReports into one. .DESCRIPTION When you run tests independently for the same module, you may want to get a unified report of all the code paths that were tested. For instance, you want to get a unified report when the runs where done on Linux and Windows. This function helps merge the results of two runs into one file. If you have more than two reports, keep merging them. .PARAMETER OriginalDocument One of the JaCoCoReports you would like to merge. .PARAMETER MergeDocument Second JaCoCoReports you would like to merge with the other one. .EXAMPLE Merge-JaCoCoReport -OriginalDocument 'C:\src\MyModule\Output\JaCoCoRun_linux.xml' -MergeDocument 'C:\src\MyModule\Output\JaCoCoRun_windows.xml' .NOTES See also Update-JaCoCoStatistic Thanks to Yorick (@ykuijs) for this great feature! #> function Merge-JaCoCoReport { param ( [Parameter(Mandatory = $true)] [System.Xml.XmlDocument] $OriginalDocument, [Parameter(Mandatory = $true)] [System.Xml.XmlDocument] $MergeDocument ) foreach ($mPackage in $MergeDocument.report.package) { Write-Verbose " Processing package: $($mPackage.Name)" $oPackage = $OriginalDocument.report.package | Where-Object { $_.Name -eq $mPackage.Name } foreach ($mSourcefile in $mPackage.sourcefile) { Write-Verbose " Processing sourcefile: $($mSourcefile.Name)" if ($null -ne $oPackage) { foreach ($mPackageLine in $mSourcefile.line) { $oSourcefile = $oPackage.sourcefile | Where-Object { $_.name -eq $mSourcefile.name } $oPackageLine = $oSourcefile.line | Where-Object { $_.nr -eq $mPackageLine.nr } if ($null -eq $oPackageLine) { # Missed line in origin, covered in merge Write-Verbose " Adding line: $($mPackageLine.nr)" $null = $oPackage.sourcefile.AppendChild($oPackage.sourcefile.OwnerDocument.ImportNode($mPackageLine, $true)) continue } if (($oPackageLine.ci -eq 0) -and ($oPackageLine.mi -ne 0) -and ` ($mPackageLine.ci -ne 0) -and ($mPackageLine.mi -eq 0)) { # Missed line in origin, covered in merge Write-Verbose " Updating missed line: $($mPackageLine.nr)" $oPackageLine.ci = $mPackageLine.ci $oPackageLine.mi = $mPackageLine.mi continue } if ($oPackageLine.ci -lt $mPackageLine.ci) { # Missed line in origin, covered in merge Write-Verbose " Updating line: $($mPackageLine.nr)" $oPackageLine.ci = $mPackageLine.ci $oPackageLine.mi = $mPackageLine.mi continue } } } else { # New package, does not exist in origin. Add package. Write-Verbose " Package '$($mPackage.Name)' does not exist in original file. Adding..." foreach ($xmlElement in $OriginalDocument.report) { if ($xmlElement -is [System.Xml.XmlElement]) { $null = $xmlElement.AppendChild($OriginalDocument.report.OwnerDocument.ImportNode($mPackage, $true)) break } } } } } return $OriginalDocument } #EndRegion './Public/Merge-JaCoCoReport.ps1' 101 #Region './Public/New-SampleModule.ps1' 0 <# .SYNOPSIS Create a module scaffolding and add samples & build pipeline. .DESCRIPTION New-SampleModule helps you bootstrap your PowerShell module project by creating a the folder structure of your module, and optionally add the pipeline files to help with compiling the module, publishing to PSGallery and GitHub and testing quality and style such as per the DSC Community guildelines. .PARAMETER DestinationPath Destination of your module source root folder, defaults to the current directory ".". We assume that your current location is the module folder, and within this folder we will find the source folder, the tests folder and other supporting files. .PARAMETER ModuleType Specifies the type of module to create. The default value is 'SimpleModule'. Preset of module you would like to create: - CompleteSample - SimpleModule - SimpleModule_NoBuild - dsccommunity .PARAMETER ModuleAuthor The author of module that will be populated in the Module Manifest and will show in the Gallery. .PARAMETER ModuleName The Name of your Module. .PARAMETER ModuleDescription The Description of your Module, to be used in your Module manifest. .PARAMETER CustomRepo The Custom PS repository if you want to use an internal (private) feed to pull for dependencies. .PARAMETER ModuleVersion Version you want to set in your Module Manifest. If you follow our approach, this will be updated during compilation anyway. .PARAMETER LicenseType Type of license you would like to add to your repository. We recommend MIT for Open Source projects. .PARAMETER SourceDirectory How you would like to call your Source repository to differentiate from the output and the tests folder. We recommend to call it 'source', and the default value is 'source'. .PARAMETER Features If you'd rather select specific features from this template to build your module, use this parameter instead. .EXAMPLE C:\src> New-SampleModule -DestinationPath . -ModuleType CompleteSample -ModuleAuthor "Gael Colas" -ModuleName MyModule -ModuleVersion 0.0.1 -ModuleDescription "a sample module" -LicenseType MIT -SourceDirectory Source .NOTES See Add-Sample to add elements such as functions (private or public), tests, DSC Resources to your project. #> function New-SampleModule { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] [CmdletBinding(DefaultParameterSetName = 'ByModuleType')] [OutputType([System.Void])] param ( [Parameter(Mandatory = $true)] [Alias('Path')] [System.String] $DestinationPath, [Parameter(ParameterSetName = 'ByModuleType')] [string] [ValidateSet('SimpleModule', 'CompleteSample', 'SimpleModule_NoBuild', 'newDscCommunity', 'dsccommunity')] $ModuleType = 'SimpleModule', [Parameter()] [System.String] $ModuleAuthor = $env:USERNAME, [Parameter(Mandatory = $true)] [System.String] $ModuleName, [Parameter()] [AllowNull()] [System.String] $ModuleDescription, [Parameter()] [System.String] $CustomRepo = 'PSGallery', [Parameter()] [System.String] $ModuleVersion = '0.0.1', [Parameter()] [System.String] [ValidateSet('MIT','Apache','None')] $LicenseType = 'MIT', [Parameter()] [System.String] [ValidateSet('source','src')] $SourceDirectory = 'source', [Parameter(ParameterSetName = 'ByFeature')] [ValidateSet( 'All', 'Enum', 'Classes', 'DSCResources', 'ClassDSCResource', 'SampleScripts', 'git', 'Gherkin', 'UnitTests', 'ModuleQuality', 'Build', 'AppVeyor', 'TestKitchen' )] [System.String[]] $Features ) $templateSubPath = 'Templates/Sampler' $samplerBase = $MyInvocation.MyCommand.Module.ModuleBase $invokePlasterParam = @{ TemplatePath = Join-Path -Path $samplerBase -ChildPath $templateSubPath DestinationPath = $DestinationPath NoLogo = $true ModuleName = $ModuleName } foreach ($paramName in $MyInvocation.MyCommand.Parameters.Keys) { $paramValue = Get-Variable -Name $paramName -ValueOnly -ErrorAction SilentlyContinue # if $paramName is $null, leave it to Plaster to ask the user. if ($paramvalue -and -not $invokePlasterParam.ContainsKey($paramName)) { $invokePlasterParam.Add($paramName, $paramValue) } } if ($LicenseType -eq 'none') { $invokePlasterParam.Remove('LicenseType') $invokePlasterParam.add('License', 'false') } else { $invokePlasterParam.add('License', 'true') } Invoke-Plaster @invokePlasterParam } #EndRegion './Public/New-SampleModule.ps1' 157 #Region './Public/Split-ModuleVersion.ps1' 0 <# .SYNOPSIS Parse a SemVer2 Version string. .DESCRIPTION This function parses a SemVer (semver.org) version string into an object with the following properties: - Version: The version without tag or metadata, as used by folder versioning in PowerShell modules. - PreReleaseString: A Publish-Module compliant prerelease tag (see below). - ModuleVersion: The Version and Prerelease tag compliant with Publish-Module. For instance, this is a valid SemVer: `1.15.0-pr0224-0022+Sha.47ae45eb2cfed02b249f239a7c55e5c71b26ab76.Date.2020-01-07` The Metadata is stripped: `1.15.0-pr0224-0022` The Version is `1.15.0`. The prerelease tag is `-pr0224-0022` However, Publish-Module (or NuGet/PSGallery) does not support such pre-release, so this function only keep the first part `-pr0224` .PARAMETER ModuleVersion Full SemVer version string with (optional) metadata and prerelease tag to be parsed. .EXAMPLE Split-ModuleVersion -ModuleVersion '1.15.0-pr0224-0022+Sha.47ae45eb2cfed02b249f239a7c55e5c71b26ab76.Date.2020-01-07' # Version PreReleaseString ModuleVersion # ------- ---------------- ------------- # 1.15.0 pr0224 1.15.0-pr0224 #> function Split-ModuleVersion { [CmdletBinding()] [OutputType([System.Management.Automation.PSCustomObject])] param ( [Parameter()] [System.String] $ModuleVersion ) <# This handles a previous version of the module that suggested to pass a version string with metadata in the CI pipeline that can look like this: 1.15.0-pr0224-0022+Sha.47ae45eb2cfed02b249f239a7c55e5c71b26ab76.Date.2020-01-07 #> $ModuleVersion = ($ModuleVersion -split '\+', 2)[0] $moduleVersion, $preReleaseString = $ModuleVersion -split '-', 2 <# The cmldet Publish-Module does not yet support semver compliant pre-release strings. If the prerelease string contains a dash ('-') then the dash and everything behind is removed. For example 'pr54-0012' is parsed to 'pr54'. #> $validPreReleaseString, $preReleaseStringSuffix = $preReleaseString -split '-' if ($validPreReleaseString) { $fullModuleVersion = $moduleVersion + '-' + $validPreReleaseString } else { $fullModuleVersion = $moduleVersion } $moduleVersionParts = [PSCustomObject]@{ Version = $moduleVersion PreReleaseString = $validPreReleaseString ModuleVersion = $fullModuleVersion } return $moduleVersionParts } #EndRegion './Public/Split-ModuleVersion.ps1' 75 #Region './Public/Update-JaCoCoStatistic.ps1' 0 <# .SYNOPSIS Update the Statistics of a freshly merged JaCoCoReports. .DESCRIPTION When you merge two or several JaCoCoReports together using the Merge-JaCoCoReport, the calculated statistics of the Original document are not updated. This Command will re-calculate the JaCoCo statistics and update the Document. For the Package, Class, Method of all source files and the total it will update: - the Instruction Covered - the Instruction Missed - the Line Covered - the Line Missed - the Method Covered - the Method Missed - the Class Covered - the Class Missed .PARAMETER Document JaCoCo report XML document that needs its statistics recalculated. .EXAMPLE Update-JaCoCoStatistic -Document (Merge-JaCoCoReport $file1 $file2) .NOTES See also Merge-JaCoCoReport Thanks to Yorick (@ykuijs) for this great feature! #> function Update-JaCoCoStatistic { [CmdletBinding()] [OutputType([System.Xml.XmlDocument])] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] param ( [Parameter(Mandatory = $true)] [System.Xml.XmlDocument] $Document ) Write-Verbose "Start updating statistics!" $totalInstructionCovered = 0 $totalInstructionMissed = 0 $totalLineCovered = 0 $totalLineMissed = 0 $totalMethodCovered = 0 $totalMethodMissed = 0 $totalClassCovered = 0 $totalClassMissed = 0 foreach ($oPackage in $Document.report.package) { Write-Verbose "Processing package $($oPackage.name)" $packageInstructionCovered = 0 $packageInstructionMissed = 0 $packageLineCovered = 0 $packageLineMissed = 0 $packageMethodCovered = 0 $packageMethodMissed = 0 $packageClassCovered = 0 $packageClassMissed = 0 foreach ($oPackageClass in $oPackage.class) { $classInstructionCovered = 0 $classInstructionMissed = 0 $classLineCovered = 0 $classLineMissed = 0 $classMethodCovered = 0 $classMethodMissed = 0 Write-Verbose " Processing sourcefile $($oPackageClass.sourcefilename)" $oPackageSourcefile = $oPackage.sourcefile | Where-Object -FilterScript { $_.Name -eq $oPackageClass.sourcefilename } $oneMethodProcessed = $false for ($i = 0; $i -lt ([array]($oPackageClass.method)).Count; $i++) { $methodInstructionCovered = 0 $methodInstructionMissed = 0 $methodLineCovered = 0 $methodLineMissed = 0 $methodCovered = 0 $methodMissed = 0 $currentMethod = [array]$oPackageClass.method $start = $currentMethod[$i].line if ($i -ne ($currentMethod.Count - 1)) { $end = $currentMethod[$i+1].Line Write-Verbose " Processing method: $($currentMethod[$i].Name)" [array]$coll = $oPackageSourcefile.line | Where-Object { [int]$_.nr -ge $start -and [int]$_.nr -lt $end } foreach ($line in $coll) { $methodInstructionCovered += $line.ci $methodInstructionMissed += $line.mi } [array]$cov = $coll | Where-Object -FilterScript { $_.ci -ne "0" } $methodLineCovered = $cov.Count [array]$mis = $coll | Where-Object -FilterScript { $_.ci -eq "0" } $methodLineMissed = $mis.Count } else { Write-Verbose " Processing method: $($currentMethod[$i].Name)" [array]$coll = $oPackageSourcefile.line | Where-Object { [int]$_.nr -ge $start } foreach ($line in $coll) { $methodInstructionCovered += $line.ci $methodInstructionMissed += $line.mi } [array]$cov = $coll | Where-Object -FilterScript { $_.ci -ne "0" } $methodLineCovered = $cov.Count [array]$mis = $coll | Where-Object -FilterScript { $_.ci -eq "0" } $methodLineMissed = $mis.Count } $classInstructionCovered += $methodInstructionCovered $classInstructionMissed += $methodInstructionMissed $classLineCovered += $methodLineCovered $classLineMissed += $methodLineMissed if ($methodInstructionCovered -ne 0) { $methodCovered = 1 $methodMissed = 0 $classMethodCovered++ } else { $methodCovered = 0 $methodMissed = 1 $classMethodMissed++ } if ($currentMethod[$i].Name -ne '<script>' -and $methodMissed -eq 0) { $oneMethodProcessed = $true } # Update Method stats $counterInstruction = $currentMethod[$i].counter | Where-Object { $_.type -eq 'INSTRUCTION' } $counterInstruction.covered = [string]$methodInstructionCovered $counterInstruction.missed = [string]$methodInstructionMissed $counterLine = $currentMethod[$i].counter | Where-Object { $_.type -eq 'LINE' } $counterLine.covered = [string]$methodLineCovered $counterLine.missed = [string]$methodLineMissed $counterMethod = $currentMethod[$i].counter | Where-Object { $_.type -eq 'METHOD' } $counterMethod.covered = [string]$methodCovered $counterMethod.missed = [string]$methodMissed Write-Verbose " Method Instruction Covered : $methodInstructionCovered" Write-Verbose " Method Instruction Missed : $methodInstructionMissed" Write-Verbose " Method Line Covered : $methodLineCovered" Write-Verbose " Method Line Missed : $methodLineMissed" Write-Verbose " Method Covered : $methodCovered" Write-Verbose " Method Missed : $methodMissed" } $packageInstructionCovered += $classInstructionCovered $packageInstructionMissed += $classInstructionMissed $packageLineCovered += $classLineCovered $packageLineMissed += $classLineMissed $packageMethodCovered += $classMethodCovered $packageMethodMissed += $classMethodMissed if ($oneMethodProcessed -eq $true) { $packageClassCovered++ $classClassCovered = 1 $classClassMissed = 0 } else { $classClassCovered = 0 $classClassMissed = 1 } # Update Class stats $counterInstruction = $oPackageClass.counter | Where-Object { $_.type -eq 'INSTRUCTION' } $counterInstruction.covered = [string]$classInstructionCovered $counterInstruction.missed = [string]$classInstructionMissed $counterLine = $oPackageClass.counter | Where-Object { $_.type -eq 'LINE' } $counterLine.covered = [string]$classLineCovered $counterLine.missed = [string]$classLineMissed $counterMethod = $oPackageClass.counter | Where-Object { $_.type -eq 'METHOD' } $counterMethod.covered = [string]$classMethodCovered $counterMethod.missed = [string]$classMethodMissed $counterMethod = $oPackageClass.counter | Where-Object { $_.type -eq 'CLASS' } $counterMethod.covered = [string]$classClassCovered $counterMethod.missed = [string]$classClassMissed # Update Sourcefile stats $counterInstruction = $oPackageSourcefile.counter | Where-Object { $_.type -eq 'INSTRUCTION' } $counterInstruction.covered = [string]$classInstructionCovered $counterInstruction.missed = [string]$classInstructionMissed $counterLine = $oPackageSourcefile.counter | Where-Object { $_.type -eq 'LINE' } $counterLine.covered = [string]$classLineCovered $counterLine.missed = [string]$classLineMissed $counterMethod = $oPackageSourcefile.counter | Where-Object { $_.type -eq 'METHOD' } $counterMethod.covered = [string]$classMethodCovered $counterMethod.missed = [string]$classMethodMissed $counterMethod = $oPackageSourcefile.counter | Where-Object { $_.type -eq 'CLASS' } $counterMethod.covered = [string]$classClassCovered $counterMethod.missed = [string]$classClassMissed Write-Verbose " Class Instruction Covered : $classInstructionCovered" Write-Verbose " Class Instruction Missed : $classInstructionMissed" Write-Verbose " Class Line Covered : $classLineCovered" Write-Verbose " Class Line Missed : $classLineMissed" Write-Verbose " Class Method Covered : $classMethodCovered" Write-Verbose " Class Method Missed : $classMethodMissed" } $totalInstructionCovered += $packageInstructionCovered $totalInstructionMissed += $packageInstructionMissed $totalLineCovered += $packageLineCovered $totalLineMissed += $packageLineMissed $totalMethodCovered += $packageMethodCovered $totalMethodMissed += $packageMethodMissed $totalClassCovered += $packageClassCovered $totalClassMissed += $packageClassMissed # Update Package stats $counterInstruction = $oPackage.counter | Where-Object { $_.type -eq 'INSTRUCTION' } $counterInstruction.covered = [string]$packageInstructionCovered $counterInstruction.missed = [string]$packageInstructionMissed $counterLine = $oPackage.counter | Where-Object { $_.type -eq 'LINE' } $counterLine.covered = [string]$packageLineCovered $counterLine.missed = [string]$packageLineMissed $counterMethod = $oPackage.counter | Where-Object { $_.type -eq 'METHOD' } $counterMethod.covered = [string]$packageMethodCovered $counterMethod.missed = [string]$packageMethodMissed $counterClass = $oPackage.counter | Where-Object { $_.type -eq 'CLASS' } $counterClass.covered = [string]$packageClassCovered $counterClass.missed = [string]$packageClassMissed Write-Verbose " Package Instruction Covered: $packageInstructionCovered" Write-Verbose " Package Instruction Missed : $packageInstructionMissed" Write-Verbose " Package Line Covered : $packageLineCovered" Write-Verbose " Package Line Missed : $packageLineMissed" Write-Verbose " Package Method Covered : $packageMethodCovered" Write-Verbose " Package Method Missed : $packageMethodMissed" Write-Verbose " Package Class Covered : $packageClassCovered" Write-Verbose " Package Class Missed : $packageClassMissed" } #Update Total stats $counterInstruction = $Document.report.counter | Where-Object { $_.type -eq 'INSTRUCTION' } $counterInstruction.covered = [string]$totalInstructionCovered $counterInstruction.missed = [string]$totalInstructionMissed $counterLine = $Document.report.counter | Where-Object { $_.type -eq 'LINE' } $counterLine.covered = [string]$totalLineCovered $counterLine.missed = [string]$totalLineMissed $counterMethod = $Document.report.counter | Where-Object { $_.type -eq 'METHOD' } $counterMethod.covered = [string]$totalMethodCovered $counterMethod.missed = [string]$totalMethodMissed $counterClass = $Document.report.counter | Where-Object { $_.type -eq 'CLASS' } $counterClass.covered = [string]$totalClassCovered $counterClass.missed = [string]$totalClassMissed Write-Verbose "----------------------------------------" Write-Verbose " Totals" Write-Verbose "----------------------------------------" Write-Verbose " Total Instruction Covered : $totalInstructionCovered" Write-Verbose " Total Instruction Missed : $totalInstructionMissed" Write-Verbose " Total Line Covered : $totalLineCovered" Write-Verbose " Total Line Missed : $totalLineMissed" Write-Verbose " Total Method Covered : $totalMethodCovered" Write-Verbose " Total Method Missed : $totalMethodMissed" Write-Verbose " Total Class Covered : $totalClassCovered" Write-Verbose " Total Class Missed : $totalClassMissed" Write-Verbose "----------------------------------------" Write-Verbose "Completed merging files and updating statistics!" return $Document } #EndRegion './Public/Update-JaCoCoStatistic.ps1' 305 #Region './suffix.ps1' 0 # Inspired from https://github.com/nightroman/Invoke-Build/blob/64f3434e1daa806814852049771f4b7d3ec4d3a3/Tasks/Import/README.md#example-2-import-from-a-module-with-tasks Get-ChildItem -Path (Join-Path -Path $PSScriptRoot -ChildPath 'tasks\*') -Include '*.build.*' | ForEach-Object -Process { $ModuleName = ([System.IO.FileInfo] $MyInvocation.MyCommand.Name).BaseName $taskFileAliasName = "$($_.BaseName).$ModuleName.ib.tasks" Set-Alias -Name $taskFileAliasName -Value $_.FullName Export-ModuleMember -Alias $taskFileAliasName } #EndRegion './suffix.ps1' 11 |