tests/WorkflowCompiler-Backend.Tests.ps1
|
#Requires -Version 7 #Requires -Modules Pester BeforeAll { Import-Module (Join-Path $PSScriptRoot '../CpmfUipsPack.psd1') -Force # A stand-in compiler binary. Resolve-CpmfUipsWorkflowCompiler only checks # that the path exists; the process itself is always mocked away. $script:fakeCompiler = Join-Path ([System.IO.Path]::GetTempPath()) "CpmfUipsFakeCompiler-$(New-Guid).exe" Set-Content -LiteralPath $script:fakeCompiler -Value 'not a real binary' # Materialise the fixture props with the placeholder pointing at it. $fixture = Get-Content (Join-Path $PSScriptRoot 'fixtures/publisher/test-publisher.props') -Raw $script:propsPath = Join-Path ([System.IO.Path]::GetTempPath()) "CpmfUipsProps-$(New-Guid).props" # String.Replace, not -replace: a Windows path is full of regex metacharacters. Set-Content -LiteralPath $script:propsPath -Value $fixture.Replace('__COMPILER_PATH__', $script:fakeCompiler) } AfterAll { Remove-Item $script:fakeCompiler -Force -ErrorAction SilentlyContinue Remove-Item $script:propsPath -Force -ErrorAction SilentlyContinue } Describe 'Get-CpmfUipsPublisherProps' { It 'flattens every PropertyGroup into one lookup' { InModuleScope CpmfUipsPack -Parameters @{ p = $script:propsPath } { param($p) $props = Get-CpmfUipsPublisherProps -Path $p $props['PublisherAuthors'] | Should -Be 'Test Author' $props['PublisherPackPolicyFileType'] | Should -Be 'Default' } } It 'drops empty elements so callers can tell unset from set' { InModuleScope CpmfUipsPack -Parameters @{ p = $script:propsPath } { param($p) (Get-CpmfUipsPublisherProps -Path $p).ContainsKey('PublisherIconUrl') | Should -BeFalse } } It 'returns an empty hashtable for an empty path' { InModuleScope CpmfUipsPack { (Get-CpmfUipsPublisherProps -Path '').Count | Should -Be 0 } } It 'throws when a path is given but missing' { InModuleScope CpmfUipsPack { { Get-CpmfUipsPublisherProps -Path 'X:\nope\missing.props' } | Should -Throw '*not found*' } } } Describe 'Resolve-CpmfUipsWorkflowCompiler' { It 'runs a .dll through the dotnet host' { InModuleScope CpmfUipsPack { $dll = Join-Path ([System.IO.Path]::GetTempPath()) "CpmfUipsFake-$(New-Guid).dll" Set-Content -LiteralPath $dll -Value 'x' try { $r = Resolve-CpmfUipsWorkflowCompiler -WorkflowCompilerPath $dll -DotnetPath 'dotnet' $r.FilePath | Should -Be 'dotnet' $r.ArgumentPrefix | Should -HaveCount 1 $r.ArgumentPrefix[0] | Should -BeLike '*.dll' } finally { Remove-Item $dll -Force -ErrorAction SilentlyContinue } } } It 'runs an .exe directly with no dotnet host' { InModuleScope CpmfUipsPack -Parameters @{ exe = $script:fakeCompiler } { param($exe) $r = Resolve-CpmfUipsWorkflowCompiler -WorkflowCompilerPath $exe $r.FilePath | Should -Be $exe $r.ArgumentPrefix | Should -HaveCount 0 } } It 'falls back to the props file when no explicit path is given' { InModuleScope CpmfUipsPack -Parameters @{ exe = $script:fakeCompiler } { param($exe) $r = Resolve-CpmfUipsWorkflowCompiler -PublisherProps @{ PublisherPackWorkflowCompilerPath = $exe } $r.FilePath | Should -Be $exe } } It 'throws a message naming all three sources when nothing is configured' { InModuleScope CpmfUipsPack { $saved = $env:UIPATH_WORKFLOWCOMPILER_LOCATION Remove-Item Env:\UIPATH_WORKFLOWCOMPILER_LOCATION -ErrorAction SilentlyContinue try { { Resolve-CpmfUipsWorkflowCompiler } | Should -Throw '*UIPATH_WORKFLOWCOMPILER_LOCATION*' } finally { if ($saved) { $env:UIPATH_WORKFLOWCOMPILER_LOCATION = $saved } } } } } Describe 'Invoke-WorkflowCompilerPack argument construction' { BeforeEach { $script:tmpRoot = Join-Path ([System.IO.Path]::GetTempPath()) "CpmfUipsWcTest-$(New-Guid)" $script:tmpFeed = Join-Path ([System.IO.Path]::GetTempPath()) "CpmfUipsWcFeed-$(New-Guid)" $script:projectJson = Join-Path $script:tmpRoot 'project.json' $null = New-Item -ItemType Directory -Path $script:tmpRoot -Force Set-Content $script:projectJson '{"projectVersion": "1.0.0", "name": "TestProject"}' } AfterEach { Remove-Item $script:tmpRoot -Recurse -Force -ErrorAction SilentlyContinue Remove-Item $script:tmpFeed -Recurse -Force -ErrorAction SilentlyContinue } It 'excludes sources by default — the compiler default is the opposite' { InModuleScope CpmfUipsPack -Parameters @{ pj = $script:projectJson; feed = $script:tmpFeed; exe = $script:fakeCompiler } { param($pj, $feed, $exe) $script:args = $null Mock Invoke-NativeCommandCapture { param($FilePath, $ArgumentList, $WorkingDirectory) $script:args = $ArgumentList $outDir = $ArgumentList[$ArgumentList.IndexOf('-o') + 1] New-Item -ItemType File -Path (Join-Path $outDir 'TestProject.1.1.0.nupkg') -Force | Out-Null [pscustomobject]@{ ExitCode = 0; StdOutLines = @('{"Type":"Result","Success":true}'); StdErrLines = @() } } Invoke-PackAndStage -ProjectJson $pj -FeedPath $feed -UipcliArgs @() -UipcliExe 'unused.exe' ` -Backend workflowcompiler ` -WorkflowCompiler @{ FilePath = $exe; ArgumentPrefix = @() } ` -PackOptions @{ id = 'TestProject' } -PackSettings @{ IncludeSources = $false } | Out-Null $i = $script:args.IndexOf('--include-sources') $i | Should -BeGreaterThan -1 $script:args[$i + 1] | Should -Be 'false' } } It 'passes --include-sources true when explicitly requested' { InModuleScope CpmfUipsPack -Parameters @{ pj = $script:projectJson; feed = $script:tmpFeed; exe = $script:fakeCompiler } { param($pj, $feed, $exe) $script:args = $null Mock Invoke-NativeCommandCapture { param($FilePath, $ArgumentList, $WorkingDirectory) $script:args = $ArgumentList $outDir = $ArgumentList[$ArgumentList.IndexOf('-o') + 1] New-Item -ItemType File -Path (Join-Path $outDir 'TestProject.1.1.0.nupkg') -Force | Out-Null [pscustomobject]@{ ExitCode = 0; StdOutLines = @(); StdErrLines = @() } } Invoke-PackAndStage -ProjectJson $pj -FeedPath $feed -UipcliArgs @() -UipcliExe 'unused.exe' ` -Backend workflowcompiler ` -WorkflowCompiler @{ FilePath = $exe; ArgumentPrefix = @() } ` -PackOptions @{ id = 'TestProject' } -PackSettings @{ IncludeSources = $true } | Out-Null $script:args[$script:args.IndexOf('--include-sources') + 1] | Should -Be 'true' } } It 'sends the project directory, not project.json, and writes identity to a pack-options file' { InModuleScope CpmfUipsPack -Parameters @{ pj = $script:projectJson; feed = $script:tmpFeed; exe = $script:fakeCompiler } { param($pj, $feed, $exe) $script:args = $null $script:packJson = $null Mock Invoke-NativeCommandCapture { param($FilePath, $ArgumentList, $WorkingDirectory) $script:args = $ArgumentList # Read the options file while it still exists — it is deleted in finally. $optFile = $ArgumentList[$ArgumentList.IndexOf('--pack-options-file') + 1] $script:packJson = Get-Content -LiteralPath $optFile -Raw | ConvertFrom-Json $outDir = $ArgumentList[$ArgumentList.IndexOf('-o') + 1] New-Item -ItemType File -Path (Join-Path $outDir 'TestProject.1.1.0.nupkg') -Force | Out-Null [pscustomobject]@{ ExitCode = 0; StdOutLines = @(); StdErrLines = @() } } Invoke-PackAndStage -ProjectJson $pj -FeedPath $feed -UipcliArgs @() -UipcliExe 'unused.exe' ` -Backend workflowcompiler ` -WorkflowCompiler @{ FilePath = $exe; ArgumentPrefix = @() } ` -PackOptions @{ id = 'RPAForge.Thing'; author = 'Test Author'; tags = 'a b' projectUrl = 'https://example.invalid'; repositoryType = 'git' } ` -PackSettings @{ IncludeSources = $false } | Out-Null $script:args[$script:args.IndexOf('-p') + 1] | Should -Be (Split-Path -Parent $pj) $script:args | Should -Contain '--package-name' $script:args[$script:args.IndexOf('--package-name') + 1] | Should -Be 'RPAForge.Thing' $script:packJson.id | Should -Be 'RPAForge.Thing' $script:packJson.author | Should -Be 'Test Author' $script:packJson.projectUrl | Should -Be 'https://example.invalid' # Version is only known after the bump, and is injected there. $script:packJson.version | Should -Be '1.1.0' } } It 'omits empty identity fields rather than stamping empty strings' { InModuleScope CpmfUipsPack -Parameters @{ pj = $script:projectJson; feed = $script:tmpFeed; exe = $script:fakeCompiler } { param($pj, $feed, $exe) $script:packJson = $null Mock Invoke-NativeCommandCapture { param($FilePath, $ArgumentList, $WorkingDirectory) $optFile = $ArgumentList[$ArgumentList.IndexOf('--pack-options-file') + 1] $script:packJson = Get-Content -LiteralPath $optFile -Raw | ConvertFrom-Json $outDir = $ArgumentList[$ArgumentList.IndexOf('-o') + 1] New-Item -ItemType File -Path (Join-Path $outDir 'TestProject.1.1.0.nupkg') -Force | Out-Null [pscustomobject]@{ ExitCode = 0; StdOutLines = @(); StdErrLines = @() } } Invoke-PackAndStage -ProjectJson $pj -FeedPath $feed -UipcliArgs @() -UipcliExe 'unused.exe' ` -Backend workflowcompiler ` -WorkflowCompiler @{ FilePath = $exe; ArgumentPrefix = @() } ` -PackOptions @{ id = 'TestProject'; author = ''; iconUrl = '' } ` -PackSettings @{ IncludeSources = $false } | Out-Null $script:packJson.PSObject.Properties.Name | Should -Not -Contain 'author' $script:packJson.PSObject.Properties.Name | Should -Not -Contain 'iconUrl' } } } Describe 'Invoke-CpmfUipsPack — workflowcompiler backend' { BeforeEach { $script:tmpRoot = Join-Path ([System.IO.Path]::GetTempPath()) "CpmfUipsWcE2E-$(New-Guid)" $script:tmpFeed = Join-Path ([System.IO.Path]::GetTempPath()) "CpmfUipsWcE2EFeed-$(New-Guid)" $script:projectJson = Join-Path $script:tmpRoot 'project.json' $null = New-Item -ItemType Directory -Path $script:tmpRoot -Force Set-Content $script:projectJson '{"projectVersion": "1.0.0", "name": "TestProject"}' } AfterEach { Remove-Item $script:tmpRoot -Recurse -Force -ErrorAction SilentlyContinue Remove-Item $script:tmpFeed -Recurse -Force -ErrorAction SilentlyContinue } It 'reads identity and policy from the publisher props file' { InModuleScope CpmfUipsPack -Parameters @{ pj = $script:projectJson; feed = $script:tmpFeed; props = $script:propsPath } { param($pj, $feed, $props) $script:packJson = $null $script:args = $null Mock Test-CpmfUipsPackPrerequisites { } Mock Invoke-WithFileLock { param($LockFile, $ScriptBlock) & $ScriptBlock } Mock Get-CpmfUipsToolPaths { @{ UipcliExe = 'fake.exe'; DotnetDir = 'C:\fake' } } Mock Get-CpmfUipsGitMetadata { @{ RepositoryUrl = 'https://git.invalid/r.git'; RepositoryBranch = 'main'; RepositoryCommit = 'abc1234' } } Mock Install-CpmfUipsPackCommandLineTool { throw 'must not install for this backend' } Mock Invoke-NativeCommandCapture { param($FilePath, $ArgumentList, $WorkingDirectory) $script:args = $ArgumentList $optFile = $ArgumentList[$ArgumentList.IndexOf('--pack-options-file') + 1] $script:packJson = Get-Content -LiteralPath $optFile -Raw | ConvertFrom-Json $outDir = $ArgumentList[$ArgumentList.IndexOf('-o') + 1] New-Item -ItemType File -Path (Join-Path $outDir 'TestProject.1.1.0.nupkg') -Force | Out-Null [pscustomobject]@{ ExitCode = 0; StdOutLines = @(); StdErrLines = @() } } Invoke-CpmfUipsPack -ProjectJson $pj -FeedPath $feed -Targets net8 ` -Backend workflowcompiler -PublisherProps $props -PackageId 'CPMForge.Thing' | Out-Null $script:packJson.author | Should -Be 'Test Author' $script:packJson.tags | Should -Be 'testforge uipath rpa' $script:packJson.projectUrl | Should -Be 'https://github.com/testforge' $script:packJson.repositoryType | Should -Be 'git' # Repository specifics come from git, not from shared identity. $script:packJson.repositoryUrl | Should -Be 'https://git.invalid/r.git' $script:packJson.repositoryCommit | Should -Be 'abc1234' # PublisherPackSkipAnalyze is true in the fixture. $script:args[$script:args.IndexOf('--skip-analyze') + 1] | Should -Be 'true' $script:args[$script:args.IndexOf('--include-sources') + 1] | Should -Be 'false' } } It 'never emits copyright or licence — the compiler has no field for them' { InModuleScope CpmfUipsPack -Parameters @{ pj = $script:projectJson; feed = $script:tmpFeed; props = $script:propsPath } { param($pj, $feed, $props) $script:packJson = $null Mock Test-CpmfUipsPackPrerequisites { } Mock Invoke-WithFileLock { param($LockFile, $ScriptBlock) & $ScriptBlock } Mock Get-CpmfUipsToolPaths { @{ UipcliExe = 'fake.exe'; DotnetDir = 'C:\fake' } } Mock Get-CpmfUipsGitMetadata { @{} } Mock Invoke-NativeCommandCapture { param($FilePath, $ArgumentList, $WorkingDirectory) $optFile = $ArgumentList[$ArgumentList.IndexOf('--pack-options-file') + 1] $script:packJson = Get-Content -LiteralPath $optFile -Raw $outDir = $ArgumentList[$ArgumentList.IndexOf('-o') + 1] New-Item -ItemType File -Path (Join-Path $outDir 'TestProject.1.1.0.nupkg') -Force | Out-Null [pscustomobject]@{ ExitCode = 0; StdOutLines = @(); StdErrLines = @() } } Invoke-CpmfUipsPack -ProjectJson $pj -FeedPath $feed -Targets net8 ` -Backend workflowcompiler -PublisherProps $props | Out-Null $script:packJson | Should -Not -Match 'Copyright' $script:packJson | Should -Not -Match 'Apache' } } It 'lets an explicit parameter win over the props file' { InModuleScope CpmfUipsPack -Parameters @{ pj = $script:projectJson; feed = $script:tmpFeed; props = $script:propsPath } { param($pj, $feed, $props) $script:packJson = $null Mock Test-CpmfUipsPackPrerequisites { } Mock Invoke-WithFileLock { param($LockFile, $ScriptBlock) & $ScriptBlock } Mock Get-CpmfUipsToolPaths { @{ UipcliExe = 'fake.exe'; DotnetDir = 'C:\fake' } } Mock Get-CpmfUipsGitMetadata { @{} } Mock Invoke-NativeCommandCapture { param($FilePath, $ArgumentList, $WorkingDirectory) $optFile = $ArgumentList[$ArgumentList.IndexOf('--pack-options-file') + 1] $script:packJson = Get-Content -LiteralPath $optFile -Raw | ConvertFrom-Json $outDir = $ArgumentList[$ArgumentList.IndexOf('-o') + 1] New-Item -ItemType File -Path (Join-Path $outDir 'TestProject.1.1.0.nupkg') -Force | Out-Null [pscustomobject]@{ ExitCode = 0; StdOutLines = @(); StdErrLines = @() } } Invoke-CpmfUipsPack -ProjectJson $pj -FeedPath $feed -Targets net8 ` -Backend workflowcompiler -PublisherProps $props -PackageAuthor 'Explicit Wins' | Out-Null $script:packJson.author | Should -Be 'Explicit Wins' } } It 'rejects -MultiTfm and net6, which the single net8 compiler cannot serve' { InModuleScope CpmfUipsPack -Parameters @{ pj = $script:projectJson; feed = $script:tmpFeed; props = $script:propsPath } { param($pj, $feed, $props) Mock Test-CpmfUipsPackPrerequisites { } Mock Invoke-WithFileLock { param($LockFile, $ScriptBlock) & $ScriptBlock } { Invoke-CpmfUipsPack -ProjectJson $pj -FeedPath $feed -Backend workflowcompiler ` -PublisherProps $props -Targets net6, net8 -MultiTfm } | Should -Throw '*MultiTfm*' { Invoke-CpmfUipsPack -ProjectJson $pj -FeedPath $feed -Backend workflowcompiler ` -PublisherProps $props -Targets net6 } | Should -Throw '*net6*' } } It 'is the default backend, at the default net8 target' { InModuleScope CpmfUipsPack -Parameters @{ pj = $script:projectJson; feed = $script:tmpFeed; props = $script:propsPath } { param($pj, $feed, $props) $script:args = $null Mock Test-CpmfUipsPackPrerequisites { } Mock Invoke-WithFileLock { param($LockFile, $ScriptBlock) & $ScriptBlock } Mock Get-CpmfUipsToolPaths { @{ UipcliExe = 'fake.exe'; DotnetDir = 'C:\fake' } } Mock Get-CpmfUipsGitMetadata { @{} } Mock Invoke-UipcliPack { throw 'uipcli must not be the default backend' } Mock Invoke-NativeCommandCapture { param($FilePath, $ArgumentList, $WorkingDirectory) $script:args = $ArgumentList $outDir = $ArgumentList[$ArgumentList.IndexOf('-o') + 1] New-Item -ItemType File -Path (Join-Path $outDir 'TestProject.1.1.0.nupkg') -Force | Out-Null [pscustomobject]@{ ExitCode = 0; StdOutLines = @(); StdErrLines = @() } } # No -Backend, no -Targets: both must default. Invoke-CpmfUipsPack -ProjectJson $pj -FeedPath $feed -PublisherProps $props | Out-Null $script:args | Should -Contain 'build' $script:args[$script:args.IndexOf('--include-sources') + 1] | Should -Be 'false' Should -Invoke Get-CpmfUipsToolPaths -Times 1 -ParameterFilter { $CliVersion -notlike '23.*' } } } It 'survives a session where no native command has run yet ($LASTEXITCODE unset)' { # Set-StrictMode -Version Latest makes reading an unset $LASTEXITCODE a # terminating error, and it does not exist until a native command runs. $probe = Join-Path ([System.IO.Path]::GetTempPath()) "CpmfUipsLec-$(New-Guid).ps1" $manifest = Join-Path $PSScriptRoot '../CpmfUipsPack.psd1' @" Set-StrictMode -Version Latest Import-Module '$manifest' -Force if (Get-Variable LASTEXITCODE -Scope Global -ErrorAction SilentlyContinue) { throw 'precondition: LASTEXITCODE already set' } InModuleScope CpmfUipsPack { (Get-CpmfUipsGitMetadata -Path '$PSScriptRoot').Count } | Out-Null 'OK' "@ | Set-Content -LiteralPath $probe try { $out = & (Get-Command pwsh).Source -NoProfile -File $probe 2>&1 ($out -join "`n") | Should -Match 'OK' } finally { Remove-Item $probe -Force -ErrorAction SilentlyContinue } } It 'warns that uipcli cannot honour source-inclusion or identity settings' { InModuleScope CpmfUipsPack -Parameters @{ pj = $script:projectJson; feed = $script:tmpFeed } { param($pj, $feed) Mock Test-CpmfUipsPackPrerequisites { } Mock Invoke-WithFileLock { param($LockFile, $ScriptBlock) & $ScriptBlock } Mock Get-CpmfUipsToolPaths { @{ UipcliExe = 'fake.exe'; DotnetDir = 'C:\fake' } } Mock Install-CpmfUipsPackCommandLineTool { } Mock Invoke-UipcliPack { param($UipcliExe, $PackArgs) New-Item -ItemType File -Path (Join-Path $PackArgs[$PackArgs.IndexOf('-o') + 1] 'TestProject.1.1.0.nupkg') -Force | Out-Null return 0 } $warnings = @() Invoke-CpmfUipsPack -ProjectJson $pj -FeedPath $feed -Targets net8 ` -Backend uipcli -IncludeSources:$false -PackageId 'Ignored.Id' ` -WarningVariable warnings -WarningAction SilentlyContinue | Out-Null ($warnings -join "`n") | Should -Match 'cannot control source inclusion' ($warnings -join "`n") | Should -Match '-PackageId' } } It 'throws a clear message for analyze, which this backend does not implement' { InModuleScope CpmfUipsPack { { Invoke-CliBackend -Op analyze -Backend workflowcompiler -ProjectJson 'C:\x\project.json' } | Should -Throw '*does not support analyze*' } } } |