diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e9286f --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +/output/ +/debug*.ps1 +/temp*.ps1 + +# Most of these heavily cannibalized by Travis Drake from https://gist.github.com/kmorcinek/2710267 + +# C# VS build detritus +/.vs/ +/*/[Bb]in/ +/*/[Oo]bj/ +**/packages/* + +# Except this, this needs to be checked in when present +!**/packages/build/ + +# More C# / Visual Studio detritus +*.suo +*.user +*.sln.docstates +*.psess +*.vsp +*.vspx +project.lock.json +_UpgradeReport_Files/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SASS Compiler cache +.sass-cache + +# Mac OS stuff +.DS_Store* +Icon? + +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ diff --git a/.vscode/settings.json b/.vscode/settings.json index 31de61a..0df401f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,28 @@ // Place your settings in this file to overwrite default and user settings. { - "powershell.codeFormatting.preset": "Allman" -} \ No newline at end of file + //-------- Editor configuration -------- + "editor.insertSpaces": true, + "editor.tabSize": 4, + + //-------- Files configuration -------- + "files.autoGuessEncoding": false, + "files.insertFinalNewline": true, + "files.trimTrailingWhitespace": true, + "search.exclude": { + "**/Tests/Data*": true + }, + + //-------- PowerShell configuration -------- + "powershell.codeFormatting.alignPropertyValuePairs": true, + "powershell.codeFormatting.preset": "Allman", + "powershell.scriptAnalysis.settingsPath": "./ScriptAnalyzerSettings.psd1", + + //-------- Language configuration -------- + "[json]": { + "editor.tabSize": 2 + }, + + "[xml]": { + "editor.tabSize": 2 + } +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..8845dfa --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,95 @@ +// Available variables which can be used inside of strings: +// ${workspaceRoot}: The root folder of the team +// ${file}: The current opened file +// ${relativeFile}: The current opened file relative to workspaceRoot +// ${fileBasename}: The current opened file's basename +// ${fileDirname}: The current opened file's dirname +// ${fileExtname}: The current opened file's extension +// ${cwd}: The current working directory of the spawned process +{ + "version": "2.0.0", + "windows": { + "options": { + "shell": { + "executable": "powershell.exe", + "args": [ "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command" ] + } + } + }, + "linux": { + "options": { + "shell": { + "executable": "/usr/bin/pwsh", + "args": [ "-NoProfile", "-Command" ] + } + } + }, + "osx": { + "options": { + "shell": { + "executable": "/usr/local/bin/pwsh", + "args": [ "-NoProfile", "-Command" ] + } + } + }, + "tasks": [ + { + "label": "Default", + "type": "shell", + "problemMatcher": [ "$msCompile" ], + "group": { + "kind": "build", + "isDefault": true + }, + "command": "Invoke-Build -Task Default -File './Module.build.ps1'" + }, + { + "label": "Analyze", + "type": "shell", + "problemMatcher": [ "$msCompile" ], + "command": "Invoke-Build -Task Analyze -File './Module.build.ps1'" + }, + { + "label": "Build", + "type": "shell", + "problemMatcher": [ "$msCompile" ], + "command": "Invoke-Build -Task Build -File './Module.build.ps1'" + }, + { + "label": "Clean", + "type": "shell", + "problemMatcher": [ "$msCompile" ], + "command": "Invoke-Build -Task Clean -File './Module.build.ps1'" + }, + { + "label": "Helpify", + "type": "shell", + "problemMatcher": [ "$msCompile" ], + "command": "Invoke-Build -Task Helpify -File './Module.build.ps1'" + }, + { + "label": "Install", + "type": "shell", + "problemMatcher": [ "$msCompile" ], + "command": "Invoke-Build -Task Install -File './Module.build.ps1'" + }, + { + "label": "Test", + "type": "shell", + "problemMatcher": [ "$msCompile" ], + "command": "Invoke-Build -Task Test -File './Module.build.ps1'" + }, + { + "label": "Uninstall", + "type": "shell", + "problemMatcher": [ "$msCompile" ], + "command": "Invoke-Build -Task Uninstall -File './Module.build.ps1'" + }, + { + "label": "?", + "type": "shell", + "problemMatcher": [], + "command": "Invoke-Build -Task ? -File './Module.build.ps1'" + } + ] +} diff --git a/BuildTasks/Analyze.Task.ps1 b/BuildTasks/Analyze.Task.ps1 new file mode 100644 index 0000000..73825e3 --- /dev/null +++ b/BuildTasks/Analyze.Task.ps1 @@ -0,0 +1,17 @@ +task Analyze { + $params = @{ + IncludeDefaultRules = $true + Path = $ManifestPath + Settings = "$BuildRoot\ScriptAnalyzerSettings.psd1" + Severity = 'Warning' + } + + "Analyzing $ManifestPath..." + $results = Invoke-ScriptAnalyzer @params + if ($results) + { + 'One or more PSScriptAnalyzer errors/warnings were found.' + 'Please investigate or add the required SuppressMessage attribute.' + $results | Format-Table -AutoSize + } +} diff --git a/BuildTasks/BuildManifest.Task.ps1 b/BuildTasks/BuildManifest.Task.ps1 new file mode 100644 index 0000000..343e0e3 --- /dev/null +++ b/BuildTasks/BuildManifest.Task.ps1 @@ -0,0 +1,18 @@ + +taskx BuildManifest @{ + Inputs = (Get-ChildItem -Path $Source -Recurse -File) + Outputs = $ManifestPath + Jobs = { + "Updating [$ManifestPath]..." + Copy-Item -Path "$Source\$ModuleName.psd1" -Destination $ManifestPath + + $functions = Get-ChildItem -Path "$ModuleName\Public\*.ps1" -ErrorAction 'Ignore' | + Where-Object 'Name' -notmatch 'Tests' + + if ($functions) + { + 'Setting FunctionsToExport...' + Set-ModuleFunctions -Name $ManifestPath -FunctionsToExport $functions.BaseName + } + } +} diff --git a/BuildTasks/BuildModule.Task.ps1 b/BuildTasks/BuildModule.Task.ps1 new file mode 100644 index 0000000..5b3247c --- /dev/null +++ b/BuildTasks/BuildModule.Task.ps1 @@ -0,0 +1,214 @@ + +# namespaces for Move-Statement +using namespace System.Collections.Generic +using namespace System.IO +using namespace System.Management.Automation + +function Import-ClassOrder +{ + [cmdletbinding()] + param($cName,$Map) + Write-Verbose "Checking on [$cName]" + if($Map.ContainsKey($cName) -and $Map[$cName].Imported -ne $true) + { + if($Map[$cName].Base) + { + Write-Verbose " Base class [$($Map[$cName].Base)]" + Import-ClassOrder $Map[$cName].Base $Map + } + $cPath = $Map[$cName].Path + Write-Verbose "Dot Sourcing [$cPath]" + $cPath + $Map[$cName].Imported = $true + } +} + +# Temporarily added this here to be refactored/replaced by LDModuleBuilder Module + + +function Move-Statement +{ +<# +.SYNOPSIS + Moves statements containing a specified token to the specified index in a file. +.DESCRIPTION + Move-Statement moves statements containing a specified token, to the specified index + in a file. This can be used when building a module to move any using directives and + #Requires statements to the top of a file. +.PARAMETER Path + Specifies the path to an item to get its contents. +.PARAMETER Type + Specifies the type of tokens to examine. Accepted values include "Comment" and "Keyword". +.PARAMETER Token + Specifies the contents to filter on when examining a supplied token. +.PARAMETER Index + Specifies the line to move a statement to. Each line in an item has a corresponding + index, starting from 0. +.EXAMPLE + Move-Statement -Path $Path -Type 'Comment', 'Keyword' -Token '#Requires', 'using' -Index 0 + + Moves any using directives or #Requires statements to the top of a file. +.NOTES + Copy/Paste from LDModuleBuilder +#> + [CmdletBinding(SupportsShouldProcess)] + + param( + [Parameter(Mandatory, + Position = 0, + ValueFromPipeline, + ValueFromPipelineByPropertyName)] + [ValidateNotNullOrEmpty()] + [ValidateScript({ Test-Path -Path $PSItem })] + [string] $Path, + + [Parameter(Position = 1, + ValueFromPipelineByPropertyName)] + [ValidateNotNullOrEmpty()] + [ValidateSet('Comment', 'Keyword')] + [string[]] $Type = ('Comment', 'Keyword'), + + [Parameter(Position = 2, + ValueFromPipelineByPropertyName)] + [ValidateNotNullOrEmpty()] + [string[]] $Token = ('#Requires', 'using'), + + [Parameter(Position = 3, + ValueFromPipelineByPropertyName)] + [ValidateNotNullOrEmpty()] + [int] $Index = 0 + ) + + process + { + try + { + $statements = [SortedSet[String]]::new( + [StringComparer]::InvariantCultureIgnoreCase + ) + + Write-Verbose -Message "Reading content from $Path..." + $content = [List[String]] ([File]::ReadLines($Path)) + + Write-Verbose -Message "Tokenizing content from $Path..." + $tokens = [PSParser]::Tokenize($content, [ref] $null) + + $match = $Token -join '|' + + Write-Verbose -Message 'Matching tokens...' + Write-Verbose -Message "Type = [$Type]; Token = [$Token]" + $keywords = $tokens.Where({ + $PSItem.Type -in $Type -and + $PSItem.Content -imatch "^(?:$match)" + }) + + if (-not $keywords) { + Write-Verbose -Message 'No matching tokens found! Returning...' + return + } + + $offset = 1 + foreach ($keyword in $keywords) + { + $line = $keyword.StartLine - $offset + + Write-Verbose -Message "Moving [$($content[$line])] to Index [$Index]..." + $null = $statements.Add($content[$line]), + $content.RemoveAt($line) + $offset++ + } + + [string[]] $comments, [string[]] $statements = $statements.Where({ + $PSItem -match '^#' + }, 'Split') + + foreach ($item in ($statements, $comments)) + { + $content.Insert($Index, '') + $content.InsertRange($Index, $item) + } + + if ($PSCmdlet.ShouldProcess($Path, $MyInvocation.MyCommand.Name)) + { + Write-Verbose -Message "Writing content to $Path..." + [File]::WriteAllLines($Path, $content) + } + } + catch + { + $PSCmdlet.ThrowTerminatingError($PSItem) + } + } +} + +taskx BuildModule @{ + Inputs = (Get-ChildItem -Path $Source -Recurse -Filter *.ps1) + Outputs = $ModulePath + Jobs = { + $sb = [Text.StringBuilder]::new() + $null = $sb.AppendLine('$Script:PSModuleRoot = $PSScriptRoot') + + # Class importer + $root = Join-Path -Path $source -ChildPath 'Classes' + "Load classes from [$root]" + $classFiles = Get-ChildItem -Path $root -Filter '*.ps1' -Recurse | + Where-Object Name -notlike '*.Tests.ps1' + + $classes = @{} + + foreach($file in $classFiles) + { + $name = $file.BaseName + $classes[$name] = @{ + Name = $name + Path = $file.FullName + } + $data = Get-Content $file.fullname + foreach($line in $data) + { + if($line -match "\s+($Name)\s*(:|requires)\s*(?\w*)") + { + $classes[$name].Base = $Matches.baseclass + } + } + } + + $importOrder = foreach($className in $classes.Keys) + { + Import-ClassOrder $className $classes + } + + foreach($class in $importOrder) + { + "Importing [$class]..." + $null = $sb.AppendLine("# .$class") + $null = $sb.AppendLine([IO.File]::ReadAllText($class)) + } + + foreach ($folder in ($Folders -ne 'Classes')) + { + if (Test-Path -Path "$Source\$folder") + { + $null = $sb.AppendLine("# Importing from [$Source\$folder]") + $files = Get-ChildItem -Path "$Source\$folder\*.ps1" | + Where-Object 'Name' -notlike '*.Tests.ps1' + + foreach ($file in $files) + { + $name = $file.Fullname.Replace($buildroot, '') + + "Importing [$($file.FullName)]..." + $null = $sb.AppendLine("# .$name") + $null = $sb.AppendLine([IO.File]::ReadAllText($file.FullName)) + } + } + } + + "Creating Module [$ModulePath]..." + $null = New-Item -Path (Split-path $ModulePath) -ItemType Directory -ErrorAction SilentlyContinue -Force + Set-Content -Path $ModulePath -Value $sb.ToString() -Encoding 'UTF8' + + 'Moving "#Requires" statements and "using" directives...' + Move-Statement -Path $ModulePath -Type 'Comment', 'Keyword' -Token '#Requires', 'using' -Index 0 + } +} diff --git a/BuildTasks/Clean.Task.ps1 b/BuildTasks/Clean.Task.ps1 new file mode 100644 index 0000000..7ccead5 --- /dev/null +++ b/BuildTasks/Clean.Task.ps1 @@ -0,0 +1,12 @@ +task Clean { + if (Test-Path $Output) + { + "Cleaning Output files in [$Output]..." + $null = Get-ChildItem -Path $Output -File -Recurse | + Remove-Item -Force -ErrorAction 'Ignore' + + "Cleaning Output directories in [$Output]..." + $null = Get-ChildItem -Path $Output -Directory -Recurse | + Remove-Item -Recurse -Force -ErrorAction 'Ignore' + } +} diff --git a/BuildTasks/Compile.Task.ps1 b/BuildTasks/Compile.Task.ps1 new file mode 100644 index 0000000..28d1886 --- /dev/null +++ b/BuildTasks/Compile.Task.ps1 @@ -0,0 +1,14 @@ +taskx Compile @{ + If = (Get-ChildItem -Path $BuildRoot -Include *.csproj -Recurse) + Inputs = { + Get-ChildItem $BuildRoot -Recurse -File -Include *.cs + } + Outputs = "$Destination\bin\$ModuleName.dll" + Jobs = { + # This build command requires .Net Core + "Building Module" + $csproj = Get-ChildItem -Path $BuildRoot -Include *.csproj -Recurse + $folder = Split-Path $csproj + dotnet build $folder -c Release -o $Destination\bin + } +} diff --git a/BuildTasks/Copy.Task.ps1 b/BuildTasks/Copy.Task.ps1 new file mode 100644 index 0000000..2daadd3 --- /dev/null +++ b/BuildTasks/Copy.Task.ps1 @@ -0,0 +1,29 @@ + +task Copy { + "Creating Directory [$Destination]..." + $null = New-Item -ItemType 'Directory' -Path $Destination -ErrorAction 'Ignore' + + $files = Get-ChildItem -Path $Source -File | + Where-Object 'Name' -notmatch "$ModuleName\.ps[dm]1" + + foreach ($file in $files) + { + 'Creating [.{0}]...' -f $file.FullName.Replace($buildroot, '') + Copy-Item -Path $file.FullName -Destination $Destination -Force + } + + $directories = Get-ChildItem -Path $Source -Directory | + Where-Object 'Name' -notin $Folders + + foreach ($directory in $directories) + { + 'Creating [.{0}]...' -f $directory.FullName.Replace($buildroot, '') + Copy-Item -Path $directory.FullName -Destination $Destination -Recurse -Force + } + + $license = Join-Path -Path $buildroot -ChildPath 'LICENSE' + if ( Test-Path -Path $license -PathType Leaf ) + { + Copy-Item -Path $license -Destination $Destination + } +} diff --git a/BuildTasks/GenerateHelp.Task.ps1 b/BuildTasks/GenerateHelp.Task.ps1 new file mode 100644 index 0000000..51d3a36 --- /dev/null +++ b/BuildTasks/GenerateHelp.Task.ps1 @@ -0,0 +1,23 @@ + +task GenerateHelp { + if (-not(Get-ChildItem -Path $DocsPath -Filter '*.md' -Recurse -ErrorAction 'Ignore')) + { + "No Markdown help files to process. Skipping help file generation..." + return + } + + $locales = (Get-ChildItem -Path $DocsPath -Directory).Name + foreach ($locale in $locales) + { + $params = @{ + ErrorAction = 'SilentlyContinue' + Force = $true + OutputPath = "$Destination\en-US" + Path = "$DocsPath\en-US" + } + + # Generate the module's primary MAML help file. + "Creating new External help for [$ModuleName]..." + $null = New-ExternalHelp @params + } +} diff --git a/BuildTasks/GenerateMarkdown.Task.ps1 b/BuildTasks/GenerateMarkdown.Task.ps1 new file mode 100644 index 0000000..cfb4204 --- /dev/null +++ b/BuildTasks/GenerateMarkdown.Task.ps1 @@ -0,0 +1,41 @@ + +task GenerateMarkdown { + $module = Import-Module -FullyQualifiedName $ManifestPath -Force -PassThru + + try + { + if ($module.ExportedFunctions.Count -eq 0) + { + 'No functions have been exported for this module. Skipping Markdown generation...' + return + } + + if (Get-ChildItem -Path $DocsPath -Filter '*.md' -Recurse) + { + $items = Get-ChildItem -Path $DocsPath -Directory -Recurse + foreach ($item in $items) + { + "Updating Markdown help in [$($item.BaseName)]..." + $null = Update-MarkdownHelp -Path $item.FullName -AlphabeticParamsOrder + } + } + + $params = @{ + AlphabeticParamsOrder = $true + ErrorAction = 'SilentlyContinue' + Locale = 'en-US' + Module = $ModuleName + OutputFolder = "$DocsPath\en-US" + WithModulePage = $true + } + + # ErrorAction is set to SilentlyContinue so this + # command will not overwrite an existing Markdown file. + "Creating new Markdown help for [$ModuleName]..." + $null = New-MarkdownHelp @params + } + finally + { + Remove-Module -Name $ModuleName -Force + } +} diff --git a/BuildTasks/ImportDevModule.Task.ps1 b/BuildTasks/ImportDevModule.Task.ps1 new file mode 100644 index 0000000..c868aa7 --- /dev/null +++ b/BuildTasks/ImportDevModule.Task.ps1 @@ -0,0 +1,4 @@ + +task ImportDevModule { + ImportModule -Path "$Source\$ModuleName.psd1" -Force +} diff --git a/BuildTasks/ImportModule.Task.ps1 b/BuildTasks/ImportModule.Task.ps1 new file mode 100644 index 0000000..a903b45 --- /dev/null +++ b/BuildTasks/ImportModule.Task.ps1 @@ -0,0 +1,33 @@ +function ImportModule +{ + param( + [string]$path, + [switch]$PassThru + ) + + + if (-not(Test-Path -Path $path)) + { + "Cannot find [$path]." + Write-Error -Message "Could not find module manifest [$path]" + } + else + { + $file = Get-Item $path + $name = $file.BaseName + + $loaded = Get-Module -Name $name -All -ErrorAction Ignore + if ($loaded) + { + "Unloading Module [$name] from a previous import..." + $loaded | Remove-Module -Force + } + + "Importing Module [$name] from [$($file.fullname)]..." + Import-Module -Name $file.fullname -Force -PassThru:$PassThru + } +} + +task ImportModule { + ImportModule -Path $ManifestPath +} diff --git a/BuildTasks/Install.Task.ps1 b/BuildTasks/Install.Task.ps1 new file mode 100644 index 0000000..1727f86 --- /dev/null +++ b/BuildTasks/Install.Task.ps1 @@ -0,0 +1,21 @@ + +task Install Uninstall, { + $version = [version] (Get-Metadata -Path $manifestPath -PropertyName 'ModuleVersion') + + $path = $env:PSModulePath.Split(';').Where({ + $_ -like 'C:\Users\*' + }, 'First', 1) + + if ($path -and (Test-Path -Path $path)) + { + "Using [$path] as base path..." + $path = Join-Path -Path $path -ChildPath $ModuleName + $path = Join-Path -Path $path -ChildPath $version + + "Creating directory at [$path]..." + New-Item -Path $path -ItemType 'Directory' -Force -ErrorAction 'Ignore' + + "Copying items from [$Destination] to [$path]..." + Copy-Item -Path "$Destination\*" -Destination $path -Recurse -Force + } +} diff --git a/BuildTasks/InvokeBuildInit.ps1 b/BuildTasks/InvokeBuildInit.ps1 new file mode 100644 index 0000000..620e51d --- /dev/null +++ b/BuildTasks/InvokeBuildInit.ps1 @@ -0,0 +1,31 @@ +Write-Verbose "Initializing build variables" -Verbose +Write-Verbose " Existing BuildRoot [$BuildRoot]" -Verbose + +$Script:DocsPath = Join-Path -Path $BuildRoot -ChildPath 'Docs' +Write-Verbose " DocsPath [$DocsPath]" -Verbose + +$Script:Output = Join-Path -Path $BuildRoot -ChildPath 'Output' +Write-Verbose " Output [$Output]" -Verbose + +$Script:Source = Join-Path -Path $BuildRoot -ChildPath $ModuleName +Write-Verbose " Source [$Source]" -Verbose + +$Script:Destination = Join-Path -Path $Output -ChildPath $ModuleName +Write-Verbose " Destination [$Destination]" -Verbose + +$Script:ManifestPath = Join-Path -Path $Destination -ChildPath "$ModuleName.psd1" +Write-Verbose " ManifestPath [$ManifestPath]" -Verbose + +$Script:ModulePath = Join-Path -Path $Destination -ChildPath "$ModuleName.psm1" +Write-Verbose " ModulePath [$ModulePath]" -Verbose + +$Script:Folders = 'Classes', 'Includes', 'Internal', 'Private', 'Public', 'Resources' +Write-Verbose " Folders [$Folders]" -Verbose + +$Script:TestFile = "$BuildRoot\Output\TestResults_PS$PSVersion`_$TimeStamp.xml" +Write-Verbose " TestFile [$TestFile]" -Verbose + +$Script:PSRepository = 'PSGallery' +Write-Verbose " PSRepository [$TestFile]" -Verbose + +function taskx($Name, $Parameters) { task $Name @Parameters -Source $MyInvocation } diff --git a/BuildTasks/Pester.Task.ps1 b/BuildTasks/Pester.Task.ps1 new file mode 100644 index 0000000..3a4f2fe --- /dev/null +++ b/BuildTasks/Pester.Task.ps1 @@ -0,0 +1,34 @@ +task Pester { + $requiredPercent = $Script:CodeCoveragePercent + + $params = @{ + OutputFile = $testFile + OutputFormat = 'NUnitXml' + PassThru = $true + Path = 'Tests' + Show = 'Failed', 'Fails', 'Summary' + Tag = 'Build' + } + + if($requiredPercent -gt 0.00) + { + $params['CodeCoverage'] = 'Output\*\*.psm1' + $params['CodeCoverageOutputFile'] = 'Output\codecoverage.xml' + } + + $results = Invoke-Pester @params + if ($results.FailedCount -gt 0) + { + Write-Error -Message "Failed [$($results.FailedCount)] Pester tests." + } + + if($results.codecoverage.NumberOfCommandsAnalyzed -gt 0) + { + $codeCoverage = $results.codecoverage.NumberOfCommandsExecuted / $results.codecoverage.NumberOfCommandsAnalyzed + + if($codeCoverage -lt $requiredPercent) + { + Write-Error ("Failed Code Coverage [{0:P}] below {1:P}" -f $codeCoverage,$requiredPercent) + } + } +} diff --git a/BuildTasks/PublishModule.Task.ps1 b/BuildTasks/PublishModule.Task.ps1 new file mode 100644 index 0000000..bf01c12 --- /dev/null +++ b/BuildTasks/PublishModule.Task.ps1 @@ -0,0 +1,31 @@ +task PublishModule { + + if ( $ENV:BHBuildSystem -ne 'Unknown' -and + $ENV:BHBranchName -eq "master" -and + -not [string]::IsNullOrWhiteSpace($ENV:nugetapikey)) + { + $publishModuleSplat = @{ + Path = $Destination + NuGetApiKey = $ENV:nugetapikey + Verbose = $true + Force = $true + Repository = $PSRepository + ErrorAction = 'Stop' + } + "Files in module output:" + Get-ChildItem $Destination -Recurse -File | + Select-Object -Expand FullName + + "Publishing [$Destination] to [$PSRepository]" + + Publish-Module @publishModuleSplat + } + else + { + "Skipping deployment: To deploy, ensure that...`n" + + "`t* You are in a known build system (Current: $ENV:BHBuildSystem)`n" + + "`t* You are committing to the master branch (Current: $ENV:BHBranchName) `n" + + "`t* The repository APIKey is defined in `$ENV:nugetapikey (Current: $(![string]::IsNullOrWhiteSpace($ENV:nugetapikey))) `n" + + "`t* This is not a pull request" + } +} diff --git a/BuildTasks/PublishVersion.Task.ps1 b/BuildTasks/PublishVersion.Task.ps1 new file mode 100644 index 0000000..79c9eba --- /dev/null +++ b/BuildTasks/PublishVersion.Task.ps1 @@ -0,0 +1,7 @@ +task PublishVersion { + [version] $sourceVersion = (Get-Metadata -Path $manifestPath -PropertyName 'ModuleVersion') + "##vso[build.updatebuildnumber]$sourceVersion" + + # Do the same for appveyor + # https://www.appveyor.com/docs/build-worker-api/#update-build-details +} diff --git a/BuildTasks/SetVersion.Task.ps1 b/BuildTasks/SetVersion.Task.ps1 new file mode 100644 index 0000000..1587856 --- /dev/null +++ b/BuildTasks/SetVersion.Task.ps1 @@ -0,0 +1,125 @@ +function GetModulePublicInterfaceMap +{ + param($Path) + $module = ImportModule -Path $Path -PassThru + $exportedCommands = @( + $module.ExportedFunctions.values + $module.ExportedCmdlets.values + $module.ExportedAliases.values + ) + + foreach($command in $exportedCommands) + { + foreach ($parameter in $command.Parameters.Keys) + { + if($false -eq $command.Parameters[$parameter].IsDynamic) + { + '{0}:{1}' -f $command.Name, $command.Parameters[$parameter].Name + foreach ($alias in $command.Parameters[$parameter].Aliases) + { + '{0}:{1}' -f $command.Name, $alias + } + } + } + } +} + +task SetVersion { + $version = [version]"0.1.0" + $publishedModule = $null + $bumpVersionType = 'Patch' + $versionStamp = (git rev-parse origin/master) + (git rev-parse head) + + "Load current version" + [version] $sourceVersion = (Get-Metadata -Path $manifestPath -PropertyName 'ModuleVersion') + " Source version [$sourceVersion]" + + $downloadFolder = Join-Path -Path $output downloads + $null = New-Item -ItemType Directory -Path $downloadFolder -Force -ErrorAction Ignore + + $versionFile = Join-Path $downloadFolder versionfile + if(Test-Path $versionFile) + { + $versionFileData = Get-Content $versionFile -raw + if($versionFileData -eq $versionStamp) + { + continue + } + } + + "Checking for published version" + $publishedModule = Find-Module -Name $ModuleName -ErrorAction 'Ignore' | + Sort-Object -Property {[version]$_.Version} -Descending | + Select -First 1 + + if($null -ne $publishedModule) + { + [version] $publishedVersion = $publishedModule.Version + " Published version [$publishedVersion]" + + $version = $publishedVersion + + "Downloading published module to check for breaking changes" + $publishedModule | Save-Module -Path $downloadFolder + + [System.Collections.Generic.HashSet[string]] $publishedInterface = @(GetModulePublicInterfaceMap -Path (Join-Path $downloadFolder $ModuleName)) + [System.Collections.Generic.HashSet[string]] $buildInterface = @(GetModulePublicInterfaceMap -Path $ManifestPath) + + if (-not $publishedInterface.IsSubsetOf($buildInterface)) + { + $bumpVersionType = 'Major' + } + elseif ($publishedInterface.count -ne $buildInterface.count) + { + $bumpVersionType = 'Minor' + } + } + + if ($version -lt ([version] '1.0.0')) + { + "Module is still in beta; don't bump major version." + if ($bumpVersionType -eq 'Major') + { + $bumpVersionType = 'Minor' + } + else + { + $bumpVersionType = 'Patch' + } + } + + " Steping version [$bumpVersionType]" + $version = [version] (Step-Version -Version $version -Type $bumpVersionType) + + " Comparing to source version [$sourceVersion]" + if($sourceVersion -gt $version) + { + " Using existing version" + $version = $sourceVersion + } + + if ( -not [string]::IsNullOrEmpty( $env:Build_BuildID ) ) + { + $build = $env:Build_BuildID + $version = [version]::new($version.Major, $version.Minor, $version.Build, $build) + } + elseif ( -not [string]::IsNullOrEmpty( $env:APPVEYOR_BUILD_ID ) ) + { + $build = $env:APPVEYOR_BUILD_ID + $version = [version]::new($version.Major, $version.Minor, $version.Build, $build) + } + + " Setting version [$version]" + Update-Metadata -Path $ManifestPath -PropertyName 'ModuleVersion' -Value $version + + (Get-Content -Path $ManifestPath -Raw -Encoding UTF8) | + ForEach-Object {$_.TrimEnd()} | + Set-Content -Path $ManifestPath -Encoding UTF8 + + Set-Content -Path $versionFile -Value $versionStamp -NoNewline -Encoding UTF8 + + if(Test-Path $BuildRoot\fingerprint) + { + Remove-Item $BuildRoot\fingerprint + } +} diff --git a/BuildTasks/Uninstall.Task.ps1 b/BuildTasks/Uninstall.Task.ps1 new file mode 100644 index 0000000..61e0539 --- /dev/null +++ b/BuildTasks/Uninstall.Task.ps1 @@ -0,0 +1,28 @@ + +task Uninstall { + 'Unloading Modules...' + Get-Module -Name $ModuleName -ErrorAction 'Ignore' | Remove-Module -Force + + 'Uninstalling Module packages...' + $modules = Get-Module $ModuleName -ErrorAction 'Ignore' -ListAvailable + foreach ($module in $modules) + { + Uninstall-Module -Name $module.Name -RequiredVersion $module.Version -ErrorAction 'Ignore' + } + + 'Cleaning up manually installed Modules...' + $path = $env:PSModulePath.Split(';').Where({ + $_ -like 'C:\Users\*' + }, 'First', 1) + + $path = Join-Path -Path $path -ChildPath $ModuleName + if ($path -and (Test-Path -Path $path)) + { + 'Removing files... (This may fail if any DLLs are in use.)' + Get-ChildItem -Path $path -File -Recurse | + Remove-Item -Force | ForEach-Object 'FullName' + + 'Removing folders... (This may fail if any DLLs are in use.)' + Remove-Item $path -Recurse -Force + } +} diff --git a/BuildTasks/UpdateSource.Task.ps1 b/BuildTasks/UpdateSource.Task.ps1 new file mode 100644 index 0000000..fa46401 --- /dev/null +++ b/BuildTasks/UpdateSource.Task.ps1 @@ -0,0 +1,6 @@ +task UpdateSource { + Copy-Item -Path $ManifestPath -Destination "$Source\$ModuleName.psd1" + + $content = Get-Content -Path "$Source\$ModuleName.psd1" -Raw -Encoding UTF8 + $content.Trim() | Set-Content -Path "$Source\$ModuleName.psd1" -Encoding UTF8 +} diff --git a/Chronometer/chronometer.psd1 b/Chronometer/chronometer.psd1 index 24c1106..96d7bb7 100644 --- a/Chronometer/chronometer.psd1 +++ b/Chronometer/chronometer.psd1 @@ -1,4 +1,4 @@ -# Module manifest for module 'chronometer' +# Module manifest for module 'chronometer' # Generated by: Kevin Marquette # Generated on: 2/2/2017 @@ -8,7 +8,7 @@ RootModule = 'chronometer.psm1' # Version number of this module. -ModuleVersion = '0.5.2' +ModuleVersion = '1.0.0' # ID used to uniquely identify this module GUID = 'f3719c3c-008a-4b25-b94d-fc9f587f62dd' @@ -17,7 +17,7 @@ GUID = 'f3719c3c-008a-4b25-b94d-fc9f587f62dd' Author = 'Kevin Marquette' # Copyright statement for this module -Copyright = '(c) 2017 Kevin Marquette. All rights reserved.' +Copyright = '(c) 2019 Kevin Marquette. All rights reserved.' # Description of the functionality provided by this module Description = 'Performs a line by line measurement of execution times for scripts' @@ -26,7 +26,7 @@ Description = 'Performs a line by line measurement of execution times for script PowerShellVersion = '5.0' # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. -FunctionsToExport = @('Get-Chronometer','Format-Chronometer') +FunctionsToExport = @('Format-Chronometer','Get-Chronometer') # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. CmdletsToExport = @() @@ -55,4 +55,3 @@ PrivateData = @{ } # End of PrivateData hashtable } - diff --git a/Docs/en-US/Format-Chronometer.md b/Docs/en-US/Format-Chronometer.md new file mode 100644 index 0000000..033c8e2 --- /dev/null +++ b/Docs/en-US/Format-Chronometer.md @@ -0,0 +1,126 @@ +--- +external help file: chronometer-help.xml +Module Name: chronometer +online version: +schema: 2.0.0 +--- + +# Format-Chronometer + +## SYNOPSIS + +## SYNTAX + +### Script (Default) +``` +Format-Chronometer [-InputObject ] [-WarningAt ] [-ErrorAt ] [-ShowAll] + [] +``` + +### Line +``` +Format-Chronometer [-Line ] [-WarningAt ] [-ErrorAt ] [-ShowAll] + [] +``` + +## DESCRIPTION +Generates a report from a Chronometer + +## EXAMPLES + +### EXAMPLE 1 +``` +$script = ls C:\workspace\PSGraph\PSGraph -Recurse -Filter *.ps1 +``` + +$resultes = Get-Chronometer -Path $script.fullname -ScriptBlock {Invoke-Pester C:\workspace\PSGraph} +$results | Format-Chronometer -WarnAt 20 -ErrorAt 200 + +## PARAMETERS + +### -ErrorAt +If the average time of a comamand is more than this, the output is red + +```yaml +Type: Int32 +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: 200 +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -InputObject +This is a MonitoredScript object from Get-Chronometer + +```yaml +Type: MonitoredScript[] +Parameter Sets: Script +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByValue) +Accept wildcard characters: False +``` + +### -Line +This is a ScriptLine object from a MonitoredScript object + +```yaml +Type: ScriptLine[] +Parameter Sets: Line +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByValue) +Accept wildcard characters: False +``` + +### -ShowAll +Forces the report to show scripts with no execution time + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -WarningAt +If the average time of a command is more than this, the output is yellow + +```yaml +Type: Int32 +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: 20 +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. +For more information, see about_CommonParameters (http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +## NOTES + +## RELATED LINKS diff --git a/Docs/en-US/Get-Chronometer.md b/Docs/en-US/Get-Chronometer.md new file mode 100644 index 0000000..2ccdaf1 --- /dev/null +++ b/Docs/en-US/Get-Chronometer.md @@ -0,0 +1,88 @@ +--- +external help file: chronometer-help.xml +Module Name: chronometer +online version: +schema: 2.0.0 +--- + +# Get-Chronometer + +## SYNOPSIS + +## SYNTAX + +``` +Get-Chronometer [-Path ] [-LineNumber ] [[-ScriptBlock] ] [] +``` + +## DESCRIPTION +Loads a script and then tracks the line by line execution times + +## EXAMPLES + +### EXAMPLE 1 +``` +Get-Chronometer -Path .\example.ps1 -Script { +``` + +.\example.ps1 +} + +## PARAMETERS + +### -LineNumber +Line numbers within the script file to measure + +```yaml +Type: Int32[] +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Path +Script file to measure execution times on + +```yaml +Type: Object[] +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByValue) +Accept wildcard characters: False +``` + +### -ScriptBlock +The script to start the scrupt or execute other commands + +```yaml +Type: ScriptBlock +Parameter Sets: (All) +Aliases: Script, CommandScript + +Required: False +Position: 1 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. +For more information, see about_CommonParameters (http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +## NOTES + +## RELATED LINKS diff --git a/Docs/en-US/chronometer.md b/Docs/en-US/chronometer.md new file mode 100644 index 0000000..8d269f2 --- /dev/null +++ b/Docs/en-US/chronometer.md @@ -0,0 +1,19 @@ +--- +Module Name: chronometer +Module Guid: f3719c3c-008a-4b25-b94d-fc9f587f62dd +Download Help Link: {{Please enter FwLink manually}} +Help Version: {{Please enter version of help manually (X.X.X.X) format}} +Locale: en-US +--- + +# chronometer Module +## Description +{{Manually Enter Description Here}} + +## chronometer Cmdlets +### [Format-Chronometer](Format-Chronometer.md) +{{Manually Enter Format-Chronometer Description Here}} + +### [Get-Chronometer](Get-Chronometer.md) +{{Manually Enter Get-Chronometer Description Here}} + diff --git a/LICENSE b/LICENSE index 8a60580..0d1df97 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017 Kevin Marquette +Copyright (c) 2019 Kevin Marquette Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Module.build.ps1 b/Module.build.ps1 new file mode 100644 index 0000000..f549433 --- /dev/null +++ b/Module.build.ps1 @@ -0,0 +1,15 @@ +$Script:ModuleName = Get-ChildItem .\*\*.psm1 | Select-object -ExpandProperty BaseName +$Script:CodeCoveragePercent = 0.0 # 0 to 1 +. $psscriptroot\BuildTasks\InvokeBuildInit.ps1 + +task Default Build, Helpify, Test, UpdateSource +task Build Copy, Compile, BuildModule, BuildManifest, SetVersion +task Helpify GenerateMarkdown, GenerateHelp +task Test Build, ImportModule, Pester +task Publish Build, PublishVersion, Helpify, Test, PublishModule +task TFS Clean, Build, PublishVersion, Helpify, Test +task DevTest ImportDevModule, Pester + +Write-Host 'Import common tasks' +Get-ChildItem -Path $buildroot\BuildTasks\*.Task.ps1 | + ForEach-Object {Write-Host $_.FullName;. $_.FullName} diff --git a/ScriptAnalyzerSettings.psd1 b/ScriptAnalyzerSettings.psd1 new file mode 100644 index 0000000..e42e87d --- /dev/null +++ b/ScriptAnalyzerSettings.psd1 @@ -0,0 +1,34 @@ +@{ + # Use Severity when you want to limit the generated diagnostic records to a + # subset of: Error, Warning and Information. + # Uncomment the following line if you only want Errors and Warnings but + # not Information diagnostic records. + Severity = @('Error','Warning') + + # Use IncludeRules when you want to run only a subset of the default rule set. + #IncludeRules = @('PSAvoidDefaultValueSwitchParameter', + # 'PSMisleadingBacktick', + # 'PSMissingModuleManifestField', + # 'PSReservedCmdletChar', + # 'PSReservedParams', + # 'PSShouldProcess', + # 'PSUseApprovedVerbs', + # 'PSUseDeclaredVarsMoreThanAssigments') + + # Use ExcludeRules when you want to run most of the default set of rules except + # for a few rules you wish to "exclude". Note: if a rule is in both IncludeRules + # and ExcludeRules, the rule will be excluded. + ExcludeRules = @('PSUseToExportFieldsInManifest','PSMissingModuleManifestField') + + # You can use the following entry to supply parameters to rules that take parameters. + # For instance, the PSAvoidUsingCmdletAliases rule takes a whitelist for aliases you + # want to allow. + Rules = @{ + # Do not flag 'cd' alias. + PSAvoidUsingCmdletAliases = @{Whitelist = @('Where','Select')} + + # Check if your script uses cmdlets that are compatible on PowerShell Core, + # version 6.0.0-alpha, on Linux. + # PSUseCompatibleCmdlets = @{Compatibility = @("core-6.0.0-alpha-linux")} + } +} diff --git a/Tests/Classes/MonitoredScript.Tests.ps1 b/Tests/Classes/MonitoredScript.Tests.ps1 new file mode 100644 index 0000000..bf1abe1 --- /dev/null +++ b/Tests/Classes/MonitoredScript.Tests.ps1 @@ -0,0 +1,16 @@ +InModuleScope Chronometer { + + Describe "Class: MonitoredScript" -Tag Build { + + It "Creates an object" { + {[MonitoredScript]::New()} | Should Not Throw + } + + It "SetScript()" { + + $monitor = [MonitoredScript]::New() + {$monitor.SetScript("$PSScriptRoot\..\..\scratchfiles\example.ps1")} | Should Not Throw + } + } + +} diff --git a/Tests/Classes/ScriptLine.Tests.ps1 b/Tests/Classes/ScriptLine.Tests.ps1 new file mode 100644 index 0000000..dbe2244 --- /dev/null +++ b/Tests/Classes/ScriptLine.Tests.ps1 @@ -0,0 +1,16 @@ +InModuleScope Chronometer { + Describe "Class: ScriptLine" -Tag Build { + + It "Creates an Object" { + {[ScriptLine]::New()} | Should Not Throw + } + + It "ToString()" { + {[ScriptLine]::New().toString()} | Should Not Throw + } + + It "Creates an Object" { + {[ScriptLine]::New().AddExecutionTime(1)} | Should Not Throw + } + } +} diff --git a/Tests/Classes/ScriptProfiler.Tests.ps1 b/Tests/Classes/ScriptProfiler.Tests.ps1 new file mode 100644 index 0000000..1e499d2 --- /dev/null +++ b/Tests/Classes/ScriptProfiler.Tests.ps1 @@ -0,0 +1,13 @@ +InModuleScope Chronometer { + + Describe "Class: ScriptProfiler" -Tag Build { + + It "Creates an Object" { + {[ScriptProfiler]::New()} | Should Not Throw + } + + It "Start()" { + {[ScriptProfiler]::Start()} | Should Not Throw + } + } +} diff --git a/Tests/Format-Chronometer.Tests.ps1 b/Tests/Format-Chronometer.Tests.ps1 new file mode 100644 index 0000000..d9bc862 --- /dev/null +++ b/Tests/Format-Chronometer.Tests.ps1 @@ -0,0 +1,11 @@ +Describe "Function: Format-Chronometer" -Tag Build { + + It "Does not throw" { + {$null | Format-Chronometer } | Should Not Throw + } + + It "Can process a result object without throwing" { + $results = Get-Chronometer -Path $PSScriptRoot\..\ScratchFiles\example.ps1 -Script {. "$PSScriptRoot\..\ScratchFiles\example.ps1"} + $results | Format-Chronometer *>&1 | Should Not BeNullOrEmpty + } +} diff --git a/Tests/Get-Chronometer.Tests.ps1 b/Tests/Get-Chronometer.Tests.ps1 new file mode 100644 index 0000000..1462c47 --- /dev/null +++ b/Tests/Get-Chronometer.Tests.ps1 @@ -0,0 +1,23 @@ +Describe "Function: Get-Chronometer" -Tag Build { + It "Does not throw" { + # Get-Chronometer -Path ScratchFiles\example.ps1 -Script {"Test"} + {Get-Chronometer -Path $PSScriptRoot\..\ScratchFiles\example.ps1 -Script {"Test"} } | Should Not Throw + } + + It "Executes a script and gives results" { + # Get-Chronometer -Path ScratchFiles\example.ps1 -Script {"Test"} + $results = Get-Chronometer -Path $PSScriptRoot\..\ScratchFiles\example.ps1 -Script {. "$PSScriptRoot\..\ScratchFiles\example.ps1"} + $results | Should Not BeNullOrEmpty + } + + It "Executes a script with line numbers and gives results" { + # Get-Chronometer -Path ScratchFiles\example.ps1 -Script {"Test"} + $params = @{ + Path = "$PSScriptRoot\..\ScratchFiles\example.ps1" + Script = {. "$PSScriptRoot\..\ScratchFiles\example.ps1"} + LineNumber = 2,3,5,6 + } + $results = Get-Chronometer @params + $results | Should Not BeNullOrEmpty + } +} diff --git a/Tests/Help.Tests.ps1 b/Tests/Help.Tests.ps1 deleted file mode 100644 index 7a61ead..0000000 --- a/Tests/Help.Tests.ps1 +++ /dev/null @@ -1,30 +0,0 @@ -$projectRoot = Resolve-Path "$PSScriptRoot\.." -$moduleRoot = Split-Path (Resolve-Path "$projectRoot\*\*.psm1") -$moduleName = Split-Path $moduleRoot -Leaf - - -Describe "Help tests for $moduleName" -Tags Build { - - Import-Module (Join-Path $moduleRoot "$moduleName.psm1") -force - - $functions = Get-Command -Module $moduleName - $help = $functions | %{Get-Help $_.name} - foreach($node in $help) - { - Context $node.name { - - it "has a description" { - $node.description | Should Not BeNullOrEmpty - } - it "has an example" { - $node.examples | Should Not BeNullOrEmpty - } - foreach($parameter in $node.parameters.parameter) - { - it "parameter $($parameter.name) has a description" { - $parameter.Description.text | Should Not BeNullOrEmpty - } - } - } - } -} diff --git a/Tests/Project.Tests.ps1 b/Tests/Project.Tests.ps1 deleted file mode 100644 index 0cd47cb..0000000 --- a/Tests/Project.Tests.ps1 +++ /dev/null @@ -1,51 +0,0 @@ -$projectRoot = Resolve-Path "$PSScriptRoot\.." -$moduleRoot = Split-Path (Resolve-Path "$projectRoot\*\*.psd1") -$moduleName = Split-Path $moduleRoot -Leaf - -Describe "General project validation: $moduleName" -Tags Build { - - Context "Valid Powershell" { - $scripts = Get-ChildItem $projectRoot -Include *.ps1,*.psm1,*.psd1 -Recurse | where fullname -notmatch 'classes' - # TestCases are splatted to the script so we need hashtables - $testCase = $scripts | Foreach-Object{@{file=$_}} - - It "Script should be valid powershell" -TestCases $testCase { - param($file) - - $file.fullname | Should Exist - - $contents = Get-Content -Path $file.fullname -ErrorAction Stop - $errors = $null - $null = [System.Management.Automation.PSParser]::Tokenize($contents, [ref]$errors) - $errors.Count | Should Be 0 - } - - It "Classes are valid" { - $classes = Get-ChildItem $projectRoot -Include *.ps1,*.psm1,*.psd1 -Recurse | where fullname -match 'classes' - - # Must be imported togehter incase they depend on each other - $contents = Get-Content -Path $classes.FullName | Out-String - - $errors = $null - $null = [System.Management.Automation.PSParser]::Tokenize($contents, [ref]$errors) - $errors.Count | Should Be 0 - } - } - - Context "ScriptAnalyzer" { - - $scripts = Get-ChildItem $moduleRoot -Include *.ps1,*.psm1,*.psd1 -Recurse | where fullname -notmatch 'classes' - $testCase = $scripts | Foreach-Object{@{file=$_}} - - it "Script should pass ScriptAnalyzer rules" -TestCases $testCase { - param($file) - - $file.fullname | Should Exist - Invoke-ScriptAnalyzer $file| Should BeNullOrEmpty - } - } - - It "Module '$moduleName' can import cleanly" { - {Import-Module (Join-Path $moduleRoot "$moduleName.psm1") -force } | Should Not Throw - } -} diff --git a/Tests/Project/Help.Tests.ps1 b/Tests/Project/Help.Tests.ps1 new file mode 100644 index 0000000..954cf88 --- /dev/null +++ b/Tests/Project/Help.Tests.ps1 @@ -0,0 +1,33 @@ +$Script:ModuleRoot = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent +$Script:ModuleName = $Script:ModuleName = Get-ChildItem $ModuleRoot\*\*.psm1 | Select-object -ExpandProperty BaseName + +Describe "Public commands have comment-based or external help" -Tags 'Build' { + $functions = Get-Command -Module $ModuleName + $help = foreach ($function in $functions) { + Get-Help -Name $function.Name + } + + foreach ($node in $help) + { + Context $node.Name { + It "Should have a Description or Synopsis" { + ($node.Description + $node.Synopsis) | Should Not BeNullOrEmpty + } + + It "Should have an Example" { + $node.Examples | Should Not BeNullOrEmpty + $node.Examples | Out-String | Should -Match ($node.Name) + } + + foreach ($parameter in $node.Parameters.Parameter) + { + if ($parameter -notmatch 'WhatIf|Confirm') + { + It "Should have a Description for Parameter [$($parameter.Name)]" { + $parameter.Description.Text | Should Not BeNullOrEmpty + } + } + } + } + } +} diff --git a/Tests/Project/Module.Tests.ps1 b/Tests/Project/Module.Tests.ps1 new file mode 100644 index 0000000..f652dc4 --- /dev/null +++ b/Tests/Project/Module.Tests.ps1 @@ -0,0 +1,45 @@ +$Script:ModuleRoot = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent +$Script:ModuleName = $Script:ModuleName = Get-ChildItem $ModuleRoot\*\*.psm1 | Select-object -ExpandProperty BaseName + +$Script:SourceRoot = Join-Path -Path $ModuleRoot -ChildPath $ModuleName + +Describe "All commands pass PSScriptAnalyzer rules" -Tag 'Build' { + $rules = "$ModuleRoot\ScriptAnalyzerSettings.psd1" + $scripts = Get-ChildItem -Path $SourceRoot -Include '*.ps1', '*.psm1', '*.psd1' -Recurse | + Where-Object FullName -notmatch 'Classes' + + foreach ($script in $scripts) + { + Context $script.FullName { + $results = Invoke-ScriptAnalyzer -Path $script.FullName -Settings $rules + if ($results) + { + foreach ($rule in $results) + { + It $rule.RuleName { + $message = "{0} Line {1}: {2}" -f $rule.Severity, $rule.Line, $rule.Message + $message | Should Be "" + } + } + } + else + { + It "Should not fail any rules" { + $results | Should BeNullOrEmpty + } + } + } + } +} + +Describe "Public commands have Pester tests" -Tag 'Build' { + $commands = Get-Command -Module $ModuleName + + foreach ($command in $commands.Name) + { + $file = Get-ChildItem -Path "$ModuleRoot\Tests" -Include "$command.Tests.ps1" -Recurse + It "Should have a Pester test for [$command]" { + $file.FullName | Should Not BeNullOrEmpty + } + } +} diff --git a/Tests/Unit.Tests.ps1 b/Tests/Unit.Tests.ps1 deleted file mode 100644 index 71f832d..0000000 --- a/Tests/Unit.Tests.ps1 +++ /dev/null @@ -1,83 +0,0 @@ -$projectRoot = Resolve-Path "$PSScriptRoot\.." -$moduleRoot = Split-Path (Resolve-Path "$projectRoot\*\*.psd1") -$moduleName = Split-Path $moduleRoot -Leaf - -Describe "Basic unit tests" -Tags Build { - - Import-Module (Join-Path $moduleRoot "$moduleName.psm1") -force - - Context "Function: Get-Chronometer" { - it "Does not throw" { - # Get-Chronometer -Path ScratchFiles\example.ps1 -Script {"Test"} - {Get-Chronometer -Path $PSScriptRoot\..\ScratchFiles\example.ps1 -Script {"Test"} } | Should Not Throw - } - - it "Executes a script and gives results" { - # Get-Chronometer -Path ScratchFiles\example.ps1 -Script {"Test"} - $results = Get-Chronometer -Path $PSScriptRoot\..\ScratchFiles\example.ps1 -Script {. "$PSScriptRoot\..\ScratchFiles\example.ps1"} - $results | Should Not BeNullOrEmpty - } - - it "Executes a script with linenumbers and gives results" { - # Get-Chronometer -Path ScratchFiles\example.ps1 -Script {"Test"} - $params = @{ - Path = "$PSScriptRoot\..\ScratchFiles\example.ps1" - Script = {. "$PSScriptRoot\..\ScratchFiles\example.ps1"} - LineNumber = 2,3,5,6 - } - $results = Get-Chronometer @params - $results | Should Not BeNullOrEmpty - } - } - - Context "Function: Format-Chronometer" { - - it "Does not throw" { - {$null | Format-Chronometer } | Should Not Throw - } - - it "Can process a result object without throwing" { - $results = Get-Chronometer -Path $PSScriptRoot\..\ScratchFiles\example.ps1 -Script {. "$PSScriptRoot\..\ScratchFiles\example.ps1"} - $results | Format-Chronometer *>&1 | Should Not BeNullOrEmpty - } - } - - InModuleScope $moduleName { - Context "Class: ScriptLine" { - - it "Creates an Object" { - {[ScriptLine]::New()} | Should Not Throw - } - it "ToString()" { - {[ScriptLine]::New().toString()} | Should Not Throw - } - it "Creates an Object" { - {[ScriptLine]::New().AddExecutionTime(1)} | Should Not Throw - } - } - - Context "Class: ScriptProfiler" { - - it "Creates an Object" { - {[ScriptProfiler]::New()} | Should Not Throw - } - it "Start()" { - {[ScriptProfiler]::Start()} | Should Not Throw - } - } - - Context "Class: MonitoredScript" { - it "Creates an object" { - {[MonitoredScript]::New()} | Should Not Throw - } - - it "SetScript()" { - pushd $projectRoot - $monitor = [MonitoredScript]::New() - {$monitor.SetScript(".\scratchfiles\example.ps1")} | Should Not Throw - popd - } - } - - } -} \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml index 4918e7a..5e0a4b1 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -16,4 +16,4 @@ build: false #Kick off the CI/CD pipeline test_script: - - ps: . .\build.ps1 \ No newline at end of file + - ps: . .\build.ps1 diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 0000000..d976f2c --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,25 @@ +# Starter pipeline +# Start with a minimal pipeline that you can customize to build and deploy your code. +# Add steps that build, run tests, deploy, and more: +# https://aka.ms/yaml + +#resources: +#- repo: self +# clean: true +# fetchDepth: 1 + +trigger: + batch: true + branches: + include: + - master + +pool: + vmImage: 'Ubuntu 16.04' + +steps: +- script: pwsh -File build.ps1 Publish + displayName: 'Build and Publish Module' + env: + nugetapikey: $(nugetapikey) + diff --git a/build.ps1 b/build.ps1 index b257254..ec892fc 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,21 +1,38 @@ -<# -.Description -Installs and loads all the required modules for the build. -.Author -Warren F. (RamblingCookieMonster) -#> +[CmdletBinding()] -[cmdletbinding()] -param ($Task = 'Default') +param($Task = 'Default') -# Grab nuget bits, install modules, set build variables, start build. -Get-PackageProvider -Name NuGet -ForceBootstrap | Out-Null +$Script:Modules = @( + 'BuildHelpers', + 'InvokeBuild', + 'Pester', + 'platyPS', + 'PSScriptAnalyzer', + 'DependsOn' +) -Install-Module Psake, PSDeploy, BuildHelpers, PSScriptAnalyzer -force -Install-Module Pester -Force -SkipPublisherCheck -Import-Module Psake, BuildHelpers, PSScriptAnalyzer +$Script:ModuleInstallScope = 'CurrentUser' + +'Starting build...' +'Installing module dependencies...' + +Get-PackageProvider -Name 'NuGet' -ForceBootstrap | Out-Null + +Install-Module -Name $Script:Modules -Scope $Script:ModuleInstallScope -Force -SkipPublisherCheck Set-BuildEnvironment +Get-ChildItem Env:BH* +Get-ChildItem Env:APPVEYOR* -Invoke-psake -buildFile .\psake.ps1 -taskList $Task -nologo -exit ( [int]( -not $psake.build_success ) ) \ No newline at end of file +$Error.Clear() + +'Invoking build...' + +Invoke-Build $Task -Result 'Result' +if ($Result.Error) +{ + $Error[-1].ScriptStackTrace | Out-String + exit 1 +} + +exit 0