src/include.tests.ps1

# Copyright 2018, Adam Edwards
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

set-strictmode -version 2

$here = Split-Path -Parent $MyInvocation.MyCommand.Path

Describe "The import-script cmdlet" {
    $importCommand = "import-module -force " + (join-path $here "..\scriptclass.psd1")
    $simpleClientScriptPath = "TestDrive:\simplesclientcript.ps1"
    $parameterizedClientScriptFile = "parameterizedclientscript.ps1"
    $parameterizedClientScriptPath = join-path "TestDrive:" $parameterizedClientScriptFile
    $simpleScriptFile = "simplescript.ps1"
    $simpleScriptPath = join-path TestDrive: $simpleScriptFile
    $errorscriptFile = "errorscript.ps1"
    $errorScriptPath = join-path TestDrive: $errorscriptfile
    $includeOnceFile = 'includeonce.ps1'
    $includeOncePath = join-path "TestDrive:" $includeoncefile
    $indirectFile = "indirect.ps1"
    $indirectPath = join-path TestDrive: $indirectFile
    $subdirfile = join-path (mkdir "TestDrive:\subdir").fullname subdirfile.ps1
    $subdirfile2 = join-path (mkdir -f "TestDrive:\subdir2\subdir2a").fullname subdirfile.ps1
    $appfile = "TestDrive:\appfile.ps1"
    $app2dir = mkdir "TestDrive:\appdir" | select -expandproperty fullname

    $moduledir = (mkdir "TestDrive:\moduledir").fullname
    $modulefile = 'modulefile.psm1'
    $modulefileinclude = 'moduleincludefile.psm1'
    $modulePath = join-path $moduledir $modulefile
    $modulePathinclude = join-path $moduledir $modulefileinclude
    $modulescriptdir1name = 'src/dir1'
    $modulescriptdir1path = (mkdir (join-path $moduledir src/dir1)).fullname
    $modulescriptfile1 = 'file1cmdlets.ps1'
    $modulescriptfile1include = 'file1cmdletsincludevar.ps1'
    $modulescriptpath1 = join-path $modulescriptdir1path $modulescriptfile1
    $modulescriptpath1include = join-path $modulescriptdir1path $modulescriptfile1include
    $modulescriptdir2name = 'src/dir2'
    $modulescriptdirpath2 = (mkdir (join-path $moduledir src/dir2)).fullname
    $modulescriptfile2 = 'file2common.ps1'
    $modulescriptfile2include = 'file2commoninclude.ps1'
    $modulescriptpath2 = join-path $modulescriptdirpath2 $modulescriptfile2
    $modulescriptpath2include = join-path $modulescriptdirpath2 $modulescriptfile2include
    $moduleclientdir = (mkdir "TestDrive:\moduleclientdir").fullname
    $moduleclientFile = 'moduleclientapp.ps1'
    $moduleclientFileinclude = 'moduleclientincludeapp.ps1'
    $moduleclientPath = join-path $moduleclientdir $moduleclientfile
    $moduleclientPathinclude = join-path $moduleclientdir $moduleclientfileinclude
    $importmodulepathtestcommand = "import-module -force '$modulePath'"
    $importmodulepathincludetestcommand = "import-module -force '$modulePathinclude'"

    function remove-ext ([string] $path) {
        $ext = $path.substring($path.length - 4, 4)
        if ($ext -ne '.ps1') {
            throw "Invalid path specified to remove-ext -- missing '.ps1' extension in path '$path'"
        }

        $path.substring(0, $path.length - 4)
    }

    function run-command([string] $command) {
        powershell -noprofile -command "`$erroractionpreference = 'stop'; exit (iex '$command')" | out-null
        $lastexitcode
    }

    set-content $errorScriptPath -value @"
function incomplete {
 echo hi
"@


    set-content $simpleclientscriptpath -value @"
$importCommand
`$scriptname = '$simplescriptfile'.split('.')[0]
# . `$include $(remove-ext $simplescriptfile)
. `$include `$scriptname
# . (import-script '$(remove-ext $simplescriptfile)')
"@


    set-content $parameterizedClientScriptPath -value @"
param([string] `$fileToInclude, [string] `$exprtoeval = '0')
$importCommand
`$filename = `$filetoinclude.split('.')[0]
. `$include `$filename
iex `$exprtoeval
"@

    set-content $simpleScriptPath -value @"
`$simplescriptvar = 2371
function simplescriptfunc { `$simplescriptvar }
set-alias set-aliassimple simplescriptfunc
"@


    set-content $includeOncePath -value @"
`$alreadyDefined = try {
    get-variable includeonce >* $null
    `$true
} catch {
    `$false
}
if (`$alreadyDefined) {
   throw `"Already defined!`"
}
"@


    set-content $indirectPath -value @"
$importCommand
. `$include $(remove-ext $includeonceFile)
. '$parameterizedClientScriptPath' '$includeOnceFile'
"@


    set-content $modulePath -value @"
$importCommand
`$modfile = (join-path '$modulescriptdir1name' '$(remove-ext $modulescriptfile1)')
. (join-path `$psscriptroot `$modfile)
"@


    set-content $modulePathinclude -value @"
$importCommand
`$modfile = (join-path '$modulescriptdir1name' '$(remove-ext $modulescriptfile1include)')
. (join-path `$psscriptroot `$modfile)
"@



    set-content $modulescriptpath1 -value @"
. (import-script (join-path '../dir2' $(remove-ext $modulescriptfile2)))
function testvalue(`$arg1, `$arg2) {
    (constantval) + `$arg1 * `$arg2
}
"@


set-content $modulescriptpath1include -value @"
`$scriptval = 'hi2'
`$scriptval = '$(join-path ../dir2 (remove-ext $modulescriptfile2include))'

# The use of `$include fails for unknown reasons in the context of this
# test, possibly due to scope issues in the way the script block is
# invoked. A workaround is to use import-script, which is validated
# in a separate test. Any tests using this capability must be marked
# pending until the issue is investigated and fixed.

# . `$include `$scriptval
# The equivalent workaround below works just fine in place of the above line
. (import-script `$scriptval)
function testvalue(`$arg1, `$arg2) {
    (constantval) + `$arg1 * `$arg2
}
"@


    set-content $modulescriptpath2 -value @"
function constantval {37 }
"@


    set-content $modulescriptpath2include -value @"
function constantval {37 }
"@


    set-content $moduleclientpath -value @"
param(`$arg1, `$arg2)
$importmodulepathtestcommand
testvalue `$arg1 `$arg2
"@


    set-content $moduleclientpathinclude -value @"
param(`$arg1, `$arg2)
$importmodulepathincludetestcommand
testvalue `$arg1 `$arg2
"@



    Context 'When loading source into a script' {
        It 'should load the valid script file without throwing an exception' {
            { iex $simpleclientscriptpath | out-null } | Should Not Throw
            run-command ("& " + (gi $simpleclientscriptpath).fullname) | Should BeExactly 0
        }

        It 'should throw an exception if the include path does not exist' {
            { import-script 'thisdoesnotexist.io' 2>&1 | out-null } | Should Throw
        }

        It 'should throw an exception when loading a script file with an error' {
            { run-command ". `$include $(remove-ext $errorscriptfile)" } | Should Not Throw
            run-command ". `$include $(remove-ext $errorscriptfile)" | Should Not Be 0
        }

        It 'should process the file only once even if it is included in a script more than once' {
            { iex "& '$parameterizedClientScriptPath' '$includeOnceFile'" } | Should Not Throw
        }

        It 'should process the file only once even if it is included in a script more than once through an indirect inclusion' {
            { iex "& '$parameterizedClientScriptPath' '$indirectFile'" } | Should Not Throw
        }

        It 'should treat include paths as relative to the calling module using import-script' {
            run-command "& $moduleClientPath 5 3 " | Should Be (5 * 3 + 37)
        }

        It 'should treat include paths as relative to the calling module using the $include variable' -pending {
            run-command "& $moduleClientPathinclude 5 3 " | Should Be (5 * 3 + 37)
        }

        It 'should throw an exception if the include path starts with a pathsep' {
            { iex "& '$parameterizedClientScriptPath' '/$includeOnceFile'" } | Should Throw "Path specified to include-source '/$(remove-ext $includeOnceFile)' started with a path separator which is not allowed -- only relative paths may be specified"
            { iex "& '$parameterizedClientScriptPath' '\$includeOnceFile'" } | Should Throw "Path specified to include-source '\$(remove-ext $includeOnceFile)' started with a path separator which is not allowed -- only relative paths may be specified"
        }
    }
}