From b229c1ad9a45f737be6362653e7346d14dd8d786 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 19 Jul 2026 04:48:43 +0200 Subject: [PATCH 01/12] =?UTF-8?q?=E2=9A=A1=20[Performance]:=20Remove=20red?= =?UTF-8?q?undant=20command=20initialization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Initialize Sodium once during module import and derive byte-array public keys directly. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- PSModule/Sodium/Sodium.cs | 7 ++++++- src/functions/public/ConvertFrom-SodiumSealedBox.ps1 | 4 ---- src/functions/public/ConvertTo-SodiumSealedBox.ps1 | 4 ---- src/functions/public/Get-SodiumPublicKey.ps1 | 7 +------ src/functions/public/New-SodiumKeyPair.ps1 | 4 ---- 5 files changed, 7 insertions(+), 19 deletions(-) diff --git a/PSModule/Sodium/Sodium.cs b/PSModule/Sodium/Sodium.cs index 08ec69e..75f6d82 100644 --- a/PSModule/Sodium/Sodium.cs +++ b/PSModule/Sodium/Sodium.cs @@ -203,6 +203,11 @@ public static KeyPairBase64 GenerateKeyPairBase64(string seedText) } public static string DerivePublicKeyBase64(string privateKeyBase64) + { + return Convert.ToBase64String(DerivePublicKey(privateKeyBase64)); + } + + public static byte[] DerivePublicKey(string privateKeyBase64) { ArgumentNullException.ThrowIfNull(privateKeyBase64); var privateKey = DecodeBase64Exact(privateKeyBase64, SecretKeyBytes, "private key"); @@ -213,7 +218,7 @@ public static string DerivePublicKeyBase64(string privateKeyBase64) { throw new InvalidOperationException("Unable to derive public key from private key."); } - return Convert.ToBase64String(publicKey); + return publicKey; } finally { diff --git a/src/functions/public/ConvertFrom-SodiumSealedBox.ps1 b/src/functions/public/ConvertFrom-SodiumSealedBox.ps1 index 5b72e4a..540b0d1 100644 --- a/src/functions/public/ConvertFrom-SodiumSealedBox.ps1 +++ b/src/functions/public/ConvertFrom-SodiumSealedBox.ps1 @@ -68,10 +68,6 @@ [string] $PrivateKey ) - begin { - Initialize-Sodium - } - process { try { if (-not [string]::IsNullOrWhiteSpace($PublicKey)) { diff --git a/src/functions/public/ConvertTo-SodiumSealedBox.ps1 b/src/functions/public/ConvertTo-SodiumSealedBox.ps1 index ff87d42..43dd858 100644 --- a/src/functions/public/ConvertTo-SodiumSealedBox.ps1 +++ b/src/functions/public/ConvertTo-SodiumSealedBox.ps1 @@ -55,10 +55,6 @@ [ValidateNotNullOrEmpty()] [string] $PublicKey ) - begin { - Initialize-Sodium - } - process { try { return [PSModule.Sodium]::SealBase64($Message, $PublicKey) diff --git a/src/functions/public/Get-SodiumPublicKey.ps1 b/src/functions/public/Get-SodiumPublicKey.ps1 index 8a7ab08..5dd35ef 100644 --- a/src/functions/public/Get-SodiumPublicKey.ps1 +++ b/src/functions/public/Get-SodiumPublicKey.ps1 @@ -81,14 +81,10 @@ [switch] $AsByteArray ) - begin { - Initialize-Sodium - } - process { if ($AsByteArray) { try { - return [System.Convert]::FromBase64String([PSModule.Sodium]::DerivePublicKeyBase64($PrivateKey)) + return [PSModule.Sodium]::DerivePublicKey($PrivateKey) } catch [System.Management.Automation.MethodInvocationException] { throw $_.Exception.InnerException } @@ -100,5 +96,4 @@ } } - end {} } diff --git a/src/functions/public/New-SodiumKeyPair.ps1 b/src/functions/public/New-SodiumKeyPair.ps1 index a8bebe5..af515a2 100644 --- a/src/functions/public/New-SodiumKeyPair.ps1 +++ b/src/functions/public/New-SodiumKeyPair.ps1 @@ -84,10 +84,6 @@ [string] $Seed ) - begin { - Initialize-Sodium - } - process { try { if ($PSCmdlet.ParameterSetName -eq 'SeededKeyPair') { From f3f481df82efd9fe50d99d7c24c73e6b9d904fb5 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 19 Jul 2026 04:49:01 +0200 Subject: [PATCH 02/12] =?UTF-8?q?=F0=9F=A7=AA=20[Test]:=20Cover=20parallel?= =?UTF-8?q?=20module=20sessions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exercise concurrent runspace and process imports plus direct byte-array key derivation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- tests/Sodium.Tests.ps1 | 50 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/Sodium.Tests.ps1 b/tests/Sodium.Tests.ps1 index 95680de..ab5ec24 100644 --- a/tests/Sodium.Tests.ps1 +++ b/tests/Sodium.Tests.ps1 @@ -163,6 +163,14 @@ Describe 'Sodium' { $derivedPublicKey | Should -Be $expectedPublicKey } + It 'Get-SodiumPublicKey - Returns the public key as a byte array' { + $keyPair = New-SodiumKeyPair + $derivedPublicKey = Get-SodiumPublicKey -PrivateKey $keyPair.PrivateKey -AsByteArray + + $derivedPublicKey | Should -BeOfType ([byte]) + [Convert]::ToBase64String($derivedPublicKey) | Should -Be $keyPair.PublicKey + } + It 'Get-SodiumPublicKey - Throws an error when an invalid private key is provided' { $invalidPrivateKey = 'InvalidKey' @@ -186,4 +194,46 @@ Describe 'Sodium' { } } } + + Context 'Parallel sessions' { + It 'Loads and completes crypto round trips in parallel runspaces' { + $modulePath = (Get-Module -Name Sodium -ErrorAction Stop).Path + Test-Path -Path $modulePath | Should -BeTrue + $results = 1..4 | ForEach-Object -Parallel { + Import-Module -Name $using:modulePath -Force + $keyPair = New-SodiumKeyPair -Seed "Runspace-$_" + $message = "Parallel runspace $_" + $sealedBox = ConvertTo-SodiumSealedBox -Message $message -PublicKey $keyPair.PublicKey + ConvertFrom-SodiumSealedBox -SealedBox $sealedBox -PrivateKey $keyPair.PrivateKey + } -ThrottleLimit 4 + + $results | Should -HaveCount 4 + $results | Should -Contain 'Parallel runspace 1' + $results | Should -Contain 'Parallel runspace 4' + } + + It 'Loads and completes crypto round trips in parallel processes' { + $modulePath = (Get-Module -Name Sodium -ErrorAction Stop).Path + $jobs = 1..4 | ForEach-Object { + Start-Job -ScriptBlock { + $id = $args[0] + $modulePath = $args[1] + Import-Module -Name $modulePath -Force + $keyPair = New-SodiumKeyPair -Seed "Process-$id" + $message = "Parallel process $id" + $sealedBox = ConvertTo-SodiumSealedBox -Message $message -PublicKey $keyPair.PublicKey + ConvertFrom-SodiumSealedBox -SealedBox $sealedBox -PrivateKey $keyPair.PrivateKey + } -ArgumentList $_, $modulePath + } + + try { + $results = $jobs | Receive-Job -Wait + $results | Should -HaveCount 4 + $results | Should -Contain 'Parallel process 1' + $results | Should -Contain 'Parallel process 4' + } finally { + $jobs | Remove-Job -Force + } + } + } } From 85367ff1d12b6c7ee7b7bf61958691d7c0c00123 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 19 Jul 2026 04:59:27 +0200 Subject: [PATCH 03/12] =?UTF-8?q?=F0=9F=A7=AA=20[Test]:=20Cover=20module?= =?UTF-8?q?=20initialization=20paths?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exercise idempotent initialization, buffer-size caching, unsupported platforms, and missing runtime diagnostics. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- tests/Sodium.Tests.ps1 | 45 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/Sodium.Tests.ps1 b/tests/Sodium.Tests.ps1 index ab5ec24..dbd6715 100644 --- a/tests/Sodium.Tests.ps1 +++ b/tests/Sodium.Tests.ps1 @@ -193,6 +193,51 @@ Describe 'Sodium' { $result | Should -BeTrue } } + + It 'Assert-VisualCRedistributableInstalled reports a missing runtime' { + InModuleScope Sodium { + Mock Get-ItemProperty { $null } + + $result = Assert-VisualCRedistributableInstalled -Version '14.0' -Architecture 'X64' 3>$null + $result | Should -BeFalse + } + } + + It 'Initialize-Sodium treats repeated initialization as a no-op' { + InModuleScope Sodium { + $script:SodiumInitialized | Should -BeTrue + { Initialize-Sodium } | Should -Not -Throw + } + } + + It 'Initialize-Sodium restores cached native buffer sizes' { + InModuleScope Sodium { + $script:SodiumInitialized = $false + $script:SodiumPublicKeyBytes = $null + $script:SodiumPrivateKeyBytes = $null + $script:SodiumSealBytes = $null + $script:SodiumSeedBytes = $null + + Initialize-Sodium + + $script:SodiumInitialized | Should -BeTrue + $script:SodiumPublicKeyBytes | Should -Be 32 + $script:SodiumPrivateKeyBytes | Should -Be 32 + $script:SodiumSealBytes | Should -Be 48 + $script:SodiumSeedBytes | Should -Be 32 + } + } + + It 'Initialize-Sodium rejects an unsupported platform' { + InModuleScope Sodium { + $script:Supported = $false + try { + { Initialize-Sodium } | Should -Throw 'Sodium is not supported on this platform.' + } finally { + $script:Supported = $true + } + } + } } Context 'Parallel sessions' { From 917fe7a8382e2d0a3dec8bd189d213e82af1ae57 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 19 Jul 2026 05:09:08 +0200 Subject: [PATCH 04/12] =?UTF-8?q?=F0=9F=A7=AA=20[Test]:=20Clarify=20native?= =?UTF-8?q?=20runtime=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Isolate untriggerable native-load diagnostics, simulate the Windows registry branch, and assert byte-array output without pipeline enumeration. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../private/Assert-SodiumNativeRuntime.ps1 | 28 +++++++++++++++++++ src/functions/private/Initialize-Sodium.ps1 | 11 +------- src/functions/public/Get-SodiumPublicKey.ps1 | 6 +++- tests/Sodium.Tests.ps1 | 21 +++++++++++++- 4 files changed, 54 insertions(+), 12 deletions(-) create mode 100644 src/functions/private/Assert-SodiumNativeRuntime.ps1 diff --git a/src/functions/private/Assert-SodiumNativeRuntime.ps1 b/src/functions/private/Assert-SodiumNativeRuntime.ps1 new file mode 100644 index 0000000..2ebcbd3 --- /dev/null +++ b/src/functions/private/Assert-SodiumNativeRuntime.ps1 @@ -0,0 +1,28 @@ +function Assert-SodiumNativeRuntime { + <# + .SYNOPSIS + Provides platform-specific diagnostics after Sodium native initialization fails. + + .DESCRIPTION + Checks the Windows Visual C++ runtime after a native initialization exception and throws a targeted message when the required + runtime is unavailable. + + .NOTES + This function only runs after native library loading fails, which cannot be reproduced safely after the library is loaded in the test process. + #> + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute()] + [OutputType([void])] + [CmdletBinding()] + param() + + process { + if ($IsWindows -and $script:ProcessArchitecture -in @('X64', 'X86')) { + $hasRuntime = Assert-VisualCRedistributableInstalled -Version '14.0' -Architecture $script:ProcessArchitecture + if (-not $hasRuntime) { + $message = "Sodium native initialization failed; the Visual C++ Redistributable for " + + "$($script:ProcessArchitecture) appears to be missing or below the required version." + throw $message + } + } + } +} diff --git a/src/functions/private/Initialize-Sodium.ps1 b/src/functions/private/Initialize-Sodium.ps1 index 2ad47bb..e0469df 100644 --- a/src/functions/private/Initialize-Sodium.ps1 +++ b/src/functions/private/Initialize-Sodium.ps1 @@ -23,16 +23,7 @@ $initializationResult = [PSModule.Sodium]::sodium_init() } catch { $script:Supported = $false - if ($IsWindows) { - if ($script:ProcessArchitecture -in @('X64', 'X86')) { - $hasRuntime = Assert-VisualCRedistributableInstalled -Version '14.0' -Architecture $script:ProcessArchitecture - if (-not $hasRuntime) { - $message = "Sodium native initialization failed; the Visual C++ Redistributable for " + - "$($script:ProcessArchitecture) appears to be missing or below the required version." - throw $message - } - } - } + Assert-SodiumNativeRuntime throw } if ($initializationResult -lt 0) { diff --git a/src/functions/public/Get-SodiumPublicKey.ps1 b/src/functions/public/Get-SodiumPublicKey.ps1 index 5dd35ef..77ac1d2 100644 --- a/src/functions/public/Get-SodiumPublicKey.ps1 +++ b/src/functions/public/Get-SodiumPublicKey.ps1 @@ -67,6 +67,10 @@ https://psmodule.io/Sodium/Functions/Get-SodiumPublicKey/ #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseOutputTypeCorrectly', '', + Justification = 'The unary comma preserves the byte array as one pipeline object.' + )] [OutputType([string], ParameterSetName = 'Base64')] [OutputType([byte[]], ParameterSetName = 'AsByteArray')] [CmdletBinding(DefaultParameterSetName = 'Base64')] @@ -84,7 +88,7 @@ process { if ($AsByteArray) { try { - return [PSModule.Sodium]::DerivePublicKey($PrivateKey) + return , ([PSModule.Sodium]::DerivePublicKey($PrivateKey)) } catch [System.Management.Automation.MethodInvocationException] { throw $_.Exception.InnerException } diff --git a/tests/Sodium.Tests.ps1 b/tests/Sodium.Tests.ps1 index dbd6715..2a423b0 100644 --- a/tests/Sodium.Tests.ps1 +++ b/tests/Sodium.Tests.ps1 @@ -167,7 +167,7 @@ Describe 'Sodium' { $keyPair = New-SodiumKeyPair $derivedPublicKey = Get-SodiumPublicKey -PrivateKey $keyPair.PrivateKey -AsByteArray - $derivedPublicKey | Should -BeOfType ([byte]) + ($derivedPublicKey -is [byte[]]) | Should -BeTrue [Convert]::ToBase64String($derivedPublicKey) | Should -Be $keyPair.PublicKey } @@ -203,6 +203,25 @@ Describe 'Sodium' { } } + It 'Assert-VisualCRedistributableInstalled accepts a matching runtime' { + InModuleScope Sodium { + Mock Get-ItemProperty { + [pscustomobject]@{ + Installed = 1 + Version = 'v14.30.30704.0' + } + } + + Set-Variable -Name IsWindows -Value $true -Scope Script + try { + $result = Assert-VisualCRedistributableInstalled -Version '14.0' + $result | Should -BeTrue + } finally { + Remove-Variable -Name IsWindows -Scope Script + } + } + } + It 'Initialize-Sodium treats repeated initialization as a no-op' { InModuleScope Sodium { $script:SodiumInitialized | Should -BeTrue From dd229fb92a13041ef75f1295e509e917423716f2 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 19 Jul 2026 05:15:15 +0200 Subject: [PATCH 05/12] =?UTF-8?q?=F0=9F=A7=AA=20[Test]:=20Cover=20runtime?= =?UTF-8?q?=20selection=20branches?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract runtime identifier resolution for deterministic cross-platform tests and preserve byte arrays as single pipeline objects. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Resolve-SodiumRuntimeIdentifier.ps1 | 65 +++++++++++++++++++ src/main.ps1 | 34 +--------- tests/Sodium.Tests.ps1 | 24 +++++++ 3 files changed, 91 insertions(+), 32 deletions(-) create mode 100644 src/functions/private/Resolve-SodiumRuntimeIdentifier.ps1 diff --git a/src/functions/private/Resolve-SodiumRuntimeIdentifier.ps1 b/src/functions/private/Resolve-SodiumRuntimeIdentifier.ps1 new file mode 100644 index 0000000..50b9dfb --- /dev/null +++ b/src/functions/private/Resolve-SodiumRuntimeIdentifier.ps1 @@ -0,0 +1,65 @@ +function Resolve-SodiumRuntimeIdentifier { + <# + .SYNOPSIS + Resolves the native Sodium runtime identifier. + + .DESCRIPTION + Maps the active operating system and process architecture to the runtime identifier used by the bundled native library. + #> + [OutputType([string])] + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [System.Runtime.InteropServices.Architecture] $ProcessArchitecture, + + [Parameter()] + [switch] $Linux, + + [Parameter()] + [switch] $MacOS, + + [Parameter()] + [switch] $Windows + ) + + process { + switch ($true) { + $Linux { + switch ($ProcessArchitecture) { + 'Arm64' { return 'linux-arm64' } + 'X64' { return 'linux-x64' } + default { + $message = "Unsupported Linux process architecture: $ProcessArchitecture. " + + 'Please refer to the documentation for supported architectures.' + throw $message + } + } + } + $MacOS { + switch ($ProcessArchitecture) { + 'Arm64' { return 'osx-arm64' } + 'X64' { return 'osx-x64' } + default { + $message = "Unsupported macOS process architecture: $ProcessArchitecture. " + + 'Please refer to the documentation for supported architectures.' + throw $message + } + } + } + $Windows { + switch ($ProcessArchitecture) { + 'X64' { return 'win-x64' } + 'X86' { return 'win-x86' } + default { + $message = "Unsupported Windows process architecture: $ProcessArchitecture. " + + 'Please refer to the documentation for supported architectures.' + throw $message + } + } + } + default { + throw 'Unsupported platform. Please refer to the documentation for more information.' + } + } + } +} diff --git a/src/main.ps1 b/src/main.ps1 index 0d1d6f2..eb585a8 100644 --- a/src/main.ps1 +++ b/src/main.ps1 @@ -1,37 +1,7 @@ $processArchitecture = [System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture -switch ($true) { - $IsLinux { - switch ($processArchitecture) { - 'Arm64' { $runtimeIdentifier = 'linux-arm64' } - 'X64' { $runtimeIdentifier = 'linux-x64' } - default { - throw "Unsupported Linux process architecture: $processArchitecture. Please refer to the documentation for supported architectures." - } - } - } - $IsMacOS { - switch ($processArchitecture) { - 'Arm64' { $runtimeIdentifier = 'osx-arm64' } - 'X64' { $runtimeIdentifier = 'osx-x64' } - default { - throw "Unsupported macOS process architecture: $processArchitecture. Please refer to the documentation for supported architectures." - } - } - } - $IsWindows { - switch ($processArchitecture) { - 'X64' { $runtimeIdentifier = 'win-x64' } - 'X86' { $runtimeIdentifier = 'win-x86' } - default { - throw "Unsupported Windows process architecture: $processArchitecture. Please refer to the documentation for supported architectures." - } - } - } - default { - throw 'Unsupported platform. Please refer to the documentation for more information.' - } -} +$runtimeIdentifier = Resolve-SodiumRuntimeIdentifier -ProcessArchitecture $processArchitecture ` + -Linux:$IsLinux -MacOS:$IsMacOS -Windows:$IsWindows $assemblyPath = Join-Path -Path $PSScriptRoot -ChildPath "libs/$runtimeIdentifier/PSModule.Sodium.dll" Import-Module $assemblyPath -ErrorAction Stop diff --git a/tests/Sodium.Tests.ps1 b/tests/Sodium.Tests.ps1 index 2a423b0..a3490da 100644 --- a/tests/Sodium.Tests.ps1 +++ b/tests/Sodium.Tests.ps1 @@ -257,6 +257,30 @@ Describe 'Sodium' { } } } + + It 'Resolve-SodiumRuntimeIdentifier maps every supported runtime' { + InModuleScope Sodium { + Resolve-SodiumRuntimeIdentifier -ProcessArchitecture 'Arm64' -Linux | Should -Be 'linux-arm64' + Resolve-SodiumRuntimeIdentifier -ProcessArchitecture 'X64' -Linux | Should -Be 'linux-x64' + Resolve-SodiumRuntimeIdentifier -ProcessArchitecture 'Arm64' -MacOS | Should -Be 'osx-arm64' + Resolve-SodiumRuntimeIdentifier -ProcessArchitecture 'X64' -MacOS | Should -Be 'osx-x64' + Resolve-SodiumRuntimeIdentifier -ProcessArchitecture 'X64' -Windows | Should -Be 'win-x64' + Resolve-SodiumRuntimeIdentifier -ProcessArchitecture 'X86' -Windows | Should -Be 'win-x86' + } + } + + It 'Resolve-SodiumRuntimeIdentifier rejects unsupported runtimes' { + InModuleScope Sodium { + { Resolve-SodiumRuntimeIdentifier -ProcessArchitecture 'Arm' -Linux } | + Should -Throw 'Unsupported Linux process architecture: Arm.*' + { Resolve-SodiumRuntimeIdentifier -ProcessArchitecture 'Arm' -MacOS } | + Should -Throw 'Unsupported macOS process architecture: Arm.*' + { Resolve-SodiumRuntimeIdentifier -ProcessArchitecture 'Arm' -Windows } | + Should -Throw 'Unsupported Windows process architecture: Arm.*' + { Resolve-SodiumRuntimeIdentifier -ProcessArchitecture 'X64' } | + Should -Throw 'Unsupported platform.*' + } + } } Context 'Parallel sessions' { From 53cf06d6c0362a2e536ca28a7422ff382621990a Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 19 Jul 2026 05:20:23 +0200 Subject: [PATCH 06/12] =?UTF-8?q?=F0=9F=A7=AA=20[Test]:=20Exercise=20missi?= =?UTF-8?q?ng=20Windows=20runtime=20branch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shadow the platform flag so the cross-platform test executes the mocked registry lookup. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- tests/Sodium.Tests.ps1 | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/Sodium.Tests.ps1 b/tests/Sodium.Tests.ps1 index a3490da..404c4b8 100644 --- a/tests/Sodium.Tests.ps1 +++ b/tests/Sodium.Tests.ps1 @@ -198,8 +198,13 @@ Describe 'Sodium' { InModuleScope Sodium { Mock Get-ItemProperty { $null } - $result = Assert-VisualCRedistributableInstalled -Version '14.0' -Architecture 'X64' 3>$null - $result | Should -BeFalse + Set-Variable -Name IsWindows -Value $true -Scope Script + try { + $result = Assert-VisualCRedistributableInstalled -Version '14.0' -Architecture 'X64' 3>$null + $result | Should -BeFalse + } finally { + Remove-Variable -Name IsWindows -Scope Script + } } } From 0477be704129528e2802135e3d35160edffc06d1 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 19 Jul 2026 05:59:56 +0200 Subject: [PATCH 07/12] Add performance benchmark tools under tools/benchmark Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- tools/benchmark/Build-LocalModule.ps1 | 67 +++++++++ tools/benchmark/Invoke-ImportBenchmark.ps1 | 56 ++++++++ .../benchmark/Invoke-PerformanceBenchmark.ps1 | 131 ++++++++++++++++++ tools/benchmark/Invoke-ProfilerTrace.ps1 | 54 ++++++++ tools/benchmark/README.md | 27 ++++ 5 files changed, 335 insertions(+) create mode 100644 tools/benchmark/Build-LocalModule.ps1 create mode 100644 tools/benchmark/Invoke-ImportBenchmark.ps1 create mode 100644 tools/benchmark/Invoke-PerformanceBenchmark.ps1 create mode 100644 tools/benchmark/Invoke-ProfilerTrace.ps1 create mode 100644 tools/benchmark/README.md diff --git a/tools/benchmark/Build-LocalModule.ps1 b/tools/benchmark/Build-LocalModule.ps1 new file mode 100644 index 0000000..d168a8f --- /dev/null +++ b/tools/benchmark/Build-LocalModule.ps1 @@ -0,0 +1,67 @@ +<# + .SYNOPSIS + Assembles an importable Sodium module from the src folder for local testing and benchmarking. + + .DESCRIPTION + Mimics the PSModule build pipeline order (variables -> private functions -> public functions -> main.ps1) + and produces a Sodium.psd1/Sodium.psm1 pair with the native libs copied alongside. + + .EXAMPLE + .\Build-LocalModule.ps1 -OutputPath $env:TEMP\SodiumBench +#> +[CmdletBinding()] +param( + # Path to the module source folder. Defaults to /src. + [Parameter()] + [string] $SourcePath = (Join-Path $PSScriptRoot '..' '..' 'src'), + + # Folder in which the 'Sodium' module folder is created. + [Parameter(Mandatory)] + [string] $OutputPath +) +$ErrorActionPreference = 'Stop' + +$SourcePath = (Resolve-Path $SourcePath).Path +$moduleDir = Join-Path $OutputPath 'Sodium' +if (Test-Path $moduleDir) { + try { + Remove-Item $moduleDir -Recurse -Force + } catch { + # Native libs may be locked by a process that imported a previous build; build into a unique folder instead. + $moduleDir = Join-Path $OutputPath "Sodium-$([Guid]::NewGuid().ToString('N').Substring(0, 8))" + Write-Verbose "Previous build is locked; building into $moduleDir" -Verbose + } +} +New-Item -ItemType Directory -Path $moduleDir -Force | Out-Null + +$sb = [System.Text.StringBuilder]::new() +$publicFunctions = @() + +foreach ($file in (Get-ChildItem (Join-Path $SourcePath 'variables' 'private') -Filter *.ps1 -ErrorAction SilentlyContinue)) { + [void]$sb.AppendLine((Get-Content $file.FullName -Raw)) +} +foreach ($file in (Get-ChildItem (Join-Path $SourcePath 'functions' 'private') -Filter *.ps1)) { + [void]$sb.AppendLine((Get-Content $file.FullName -Raw)) +} +foreach ($file in (Get-ChildItem (Join-Path $SourcePath 'functions' 'public') -Filter *.ps1)) { + [void]$sb.AppendLine((Get-Content $file.FullName -Raw)) + $publicFunctions += $file.BaseName +} +$mainPath = Join-Path $SourcePath 'main.ps1' +if (Test-Path $mainPath) { + [void]$sb.AppendLine((Get-Content $mainPath -Raw)) +} +[void]$sb.AppendLine("Export-ModuleMember -Function '$($publicFunctions -join "', '")' -Alias '*'") + +Set-Content -Path (Join-Path $moduleDir 'Sodium.psm1') -Value $sb.ToString() -Encoding UTF8BOM + +Copy-Item -Path (Join-Path $SourcePath 'libs') -Destination $moduleDir -Recurse + +New-ModuleManifest -Path (Join-Path $moduleDir 'Sodium.psd1') ` + -RootModule 'Sodium.psm1' ` + -ModuleVersion '999.0.0' ` + -FunctionsToExport $publicFunctions ` + -CmdletsToExport @() -VariablesToExport @() -AliasesToExport @() + +Write-Verbose "Built module at $moduleDir" -Verbose +Join-Path $moduleDir 'Sodium.psd1' diff --git a/tools/benchmark/Invoke-ImportBenchmark.ps1 b/tools/benchmark/Invoke-ImportBenchmark.ps1 new file mode 100644 index 0000000..9eff203 --- /dev/null +++ b/tools/benchmark/Invoke-ImportBenchmark.ps1 @@ -0,0 +1,56 @@ +<# + .SYNOPSIS + Measures cold module import time in fresh PowerShell processes. + + .DESCRIPTION + Starts a new pwsh process per sample, imports the module once and reports average/min/max import time. + This is the dominant cost when the module is started in multiple sessions. + + .EXAMPLE + .\Invoke-ImportBenchmark.ps1 -ModulePath C:\temp\SodiumBench\Sodium\Sodium.psd1 -Label baseline +#> +[CmdletBinding()] +param( + # Path to the Sodium.psd1 to benchmark. Defaults to building the module from src into a temp folder. + [Parameter()] + [string] $ModulePath, + + # Label used in output file name (import-