From 2e1857f3f31c005bae6d84ca7c97cbbc12c6834c Mon Sep 17 00:00:00 2001 From: Bernie White Date: Thu, 24 Aug 2017 17:14:38 +1000 Subject: [PATCH] Release merge for v0.2.0 (#1) * Updates for v0.2.0 release --- .codecov.yml | 26 + .gitattributes | 63 ++ .gitignore | 9 + .vscode/tasks.json | 26 + README.md | 82 +- appveyor.yml | 5 + docs/commands/Invoke-DscNodeDocument.md | 30 + docs/commands/Invoke-PSDocument.md | 11 + docs/examples/Get-child-item-output.md | 11 + docs/keywords/Code.md | 45 + docs/keywords/Document.md | 38 + docs/keywords/Note.md | 46 + docs/keywords/Section.md | 76 ++ docs/keywords/Table.md | 28 + docs/keywords/Warning.md | 46 + docs/keywords/Yaml.md | 46 + scripts/build.ps1 | 94 ++ scripts/common.ps1 | 181 ++++ scripts/test.ps1 | 60 ++ src/PSDocs.Dsc/PSDocs.Dsc.psd1 | 123 +++ src/PSDocs.Dsc/PSDocs.Dsc.psm1 | 346 ++++++++ .../en-AU/PSDocs.Dsc.Resources.psd1 | 7 + .../en-US/PSDocs.Dsc.Resources.psd1 | 7 + src/PSDocs/PSDocs.psd1 | 122 +++ src/PSDocs/PSDocs.psm1 | 818 ++++++++++++++++++ .../PSDocsProcessor/Markdown/Markdown.psm1 | 170 ++++ src/PSDocs/en-AU/PSDocs.Resources.psd1 | 6 + src/PSDocs/en-US/PSDocs.Resources.psd1 | 6 + .../PSDocs.Dsc.Common.Tests.ps1 | 148 ++++ .../Templates/WithExternalScript.ps1 | 5 + tests/PSDocs.Tests/PSDocs.Code.Tests.ps1 | 86 ++ tests/PSDocs.Tests/PSDocs.Common.Tests.ps1 | 102 +++ tests/PSDocs.Tests/PSDocs.Note.Tests.ps1 | 109 +++ tests/PSDocs.Tests/PSDocs.Section.Tests.ps1 | 88 ++ tests/PSDocs.Tests/PSDocs.Table.Tests.ps1 | 98 +++ tests/PSDocs.Tests/PSDocs.Warning.Tests.ps1 | 109 +++ 36 files changed, 3271 insertions(+), 2 deletions(-) create mode 100644 .codecov.yml create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .vscode/tasks.json create mode 100644 appveyor.yml create mode 100644 docs/commands/Invoke-DscNodeDocument.md create mode 100644 docs/commands/Invoke-PSDocument.md create mode 100644 docs/examples/Get-child-item-output.md create mode 100644 docs/keywords/Code.md create mode 100644 docs/keywords/Document.md create mode 100644 docs/keywords/Note.md create mode 100644 docs/keywords/Section.md create mode 100644 docs/keywords/Table.md create mode 100644 docs/keywords/Warning.md create mode 100644 docs/keywords/Yaml.md create mode 100644 scripts/build.ps1 create mode 100644 scripts/common.ps1 create mode 100644 scripts/test.ps1 create mode 100644 src/PSDocs.Dsc/PSDocs.Dsc.psd1 create mode 100644 src/PSDocs.Dsc/PSDocs.Dsc.psm1 create mode 100644 src/PSDocs.Dsc/en-AU/PSDocs.Dsc.Resources.psd1 create mode 100644 src/PSDocs.Dsc/en-US/PSDocs.Dsc.Resources.psd1 create mode 100644 src/PSDocs/PSDocs.psd1 create mode 100644 src/PSDocs/PSDocs.psm1 create mode 100644 src/PSDocs/PSDocsProcessor/Markdown/Markdown.psm1 create mode 100644 src/PSDocs/en-AU/PSDocs.Resources.psd1 create mode 100644 src/PSDocs/en-US/PSDocs.Resources.psd1 create mode 100644 tests/PSDocs.Dsc.Tests/PSDocs.Dsc.Common.Tests.ps1 create mode 100644 tests/PSDocs.Dsc.Tests/Templates/WithExternalScript.ps1 create mode 100644 tests/PSDocs.Tests/PSDocs.Code.Tests.ps1 create mode 100644 tests/PSDocs.Tests/PSDocs.Common.Tests.ps1 create mode 100644 tests/PSDocs.Tests/PSDocs.Note.Tests.ps1 create mode 100644 tests/PSDocs.Tests/PSDocs.Section.Tests.ps1 create mode 100644 tests/PSDocs.Tests/PSDocs.Table.Tests.ps1 create mode 100644 tests/PSDocs.Tests/PSDocs.Warning.Tests.ps1 diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000..9197246 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,26 @@ +codecov: + notify: + require_ci_to_pass: no + # dev should be the baseline for reporting + branch: dev + +comment: + layout: "reach, diff" + behavior: default + +coverage: + range: 50..80 + round: down + precision: 0 + + status: + project: + default: + # Set the overall project code coverage requirement to 70% + target: 80 + patch: + default: + # Set the pull request requirement to not regress overall coverage by more than 5% + # and let codecov.io set the goal for the code changed in the patch. + target: auto + threshold: 5 \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..a1e1e97 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3863e93 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ + +/**/.vs/ +/**/bin/ +/**/obj/ +/artifacts/ +/build/ +/reports/ +/**/*.user +/.vscode/settings.json \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..3eaf805 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,26 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "taskName": "test", + "type": "shell", + "command": ".\\scripts\\test.ps1", + "group": { + "kind": "test", + "isDefault": true + }, + "problemMatcher": [ "$pester" ] + }, + { + "taskName": "build", + "type": "shell", + "command": ".\\scripts\\build.ps1 -Clean -Module PSDocs", + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index bffb012..8959b53 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,90 @@ # PSDocs -This project aims to assist IT pros to generate documentation from PowerShell objects. +A PowerShell module with commands to generate markdown from objects using PowerShell syntax. + +| AppVeyor (Windows) | Codecov (Windows) | +| --- | --- | +| [![av-image][]][av-site] | [![cc-image][]][cc-site] | + +[av-image]: https://ci.appveyor.com/api/projects/status/pl7tu7ktue388n7s +[av-site]: https://ci.appveyor.com/project/BernieWhite/psdocs +[cc-image]: https://codecov.io/gh/BernieWhite/PSDocs/branch/master/graph/badge.svg +[cc-site]: https://codecov.io/gh/BernieWhite/PSDocs ## Disclaimer This project is to be considered a **proof-of-concept** and **not a supported Microsoft product**. +## Modules +The following modules are included in this repository. + +| Module | Description | Latest version | +| ------ | ----------- | -------------- | +| PSDocs | Generate markdown from PowerShell | [v0.2.0][psg-psdocs] | +| PSDocs.Dsc | Extension for PSDocs to generate markdown from Desired State Configuration | [v0.2.0][psg-psdocsdsc] | + +[psg-psdocs]: https://www.powershellgallery.com/packages/PSDocs +[psg-psdocsdsc]: https://www.powershellgallery.com/packages/PSDocs.Dsc + ## Getting started -_More to come._ +### 1. Prerequsits + +- Windows Management Framework (WMF) 5.0 or greater +- .NET Framework 4.6 or greater + +### 2. Get PSDocs + +- Install from PowerShellGallery.com + +```powershell +# Install base PSDocs module +Install-Module -Name 'PSDocs'; +``` + +```powershell +# Optionally install DSC extensions module, which will install PSDocs if not already installed +Install-Module -Name 'PSDocs.Dsc'; +``` + +### 3. Usage + +```powershell +# Import PSDocs module +Import-Module -Name PSDocs; + +# Define a sample document +document Sample { + + Section Introduction { + # Add a comment + "This is a sample file list from $InputObject" + + # Generate a table + Get-ChildItem -Path $InputObject | Table -Property Name,PSIsContainer + } +} + +# Call the sample document and generate markdown +Invoke-PSDocument -Name Sample -InputObject 'C:\'; +``` + +For an example of the output generated see [Get-ChildItemExample](/docs/examples/Get-child-item-output.md) + +## Language reference + +### Keywords + +- [Document](/docs/keywords/Document.md) +- [Section](/docs/keywords/Section.md) +- [Code](/docs/keywords/Code.md) +- [Note](/docs/keywords/Note.md) +- [Warning](/docs/keywords/Warning.md) +- [Yaml](/docs/keywords/Yaml.md) +- [Table](/docs/keywords/Table.md) + +### Commands + +- [Invoke-PSDocument](/docs/commands/Invoke-PSDocument.md) +- [Invoke-DscNodeDocument](/docs/commands/Invoke-DscNodeDocument.md) ## Maintainers diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..564eb62 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,5 @@ +version: 0.1.0.{build} +image: Visual Studio 2017 +build: off +test_script: +- ps: .\scripts\test.ps1 \ No newline at end of file diff --git a/docs/commands/Invoke-DscNodeDocument.md b/docs/commands/Invoke-DscNodeDocument.md new file mode 100644 index 0000000..fbd89fa --- /dev/null +++ b/docs/commands/Invoke-DscNodeDocument.md @@ -0,0 +1,30 @@ + +# Invoke-DscNodeDocument + +## SYNOPSIS +Calls a document definition. + +## SYNTAX + +``` +Invoke-DscNodeDocument -DocumentName [-Path ] [-OutputPath ] +``` + +## EXAMPLES + +### EXAMPLE 1 + +```powershell +Document 'Test' { + + Section 'Installed features' { + 'The following Windows features have been installed.' + + $InputObject.ResourceType.WindowsFeature | Table -Property Name,Ensure; + } +} + +Invoke-DscNodeDocument -DocumentName 'Test' -Path '.\nodes' -OutputPath '.\docs'; +``` + +Generates a new markdown document for each node .mof in the path `.\nodes`. diff --git a/docs/commands/Invoke-PSDocument.md b/docs/commands/Invoke-PSDocument.md new file mode 100644 index 0000000..c11894d --- /dev/null +++ b/docs/commands/Invoke-PSDocument.md @@ -0,0 +1,11 @@ + +# Invoke-PSDocument + +## SYNOPSIS +Calls a document definition. + +## SYNTAX + +``` +Invoke-PSDocument [-Name] [-InstanceName ] [-InputObject ] [-ConfigurationData ] [-Path ] [-OutputPath ] [-Function >] +``` diff --git a/docs/examples/Get-child-item-output.md b/docs/examples/Get-child-item-output.md new file mode 100644 index 0000000..1202bff --- /dev/null +++ b/docs/examples/Get-child-item-output.md @@ -0,0 +1,11 @@ + +## Introduction +This is a sample file list from C:\\ + +|Name|PSIsContainer| +| --- | --- | +|PerfLogs|True| +|Program Files|True| +|Program Files (x86)|True| +|Users|True| +|Windows|True| \ No newline at end of file diff --git a/docs/keywords/Code.md b/docs/keywords/Code.md new file mode 100644 index 0000000..8150493 --- /dev/null +++ b/docs/keywords/Code.md @@ -0,0 +1,45 @@ + +# Code + +## SYNOPSIS +Creates a formatted code section. + +## SYNTAX + +``` +Code [-Body] +``` + +## EXAMPLES + +### EXAMPLE 1 + +```powershell +Document 'Test' { + + Code { + 'Get-Item -Path .\;' + } +} + +Invoke-PSDocument -Name 'Test' -InputObject $Null; +``` + +Generates a new Test.md document containing code. + +## PARAMETERS + +### -Body +A block of inline code to insert. + +```yaml +Type: ScriptBlock +Parameter Sets: (All) +Aliases: + +Required: True +Position: 1 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` diff --git a/docs/keywords/Document.md b/docs/keywords/Document.md new file mode 100644 index 0000000..991a1d0 --- /dev/null +++ b/docs/keywords/Document.md @@ -0,0 +1,38 @@ + +# Document + +## SYNOPSIS +Defines a named block that can be called to output documentation. + +## SYNTAX + +``` +Document [-Name] +``` + +## EXAMPLES + +### EXAMPLE 1 + +```powershell +Document 'Test' { + +} +``` + +## PARAMETERS + +### -Name +The name of the document. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: True +Position: 1 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` diff --git a/docs/keywords/Note.md b/docs/keywords/Note.md new file mode 100644 index 0000000..bad1189 --- /dev/null +++ b/docs/keywords/Note.md @@ -0,0 +1,46 @@ + +# Note + +## SYNOPSIS +Creates a formatted note block. + +## SYNTAX + +``` +Note [-Body] +``` + +## EXAMPLES + +### EXAMPLE 1 + +```powershell +Document 'Test' { + + Note { + 'This is a note.' + } +} + +Invoke-PSDocument -Name 'Test'; +``` + +Generates a new Test.md document containing a block quote formatted as a DFM note. + + +## PARAMETERS + +### -Body +A block of inline text to insert. + +```yaml +Type: ScriptBlock +Parameter Sets: (All) +Aliases: + +Required: True +Position: 1 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` diff --git a/docs/keywords/Section.md b/docs/keywords/Section.md new file mode 100644 index 0000000..5314a29 --- /dev/null +++ b/docs/keywords/Section.md @@ -0,0 +1,76 @@ + +# Section + +## SYNOPSIS +Creates a new document section that contains content. + +## SYNTAX + +``` +Section [-Name] [-When ] +``` + +## EXAMPLES + +### EXAMPLE 1 + +```powershell +Document 'Test' { + + Section 'Directory list' { + Get-ChildItem -Path 'C:\' | Table -Property Name,PSIsContainer; + } +} + +Invoke-PSDocument -Name 'Test'; +``` + +Generates a new Test.md document containing a table listing all items directly within C:\. + +### EXAMPLE 2 + +```powershell +Document 'Test' { + + Section 'Directory list' -When { Test-Path -Path 'C:\' } { + Get-ChildItem -Path 'C:\' | Table -Property Name,PSIsContainer; + } +} + +Invoke-PSDocument -Name 'Test'; +``` + +Generates a new Test.md document containing a table listing all items directly within C:\, but only if C:\ exists. + +## PARAMETERS + +### -Name +The name of the section. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: True +Position: 1 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -When +An optional condition that must be met before the section is included. + +```yaml +Type: ScriptBlock +Parameter Sets: (All) +Aliases: + +Required: False +Position: None +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + diff --git a/docs/keywords/Table.md b/docs/keywords/Table.md new file mode 100644 index 0000000..a97c217 --- /dev/null +++ b/docs/keywords/Table.md @@ -0,0 +1,28 @@ + +# Table + +## SYNOPSIS +Creates a formatted table. + +## SYNTAX + +``` +Table [-Property ] +``` + +## EXAMPLES + +### EXAMPLE 1 + +```powershell +Document 'Test' { + + Section 'Directory list' { + Get-ChildItem -Path 'C:\' | Table -Property Name,PSIsContainer; + } +} + +Invoke-PSDocument -Name 'Test'; +``` + +Generates a new Test.md document containing a table populated with a row for each item. Only the properties Name and PSIsContainer are added as columns. \ No newline at end of file diff --git a/docs/keywords/Warning.md b/docs/keywords/Warning.md new file mode 100644 index 0000000..06dfa95 --- /dev/null +++ b/docs/keywords/Warning.md @@ -0,0 +1,46 @@ + +# Warning + +## SYNOPSIS +Creates a formatted warning block. + +## SYNTAX + +``` +Warning [-Body] +``` + +## EXAMPLES + +### EXAMPLE 1 + +```powershell +Document 'Test' { + + Warning { + 'This is a warning.' + } +} + +Invoke-PSDocument -Name 'Test'; +``` + +Generates a new Test.md document containing a block quote formatted as a DFM warning. + + +## PARAMETERS + +### -Body +A block of inline text to insert. + +```yaml +Type: ScriptBlock +Parameter Sets: (All) +Aliases: + +Required: True +Position: 1 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` diff --git a/docs/keywords/Yaml.md b/docs/keywords/Yaml.md new file mode 100644 index 0000000..ef72506 --- /dev/null +++ b/docs/keywords/Yaml.md @@ -0,0 +1,46 @@ + +# Yaml + +## SYNOPSIS +Creates a yaml header. + +## SYNTAX + +``` +Yaml [-Body] +``` + +## EXAMPLES + +### EXAMPLE 1 + +```powershell +Document 'Test' { + + Yaml @{ + title = 'An example title' + } +} + +Invoke-PSDocument -Name 'Test'; +``` + +Generates a new Test.md document containing a yaml header. + + +## PARAMETERS + +### -Body +A hashtable containing header key/values. + +```yaml +Type: Hashtable +Parameter Sets: (All) +Aliases: + +Required: True +Position: 1 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` diff --git a/scripts/build.ps1 b/scripts/build.ps1 new file mode 100644 index 0000000..a1e1e88 --- /dev/null +++ b/scripts/build.ps1 @@ -0,0 +1,94 @@ +# +# Build script +# + +# Note: +# This script builds modules and related files. + +[CmdletBinding()] +param ( + # The path to source files + [Parameter(Mandatory = $False)] + [String]$Path = "$PWD\src", + + # The path to stored the processed files + [Parameter(Mandatory = $False)] + [String]$OutputPath = "$PWD\build", + + # Should output paths be cleaned first + [Parameter(Mandatory = $False)] + [Switch]$Clean = $False, + + # The modules to build + [Parameter(Mandatory = $False)] + [String[]]$Module, + + [Parameter(Mandatory = $False)] + [String[]]$IncludePackage +) + +Write-Verbose -Message "[Build]`tBEGIN::"; + +# STEP : Add includes + +# Include common library +. $PWD\scripts\common.ps1; + +# STEP: Setup environment + +# Setup variables +$rootPath = "$PWD"; +$sourcePath = $Path; +$buildPath = $OutputPath; +$packagePath = "$rootPath\packages"; +$configOut = "$buildPath"; +$modulesOut = "$rootPath\artifacts\modules"; + +# STEP : Validate parameters + +Write-Verbose -Message "[Build] -- Validating parameters"; + +if (!(Test-Path -Path $Path)) { + Write-Error -Message "The specified path ($Path) does not exist."; + + return; +} + +# STEP : Create output paths + +Write-Verbose -Message "[Build] -- Creating output paths: $buildPath"; + +# Create output path +@($buildPath, $configOut, $modulesOut) | CreatePath -Clean:$Clean -Verbose:$VerbosePreference; + +# STEP : Build modules + +Write-Verbose -Message "[Build] -- Build modules: $buildPath"; + +$Module | BuildModule -Path $sourcePath -OutputPath $buildPath -Verbose:$VerbosePreference; + +# STEP : Package modules + +Write-Verbose -Message "[Build] -- Package modules: $modulesOut"; + +$Module | PackageModule -Path $buildPath -OutputPath $modulesOut -Verbose:$VerbosePreference; + +# STEP : Include packages + +# Write-Verbose -Message "[Build] -- Copy packages"; + +# if (![String]::IsNullOrEmpty($IncludePackage)) { +# Get-ChildItem -Path $packagePath | Where-Object -FilterScript { +# $_.Name -like $IncludePackage -or $IncludePackage -contains $_.Name +# } | ForEach-Object -Process { +# $targetPath = $_; + +# Write-Verbose -Message "[Build] -- Copying package: $($targetPath.Name)"; + +# Copy-Item -Path $targetPath.FullName -Destination $buildPath -Recurse -Force; +# } +# } + +Write-Verbose -Message "[Build] END::"; + +# EOF \ No newline at end of file diff --git a/scripts/common.ps1 b/scripts/common.ps1 new file mode 100644 index 0000000..4b5ea08 --- /dev/null +++ b/scripts/common.ps1 @@ -0,0 +1,181 @@ +# +# Common helper functions +# + +function CreatePath { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $True, ValueFromPipeline = $True)] + [String]$Path, + + [Switch]$Clean = $False + ) + + process { + + # If the directory does not exist, force the creation of the path + if (!(Test-Path -Path $Path)) { + Write-Verbose -Message "[CreatePath] -- Creating path: $Path"; + + New-Item -Path $Path -ItemType Directory -Force | Out-Null; + } else { + Write-Verbose -Message "[CreatePath] -- Path already exists: $Path"; + + if ($Clean) { + Write-Verbose -Message "[CreatePath] -- Cleaning path: $Path"; + + Remove-Item -Path "$Path\" -Force -Recurse -Confirm:$False; + + New-Item -Path $Path -ItemType Directory -Force | Out-Null; + } + } + } +} + +function RunTest { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $True, ValueFromPipeline = $True)] + [String]$TestGroup, + + [Parameter(Mandatory = $True)] + [String]$Path, + + [Parameter(Mandatory = $True)] + [String]$OutputPath + ) + + begin { + Write-Verbose -Message "[RunTest] BEGIN::"; + } + + process { + + $currentPath = $PWD; + + try { + Set-Location -Path "$Path\$TestGroup.Tests" -ErrorAction Stop; + + Write-Verbose -Message "[RunTest] -- Running tests: $Path\$TestGroup.Tests"; + + # Run Pester tests + $pesterParams = @{ OutputFile = "$OutputPath\$TestGroup.xml"; OutputFormat = 'NUnitXml'; PesterOption = @{ IncludeVSCodeMarker = $True }; }; + + Invoke-Pester @pesterParams; + + } finally { + Set-Location -Path $currentPath; + } + } + + end { + Write-Verbose -Message "[RunTest] END::"; + } +} + +function BuildModule { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $True, ValueFromPipeline = $True)] + [String]$Module, + + [Parameter(Mandatory = $True)] + [String]$Path, + + [Parameter(Mandatory = $True)] + [String]$OutputPath + ) + + begin { + Write-Verbose -Message "[BuildModule] BEGIN::"; + } + + process { + + if (Test-Path -Path ("$OutputPath\$Module")) { + Remove-Item -Path "$OutputPath\$Module" -Recurse -Force; + } + + Copy-Item -Path "$Path\$Module" -Destination $OutputPath -Recurse -Force; + } + + end { + Write-Verbose -Message "[BuildModule] END::"; + } +} + +function PackageModule { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $True, ValueFromPipeline = $True)] + [String]$Module, + + [Parameter(Mandatory = $True)] + [String]$Path, + + [Parameter(Mandatory = $True)] + [String]$OutputPath + ) + + begin { + Write-Verbose -Message "[PackageModule] BEGIN::"; + } + + process { + + Write-Verbose -Message "[PackageModule] -- Packaging module: $Module"; + + $targetFile = "$OutputPath\$Module.zip"; + + Compress-Archive -DestinationPath $targetFile -Path "$Path\$Module" -Force; + + Write-Verbose -Message "[PackageModule] -- Saved module to: $targetFile"; + } + + end { + Write-Verbose -Message "[PackageModule] END::"; + } +} + +function SendAppveyorTestResult { + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $True)] + [String]$Uri, + + [Parameter(Mandatory = $True)] + [String]$Path, + + [Parameter(Mandatory = $False)] + [String]$Include = '*' + ) + + begin { + Write-Verbose -Message "[SendAppveyorTestResult] BEGIN::"; + } + + process { + + try { + $webClient = New-Object -TypeName 'System.Net.WebClient'; + + foreach ($resultFile in (Get-ChildItem -Path $Path -Filter $Include -File -Recurse)) { + + Write-Verbose -Message "[SendAppveyorTestResult] -- Uploading file: $($resultFile.FullName)"; + + $webClient.UploadFile($Uri, "$($resultFile.FullName)"); + } + } + catch { + throw $_.Exception; + } + finally { + $webClient = $Null; + } + } + + end { + Write-Verbose -Message "[SendAppveyorTestResult] END::"; + } +} \ No newline at end of file diff --git a/scripts/test.ps1 b/scripts/test.ps1 new file mode 100644 index 0000000..e97b1df --- /dev/null +++ b/scripts/test.ps1 @@ -0,0 +1,60 @@ +# +# Test script +# + +# Note: +# This script runs unit tests. + +[CmdletBinding()] +param ( + # Should output paths be cleaned first + [Parameter(Mandatory = $False)] + [Switch]$Clean = $False +) + +Write-Verbose -Message "[Test] BEGIN::"; + +# Include common library +. $PWD\scripts\common.ps1; + +$rootPath = "$PWD"; +$sourcePath = "$rootPath\src"; +$reportsPath = "$rootPath\reports"; +$testPath = "$rootPath\tests"; + +# Setup path to load modules +$Env:PSModulePath = $Env:PSModulePath + ";$rootPath\packages;$sourcePath"; + +if ([String]::IsNullOrEmpty($ResultsPath)) { + $ResultsPath = "$rootPath\reports"; +} + +# STEP : Create output paths + +Write-Verbose -Message "[Test] -- Creating output paths"; + +# Create output path +@($reportsPath) | CreatePath -Clean:$Clean -Verbose:$VerbosePreference; + +# STEP : Run tests + +$pesterModule = Get-Module -Name Pester -ListAvailable | Where-Object -FilterScript { $_.Version -like '3.4.0' }; + +if ($Null -eq $pesterModule) { + Install-PackageProvider -Name NuGet -Force -Scope CurrentUser; + + Install-Module -Name Pester -RequiredVersion '3.4.0' -Force -Scope CurrentUser; +} + +# Load Pester module +Import-Module -Name Pester -Verbose:$False; + +@('PSDocs', 'PSDocs.Dsc') | RunTest -Path $testPath -OutputPath $reportsPath -Verbose:$VerbosePreference; + +# STEP : Publish results + +if (![String]::IsNullOrEmpty($Env:APPVEYOR_JOB_ID)) { + SendAppveyorTestResult -Uri "https://ci.appveyor.com/api/testresults/nunit/$($env:APPVEYOR_JOB_ID)" -Path '.\reports' -Include '*.xml'; +} + +Write-Verbose -Message "[Test] END::"; \ No newline at end of file diff --git a/src/PSDocs.Dsc/PSDocs.Dsc.psd1 b/src/PSDocs.Dsc/PSDocs.Dsc.psd1 new file mode 100644 index 0000000..f94c360 --- /dev/null +++ b/src/PSDocs.Dsc/PSDocs.Dsc.psd1 @@ -0,0 +1,123 @@ +# +# PSDocs Dsc extensions module +# + +@{ + +# Script module or binary module file associated with this manifest. +RootModule = 'PSDocs.Dsc.psm1' + +# Version number of this module. +ModuleVersion = '0.2.0' + +# Supported PSEditions +# CompatiblePSEditions = @() + +# ID used to uniquely identify this module +GUID = '9c6339e5-174f-447d-b7a7-7dd58ae9a13d' + +# Author of this module +Author = 'Microsoft Corporation' + +# Company or vendor of this module +CompanyName = 'Microsoft Corporation' + +# Copyright statement for this module +Copyright = '(c) Microsoft Corporation. All rights reserved.' + +# Description of the functionality provided by this module +Description = 'Desired State Configuration (DSC) extensions for PSDocs.' + +# Minimum version of the Windows PowerShell engine required by this module +# PowerShellVersion = '' + +# Name of the Windows PowerShell host required by this module +# PowerShellHostName = '' + +# Minimum version of the Windows PowerShell host required by this module +# PowerShellHostVersion = '' + +# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# DotNetFrameworkVersion = '' + +# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# CLRVersion = '' + +# Processor architecture (None, X86, Amd64) required by this module +# ProcessorArchitecture = '' + +# Modules that must be imported into the global environment prior to importing this module +RequiredModules = @( + @{ ModuleName = 'PSDocs'; ModuleVersion = '0.2.0'; } +) + +# Assemblies that must be loaded prior to importing this module +# RequiredAssemblies = @() + +# Script files (.ps1) that are run in the caller's environment prior to importing this module. +# ScriptsToProcess = @() + +# Type files (.ps1xml) to be loaded when importing this module +# TypesToProcess = @() + +# Format files (.ps1xml) to be loaded when importing this module +# FormatsToProcess = @() + +# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess +# NestedModules = @() + +# 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 = @( + 'Invoke-DscNodeDocument' + 'Get-DscMofDocument' +) + +# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. +CmdletsToExport = @() + +# Variables to export from this module +VariablesToExport = @() + +# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. +AliasesToExport = @() + +# DSC resources to export from this module +# DscResourcesToExport = @() + +# List of all modules packaged with this module +# ModuleList = @() + +# List of all files packaged with this module +# FileList = @() + +# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('Markdown') + + # A URL to the license for this module. + LicenseUri = 'https://github.com/BernieWhite/PSDocs/blob/master/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/BernieWhite/PSDocs' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + ReleaseNotes = 'https://github.com/BernieWhite/PSDocs/releases' + + } # End of PSData hashtable + +} # End of PrivateData hashtable + +# HelpInfo URI of this module +# HelpInfoURI = '' + +# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. +# DefaultCommandPrefix = '' + +} \ No newline at end of file diff --git a/src/PSDocs.Dsc/PSDocs.Dsc.psm1 b/src/PSDocs.Dsc/PSDocs.Dsc.psm1 new file mode 100644 index 0000000..8f4fd25 --- /dev/null +++ b/src/PSDocs.Dsc/PSDocs.Dsc.psm1 @@ -0,0 +1,346 @@ +# +# PSDocs DSC extensions module +# + +class DscMofDocument { + + [System.Collections.Generic.Dictionary[String, PSObject[]]]$ResourceType + + [System.Collections.Generic.Dictionary[String, PSObject]]$ResourceId + + [String]$Path + + [String]$InstanceName + + DscMofDocument() { + $this.ResourceId = @{ }; + $this.ResourceType = @{ }; + } +} + +# +# Localization +# + +$LocalizedData = data { + +} + +Import-LocalizedData -BindingVariable LocalizedData -FileName 'PSDocs.Dsc.Resources.psd1' -ErrorAction SilentlyContinue; + +# +# Public functions +# + +function Invoke-DscNodeDocument { + + [CmdletBinding()] + param ( + # The name of the document + [Parameter(Mandatory = $False)] + [String]$DocumentName, + + # A script or path to the script to run + [Parameter(Mandatory = $False)] + [String]$Script, + + [Parameter(Mandatory = $False)] + [String[]]$InstanceName, + + # The path to the .mof files + [Parameter(Mandatory = $False)] + [String]$Path = $PWD, + + # The path to output documentation + [Parameter(Mandatory = $False)] + [String]$OutputPath = $PWD + ) + + begin { + Write-Verbose -Message "[Invoke-DscNodeDocument]::BEGIN"; + } + + process { + # Build the documentation + BuildDocumentation @PSBoundParameters; + } + + end { + Write-Verbose -Message "[Invoke-DscNodeDocument]::END"; + } +} + +function Get-DscMofDocument { + + [CmdletBinding()] + [OutputType([DscMofDocument])] + param ( + [Parameter(Mandatory = $True)] + [String]$Path + ) + + process { + # Import and return the .mof as an object containing resource instances + ImportMofDocument -Path $Path -Verbose:$VerbosePreference; + } +} + +# +# Helper functions +# + +function BuildDocumentation { + + [CmdletBinding()] + [OutputType([void])] + param ( + [Parameter(Mandatory = $False)] + [String]$DocumentName, + + [Parameter(Mandatory = $False)] + [String]$Script, + + [Parameter(Mandatory = $False)] + [String[]]$InstanceName, + + # The path to the .mof file + [Parameter(Mandatory = $False)] + [String]$Path = $PWD, + + # The output path to store documentaion + [Parameter(Mandatory = $False)] + [String]$OutputPath = $PWD + ) + + process { + + $referenceConfig = New-Object -TypeName System.Collections.Generic.List[PSObject]; + + try { + # Look for .mof file within the path + $referenceConfigFilePath = FindMofDocument -Path $Path -InstanceName $InstanceName; + + if ($Null -eq $referenceConfigFilePath -or $referenceConfigFilePath.Length -le 0) { + return; + } + + # Extract a reference configuration for each .mof file + foreach ($file in $referenceConfigFilePath) { + $referenceConfig.Add((ImportMofDocument -Path $file -Verbose:$VerbosePreference)); + } + } + catch { + Write-Error -Message ($LocalizedData.ImportMofFailed -f $Path, $_.Exception.Message) -Exception $_.Exception; + + return; + } + + if ($PSBoundParameters.ContainsKey('Script')) { + + try { + Import-PSDocumentTemplate -Path $Script -Verbose:$VerbosePreference; + } + catch { + Write-Error -Message ($LocalizedData.ImportDocumentTemplateFailed -f $Script, $_.Exception.Message) -Exception $_.Exception; + + return; + } + } + + foreach ($r in $referenceConfig) { + # Write-Verbose -Message "[Doc][Mof] -- Analysing document: $($r.Path)"; + + # Write-Verbose -Message "[Doc] -- Generating documentation: $OutputPath"; + + # Generate a document for the configuration + Invoke-PSDocument -InputObject $r -InstanceName $r.InstanceName -Name $DocumentName -OutputPath $OutputPath -Verbose:$VerbosePreference; + } + + # Write-Verbose -Message "[Doc][$dokOperation] -- Update TOC: $($buildResult.FullName)"; + + # Update TOC + # UpdateToc -OutputPath $OutputPath -Verbose:$VerbosePreference; + } +} + +# Builds a configuration graph from a .mof file +function ImportMofDocument { + + [CmdletBinding()] + [OutputType([DscMofDocument])] + param ( + [Parameter(Mandatory = $True)] + [String]$Path + ) + + process { + + Write-Verbose -Message "[Doc][Mof][Import]::BEGIN"; + + # Parse a .mof file and extract object instances + $instances = ParseMofDocument -Path $Path -Verbose:$VerbosePreference; + + # Extract the instance name from the .mof file name + $Path -match '\\((?[A-Z0-9_]{3,})(\.meta){0,}\.mof)$' | Out-Null; + $instanceName = $Matches.name; + + # Build a configuration object + $result = New-Object -TypeName DscMofDocument -Property @{ + InstanceName = $instanceName + Path = $path + }; + + # Process each instance and inde by id and type + foreach ($instance in $instances) { + + $resourceId = $instance.ResourceId; + $resourceType = $Null; + + if (![String]::IsNullOrEmpty($resourceId)) { + + # Extract resource type from ResourceId + if ($resourceId -match '^(\s{0,}\[(?[A-Z0-9_:]*)\][A-Z0-9_:\]\[]*)$') { + $resourceType = $Matches.type; + } + + Write-Verbose -Message "[Doc][Mof][Import] -- Adding resource id: $resourceId"; + + # Add the instance indexed by ResourceId + $result.ResourceId[$resourceId] = $instance; + + if ($Null -ne $resourceType) { + if (!$result.ResourceType.ContainsKey($resourceType)) { + Write-Verbose -Message "[Doc][Mof][Import] -- Adding resource type: $resourceType"; + + $result.ResourceType.Add($resourceType, @()); + } + + # Add the instance indexed by ResourceType + $result.ResourceType[$resourceType] += $instance; + } + } + } + + # Emit the mof graph object to the pipeline + $result; + + Write-Verbose -Message "[Doc][Mof][Import]::END"; + } +} + +# Parses a .mof file into object insances +function ParseMofDocument { + [CmdletBinding()] + param ( + # The path to the .mof file + [Parameter(Mandatory = $True)] + [String]$Path + ) + + process { + + Write-Verbose -Message "[Doc][Mof][Import] -- Parsing: $Path"; + + # Split the .mof into instances + $instances = ((Get-Content $Path -Raw) -split "\n(?=instance of)" -match 'instance of ([A-Z_]*) as'); + + # This variable will store configuration items + $result = New-Object -TypeName System.Collections.Generic.List[PSObject]; + + # Process each instance + foreach ($instance in $instances) { + + # This variable will store properties for a single configuration item + $props = New-Object -TypeName 'System.Collections.Generic.Dictionary[String,Object]'([System.StringComparer]::OrdinalIgnoreCase); + + # Extract out properties from mof instance block + $instance -match '\n\{(\r|\n)(?(.|\n)+)\};' | Out-Null; + + # Cleanup new line, line feeds and space padding + $inner = ($Matches.props -replace '(\r|\n){1,}\s{1,}', "`n") -replace "\n\r", "`n" -split ";\n"; + + # Process each property for the configuration item + $inner | ForEach-Object -Process { + + # Break out key value pairs + $prop = ($_ -replace '\r|\n', '') -Split '\s{0,}=\s{0,}',2; + + # Ensure that a key value pair was found + if ($prop.Length -eq 2) { + + # Cleanup value by removing quotes, line feeds and escaped slashes + $value = ($prop[1] -replace '^(\")|(\"(\;\r){0,})$', '') -replace '\\\\', '\'; + + # Look for array values + if ($value -match '^(\{(?.*)\})$') { + + $valueArray = $Matches.array; + + # Look for string array for type convertion + if ($valueArray -match '(^\"|\"$)') { + + # Force value to be a string array, and cleanup quotes + $value = [String[]]@(($valueArray -split '","' -replace '(^\"|\"$)', '')); + } else { + + # Convert value to object array + $value = $valueArray -split ','; + } + } + + # Add key value pair to dictionary + $props[$prop[0]] = $value; + } + } + + # Add object based on properties to result + $result.Add((New-Object -TypeName PSObject -Property $props)); + } + + # Emit result to the pipeline + $result; + + Write-Verbose -Message "[Doc][Mof][Import] -- Found instances: $($result.Count)"; + } +} + +# Finds .mof file in a specified path +function FindMofDocument { + + [CmdletBinding()] + [OutputType([String])] + param ( + # The directory path to search for .mof files within + [Parameter(Mandatory = $True)] + [String]$Path, + + # An optional InstanceName filter to filter .mof files returned + [Parameter(Mandatory = $False)] + [String[]]$InstanceName + ) + + process { + Write-Verbose -Message "[Doc][Mof] -- Scanning for .mof files in: $Path"; + + # Search for mof files + $items = Get-ChildItem -Path $Path -Filter *.mof -File; + + foreach ($item in $items) { + if ($Null -eq $InstanceName -or $InstanceName -contains $item.BaseName) { + # Emit the full name of a mof file to the pipeline when it matches the criteria + $item.FullName; + } + } + } +} + +# +# Export module +# + +Export-ModuleMember -Function @( + 'Invoke-DscNodeDocument' + 'Get-DscMofDocument' +) + +# EOM \ No newline at end of file diff --git a/src/PSDocs.Dsc/en-AU/PSDocs.Dsc.Resources.psd1 b/src/PSDocs.Dsc/en-AU/PSDocs.Dsc.Resources.psd1 new file mode 100644 index 0000000..4fb02a3 --- /dev/null +++ b/src/PSDocs.Dsc/en-AU/PSDocs.Dsc.Resources.psd1 @@ -0,0 +1,7 @@ + +ConvertFrom-StringData @' +###PSLOC +ImportMofFailed=Failed to import .mof files from ({0}): {1} +ImportDocumentTemplateFailed=Failed to import document template ({0}): {1} +###PSLOC +'@ \ No newline at end of file diff --git a/src/PSDocs.Dsc/en-US/PSDocs.Dsc.Resources.psd1 b/src/PSDocs.Dsc/en-US/PSDocs.Dsc.Resources.psd1 new file mode 100644 index 0000000..4fb02a3 --- /dev/null +++ b/src/PSDocs.Dsc/en-US/PSDocs.Dsc.Resources.psd1 @@ -0,0 +1,7 @@ + +ConvertFrom-StringData @' +###PSLOC +ImportMofFailed=Failed to import .mof files from ({0}): {1} +ImportDocumentTemplateFailed=Failed to import document template ({0}): {1} +###PSLOC +'@ \ No newline at end of file diff --git a/src/PSDocs/PSDocs.psd1 b/src/PSDocs/PSDocs.psd1 new file mode 100644 index 0000000..5b962ea --- /dev/null +++ b/src/PSDocs/PSDocs.psd1 @@ -0,0 +1,122 @@ +# +# PSDocs module +# + +@{ + +# Script module or binary module file associated with this manifest. +RootModule = 'PSDocs.psm1' + +# Version number of this module. +ModuleVersion = '0.2.0' + +# Supported PSEditions +# CompatiblePSEditions = @() + +# ID used to uniquely identify this module +GUID = '1f6df554-c081-40d8-9aca-32c1abe4a1b6' + +# Author of this module +Author = 'Microsoft Corporation' + +# Company or vendor of this module +CompanyName = 'Microsoft Corporation' + +# Copyright statement for this module +Copyright = '(c) Microsoft Corporation. All rights reserved.' + +# Description of the functionality provided by this module +Description = 'Generate markdown from PowerShell.' + +# Minimum version of the Windows PowerShell engine required by this module +# PowerShellVersion = '' + +# Name of the Windows PowerShell host required by this module +# PowerShellHostName = '' + +# Minimum version of the Windows PowerShell host required by this module +# PowerShellHostVersion = '' + +# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# DotNetFrameworkVersion = '' + +# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# CLRVersion = '' + +# Processor architecture (None, X86, Amd64) required by this module +# ProcessorArchitecture = '' + +# Modules that must be imported into the global environment prior to importing this module +# RequiredModules = @() + +# Assemblies that must be loaded prior to importing this module +# RequiredAssemblies = @() + +# Script files (.ps1) that are run in the caller's environment prior to importing this module. +# ScriptsToProcess = @() + +# Type files (.ps1xml) to be loaded when importing this module +# TypesToProcess = @() + +# Format files (.ps1xml) to be loaded when importing this module +# FormatsToProcess = @() + +# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess +# NestedModules = @() + +# 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 = @( + 'Document' + 'Invoke-PSDocument' + 'Import-PSDocumentTemplate' +) + +# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. +CmdletsToExport = @() + +# Variables to export from this module +VariablesToExport = @() + +# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. +AliasesToExport = @() + +# DSC resources to export from this module +# DscResourcesToExport = @() + +# List of all modules packaged with this module +# ModuleList = @() + +# List of all files packaged with this module +# FileList = @() + +# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('Markdown') + + # A URL to the license for this module. + LicenseUri = 'https://github.com/BernieWhite/PSDocs/blob/master/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/BernieWhite/PSDocs' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + ReleaseNotes = 'https://github.com/BernieWhite/PSDocs/releases' + + } # End of PSData hashtable + +} # End of PrivateData hashtable + +# HelpInfo URI of this module +# HelpInfoURI = '' + +# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. +# DefaultCommandPrefix = '' + +} \ No newline at end of file diff --git a/src/PSDocs/PSDocs.psm1 b/src/PSDocs/PSDocs.psm1 new file mode 100644 index 0000000..5bdb288 --- /dev/null +++ b/src/PSDocs/PSDocs.psm1 @@ -0,0 +1,818 @@ +# +# PSDocs module +# + +# +# Localization +# + +$LocalizedData = data { + +} + +Import-LocalizedData -BindingVariable LocalizedData -FileName 'PSDocs.Resources.psd1' -ErrorAction SilentlyContinue; + +# +# Public functions +# + +# Implement the document keyword +function Document { + [CmdletBinding()] + param ( + [Parameter(Position = 0, Mandatory = $True)] + [String]$Name, + + [Parameter(Position = 1, Mandatory = $True)] + [ScriptBlock]$Body + ) + + process { + + Write-Verbose -Message "[Document]::BEGIN" + + InitDocumentContext; + + $Script:DocumentBody[$Name] = $Body; + + # Export documentation function + Set-Item -Path "function:global:$Name" -Value (${function:GenerateDocumentFn}); + + Write-Verbose -Message "[Document]::END" + } +} + +function Invoke-PSDocument { + + [CmdletBinding()] + param ( + # The name of the document + [Parameter(Position = 0, Mandatory = $True)] + [String]$Name, + + [Parameter(Mandatory = $False)] + [String[]]$InstanceName, + + [Parameter(Mandatory = $False, ValueFromPipeline = $True)] + [PSObject]$InputObject, + + [Parameter(Mandatory = $False)] + [Object]$ConfigurationData, + + # The path to look for document definitions in + [Parameter(Mandatory = $False)] + [String]$Path = $PWD, + + # The output path to save generated documentation + [Parameter(Mandatory = $False)] + [String]$OutputPath = $PWD, + + [Parameter(Mandatory = $False)] + [ValidateNotNull()] + [System.Collections.Generic.Dictionary[String, ScriptBlock]]$Function, + + [Parameter(Mandatory = $False)] + [Switch]$PassThru = $False + ) + + process { + Write-Verbose -Message "[Invoke-PSDocument]::BEGIN"; + + $fnParams = $PSBoundParameters; + + GenerateDocument @fnParams; + + Write-Verbose -Message "[Invoke-PSDocument]::END"; + } +} + +function Import-PSDocumentTemplate { + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $True)] + [String]$Path + ) + + process { + Write-Verbose -Message "[Doc] -- Reading template: $Path"; + + ReadTemplate @PSBoundParameters; + } +} + +# +# Internal language keywords +# + +# Implement the Section keyword +function Section { + + [CmdletBinding()] + [OutputType([PSObject])] + param ( + # The name of the Section + [Parameter(Position = 0, Mandatory = $True)] + [String]$Name, + + # A script block with the body of the Section + [Parameter(Position = 1, Mandatory = $True)] + [ScriptBlock]$Body, + + # Optionally a condition that must be met prior to including the Section + [Parameter(Mandatory = $False)] + [ScriptBlock]$When + ) + + begin { + Write-Verbose -Message "[Doc][Section] BEGIN::"; + } + + process { + + $shouldProcess = $True; + + # Evaluate if the Section condition is met + if ($Null -ne $When) { + + Write-Verbose -Message "[Doc][Section] -- When: $When"; + + $conditionResult = $When.InvokeReturnAsIs(); + + Write-Verbose -Message "[Doc][Section] -- When: $conditionResult"; + + if (($Null -eq $conditionResult) -or ($conditionResult -is [System.Boolean] -and $conditionResult -eq $False)) { + $shouldProcess = $False; + } + } + + # Run Section block if condition was met + if ($shouldProcess) { + Write-Verbose -Message "[Doc][Section] -- Adding section: $Name"; + + $result = New-Object -TypeName PSObject -Property @{ Content = $Name; Type = 'Section'; Node = @(); Level = ($Section.Level+1) }; + + $Section = $result; + + # Invoke the Section body and collect the results + $innerResult = $Body.Invoke(); + + foreach ($r in $innerResult) { + $result.Node += $r; + } + + # Emit Section object to the pipeline + $result; + } + } + + end { + Write-Verbose -Message "[Doc][Section] END::"; + } +} + +function Title { + [CmdletBinding()] + param ( + [Parameter(Position = 0, Mandatory = $True)] + [AllowEmptyString()] + [String]$Title + ) + + process { + $result = New-Object -TypeName PSObject -Property @{ Type = 'Title'; Content = $Title; }; + + $result; + } +} + +function Code { + [CmdletBinding()] + param ( + [Parameter(Position = 0, Mandatory = $True)] + [ScriptBlock]$Body + ) + + process { + $result = New-Object -TypeName PSObject -Property @{ Type = 'Code'; Content = ''; }; + + $innerResult = $Body.InvokeWithContext($Null, $Null); + + foreach ($r in $innerResult) { + $result.Content += $r; + } + + $result; + } +} + +function List { + + [CmdletBinding()] + param ( + [Parameter(Position = 0, Mandatory = $True)] + [ScriptBlock]$Body + ) + + process { + + $result = New-Object -TypeName PSObject -Property @{ Type = 'List'; Node = @(); }; + + $innerResult = $Body.InvokeWithContext($Null, $Null); + + foreach ($r in $innerResult) { + $result.Node += $r; + } + + $result; + } +} + +function Note { + + [CmdletBinding()] + param ( + [Parameter(Position = 0, Mandatory = $True)] + [ScriptBlock]$Body + ) + + process { + + $result = New-Object -TypeName PSObject -Property @{ Type = 'Note'; Node = @(); Content = [String[]]@(); }; + + $innerResult = $Body.InvokeWithContext($Null, $Null); + + foreach ($r in $innerResult) { + $result.Content += $r; + } + + $result; + } +} + +function Warning { + + [CmdletBinding()] + param ( + [Parameter(Position = 0, Mandatory = $True)] + [ScriptBlock]$Body + ) + + process { + + $result = New-Object -TypeName PSObject -Property @{ Type = 'Warning'; Node = @(); Content = [String[]]@(); }; + + $innerResult = $Body.InvokeWithContext($Null, $Null); + + foreach ($r in $innerResult) { + $result.Content += $r; + } + + $result; + } +} + +function Yaml { + + [CmdletBinding()] + param ( + [Parameter(Position = 0, Mandatory = $True)] + [Hashtable]$Body + ) + + process { + + $result = New-Object -TypeName PSObject -Property @{ Type = 'Yaml'; Node = @(); Content = $Body; }; + + $result; + } +} + +function Table { + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $True, ValueFromPipeline = $True)] + [Object]$InputObject, + + [Parameter(Mandatory = $False, Position = 0)] + [String[]]$Property + ) + + begin { + Write-Verbose -Message "[Doc][Table] BEGIN::"; + + $table = New-Object -TypeName PSObject -Property @{ Type = 'Table'; Header = @(); Rows = (New-Object -TypeName Collections.Generic.List[String[]]); ColumnCount = 0; }; + + $recordIndex = 0; + + $rowData = New-Object -TypeName Collections.Generic.List[Object]; + + # if ($Property -is [Hashtable[]]) { + # $Property = $Property -as [Hashtable[]]; + # } else { + # $Property = $Property -as [String[]]; + # } + } + + process { + + Write-Verbose -Message "[Doc][Table][$recordIndex] BEGIN::"; + + Write-Verbose -Message "[Doc][Table][$recordIndex] -- Adding '$($InputObject)'"; + + if ($Null -ne $InputObject) { + $selectedObject = Select-Object -InputObject $InputObject -Property $Property; + + $rowData.Add($selectedObject); + } + + Write-Verbose -Message "[Doc][Table][$recordIndex] END::"; + + $recordIndex++; + } + + end { + [String[]]$headers = $rowData | ForEach-Object -Process { + $_.PSObject.Properties + } | Where-Object -FilterScript { + $_.IsGettable -and $_.IsInstance + } | Select-Object -Unique -ExpandProperty Name; + + $table.Header = @($headers); + + foreach ($r in $rowData) { + + $row = New-Object -TypeName 'String[]' -ArgumentList $headers.Length; + + for ($i = 0; $i -lt $row.Length; $i++) { + $field = GetObjectField -InputObject $r -Field $headers[$i] -Verbose:$VerbosePreference; + + if ($Null -ne $field -and $Null -ne $field.Value) { + $row[$i] = $field.Value.ToString(); + } + } + + $table.Rows.Add($row); + } + + $table; + + Write-Verbose -Message "[Doc][Table] END:: [$($headers.Length)]"; + } +} + +function FormatList { + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $True, ValueFromPipeline = $True)] + [Object]$InputObject, + + [Parameter(Mandatory = $False, Position = 0)] + [String[]]$Property + ) + + begin { + Write-Verbose -Message "[Doc][FormatList] BEGIN::"; + + $recordIndex = 0; + } + + process { + + Write-Verbose -Message "[Doc][FormatList][$recordIndex] BEGIN::"; + + $table = New-Object -TypeName PSObject -Property @{ Type = 'Table'; Header = @($Property); Rows = (New-Object -TypeName Collections.Generic.List[String[]]); ColumnCount = 0; }; + + [String[]]$objectFields = @($Property); + + if ($Null -ne $InputObject) { + + for ($i = 0; $i -lt $table.Header.Count; $i++) { + $field = GetObjectField -InputObject $InputObject -Field $objectFields[$i] -Verbose:$VerbosePreference; + + if ($Null -ne $field -and $Null -ne $field.Value) { + + Write-Verbose -Message "[Doc][FormatList][$recordIndex] -- Adding $($field.Name): $($field.Value)"; + + [String[]]$row = , [String]::Empty * 2; + + $row[0] = $field.Name; + + $row[1] = $field.Value; + + $table.Rows.Add($row); + } + } + + $table; + } + + Write-Verbose -Message "[Doc][FormatList][$recordIndex] END::"; + + $recordIndex++; + } + + end { + Write-Verbose -Message "[Doc][FormatList] END::"; + } +} + +# +# Helper functions +# + +function InitDocumentContext { + [CmdletBinding()] + param ( + + ) + + process { + + if ($Null -eq $Script:DocumentBody) { + $Script:DocumentBody = @{ }; + } + } +} + +function GenerateDocumentFn { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $True, ValueFromPipeline = $True)] + [PSObject]$InputObject, + + [Parameter(Mandatory = $False)] + [Object]$ConfigurationData, + + [Parameter(Mandatory = $False)] + [String]$OutputPath = $PWD + ) + + process { + Write-Verbose -Message "[$($MyInvocation.InvocationName)]::BEGIN"; + + $fnParams = $PSBoundParameters; + + GenerateDocument -Name $MyInvocation.InvocationName @fnParams; + + Write-Verbose -Message "[$($MyInvocation.InvocationName)]::END"; + } +} + +function GenerateDocument { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $True)] + [String]$Name, + + [Parameter(Mandatory = $False)] + [String[]]$InstanceName, + + [Parameter(Mandatory = $False)] + [PSObject]$InputObject, + + [Parameter(Mandatory = $False)] + [Object]$ConfigurationData, + + [Parameter(Mandatory = $False)] + [String]$Path = $PWD, + + [Parameter(Mandatory = $False)] + [String]$OutputPath = $PWD, + + [Parameter(Mandatory = $False)] + [System.Collections.Generic.Dictionary[String, ScriptBlock]]$Function, + + [Parameter(Mandatory = $False)] + [Switch]$PassThru = $False + ) + + begin { + if ($Null -eq $Script:DocumentBody -or !$Script:DocumentBody.ContainsKey($Name)) { + + Write-Error -Message ($LocalizedData.DocumentNotFound -f $Name) -ErrorAction Stop; + + return; + } + + [Hashtable]$parameter = $Null; + + # Import configuration data from either a hashtable or .psd1 file + if ($ConfigurationData -is [Hashtable]) { + $parameter = $ConfigurationData + } elseif ($ConfigurationData -is [String] -and (Test-Path -Path $ConfigurationData -File)) { + $parentPath = Split-Path -Parent -Path $ConfigurationData; + $leafPath = Split-Path -Left -Path $ConfigurationData; + + Import-LocalizedData -BindingVariable parameter -BaseDirectory $parentPath -FileName $leafPath; + } + + $body = $Script:DocumentBody[$Name]; + + # Prepare PSDocs language functions + $functionsToDefine = New-Object -TypeName 'System.Collections.Generic.Dictionary[String,ScriptBlock]'([System.StringComparer]::OrdinalIgnoreCase); + + # Add external functions + if ($Null -ne $Function -and $Function.Count -gt 0) { + foreach ($fn in $Function) { + $functionsToDefine.Add($fn.Key, $fn.Value); + } + } + + # Define built-in functions + $functionsToDefine['Section'] = ${function:Section}; + $functionsToDefine['Title'] = ${function:Title}; + $functionsToDefine['List'] = ${function:List}; + $functionsToDefine['Code'] = ${function:Code}; + $functionsToDefine['Note'] = ${function:Note}; + $functionsToDefine['Warning'] = ${function:Warning}; + $functionsToDefine['Yaml'] = ${function:Yaml}; + $functionsToDefine['Table'] = ${function:Table}; + $functionsToDefine['Format-Table'] = ${function:Table}; + $functionsToDefine['Format-List'] = ${function:FormatList}; + } + + process { + + [String[]]$instances = @($InstanceName); + + # If an instance name is not specified, default to the document name + if ($Null -eq $InstanceName) { + $instances = @($Name); + } + + # Set the default section level so that sections in the document start from 2 + $Section = @{ Level = 1; }; + + foreach ($instance in $instances) { + + Write-Verbose -Message "[Doc] -- Processing: $instance"; + + # Define built-in variables + [PSVariable[]]$variablesToDefine = @( + New-Object -TypeName PSVariable -ArgumentList ('InstanceName', $instance) + New-Object -TypeName PSVariable -ArgumentList ('InputObject', $InputObject) + New-Object -TypeName PSVariable -ArgumentList ('Parameter', $parameter) + New-Object -TypeName PSVariable -ArgumentList ('Section', $Section) + ); + + # Invoke the body of the document definition and get the output + $innerResult = $body.InvokeWithContext($functionsToDefine, $variablesToDefine); + + # Create a document object model based on the output + $dom = New-Object -TypeName PSObject -Property @{ Node = $innerResult; }; + + # Build a path for the document + $documentPath = Join-Path -Path $OutputPath -ChildPath "$instance.md"; + + # Parse the model + ParseDom -Dom $dom -Processor (NewMarkdownProcessor) -Verbose:$VerbosePreference | WriteDocumentContent -Path $documentPath -PassThru:$PassThru; + } + } +} + +function WriteDocumentContent { + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $True, ValueFromPipeline = $True)] + [PSObject]$InputObject, + + # The path to the document. + [Parameter(Mandatory = $True)] + [String]$Path, + + [Parameter(Mandatory = $False)] + [Switch]$PassThru = $False + ) + + begin { + $content = @(); + } + + process { + $content += $InputObject; + } + + end { + if ($PassThru) { + $content; + } else { + $content | Set-Content -Path $Path; + } + + } +} + +function ParseDom { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $True)] + [PSObject]$Dom, + + [Parameter(Mandatory = $True)] + [PSObject]$Processor + ) + + process { + + $nodeCounter = 0; + + # Process each node of the DOM + $innerResult = $Dom.Node | ForEach-Object -Process { + $node = $_; + + Write-Verbose -Message "[Doc][ParseDom] -- Processing node"; + + if ($Null -ne $node) { + + # Visit the node + $Processor.Visit($node); + } + + $nodeCounter++; + } + + $innerResult; + } +} + +function HasProperty { + [CmdletBinding()] + [OutputType([System.Boolean])] + param ( + [PSObject]$InputObject, + + [Parameter(Mandatory = $True)] + [String]$Name + ) + + process { + return $Null -ne ($InputObject.PSObject.Properties | Where-Object -FilterScript { $_.Name -eq $Name }); + } +} + +function GetObjectField { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $True)] + [PSObject]$InputObject, + + [Parameter(Mandatory = $True)] + [String]$Field + ) + + process { + # Split field into dotted notation + $fieldParts = $Field.Split('.'); + + if ($Null -eq $InputObject) { + Write-Error -Message "Failed to bind to InputObject" + + return; + } + + Write-Verbose -Message "[GetObjectField] -- Getting field: $Field"; + + Write-Debug -Message "[GetObjectField] - Splitting into fields: $([String]::Join(',', $fieldParts))"; + + # Write-Verbose -Message "[Get-ObjectField] - Detecting type as $($InputObject.GetType())"; + + $resultProperty = $Null; + + $nextObj = $InputObject; + $partIndex = 0; + + $resultPropertyPath = New-Object -TypeName 'System.Collections.Generic.List[String]'; + + while ($Null -ne $nextObj -and $partIndex -lt $fieldParts.Length -and $Null -eq $resultProperty) { + + Write-Debug -Message "[GetObjectField] - Checking field part $($fieldParts[$partIndex])"; + + # Find a property of the object that matches the current field part + + $property = $Null; + + if ($nextObj -is [System.Collections.Hashtable]) { + # Handle hash table + + $property = $nextObj.GetEnumerator() | Where-Object ` + -FilterScript { + $_.Name -eq $fieldParts[$partIndex] + } + } elseif ($nextObj -is [PSObject]) { + # Handle regular object + + $property = $nextObj.PSObject.Properties.GetEnumerator() | Where-Object ` + -FilterScript { + $_.Name -eq $fieldParts[$partIndex] + } + } + + if ($Null -ne $property -and $partIndex -eq ($fieldParts.Length - 1)) { + # We have reached the last field part and found a property + + # Build the remaining field path + $resultPropertyPath.Add($property.Name); + + # Create a result property object + $resultProperty = New-Object -TypeName PSObject -Property @{ Name = $property.Name; Value = $property.Value; Path = [String]::Join('.', $resultPropertyPath); }; + } else { + $nextObj = $property.Value; + + $resultPropertyPath.Add($property.Name); + + $partIndex++; + } + } + + # Return the result property + return $resultProperty; + } +} + +function NewMarkdownProcessor { + + [CmdletBinding()] + param ( + + ) + + process { + # Create an instanced of a markdown processor from an external module + $result = Import-Module $PSScriptRoot\PSDocsProcessor\Markdown -AsCustomObject -PassThru -Verbose:$False; + + # Return the processor + $result; + } +} + +function ReadTemplate { + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $True)] + [String]$Path + ) + + process { + + # Read the contents of a .ps1 file + $template = Get-Content -Path $Path -Raw; + + # Invoke the contents of the .ps1 file as a script block + $templateScriptBlock = [ScriptBlock]::Create($template); + $templateScriptBlock.Invoke(); + } +} + +function ReadYamlHeader { + + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param ( + [Parameter(Mandatory = $True)] + [String]$Path + ) + + process { + + # Read the file + $content = Get-Content -Path $Path -Raw; + + # Detect Yaml header + if (![String]::IsNullOrEmpty($content) -and $content -match '^(---\r\n(?([A-Z0-9]{1,}:[A-Z0-9 ]{1,}(\r\n){0,}){1,})\r\n---\r\n)') { + + Write-Verbose -Message "[Doc][Toc]`t-- Reading Yaml header: $Path"; + + # Extract yaml header key value pair + [String[]]$yamlHeader = $Matches.yaml -split "`n"; + + $result = @{ }; + + # Read key values into hashtable + foreach ($item in $yamlHeader) { + $kv = $item.Split(':', 2, [System.StringSplitOptions]::RemoveEmptyEntries); + + Write-Debug -Message "Found yaml keypair from: $item"; + + if ($kv.Length -eq 2) { + $result[$kv[0].Trim()] = $kv[1].Trim(); + } + } + + # Emit result to the pipeline + return $result; + } + } +} + +# +# Export module +# + +Export-ModuleMember -Function 'Document','Invoke-PSDocument','Import-PSDocumentTemplate'; + +# EOM \ No newline at end of file diff --git a/src/PSDocs/PSDocsProcessor/Markdown/Markdown.psm1 b/src/PSDocs/PSDocsProcessor/Markdown/Markdown.psm1 new file mode 100644 index 0000000..9eb2f4c --- /dev/null +++ b/src/PSDocs/PSDocsProcessor/Markdown/Markdown.psm1 @@ -0,0 +1,170 @@ +# +# PSDocs Markdown processor +# + +function Visit { + + param ( + $InputObject + ) + + if ($Null -eq $InputObject) { + return; + } + + if ($InputObject -is [String]) { + return VisitString($InputObject); + } + + switch ($InputObject.Type) { + 'Code' { return VisitCode($InputObject); } + 'Section' { return VisitSection($InputObject); } + 'Title' { return VisitTitle($InputObject); } + 'List' { return VisitList($InputObject); } + 'Table' { return VisitTable($InputObject); } + 'Note' { return VisitNote($InputObject); } + 'Warning' { return VisitWarning($InputObject); } + 'Yaml' { return VisitYaml($InputObject); } + + default { return VisitString($InputObject); } + } +} + +function VisitString { + + param ( + $InputObject + ) + + Write-Verbose -Message "Visit string $InputObject"; + + if ($InputObject -isnot [String]) { + return $InputObject.ToString() -replace '\\', '\\'; + } + + return $InputObject -replace '\\', '\\'; +} + +function VisitSection { + + param ( + $InputObject + ) + + $section = $InputObject; + + Write-Verbose -Message "[Doc][Processor][Section] BEGIN::"; + + Write-Verbose -Message "[Doc][Processor][Section] -- Writing section: $($section.Content)"; + + # Generate markdown for the section name + VisitString("`n$(''.PadLeft($section.Level, '#')) $($section.Content)"); + + foreach ($n in $section.Node) { + + # Visit each node within the section + Visit($n); + } + + Write-Verbose -Message "[Doc][Processor][Section] END:: [$($section.Node.Length)]"; +} + +function VisitCode { + + param ( + $InputObject + ) + + Write-Verbose -Message "[Doc][Processor] -- Visit code"; + + VisitString(" $($InputObject.Content)"); +} + +function VisitTitle { + param ($InputObject) + + Write-Verbose -Message "[Doc][Processor] -- Visit title"; + + VisitString("# $($InputObject.Content)"); +} + +function VisitList { + param ($InputObject) + + Write-Verbose -Message "[Doc][Processor] -- Visit list"; + "" + + foreach ($n in $InputObject.Node) { + [String]::Concat("- ", $This.Visit($n)); + } +} + +function VisitNote { + param ($InputObject) + + Write-Verbose -Message "[Doc][Processor] -- Visit note"; + + VisitString(''); + VisitString('> [!NOTE]'); + + foreach ($n in $InputObject.Content) { + VisitString("> $n"); + } +} + +function VisitWarning { + param ($InputObject) + + Write-Verbose -Message "[Doc][Processor] -- Visit warning"; + + VisitString(''); + VisitString('> [!WARNING]'); + + foreach ($w in $InputObject.Content) { + VisitString("> $w"); + } +} + +function VisitYaml { + param ($InputObject) + + Write-Verbose -Message "[Doc][Processor] -- Visit yaml"; + + VisitString('---'); + + foreach ($kv in $InputObject.Content.GetEnumerator()) { + VisitString("$($kv.Key): $($kv.Value)"); + } + + VisitString('---'); +} + +function VisitTable { + + param ( + $InputObject + ) + + $table = $InputObject; + + Write-Verbose -Message "[Doc][Processor][Table] BEGIN::"; + + $headerCount = $table.Header.Length; + + if ($Null -ne $table.Header -and $table.Header.Length -gt 0) { + VisitString(''); + + # Create header + VisitString([String]::Concat('|', [String]::Join('|', $table.Header), '|')); + VisitString([String]::Concat(''.PadLeft($headerCount, 'X').Replace('X', '| --- '), '|')); + + # Write each row + foreach ($row in $table.Rows) { + Write-Debug -Message "Generating row"; + + VisitString([String]::Concat('|', [String]::Join('|', [String[]]$row), '|')); + } + } + + Write-Verbose -Message "[Doc][Processor][Table] END:: [$($table.Rows.Count)]"; +} \ No newline at end of file diff --git a/src/PSDocs/en-AU/PSDocs.Resources.psd1 b/src/PSDocs/en-AU/PSDocs.Resources.psd1 new file mode 100644 index 0000000..811338b --- /dev/null +++ b/src/PSDocs/en-AU/PSDocs.Resources.psd1 @@ -0,0 +1,6 @@ + +ConvertFrom-StringData @' +###PSLOC +DocumentNotFound=Failed to find document: {0} +###PSLOC +'@ \ No newline at end of file diff --git a/src/PSDocs/en-US/PSDocs.Resources.psd1 b/src/PSDocs/en-US/PSDocs.Resources.psd1 new file mode 100644 index 0000000..811338b --- /dev/null +++ b/src/PSDocs/en-US/PSDocs.Resources.psd1 @@ -0,0 +1,6 @@ + +ConvertFrom-StringData @' +###PSLOC +DocumentNotFound=Failed to find document: {0} +###PSLOC +'@ \ No newline at end of file diff --git a/tests/PSDocs.Dsc.Tests/PSDocs.Dsc.Common.Tests.ps1 b/tests/PSDocs.Dsc.Tests/PSDocs.Dsc.Common.Tests.ps1 new file mode 100644 index 0000000..301a8c3 --- /dev/null +++ b/tests/PSDocs.Dsc.Tests/PSDocs.Dsc.Common.Tests.ps1 @@ -0,0 +1,148 @@ +# +# Unit tests for core PSDocs functionality +# + +[CmdletBinding()] +param ( + +) + +# Setup error handling +$ErrorActionPreference = 'Stop'; +Set-StrictMode -Version latest; + +# Setup tests paths +$rootPath = (Resolve-Path $PSScriptRoot\..\..).Path; +$here = Split-Path -Parent $MyInvocation.MyCommand.Path; +$src = ($here -replace '\\tests\\', '\\src\\') -replace '\.Tests', ''; +$temp = "$here\..\..\build"; +# $sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.'; + +Import-Module $src -Force; + +$outputPath = "$temp\PSDocs.Dsc.Tests\Common"; +Remove-Item -Path $outputPath -Force -Recurse -Confirm:$False -ErrorAction SilentlyContinue; +New-Item -Path $outputPath -ItemType Directory -Force | Out-Null; + +$Global:TestVars = @{ }; + +configuration TestConfiguration { + + param ( + [Parameter(Mandatory = $True)] + [String[]]$ComputerName + ) + + Import-DscResource -ModuleName PSDesiredStateConfiguration; + + node $ComputerName { + + File FileResource { + Ensure = 'Present' + Type = 'File' + DestinationPath = 'C:\environment.tag' + Contents = "Node=$($Node.NodeName)" + } + + WindowsFeature SMB1 { + Ensure = 'Absent' + Name = 'FS-SMB1' + } + } +} + +Describe 'PSDocs.Dsc' { + Context 'Generate a document without an instance name' { + + # Define a test document with a table + document 'WithoutInstanceName' { + + $InputObject.ResourceType.File | Table -Property Contents,DestinationPath; + } + + TestConfiguration -OutputPath $outputPath -ComputerName 'WithoutInstanceName'; + + $outputDoc = "$outputPath\WithoutInstanceName.md"; + Invoke-DscNodeDocument -DocumentName 'WithoutInstanceName' -Path $outputPath -OutputPath $outputPath; + + It 'Should generate an output named WithoutInstanceName.md' { + Test-Path -Path $outputDoc | Should be $True; + } + + It 'Should contain document name' { + Get-Content -Path $outputDoc -Raw | Should match '\|Node\=WithoutInstanceName\|'; + } + } + + Context 'Generate a document with an instance name' { + + # Define a test document with a table + document 'WithInstanceName' { + + $InputObject.ResourceType.File | Table -Property Contents,DestinationPath; + } + + TestConfiguration -OutputPath $outputPath -ComputerName 'Instance1'; + + Invoke-DscNodeDocument -DocumentName 'WithInstanceName' -InstanceName 'Instance1' -Path $outputPath -OutputPath $outputPath; + + It 'Should not create a output with the document name' { + Test-Path -Path "$outputPath\WithInstanceName.md" | Should be $False; + } + + It 'Should generate an output named Instance1.md' { + Test-Path -Path "$outputPath\Instance1.md" | Should be $True; + } + + It 'Should contain instance name' { + Get-Content -Path "$outputPath\Instance1.md" -Raw | Should match '|Content=Instance1|'; + } + } + + Context 'Generate a document with multiple instance names' { + + # Define a test document with a table + document 'WithMultiInstanceName' { + $InputObject.ResourceType.File | Table -Property Contents,DestinationPath; + } + + TestConfiguration -OutputPath $outputPath -ComputerName 'Instance2','Instance3'; + + Invoke-DscNodeDocument -DocumentName 'WithMultiInstanceName' -InstanceName 'Instance2','Instance3' -Path $outputPath -OutputPath $outputPath; + + It 'Should not create a output with the document name' { + Test-Path -Path "$outputPath\WithMultiInstanceName.md" | Should be $False; + } + + It 'Should generate an output named Instance2.md' { + Test-Path -Path "$outputPath\Instance2.md" | Should be $True; + } + + It 'Should contain instance name Instance2' { + Get-Content -Path "$outputPath\Instance2.md" -Raw | Should match '\|Node\=Instance2\|'; + } + + It 'Should generate an output named Instance3.md' { + Test-Path -Path "$outputPath\Instance3.md" | Should be $True; + } + + It 'Should contain instance name Instance3' { + Get-Content -Path "$outputPath\Instance3.md" -Raw | Should match '\|Node\=Instance3\|'; + } + } + + Context 'Generate a document with an external script' { + + TestConfiguration -OutputPath $outputPath -ComputerName 'WithExternalScript'; + + Invoke-DscNodeDocument -Script "$here\Templates\WithExternalScript.ps1" -DocumentName 'WithExternalScript' -InstanceName 'WithExternalScript' -Path $outputPath -OutputPath $outputPath; + + It 'Should generate an output named WithExternalScript.md' { + Test-Path -Path "$outputPath\WithExternalScript.md" | Should be $True; + } + + It 'Should contain instance name' { + Get-Content -Path "$outputPath\WithExternalScript.md" -Raw | Should match '\|FS\-SMB1\|'; + } + } +} diff --git a/tests/PSDocs.Dsc.Tests/Templates/WithExternalScript.ps1 b/tests/PSDocs.Dsc.Tests/Templates/WithExternalScript.ps1 new file mode 100644 index 0000000..aa14d88 --- /dev/null +++ b/tests/PSDocs.Dsc.Tests/Templates/WithExternalScript.ps1 @@ -0,0 +1,5 @@ + +# Define a test document with a table +document 'WithExternalScript' { + $InputObject.ResourceType.WindowsFeature | Where-Object { $_.Ensure -eq 'Absent' } | Table -Property Name; +} diff --git a/tests/PSDocs.Tests/PSDocs.Code.Tests.ps1 b/tests/PSDocs.Tests/PSDocs.Code.Tests.ps1 new file mode 100644 index 0000000..4137d85 --- /dev/null +++ b/tests/PSDocs.Tests/PSDocs.Code.Tests.ps1 @@ -0,0 +1,86 @@ +# +# Unit tests for the Code keyword +# + +[CmdletBinding()] +param ( + +) + +# Setup error handling +$ErrorActionPreference = 'Stop'; +Set-StrictMode -Version latest; + +# Setup tests paths +$rootPath = (Resolve-Path $PSScriptRoot\..\..).Path; +$here = Split-Path -Parent $MyInvocation.MyCommand.Path; +$src = ($here -replace '\\tests\\', '\\src\\') -replace '\.Tests', ''; +$temp = "$here\..\..\build"; +# $sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.'; + +Import-Module $src -Force; +Import-Module $src\PSDocsProcessor\Markdown -Force; + +$outputPath = "$temp\PSDocs.Tests\Code"; +New-Item $outputPath -ItemType Directory -Force | Out-Null; + +$dummyObject = New-Object -TypeName PSObject; + +$Global:TestVars = @{ }; + +Describe 'PSDocs -- Code keyword' { + Context 'Code' { + + # Define a test document with a table + document 'CodeTests' { + + Code { + 'This is code' + } + } + + Mock -CommandName 'VisitCode' -ModuleName 'Markdown' -Verifiable -MockWith { + param ( + $InputObject + ) + + $Global:TestVars['VisitCode'] = $InputObject; + } + + Invoke-PSDocument -Name 'CodeTests' -InstanceName 'Code' -InputObject $dummyObject -OutputPath $outputPath; + + It 'Should process Code keyword' { + Assert-MockCalled -CommandName 'VisitCode' -ModuleName 'Markdown' -Times 1; + } + + It 'Should be Code object' { + $Global:TestVars['VisitCode'].Type | Should be 'Code'; + } + + It 'Should have expected content' { + $Global:TestVars['VisitCode'].Content | Should be 'This is code'; + } + } + + Context 'Code markdown' { + + # Define a test document with a table + document 'CodeTests' { + + Code { + 'This is code' + } + } + + $outputDoc = "$outputPath\Code.md"; + Invoke-PSDocument -Name 'CodeTests' -InstanceName 'Code' -InputObject $dummyObject -OutputPath $outputPath; + + It 'Should have generated output' { + Test-Path -Path $outputDoc | Should be $True; + } + + It 'Should match expected format' { + Get-Content -Path $outputDoc -Raw | Should match ' This is code'; + } + } +} diff --git a/tests/PSDocs.Tests/PSDocs.Common.Tests.ps1 b/tests/PSDocs.Tests/PSDocs.Common.Tests.ps1 new file mode 100644 index 0000000..cea37ed --- /dev/null +++ b/tests/PSDocs.Tests/PSDocs.Common.Tests.ps1 @@ -0,0 +1,102 @@ +# +# Unit tests for core PSDocs functionality +# + +[CmdletBinding()] +param ( + +) + +# Setup error handling +$ErrorActionPreference = 'Stop'; +Set-StrictMode -Version latest; + +# Setup tests paths +$rootPath = (Resolve-Path $PSScriptRoot\..\..).Path; +$here = Split-Path -Parent $MyInvocation.MyCommand.Path; +$src = ($here -replace '\\tests\\', '\\src\\') -replace '\.Tests', ''; +$temp = "$here\..\..\build"; +# $sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.'; + +Import-Module $src -Force; + +$outputPath = "$temp\PSDocs.Tests\Common"; +Remove-Item -Path $outputPath -Force -Recurse -Confirm:$False -ErrorAction SilentlyContinue; +New-Item -Path $outputPath -ItemType Directory -Force | Out-Null; + +$dummyObject = New-Object -TypeName PSObject; + +$Global:TestVars = @{ }; + +Describe 'PSDocs' { + Context 'Generate a document without an instance name' { + + # Define a test document with a table + document 'WithoutInstanceName' { + $InstanceName; + } + + $outputDoc = "$outputPath\WithoutInstanceName.md"; + Invoke-PSDocument -Name 'WithoutInstanceName' -InputObject $dummyObject -OutputPath $outputPath; + + It 'Should generate an output named WithoutInstanceName.md' { + Test-Path -Path $outputDoc | Should be $True; + } + + It 'Should contain document name' { + Get-Content -Path $outputDoc -Raw | Should match 'WithoutInstanceName'; + } + } + + Context 'Generate a document with an instance name' { + + # Define a test document with a table + document 'WithInstanceName' { + $InstanceName; + } + + Invoke-PSDocument -Name 'WithInstanceName' -InstanceName 'Instance1' -InputObject $dummyObject -OutputPath $outputPath; + + It 'Should not create a output with the document name' { + Test-Path -Path "$outputPath\WithInstanceName.md" | Should be $False; + } + + It 'Should generate an output named Instance1.md' { + Test-Path -Path "$outputPath\Instance1.md" | Should be $True; + } + + It 'Should contain instance name' { + Get-Content -Path "$outputPath\Instance1.md" -Raw | Should match 'Instance1'; + } + } + + Context 'Generate a document with multiple instance names' { + + # Define a test document with a table + document 'WithMultiInstanceName' { + $InstanceName; + } + + Invoke-PSDocument -Name 'WithMultiInstanceName' -InstanceName 'Instance2','Instance3' -InputObject $dummyObject -OutputPath $outputPath; + + It 'Should not create a output with the document name' { + Test-Path -Path "$outputPath\WithMultiInstanceName.md" | Should be $False; + } + + It 'Should generate an output named Instance2.md' { + Test-Path -Path "$outputPath\Instance2.md" | Should be $True; + } + + It 'Should contain instance name Instance2' { + Get-Content -Path "$outputPath\Instance2.md" -Raw | Should match 'Instance2'; + } + + It 'Should generate an output named Instance3.md' { + Test-Path -Path "$outputPath\Instance3.md" | Should be $True; + } + + It 'Should contain instance name Instance3' { + Get-Content -Path "$outputPath\Instance3.md" -Raw | Should match 'Instance3'; + } + } +} diff --git a/tests/PSDocs.Tests/PSDocs.Note.Tests.ps1 b/tests/PSDocs.Tests/PSDocs.Note.Tests.ps1 new file mode 100644 index 0000000..9a7ebe2 --- /dev/null +++ b/tests/PSDocs.Tests/PSDocs.Note.Tests.ps1 @@ -0,0 +1,109 @@ +# +# Unit tests for the Note keyword +# + +[CmdletBinding()] +param ( + +) + +# Setup error handling +$ErrorActionPreference = 'Stop'; +Set-StrictMode -Version latest; + +# Setup tests paths +$rootPath = (Resolve-Path $PSScriptRoot\..\..).Path; +$here = Split-Path -Parent $MyInvocation.MyCommand.Path; +$src = ($here -replace '\\tests\\', '\\src\\') -replace '\.Tests', ''; +$temp = "$here\..\..\build"; +# $sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.'; + +Import-Module $src -Force; +Import-Module $src\PSDocsProcessor\Markdown -Force; + +$outputPath = "$temp\PSDocs.Tests\Note"; +New-Item $outputPath -ItemType Directory -Force | Out-Null; + +$dummyObject = New-Object -TypeName PSObject; + +$Global:TestVars = @{ }; + +Describe 'PSDocs -- Note keyword' { + Context 'Note' { + + # Define a test document with a note + document 'NoteVisitor' { + + Note { + 'This is a note' + } + } + + Mock -CommandName 'VisitNote' -ModuleName 'Markdown' -Verifiable -MockWith { + param ( + $InputObject + ) + + $Global:TestVars['VisitNote'] = $InputObject; + } + + Invoke-PSDocument -Name 'NoteVisitor' -InputObject $dummyObject -OutputPath $outputPath; + + It 'Should process Note keyword' { + Assert-MockCalled -CommandName 'VisitNote' -ModuleName 'Markdown' -Times 1; + } + + It 'Should be Note object' { + $Global:TestVars['VisitNote'].Type | Should be 'Note'; + } + + It 'Should have expected content' { + $Global:TestVars['VisitNote'].Content | Should be 'This is a note'; + } + } + + Context 'Note single line markdown' { + + # Define a test document with a note + document 'NoteSingleMarkdown' { + + Note { + 'This is a single line note' + } + } + + $outputDoc = "$outputPath\NoteSingleMarkdown.md"; + Invoke-PSDocument -Name 'NoteSingleMarkdown' -InputObject $dummyObject -OutputPath $outputPath; + + It 'Should have generated output' { + Test-Path -Path $outputDoc | Should be $True; + } + + It 'Should match expected format' { + Get-Content -Path $outputDoc -Raw | Should match '\> \[\!NOTE\]\r\n\> This is a single line note'; + } + } + + Context 'Note multi-line markdown' { + + # Define a test document with a note + document 'NoteMultiMarkdown' { + + Note { + 'This is the first line of the note.' + 'This is the second line of the note.' + } + } + + $outputDoc = "$outputPath\NoteMultiMarkdown.md"; + Invoke-PSDocument -Name 'NoteMultiMarkdown' -InputObject $dummyObject -OutputPath $outputPath; + + It 'Should have generated output' { + Test-Path -Path $outputDoc | Should be $True; + } + + It 'Should match expected format' { + Get-Content -Path $outputDoc -Raw | Should match '\> \[\!NOTE\]\r\n\> This is the first line of the note.\r\n\> This is the second line of the note.'; + } + } +} diff --git a/tests/PSDocs.Tests/PSDocs.Section.Tests.ps1 b/tests/PSDocs.Tests/PSDocs.Section.Tests.ps1 new file mode 100644 index 0000000..f1b010b --- /dev/null +++ b/tests/PSDocs.Tests/PSDocs.Section.Tests.ps1 @@ -0,0 +1,88 @@ +# +# Unit tests for the Section keyword +# + +[CmdletBinding()] +param ( + +) + +# Setup error handling +$ErrorActionPreference = 'Stop'; +Set-StrictMode -Version latest; + +# Setup tests paths +$rootPath = (Resolve-Path $PSScriptRoot\..\..).Path; +$here = Split-Path -Parent $MyInvocation.MyCommand.Path; +$src = ($here -replace '\\tests\\', '\\src\\') -replace '\.Tests', ''; +$temp = "$here\..\..\build"; +# $sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.'; + +Import-Module $src -Force; +Import-Module $src\PSDocsProcessor\Markdown -Force; + +$outputPath = "$temp\PSDocs.Tests\Section"; +New-Item $outputPath -ItemType Directory -Force | Out-Null; + +$dummyObject = New-Object -TypeName PSObject; + +$Global:TestVars = @{ }; + +Describe 'PSDocs -- Section keyword' { + Context 'Simple Section block' { + + # Define a test document with a section block + document 'SectionBlockTests' { + Section 'Test' { + 'Content' + } + } + + Mock -CommandName 'VisitSection' -ModuleName 'Markdown' -Verifiable -MockWith { + param ( + $InputObject + ) + + $Global:TestVars['VisitSection'] = $InputObject; + } + + $result = Invoke-PSDocument -Name 'SectionBlockTests' -InstanceName 'Section' -InputObject $dummyObject -OutputPath $outputPath -PassThru; + + It 'Should process Section keyword' { + Assert-MockCalled -CommandName 'VisitSection' -ModuleName 'Markdown' -Times 1; + } + + It 'Should be Section object' { + $Global:TestVars['VisitSection'].Type | Should be 'Section'; + } + + It 'Should have expected section name' { + $Global:TestVars['VisitSection'].Content | Should be 'Test'; + } + + It 'Should have expected section level' { + $Global:TestVars['VisitSection'].Level | Should be 2; + } + } + + Context 'Section markdown' { + + # Define a test document with a section block + document 'SectionBlockTests' { + Section 'Test' { + 'Content' + } + } + + $outputDoc = "$outputPath\Section.md"; + Invoke-PSDocument -Name 'SectionBlockTests' -InstanceName 'Section' -InputObject $dummyObject -OutputPath $outputPath; + + It 'Should have generated output' { + Test-Path -Path $outputDoc | Should be $True; + } + + It 'Should match expected format' { + Get-Content -Path $outputDoc -Raw | Should match '## Test(\n|\r){1,2}Content'; + } + } +} diff --git a/tests/PSDocs.Tests/PSDocs.Table.Tests.ps1 b/tests/PSDocs.Tests/PSDocs.Table.Tests.ps1 new file mode 100644 index 0000000..1384c81 --- /dev/null +++ b/tests/PSDocs.Tests/PSDocs.Table.Tests.ps1 @@ -0,0 +1,98 @@ +# +# Unit tests for the Table keyword +# + +[CmdletBinding()] +param ( + +) + +# Setup error handling +$ErrorActionPreference = 'Stop'; +Set-StrictMode -Version latest; + +# Setup tests paths +$rootPath = (Resolve-Path $PSScriptRoot\..\..).Path; +$here = Split-Path -Parent $MyInvocation.MyCommand.Path; +$src = ($here -replace '\\tests\\', '\\src\\') -replace '\.Tests', ''; +$temp = "$here\..\..\build"; +# $sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.'; + +Import-Module $src -Force; +Import-Module $src\PSDocsProcessor\Markdown -Force; + +$outputPath = "$temp\PSDocs.Tests\Table"; +New-Item $outputPath -ItemType Directory -Force | Out-Null; + +$dummyObject = New-Object -TypeName PSObject; + +$Global:TestVars = @{ }; + +Describe 'PSDocs -- Table keyword' { + Context 'Table with a single named property' { + + # Define a test document with a table + document 'WithSingleNamedProperty' { + + Get-ChildItem -Path '.\' | Table -Property 'Name' + } + + Mock -CommandName 'VisitTable' -ModuleName 'Markdown' -Verifiable -MockWith { + param ( + $InputObject + ) + + $Global:TestVars['VisitTable'] = $InputObject; + } + + Invoke-PSDocument -Name 'WithSingleNamedProperty' -InputObject $dummyObject -OutputPath $outputPath; + + It 'Should process Table keyword' { + Assert-MockCalled -CommandName 'VisitTable' -ModuleName 'Markdown' -Times 1; + } + + It 'Should be Table object' { + $Global:TestVars['VisitTable'].Type | Should be 'Table'; + } + } + + Context 'Table markdown' { + + # Define a test document with a table + document 'TableTests' { + + Get-ChildItem -Path $rootPath | Where-Object -FilterScript { 'README.md','LICENSE' -contains $_.Name } | Format-Table -Property 'Name','PSIsContainer' + } + + $outputDoc = "$outputPath\Table.md"; + Invoke-PSDocument -Name 'TableTests' -InstanceName 'Table' -InputObject $dummyObject -OutputPath $outputPath; + + It 'Should have generated output' { + Test-Path -Path $outputDoc | Should be $True; + } + + It 'Should match expected format' { + Get-Content -Path $outputDoc -Raw | Should match '\|LICENSE\|False\|(\n|\r){1,2}\|README.md\|False\|'; + } + } + + Context 'Table single entry markdown' { + + # Define a test document with a warning + document 'TableSingleEntryMarkdown' { + + New-Object -TypeName PSObject -Property @{ Name = 'Single' } | Table -Property Name; + } + + $outputDoc = "$outputPath\TableSingleEntryMarkdown.md"; + Invoke-PSDocument -Name 'TableSingleEntryMarkdown' -InputObject $dummyObject -OutputPath $outputPath; + + It 'Should have generated output' { + Test-Path -Path $outputDoc | Should be $True; + } + + It 'Should match expected format' { + Get-Content -Path $outputDoc -Raw | Should match '\|Name\|\r\n\| --- \|\r\n\|Single\|'; + } + } +} diff --git a/tests/PSDocs.Tests/PSDocs.Warning.Tests.ps1 b/tests/PSDocs.Tests/PSDocs.Warning.Tests.ps1 new file mode 100644 index 0000000..aa42d59 --- /dev/null +++ b/tests/PSDocs.Tests/PSDocs.Warning.Tests.ps1 @@ -0,0 +1,109 @@ +# +# Unit tests for the Warning keyword +# + +[CmdletBinding()] +param ( + +) + +# Setup error handling +$ErrorActionPreference = 'Stop'; +Set-StrictMode -Version latest; + +# Setup tests paths +$rootPath = (Resolve-Path $PSScriptRoot\..\..).Path; +$here = Split-Path -Parent $MyInvocation.MyCommand.Path; +$src = ($here -replace '\\tests\\', '\\src\\') -replace '\.Tests', ''; +$temp = "$here\..\..\build"; +# $sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.'; + +Import-Module $src -Force; +Import-Module $src\PSDocsProcessor\Markdown -Force; + +$outputPath = "$temp\PSDocs.Tests\Warning"; +New-Item $outputPath -ItemType Directory -Force | Out-Null; + +$dummyObject = New-Object -TypeName PSObject; + +$Global:TestVars = @{ }; + +Describe 'PSDocs -- Warning keyword' { + Context 'Warning' { + + # Define a test document with a note + document 'WarningVisitor' { + + Warning { + 'This is a warning' + } + } + + Mock -CommandName 'VisitWarning' -ModuleName 'Markdown' -Verifiable -MockWith { + param ( + $InputObject + ) + + $Global:TestVars['VisitWarning'] = $InputObject; + } + + Invoke-PSDocument -Name 'WarningVisitor' -InputObject $dummyObject -OutputPath $outputPath; + + It 'Should process Note keyword' { + Assert-MockCalled -CommandName 'VisitWarning' -ModuleName 'Markdown' -Times 1; + } + + It 'Should be Note object' { + $Global:TestVars['VisitWarning'].Type | Should be 'Warning'; + } + + It 'Should have expected content' { + $Global:TestVars['VisitWarning'].Content | Should be 'This is a warning'; + } + } + + Context 'Warning single line markdown' { + + # Define a test document with a warning + document 'WarningSingleMarkdown' { + + Warning { + 'This is a single line warning' + } + } + + $outputDoc = "$outputPath\WarningSingleMarkdown.md"; + Invoke-PSDocument -Name 'WarningSingleMarkdown' -InputObject $dummyObject -OutputPath $outputPath; + + It 'Should have generated output' { + Test-Path -Path $outputDoc | Should be $True; + } + + It 'Should match expected format' { + Get-Content -Path $outputDoc -Raw | Should match '\> \[\!WARNING\]\r\n\> This is a single line warning'; + } + } + + Context 'Warning multi-line markdown' { + + # Define a test document with a warning + document 'WarningMultiMarkdown' { + + Warning { + 'This is the first line of the warning.' + 'This is the second line of the warning.' + } + } + + $outputDoc = "$outputPath\WarningMultiMarkdown.md"; + Invoke-PSDocument -Name 'WarningMultiMarkdown' -InputObject $dummyObject -OutputPath $outputPath; + + It 'Should have generated output' { + Test-Path -Path $outputDoc | Should be $True; + } + + It 'Should match expected format' { + Get-Content -Path $outputDoc -Raw | Should match '\> \[\!WARNING\]\r\n\> This is the first line of the warning.\r\n\> This is the second line of the warning.'; + } + } +}