PowerShell/test/powershell/Language/Classes/scripting.Classes.using.tests.ps1

563 lines
19 KiB
PowerShell

# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
Describe 'using module' -Tags "CI" {
BeforeAll {
$originalPSModulePath = $env:PSModulePath
function New-TestModule {
param(
[string]$Name,
[string]$Content,
[switch]$Manifest,
[version]$Version = '1.0', # ignored, if $Manifest -eq $false
[string]$ModulePathPrefix = 'modules' # module is created under TestDrive:\$ModulePathPrefix\$Name
)
if ($manifest) {
New-Item -type directory -Force "${TestDrive}\$ModulePathPrefix\$Name\$Version" > $null
Set-Content -Path "${TestDrive}\$ModulePathPrefix\$Name\$Version\$Name.psm1" -Value $Content
New-ModuleManifest -RootModule "$Name.psm1" -Path "${TestDrive}\$ModulePathPrefix\$Name\$Version\$Name.psd1" -ModuleVersion $Version
} else {
New-Item -type directory -Force "${TestDrive}\$ModulePathPrefix\$Name" > $null
Set-Content -Path "${TestDrive}\$ModulePathPrefix\$Name\$Name.psm1" -Value $Content
}
$resolvedTestDrivePath = Split-Path ((Get-ChildItem "${TestDrive}\$ModulePathPrefix")[0].FullName)
if (-not ($env:PSModulePath -like "*$resolvedTestDrivePath*")) {
$env:PSModulePath += "$([System.IO.Path]::PathSeparator)$resolvedTestDrivePath"
}
}
}
AfterAll {
$env:PSModulePath = $originalPSModulePath
}
It 'Import-Module has ImplementedAssembly, when classes are present in the module' {
# Create modules in TestDrive:\
New-TestModule -Name Foo -Content 'class Foo { [string] GetModuleName() { return "Foo" } }'
New-TestModule -Manifest -Name FooWithManifest -Content 'class Foo { [string] GetModuleName() { return "FooWithManifest" } }'
$module = Import-Module Foo -PassThru
try {
$module.ImplementingAssembly | Should -Not -BeNullOrEmpty
} finally {
$module | Remove-Module
}
}
It "can use class from another module as a base class with using module" {
$barType = [scriptblock]::Create(@"
using module Foo
class Bar : Foo {}
[Bar]
"@).Invoke()
$barType.BaseType.Name | Should -BeExactly 'Foo'
}
It "can use class from another module in New-Object" {
$foo = [scriptblock]::Create(@"
using module FooWithManifest
using module Foo
New-Object FooWithManifest.Foo
New-Object Foo.Foo
"@).Invoke()
$foo.Count | Should -Be 2
$foo[0].GetModuleName() | Should -BeExactly 'FooWithManifest'
$foo[1].GetModuleName() | Should -BeExactly 'Foo'
}
It "can use class from another module by full name as base class and [type]" {
$fooObject = [scriptblock]::Create(@"
using module Foo
class Bar : Foo.Foo {}
[Foo.Foo]::new()
"@).Invoke()
$fooObject.GetModuleName() | Should -BeExactly 'Foo'
}
It "can use modules with classes collision" {
# we use 3 classes with name Foo at the same time
# two of them come from 'using module' and one is defined in the scriptblock itself.
# we should be able to use first two of them by the module-qualified name and the third one it's name.
$fooModuleName = [scriptblock]::Create(@"
using module Foo
using module FooWithManifest
class Foo { [string] GetModuleName() { return "This" } }
class Bar1 : Foo.Foo {}
class Bar2 : FooWithManifest.Foo {}
class Bar : Foo {}
[Bar1]::new().GetModuleName() # Foo
[Bar2]::new().GetModuleName() # FooWithManifest
[Bar]::new().GetModuleName() # This
(New-Object Foo).GetModuleName() # This
"@).Invoke()
$fooModuleName.Count | Should -Be 4
$fooModuleName[0] | Should -BeExactly 'Foo'
$fooModuleName[1] | Should -BeExactly 'FooWithManifest'
$fooModuleName[2] | Should -BeExactly 'This'
$fooModuleName[3] | Should -BeExactly 'This'
}
It "doesn't mess up two consecutive scripts" {
$sb1 = [scriptblock]::Create(@"
using module Foo
class Bar : Foo {}
[Bar]::new().GetModuleName()
"@)
$sb2 = [scriptblock]::Create(@"
using module Foo
class Foo { [string] GetModuleName() { return "This" } }
class Bar : Foo {}
[Bar]::new().GetModuleName()
"@)
$sb1.Invoke() | Should -BeExactly 'Foo'
$sb2.Invoke() | Should -BeExactly 'This'
}
It "can use modules with classes collision simple" {
$fooModuleName = [scriptblock]::Create(@"
using module Foo
class Foo { [string] GetModuleName() { return "This" } }
class Bar1 : Foo.Foo {}
class Bar : Foo {}
[Foo.Foo]::new().GetModuleName() # Foo
[Bar1]::new().GetModuleName() # Foo
[Bar]::new().GetModuleName() # This
[Foo]::new().GetModuleName() # This
(New-Object Foo).GetModuleName() # This
"@).Invoke()
$fooModuleName.Count | Should -Be 5
$fooModuleName[0] | Should -BeExactly 'Foo'
$fooModuleName[1] | Should -BeExactly 'Foo'
$fooModuleName[2] | Should -BeExactly 'This'
$fooModuleName[3] | Should -BeExactly 'This'
$fooModuleName[4] | Should -BeExactly 'This'
}
It "can use class from another module as a base class with using module with manifest" {
$barType = [scriptblock]::Create(@"
using module FooWithManifest
class Bar : Foo {}
[Bar]
"@).Invoke()
$barType.BaseType.Name | Should -BeExactly 'Foo'
}
It "can instantiate class from another module" {
$foo = [scriptblock]::Create(@"
using module Foo
[Foo]::new()
"@).Invoke()
$foo.GetModuleName() | Should -BeExactly 'Foo'
}
It "cannot instantiate class from another module without using statement" {
$err = Get-RuntimeError @"
#using module Foo
[Foo]::new()
"@
$err.FullyQualifiedErrorId | Should -BeExactly 'TypeNotFound'
}
It "can use class from another module in New-Object by short name" {
$foo = [scriptblock]::Create(@"
using module FooWithManifest
New-Object Foo
"@).Invoke()
$foo.GetModuleName() | Should -BeExactly 'FooWithManifest'
}
It "can use class from this module in New-Object by short name" {
$foo = [scriptblock]::Create(@"
class Foo {}
New-Object Foo
"@).Invoke()
$foo | Should -Not -BeNullOrEmpty
}
# Pending reason:
# it's not yet implemented.
It "accept module specification" {
$foo = [scriptblock]::Create(@"
using module @{ ModuleName = 'FooWithManifest'; ModuleVersion = '1.0' }
New-Object Foo
"@).Invoke()
$foo.GetModuleName() | Should -BeExactly 'FooWithManifest'
}
Context 'parse time errors' {
It "report an error about not found module" {
$err = Get-ParseResults "using module ThisModuleDoesntExist"
$err.Count | Should -Be 1
$err[0].ErrorId | Should -BeExactly 'ModuleNotFoundDuringParse'
}
It "report an error about misformatted module specification" {
$err = Get-ParseResults "using module @{ Foo = 'Foo' }"
$err.Count | Should -Be 1
$err[0].ErrorId | Should -BeExactly 'RequiresModuleInvalid'
}
It "report an error about wildcard in the module name" {
$err = Get-ParseResults "using module fo*"
$err.Count | Should -Be 1
$err[0].ErrorId | Should -BeExactly 'WildCardModuleNameError'
}
It "report an error about wildcard in the module path" {
$err = Get-ParseResults "using module C:\fo*"
$err.Count | Should -Be 1
$err[0].ErrorId | Should -BeExactly 'WildCardModuleNameError'
}
It "report an error about wildcard in the module name inside ModuleSpecification hashtable" {
$err = Get-ParseResults "using module @{ModuleName = 'Fo*'; RequiredVersion = '1.0'}"
$err.Count | Should -Be 1
$err[0].ErrorId | Should -BeExactly 'WildCardModuleNameError'
}
# MSFT:5246105
It "report an error when tokenizer encounters comma" {
$err = Get-ParseResults "using module ,FooWithManifest"
$err.Count | Should -Be 1
$err[0].ErrorId | Should -BeExactly 'MissingUsingItemName'
}
It "report an error when tokenizer encounters nothing" {
$err = Get-ParseResults "using module "
$err.Count | Should -Be 1
$err[0].ErrorId | Should -BeExactly 'MissingUsingItemName'
}
It "report an error on badly formatted RequiredVersion" {
$err = Get-ParseResults "using module @{ModuleName = 'FooWithManifest'; RequiredVersion = 1. }"
$err.Count | Should -Be 1
$err[0].ErrorId | Should -BeExactly 'RequiresModuleInvalid'
}
# MSFT:6897275
It "report an error on incomplete using input" {
$err = Get-ParseResults "using module @{ModuleName = 'FooWithManifest'; FooWithManifest = 1." # missing closing bracket
$err.Count | Should -Be 2
$err[0].ErrorId | Should -BeExactly 'MissingEndCurlyBrace'
$err[1].ErrorId | Should -BeExactly 'RequiresModuleInvalid'
}
It "report an error when 'using module' terminating by NewLine" {
$err = Get-ParseResults "using module"
$err.Count | Should -Be 1
$err[0].ErrorId | Should -BeExactly 'MissingUsingItemName'
}
It "report an error when 'using module' terminating by Semicolon" {
$err = Get-ParseResults "using module; $testvar=1"
$err.Count | Should -Be 1
$err[0].ErrorId | Should -BeExactly 'MissingUsingItemName'
}
It "report an error when a value after 'using module' is a unallowed expression" {
$err = Get-ParseResults "using module )"
$err.Count | Should -Be 1
$err[0].ErrorId | Should -BeExactly 'InvalidValueForUsingItemName'
}
It "report an error when a value after 'using module' is not a valid module name" {
$err = Get-ParseResults "using module 123"
$err.Count | Should -Be 1
$err[0].ErrorId | Should -BeExactly 'InvalidValueForUsingItemName'
}
}
Context 'short name in case of name collision' {
It "cannot use as base class" {
$err = Get-RuntimeError @"
using module Foo
using module FooWithManifest
class Bar : Foo {}
"@
$err.FullyQualifiedErrorId | Should -Be AmbiguousTypeReference
}
It "cannot use as [...]" {
$err = Get-RuntimeError @"
using module Foo
using module FooWithManifest
[Foo]
"@
$err.FullyQualifiedErrorId | Should -BeExactly 'AmbiguousTypeReference'
}
It "cannot use in New-Object" {
$err = Get-RuntimeError @"
using module Foo
using module FooWithManifest
New-Object Foo
"@
$err.FullyQualifiedErrorId | Should -BeExactly 'AmbiguousTypeReference,Microsoft.PowerShell.Commands.NewObjectCommand'
}
It "cannot use [type] cast from string" {
$err = Get-RuntimeError @"
using module Foo
using module FooWithManifest
[type]"Foo"
"@
$err.FullyQualifiedErrorId | Should -BeExactly 'AmbiguousTypeReference'
}
}
Context 'using use the latest version of module after Import-Module -Force' {
BeforeAll {
New-TestModule -Name Foo -Content 'class Foo { [string] GetModuleName() { return "Foo2" } }'
Import-Module Foo -Force
}
It "can use class from another module as a base class with using module" {
$moduleName = [scriptblock]::Create(@"
using module Foo
[Foo]::new().GetModuleName()
"@).Invoke()
$moduleName | Should -BeExactly 'Foo2'
}
}
Context 'Side by side' {
BeforeAll {
# Add side-by-side module
$newVersion = '3.4.5'
New-TestModule -Manifest -Name FooWithManifest -Content 'class Foo { [string] GetModuleName() { return "Foo230" } }' -Version '2.3.0'
New-TestModule -Manifest -Name FooWithManifest -Content 'class Foo { [string] GetModuleName() { return "Foo345" } }' -Version '3.4.5' -ModulePathPrefix 'Modules2'
}
# 'using module' behavior must be aligned with Import-Module.
# Import-Module does the following:
# 1) find the first directory from $env:PSModulePath that contains the module
# 2) Import highest available version of the module
# In out case TestDrive:\Module is before TestDrive:\Modules2 and so 2.3.0 is the right version
It "uses the last module, if multiple versions are present" {
$foo = [scriptblock]::Create(@"
using module FooWithManifest
[Foo]::new()
"@).Invoke()
$foo.GetModuleName() | Should -BeExactly 'Foo230'
}
It "uses right version, when RequiredModule=1.0 specified" {
$foo = [scriptblock]::Create(@"
using module @{ModuleName = 'FooWithManifest'; RequiredVersion = '1.0'}
[Foo]::new()
"@).Invoke()
$foo.GetModuleName() | Should -BeExactly 'FooWithManifest'
}
It "uses right version, when RequiredModule=2.3.0 specified" {
$foo = [scriptblock]::Create(@"
using module @{ModuleName = 'FooWithManifest'; RequiredVersion = '2.3.0'}
[Foo]::new()
"@).Invoke()
$foo.GetModuleName() | Should -BeExactly 'Foo230'
}
It "uses right version, when RequiredModule=3.4.5 specified" {
$foo = [scriptblock]::Create(@"
using module @{ModuleName = 'FooWithManifest'; RequiredVersion = '3.4.5'}
[Foo]::new()
"@).Invoke()
$foo.GetModuleName() | Should -BeExactly 'Foo345'
}
}
Context 'Use module with runtime error' {
BeforeAll {
New-TestModule -Name ModuleWithRuntimeError -Content @'
class Foo { [string] GetModuleName() { return "ModuleWithRuntimeError" } }
throw 'error'
'@
}
It "handles runtime errors in imported module" {
$err = Get-RuntimeError @"
using module ModuleWithRuntimeError
[Foo]::new().GetModuleName()
"@
$err | Should -Be 'error'
}
}
Context 'shared InitialSessionState' {
It 'can pick the right module' {
$scriptToProcessPath = "${TestDrive}\toProcess.ps1"
Set-Content -Path $scriptToProcessPath -Value @'
using module Foo
function foo()
{
[Foo]::new()
}
'@
# resolve name to absolute path
$scriptToProcessPath = (Get-ChildItem $scriptToProcessPath).FullName
$iss = [System.Management.Automation.Runspaces.initialsessionstate]::CreateDefault()
$iss.StartupScripts.Add($scriptToProcessPath)
$ps = [powershell]::Create($iss)
$ps.AddCommand("foo").Invoke() | Should -BeExactly 'Foo'
$ps.Streams.Error | Should -BeNullOrEmpty
$ps1 = [powershell]::Create($iss)
$ps1.AddCommand("foo").Invoke() | Should -BeExactly 'Foo'
$ps1.Streams.Error | Should -BeNullOrEmpty
$ps.Commands.Clear()
$ps.Streams.Error.Clear()
$ps.AddScript(". foo").Invoke() | Should -BeExactly 'Foo'
$ps.Streams.Error | Should -BeNullOrEmpty
}
}
# here we are back to normal $env:PSModulePath, but all modules are there
Context "Module by path" {
BeforeAll {
# this is a setup for Context "Module by path"
New-TestModule -Name FooForPaths -Content 'class Foo { [string] GetModuleName() { return "FooForPaths" } }'
$env:PSModulePath = $originalPSModulePath
New-Item -type directory -Force TestDrive:\FooRelativeConsumer
Set-Content -Path "${TestDrive}\FooRelativeConsumer\FooRelativeConsumer.ps1" -Value @'
using module ..\modules\FooForPaths
class Bar : Foo {}
[Bar]::new()
'@
Set-Content -Path "${TestDrive}\FooRelativeConsumerErr.ps1" -Value @'
using module FooForPaths
class Bar : Foo {}
[Bar]::new()
'@
}
It 'use non-modified PSModulePath' {
$env:PSModulePath | Should -BeExactly $originalPSModulePath
}
It "can be accessed by relative path" {
$barObject = & TestDrive:\FooRelativeConsumer\FooRelativeConsumer.ps1
$barObject.GetModuleName() | Should -BeExactly 'FooForPaths'
}
It "cannot be accessed by relative path without .\ from a script" {
$err = Get-RuntimeError '& TestDrive:\FooRelativeConsumerErr.ps1'
$err.FullyQualifiedErrorId | Should -BeExactly 'ModuleNotFoundDuringParse'
}
It "can be accessed by absolute path" {
$resolvedTestDrivePath = Split-Path ((Get-ChildItem TestDrive:\modules)[0].FullName)
$s = @"
using module $resolvedTestDrivePath\FooForPaths
[Foo]::new()
"@
$err = Get-ParseResults $s
$err.Count | Should -Be 0
$barObject = [scriptblock]::Create($s).Invoke()
$barObject.GetModuleName() | Should -BeExactly 'FooForPaths'
}
It "can be accessed by absolute path with file extension" {
$resolvedTestDrivePath = Split-Path ((Get-ChildItem TestDrive:\modules)[0].FullName)
$barObject = [scriptblock]::Create(@"
using module $resolvedTestDrivePath\FooForPaths\FooForPaths.psm1
[Foo]::new()
"@).Invoke()
$barObject.GetModuleName() | Should -BeExactly 'FooForPaths'
}
It "can be accessed by relative path without file" {
# we should not be able to access .\FooForPaths without cd
$err = Get-RuntimeError @"
using module .\FooForPaths
[Foo]::new()
"@
$err.FullyQualifiedErrorId | Should -BeExactly 'ModuleNotFoundDuringParse'
Push-Location TestDrive:\modules
try {
$barObject = [scriptblock]::Create(@"
using module .\FooForPaths
[Foo]::new()
"@).Invoke()
$barObject.GetModuleName() | Should -BeExactly 'FooForPaths'
} finally {
Pop-Location
}
}
It "cannot be accessed by relative path without .\" {
Push-Location TestDrive:\modules
try {
$err = Get-RuntimeError @"
using module FooForPaths
[Foo]::new()
"@
$err.FullyQualifiedErrorId | Should -BeExactly 'ModuleNotFoundDuringParse'
} finally {
Pop-Location
}
}
It 'can be accessed by relative path with .<Separator>' -TestCases @(
@{ Separator = '\' },
@{ Separator = '/' }
) {
param([string]$Separator)
$name = 'relative-slash-paths'
'function Get-TestString { "Worked" }' | Set-Content "TestDrive:\modules\$name.psm1"
"using module .$Separator$name.psm1; Get-TestString" | Set-Content "TestDrive:\modules\$name.ps1"
& "TestDrive:\modules\$name.ps1" | Should -BeExactly "Worked"
}
}
Context "module has non-terminating error handled with 'SilentlyContinue'" {
BeforeAll {
$testFile = Join-Path -Path $TestDrive -ChildPath "testmodule.psm1"
$content = @'
Get-Command -CommandType Application -Name NonExisting -ErrorAction SilentlyContinue
class TestClass { [string] GetName() { return "TestClass" } }
'@
Set-Content -Path $testFile -Value $content -Force
}
AfterAll {
Remove-Module -Name testmodule -Force -ErrorAction SilentlyContinue
}
It "'using module' should succeed" {
$result = [scriptblock]::Create(@"
using module $testFile
[TestClass]::new()
"@).Invoke()
$result.GetName() | Should -BeExactly "TestClass"
}
}
}