test/ins.Tests.ps1

Set-StrictMode -Version 1
$ErrorActionPreference = 'Stop'

# For older WinPS versions: Set OS/edition flags (which in PSCore are automatically defined).
# !! At least with Pester v5.x, script-level variables must explicitly created with scope $script:
# !! Do NOT *refer to* these variables with $script: below, however.
if (-not (Test-Path Variable:IsWindows)) { $script:IsWindows = $true }
if (-not (Test-Path Variable:IsCoreCLR)) { $script:IsCoreCLR = $false }

# Force-(re)import this module.
# Target the *.psd1 file explicitly, so the tests can run from versioned subfolders too. Note that the
# loaded module's ModuleInfo's .Path property will reflect the *.psm1 instead.
$manifest = (Get-Item $PSScriptRoot/../*.psd1)
Remove-Module -ea Ignore -Force $manifest.BaseName # Note: To be safe, we unload any modules with the same name first (they could be in a different location and end up side by side in memory with this one.)
Import-Module $manifest -Force -Global # -Global makes sure that when psake runs tester in a child scope, the module is still imported globally.

Describe 'ins (Invoke-NativeShell) tests' {

  # Note: There's one scenario we cannot test in an automated fashion:
  # Making sure that if the invoked commands are passed *by argument*
  # that interactive prompts still work.
  # To make this work, `$Input | ...` must *not* be used for invocation in that scenario:
  # Even if there is no actual pipeline input, PowerShell will redirect stdin,
  # pipe nothing to it, so the prompt will not display and the variable will
  # receive no value.
  # Commands for interactive testing: these should prompt for a value and echo it.
  # Windows:
  # ins 'setlocal enabledelayedexpansion & set /p var="Enter a value: " & echo !var!'
  # Unix:
  # ins 'read -p "Enter a value: " var; printf ''%s\n'' "$var"'

  Context 'PlatformNeutral' {

    It 'Throws an error for a nonzero exit code with -ErrorOnFailure / -e' {      
      $cmd = 'whoami -nosuchoptions'
      # Note: Due to the bug up to 7.0 where stderr output being redirected
      # causing $ErrorActionPreference to apply , *>$null would trigger
      # a script-terminating error, given that 'Stop' is in effect.
      # Invocations via Psake invariably have this effect.
      Write-Host -ForegroundColor Green 'Note: The following 4 lines are expected.'
      { ins -e $cmd } | Should -Throw -ErrorId NativeCommandFailed
      { ins -ErrorOnFailure $cmd } | Should -Throw -ErrorId NativeCommandFailed
    }

  }

  Context 'Windows' -Skip:(-not $IsWindows) {
  
    It 'Reflects the native shell''s exit code in $LASTEXITCODE' {
      $successCommand = 'echo.'
      $failureCommand = 'echo a | findstr b'
      
      & { $null = ins $successCommand; $LASTEXITCODE } | Should -Be 0
      & { $null = ins $failureCommand; $LASTEXITCODE } | Should -Be 1
    }

    It 'Correctly invokes a cmd.exe command line via an argument' {
      ins 'echo "1"&echo 2' | Should -Be '"1"', '2'
    }
  
    It 'Correctly invokes a cmd.exe command line via stdin (pipeline)' {
      'echo "1"&echo 2' | ins | Should -Be '"1"', '2'
    }
  
    It 'Correctly invokes a cmd.exe command line via an argument and wth pass-through arguments' {
      ins 'echo %2' one foo | Should -Be foo
    }

    It 'Correctly invokes a cmd.exe command line via stdin (pipeline) and with pass-through arguments' {
      'echo %2' | ins - one foo | Should -Be foo
    }


    It 'Correctly passes pipeline (stdin) data to a cmd.exe command line' {
      'foo', 'bar' | ins 'findstr "bar" & echo ''hi''' | Should -Be 'bar', "'hi'"
      'foo', 'bar' | ins 'findstr "%1" & echo ''hi''' 'bar' | Should -Be 'bar', "'hi'"
    }

    It 'A multi-line string with line continuations works as expected.' {
      "echo a^`nb^`nc" | ins | Should -Be 'abc'
    }

    It 'A multi-line string with individual commands works as expected.' {
      $cmd = "echo one`necho two"
      $expected = 'one', 'two'
      ins $cmd | Should -Be $expected
      $cmd | ins | Should -Be $expected
    }


  }

  Context 'Unix' -Skip:$IsWindows {

    It 'Uses /bin/bash by default, /bin/sh if requested' {
    
      # Note: Hypothetically, if the system running these tests doesn't have /bin/bash,
      # the test fails due to fallback to /bin/sh.

      # Command that echoes the full executable path.
      # Note: `ps -o comm=` outputs just 'bash' on Linux, not '/bin/bash', for instance.
      # `-o args` is the whole command line, but it is POSIX-mandated, so we can rely on it.
      # We use awk to extract the first argument, which is the executable path used.
      $cmd = ' ps -p $$ -o args= | awk ''{ print $1 }'' ' 

      ins $cmd | Should -Be '/bin/bash'
      ins -UseSh $cmd | Should -Be '/bin/sh'

    }

    It 'Reflects the native shell''s exit code in $LASTEXITCODE' {
      $successCommand = 'true'
      $failureCommand = 'false'
      
      & { ins $successCommand; $LASTEXITCODE } | Should -Be 0
      & { ins -UseSh $successCommand; $LASTEXITCODE } | Should -Be 0

      & { ins $failureCommand; $LASTEXITCODE } | Should -Be 1
      & { ins -UseSh $failureCommand; $LASTEXITCODE } | Should -Be 1

    }
    
    It 'Correctly invokes a bash/sh command line via an argument' {
      $cmd = 'ls -d ~ | cat -n; echo "hi"'
      $expected = '1', $HOME, 'hi'
      -split (ins $cmd) | Should -Be $expected
      -split (ins -UseSh $cmd) | Should -Be $expected
    }

    It 'Correctly invokes a bash/sh command line via stdin (pipeline)' {
      $cmd = 'ls -d ~ | cat -n; echo "hi"'
      $expected = '1', $HOME, 'hi'
      -split ($cmd | ins) | Should -Be $expected
      -split ($cmd | ins -UseSh) | Should -Be $expected
    }

    It 'Correctly invokes a bash/sh command line via an argument and with pass-through arguments' {
      $cmd = 'ls -d "$1" | cat -n; echo "$2"'
      $cmdArgs = $HOME, 'hi'
      $expected = '1', $HOME, 'hi'
      -split (ins $cmd $cmdArgs) | Should -Be $expected
      -split (ins -UseSh $cmd $cmdArgs) | Should -Be $expected
    }

    It 'Correctly invokes a bash/sh command line via stdin (pipeline) and with pass-through arguments' {
      $cmd = 'ls -d "$1" | cat -n; echo "$2"'
      $cmdArgs = $HOME, 'hi'
      $expected = '1', $HOME, 'hi'
      -split ($cmd | ins - $cmdArgs) | Should -Be $expected
      -split ($cmd | ins -Args $cmdArgs) | Should -Be $expected
      -split ($cmd | ins -UseSh - $cmdArgs) | Should -Be $expected
      -split ($cmd | ins -UseSh -Args $cmdArgs) | Should -Be $expected
    }

    It 'Correctly passes pipeline (stdin) data to a bash/sh command line' {
      $data = 'foo', 'bar'
      $cmd = 'grep bar | cat -n'
      $cmdWithArg = 'grep "$1" | cat -n'
      $expected = '1', 'bar'

      -split ($data | ins $cmd) | Should -Be $expected
      -split ($data | ins -UseSh $cmd) | Should -Be $expected

      -split ($data | ins $cmdWithArg 'bar') | Should -Be $expected
      -split ($data | ins -UseSh $cmdWithArg 'bar') | Should -Be $expected

    }

    It 'A multi-line string with line continuations works as expected.' {
      $cmd = "printf '%s\n' a\`nb\`nc"
      $expected = 'abc'
      ins $cmd | Should -Be $expected
      ins -UseSh $cmd | Should -Be $expected
    }

    It 'A multi-line string with individual commands works as expected.' {
      $cmd = "ls -d /`necho hi"
      $expected = '/', 'hi'
      ins $cmd | Should -Be $expected
      $cmd | ins | Should -Be $expected
      ins -UseSh $cmd | Should -Be $expected
      $cmd | ins -UseSh | Should -Be $expected
    }

  }

}