diff --git a/.gitignore b/.gitignore index 2329206..acc0d4c 100644 --- a/.gitignore +++ b/.gitignore @@ -214,3 +214,6 @@ pip-log.txt #Mr Developer .mr.developer.cfg + +# The default build.ps1 output folders +[0-9]*\.[0-9]*\.[0-9]*/ diff --git a/Build.ps1 b/Build.ps1 new file mode 100644 index 0000000..1e767e9 --- /dev/null +++ b/Build.ps1 @@ -0,0 +1,29 @@ +[CmdletBinding()] +param( + # A version number to update the output with (uses gitversion by default) + $Version = $(gitversion -showvariable nugetversion), + + # The output folder (defaults to the version number) + $OutputDirectory = $("$PSScriptRoot\$(($Version -split '[-+]',2)[0])"), + + # If set, removes the output folder without prompting! + [switch]$Force +) + +$VersionPrefix, $VersionSuffix = $Version -split '[-+]', 2 + +if (Test-Path $OutputDirectory) { + Remove-Item $OutputDirectory -Recurse -Confirm:(!$Force) +} + +Copy-Item $PSScriptRoot\Module\PowerShellLogging -Destination $OutputDirectory -recurse +Set-Content $OutputDirectory\PowerShellLogging.psd1 ( + (Get-Content $OutputDirectory\PowerShellLogging.psd1) -replace + "(ModuleVersion\s+=\s+)'.*'", "`$1'$VersionPrefix'" -replace + "(Prerelease\s+=\s+)'.*'", "`$1'$VersionSuffix'" +) + +dotnet build -c Release -p:VersionPrefix=$VersionPrefix -p:VersionSuffix=$VersionSuffix + +Get-ChildItem bin/Release -recurse -filter *.dll | + Move-Item -Destination $OutputDirectory\ \ No newline at end of file diff --git a/HostIOInterceptor.cs b/HostIOInterceptor.cs index c20dd72..6602ba8 100644 --- a/HostIOInterceptor.cs +++ b/HostIOInterceptor.cs @@ -39,7 +39,7 @@ private HostIOInterceptor() #endregion #region Properties - + public bool Paused { get { return paused; } @@ -93,13 +93,25 @@ public void AttachToHost(PSHost host) const BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic; - object uiRef = host.GetType().GetField("internalUIRef", flags).GetValue(host); - object ui = uiRef.GetType().GetProperty("Value", flags).GetValue(uiRef, null); - - FieldInfo externalUIField = ui.GetType().GetField("externalUI", flags); + // When PowerShell went open source, they renamed the private variables to have _underbarPrefixes + if (host.Version >= new Version(6, 0)) + { + object uiRef = host.GetType().GetField("_internalUIRef", flags)?.GetValue(host); + object ui = uiRef.GetType().GetProperty("Value", flags).GetValue(uiRef, null); + FieldInfo externalUIField = ui.GetType().GetField("_externalUI", flags); + externalUI = (PSHostUserInterface)externalUIField.GetValue(ui); + externalUIField.SetValue(ui, this); + } + else + { + // Try the WindowsPowerShell version: + object uiRef = host.GetType().GetField("internalUIRef", flags).GetValue(host); + object ui = uiRef.GetType().GetProperty("Value", flags).GetValue(uiRef, null); + FieldInfo externalUIField = ui.GetType().GetField("externalUI", flags); + externalUI = (PSHostUserInterface)externalUIField.GetValue(ui); + externalUIField.SetValue(ui, this); + } - externalUI = (PSHostUserInterface)externalUIField.GetValue(ui); - externalUIField.SetValue(ui, this); this.host = host; } @@ -109,14 +121,27 @@ public void DetachFromHost() const BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic; - object uiRef = host.GetType().GetField("internalUIRef", flags).GetValue(host); - object ui = uiRef.GetType().GetProperty("Value", flags).GetValue(uiRef, null); - - FieldInfo externalUIField = ui.GetType().GetField("externalUI", flags); - - if (externalUIField.GetValue(ui) == this) + // When PowerShell went open source, they renamed the private variables to have _underbarPrefixes + if (host.Version >= new Version(6, 0)) { - externalUIField.SetValue(ui, externalUI); + object uiRef = host.GetType().GetField("_internalUIRef", flags)?.GetValue(host); + object ui = uiRef.GetType().GetProperty("Value", flags).GetValue(uiRef, null); + FieldInfo externalUIField = ui.GetType().GetField("_externalUI", flags); + if (externalUIField.GetValue(ui) == this) + { + externalUIField.SetValue(ui, externalUI); + } + } + else + { + // Try the WindowsPowerShell version: + object uiRef = host.GetType().GetField("internalUIRef", flags).GetValue(host); + object ui = uiRef.GetType().GetProperty("Value", flags).GetValue(uiRef, null); + FieldInfo externalUIField = ui.GetType().GetField("externalUI", flags); + if (externalUIField.GetValue(ui) == this) + { + externalUIField.SetValue(ui, externalUI); + } } externalUI = null; @@ -129,7 +154,7 @@ public override Dictionary Prompt(string caption, { if (externalUI == null) { - throw new InvalidOperationException(); + throw new InvalidOperationException("Unable to prompt user in headless session"); } Dictionary result = externalUI.Prompt(caption, message, descriptions); @@ -146,7 +171,7 @@ public override int PromptForChoice(string caption, { if (externalUI == null) { - throw new InvalidOperationException(); + throw new InvalidOperationException("Unable to prompt user for choice in headless session"); } int result = externalUI.PromptForChoice(caption, message, choices, defaultChoice); @@ -163,7 +188,7 @@ public override PSCredential PromptForCredential(string caption, { if (externalUI == null) { - throw new InvalidOperationException(); + throw new InvalidOperationException("Unable to prompt user for credential in headless session"); } PSCredential result = externalUI.PromptForCredential(caption, message, userName, targetName); @@ -182,7 +207,7 @@ public override PSCredential PromptForCredential(string caption, { if (externalUI == null) { - throw new InvalidOperationException(); + throw new InvalidOperationException("Unable to prompt user for credential in headless session"); } PSCredential result = externalUI.PromptForCredential(caption, @@ -201,7 +226,7 @@ public override string ReadLine() { if (externalUI == null) { - throw new InvalidOperationException(); + throw new InvalidOperationException("Unable to ReadLine from host in headless session"); } string result = externalUI.ReadLine(); @@ -215,7 +240,7 @@ public override SecureString ReadLineAsSecureString() { if (externalUI == null) { - throw new InvalidOperationException(); + throw new InvalidOperationException("Unable to ReadLineAsSecureString from host in headless session"); } return externalUI.ReadLineAsSecureString(); @@ -246,13 +271,11 @@ public void RemoveSubscriber(IHostIOSubscriber subscriber) public override void Write(string value) { - if (externalUI == null) + if (externalUI != null) { - throw new InvalidOperationException(); + externalUI.Write(value); } - externalUI.Write(value); - if (!paused) { writeCache.Append(value); @@ -261,13 +284,11 @@ public override void Write(string value) public override void Write(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value) { - if (externalUI == null) + if (externalUI != null) { - throw new InvalidOperationException(); + externalUI.Write(foregroundColor, backgroundColor, value); } - externalUI.Write(foregroundColor, backgroundColor, value); - if (!paused) { writeCache.Append(value); @@ -276,11 +297,6 @@ public override void Write(ConsoleColor foregroundColor, ConsoleColor background public override void WriteDebugLine(string message) { - if (externalUI == null) - { - throw new InvalidOperationException(); - } - string[] lines = message.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None); foreach (string line in lines) { @@ -288,16 +304,14 @@ public override void WriteDebugLine(string message) SendToSubscribers(s => s.WriteDebug(temp.TrimEnd() + "\r\n")); } - externalUI.WriteDebugLine(message); + if (externalUI != null) + { + externalUI.WriteDebugLine(message); + } } public override void WriteErrorLine(string message) { - if (externalUI == null) - { - throw new InvalidOperationException(); - } - string[] lines = message.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None); foreach (string line in lines) { @@ -305,16 +319,14 @@ public override void WriteErrorLine(string message) SendToSubscribers(s => s.WriteError(temp.TrimEnd() + "\r\n")); } - externalUI.WriteErrorLine(message); + if (externalUI != null) + { + externalUI.WriteErrorLine(message); + } } public override void WriteLine() { - if (externalUI == null) - { - throw new InvalidOperationException(); - } - string[] lines = writeCache.ToString().Split(new[] { "\r\n", "\n" }, StringSplitOptions.None); foreach (string line in lines) { @@ -323,16 +335,13 @@ public override void WriteLine() } writeCache.Length = 0; - externalUI.WriteLine(); + if (externalUI != null) { + externalUI.WriteLine(); + } } public override void WriteLine(string value) { - if (externalUI == null) - { - throw new InvalidOperationException(); - } - string[] lines = (writeCache + value).Split(new[] { "\r\n", "\n" }, StringSplitOptions.None); foreach (string line in lines) { @@ -341,16 +350,13 @@ public override void WriteLine(string value) } writeCache.Length = 0; - externalUI.WriteLine(value); + if (externalUI != null) { + externalUI.WriteLine(value); + } } public override void WriteLine(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value) { - if (externalUI == null) - { - throw new InvalidOperationException(); - } - string[] lines = (writeCache + value).Split(new[] { "\r\n", "\n" }, StringSplitOptions.None); foreach (string line in lines) { @@ -359,28 +365,23 @@ public override void WriteLine(ConsoleColor foregroundColor, ConsoleColor backgr } writeCache.Length = 0; - externalUI.WriteLine(foregroundColor, backgroundColor, value); + if (externalUI != null){ + externalUI.WriteLine(foregroundColor, backgroundColor, value); + } } public override void WriteProgress(long sourceId, ProgressRecord record) { - if (externalUI == null) - { - throw new InvalidOperationException(); - } - SendToSubscribers(s => s.WriteProgress(sourceId, record)); - externalUI.WriteProgress(sourceId, record); + if (externalUI != null) + { + externalUI.WriteProgress(sourceId, record); + } } public override void WriteVerboseLine(string message) { - if (externalUI == null) - { - throw new InvalidOperationException(); - } - string[] lines = message.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None); foreach (string line in lines) { @@ -388,16 +389,14 @@ public override void WriteVerboseLine(string message) SendToSubscribers(s => s.WriteVerbose(temp.TrimEnd() + "\r\n")); } - externalUI.WriteVerboseLine(message); + if (externalUI != null) + { + externalUI.WriteVerboseLine(message); + } } public override void WriteWarningLine(string message) { - if (externalUI == null) - { - throw new InvalidOperationException(); - } - string[] lines = message.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None); foreach (string line in lines) { @@ -405,7 +404,10 @@ public override void WriteWarningLine(string message) SendToSubscribers(s => s.WriteWarning(temp.TrimEnd() + "\r\n")); } - externalUI.WriteWarningLine(message); + if (externalUI != null) + { + externalUI.WriteWarningLine(message); + } } #endregion diff --git a/Module/PowerShellLogging/PowerShellLogging.psd1 b/Module/PowerShellLogging/PowerShellLogging.psd1 index 0e31e04..0bc5444 100644 --- a/Module/PowerShellLogging/PowerShellLogging.psd1 +++ b/Module/PowerShellLogging/PowerShellLogging.psd1 @@ -1,6 +1,7 @@ @{ ModuleToProcess = 'PowerShellLogging.psm1' - ModuleVersion = '1.3.0' + NestedModules = @( "PowerShellLoggingModule.dll" ) + ModuleVersion = '1.4.0' GUID = '345320b5-bdc3-4ab6-a13e-fcb019362fe6' CompanyName = 'Home' CopyRight = 'Copyright 2017 David Wyatt' @@ -14,12 +15,14 @@ AliasesToExport = '*' FileList = @('PowerShellLogging.psm1','PowerShellLogging.psd1','PowerShellLoggingModule.dll','en-US\about_PowerShellLogging.help.txt','en-US\PowerShellLoggingModule.dll-help.xml') - + PrivateData = @{ PSData = @{ + Prerelease = '' LicenseUri = 'http://www.apache.org/licenses/LICENSE-2.0.txt' ProjectUri = 'https://github.com/dlwyatt/PowerShellLoggingModule' - ReleaseNotes = 'Added DateTimeFormat parameter to Enable-LogFile' + ReleaseNotes = 'Added support for PowerShell 6 & 7 +Added support for logging from runspaces in pools (i.e. without an actual host)' } } } diff --git a/Module/PowerShellLogging/PowerShellLogging.psm1 b/Module/PowerShellLogging/PowerShellLogging.psm1 index 0c424c5..b5a9d92 100644 --- a/Module/PowerShellLogging/PowerShellLogging.psm1 +++ b/Module/PowerShellLogging/PowerShellLogging.psm1 @@ -1,15 +1,5 @@ -# Cmdlets and associated data types are defined in PowerShellLoggingModule.dll. This script file just handles detaching the HostIOInterceptor object when the module unloads. - -$dllPath = Join-Path -Path $MyInvocation.MyCommand.ScriptBlock.Module.ModuleBase -ChildPath PowerShellLoggingModule.dll - -try -{ - Import-Module -Name $dllPath -ErrorAction Stop -} -catch -{ - throw -} +# Cmdlets and associated data types are defined in PowerShellLoggingModule.dll. +# This script file just handles detaching the HostIOInterceptor object when the module unloads. $MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { [PSLogging.HostIOInterceptor]::Instance.RemoveAllSubscribers() diff --git a/PSLogging.csproj b/PSLogging.csproj deleted file mode 100644 index 6dd6501..0000000 --- a/PSLogging.csproj +++ /dev/null @@ -1,71 +0,0 @@ - - - - - Debug - AnyCPU - {9793E825-3711-4C3E-8F12-A06DA73BBEB3} - Library - Properties - PSLogging - PowerShellLoggingModule - v2.0 - 512 - - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - False - ..\..\..\..\..\..\Program Files (x86)\Reference Assemblies\Microsoft\WindowsPowerShell\v1.0\System.Management.Automation.dll - - - - - - - - - - - - - - - - - - - - - - Designer - - - - - \ No newline at end of file diff --git a/PowerShellLoggingModule.csproj b/PowerShellLoggingModule.csproj new file mode 100644 index 0000000..0e95f48 --- /dev/null +++ b/PowerShellLoggingModule.csproj @@ -0,0 +1,41 @@ + + + + + netstandard2.0 + + 1.4.0 + Library + embedded + + + + + + + + + + + + + Designer + + + + + + \ No newline at end of file diff --git a/PowerShellLoggingModule.sln b/PowerShellLoggingModule.sln index 92618d2..29bcec5 100644 --- a/PowerShellLoggingModule.sln +++ b/PowerShellLoggingModule.sln @@ -1,7 +1,9 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2012 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PSLogging", "PSLogging.csproj", "{9793E825-3711-4C3E-8F12-A06DA73BBEB3}" +# Visual Studio Version 16 +VisualStudioVersion = 16.0.28803.352 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerShellLoggingModule", "PowerShellLoggingModule.csproj", "{9793E825-3711-4C3E-8F12-A06DA73BBEB3}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -17,4 +19,7 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {72D99336-7067-4FE7-901A-601CAC37CFF3} + EndGlobalSection EndGlobal diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs deleted file mode 100644 index 7bdb026..0000000 --- a/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. - -[assembly: AssemblyTitle("PowerShellLoggingModule")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("PowerShellLoggingModule")] -[assembly: AssemblyCopyright("Copyright © 2013")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. - -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM - -[assembly: Guid("6E5EF5E0-E266-483F-9A9E-6DCC04866167")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] - -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/README.md b/README.md index bb7f625..62d5bfa 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,62 @@ PowerShellLoggingModule ======================= -Uses Reflection to intercept text output headed for the PowerShell -console or ISE. Lines of output are sent to any number of -subscriber objects for the purpose of producing complete log files -of script output (including date/timestamps) without the need for -extra code by the script author. +Uses Reflection to intercept text output headed for the PowerShell console. +All lines of output are sent to any number of subscriber objects +producing complete log files of script output (adding date/timestamps) +without needing extra code by the script author. -The source code is maintained on GitHub. If you would like to use -the module but can't compile it yourself, you can install it with -PowerShellGet: +The upside is that any text which would have shown up in the console is logged, +and the downside is that only that text is logged. +For example, verbose output is only logged if VerbosePreference is Continue... + + +Supports PowerShell 2, 3, 4, 5, 6 and 7 + + +Install from the PowerShell Gallery +================================== ```posh Install-Module PowerShellLogging ``` -Or download a zip file of the module from: +Compile your own copy +==================== + +You can compile the assembly with: + +```posh +dotnet build -c Release +``` + +To generate the full module, you can run the build script: + +```posh +.\build -Version $(gitversion -showvariable nugetversion) +``` + +**Note:** this script builds into a version numbered folder in the module root. +The expectation is that you have the source in a folder like `~\Projects\Modules\PowerShellLogging` +where the parent folder can be added to your PSModulePath for testing purposes, +so the build will end up in, e.g.: `~\Projects\Modules\PowerShellLogging\1.4.0` +and the build script will update the metadata to make it all versioned properly! + +Testing +======= + +The test cases are very minimal (basically just covering the fact that it logs, and testing a couple of edge cases where it used to fail to log). Despite that, weand have some problems due to the way that WindowsPowerShell breaks when they fail. + + As a result, _to be sure that the tests are actually working_ (reporting the correct results), you should run each test case in a new session. There is a wrapper script `test.ps1` which you can use to do that, it basically just runs each test case in `PowerShell` and `pwsh` to ensure everything is working in both WindowsPowerShell and PowerShell core. E.g.: + +``` +foreach ($testcase in Get-ChildItem Tests\*.Tests.ps1) { + powershell -NoProfile -Command Invoke-Pester $testcase.FullName +} +``` + +Alternative Download +=================== + +Note, the original version is also available from: http://gallery.technet.microsoft.com/scriptcenter/Enhanced-Script-Logging-27615f85 diff --git a/Test.ps1 b/Test.ps1 new file mode 100644 index 0000000..a6e0c9a --- /dev/null +++ b/Test.ps1 @@ -0,0 +1,18 @@ +# The tests in here do not work properly in PowerShell 5.x +# If you run them all at once, you will get a lot of FALSE PASSES using the old code +# Running one test at a time in a new powershell session solves this problem: +if (Get-Command powershell.exe -ErrorAction SilentlyContinue) { + foreach ($testcase in ls $PSScriptRoot\Tests\*.Tests.ps1) { + powershell -NoProfile -Command Invoke-Pester $testcase.FullName + } +} else { + Write-Warning "Skipping Windows PowerShell tests" +} + +if (Get-Command pwsh -ErrorAction SilentlyContinue) { + foreach ($testcase in ls $PSScriptRoot\Tests\*.Tests.ps1) { + pwsh -NoProfile -Command Invoke-Pester $testcase.FullName + } +} else { + Write-Warning "Skipping PowerShell Core tests" +} diff --git a/Tests/Invoke.Tests.ps1 b/Tests/Invoke.Tests.ps1 new file mode 100644 index 0000000..7c35c7b --- /dev/null +++ b/Tests/Invoke.Tests.ps1 @@ -0,0 +1,70 @@ +#requires -Module PowerShellLogging +Describe "Working when called inside PowerShell.Invoke (as long as we Remove-Module)" { + + $Path = "TestDrive:\log.txt" + $Path = (Join-Path (Convert-Path (Split-Path $Path)) (Split-Path $Path -Leaf)) + + BeforeAll { + $script:PowerShell = [PowerShell]::Create() + } + + AfterAll { + if ($script:PowerShell) { + $script:PowerShell.Dispose() + } + } + + $TestScript = " + Import-Module PowerShellLogging + `$Logging = Enable-LogFile -Path '$Path' + Write-Host 'This is a host test' + 'Returned OK' + Write-Verbose 'This is a verbose test' -Verbose + Disable-LogFile `$Logging + Remove-Module PowerShellLogging + " + + It "Should not crash when used" { + $script:Result = $script:PowerShell.AddScript($TestScript).Invoke() + } + + It "Should not interfere with output" { + $script:Result | Should -Be "Returned OK" + } + + It "Should not cause Enable-LogFile to fail" { + $script:PowerShell.Streams.Error.InvocationInfo.MyCommand.Name | Should -Not -Contain "Enable-LogFile" + } + + It "Should not cause Write-Host to fail" { + $script:PowerShell.Streams.Error.InvocationInfo.MyCommand.Name | Should -Not -Contain "Write-Host" + } + + It "Should not cause Write-Verbose to fail" { + $script:PowerShell.Streams.Error.InvocationInfo.MyCommand.Name | Should -Not -Contain "Write-Verbose" + } + + It "Should not cause Disable-LogFile to fail" { + $script:PowerShell.Streams.Error.InvocationInfo.MyCommand.Name | Should -Not -Contain "Disable-LogFile" + } + + It "Should not cause any errors" { + $script:PowerShell.Streams.Error + $script:PowerShell.Streams.Error.Count | Should -Be 0 + } + + It "Should create the log file" { + # this proves the logging works + $Path | Should -Exist + } + + It "Should log host output" -Skip:(!(Test-Path $Path)) { + (Get-Content $Path) -match ".*This is a host test$" | Should -Not -BeNullOrEmpty + } + + It "Should log verbose output" -Skip:(!(Test-Path $Path)) { + (Get-Content $Path) -match ".*This is a verbose test$" | Should -Not -BeNullOrEmpty + } + + +} \ No newline at end of file diff --git a/Tests/ParallelRemote.Tests.ps1 b/Tests/ParallelRemote.Tests.ps1 new file mode 100644 index 0000000..fa8b981 --- /dev/null +++ b/Tests/ParallelRemote.Tests.ps1 @@ -0,0 +1,114 @@ +#requires -Module PowerShellLogging, ThreadJob +param($Count = 4) + +Describe "Working when called in parallel in remote runspaces" -Tag "Remoting" { + + $Path = "TestDrive:\log{0}.txt" + $Path = (Join-Path (Convert-Path (Split-Path $Path)) (Split-Path $Path -Leaf)) + + BeforeAll { + $script:PowerShell = $( + foreach($index in 1..$Count) { + $LocalHost = [System.Management.Automation.Runspaces.WSManConnectionInfo]@{ComputerName = "."; EnableNetworkAccess = $true } + $Runspace = [runspacefactory]::CreateRunspace($LocalHost) + $Runspace.Open() + $PowerShell = [PowerShell]::Create() + $PowerShell.Runspace = $Runspace + $PowerShell + } + ) + } + + AfterAll { + foreach($PS in $script:PowerShell) { + $PS.Runspace.Dispose() + $PS.Dispose() + } + } + + $TestScript = " + Import-Module PowerShellLogging + `$Logging = Enable-LogFile -Path '${Path}' + Write-Host 'This is a host test from attempt {0}' + '${Path}' + Start-Sleep 2 + Write-Verbose 'This is a verbose test from attempt {0}' -Verbose + Disable-LogFile `$Logging + " + + It "Should not crash when used" { + $script:Results = & { + $i = 0 + foreach($PS in $script:PowerShell) { + $i += 1 + Start-ThreadJob { param($PS, $Script) $PS.AddScript($Script).Invoke() } -ArgumentList $PS, ($TestScript -f $i) + } + } | Wait-Job | Receive-Job + } + + It "Should not interfere with output" { + $script:Results.Count | Should -Be $script:PowerShell.Count + $i = 0 + foreach ($resultPath in $script:Results) { + $i += 1 + $resultPath | Should -Be ($Path -f $i) + } + } + + It "Should not cause Enable-LogFile to fail" { + foreach($PS in $script:PowerShell) { + $PS.Streams.Error.InvocationInfo.MyCommand.Name | Should -Not -Contain "Enable-LogFile" + } + } + + It "Should not cause Write-Host to fail" { + foreach ($PS in $script:PowerShell) { + $PS.Streams.Error.InvocationInfo.MyCommand.Name | Should -Not -Contain "Write-Host" + } + } + + It "Should not cause Write-Verbose to fail" { + foreach ($PS in $script:PowerShell) { + $PS.Streams.Error.InvocationInfo.MyCommand.Name | Should -Not -Contain "Write-Verbose" + } + } + + It "Should not cause Disable-LogFile to fail" { + foreach ($PS in $script:PowerShell) { + $PS.Streams.Error.InvocationInfo.MyCommand.Name | Should -Not -Contain "Disable-LogFile" + } + } + + It "Should not cause any errors" { + foreach ($PS in $script:PowerShell) { + $PS.Streams.Error.Count | Should -Be 0 + } + } + + Write-Warning "Expecting $($script:Results.Count) log files!" + + It "Should create the log file" { + # this is enough to prove the logging works + $i = 0 + foreach($PS in $script:PowerShell) { + $i += 1 + ($Path -f $i) | Should -Exist + } + } + + $i = 0 + foreach ($PS in $script:PowerShell) { + $i += 1 + It "Should log host output to $($Path -f $i)" -Skip:(!(Test-Path ($Path -f $i))) { + (Get-Content ($Path -f $i)) -match "This is a host test from attempt $i$" | Should -Not -BeNullOrEmpty + } + } + + $i = 0 + foreach ($PS in $script:PowerShell) { + $i += 1 + It "Should log host output to $($Path -f $i)" -Skip:(!(Test-Path ($Path -f $i))) { + (Get-Content ($Path -f $i)) -match "This is a verbose test from attempt $i$" | Should -Not -BeNullOrEmpty + } + } +} \ No newline at end of file diff --git a/Tests/ParallelRunspace.Tests.ps1 b/Tests/ParallelRunspace.Tests.ps1 new file mode 100644 index 0000000..5d9b1de --- /dev/null +++ b/Tests/ParallelRunspace.Tests.ps1 @@ -0,0 +1,93 @@ +#requires -Module PowerShellLogging, ThreadJob +param($Count = 2) + +Describe "Working when called simultaneously in parallel runspaces" -Tag "ThreadJob", "WIP" { + + $Path = "TestDrive:\log{0}.txt" + $Path = (Join-Path (Convert-Path (Split-Path $Path)) (Split-Path $Path -Leaf)) + + $TestScript = " + Import-Module PowerShellLogging + `$Logging = Enable-LogFile -Path '${Path}' + Write-Host 'This is a host test from attempt {0}' + '${Path}' + Start-Sleep 2 + Write-Verbose 'This is a verbose test from attempt {0}' -Verbose + Disable-LogFile `$Logging + " + + $TestScript = { + param($Path, $Index) + Import-Module PowerShellLogging + $Logging = Enable-LogFile -Path $Path + Microsoft.PowerShell.Utility\Write-Host "This is a host test from attempt $index" + $Path + Microsoft.PowerShell.Utility\Write-Verbose 'This is a verbose test' -Verbose + # Does not output, because Verbose is suppressed + Microsoft.PowerShell.Utility\Write-Verbose "This is a verbose test from attempt $index" -Verbose + Disable-LogFile $Logging + } + + + It "Should not crash when used" { + $script:Results = & { + foreach ($i in 1..$Count) { + Start-ThreadJob $TestScript -ArgumentList ($Path -f $i), $i + } + } | Wait-Job | Receive-Job -ErrorVariable Ev + $Script:Ev = $Ev + } + + It "Should not interfere with output" { + $script:Results.Count | Should -Be $Count + $i = 0 + foreach ($resultPath in $script:Results) { + $i += 1 + $resultPath | Should -Be ($Path -f $i) + } + } + + It "Should not cause Enable-LogFile to fail" { + $script:Ev.InvocationInfo.MyCommand.Name | Should -Not -Contain "Enable-LogFile" + } + + It "Should not cause Write-Host to fail" { + $script:Ev.InvocationInfo.MyCommand.Name | Should -Not -Contain "Write-Host" + } + + It "Should not cause Write-Verbose to fail" { + $script:Ev.InvocationInfo.MyCommand.Name | Should -Not -Contain "Write-Verbose" + } + + It "Should not cause Disable-LogFile to fail" { + $script:Ev.InvocationInfo.MyCommand.Name | Should -Not -Contain "Disable-LogFile" + } + + It "Should not cause any errors" { + $script:Ev.Count | Should -Be 0 + } + + + Write-Warning "Expecting $($script:Results.Count) log files!" + + It "Should create the log file" { + # this is enough to prove the logging works + foreach ($i in 1..$Count) { + ($Path -f $i) | Should -Exist + } + } + + + foreach ($i in 1..$Count) { + It "Should log host output to $($Path -f $i)" -Skip:(!(Test-Path ($Path -f $i))) { + (Get-Content ($Path -f $i)) -match "This is a host test from attempt $i$" | Should -Not -BeNullOrEmpty + } + } + + + foreach ($i in 1..$Count) { + It "Should log host output to $($Path -f $i)" -Skip:(!(Test-Path ($Path -f $i))) { + (Get-Content ($Path -f $i)) -match "This is a verbose test from attempt $i$" | Should -Not -BeNullOrEmpty + } + } +} \ No newline at end of file diff --git a/Tests/Remote.Tests.ps1 b/Tests/Remote.Tests.ps1 new file mode 100644 index 0000000..2ce7526 --- /dev/null +++ b/Tests/Remote.Tests.ps1 @@ -0,0 +1,74 @@ +#requires -Module PowerShellLogging +Describe "Working when called in a remote runspace" -Tag "Remoting" { + + $Path = "TestDrive:\log.txt" + $Path = (Join-Path (Convert-Path (Split-Path $Path)) (Split-Path $Path -Leaf)) + + BeforeAll { + $LocalHost = [System.Management.Automation.Runspaces.WSManConnectionInfo]@{ComputerName = "."; EnableNetworkAccess = $true } + $Runspace = [runspacefactory]::CreateRunspace($LocalHost) + $Runspace.Open() + + $script:PowerShell = [PowerShell]::Create() + $script:PowerShell.Runspace = $Runspace + } + + AfterAll { + if ($script:PowerShell) { + $script:PowerShell.Runspace.Dispose() + $script:PowerShell.Dispose() + } + } + + $TestScript = " + Import-Module PowerShellLogging + `$Logging = Enable-LogFile -Path '$Path' + Write-Host 'This is a host test' + 'Returned OK' + Write-Verbose 'This is a verbose test' -Verbose + Disable-LogFile `$Logging + " + + It "Should not crash when used" { + $script:Result = $script:PowerShell.AddScript($TestScript).Invoke() + } + + It "Should not interfere with output" { + $script:Result | Should -Be "Returned OK" + } + + It "Should not cause Enable-LogFile to fail" { + $script:PowerShell.Streams.Error.InvocationInfo.MyCommand.Name | Should -Not -Contain "Enable-LogFile" + } + + It "Should not cause Write-Host to fail" { + $script:PowerShell.Streams.Error.InvocationInfo.MyCommand.Name | Should -Not -Contain "Write-Host" + } + + It "Should not cause Write-Verbose to fail" { + $script:PowerShell.Streams.Error.InvocationInfo.MyCommand.Name | Should -Not -Contain "Write-Verbose" + } + + It "Should not cause Disable-LogFile to fail" { + $script:PowerShell.Streams.Error.InvocationInfo.MyCommand.Name | Should -Not -Contain "Disable-LogFile" + } + + It "Should not cause any errors" { + $script:PowerShell.Streams.Error.Count | Should -Be 0 + } + + It "Should create the log file" { + # this proves the logging works + $Path | Should -Exist + } + + It "Should log host output" -Skip:(!(Test-Path $Path)) { + (Get-Content $Path) -match ".*This is a host test$" | Should -Not -BeNullOrEmpty + } + + It "Should log verbose output" -Skip:(!(Test-Path $Path)) { + (Get-Content $Path) -match ".*This is a verbose test$" | Should -Not -BeNullOrEmpty + } + + +} \ No newline at end of file diff --git a/Tests/Runspace.Tests.ps1 b/Tests/Runspace.Tests.ps1 new file mode 100644 index 0000000..880dc9e --- /dev/null +++ b/Tests/Runspace.Tests.ps1 @@ -0,0 +1,72 @@ +#requires -Module PowerShellLogging +Describe "Working when called synchronously in a parallel runspace (as long as we Remove-Module)" { + + $Path = "TestDrive:\log.txt" + $Path = (Join-Path (Convert-Path (Split-Path $Path)) (Split-Path $Path -Leaf)) + + BeforeAll { + $script:PowerShell = [PowerShell]::Create() + $PowerShell.Runspace = [runspacefactory]::CreateRunspace() + $PowerShell.Runspace.Open() + } + + AfterAll { + if ($script:PowerShell) { + $script:PowerShell.Runspace.Dispose() + $script:PowerShell.Dispose() + } + } + + $TestScript = " + Import-Module PowerShellLogging + `$Logging = Enable-LogFile -Path '$Path' + Write-Host 'This is a host test' + 'Returned OK' + Write-Verbose 'This is a verbose test' -Verbose + Disable-LogFile `$Logging + Remove-Module PowerShellLogging + " + + It "Should not crash when used" { + $script:Result = $script:PowerShell.AddScript($TestScript).Invoke() + } + + It "Should not interfere with output" { + $script:Result | Should -Be "Returned OK" + } + + It "Should not cause Enable-LogFile to fail" { + $script:PowerShell.Streams.Error.InvocationInfo.MyCommand.Name | Should -Not -Contain "Enable-LogFile" + } + + It "Should not cause Write-Host to fail" { + $script:PowerShell.Streams.Error.InvocationInfo.MyCommand.Name | Should -Not -Contain "Write-Host" + } + + It "Should not cause Write-Verbose to fail" { + $script:PowerShell.Streams.Error.InvocationInfo.MyCommand.Name | Should -Not -Contain "Write-Verbose" + } + + It "Should not cause Disable-LogFile to fail" { + $script:PowerShell.Streams.Error.InvocationInfo.MyCommand.Name | Should -Not -Contain "Disable-LogFile" + } + + It "Should not cause any errors" { + $script:PowerShell.Streams.Error.Count | Should -Be 0 + } + + It "Should create the log file" { + # this proves the logging works + $Path | Should -Exist + } + + It "Should log host output" -Skip:(!(Test-Path $Path)) { + (Get-Content $Path) -match ".*This is a host test$" | Should -Not -BeNullOrEmpty + } + + It "Should log verbose output" -Skip:(!(Test-Path $Path)) { + (Get-Content $Path) -match ".*This is a verbose test$" | Should -Not -BeNullOrEmpty + } + + +} \ No newline at end of file diff --git a/Tests/Simple.Tests.ps1 b/Tests/Simple.Tests.ps1 new file mode 100644 index 0000000..8512c98 --- /dev/null +++ b/Tests/Simple.Tests.ps1 @@ -0,0 +1,66 @@ +#requires -Module PowerShellLogging +Describe "Working in scripts run locally in the host (as long as we Remove-Module)" { + + $Path = "TestDrive:\log.txt" + $Path = (Join-Path (Convert-Path (Split-Path $Path)) (Split-Path $Path -Leaf)) + + $TestScript = { + [CmdletBinding()]param() + + Import-Module PowerShellLogging + $Logging = Enable-LogFile -Path $Path + Write-Host 'This is a host test' + 'Returned OK' + Write-Verbose 'This is a verbose test' -verbose + Write-Verbose 'This is a another verbose test' + Disable-LogFile $Logging + Remove-Module PowerShellLogging + } + + It "Should not crash when used" { + $script:Result = & $TestScript -ErrorVariable Ev + $script:Ev = $Ev + } + + It "Should not interfere with output" { + $script:Result | Should -Be "Returned OK" + } + + It "Should not cause Enable-LogFile to fail" { + $script:Ev.InvocationInfo.MyCommand.Name | Should -Not -Contain "Enable-LogFile" + } + + It "Should not cause Write-Host to fail" { + $script:Ev.InvocationInfo.MyCommand.Name | Should -Not -Contain "Write-Host" + } + + It "Should not cause Write-Verbose to fail" { + $script:Ev.InvocationInfo.MyCommand.Name | Should -Not -Contain "Write-Verbose" + } + + It "Should not cause Disable-LogFile to fail" { + $script:Ev.InvocationInfo.MyCommand.Name | Should -Not -Contain "Disable-LogFile" + } + + It "Should not cause any errors" { + $script:Ev.Count | Should -Be 0 + } + + It "Should create the log file" { + # this proves the logging works + $Path | Should -Exist + } + + It "Should log host output" -Skip:(!(Test-Path $Path)) { + (Get-Content $Path) -match ".*This is a host test$" | Should -Not -BeNullOrEmpty + } + + It "Should log verbose output" -Skip:(!(Test-Path $Path)) { + (Get-Content $Path) -match ".*This is a verbose test$" | Should -Not -BeNullOrEmpty + } + + It "Should not log verbose that's not output" -Skip:(!(Test-Path $Path)) { + (Get-Content $Path) -match ".*This is another verbose test$" | Should -BeNullOrEmpty + } + +} \ No newline at end of file