diff --git a/src/System.Management.Automation/engine/ExecutionContext.cs b/src/System.Management.Automation/engine/ExecutionContext.cs index 544ab05983a..72ea7fcb6d8 100644 --- a/src/System.Management.Automation/engine/ExecutionContext.cs +++ b/src/System.Management.Automation/engine/ExecutionContext.cs @@ -1322,71 +1322,71 @@ internal void RemoveAssembly(string name) [SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Reflection.Assembly.LoadFrom")] internal static Assembly LoadAssembly(string name, string filename, out Exception error) { - // First we try to load the assembly based on the filename - - Assembly loadedAssembly = null; - error = null; - - if (!String.IsNullOrEmpty(filename)) - { - try - { - loadedAssembly = Assembly.LoadFrom(filename); - } - catch (FileNotFoundException fileNotFound) - { - error = fileNotFound; - } - catch (FileLoadException fileLoadException) - { - error = fileLoadException; - } - catch (BadImageFormatException badImage) - { - error = badImage; - } - catch (SecurityException securityException) - { - error = securityException; - } - } - else if (!String.IsNullOrEmpty(name)) - { - string fixedName = null; - // Remove the '.dll' if it's there... - fixedName = name.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) - ? Path.GetFileNameWithoutExtension(name) - : name; - - var assemblyString = Utils.IsPowerShellAssembly(fixedName) - ? Utils.GetPowerShellAssemblyStrongName(fixedName) - : fixedName; - - try - { - loadedAssembly = Assembly.Load(new AssemblyName(assemblyString)); - } - catch (FileNotFoundException fileNotFound) - { - error = fileNotFound; - } - catch (FileLoadException fileLoadException) - { - error = fileLoadException; - // this is a legitimate error on CoreCLR for a newly emited with Add-Type assemblies - // they cannot be loaded by name, but we are only interested in importing them by path - } - catch (BadImageFormatException badImage) - { - error = badImage; - } - catch (SecurityException securityException) - { - error = securityException; - } - } - - // We either return the loaded Assembly, or return null. + // First we try to load the assembly based on the filename + + Assembly loadedAssembly = null; + error = null; + + if (!String.IsNullOrEmpty(filename)) + { + try + { + loadedAssembly = Assembly.LoadFrom(filename); + } + catch (FileNotFoundException fileNotFound) + { + error = fileNotFound; + } + catch (FileLoadException fileLoadException) + { + error = fileLoadException; + } + catch (BadImageFormatException badImage) + { + error = badImage; + } + catch (SecurityException securityException) + { + error = securityException; + } + } + else if (!String.IsNullOrEmpty(name)) + { + string fixedName = null; + // Remove the '.dll' if it's there... + fixedName = name.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) + ? Path.GetFileNameWithoutExtension(name) + : name; + + var assemblyString = Utils.IsPowerShellAssembly(fixedName) + ? Utils.GetPowerShellAssemblyStrongName(fixedName) + : fixedName; + + try + { + loadedAssembly = Assembly.Load(new AssemblyName(assemblyString)); + } + catch (FileNotFoundException fileNotFound) + { + error = fileNotFound; + } + catch (FileLoadException fileLoadException) + { + error = fileLoadException; + // this is a legitimate error on CoreCLR for a newly emited with Add-Type assemblies + // they cannot be loaded by name, but we are only interested in importing them by path + } + catch (BadImageFormatException badImage) + { + error = badImage; + } + catch (SecurityException securityException) + { + error = securityException; + } + } + + // We either return the loaded Assembly, or return null. return loadedAssembly; } diff --git a/src/libpsl-native/src/createprocess.cpp b/src/libpsl-native/src/createprocess.cpp index 5991435c4be..b0cc2e81523 100644 --- a/src/libpsl-native/src/createprocess.cpp +++ b/src/libpsl-native/src/createprocess.cpp @@ -1,217 +1,217 @@ -#include "createprocess.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -enum -{ - SUPPRESS_PROCESS_SIGINT = 0x00000001 -}; - -enum -{ - READ_END_OF_PIPE = 0, - WRITE_END_OF_PIPE = 1 -}; - -static void CloseIfOpen(int fd) -{ - if (fd >= 0) - { - close(fd); // Ignoring errors from close is a deliberate choice - } -} - -// Checks if the IO operation was interrupted and needs to be retried. -// Returns true if the operation was interrupted; otherwise, false. -template -static inline bool CheckInterrupted(TInt result) -{ - return result < 0 && errno == EINTR; -} - -static int Dup2WithInterruptedRetry(int oldfd, int newfd) -{ - int result; - while (CheckInterrupted(result = dup2(oldfd, newfd))); - return result; -} - -int32_t SystemNative_Pipe(int32_t pipeFds[2], int32_t flags) -{ - int32_t result; - while (CheckInterrupted(result = pipe(pipeFds))); - - // Then, if O_CLOEXEC was specified, use fcntl to configure the file descriptors appropriately. - if ((flags & O_CLOEXEC) != 0 && result == 0) - { - while (CheckInterrupted(result = fcntl(pipeFds[0], F_SETFD, FD_CLOEXEC))); - if (result == 0) - { - while (CheckInterrupted(result = fcntl(pipeFds[1], F_SETFD, FD_CLOEXEC))); - } - - if (result != 0) - { - int tmpErrno = errno; - close(pipeFds[0]); - close(pipeFds[1]); - errno = tmpErrno; - } - } - - return result; -} - -int32_t ForkAndExecProcess( - const char* filename, - char* const argv[], - char* const envp[], - const char* cwd, - int32_t redirectStdin, - int32_t redirectStdout, - int32_t redirectStderr, - int32_t creationFlags, - int32_t* childPid, - int32_t* stdinFd, - int32_t* stdoutFd, - int32_t* stderrFd) -{ - int success = true; - int processId = -1; - int stdinFds[2] = { -1, -1 }; - int stdoutFds[2] = { -1, -1 }; - int stderrFds[2] = { -1, -1 }; - - // Validate arguments - if (nullptr == filename || nullptr == argv || nullptr == envp || nullptr == stdinFd || nullptr == stdoutFd || - nullptr == stderrFd || nullptr == childPid) - { - assert(false && "null argument."); - errno = EINVAL; - success = false; - goto done; - } - - if ((redirectStdin & ~1) != 0 || (redirectStdout & ~1) != 0 || (redirectStderr & ~1) != 0) - { - assert(false && "Boolean redirect* inputs must be 0 or 1."); - errno = EINVAL; - success = false; - goto done; - } - - // Make sure we can find and access the executable. exec will do this, of course, but at that point it's already - // in the child process, at which point it'll translate to the child process' exit code rather than to failing - // the Start itself. There's a race condition here, in that this could change prior to exec's checks, but there's - // little we can do about that. There are also more rigorous checks exec does, such as validating the executable - // format of the target; such errors will emerge via the child process' exit code. - if (access(filename, X_OK) != 0) - { - success = false; - goto done; - } - - // Open pipes for any requests to redirect stdin/stdout/stderr - if ((redirectStdin && SystemNative_Pipe(stdinFds, O_CLOEXEC) != 0) || - (redirectStdout && SystemNative_Pipe(stdoutFds, O_CLOEXEC) != 0) || - (redirectStderr && SystemNative_Pipe(stderrFds, O_CLOEXEC) != 0)) - { - success = false; - goto done; - } - - // Fork the child process - if ((processId = fork()) == -1) - { - success = false; - goto done; - } - - if (processId == 0) // processId == 0 if this is child process - { - // For any redirections that should happen, dup the pipe descriptors onto stdin/out/err. - // We don't explicitly close out the old pipe descriptors because they are set to close on execve. - if ((redirectStdin && Dup2WithInterruptedRetry(stdinFds[READ_END_OF_PIPE], STDIN_FILENO) == -1) || - (redirectStdout && Dup2WithInterruptedRetry(stdoutFds[WRITE_END_OF_PIPE], STDOUT_FILENO) == -1) || - (redirectStderr && Dup2WithInterruptedRetry(stderrFds[WRITE_END_OF_PIPE], STDERR_FILENO) == -1)) - { - _exit(errno != 0 ? errno : EXIT_FAILURE); - } - - // Change to the designated working directory, if one was specified - if (nullptr != cwd) - { - int result; - while (CheckInterrupted(result = chdir(cwd))); - if (result == -1) - { - _exit(errno != 0 ? errno : EXIT_FAILURE); - } - } - - // If SUPPRESS_PROCESS_SIGINT was chosen then create a process that ignores - // interrupt signals - if (creationFlags & SUPPRESS_PROCESS_SIGINT) - { - struct sigaction sa, saOld; - memset(&sa, 0, sizeof(sa)); - memset(&saOld, 0, sizeof(saOld)); - sigemptyset(&(sa.sa_mask)); - sa.sa_handler = SIG_IGN; // Ignore the signal - - int result = sigaction(SIGINT, &sa, &saOld); - if (result == -1) - { - _exit(errno != 0 ? errno : EXIT_FAILURE); - } - } - - // Finally, execute the new process. execve will not return if it's successful. - execve(filename, argv, envp); - _exit(errno != 0 ? errno : EXIT_FAILURE); // execve failed - } - - // This is the parent process. processId == pid of the child - *childPid = processId; - *stdinFd = stdinFds[WRITE_END_OF_PIPE]; - *stdoutFd = stdoutFds[READ_END_OF_PIPE]; - *stderrFd = stderrFds[READ_END_OF_PIPE]; - -done: - int priorErrno = errno; - - // Regardless of success or failure, close the parent's copy of the child's end of - // any opened pipes. The parent doesn't need them anymore. - CloseIfOpen(stdinFds[READ_END_OF_PIPE]); - CloseIfOpen(stdoutFds[WRITE_END_OF_PIPE]); - CloseIfOpen(stderrFds[WRITE_END_OF_PIPE]); - - // If we failed, close everything else and give back error values in all out arguments. - if (!success) - { - CloseIfOpen(stdinFds[WRITE_END_OF_PIPE]); - CloseIfOpen(stdoutFds[READ_END_OF_PIPE]); - CloseIfOpen(stderrFds[READ_END_OF_PIPE]); - - *stdinFd = -1; - *stdoutFd = -1; - *stderrFd = -1; - *childPid = -1; - - errno = priorErrno; - return -1; - } - - return 0; -} +#include "createprocess.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +enum +{ + SUPPRESS_PROCESS_SIGINT = 0x00000001 +}; + +enum +{ + READ_END_OF_PIPE = 0, + WRITE_END_OF_PIPE = 1 +}; + +static void CloseIfOpen(int fd) +{ + if (fd >= 0) + { + close(fd); // Ignoring errors from close is a deliberate choice + } +} + +// Checks if the IO operation was interrupted and needs to be retried. +// Returns true if the operation was interrupted; otherwise, false. +template +static inline bool CheckInterrupted(TInt result) +{ + return result < 0 && errno == EINTR; +} + +static int Dup2WithInterruptedRetry(int oldfd, int newfd) +{ + int result; + while (CheckInterrupted(result = dup2(oldfd, newfd))); + return result; +} + +int32_t SystemNative_Pipe(int32_t pipeFds[2], int32_t flags) +{ + int32_t result; + while (CheckInterrupted(result = pipe(pipeFds))); + + // Then, if O_CLOEXEC was specified, use fcntl to configure the file descriptors appropriately. + if ((flags & O_CLOEXEC) != 0 && result == 0) + { + while (CheckInterrupted(result = fcntl(pipeFds[0], F_SETFD, FD_CLOEXEC))); + if (result == 0) + { + while (CheckInterrupted(result = fcntl(pipeFds[1], F_SETFD, FD_CLOEXEC))); + } + + if (result != 0) + { + int tmpErrno = errno; + close(pipeFds[0]); + close(pipeFds[1]); + errno = tmpErrno; + } + } + + return result; +} + +int32_t ForkAndExecProcess( + const char* filename, + char* const argv[], + char* const envp[], + const char* cwd, + int32_t redirectStdin, + int32_t redirectStdout, + int32_t redirectStderr, + int32_t creationFlags, + int32_t* childPid, + int32_t* stdinFd, + int32_t* stdoutFd, + int32_t* stderrFd) +{ + int success = true; + int processId = -1; + int stdinFds[2] = { -1, -1 }; + int stdoutFds[2] = { -1, -1 }; + int stderrFds[2] = { -1, -1 }; + + // Validate arguments + if (nullptr == filename || nullptr == argv || nullptr == envp || nullptr == stdinFd || nullptr == stdoutFd || + nullptr == stderrFd || nullptr == childPid) + { + assert(false && "null argument."); + errno = EINVAL; + success = false; + goto done; + } + + if ((redirectStdin & ~1) != 0 || (redirectStdout & ~1) != 0 || (redirectStderr & ~1) != 0) + { + assert(false && "Boolean redirect* inputs must be 0 or 1."); + errno = EINVAL; + success = false; + goto done; + } + + // Make sure we can find and access the executable. exec will do this, of course, but at that point it's already + // in the child process, at which point it'll translate to the child process' exit code rather than to failing + // the Start itself. There's a race condition here, in that this could change prior to exec's checks, but there's + // little we can do about that. There are also more rigorous checks exec does, such as validating the executable + // format of the target; such errors will emerge via the child process' exit code. + if (access(filename, X_OK) != 0) + { + success = false; + goto done; + } + + // Open pipes for any requests to redirect stdin/stdout/stderr + if ((redirectStdin && SystemNative_Pipe(stdinFds, O_CLOEXEC) != 0) || + (redirectStdout && SystemNative_Pipe(stdoutFds, O_CLOEXEC) != 0) || + (redirectStderr && SystemNative_Pipe(stderrFds, O_CLOEXEC) != 0)) + { + success = false; + goto done; + } + + // Fork the child process + if ((processId = fork()) == -1) + { + success = false; + goto done; + } + + if (processId == 0) // processId == 0 if this is child process + { + // For any redirections that should happen, dup the pipe descriptors onto stdin/out/err. + // We don't explicitly close out the old pipe descriptors because they are set to close on execve. + if ((redirectStdin && Dup2WithInterruptedRetry(stdinFds[READ_END_OF_PIPE], STDIN_FILENO) == -1) || + (redirectStdout && Dup2WithInterruptedRetry(stdoutFds[WRITE_END_OF_PIPE], STDOUT_FILENO) == -1) || + (redirectStderr && Dup2WithInterruptedRetry(stderrFds[WRITE_END_OF_PIPE], STDERR_FILENO) == -1)) + { + _exit(errno != 0 ? errno : EXIT_FAILURE); + } + + // Change to the designated working directory, if one was specified + if (nullptr != cwd) + { + int result; + while (CheckInterrupted(result = chdir(cwd))); + if (result == -1) + { + _exit(errno != 0 ? errno : EXIT_FAILURE); + } + } + + // If SUPPRESS_PROCESS_SIGINT was chosen then create a process that ignores + // interrupt signals + if (creationFlags & SUPPRESS_PROCESS_SIGINT) + { + struct sigaction sa, saOld; + memset(&sa, 0, sizeof(sa)); + memset(&saOld, 0, sizeof(saOld)); + sigemptyset(&(sa.sa_mask)); + sa.sa_handler = SIG_IGN; // Ignore the signal + + int result = sigaction(SIGINT, &sa, &saOld); + if (result == -1) + { + _exit(errno != 0 ? errno : EXIT_FAILURE); + } + } + + // Finally, execute the new process. execve will not return if it's successful. + execve(filename, argv, envp); + _exit(errno != 0 ? errno : EXIT_FAILURE); // execve failed + } + + // This is the parent process. processId == pid of the child + *childPid = processId; + *stdinFd = stdinFds[WRITE_END_OF_PIPE]; + *stdoutFd = stdoutFds[READ_END_OF_PIPE]; + *stderrFd = stderrFds[READ_END_OF_PIPE]; + +done: + int priorErrno = errno; + + // Regardless of success or failure, close the parent's copy of the child's end of + // any opened pipes. The parent doesn't need them anymore. + CloseIfOpen(stdinFds[READ_END_OF_PIPE]); + CloseIfOpen(stdoutFds[WRITE_END_OF_PIPE]); + CloseIfOpen(stderrFds[WRITE_END_OF_PIPE]); + + // If we failed, close everything else and give back error values in all out arguments. + if (!success) + { + CloseIfOpen(stdinFds[WRITE_END_OF_PIPE]); + CloseIfOpen(stdoutFds[READ_END_OF_PIPE]); + CloseIfOpen(stderrFds[READ_END_OF_PIPE]); + + *stdinFd = -1; + *stdoutFd = -1; + *stderrFd = -1; + *childPid = -1; + + errno = priorErrno; + return -1; + } + + return 0; +} diff --git a/src/libpsl-native/src/createprocess.h b/src/libpsl-native/src/createprocess.h index fc865d99a8a..9755d8cc48f 100644 --- a/src/libpsl-native/src/createprocess.h +++ b/src/libpsl-native/src/createprocess.h @@ -1,22 +1,22 @@ -#pragma once - -#include "pal.h" -#include - -PAL_BEGIN_EXTERNC - -int32_t ForkAndExecProcess( - const char* filename, // filename argument to execve - char* const argv[], // argv argument to execve - char* const envp[], // envp argument to execve - const char* cwd, // path passed to chdir in child process - int32_t redirectStdin, // whether to redirect standard input from the parent - int32_t redirectStdout, // whether to redirect standard output to the parent - int32_t redirectStderr, // whether to redirect standard error to the parent - int32_t creationFlags, // creation flags - int32_t* childPid, // [out] the child process' id - int32_t* stdinFd, // [out] if redirectStdin, the parent's fd for the child's stdin - int32_t* stdoutFd, // [out] if redirectStdout, the parent's fd for the child's stdout - int32_t* stderrFd); // [out] if redirectStderr, the parent's fd for the child's stderr - -PAL_END_EXTERNC +#pragma once + +#include "pal.h" +#include + +PAL_BEGIN_EXTERNC + +int32_t ForkAndExecProcess( + const char* filename, // filename argument to execve + char* const argv[], // argv argument to execve + char* const envp[], // envp argument to execve + const char* cwd, // path passed to chdir in child process + int32_t redirectStdin, // whether to redirect standard input from the parent + int32_t redirectStdout, // whether to redirect standard output to the parent + int32_t redirectStderr, // whether to redirect standard error to the parent + int32_t creationFlags, // creation flags + int32_t* childPid, // [out] the child process' id + int32_t* stdinFd, // [out] if redirectStdin, the parent's fd for the child's stdin + int32_t* stdoutFd, // [out] if redirectStdout, the parent's fd for the child's stdout + int32_t* stderrFd); // [out] if redirectStderr, the parent's fd for the child's stderr + +PAL_END_EXTERNC diff --git a/test/powershell/Language/Classes/MSFT_778492.psm1 b/test/powershell/Language/Classes/MSFT_778492.psm1 index 13d00162c18..f54aefda072 100644 --- a/test/powershell/Language/Classes/MSFT_778492.psm1 +++ b/test/powershell/Language/Classes/MSFT_778492.psm1 @@ -1,15 +1,15 @@ - -$foo = 'MSFT_778492 script scope' - -class MSFT_778492 -{ - [string] F() - { - return $script:foo - } -} - -function Get-MSFT_778492 -{ - [MSFT_778492]::new() -} + +$foo = 'MSFT_778492 script scope' + +class MSFT_778492 +{ + [string] F() + { + return $script:foo + } +} + +function Get-MSFT_778492 +{ + [MSFT_778492]::new() +} diff --git a/test/powershell/Language/Classes/ProtectedAccess.Tests.ps1 b/test/powershell/Language/Classes/ProtectedAccess.Tests.ps1 index e9b7be2fb43..d10e5a2ed44 100644 --- a/test/powershell/Language/Classes/ProtectedAccess.Tests.ps1 +++ b/test/powershell/Language/Classes/ProtectedAccess.Tests.ps1 @@ -1,188 +1,188 @@ - -Add-Type -WarningAction Ignore @' -public class Base -{ - private int data; - - protected Base() - { - data = 10; - } - - protected Base(int i) - { - data = i; - } - - protected int Field; - protected int Property { get; set; } - public int Property1 { get; protected set; } - public int Property2 { protected get; set; } - - protected int Method() - { - return 32 + data; - } - protected int OverloadedMethod1(int i) - { - return 32 + i + data; - } - protected int OverloadedMethod1(string i) - { - return 1 + data; - } - public int OverloadedMethod2(int i) - { - return 32 + i + data; - } - protected int OverloadedMethod2(string i) - { - return 1 + data; - } - protected int OverloadedMethod3(int i) - { - return 32 + i + data; - } - public int OverloadedMethod3(string i) - { - return 1 + data; - } -} -'@ - -$derived1,$derived2,$derived3 = Invoke-Expression @' -class Derived : Base -{ - Derived() : Base() {} - Derived([int] $i) : Base($i) {} - - [int] TestPropertyAccess() - { - $this.Property = 1111 - return $this.Property - } - - [int] TestPropertyAccess1() - { - $this.Property1 = 2111 - return $this.Property1 - } - - [int] TestPropertyAccess2() - { - $this.Property2 = 3111 - return $this.Property2 - } - - [int] TestDynamicPropertyAccess() - { - $p = 'Property' - $this.$p = 1112 - return $this.$p - } - - [int] TestFieldAccess() - { - $this.Field = 11 - return $this.Field - } - - [int] TestDynamicFieldAccess() - { - $f = 'Field' - $this.$f = 12 - return $this.$f - } - - [int] TestMethodAccess() - { - return $this.Method() - } - - [int] TestDynamicMethodAccess() - { - $m = 'Method' - return $this.$m() - } - - [int] TestOverloadedMethodAccess1a() - { - return $this.OverloadedMethod1(42) - } - [int] TestOverloadedMethodAccess1b() - { - return $this.OverloadedMethod1("abc") - } - [int] TestOverloadedMethodAccess2a() - { - return $this.OverloadedMethod2(42) - } - [int] TestOverloadedMethodAccess2b() - { - return $this.OverloadedMethod2("abc") - } - [int] TestOverloadedMethodAccess3a() - { - return $this.OverloadedMethod3(42) - } - [int] TestOverloadedMethodAccess3b() - { - return $this.OverloadedMethod3("abc") - } -} - -class Derived2 : Base {} - -[Derived]::new() -[Derived]::new(20) -[Derived2]::new() -'@ - -Describe "Protected Member Access - w/ default ctor" -Tags "CI" { - It "Method Access" { $derived1.TestMethodAccess() | Should Be 42 } - It "Dynamic Method Access" { $derived1.TestDynamicMethodAccess() | Should Be 42 } - It "Field Access" { $derived1.TestFieldAccess() | Should Be 11 } - It "Dynamic Field Access" { $derived1.TestDynamicFieldAccess() | Should Be 12 } - It "Property Access - protected get/protected set" { $derived1.TestPropertyAccess() | Should Be 1111 } - It "Property Access - public get/protected set " { $derived1.TestPropertyAccess1() | Should Be 2111 } - It "Property Access - protected get/public set" { $derived1.TestPropertyAccess2() | Should Be 3111 } - It "Dynamic Property Access" { $derived1.TestDynamicPropertyAccess() | Should Be 1112 } - - It "Method Access - overloaded 1a" { $derived1.TestOverloadedMethodAccess1a() | Should Be 84 } - It "Method Access - overloaded 1b" { $derived1.TestOverloadedMethodAccess1b() | Should Be 11 } - It "Method Access - overloaded 2a" { $derived1.TestOverloadedMethodAccess2a() | Should Be 84 } - It "Method Access - overloaded 2b" { $derived1.TestOverloadedMethodAccess2b() | Should Be 11 } - It "Method Access - overloaded 3a" { $derived1.TestOverloadedMethodAccess3a() | Should Be 84 } - It "Method Access - overloaded 3b" { $derived1.TestOverloadedMethodAccess3b() | Should Be 11 } - It "Implicit ctor calls protected ctor" { $derived3.OverloadedMethod2(42) | Should Be 84 } -} - -Describe "Protected Member Access - w/ non-default ctor" -Tags "CI" { - It "Method Access" { $derived2.TestMethodAccess() | Should Be 52 } - It "Dynamic Method Access" { $derived2.TestDynamicMethodAccess() | Should Be 52 } - It "Field Access" { $derived2.TestFieldAccess() | Should Be 11 } - It "Dynamic Field Access" { $derived2.TestDynamicFieldAccess() | Should Be 12 } - It "Property Access - protected get/protected set" { $derived1.TestPropertyAccess() | Should Be 1111 } - It "Property Access - public get/protected set " { $derived1.TestPropertyAccess1() | Should Be 2111 } - It "Property Access - protected get/public set" { $derived1.TestPropertyAccess2() | Should Be 3111 } - It "Dynamic Property Access" { $derived2.TestDynamicPropertyAccess() | Should Be 1112 } - - It "Method Access - overloaded 1a" { $derived2.TestOverloadedMethodAccess1a() | Should Be 94 } - It "Method Access - overloaded 1b" { $derived2.TestOverloadedMethodAccess1b() | Should Be 21 } - It "Method Access - overloaded 2a" { $derived2.TestOverloadedMethodAccess2a() | Should Be 94 } - It "Method Access - overloaded 2b" { $derived2.TestOverloadedMethodAccess2b() | Should Be 21 } - It "Method Access - overloaded 3a" { $derived2.TestOverloadedMethodAccess3a() | Should Be 94 } - It "Method Access - overloaded 3b" { $derived2.TestOverloadedMethodAccess3b() | Should Be 21 } -} - -Describe "Protected Member Access - members not visible outside class" -Tags "CI" { - Set-StrictMode -v 3 - It "Invalid protected field Get Access" { { $derived1.Field } | Should Throw } - It "Invalid protected property Get Access" { { $derived1.Property } | Should Throw } - It "Invalid protected field Set Access" { { $derived1.Field = 1 } | Should Throw } - It "Invalid protected property Set Access" { { $derived1.Property = 1 } | Should Throw } - - It "Invalid protected constructor Access" { { [Base]::new() } | Should Throw } - It "Invalid protected method Access" { { $derived1.Method() } | Should Throw } -} - + +Add-Type -WarningAction Ignore @' +public class Base +{ + private int data; + + protected Base() + { + data = 10; + } + + protected Base(int i) + { + data = i; + } + + protected int Field; + protected int Property { get; set; } + public int Property1 { get; protected set; } + public int Property2 { protected get; set; } + + protected int Method() + { + return 32 + data; + } + protected int OverloadedMethod1(int i) + { + return 32 + i + data; + } + protected int OverloadedMethod1(string i) + { + return 1 + data; + } + public int OverloadedMethod2(int i) + { + return 32 + i + data; + } + protected int OverloadedMethod2(string i) + { + return 1 + data; + } + protected int OverloadedMethod3(int i) + { + return 32 + i + data; + } + public int OverloadedMethod3(string i) + { + return 1 + data; + } +} +'@ + +$derived1,$derived2,$derived3 = Invoke-Expression @' +class Derived : Base +{ + Derived() : Base() {} + Derived([int] $i) : Base($i) {} + + [int] TestPropertyAccess() + { + $this.Property = 1111 + return $this.Property + } + + [int] TestPropertyAccess1() + { + $this.Property1 = 2111 + return $this.Property1 + } + + [int] TestPropertyAccess2() + { + $this.Property2 = 3111 + return $this.Property2 + } + + [int] TestDynamicPropertyAccess() + { + $p = 'Property' + $this.$p = 1112 + return $this.$p + } + + [int] TestFieldAccess() + { + $this.Field = 11 + return $this.Field + } + + [int] TestDynamicFieldAccess() + { + $f = 'Field' + $this.$f = 12 + return $this.$f + } + + [int] TestMethodAccess() + { + return $this.Method() + } + + [int] TestDynamicMethodAccess() + { + $m = 'Method' + return $this.$m() + } + + [int] TestOverloadedMethodAccess1a() + { + return $this.OverloadedMethod1(42) + } + [int] TestOverloadedMethodAccess1b() + { + return $this.OverloadedMethod1("abc") + } + [int] TestOverloadedMethodAccess2a() + { + return $this.OverloadedMethod2(42) + } + [int] TestOverloadedMethodAccess2b() + { + return $this.OverloadedMethod2("abc") + } + [int] TestOverloadedMethodAccess3a() + { + return $this.OverloadedMethod3(42) + } + [int] TestOverloadedMethodAccess3b() + { + return $this.OverloadedMethod3("abc") + } +} + +class Derived2 : Base {} + +[Derived]::new() +[Derived]::new(20) +[Derived2]::new() +'@ + +Describe "Protected Member Access - w/ default ctor" -Tags "CI" { + It "Method Access" { $derived1.TestMethodAccess() | Should Be 42 } + It "Dynamic Method Access" { $derived1.TestDynamicMethodAccess() | Should Be 42 } + It "Field Access" { $derived1.TestFieldAccess() | Should Be 11 } + It "Dynamic Field Access" { $derived1.TestDynamicFieldAccess() | Should Be 12 } + It "Property Access - protected get/protected set" { $derived1.TestPropertyAccess() | Should Be 1111 } + It "Property Access - public get/protected set " { $derived1.TestPropertyAccess1() | Should Be 2111 } + It "Property Access - protected get/public set" { $derived1.TestPropertyAccess2() | Should Be 3111 } + It "Dynamic Property Access" { $derived1.TestDynamicPropertyAccess() | Should Be 1112 } + + It "Method Access - overloaded 1a" { $derived1.TestOverloadedMethodAccess1a() | Should Be 84 } + It "Method Access - overloaded 1b" { $derived1.TestOverloadedMethodAccess1b() | Should Be 11 } + It "Method Access - overloaded 2a" { $derived1.TestOverloadedMethodAccess2a() | Should Be 84 } + It "Method Access - overloaded 2b" { $derived1.TestOverloadedMethodAccess2b() | Should Be 11 } + It "Method Access - overloaded 3a" { $derived1.TestOverloadedMethodAccess3a() | Should Be 84 } + It "Method Access - overloaded 3b" { $derived1.TestOverloadedMethodAccess3b() | Should Be 11 } + It "Implicit ctor calls protected ctor" { $derived3.OverloadedMethod2(42) | Should Be 84 } +} + +Describe "Protected Member Access - w/ non-default ctor" -Tags "CI" { + It "Method Access" { $derived2.TestMethodAccess() | Should Be 52 } + It "Dynamic Method Access" { $derived2.TestDynamicMethodAccess() | Should Be 52 } + It "Field Access" { $derived2.TestFieldAccess() | Should Be 11 } + It "Dynamic Field Access" { $derived2.TestDynamicFieldAccess() | Should Be 12 } + It "Property Access - protected get/protected set" { $derived1.TestPropertyAccess() | Should Be 1111 } + It "Property Access - public get/protected set " { $derived1.TestPropertyAccess1() | Should Be 2111 } + It "Property Access - protected get/public set" { $derived1.TestPropertyAccess2() | Should Be 3111 } + It "Dynamic Property Access" { $derived2.TestDynamicPropertyAccess() | Should Be 1112 } + + It "Method Access - overloaded 1a" { $derived2.TestOverloadedMethodAccess1a() | Should Be 94 } + It "Method Access - overloaded 1b" { $derived2.TestOverloadedMethodAccess1b() | Should Be 21 } + It "Method Access - overloaded 2a" { $derived2.TestOverloadedMethodAccess2a() | Should Be 94 } + It "Method Access - overloaded 2b" { $derived2.TestOverloadedMethodAccess2b() | Should Be 21 } + It "Method Access - overloaded 3a" { $derived2.TestOverloadedMethodAccess3a() | Should Be 94 } + It "Method Access - overloaded 3b" { $derived2.TestOverloadedMethodAccess3b() | Should Be 21 } +} + +Describe "Protected Member Access - members not visible outside class" -Tags "CI" { + Set-StrictMode -v 3 + It "Invalid protected field Get Access" { { $derived1.Field } | Should Throw } + It "Invalid protected property Get Access" { { $derived1.Property } | Should Throw } + It "Invalid protected field Set Access" { { $derived1.Field = 1 } | Should Throw } + It "Invalid protected property Set Access" { { $derived1.Property = 1 } | Should Throw } + + It "Invalid protected constructor Access" { { [Base]::new() } | Should Throw } + It "Invalid protected method Access" { { $derived1.Method() } | Should Throw } +} + diff --git a/test/powershell/Language/Classes/Scripting.Classes.Attributes.Tests.ps1 b/test/powershell/Language/Classes/Scripting.Classes.Attributes.Tests.ps1 index fb087eb3adf..959ff2239d7 100644 --- a/test/powershell/Language/Classes/Scripting.Classes.Attributes.Tests.ps1 +++ b/test/powershell/Language/Classes/Scripting.Classes.Attributes.Tests.ps1 @@ -1,484 +1,484 @@ -Describe 'Attributes Test' -Tags "CI" { - - BeforeAll { - $dummyAttributesSource = @' -using System.Management.Automation; -namespace Dummy -{ - public class DoubleStringTransformationAttribute : ArgumentTransformationAttribute - { - public override object Transform(EngineIntrinsics engineIntrinsics, object inputData) - { - string arg = inputData as string; - if (arg != null) - { - return arg + arg; - } - return inputData; - } - } - - public class AppendStringTransformationAttribute : ArgumentTransformationAttribute - { - public override object Transform(EngineIntrinsics engineIntrinsics, object inputData) - { - string arg = inputData as string; - if (arg != null) - { - return arg + "___"; - } - return inputData; - } - } - - public class DoubleInt : ArgumentTransformationAttribute - { - public override object Transform(EngineIntrinsics engineIntrinsics, object inputData) - { - int? arg = inputData as int?; - if (arg != null) - { - return arg + arg; - } - return inputData; - } - } -} -'@ - Add-Type -TypeDefinition $dummyAttributesSource - } - - - - Context 'Property.Instance.ValidateSet.String' { - class C1 { [ValidateSet("Present", "Absent")][string]$Ensure } - # This call should not throw exception - [C1]::new().Ensure = "Present" - - It 'Error when ValidateSet should be ExceptionWhenSetting' { - try - { - [C1]::new().Ensure = "foo" - throw "Exception expected" - } - catch - { - $_.FullyQualifiedErrorId | Should be 'ExceptionWhenSetting' - } - } - } - - Context 'Property.Static.ValidateSet.String' { - class C1 { static [ValidateSet("Present", "Absent")][string]$Ensure } - # This call should not throw exception - [C1]::Ensure = "Present" - It 'Error when ValidateSet should be ExceptionWhenSetting'{ - try { - [C1]::Ensure = "foo" - throw "Exception expected" - } - catch { - $_.FullyQualifiedErrorId | Should be 'ExceptionWhenSetting' - } - } - } - - Context 'Property.Instance.ValidateRange.Int' { - class C1 { [ValidateRange(1, 10)][int]$f } - # This call should not throw exception - [C1]::new().f = 10 - [C1]::new().f = 1 - It 'Error when ValidateSet should be ExceptionWhenSetting'{ - try { - [C1]::new().f = 20 - throw "Exception expected" - } - catch { - $_.FullyQualifiedErrorId | Should be 'ExceptionWhenSetting' - } - } - } - - Context 'Property.Static.ValidateRange.Int' { - class C1 { static [ValidateRange(1, 10)][int]$f } - # This call should not throw exception - [C1]::f = 5 - It 'Error when ValidateSet should be ExceptionWhenSetting'{ - try { - [C1]::f = 20 - throw "Exception expected" - } - catch { - $_.FullyQualifiedErrorId | Should be 'ExceptionWhenSetting' - } - } - } - - Context 'Property.Static.ValidateSet.ImplicitObject' { - class C1 { static [ValidateSet("abc", 5)]$o } - # This call should not throw exception - [C1]::o = "abc" - [C1]::o = 5 - It 'Error when ValidateSet should be ExceptionWhenSetting'{ - try { - [C1]::o = 1 - throw "Exception expected" - } - catch { - $_.FullyQualifiedErrorId | Should be 'ExceptionWhenSetting' - } - } - } - - # - # We use [scriptblock]::Create() here to allow SuiteSetup add Dummy.Transformation type to - # the scope. Otherwise, we will need to have all classes for attributes in parse time. - # - # Invoke() returns an array, we need first element of it. - # - - Context 'Property.Instance.Transformation.ImplicitObject' { - $c = [scriptblock]::Create('class C1 { [Dummy.DoubleStringTransformation()]$arg }; [C1]::new()').Invoke()[0] - - It 'Implicitly Transform to 100' { - $c.arg = 100 - $c.arg | should be 100 - } - It 'Implicitly Transform to foo' { - $c.arg = "foo" - $c.arg | should be "foofoo" - } - } - - Context 'Property.Instance.Transformation.String' { - $c = [scriptblock]::Create('class C1 { [Dummy.DoubleStringTransformation()][string]$arg }; [C1]::new()').Invoke()[0] - It 'set to foo' { - $c.arg = "foo" - $c.arg | should be "foofoo" - } - } - - Context Property.Instance.Transformation.Int { - $c = [scriptblock]::Create('class C1 { [Dummy.DoubleInt()][int]$arg }; [C1]::new()').Invoke()[0] - It 'arg should be 200' { - $c.arg = 100 - $c.arg | should be 200 - } - It 'Set to string should fail with ExceptionWhenSetting' { - try { - $c.arg = "abc" - throw "Exception expected" - } - catch { - $_.FullyQualifiedErrorId | Should be 'ExceptionWhenSetting' - } - } - } - - Context Property.Instance.Transformation.Nullable { - $c = [scriptblock]::Create('class C1 { [Nullable[int]][Dummy.DoubleStringTransformation()]$arg }; [C1]::new()').Invoke()[0] - It 'arg should be 100' { - $c.arg = 100 - $c.arg | should be 100 - } - } - - Context Property.Instance.Transformation.Order { - $c = [scriptblock]::Create('class C1 { [Dummy.DoubleStringTransformation()][Dummy.AppendStringTransformation()]$arg }; [C1]::new()').Invoke()[0] - It 'arg should be 100' { - $c.arg = 100 - $c.arg | should be 100 - } - - It 'arg should be foo___foo___g' { - $c.arg = "foo" - $c.arg | should be "foo___foo___" - } - } -} - -Describe 'Type resolution with attributes' -Tag "CI" { - # There is kind of a collision between names - # System.Diagnostics.Tracing.EventSource - # System.Diagnostics.Tracing.EventSourceAttribute - # We need to make sure that we resolve type name to the right class at each usage - Context 'Name collision' { - - It 'Resolve System.Diagnostics.Tracing.EventSource to Attribute and to Type in the different contexts' { - [System.Diagnostics.Tracing.EventSource(Name = "MyPSEventSource")] - class MyEventSource : System.Diagnostics.Tracing.EventSource - { - [void] OnEvent([string]$Message) {} - } - - [MyEventSource]::new() | Should Not Be $null - - } - } -} - -Describe 'ValidateSet support a dynamically generated set' -Tag "CI" { - - Context 'C# tests' { - - BeforeAll { - $a=@' - using System; - using System.Management.Automation; - using System.Collections.Generic; - - namespace Test.Language { - - [Cmdlet(VerbsCommon.Get, "TestValidateSet0")] - public class TestValidateSetCommand0 : PSCmdlet - { - [Parameter] - [ValidateSet(typeof(PSCmdlet))] - public string Param1; - - protected override void EndProcessing() - { - WriteObject(Param1); - } - } - - [Cmdlet(VerbsCommon.Get, "TestValidateSet4")] - public class TestValidateSetCommand4 : PSCmdlet - { - [Parameter] - [ValidateSet(typeof(GenValuesForParam))] - public string Param1; - - protected override void EndProcessing() - { - WriteObject(Param1); - } - } - - [Cmdlet(VerbsCommon.Get, "TestValidateSet5")] - public class TestValidateSetCommand5 : PSCmdlet - { - [Parameter] - [ValidateSet(typeof(GenValuesForParamNull))] - public string Param1; - - protected override void EndProcessing() - { - WriteObject(Param1); - } - } - - - /// Implement of test IValidateSetValuesGenerator - public class GenValuesForParamNull : IValidateSetValuesGenerator - { - public string[] GetValidValues() - { - var testValues = new string[] {"Test1","TestString1","Test2"}; - return null; - } - } - - public class GenValuesForParam : IValidateSetValuesGenerator - { - public string[] GetValidValues() - { - var testValues = new string[] {"Test1","TestString1","Test2"}; - return testValues; - } - } - } -'@ - - $cls = Add-Type -TypeDefinition $a -PassThru | select -First 1 - $testModule = Import-Module $cls.Assembly -PassThru - } - - AfterAll { - Remove-Module -ModuleInfo $testModule - } - - It 'Throw if IValidateSetValuesGenerator is not implemented' { - { Get-TestValidateSet0 -Param1 "TestString" -ErrorAction Stop } | ShouldBeErrorId "Argument" - } - - It 'Dynamically generated set works in C# with default (immediate) cache expire' { - Get-TestValidateSet4 -Param1 "TestString1" -ErrorAction SilentlyContinue | Should BeExactly "TestString1" - } - - It 'Empty dynamically generated set throws in C#' { - $exc = { - Get-TestValidateSet5 -Param1 "TestString1" -ErrorAction Stop - } | ShouldBeErrorId "ParameterArgumentValidationError,Test.Language.TestValidateSetCommand5" - $exc.Exception.InnerException.ErrorRecord.FullyQualifiedErrorId | Should BeExactly "ValidateSetGeneratedValidValuesListIsNull" - } - } - - Context 'Powershell tests' { - - BeforeAll { - class GenValuesForParam : System.Management.Automation.IValidateSetValuesGenerator { - [String[]] GetValidValues() { - - return [string[]]("Test1","TestString1","Test2") - } - } - - class GenValuesForParamNull : System.Management.Automation.IValidateSetValuesGenerator { - [String[]] GetValidValues() { - - return [string[]]$null - } - } - - # Return '$testValues2' and after 2 seconds after first use return another array '$testValues1'. - class GenValuesForParamCache1 : System.Management.Automation.IValidateSetValuesGenerator { - [String[]] GetValidValues() { - - $testValues1 = "Test11","TestString11","Test22" - $testValues2 = "Test11","TestString22","Test22" - - $currentTime = [DateTime]::Now - if ([DateTime]::Compare([GenValuesForParamCache1]::cacheTime, $currentTime) -le 0) - { - $testValues = $testValues1; - } - else - { - $testValues = $testValues2; - } - return [string[]]$testValues - } - - static [DateTime] $cacheTime = [DateTime]::Now.AddSeconds(2); - } - - function Get-TestValidateSetPS4 - { - [CmdletBinding()] - Param - ( - [ValidateSet([GenValuesForParam])] - $Param1 - ) - - $Param1 - } - - function Get-TestValidateSetPS5 - { - [CmdletBinding()] - Param - ( - [ValidateSet([GenValuesForParamNull])] - $Param1 - ) - - $Param1 - } - - function Get-TestValidateSetPS6 - { - [CmdletBinding()] - Param - ( - [ValidateSet([UnImplementedGeneratorOfValues])] - $Param1 - ) - - $Param1 - } - } - - It 'Dynamically generated set works in PowerShell script with default (immediate) cache expire' { - Get-TestValidateSetPS4 -Param1 "TestString1" -ErrorAction SilentlyContinue | Should BeExactly "TestString1" - } - - It 'Get the appropriate error message' { - {Get-TestValidateSetPS4 -Param1 "TestStringWrong" -ErrorAction Stop} | ShouldBeErrorId "ParameterArgumentValidationError,Get-TestValidateSetPS4" - } - - It 'Empty dynamically generated set throws in PowerShell script' { - $exc = { - Get-TestValidateSetPS5 -Param1 "TestString1" -ErrorAction Stop - } | ShouldBeErrorId "ParameterArgumentValidationError,Get-TestValidateSetPS5" - $exc.Exception.InnerException.ErrorRecord.FullyQualifiedErrorId | Should BeExactly "ValidateSetGeneratedValidValuesListIsNull" - } - - It 'Unimplemented valid values generator type throws in PowerShell script' { - { - Get-TestValidateSetPS6 -Param1 "AnyTestString" -ErrorAction Stop - } | ShouldBeErrorId "TypeNotFound" - } - } - - Context 'CachedValidValuesGeneratorBase class tests' { - - BeforeAll { - class GenValuesForParam : System.Management.Automation.CachedValidValuesGeneratorBase { - GenValuesForParam() : base(300) { - } - - [String[]] GenerateValidValues() { - - return [string[]]("Test1","TestString1","Test2") - } - } - - class GenValuesWithExpiration : System.Management.Automation.CachedValidValuesGeneratorBase { - GenValuesWithExpiration() : base(2) { - } - - Static [bool] $temp = $true; - - [String[]] GenerateValidValues() { - - if ([GenValuesWithExpiration]::temp) { - [GenValuesWithExpiration]::temp = $false - return [string[]]("Test1","TestString1","Test2") - } else { - [GenValuesWithExpiration]::temp = $true - return [string[]]("Test1","TestString2","Test2") - } - - } - } - - - function Get-TestValidateSetPS4 - { - [CmdletBinding()] - Param - ( - [ValidateSet([GenValuesForParam])] - $Param1 - ) - - $Param1 - } - - function Get-TestValidateSetPS5 - { - [CmdletBinding()] - Param - ( - [ValidateSet([GenValuesWithExpiration])] - $Param1 - ) - - $Param1 - } - } - - It 'Can implement CachedValidValuesGeneratorBase in PowerShell' { - Get-TestValidateSetPS4 -Param1 "TestString1" -ErrorAction SilentlyContinue | Should BeExactly "TestString1" - } - - It 'Can implement CachedValidValuesGeneratorBase with cache expiration in PowerShell' { - Get-TestValidateSetPS5 -Param1 "TestString1" -ErrorAction SilentlyContinue | Should BeExactly "TestString1" - Get-TestValidateSetPS5 -Param1 "TestString1" -ErrorAction SilentlyContinue | Should BeExactly "TestString1" - Start-Sleep 3 - Get-TestValidateSetPS5 -Param1 "TestString2" -ErrorAction SilentlyContinue | Should BeExactly "TestString2" - } - } -} +Describe 'Attributes Test' -Tags "CI" { + + BeforeAll { + $dummyAttributesSource = @' +using System.Management.Automation; +namespace Dummy +{ + public class DoubleStringTransformationAttribute : ArgumentTransformationAttribute + { + public override object Transform(EngineIntrinsics engineIntrinsics, object inputData) + { + string arg = inputData as string; + if (arg != null) + { + return arg + arg; + } + return inputData; + } + } + + public class AppendStringTransformationAttribute : ArgumentTransformationAttribute + { + public override object Transform(EngineIntrinsics engineIntrinsics, object inputData) + { + string arg = inputData as string; + if (arg != null) + { + return arg + "___"; + } + return inputData; + } + } + + public class DoubleInt : ArgumentTransformationAttribute + { + public override object Transform(EngineIntrinsics engineIntrinsics, object inputData) + { + int? arg = inputData as int?; + if (arg != null) + { + return arg + arg; + } + return inputData; + } + } +} +'@ + Add-Type -TypeDefinition $dummyAttributesSource + } + + + + Context 'Property.Instance.ValidateSet.String' { + class C1 { [ValidateSet("Present", "Absent")][string]$Ensure } + # This call should not throw exception + [C1]::new().Ensure = "Present" + + It 'Error when ValidateSet should be ExceptionWhenSetting' { + try + { + [C1]::new().Ensure = "foo" + throw "Exception expected" + } + catch + { + $_.FullyQualifiedErrorId | Should be 'ExceptionWhenSetting' + } + } + } + + Context 'Property.Static.ValidateSet.String' { + class C1 { static [ValidateSet("Present", "Absent")][string]$Ensure } + # This call should not throw exception + [C1]::Ensure = "Present" + It 'Error when ValidateSet should be ExceptionWhenSetting'{ + try { + [C1]::Ensure = "foo" + throw "Exception expected" + } + catch { + $_.FullyQualifiedErrorId | Should be 'ExceptionWhenSetting' + } + } + } + + Context 'Property.Instance.ValidateRange.Int' { + class C1 { [ValidateRange(1, 10)][int]$f } + # This call should not throw exception + [C1]::new().f = 10 + [C1]::new().f = 1 + It 'Error when ValidateSet should be ExceptionWhenSetting'{ + try { + [C1]::new().f = 20 + throw "Exception expected" + } + catch { + $_.FullyQualifiedErrorId | Should be 'ExceptionWhenSetting' + } + } + } + + Context 'Property.Static.ValidateRange.Int' { + class C1 { static [ValidateRange(1, 10)][int]$f } + # This call should not throw exception + [C1]::f = 5 + It 'Error when ValidateSet should be ExceptionWhenSetting'{ + try { + [C1]::f = 20 + throw "Exception expected" + } + catch { + $_.FullyQualifiedErrorId | Should be 'ExceptionWhenSetting' + } + } + } + + Context 'Property.Static.ValidateSet.ImplicitObject' { + class C1 { static [ValidateSet("abc", 5)]$o } + # This call should not throw exception + [C1]::o = "abc" + [C1]::o = 5 + It 'Error when ValidateSet should be ExceptionWhenSetting'{ + try { + [C1]::o = 1 + throw "Exception expected" + } + catch { + $_.FullyQualifiedErrorId | Should be 'ExceptionWhenSetting' + } + } + } + + # + # We use [scriptblock]::Create() here to allow SuiteSetup add Dummy.Transformation type to + # the scope. Otherwise, we will need to have all classes for attributes in parse time. + # + # Invoke() returns an array, we need first element of it. + # + + Context 'Property.Instance.Transformation.ImplicitObject' { + $c = [scriptblock]::Create('class C1 { [Dummy.DoubleStringTransformation()]$arg }; [C1]::new()').Invoke()[0] + + It 'Implicitly Transform to 100' { + $c.arg = 100 + $c.arg | should be 100 + } + It 'Implicitly Transform to foo' { + $c.arg = "foo" + $c.arg | should be "foofoo" + } + } + + Context 'Property.Instance.Transformation.String' { + $c = [scriptblock]::Create('class C1 { [Dummy.DoubleStringTransformation()][string]$arg }; [C1]::new()').Invoke()[0] + It 'set to foo' { + $c.arg = "foo" + $c.arg | should be "foofoo" + } + } + + Context Property.Instance.Transformation.Int { + $c = [scriptblock]::Create('class C1 { [Dummy.DoubleInt()][int]$arg }; [C1]::new()').Invoke()[0] + It 'arg should be 200' { + $c.arg = 100 + $c.arg | should be 200 + } + It 'Set to string should fail with ExceptionWhenSetting' { + try { + $c.arg = "abc" + throw "Exception expected" + } + catch { + $_.FullyQualifiedErrorId | Should be 'ExceptionWhenSetting' + } + } + } + + Context Property.Instance.Transformation.Nullable { + $c = [scriptblock]::Create('class C1 { [Nullable[int]][Dummy.DoubleStringTransformation()]$arg }; [C1]::new()').Invoke()[0] + It 'arg should be 100' { + $c.arg = 100 + $c.arg | should be 100 + } + } + + Context Property.Instance.Transformation.Order { + $c = [scriptblock]::Create('class C1 { [Dummy.DoubleStringTransformation()][Dummy.AppendStringTransformation()]$arg }; [C1]::new()').Invoke()[0] + It 'arg should be 100' { + $c.arg = 100 + $c.arg | should be 100 + } + + It 'arg should be foo___foo___g' { + $c.arg = "foo" + $c.arg | should be "foo___foo___" + } + } +} + +Describe 'Type resolution with attributes' -Tag "CI" { + # There is kind of a collision between names + # System.Diagnostics.Tracing.EventSource + # System.Diagnostics.Tracing.EventSourceAttribute + # We need to make sure that we resolve type name to the right class at each usage + Context 'Name collision' { + + It 'Resolve System.Diagnostics.Tracing.EventSource to Attribute and to Type in the different contexts' { + [System.Diagnostics.Tracing.EventSource(Name = "MyPSEventSource")] + class MyEventSource : System.Diagnostics.Tracing.EventSource + { + [void] OnEvent([string]$Message) {} + } + + [MyEventSource]::new() | Should Not Be $null + + } + } +} + +Describe 'ValidateSet support a dynamically generated set' -Tag "CI" { + + Context 'C# tests' { + + BeforeAll { + $a=@' + using System; + using System.Management.Automation; + using System.Collections.Generic; + + namespace Test.Language { + + [Cmdlet(VerbsCommon.Get, "TestValidateSet0")] + public class TestValidateSetCommand0 : PSCmdlet + { + [Parameter] + [ValidateSet(typeof(PSCmdlet))] + public string Param1; + + protected override void EndProcessing() + { + WriteObject(Param1); + } + } + + [Cmdlet(VerbsCommon.Get, "TestValidateSet4")] + public class TestValidateSetCommand4 : PSCmdlet + { + [Parameter] + [ValidateSet(typeof(GenValuesForParam))] + public string Param1; + + protected override void EndProcessing() + { + WriteObject(Param1); + } + } + + [Cmdlet(VerbsCommon.Get, "TestValidateSet5")] + public class TestValidateSetCommand5 : PSCmdlet + { + [Parameter] + [ValidateSet(typeof(GenValuesForParamNull))] + public string Param1; + + protected override void EndProcessing() + { + WriteObject(Param1); + } + } + + + /// Implement of test IValidateSetValuesGenerator + public class GenValuesForParamNull : IValidateSetValuesGenerator + { + public string[] GetValidValues() + { + var testValues = new string[] {"Test1","TestString1","Test2"}; + return null; + } + } + + public class GenValuesForParam : IValidateSetValuesGenerator + { + public string[] GetValidValues() + { + var testValues = new string[] {"Test1","TestString1","Test2"}; + return testValues; + } + } + } +'@ + + $cls = Add-Type -TypeDefinition $a -PassThru | select -First 1 + $testModule = Import-Module $cls.Assembly -PassThru + } + + AfterAll { + Remove-Module -ModuleInfo $testModule + } + + It 'Throw if IValidateSetValuesGenerator is not implemented' { + { Get-TestValidateSet0 -Param1 "TestString" -ErrorAction Stop } | ShouldBeErrorId "Argument" + } + + It 'Dynamically generated set works in C# with default (immediate) cache expire' { + Get-TestValidateSet4 -Param1 "TestString1" -ErrorAction SilentlyContinue | Should BeExactly "TestString1" + } + + It 'Empty dynamically generated set throws in C#' { + $exc = { + Get-TestValidateSet5 -Param1 "TestString1" -ErrorAction Stop + } | ShouldBeErrorId "ParameterArgumentValidationError,Test.Language.TestValidateSetCommand5" + $exc.Exception.InnerException.ErrorRecord.FullyQualifiedErrorId | Should BeExactly "ValidateSetGeneratedValidValuesListIsNull" + } + } + + Context 'Powershell tests' { + + BeforeAll { + class GenValuesForParam : System.Management.Automation.IValidateSetValuesGenerator { + [String[]] GetValidValues() { + + return [string[]]("Test1","TestString1","Test2") + } + } + + class GenValuesForParamNull : System.Management.Automation.IValidateSetValuesGenerator { + [String[]] GetValidValues() { + + return [string[]]$null + } + } + + # Return '$testValues2' and after 2 seconds after first use return another array '$testValues1'. + class GenValuesForParamCache1 : System.Management.Automation.IValidateSetValuesGenerator { + [String[]] GetValidValues() { + + $testValues1 = "Test11","TestString11","Test22" + $testValues2 = "Test11","TestString22","Test22" + + $currentTime = [DateTime]::Now + if ([DateTime]::Compare([GenValuesForParamCache1]::cacheTime, $currentTime) -le 0) + { + $testValues = $testValues1; + } + else + { + $testValues = $testValues2; + } + return [string[]]$testValues + } + + static [DateTime] $cacheTime = [DateTime]::Now.AddSeconds(2); + } + + function Get-TestValidateSetPS4 + { + [CmdletBinding()] + Param + ( + [ValidateSet([GenValuesForParam])] + $Param1 + ) + + $Param1 + } + + function Get-TestValidateSetPS5 + { + [CmdletBinding()] + Param + ( + [ValidateSet([GenValuesForParamNull])] + $Param1 + ) + + $Param1 + } + + function Get-TestValidateSetPS6 + { + [CmdletBinding()] + Param + ( + [ValidateSet([UnImplementedGeneratorOfValues])] + $Param1 + ) + + $Param1 + } + } + + It 'Dynamically generated set works in PowerShell script with default (immediate) cache expire' { + Get-TestValidateSetPS4 -Param1 "TestString1" -ErrorAction SilentlyContinue | Should BeExactly "TestString1" + } + + It 'Get the appropriate error message' { + {Get-TestValidateSetPS4 -Param1 "TestStringWrong" -ErrorAction Stop} | ShouldBeErrorId "ParameterArgumentValidationError,Get-TestValidateSetPS4" + } + + It 'Empty dynamically generated set throws in PowerShell script' { + $exc = { + Get-TestValidateSetPS5 -Param1 "TestString1" -ErrorAction Stop + } | ShouldBeErrorId "ParameterArgumentValidationError,Get-TestValidateSetPS5" + $exc.Exception.InnerException.ErrorRecord.FullyQualifiedErrorId | Should BeExactly "ValidateSetGeneratedValidValuesListIsNull" + } + + It 'Unimplemented valid values generator type throws in PowerShell script' { + { + Get-TestValidateSetPS6 -Param1 "AnyTestString" -ErrorAction Stop + } | ShouldBeErrorId "TypeNotFound" + } + } + + Context 'CachedValidValuesGeneratorBase class tests' { + + BeforeAll { + class GenValuesForParam : System.Management.Automation.CachedValidValuesGeneratorBase { + GenValuesForParam() : base(300) { + } + + [String[]] GenerateValidValues() { + + return [string[]]("Test1","TestString1","Test2") + } + } + + class GenValuesWithExpiration : System.Management.Automation.CachedValidValuesGeneratorBase { + GenValuesWithExpiration() : base(2) { + } + + Static [bool] $temp = $true; + + [String[]] GenerateValidValues() { + + if ([GenValuesWithExpiration]::temp) { + [GenValuesWithExpiration]::temp = $false + return [string[]]("Test1","TestString1","Test2") + } else { + [GenValuesWithExpiration]::temp = $true + return [string[]]("Test1","TestString2","Test2") + } + + } + } + + + function Get-TestValidateSetPS4 + { + [CmdletBinding()] + Param + ( + [ValidateSet([GenValuesForParam])] + $Param1 + ) + + $Param1 + } + + function Get-TestValidateSetPS5 + { + [CmdletBinding()] + Param + ( + [ValidateSet([GenValuesWithExpiration])] + $Param1 + ) + + $Param1 + } + } + + It 'Can implement CachedValidValuesGeneratorBase in PowerShell' { + Get-TestValidateSetPS4 -Param1 "TestString1" -ErrorAction SilentlyContinue | Should BeExactly "TestString1" + } + + It 'Can implement CachedValidValuesGeneratorBase with cache expiration in PowerShell' { + Get-TestValidateSetPS5 -Param1 "TestString1" -ErrorAction SilentlyContinue | Should BeExactly "TestString1" + Get-TestValidateSetPS5 -Param1 "TestString1" -ErrorAction SilentlyContinue | Should BeExactly "TestString1" + Start-Sleep 3 + Get-TestValidateSetPS5 -Param1 "TestString2" -ErrorAction SilentlyContinue | Should BeExactly "TestString2" + } + } +} diff --git a/test/powershell/Language/Classes/Scripting.Classes.BasicParsing.Tests.ps1 b/test/powershell/Language/Classes/Scripting.Classes.BasicParsing.Tests.ps1 index 0aad8ddb354..3d72abe33f9 100644 --- a/test/powershell/Language/Classes/Scripting.Classes.BasicParsing.Tests.ps1 +++ b/test/powershell/Language/Classes/Scripting.Classes.BasicParsing.Tests.ps1 @@ -1,857 +1,857 @@ -# -# Copyright (c) Microsoft Corporation, 2014 -# - -try { -# -# CrossGen'ed assemblies cause a hang to happen intermittently when running this test suite in Linux and macOS. -# The issue has been reported to CoreCLR team. We need to work around it for now with the following approach: -# 1. For pull request and push commit, build without '-CrossGen' and run the parsing tests -# 2. For daily build, build with '-CrossGen' but don't run the parsing tests -# In this way, we will continue to exercise these parsing tests for each CI build, and skip them for daily -# build to avoid a hang. -# Note: this change should be reverted once the 'CrossGen' issue is fixed by CoreCLR. The issue is tracked by -# https://github.com/dotnet/coreclr/issues/9745 -# -$isDailyBuild = $env:TRAVIS_EVENT_TYPE -eq 'cron' -or $env:TRAVIS_EVENT_TYPE -eq 'api' -$defaultParamValues = $PSdefaultParameterValues.Clone() -$IsSkipped = (!$IsWindows -and $isDailyBuild) -$PSDefaultParameterValues["it:skip"] = $IsSkipped -$PSDefaultParameterValues["ShouldBeParseError:SkipInTravisFullBuild"] = $IsSkipped - - -Describe 'Positive Parse Properties Tests' -Tags "CI" { - It 'PositiveParsePropertiesTest' { - # Just a bunch of random basic things here - # This test doesn't need to check anything, if there are - # any parse errors, the entire suite will fail because the - # script will fail to parse. - - # No members - class C1 {} - - # Simple field - class C2 { $x; } - - # Simple typed field - class C3 { [int] $x; } - - # Multiple fields, one line, last w/o semicolon - class C4 { $x; $y } - - # Multiple fields, multiple lines - class C5 - { - $x - $y - } - - # Static field - class C6 { static $x; } - - # Static field w/ type - order doesn't matter - class C7a { static [hashtable] $x; } - class C7b { [hashtable] static $x; } - - # Field using type defined in this scope - class C8a { [C1] $c1; } - class C8b { [c1] $c1; } - - # Field referring to self type - class C9 { [C9] $c9; } - - # Hidden fields - class C10a { hidden $x } - class C10b { hidden [int] $x } - class C10c { hidden static $x; static hidden $y } - class C10d { hidden static [int] $x; static hidden [int] $y } - } - - It 'Positive Parse Methods Tests' { - # Just a bunch of random basic things here - # This test doesn't need to check anything, if there are - # any parse errors, the entire suite will fail because the - # script will fail to parse. - - # No members - class C1 {} - - # Simple method - class C2 { f() {} } - - # Simple method with return type - class C3 { [int] f() { return 1 } } - - # Multiple methods, one line - class C4 { f() {} f1() {} } - - # Multiple methods w/ overloads - class C5 - { - f1() {} - f1($a) {} - } - - # Static method - class C6 { static f() {} } - - # Static method w/ return type - class C7 { static [hashtable] f1() { return @{} } } - - # Method using return type defined in this scope - class C8a { [C1] f1() { return [C1]::new() } } - class C8b { [c1] f1() { return [c1]::new() } } - - # Hidden methods - class C10a { hidden F() { } } - class C10b { hidden [void] F() { } } - class C10c { hidden static F() { } static hidden G() { } } - class C10d { hidden static [void] F() { } static hidden [void] G() { } } - - # return analysis - class C11a { [int]foo() { try {throw "foo"} catch {throw $_} } } - class C11b { [int]foo() { try {throw "foo"} finally {}; try {} catch {} } } - class C11c { [int]foo() { try {throw "foo"} catch [ArgumentException] {throw $_} catch {throw $_} } } - class C11d { [int]foo() { try {if (1 -eq 2) { throw "1"} else {throw "2"}} finally {} } } - class C11e { [int]foo() { try {throw "foo"} catch [ArgumentException] {throw $_} catch {return 1} } } - class C11f { [int]foo() { try {} finally {throw "bar"} } } - class C11g { [int]foo() { do { return 1 } while ($false) } } - class C11h { [int]foo() { try {throw "foo"} finally {} } } - - # local variables - class C12a { static f() { [bigint]$foo = 42 } } - class C12b { [void] f() { [bigint]$foo = 42 } } - class C12c { [void] f() { [System.Management.Automation.Host.Rectangle]$foo = [System.Management.Automation.Host.Rectangle]::new(0, 0, 0, 0) } } - } - - context "Positive ParseMethods return type Test" { - # Method with return type of self - class C9 { [C9] f() { return [C9]::new() } } - $c9 = [C9]::new().f() - It "Expected a C9 returned" { $c9.GetType().Name | should be C9 } - class C9a { [C9a[]] f() { return [C9a]::new() } } - $c9a = [C9a]::new().f() - It "Expected a C9a[] returned" { $c9a.GetType().Name | should be C9a[] } - class C9b { [System.Collections.Generic.List[C9b]] f() { return [C9b]::new() } } - $c9b = [C9b]::new().f() - It "Expected a System.Collections.Generic.List[C9b] returned" { $c9b -is [System.Collections.Generic.List[C9b]] | should be $true } - } - - It 'Positive ParseProperty Attributes Test' { - class C1a { [ValidateNotNull()][int]$x; } - class C1b - { - [ValidateNotNull()] - [int] - $x - } - class C1c - { - [ValidateNotNull()] - [int]$x - } - class C1d - { - [ValidateNotNull()][int] - $x - } - } - - It 'PositiveParseMethodAttributesTest' { - class C1a { [Obsolete()][int] f() { return 0 } } - class C1b - { - [Obsolete()] - [int] - f() { return 1 } - } - class C1c - { - [Obsolete("Message")] - [int] f() { return 0 } - } - class C1d - { - [Obsolete()][int] - f(){ return -1 } - } - } - - It 'Class Method Reference ConstantVars' { - class C1 - { - [bool] f1() { return $true } - [bool] f2() { return $false } - [object] f3() { return $null } - } - } - - It 'Positive Parsing Of DscResource' { - [DscResource()] - class C1 - { - [DscProperty(Key)][string]$Key - [bool] Test() { return $false } - [C1] Get() { return $this } - Set() {} - } - - [DscResource()] - class C2 - { - [DscProperty(Key)][int]$Key = 1 - [bool] Test() { return $false } - [C2] Get() { return $this } - Set() {} - } - - [DscResource()] - class C4 - { - [DscProperty(Key)][byte]$Key=1 - C4() { } - C4($a) { } - [bool] Test() { return $false } - [C4] Get() { return $this } - Set() {} - } - - [DscResource()] - class C5 - { - [DscProperty(Key)][int]$Key = 1 - C5() { } - static C5() { } - C5($a) { } - [bool] Test() { return $false } - [C5] Get() { return $this } - Set() {} - } - } - - It 'allows some useful implicit variables inside methods' { - class C { - [void] m() - { - $LASTEXITCODE - $lastexitcode - '111' -match '1' - $Matches - $mAtches - $Error[0] - $error - $pwd - foreach ($i in 1..10) {$foreach} - switch ($i) - { - '1' { - $switch - } - } - } - } - } - - It 'allowes [ordered] attribute inside methods' { - class A - { - $h - A() - { - $this.h = [ordered] @{} - } - } - [A]::new().h.GetType().Name | Should Be 'OrderedDictionary' - } -} - -Describe 'Negative Parsing Tests' -Tags "CI" { - ShouldBeParseError 'class' MissingNameAfterKeyword 5 - ShouldBeParseError 'class foo' MissingTypeBody 9 - ShouldBeParseError 'class foo {' MissingEndCurlyBrace 11 - ShouldBeParseError 'class foo { [int] }' IncompleteMemberDefinition 17 - ShouldBeParseError 'class foo { $private: }' InvalidVariableReference 12 - ShouldBeParseError 'class foo { [int]$global: }' InvalidVariableReference 17 - ShouldBeParseError 'class foo { [Attr()] }' IncompleteMemberDefinition 20 - ShouldBeParseError 'class foo {} class foo {}' MemberAlreadyDefined 13 - ShouldBeParseError 'class foo { $x; $x; }' MemberAlreadyDefined 16 -SkipAndCheckRuntimeError - ShouldBeParseError 'class foo { [int][string]$x; }' TooManyTypes 17 - ShouldBeParseError 'class foo { [int][string]$x; }' TooManyTypes 17 - ShouldBeParseError 'class foo { static static $x; }' DuplicateQualifier 19 - ShouldBeParseError 'class foo { [zz]$x; }' TypeNotFound 13 - ShouldBeParseError 'class foo { [zz]f() { return 0 } }' TypeNotFound 13 - ShouldBeParseError 'class foo { f([zz]$x) {} }' TypeNotFound 15 - - ShouldBeParseError 'class C {} class C {}' MemberAlreadyDefined 11 - ShouldBeParseError 'class C { f(){} f(){} }' MemberAlreadyDefined 16 -SkipAndCheckRuntimeError - ShouldBeParseError 'class C { F(){} F($o){} [int] F($o) {return 1} }' MemberAlreadyDefined 24 -SkipAndCheckRuntimeError - ShouldBeParseError 'class C { f(){} f($a){} f(){} }' MemberAlreadyDefined 24 -SkipAndCheckRuntimeError - ShouldBeParseError 'class C { f([int]$a){} f([int]$b){} }' MemberAlreadyDefined 23 -SkipAndCheckRuntimeError - ShouldBeParseError 'class C { $x; [int]$x; }' MemberAlreadyDefined 14 -SkipAndCheckRuntimeError - ShouldBeParseError 'class C { static C($x) {} }' StaticConstructorCantHaveParameters 19 -SkipAndCheckRuntimeError - ShouldBeParseError 'class C { static C([int]$x = 100) {} }' StaticConstructorCantHaveParameters 19 -SkipAndCheckRuntimeError - - ShouldBeParseError 'class C {f(){ return 1} }' VoidMethodHasReturn 14 - ShouldBeParseError 'class C {[int] f(){ return } }' NonVoidMethodMissingReturnValue 20 - ShouldBeParseError 'class C {[int] f(){} }' MethodHasCodePathNotReturn 15 - ShouldBeParseError 'class C {f(){ $x=1; if($x -lt 2){ return } elseif($x -gt 0 ) {return 1} else{return 2} return 3 } }' @("VoidMethodHasReturn", "VoidMethodHasReturn", "VoidMethodHasReturn") @(62,77,87) - - ShouldBeParseError 'class foo { [int] bar() { $y = $z; return $y} }' VariableNotLocal 31 - ShouldBeParseError 'class foo { bar() { foreach ($zz in $yy) { } } }' VariableNotLocal 36 - ShouldBeParseError 'class foo { bar() { foreach ($zz in $global:yy) { $abc = $zzzzz } } }' VariableNotLocal 57 - ShouldBeParseError 'class foo { bar() { try { $zz = 42 } finally { } $zz } }' VariableNotLocal 49 - ShouldBeParseError 'class foo { bar() { try { $zz = 42 } catch { } $zz } }' VariableNotLocal 47 - ShouldBeParseError 'class foo { bar() { switch (@()) { default { $aa = 42 } } $aa } }' VariableNotLocal 58 - ShouldBeParseError 'class C { $x; static bar() { $this.x = 1 } }' NonStaticMemberAccessInStaticMember 29 - ShouldBeParseError 'class C { $x; static $y = $this.x }' NonStaticMemberAccessInStaticMember 26 - - ShouldBeParseError 'class C { $x; static bar() { $this.x = 1 } }' NonStaticMemberAccessInStaticMember 29 - ShouldBeParseError 'class C { $x; static $y = $this.x }' NonStaticMemberAccessInStaticMember 26 - ShouldBeParseError 'class C { $x; static bar() { $This.x = 1 } }' NonStaticMemberAccessInStaticMember 29 - ShouldBeParseError 'class C { $x; static $y = $This.x }' NonStaticMemberAccessInStaticMember 26 - - ShouldBeParseError 'class C { [void]foo() { try { throw "foo"} finally { return } } }' ControlLeavingFinally 53 - ShouldBeParseError 'class C { [int]foo() { return; return 1 } }' NonVoidMethodMissingReturnValue 23 - ShouldBeParseError 'class C { [int]foo() { try { throw "foo"} catch { } } }' MethodHasCodePathNotReturn 15 - ShouldBeParseError 'class C { [int]foo() { try { throw "foo"} catch [ArgumentException] {} catch {throw $_} } }' MethodHasCodePathNotReturn 15 - ShouldBeParseError 'class C { [int]foo() { try { throw "foo"} catch [ArgumentException] {return 1} catch {} } }' MethodHasCodePathNotReturn 15 - ShouldBeParseError 'class C { [int]foo() { while ($false) { return 1 } } }' MethodHasCodePathNotReturn 15 - ShouldBeParseError 'class C { [int]foo() { try { mkdir foo } finally { rm -rec foo } } }' MethodHasCodePathNotReturn 15 - ShouldBeParseError 'class C { [int]foo() { try { mkdir foo; return 1 } catch { } } }' MethodHasCodePathNotReturn 15 - ShouldBeParseError 'class C { [bool] Test() { if ($false) { return $true; } } }' MethodHasCodePathNotReturn 17 - - ShouldBeParseError 'class C { [int]$i; [void] foo() {$i = 10} }' MissingThis 33 - ShouldBeParseError 'class C { static [int]$i; [void] foo() {$i = 10} }' MissingTypeInStaticPropertyAssignment 40 - - ShouldBeParseError 'class C : B' MissingTypeBody 11 - - ShouldBeParseError 'Class foo { q(){} w(){}' MissingEndCurlyBrace 11 -} - -Describe 'Negative methods Tests' -Tags "CI" { - ShouldBeParseError 'class foo { f() { param($x) } }' ParamBlockNotAllowedInMethod 18 - ShouldBeParseError 'class foo { f() { dynamicparam {} } }' NamedBlockNotAllowedInMethod 18 - ShouldBeParseError 'class foo { f() { begin {} } }' NamedBlockNotAllowedInMethod 18 - ShouldBeParseError 'class foo { f() { process {} } }' NamedBlockNotAllowedInMethod 18 - ShouldBeParseError 'class foo { f() { end {} } }' NamedBlockNotAllowedInMethod 18 - ShouldBeParseError 'class foo { f([Parameter()]$a) {} }' AttributeNotAllowedOnDeclaration 14 - ShouldBeParseError 'class foo { [int] foo() { return 1 }}' ConstructorCantHaveReturnType 12 - ShouldBeParseError 'class foo { [void] bar($a, [string][int]$b, $c) {} }' MultipleTypeConstraintsOnMethodParam 35 -} - -Describe 'Negative Assignment Tests' -Tags "CI" { - ShouldBeParseError 'class foo { [string ]$path; f() { $path="" } }' MissingThis 34 - ShouldBeParseError 'class foo { [string ]$path; f() { [string] $path="" } }' MissingThis 43 - ShouldBeParseError 'class foo { [string ]$path; f() { [int] [string] $path="" } }' MissingThis 49 -} - -Describe 'Negative Assignment Tests' -Tags "CI" { - ShouldBeParseError '[DscResource()]class C { [bool] Test() { return $false } [C] Get() { return $this } Set() {} }' DscResourceMissingKeyProperty 0 - - # Test method - ShouldBeParseError '[DscResource()]class C { [DscProperty(Key)][string]$Key; [C] Get() { return $this } Set() {} }' DscResourceMissingTestMethod 0 - ShouldBeParseError '[DscResource()]class C { [DscProperty(Key)][string]$Key; [C] Get() { return $this } Set() {} Test() { } }' DscResourceMissingTestMethod 0 - ShouldBeParseError '[DscResource()]class C { [DscProperty(Key)][string]$Key; [C] Get() { return $this } Set() {} [int] Test() { return 1 } }' DscResourceMissingTestMethod 0 - ShouldBeParseError '[DscResource()]class C { [DscProperty(Key)][string]$Key; [C] Get() { return $this } Set() {} [bool] Test($a) { return $false } }' DscResourceMissingTestMethod 0 - - # Get method - ShouldBeParseError '[DscResource()]class C { [DscProperty(Key)][string]$Key; [bool] Test() { return $false } Set() {} }' DscResourceMissingGetMethod 0 - ShouldBeParseError '[DscResource()]class C { [DscProperty(Key)][string]$Key; [bool] Test() { return $false } Set() {} Get() { } }' DscResourceInvalidGetMethod 98 - ShouldBeParseError '[DscResource()]class C { [DscProperty(Key)][string]$Key; [bool] Test() { return $false } Set() {} [int] Get() { return 1 } }' DscResourceInvalidGetMethod 98 - ShouldBeParseError '[DscResource()]class C { [DscProperty(Key)][string]$Key; [bool] Test() { return $false } Set() {} [C] Get($a) { return $this } }' DscResourceMissingGetMethod 0 - - # Set method - ShouldBeParseError '[DscResource()]class C { [DscProperty(Key)][string]$Key; [bool] Test() { return $false } [C] Get() { return $this } }' DscResourceMissingSetMethod 0 - ShouldBeParseError '[DscResource()]class C { [DscProperty(Key)][string]$Key; [bool] Test() { return $false } [C] Get() { return $this } [int] Set() { return 1 } }' DscResourceMissingSetMethod 0 - ShouldBeParseError '[DscResource()]class C { [DscProperty(Key)][string]$Key; [bool] Test() { return $false } [C] Get() { return $this } Set($a) { } }' DscResourceMissingSetMethod 0 - - # Default ctor - ShouldBeParseError '[DscResource()]class C { [DscProperty(Key)][string]$Key; [bool] Test() { return $false } [C] Get() { return $this } Set() {} C($a) { } }' DscResourceMissingDefaultConstructor 0 -} - -Describe 'Negative DscResources Tests' -Tags "CI" { - # Usage errors - ShouldBeParseError '[Flags()]class C{}' AttributeNotAllowedOnDeclaration 0 - ShouldBeParseError 'class C { [Flags()]$field; }' AttributeNotAllowedOnDeclaration 10 - ShouldBeParseError 'class C { [Flags()]foo(){} }' AttributeNotAllowedOnDeclaration 10 - - # Errors related to construction of the attribute - ShouldBeParseError '[UnknownAttr()]class C{}' CustomAttributeTypeNotFound 1 - ShouldBeParseError '[System.Management.Automation.Cmdlet()]class C{}' MethodCountCouldNotFindBest 0 -SkipAndCheckRuntimeError - ShouldBeParseError '[System.Management.Automation.Cmdlet("zz")]class C{}' MethodCountCouldNotFindBest 0 -SkipAndCheckRuntimeError - ShouldBeParseError '[System.Management.Automation.Cmdlet("Get", "Thing", Prop=1)]class C{}' PropertyNotFoundForAttribute 53 - ShouldBeParseError '[System.Management.Automation.Cmdlet("Get", "Thing", ConfirmImpact="foo")]class C{}' CannotConvertValue 67 -SkipAndCheckRuntimeError - ShouldBeParseError '[System.Management.Automation.Cmdlet("Get", "Thing", NounName="foo")]class C{}' ReadOnlyProperty 53 - ShouldBeParseError '[System.Management.Automation.Cmdlet("Get", "Thing", ConfirmImpact=$zed)]class C{}' ParameterAttributeArgumentNeedsToBeConstant 67 - ShouldBeParseError 'class C{ [ValidateScript({})]$p; }' ParameterAttributeArgumentNeedsToBeConstant 25 -} - -Describe 'Negative ClassAttributes Tests' -Tags "CI" { - [System.Management.Automation.Cmdlet("Get", "Thing")]class C{} - $t = [C].GetCustomAttributes($false) - - It "Should have one attribute" {$t.Count | should be 1} - It "Should have instance of CmdletAttribute" {$t[0].GetType().FullName | should be System.Management.Automation.CmdletAttribute } - - [System.Management.Automation.CmdletAttribute]$c = $t[0] - It "Verb should be Get" {$c.VerbName | should be 'Get'} - It "Noun should be Thing" {$c.NounName | should be 'Thing'} - - [System.Management.Automation.Cmdlet("Get", "Thing", SupportsShouldProcess = $true, SupportsPaging = $true)]class C2{} - $t = [C2].GetCustomAttributes($false) - It "Should have one attribute" { $t.Count | should be 1 } - It "Should have instance of CmdletAttribute" { $t[0].GetType().FullName | should be System.Management.Automation.CmdletAttribute } - [System.Management.Automation.CmdletAttribute]$c = $t[0] - It "Verb should be Get" {$c.VerbName | should be 'Get'} - It "Noun should be Thing" {$c.NounName | should be 'Thing'} - - It "SupportsShouldProcess should be $true" { $c.ConfirmImpact | should be $true } - It "SupportsPaging should be `$true" { $c.SupportsPaging | should be $true } - Context "Support ConfirmImpact as an attribute" { - It "ConfirmImpact should be high" -pending { - [System.Management.Automation.Cmdlet("Get", "Thing", ConfirmImpact = 'High', SupportsPaging = $true)]class C3{} - $t = [C3].GetCustomAttributes($false) - It "Should have one attribute" { $t.Count | should be 1 } - It "Should have instance of CmdletAttribute" { $t[0].GetType().FullName | should be System.Management.Automation.CmdletAttribute } - [System.Management.Automation.CmdletAttribute]$c = $t[0] - $c.ConfirmImpact | should be 'High' - - } - } -} - -Describe 'Property Attributes Test' -Tags "CI" { - class C { [ValidateSet('a', 'b')]$p; } - - $t = [C].GetProperty('p').GetCustomAttributes($false) - It "Should have one attribute" { $t.Count | should be 1 } - [ValidateSet]$v = $t[0] - It "Should have 2 valid values" { $v.ValidValues.Count | should be 2 } - It "first value should be a" { $v.ValidValues[0] | should be 'a' } - It "second value should be b" { $v.ValidValues[1] | should be 'b' } -} - -Describe 'Method Attributes Test' -Tags "CI" { - class C { [Obsolete("aaa")][int]f() { return 1 } } - - $t = [C].GetMethod('f').GetCustomAttributes($false) - It "Should have one attribute" {$t.Count | should be 1 } - It "Attribute type should be ObsoleteAttribute" { $t[0].GetType().FullName | should be System.ObsoleteAttribute } -} - -Describe 'Positive SelfClass Type As Parameter Test' -Tags "CI" { - class Point - { - Point($x, $y) { $this.x = $x; $this.y = $y } - Point() {} - - [int] $x = 0 - [int] $y = 0 - Add([Point] $val) { $this.x += $val.x; $this.y += $val.y; } - - Print() { Write-Host "[`$x=$($this.x) `$y=$($this.y)]" } - Set($x, $y) { $this.x = $x; $this.y = $y } - } - It "[Point]::Add works" { - $point = [Point]::new(100,200) - $point2 = [Point]::new(1,2) - $point.Add($point2) - - $point.x | should be 101 - $point.y | should be 202 - } - - It "[Point]::Add works" { - $point = New-Object Point 100,200 - $point2 = New-Object Point 1,2 - $point.Add($point2) - - $point.x | should be 101 - $point.y | should be 202 - } -} - -Describe 'PositiveReturnSelfClassTypeFromMemberFunction Test' -Tags "CI" { - class ReturnObjectFromMemberFunctionTest - { - [ReturnObjectFromMemberFunctionTest] CreateInstance() - { - return [ReturnObjectFromMemberFunctionTest]::new() - } - [string] SayHello() - { - return "Hello1" - } - } - $f = [ReturnObjectFromMemberFunctionTest]::new() - $z = $f.CreateInstance() # Line 13 - It "CreateInstance works" { $z.SayHello() | should be 'Hello1' } -} - -Describe 'TestMultipleArguments Test' -Tags "CI" { - if ( $IsCoreCLR ) { $maxCount = 14 } else { $maxCount = 16 } - for ($i = 0; $i -lt $maxCount; $i++) - { - $properties = $(for ($j = 0; $j -le $i; $j++) { - " [int]`$Prop$j" - }) -join "`n" - - $methodParameters = $(for ($j = 0; $j -le $i; $j++) { - "[int]`$arg$j" - }) -join ", " - - $ctorAssignments = $(for ($j = 0; $j -le $i; $j++) { - " `$this.Prop$j = `$arg$j" - }) -join "`n" - - $methodReturnValue = $(for ($j = 0; $j -le $i; $j++) { - "`$arg$j" - }) -join " + " - - $methodArguments = $(for ($j = 0; $j -le $i; $j++) { - $j - }) -join ", " - - $addUpProperties = $(for ($j = 0; $j -le $i; $j++) { - "`$inst.`Prop$j" - }) -join " + " - - $expectedTotal = (0..$i | Measure-Object -Sum).Sum - - $class = @" - class Foo - { -$properties - - Foo($methodParameters) - { -$ctorAssignments - } - - [int] DoSomething($methodParameters) - { - return $methodReturnValue - } - } - - `$inst = [Foo]::new($methodArguments) - `$sum = $addUpProperties - It "ExpectedTotal" { `$sum | should be $expectedTotal } - It "ExpectedTotal"{ `$inst.DoSomething($methodArguments) | should be $expectedTotal } -"@ - - Invoke-Expression $class - } -} - -Describe 'Scopes Test' -Tags "CI" { - class C1 - { - static C1() { - $global:foo = $script:foo - } - C1() { - $script:bar = $global:foo - } - static [int] f1() { - return $script:bar + $global:bar - } - [int] f2() { - return $script:bar + $global:bar - } - } -} - -Describe 'Check PS Class Assembly Test' -Tags "CI" { - class C1 {} - $assem = [C1].Assembly - $attrs = @($assem.GetCustomAttributes($true)) - $expectedAttr = @($attrs | Where-Object { $_ -is [System.Management.Automation.DynamicClassImplementationAssemblyAttribute] }) - It "Expected a DynamicClassImplementationAssembly attribute" { $expectedAttr.Length | should be 1} -} - -Describe 'ScriptScopeAccessFromClassMethod' -Tags "CI" { - Import-Module "$PSScriptRoot\MSFT_778492.psm1" - try - { - $c = Get-MSFT_778492 - It "Method should have found variable in module scope" { $c.F() | should be 'MSFT_778492 script scope'} - } - finally - { - Remove-Module MSFT_778492 - } -} - -Describe 'Hidden Members Test ' -Tags "CI" { - class C1 - { - [int]$visibleX - [int]$visibleY - hidden [int]$hiddenZ - } - - # Create an instance - $instance = [C1]@{ visibleX = 10; visibleY = 12; hiddenZ = 42 } - - It "Access hidden property should still work" { $instance.hiddenZ | should be 42 } - - - # Formatting should not include hidden members by default - $tableOutput = $instance | Format-Table -HideTableHeaders -AutoSize | Out-String - It "Table formatting should not have included hidden member hiddenZ - should contain 10" { $tableOutput.Contains(10) | should be $true} - It "Table formatting should not have included hidden member hiddenZ- should contain 12" { $tableOutput.Contains(12) | should be $true} - It "Table formatting should not have included hidden member hiddenZ - should not contain 42" { $tableOutput.Contains(42) | should be $false} - - # Get-Member should not include hidden members by default - $member = $instance | Get-Member hiddenZ - it "Get-Member should not find hidden member w/o -Force" { $member | should be $null } - - # Get-Member should include hidden members with -Force - $member = $instance | Get-Member hiddenZ -Force - It "Get-Member should find hidden member w/ -Force" { $member | should not be $null } - - # Tab completion should not return a hidden member - $line = 'class C2 { hidden [int]$hiddenZ } [C2]::new().h' - $completions = [System.Management.Automation.CommandCompletion]::CompleteInput($line, $line.Length, $null) - It "Tab completion should not return a hidden member" { $completions.CompletionMatches.Count | should be 0 } -} - -Describe 'BaseMethodCall Test ' -Tags "CI" { - It "Derived class method call" {"abc".ToString() | should be "abc" } - # call [object] ToString() method as a base class method. - It "Base class method call" {([object]"abc").ToString() | should be "System.String" } -} - -Describe 'Scoped Types Test' -Tags "CI" { - class C1 { [string] GetContext() { return "Test scope" } } - - filter f1 - { - class C1 { [string] GetContext() { return "f1 scope" } } - - return [C1]::new().GetContext() - } - - filter f2 - { - class C1 { [string] GetContext() { return "f2 scope" } } - - return (new-object C1).GetContext() - } - - It "New-Object at test scope" { (new-object C1).GetContext() | should be "Test scope" } - It "[C1]::new() at test scope" { [C1]::new().GetContext() | should be "Test scope" } - - It "[C1]::new() in nested scope" { (f1) | should be "f1 scope" } - It "'new-object C1' in nested scope" { (f2) | should be "f2 scope" } - - - It "[C1]::new() in nested scope (in pipeline)" { (1 | f1 | f2 | f1) | should be "f1 scope" } - It "'new-object C1' in nested scope (in pipeline)" { (1 | f2 | f1 | f2) | should be "f2 scope" } -} - -Describe 'ParameterOfClassTypeInModule Test' -Tags "CI" { - try - { - $sb = [scriptblock]::Create(@' -enum EE {one = 1} -function test-it([EE]$ee){$ee} -'@) - $mod = New-Module $sb -Name MSFT_2081529 | Import-Module - $result = test-it -ee one - It "Parameter of class/enum type defined in module should work" { $result | should be 1 } - } - finally - { - Remove-Module -ea ignore MSFT_2081529 - } -} - -Describe 'Type building' -Tags "CI" { - It 'should build the type only once for scriptblock' { - $a = $null - 1..10 | ForEach-Object { - class C {} - if ($a) { - $a -eq [C] | Should Be $true - } - $a = [C] - } - } - - It 'should create a new type every time scriptblock executed?' -Pending { - $sb = [scriptblock]::Create('class A {static [int] $a }; [A]::new()') - 1..2 | ForEach-Object { - $a = $sb.Invoke()[0] - ++$a::a | Should Be 1 - ++$a::a | Should Be 2 - } - } -} - -Describe 'RuntimeType created for TypeDefinitionAst' -Tags "CI" { - - It 'can make cast to the right RuntimeType in two different contexts' -pending { - - $ssfe = [System.Management.Automation.Runspaces.SessionStateFunctionEntry]::new("foo", @' -class Base -{ - [int] foo() { return 100 } -} - -class Derived : Base -{ - [int] foo() { return 2 * ([Base]$this).foo() } -} - -[Derived]::new().foo() -'@) - - $iss = [System.Management.Automation.Runspaces.initialsessionstate]::CreateDefault2() - $iss.Commands.Add($ssfe) - - $ps = [powershell]::Create($iss) - $ps.AddCommand("foo").Invoke() | Should be 200 - $ps.Streams.Error | Should Be $null - - $ps1 = [powershell]::Create($iss) - $ps1.AddCommand("foo").Invoke() | Should be 200 - $ps1.Streams.Error | Should Be $null - - $ps.Commands.Clear() - $ps.Streams.Error.Clear() - $ps.AddScript(". foo").Invoke() | Should be 200 - $ps.Streams.Error | Should Be $null - } -} - -Describe 'TypeTable lookups' -Tags "CI" { - - Context 'Call methods from a different thread' { - $b = [powershell]::Create().AddScript( -@' -class A {} -class B -{ - [object] getA1() { return New-Object A } - [object] getA2() { return [A]::new() } -} - -[B]::new() - -'@).Invoke()[0] - - It 'can do type lookup by name' { - $b.getA1() | Should Be 'A' - } - - It 'can do type lookup by [type]' { - $b.getA2() | Should Be 'A' - } - } -} - -Describe 'Protected method access' -Tags "CI" { - - Add-Type @' -namespace Foo -{ - public class Bar - { - protected int x {get; set;} - } -} -'@ - - It 'doesn''t allow protected methods access outside of inheritance chain' -pending { - $a = [scriptblock]::Create(@' -class A -{ - SetX([Foo.Bar]$bar, [int]$x) - { - $bar.x = $x - } - - [int] GetX([Foo.Bar]$bar) - { - Set-StrictMode -Version latest - return $bar.x - } -} -[A]::new() - -'@).Invoke() - $bar = [Foo.Bar]::new() - $throwCount = 0 - try { - $a.SetX($bar, 42) - } catch { - $_.FullyQualifiedErrorId | Should Be PropertyAssignmentException - $throwCount++ - } - try { - $a.GetX($bar) - } catch { - $_.FullyQualifiedErrorId | Should Be PropertyNotFoundStrict - $throwCount++ - } - $throwCount | Should Be 2 - } - - It 'can call protected methods sequentially from two different contexts' { - $ssfe = [System.Management.Automation.Runspaces.SessionStateFunctionEntry]::new("foo", @' -class A : Foo.Bar -{ - SetX([int]$x) - { - $this.x = $x - } - - [int] GetX() - { - return $this.x - } -} -return [A]::new() -'@) - - $iss = [System.Management.Automation.Runspaces.initialsessionstate]::CreateDefault() - $iss.Commands.Add($ssfe) - - $ps = [powershell]::Create($iss) - $a = $ps.AddCommand("foo").Invoke()[0] - $ps.Streams.Error | Should Be $null - - $ps1 = [powershell]::Create($iss) - $a1 = $ps1.AddCommand("foo").Invoke()[0] - $ps1.Streams.Error | Should Be $null - - $a.SetX(101) - $a1.SetX(103) - - $a.GetX() | Should Be 101 - $a1.GetX() | Should Be 103 - } -} - -Describe 'variable analysis' -Tags "CI" { - It 'can specify type construct on the local variables' { - class A { [string] getFoo() { return 'foo'} } - - class B - { - static [A] getA () - { - [A] $var = [A]::new() - return $var - } - } - - [B]::getA().getFoo() | Should Be 'foo' - } -} - -} finally { - $global:PSdefaultParameterValues = $defaultParamValues -} +# +# Copyright (c) Microsoft Corporation, 2014 +# + +try { +# +# CrossGen'ed assemblies cause a hang to happen intermittently when running this test suite in Linux and macOS. +# The issue has been reported to CoreCLR team. We need to work around it for now with the following approach: +# 1. For pull request and push commit, build without '-CrossGen' and run the parsing tests +# 2. For daily build, build with '-CrossGen' but don't run the parsing tests +# In this way, we will continue to exercise these parsing tests for each CI build, and skip them for daily +# build to avoid a hang. +# Note: this change should be reverted once the 'CrossGen' issue is fixed by CoreCLR. The issue is tracked by +# https://github.com/dotnet/coreclr/issues/9745 +# +$isDailyBuild = $env:TRAVIS_EVENT_TYPE -eq 'cron' -or $env:TRAVIS_EVENT_TYPE -eq 'api' +$defaultParamValues = $PSdefaultParameterValues.Clone() +$IsSkipped = (!$IsWindows -and $isDailyBuild) +$PSDefaultParameterValues["it:skip"] = $IsSkipped +$PSDefaultParameterValues["ShouldBeParseError:SkipInTravisFullBuild"] = $IsSkipped + + +Describe 'Positive Parse Properties Tests' -Tags "CI" { + It 'PositiveParsePropertiesTest' { + # Just a bunch of random basic things here + # This test doesn't need to check anything, if there are + # any parse errors, the entire suite will fail because the + # script will fail to parse. + + # No members + class C1 {} + + # Simple field + class C2 { $x; } + + # Simple typed field + class C3 { [int] $x; } + + # Multiple fields, one line, last w/o semicolon + class C4 { $x; $y } + + # Multiple fields, multiple lines + class C5 + { + $x + $y + } + + # Static field + class C6 { static $x; } + + # Static field w/ type - order doesn't matter + class C7a { static [hashtable] $x; } + class C7b { [hashtable] static $x; } + + # Field using type defined in this scope + class C8a { [C1] $c1; } + class C8b { [c1] $c1; } + + # Field referring to self type + class C9 { [C9] $c9; } + + # Hidden fields + class C10a { hidden $x } + class C10b { hidden [int] $x } + class C10c { hidden static $x; static hidden $y } + class C10d { hidden static [int] $x; static hidden [int] $y } + } + + It 'Positive Parse Methods Tests' { + # Just a bunch of random basic things here + # This test doesn't need to check anything, if there are + # any parse errors, the entire suite will fail because the + # script will fail to parse. + + # No members + class C1 {} + + # Simple method + class C2 { f() {} } + + # Simple method with return type + class C3 { [int] f() { return 1 } } + + # Multiple methods, one line + class C4 { f() {} f1() {} } + + # Multiple methods w/ overloads + class C5 + { + f1() {} + f1($a) {} + } + + # Static method + class C6 { static f() {} } + + # Static method w/ return type + class C7 { static [hashtable] f1() { return @{} } } + + # Method using return type defined in this scope + class C8a { [C1] f1() { return [C1]::new() } } + class C8b { [c1] f1() { return [c1]::new() } } + + # Hidden methods + class C10a { hidden F() { } } + class C10b { hidden [void] F() { } } + class C10c { hidden static F() { } static hidden G() { } } + class C10d { hidden static [void] F() { } static hidden [void] G() { } } + + # return analysis + class C11a { [int]foo() { try {throw "foo"} catch {throw $_} } } + class C11b { [int]foo() { try {throw "foo"} finally {}; try {} catch {} } } + class C11c { [int]foo() { try {throw "foo"} catch [ArgumentException] {throw $_} catch {throw $_} } } + class C11d { [int]foo() { try {if (1 -eq 2) { throw "1"} else {throw "2"}} finally {} } } + class C11e { [int]foo() { try {throw "foo"} catch [ArgumentException] {throw $_} catch {return 1} } } + class C11f { [int]foo() { try {} finally {throw "bar"} } } + class C11g { [int]foo() { do { return 1 } while ($false) } } + class C11h { [int]foo() { try {throw "foo"} finally {} } } + + # local variables + class C12a { static f() { [bigint]$foo = 42 } } + class C12b { [void] f() { [bigint]$foo = 42 } } + class C12c { [void] f() { [System.Management.Automation.Host.Rectangle]$foo = [System.Management.Automation.Host.Rectangle]::new(0, 0, 0, 0) } } + } + + context "Positive ParseMethods return type Test" { + # Method with return type of self + class C9 { [C9] f() { return [C9]::new() } } + $c9 = [C9]::new().f() + It "Expected a C9 returned" { $c9.GetType().Name | should be C9 } + class C9a { [C9a[]] f() { return [C9a]::new() } } + $c9a = [C9a]::new().f() + It "Expected a C9a[] returned" { $c9a.GetType().Name | should be C9a[] } + class C9b { [System.Collections.Generic.List[C9b]] f() { return [C9b]::new() } } + $c9b = [C9b]::new().f() + It "Expected a System.Collections.Generic.List[C9b] returned" { $c9b -is [System.Collections.Generic.List[C9b]] | should be $true } + } + + It 'Positive ParseProperty Attributes Test' { + class C1a { [ValidateNotNull()][int]$x; } + class C1b + { + [ValidateNotNull()] + [int] + $x + } + class C1c + { + [ValidateNotNull()] + [int]$x + } + class C1d + { + [ValidateNotNull()][int] + $x + } + } + + It 'PositiveParseMethodAttributesTest' { + class C1a { [Obsolete()][int] f() { return 0 } } + class C1b + { + [Obsolete()] + [int] + f() { return 1 } + } + class C1c + { + [Obsolete("Message")] + [int] f() { return 0 } + } + class C1d + { + [Obsolete()][int] + f(){ return -1 } + } + } + + It 'Class Method Reference ConstantVars' { + class C1 + { + [bool] f1() { return $true } + [bool] f2() { return $false } + [object] f3() { return $null } + } + } + + It 'Positive Parsing Of DscResource' { + [DscResource()] + class C1 + { + [DscProperty(Key)][string]$Key + [bool] Test() { return $false } + [C1] Get() { return $this } + Set() {} + } + + [DscResource()] + class C2 + { + [DscProperty(Key)][int]$Key = 1 + [bool] Test() { return $false } + [C2] Get() { return $this } + Set() {} + } + + [DscResource()] + class C4 + { + [DscProperty(Key)][byte]$Key=1 + C4() { } + C4($a) { } + [bool] Test() { return $false } + [C4] Get() { return $this } + Set() {} + } + + [DscResource()] + class C5 + { + [DscProperty(Key)][int]$Key = 1 + C5() { } + static C5() { } + C5($a) { } + [bool] Test() { return $false } + [C5] Get() { return $this } + Set() {} + } + } + + It 'allows some useful implicit variables inside methods' { + class C { + [void] m() + { + $LASTEXITCODE + $lastexitcode + '111' -match '1' + $Matches + $mAtches + $Error[0] + $error + $pwd + foreach ($i in 1..10) {$foreach} + switch ($i) + { + '1' { + $switch + } + } + } + } + } + + It 'allowes [ordered] attribute inside methods' { + class A + { + $h + A() + { + $this.h = [ordered] @{} + } + } + [A]::new().h.GetType().Name | Should Be 'OrderedDictionary' + } +} + +Describe 'Negative Parsing Tests' -Tags "CI" { + ShouldBeParseError 'class' MissingNameAfterKeyword 5 + ShouldBeParseError 'class foo' MissingTypeBody 9 + ShouldBeParseError 'class foo {' MissingEndCurlyBrace 11 + ShouldBeParseError 'class foo { [int] }' IncompleteMemberDefinition 17 + ShouldBeParseError 'class foo { $private: }' InvalidVariableReference 12 + ShouldBeParseError 'class foo { [int]$global: }' InvalidVariableReference 17 + ShouldBeParseError 'class foo { [Attr()] }' IncompleteMemberDefinition 20 + ShouldBeParseError 'class foo {} class foo {}' MemberAlreadyDefined 13 + ShouldBeParseError 'class foo { $x; $x; }' MemberAlreadyDefined 16 -SkipAndCheckRuntimeError + ShouldBeParseError 'class foo { [int][string]$x; }' TooManyTypes 17 + ShouldBeParseError 'class foo { [int][string]$x; }' TooManyTypes 17 + ShouldBeParseError 'class foo { static static $x; }' DuplicateQualifier 19 + ShouldBeParseError 'class foo { [zz]$x; }' TypeNotFound 13 + ShouldBeParseError 'class foo { [zz]f() { return 0 } }' TypeNotFound 13 + ShouldBeParseError 'class foo { f([zz]$x) {} }' TypeNotFound 15 + + ShouldBeParseError 'class C {} class C {}' MemberAlreadyDefined 11 + ShouldBeParseError 'class C { f(){} f(){} }' MemberAlreadyDefined 16 -SkipAndCheckRuntimeError + ShouldBeParseError 'class C { F(){} F($o){} [int] F($o) {return 1} }' MemberAlreadyDefined 24 -SkipAndCheckRuntimeError + ShouldBeParseError 'class C { f(){} f($a){} f(){} }' MemberAlreadyDefined 24 -SkipAndCheckRuntimeError + ShouldBeParseError 'class C { f([int]$a){} f([int]$b){} }' MemberAlreadyDefined 23 -SkipAndCheckRuntimeError + ShouldBeParseError 'class C { $x; [int]$x; }' MemberAlreadyDefined 14 -SkipAndCheckRuntimeError + ShouldBeParseError 'class C { static C($x) {} }' StaticConstructorCantHaveParameters 19 -SkipAndCheckRuntimeError + ShouldBeParseError 'class C { static C([int]$x = 100) {} }' StaticConstructorCantHaveParameters 19 -SkipAndCheckRuntimeError + + ShouldBeParseError 'class C {f(){ return 1} }' VoidMethodHasReturn 14 + ShouldBeParseError 'class C {[int] f(){ return } }' NonVoidMethodMissingReturnValue 20 + ShouldBeParseError 'class C {[int] f(){} }' MethodHasCodePathNotReturn 15 + ShouldBeParseError 'class C {f(){ $x=1; if($x -lt 2){ return } elseif($x -gt 0 ) {return 1} else{return 2} return 3 } }' @("VoidMethodHasReturn", "VoidMethodHasReturn", "VoidMethodHasReturn") @(62,77,87) + + ShouldBeParseError 'class foo { [int] bar() { $y = $z; return $y} }' VariableNotLocal 31 + ShouldBeParseError 'class foo { bar() { foreach ($zz in $yy) { } } }' VariableNotLocal 36 + ShouldBeParseError 'class foo { bar() { foreach ($zz in $global:yy) { $abc = $zzzzz } } }' VariableNotLocal 57 + ShouldBeParseError 'class foo { bar() { try { $zz = 42 } finally { } $zz } }' VariableNotLocal 49 + ShouldBeParseError 'class foo { bar() { try { $zz = 42 } catch { } $zz } }' VariableNotLocal 47 + ShouldBeParseError 'class foo { bar() { switch (@()) { default { $aa = 42 } } $aa } }' VariableNotLocal 58 + ShouldBeParseError 'class C { $x; static bar() { $this.x = 1 } }' NonStaticMemberAccessInStaticMember 29 + ShouldBeParseError 'class C { $x; static $y = $this.x }' NonStaticMemberAccessInStaticMember 26 + + ShouldBeParseError 'class C { $x; static bar() { $this.x = 1 } }' NonStaticMemberAccessInStaticMember 29 + ShouldBeParseError 'class C { $x; static $y = $this.x }' NonStaticMemberAccessInStaticMember 26 + ShouldBeParseError 'class C { $x; static bar() { $This.x = 1 } }' NonStaticMemberAccessInStaticMember 29 + ShouldBeParseError 'class C { $x; static $y = $This.x }' NonStaticMemberAccessInStaticMember 26 + + ShouldBeParseError 'class C { [void]foo() { try { throw "foo"} finally { return } } }' ControlLeavingFinally 53 + ShouldBeParseError 'class C { [int]foo() { return; return 1 } }' NonVoidMethodMissingReturnValue 23 + ShouldBeParseError 'class C { [int]foo() { try { throw "foo"} catch { } } }' MethodHasCodePathNotReturn 15 + ShouldBeParseError 'class C { [int]foo() { try { throw "foo"} catch [ArgumentException] {} catch {throw $_} } }' MethodHasCodePathNotReturn 15 + ShouldBeParseError 'class C { [int]foo() { try { throw "foo"} catch [ArgumentException] {return 1} catch {} } }' MethodHasCodePathNotReturn 15 + ShouldBeParseError 'class C { [int]foo() { while ($false) { return 1 } } }' MethodHasCodePathNotReturn 15 + ShouldBeParseError 'class C { [int]foo() { try { mkdir foo } finally { rm -rec foo } } }' MethodHasCodePathNotReturn 15 + ShouldBeParseError 'class C { [int]foo() { try { mkdir foo; return 1 } catch { } } }' MethodHasCodePathNotReturn 15 + ShouldBeParseError 'class C { [bool] Test() { if ($false) { return $true; } } }' MethodHasCodePathNotReturn 17 + + ShouldBeParseError 'class C { [int]$i; [void] foo() {$i = 10} }' MissingThis 33 + ShouldBeParseError 'class C { static [int]$i; [void] foo() {$i = 10} }' MissingTypeInStaticPropertyAssignment 40 + + ShouldBeParseError 'class C : B' MissingTypeBody 11 + + ShouldBeParseError 'Class foo { q(){} w(){}' MissingEndCurlyBrace 11 +} + +Describe 'Negative methods Tests' -Tags "CI" { + ShouldBeParseError 'class foo { f() { param($x) } }' ParamBlockNotAllowedInMethod 18 + ShouldBeParseError 'class foo { f() { dynamicparam {} } }' NamedBlockNotAllowedInMethod 18 + ShouldBeParseError 'class foo { f() { begin {} } }' NamedBlockNotAllowedInMethod 18 + ShouldBeParseError 'class foo { f() { process {} } }' NamedBlockNotAllowedInMethod 18 + ShouldBeParseError 'class foo { f() { end {} } }' NamedBlockNotAllowedInMethod 18 + ShouldBeParseError 'class foo { f([Parameter()]$a) {} }' AttributeNotAllowedOnDeclaration 14 + ShouldBeParseError 'class foo { [int] foo() { return 1 }}' ConstructorCantHaveReturnType 12 + ShouldBeParseError 'class foo { [void] bar($a, [string][int]$b, $c) {} }' MultipleTypeConstraintsOnMethodParam 35 +} + +Describe 'Negative Assignment Tests' -Tags "CI" { + ShouldBeParseError 'class foo { [string ]$path; f() { $path="" } }' MissingThis 34 + ShouldBeParseError 'class foo { [string ]$path; f() { [string] $path="" } }' MissingThis 43 + ShouldBeParseError 'class foo { [string ]$path; f() { [int] [string] $path="" } }' MissingThis 49 +} + +Describe 'Negative Assignment Tests' -Tags "CI" { + ShouldBeParseError '[DscResource()]class C { [bool] Test() { return $false } [C] Get() { return $this } Set() {} }' DscResourceMissingKeyProperty 0 + + # Test method + ShouldBeParseError '[DscResource()]class C { [DscProperty(Key)][string]$Key; [C] Get() { return $this } Set() {} }' DscResourceMissingTestMethod 0 + ShouldBeParseError '[DscResource()]class C { [DscProperty(Key)][string]$Key; [C] Get() { return $this } Set() {} Test() { } }' DscResourceMissingTestMethod 0 + ShouldBeParseError '[DscResource()]class C { [DscProperty(Key)][string]$Key; [C] Get() { return $this } Set() {} [int] Test() { return 1 } }' DscResourceMissingTestMethod 0 + ShouldBeParseError '[DscResource()]class C { [DscProperty(Key)][string]$Key; [C] Get() { return $this } Set() {} [bool] Test($a) { return $false } }' DscResourceMissingTestMethod 0 + + # Get method + ShouldBeParseError '[DscResource()]class C { [DscProperty(Key)][string]$Key; [bool] Test() { return $false } Set() {} }' DscResourceMissingGetMethod 0 + ShouldBeParseError '[DscResource()]class C { [DscProperty(Key)][string]$Key; [bool] Test() { return $false } Set() {} Get() { } }' DscResourceInvalidGetMethod 98 + ShouldBeParseError '[DscResource()]class C { [DscProperty(Key)][string]$Key; [bool] Test() { return $false } Set() {} [int] Get() { return 1 } }' DscResourceInvalidGetMethod 98 + ShouldBeParseError '[DscResource()]class C { [DscProperty(Key)][string]$Key; [bool] Test() { return $false } Set() {} [C] Get($a) { return $this } }' DscResourceMissingGetMethod 0 + + # Set method + ShouldBeParseError '[DscResource()]class C { [DscProperty(Key)][string]$Key; [bool] Test() { return $false } [C] Get() { return $this } }' DscResourceMissingSetMethod 0 + ShouldBeParseError '[DscResource()]class C { [DscProperty(Key)][string]$Key; [bool] Test() { return $false } [C] Get() { return $this } [int] Set() { return 1 } }' DscResourceMissingSetMethod 0 + ShouldBeParseError '[DscResource()]class C { [DscProperty(Key)][string]$Key; [bool] Test() { return $false } [C] Get() { return $this } Set($a) { } }' DscResourceMissingSetMethod 0 + + # Default ctor + ShouldBeParseError '[DscResource()]class C { [DscProperty(Key)][string]$Key; [bool] Test() { return $false } [C] Get() { return $this } Set() {} C($a) { } }' DscResourceMissingDefaultConstructor 0 +} + +Describe 'Negative DscResources Tests' -Tags "CI" { + # Usage errors + ShouldBeParseError '[Flags()]class C{}' AttributeNotAllowedOnDeclaration 0 + ShouldBeParseError 'class C { [Flags()]$field; }' AttributeNotAllowedOnDeclaration 10 + ShouldBeParseError 'class C { [Flags()]foo(){} }' AttributeNotAllowedOnDeclaration 10 + + # Errors related to construction of the attribute + ShouldBeParseError '[UnknownAttr()]class C{}' CustomAttributeTypeNotFound 1 + ShouldBeParseError '[System.Management.Automation.Cmdlet()]class C{}' MethodCountCouldNotFindBest 0 -SkipAndCheckRuntimeError + ShouldBeParseError '[System.Management.Automation.Cmdlet("zz")]class C{}' MethodCountCouldNotFindBest 0 -SkipAndCheckRuntimeError + ShouldBeParseError '[System.Management.Automation.Cmdlet("Get", "Thing", Prop=1)]class C{}' PropertyNotFoundForAttribute 53 + ShouldBeParseError '[System.Management.Automation.Cmdlet("Get", "Thing", ConfirmImpact="foo")]class C{}' CannotConvertValue 67 -SkipAndCheckRuntimeError + ShouldBeParseError '[System.Management.Automation.Cmdlet("Get", "Thing", NounName="foo")]class C{}' ReadOnlyProperty 53 + ShouldBeParseError '[System.Management.Automation.Cmdlet("Get", "Thing", ConfirmImpact=$zed)]class C{}' ParameterAttributeArgumentNeedsToBeConstant 67 + ShouldBeParseError 'class C{ [ValidateScript({})]$p; }' ParameterAttributeArgumentNeedsToBeConstant 25 +} + +Describe 'Negative ClassAttributes Tests' -Tags "CI" { + [System.Management.Automation.Cmdlet("Get", "Thing")]class C{} + $t = [C].GetCustomAttributes($false) + + It "Should have one attribute" {$t.Count | should be 1} + It "Should have instance of CmdletAttribute" {$t[0].GetType().FullName | should be System.Management.Automation.CmdletAttribute } + + [System.Management.Automation.CmdletAttribute]$c = $t[0] + It "Verb should be Get" {$c.VerbName | should be 'Get'} + It "Noun should be Thing" {$c.NounName | should be 'Thing'} + + [System.Management.Automation.Cmdlet("Get", "Thing", SupportsShouldProcess = $true, SupportsPaging = $true)]class C2{} + $t = [C2].GetCustomAttributes($false) + It "Should have one attribute" { $t.Count | should be 1 } + It "Should have instance of CmdletAttribute" { $t[0].GetType().FullName | should be System.Management.Automation.CmdletAttribute } + [System.Management.Automation.CmdletAttribute]$c = $t[0] + It "Verb should be Get" {$c.VerbName | should be 'Get'} + It "Noun should be Thing" {$c.NounName | should be 'Thing'} + + It "SupportsShouldProcess should be $true" { $c.ConfirmImpact | should be $true } + It "SupportsPaging should be `$true" { $c.SupportsPaging | should be $true } + Context "Support ConfirmImpact as an attribute" { + It "ConfirmImpact should be high" -pending { + [System.Management.Automation.Cmdlet("Get", "Thing", ConfirmImpact = 'High', SupportsPaging = $true)]class C3{} + $t = [C3].GetCustomAttributes($false) + It "Should have one attribute" { $t.Count | should be 1 } + It "Should have instance of CmdletAttribute" { $t[0].GetType().FullName | should be System.Management.Automation.CmdletAttribute } + [System.Management.Automation.CmdletAttribute]$c = $t[0] + $c.ConfirmImpact | should be 'High' + + } + } +} + +Describe 'Property Attributes Test' -Tags "CI" { + class C { [ValidateSet('a', 'b')]$p; } + + $t = [C].GetProperty('p').GetCustomAttributes($false) + It "Should have one attribute" { $t.Count | should be 1 } + [ValidateSet]$v = $t[0] + It "Should have 2 valid values" { $v.ValidValues.Count | should be 2 } + It "first value should be a" { $v.ValidValues[0] | should be 'a' } + It "second value should be b" { $v.ValidValues[1] | should be 'b' } +} + +Describe 'Method Attributes Test' -Tags "CI" { + class C { [Obsolete("aaa")][int]f() { return 1 } } + + $t = [C].GetMethod('f').GetCustomAttributes($false) + It "Should have one attribute" {$t.Count | should be 1 } + It "Attribute type should be ObsoleteAttribute" { $t[0].GetType().FullName | should be System.ObsoleteAttribute } +} + +Describe 'Positive SelfClass Type As Parameter Test' -Tags "CI" { + class Point + { + Point($x, $y) { $this.x = $x; $this.y = $y } + Point() {} + + [int] $x = 0 + [int] $y = 0 + Add([Point] $val) { $this.x += $val.x; $this.y += $val.y; } + + Print() { Write-Host "[`$x=$($this.x) `$y=$($this.y)]" } + Set($x, $y) { $this.x = $x; $this.y = $y } + } + It "[Point]::Add works" { + $point = [Point]::new(100,200) + $point2 = [Point]::new(1,2) + $point.Add($point2) + + $point.x | should be 101 + $point.y | should be 202 + } + + It "[Point]::Add works" { + $point = New-Object Point 100,200 + $point2 = New-Object Point 1,2 + $point.Add($point2) + + $point.x | should be 101 + $point.y | should be 202 + } +} + +Describe 'PositiveReturnSelfClassTypeFromMemberFunction Test' -Tags "CI" { + class ReturnObjectFromMemberFunctionTest + { + [ReturnObjectFromMemberFunctionTest] CreateInstance() + { + return [ReturnObjectFromMemberFunctionTest]::new() + } + [string] SayHello() + { + return "Hello1" + } + } + $f = [ReturnObjectFromMemberFunctionTest]::new() + $z = $f.CreateInstance() # Line 13 + It "CreateInstance works" { $z.SayHello() | should be 'Hello1' } +} + +Describe 'TestMultipleArguments Test' -Tags "CI" { + if ( $IsCoreCLR ) { $maxCount = 14 } else { $maxCount = 16 } + for ($i = 0; $i -lt $maxCount; $i++) + { + $properties = $(for ($j = 0; $j -le $i; $j++) { + " [int]`$Prop$j" + }) -join "`n" + + $methodParameters = $(for ($j = 0; $j -le $i; $j++) { + "[int]`$arg$j" + }) -join ", " + + $ctorAssignments = $(for ($j = 0; $j -le $i; $j++) { + " `$this.Prop$j = `$arg$j" + }) -join "`n" + + $methodReturnValue = $(for ($j = 0; $j -le $i; $j++) { + "`$arg$j" + }) -join " + " + + $methodArguments = $(for ($j = 0; $j -le $i; $j++) { + $j + }) -join ", " + + $addUpProperties = $(for ($j = 0; $j -le $i; $j++) { + "`$inst.`Prop$j" + }) -join " + " + + $expectedTotal = (0..$i | Measure-Object -Sum).Sum + + $class = @" + class Foo + { +$properties + + Foo($methodParameters) + { +$ctorAssignments + } + + [int] DoSomething($methodParameters) + { + return $methodReturnValue + } + } + + `$inst = [Foo]::new($methodArguments) + `$sum = $addUpProperties + It "ExpectedTotal" { `$sum | should be $expectedTotal } + It "ExpectedTotal"{ `$inst.DoSomething($methodArguments) | should be $expectedTotal } +"@ + + Invoke-Expression $class + } +} + +Describe 'Scopes Test' -Tags "CI" { + class C1 + { + static C1() { + $global:foo = $script:foo + } + C1() { + $script:bar = $global:foo + } + static [int] f1() { + return $script:bar + $global:bar + } + [int] f2() { + return $script:bar + $global:bar + } + } +} + +Describe 'Check PS Class Assembly Test' -Tags "CI" { + class C1 {} + $assem = [C1].Assembly + $attrs = @($assem.GetCustomAttributes($true)) + $expectedAttr = @($attrs | Where-Object { $_ -is [System.Management.Automation.DynamicClassImplementationAssemblyAttribute] }) + It "Expected a DynamicClassImplementationAssembly attribute" { $expectedAttr.Length | should be 1} +} + +Describe 'ScriptScopeAccessFromClassMethod' -Tags "CI" { + Import-Module "$PSScriptRoot\MSFT_778492.psm1" + try + { + $c = Get-MSFT_778492 + It "Method should have found variable in module scope" { $c.F() | should be 'MSFT_778492 script scope'} + } + finally + { + Remove-Module MSFT_778492 + } +} + +Describe 'Hidden Members Test ' -Tags "CI" { + class C1 + { + [int]$visibleX + [int]$visibleY + hidden [int]$hiddenZ + } + + # Create an instance + $instance = [C1]@{ visibleX = 10; visibleY = 12; hiddenZ = 42 } + + It "Access hidden property should still work" { $instance.hiddenZ | should be 42 } + + + # Formatting should not include hidden members by default + $tableOutput = $instance | Format-Table -HideTableHeaders -AutoSize | Out-String + It "Table formatting should not have included hidden member hiddenZ - should contain 10" { $tableOutput.Contains(10) | should be $true} + It "Table formatting should not have included hidden member hiddenZ- should contain 12" { $tableOutput.Contains(12) | should be $true} + It "Table formatting should not have included hidden member hiddenZ - should not contain 42" { $tableOutput.Contains(42) | should be $false} + + # Get-Member should not include hidden members by default + $member = $instance | Get-Member hiddenZ + it "Get-Member should not find hidden member w/o -Force" { $member | should be $null } + + # Get-Member should include hidden members with -Force + $member = $instance | Get-Member hiddenZ -Force + It "Get-Member should find hidden member w/ -Force" { $member | should not be $null } + + # Tab completion should not return a hidden member + $line = 'class C2 { hidden [int]$hiddenZ } [C2]::new().h' + $completions = [System.Management.Automation.CommandCompletion]::CompleteInput($line, $line.Length, $null) + It "Tab completion should not return a hidden member" { $completions.CompletionMatches.Count | should be 0 } +} + +Describe 'BaseMethodCall Test ' -Tags "CI" { + It "Derived class method call" {"abc".ToString() | should be "abc" } + # call [object] ToString() method as a base class method. + It "Base class method call" {([object]"abc").ToString() | should be "System.String" } +} + +Describe 'Scoped Types Test' -Tags "CI" { + class C1 { [string] GetContext() { return "Test scope" } } + + filter f1 + { + class C1 { [string] GetContext() { return "f1 scope" } } + + return [C1]::new().GetContext() + } + + filter f2 + { + class C1 { [string] GetContext() { return "f2 scope" } } + + return (new-object C1).GetContext() + } + + It "New-Object at test scope" { (new-object C1).GetContext() | should be "Test scope" } + It "[C1]::new() at test scope" { [C1]::new().GetContext() | should be "Test scope" } + + It "[C1]::new() in nested scope" { (f1) | should be "f1 scope" } + It "'new-object C1' in nested scope" { (f2) | should be "f2 scope" } + + + It "[C1]::new() in nested scope (in pipeline)" { (1 | f1 | f2 | f1) | should be "f1 scope" } + It "'new-object C1' in nested scope (in pipeline)" { (1 | f2 | f1 | f2) | should be "f2 scope" } +} + +Describe 'ParameterOfClassTypeInModule Test' -Tags "CI" { + try + { + $sb = [scriptblock]::Create(@' +enum EE {one = 1} +function test-it([EE]$ee){$ee} +'@) + $mod = New-Module $sb -Name MSFT_2081529 | Import-Module + $result = test-it -ee one + It "Parameter of class/enum type defined in module should work" { $result | should be 1 } + } + finally + { + Remove-Module -ea ignore MSFT_2081529 + } +} + +Describe 'Type building' -Tags "CI" { + It 'should build the type only once for scriptblock' { + $a = $null + 1..10 | ForEach-Object { + class C {} + if ($a) { + $a -eq [C] | Should Be $true + } + $a = [C] + } + } + + It 'should create a new type every time scriptblock executed?' -Pending { + $sb = [scriptblock]::Create('class A {static [int] $a }; [A]::new()') + 1..2 | ForEach-Object { + $a = $sb.Invoke()[0] + ++$a::a | Should Be 1 + ++$a::a | Should Be 2 + } + } +} + +Describe 'RuntimeType created for TypeDefinitionAst' -Tags "CI" { + + It 'can make cast to the right RuntimeType in two different contexts' -pending { + + $ssfe = [System.Management.Automation.Runspaces.SessionStateFunctionEntry]::new("foo", @' +class Base +{ + [int] foo() { return 100 } +} + +class Derived : Base +{ + [int] foo() { return 2 * ([Base]$this).foo() } +} + +[Derived]::new().foo() +'@) + + $iss = [System.Management.Automation.Runspaces.initialsessionstate]::CreateDefault2() + $iss.Commands.Add($ssfe) + + $ps = [powershell]::Create($iss) + $ps.AddCommand("foo").Invoke() | Should be 200 + $ps.Streams.Error | Should Be $null + + $ps1 = [powershell]::Create($iss) + $ps1.AddCommand("foo").Invoke() | Should be 200 + $ps1.Streams.Error | Should Be $null + + $ps.Commands.Clear() + $ps.Streams.Error.Clear() + $ps.AddScript(". foo").Invoke() | Should be 200 + $ps.Streams.Error | Should Be $null + } +} + +Describe 'TypeTable lookups' -Tags "CI" { + + Context 'Call methods from a different thread' { + $b = [powershell]::Create().AddScript( +@' +class A {} +class B +{ + [object] getA1() { return New-Object A } + [object] getA2() { return [A]::new() } +} + +[B]::new() + +'@).Invoke()[0] + + It 'can do type lookup by name' { + $b.getA1() | Should Be 'A' + } + + It 'can do type lookup by [type]' { + $b.getA2() | Should Be 'A' + } + } +} + +Describe 'Protected method access' -Tags "CI" { + + Add-Type @' +namespace Foo +{ + public class Bar + { + protected int x {get; set;} + } +} +'@ + + It 'doesn''t allow protected methods access outside of inheritance chain' -pending { + $a = [scriptblock]::Create(@' +class A +{ + SetX([Foo.Bar]$bar, [int]$x) + { + $bar.x = $x + } + + [int] GetX([Foo.Bar]$bar) + { + Set-StrictMode -Version latest + return $bar.x + } +} +[A]::new() + +'@).Invoke() + $bar = [Foo.Bar]::new() + $throwCount = 0 + try { + $a.SetX($bar, 42) + } catch { + $_.FullyQualifiedErrorId | Should Be PropertyAssignmentException + $throwCount++ + } + try { + $a.GetX($bar) + } catch { + $_.FullyQualifiedErrorId | Should Be PropertyNotFoundStrict + $throwCount++ + } + $throwCount | Should Be 2 + } + + It 'can call protected methods sequentially from two different contexts' { + $ssfe = [System.Management.Automation.Runspaces.SessionStateFunctionEntry]::new("foo", @' +class A : Foo.Bar +{ + SetX([int]$x) + { + $this.x = $x + } + + [int] GetX() + { + return $this.x + } +} +return [A]::new() +'@) + + $iss = [System.Management.Automation.Runspaces.initialsessionstate]::CreateDefault() + $iss.Commands.Add($ssfe) + + $ps = [powershell]::Create($iss) + $a = $ps.AddCommand("foo").Invoke()[0] + $ps.Streams.Error | Should Be $null + + $ps1 = [powershell]::Create($iss) + $a1 = $ps1.AddCommand("foo").Invoke()[0] + $ps1.Streams.Error | Should Be $null + + $a.SetX(101) + $a1.SetX(103) + + $a.GetX() | Should Be 101 + $a1.GetX() | Should Be 103 + } +} + +Describe 'variable analysis' -Tags "CI" { + It 'can specify type construct on the local variables' { + class A { [string] getFoo() { return 'foo'} } + + class B + { + static [A] getA () + { + [A] $var = [A]::new() + return $var + } + } + + [B]::getA().getFoo() | Should Be 'foo' + } +} + +} finally { + $global:PSdefaultParameterValues = $defaultParamValues +} diff --git a/test/powershell/Language/Classes/Scripting.Classes.Break.Tests.ps1 b/test/powershell/Language/Classes/Scripting.Classes.Break.Tests.ps1 index b4d15b3eddc..3274bf80947 100644 --- a/test/powershell/Language/Classes/Scripting.Classes.Break.Tests.ps1 +++ b/test/powershell/Language/Classes/Scripting.Classes.Break.Tests.ps1 @@ -1,145 +1,145 @@ -Describe 'Break statements with classes' -Tags "CI" { - - function Get-Errors([string]$sourceCode) { - $tokens = $null - $errors = $null - $ast = [System.Management.Automation.Language.Parser]::ParseInput($sourceCode, [ref] $tokens, [ref] $errors) - return $errors - } - - Context 'break is inside a class method' { - It 'reports parse error for break on non-existing label' { - $errors = Get-Errors @' -class A -{ - static [int] foo() - { - while (1) { break some_label } - return 1 - } -} -'@ - $errors.Count | Should be 1 - $errors[0].ErrorId | Should be 'LabelNotFound' - } - - It 'reports parse error for break outside of loop' { - $errors = Get-Errors @' -class A -{ - static [int] foo() - { - break some_label - return 1 - } -} -'@ - $errors.Count | Should be 1 - $errors[0].ErrorId | Should be 'LabelNotFound' - } - - It 'work fine, when break is legit' { - class C - { - static [int] foo() - { - foreach ($i in 101..102) { - break - } - return $i - } - } - [C]::foo() | Should be 101 - } - } - - Context 'continue inside a class method' { - It 'reports parse error for continue on non-existing label' { - $errors = Get-Errors @' -class A -{ - static [int] foo() - { - while (1) { continue some_label } - return 1 - } -} -'@ - $errors.Count | Should be 1 - $errors[0].ErrorId | Should be 'LabelNotFound' - } - } - - Context 'break is in called function' { - It 'doesn''t terminate caller method' -Skip { - - function ImBreak() { - break - } - - class C - { - static [int] getInt() - { - ImBreak - return 123 - } - } - - $canary = $false - try { - [C]::getInt() | Should Be 123 - $canary = $true - } finally { - $canary | Should be $true - } - } - - It 'doesn''t allow goto outside of function with break' -Skip { - - function ImBreak() { - break label1 - } - - class C - { - static [int] getInt() - { - $count = 123 - :label1 - foreach ($i in 0..3) { - foreach ($i in 0..3) { - ImBreak - $count++ - } - } - return $count - } - } - - $canary = $false - try { - [C]::getInt() | Should Be (123 + 4*4) - $canary = $true - } finally { - $canary | Should be $true - } - } - } - - Context 'no classes involved' { - - It 'doesn''t report parse error for non-existing label' { - $errors = Get-Errors @' -function foo() -{ - while (1) { break some_label } - while (1) { continue another_label } - return 1 -} -'@ - $errors.Count | Should be 0 - } - - } -} +Describe 'Break statements with classes' -Tags "CI" { + + function Get-Errors([string]$sourceCode) { + $tokens = $null + $errors = $null + $ast = [System.Management.Automation.Language.Parser]::ParseInput($sourceCode, [ref] $tokens, [ref] $errors) + return $errors + } + + Context 'break is inside a class method' { + It 'reports parse error for break on non-existing label' { + $errors = Get-Errors @' +class A +{ + static [int] foo() + { + while (1) { break some_label } + return 1 + } +} +'@ + $errors.Count | Should be 1 + $errors[0].ErrorId | Should be 'LabelNotFound' + } + + It 'reports parse error for break outside of loop' { + $errors = Get-Errors @' +class A +{ + static [int] foo() + { + break some_label + return 1 + } +} +'@ + $errors.Count | Should be 1 + $errors[0].ErrorId | Should be 'LabelNotFound' + } + + It 'work fine, when break is legit' { + class C + { + static [int] foo() + { + foreach ($i in 101..102) { + break + } + return $i + } + } + [C]::foo() | Should be 101 + } + } + + Context 'continue inside a class method' { + It 'reports parse error for continue on non-existing label' { + $errors = Get-Errors @' +class A +{ + static [int] foo() + { + while (1) { continue some_label } + return 1 + } +} +'@ + $errors.Count | Should be 1 + $errors[0].ErrorId | Should be 'LabelNotFound' + } + } + + Context 'break is in called function' { + It 'doesn''t terminate caller method' -Skip { + + function ImBreak() { + break + } + + class C + { + static [int] getInt() + { + ImBreak + return 123 + } + } + + $canary = $false + try { + [C]::getInt() | Should Be 123 + $canary = $true + } finally { + $canary | Should be $true + } + } + + It 'doesn''t allow goto outside of function with break' -Skip { + + function ImBreak() { + break label1 + } + + class C + { + static [int] getInt() + { + $count = 123 + :label1 + foreach ($i in 0..3) { + foreach ($i in 0..3) { + ImBreak + $count++ + } + } + return $count + } + } + + $canary = $false + try { + [C]::getInt() | Should Be (123 + 4*4) + $canary = $true + } finally { + $canary | Should be $true + } + } + } + + Context 'no classes involved' { + + It 'doesn''t report parse error for non-existing label' { + $errors = Get-Errors @' +function foo() +{ + while (1) { break some_label } + while (1) { continue another_label } + return 1 +} +'@ + $errors.Count | Should be 0 + } + + } +} diff --git a/test/powershell/Language/Classes/Scripting.Classes.Exceptions.Tests.ps1 b/test/powershell/Language/Classes/Scripting.Classes.Exceptions.Tests.ps1 index 5b95b3f6513..258354c5062 100644 --- a/test/powershell/Language/Classes/Scripting.Classes.Exceptions.Tests.ps1 +++ b/test/powershell/Language/Classes/Scripting.Classes.Exceptions.Tests.ps1 @@ -1,349 +1,349 @@ -Describe 'Exceptions flow for classes' -Tags "CI" { - - $canaryHashtable = @{} - - $iss = [initialsessionstate]::CreateDefault() - $iss.Variables.Add([System.Management.Automation.Runspaces.SessionStateVariableEntry]::new('canaryHashtable', $canaryHashtable, $null)) - $iss.Commands.Add([System.Management.Automation.Runspaces.SessionStateFunctionEntry]::new('Get-Canary', '$canaryHashtable')) - $ps = [powershell]::Create($iss) - - BeforeEach { - $canaryHashtable.Clear() - $ps.Commands.Clear() - } - - Context 'All calls are inside classes' { - - It 'does not execute statements after instance method with exception' { - - # Put try-catch outside to avoid try-catch logic altering analysis - try { - - $ps.AddScript( @' -class C -{ - [void] m1() - { - $canaryHashtable = Get-Canary - $canaryHashtable['canary'] = 42 - $this.ImThrow() - $canaryHashtable['canary'] = 100 - } - - [void] ImThrow() - { - throw 'I told you' - } -} -[C]::new().m1() -'@).Invoke() - - } catch {} - - $canaryHashtable['canary'] | Should Be 42 - } - - It 'does not execute statements after static method with exception' { - - # Put try-catch outside to avoid try-catch logic altering analysis - try { - - $ps.AddScript( @' -class C -{ - static [void] s1() - { - $canaryHashtable = Get-Canary - $canaryHashtable['canary'] = 43 - [C]::ImThrow() - $canaryHashtable['canary'] = 100 - } - - static [void] ImThrow() - { - 1 / 0 - } -} -[C]::s1() -'@).Invoke() - - } catch {} - - $canaryHashtable['canary'] | Should Be 43 - } - - It 'does not execute statements after instance method with exception and deep stack' { - - # Put try-catch outside to avoid try-catch logic altering analysis - try { - - $ps.AddScript( @' -class C -{ - [void] m1() - { - $canaryHashtable = Get-Canary - $canaryHashtable['canary'] = 1 - $this.m2() - $canaryHashtable['canary'] = -6101 - } - - [void] m2() - { - $canaryHashtable = Get-Canary - $canaryHashtable['canary'] += 10 - $this.m3() - $canaryHashtable['canary'] = -6102 - } - - [void] m3() - { - $canaryHashtable = Get-Canary - $canaryHashtable['canary'] += 100 - $this.m4() - $canaryHashtable['canary'] = -6103 - } - - [void] m4() - { - $canaryHashtable = Get-Canary - $canaryHashtable['canary'] += 1000 - $this.ImThrow() - $canaryHashtable['canary'] = -6104 - } - - [void] ImThrow() - { - $canaryHashtable = Get-Canary - $canaryHashtable['canary'] += 10000 - - 1 / 0 - } -} -[C]::new().m1() -'@).Invoke() - - } catch {} - - $canaryHashtable['canary'] | Should Be 11111 - } - } - - Context 'Class method call PS function' { - - $body = @' -class C -{ - [void] m1() - { - m2 - } - - static [void] s1() - { - s2 - } -} - - -function m2() -{ - $canary = Get-Canary - $canary['canaryM'] = 45 - ImThrow - $canary['canaryM'] = 100 -} - -function s2() -{ - $canary = Get-Canary - $canary['canaryS'] = 46 - CallImThrow - $canary['canaryS'] = 100 -} - -function CallImThrow() -{ - ImThrow -} - -function ImThrow() -{ - 1 / 0 -} - -'@ - - It 'does not execute statements after function with exception called from instance method' { - - # Put try-catch outside to avoid try-catch logic altering analysis - try { - - $ps.AddScript($body).Invoke() - $ps.AddScript('$c = [C]::new(); $c.m1()').Invoke() - - } catch {} - - $canaryHashtable['canaryM'] | Should Be 45 - } - - It 'does not execute statements after function with exception called from static method' { - - # Put try-catch outside to avoid try-catch logic altering analysis - try { - - $ps.AddScript($body).Invoke() - $ps.AddScript('[C]::s1()').Invoke() - - } catch {} - - $canaryHashtable['canaryS'] | Should Be 46 - } - - } - - Context "No class is involved" { - It "functions calls continue execution by default" { - - try { - - $ps.AddScript( @' - -$canaryHashtable = Get-Canary -function foo() { 1 / 0; $canaryHashtable['canary'] += 10 } -$canaryHashtable['canary'] = 1 -foo -$canaryHashtable['canary'] += 100 - -'@).Invoke() - - } catch {} - - $canaryHashtable['canary'] | Should Be 111 - } - } -} - -Describe "Exception error position" -Tags "CI" { - class MSFT_3090412 - { - static f1() { [MSFT_3090412]::bar = 42 } - static f2() { throw "an error in f2" } - static f3() { "".Substring(0, 10) } - static f4() { dir nosuchfile -ea Stop } - } - - It "Setting a property that doesn't exist" { - try { - [MSFT_3090412]::f1() - throw "f1 should have thrown" - } catch { - $_.InvocationInfo.Line | Should Match ([regex]::Escape('[MSFT_3090412]::bar = 42')) - } - } - - It "Throwing an exception" { - try { - [MSFT_3090412]::f2() - throw "f2 should have thrown" - } catch { - $_.InvocationInfo.Line | Should Match ([regex]::Escape('throw "an error in f2"')) - } - } - - It "Calling a .Net method that throws" { - try { - [MSFT_3090412]::f3() - throw "f3 should have thrown" - } catch { - $_.InvocationInfo.Line | Should Match ([regex]::Escape('"".Substring(0, 10)')) - } - } - - It "Terminating error" { - try { - [MSFT_3090412]::f4() - throw "f4 should have thrown" - } catch { - $_.InvocationInfo.Line | Should Match ([regex]::Escape('dir nosuchfile -ea Stop')) - } - } -} - -Describe "Exception from initializer" -Tags "CI" { - class MSFT_6397334a - { - [int]$a = "zz" - MSFT_6397334a() {} - } - - class MSFT_6397334b - { - [int]$a = "zz" - } - - class MSFT_6397334c - { - static [int]$a = "zz" - static MSFT_6397334a() {} - } - - class MSFT_6397334d - { - static [int]$a = "zz" - } - - It "instance member w/ ctor" { - try { - [MSFT_6397334a]::new() - throw "[MSFT_6397334a]::new() should have thrown" - } - catch - { - $e = $_ - $e.FullyQualifiedErrorId | Should Be InvalidCastFromStringToInteger - $e.InvocationInfo.Line | Should Match 'a = "zz"' - } - } - - It "instance member w/o ctor" { - try { - [MSFT_6397334b]::new() - throw "[MSFT_6397334b]::new() should have thrown" - } - catch - { - $e = $_ - $e.FullyQualifiedErrorId | Should Be InvalidCastFromStringToInteger - $e.InvocationInfo.Line | Should Match 'a = "zz"' - } - } - - It "static member w/ ctor" { - try { - $null = [MSFT_6397334c]::a - throw "No Exception!" - } - catch - { - $_.Exception | Should BeOfType System.TypeInitializationException - $e = $_.Exception.InnerException.InnerException.ErrorRecord - $e.FullyQualifiedErrorId | Should Be InvalidCastFromStringToInteger - $e.InvocationInfo.Line | Should Match 'a = "zz"' - } - } - - It "static member w/o ctor" { - try { - $null = [MSFT_6397334d]::a - throw "No Exception!" - } - catch - { - $_.Exception | Should BeOfType System.TypeInitializationException - $e = $_.Exception.InnerException.InnerException.ErrorRecord - $e.FullyQualifiedErrorId | Should Be InvalidCastFromStringToInteger - $e.InvocationInfo.Line | Should Match 'a = "zz"' - } - } -} +Describe 'Exceptions flow for classes' -Tags "CI" { + + $canaryHashtable = @{} + + $iss = [initialsessionstate]::CreateDefault() + $iss.Variables.Add([System.Management.Automation.Runspaces.SessionStateVariableEntry]::new('canaryHashtable', $canaryHashtable, $null)) + $iss.Commands.Add([System.Management.Automation.Runspaces.SessionStateFunctionEntry]::new('Get-Canary', '$canaryHashtable')) + $ps = [powershell]::Create($iss) + + BeforeEach { + $canaryHashtable.Clear() + $ps.Commands.Clear() + } + + Context 'All calls are inside classes' { + + It 'does not execute statements after instance method with exception' { + + # Put try-catch outside to avoid try-catch logic altering analysis + try { + + $ps.AddScript( @' +class C +{ + [void] m1() + { + $canaryHashtable = Get-Canary + $canaryHashtable['canary'] = 42 + $this.ImThrow() + $canaryHashtable['canary'] = 100 + } + + [void] ImThrow() + { + throw 'I told you' + } +} +[C]::new().m1() +'@).Invoke() + + } catch {} + + $canaryHashtable['canary'] | Should Be 42 + } + + It 'does not execute statements after static method with exception' { + + # Put try-catch outside to avoid try-catch logic altering analysis + try { + + $ps.AddScript( @' +class C +{ + static [void] s1() + { + $canaryHashtable = Get-Canary + $canaryHashtable['canary'] = 43 + [C]::ImThrow() + $canaryHashtable['canary'] = 100 + } + + static [void] ImThrow() + { + 1 / 0 + } +} +[C]::s1() +'@).Invoke() + + } catch {} + + $canaryHashtable['canary'] | Should Be 43 + } + + It 'does not execute statements after instance method with exception and deep stack' { + + # Put try-catch outside to avoid try-catch logic altering analysis + try { + + $ps.AddScript( @' +class C +{ + [void] m1() + { + $canaryHashtable = Get-Canary + $canaryHashtable['canary'] = 1 + $this.m2() + $canaryHashtable['canary'] = -6101 + } + + [void] m2() + { + $canaryHashtable = Get-Canary + $canaryHashtable['canary'] += 10 + $this.m3() + $canaryHashtable['canary'] = -6102 + } + + [void] m3() + { + $canaryHashtable = Get-Canary + $canaryHashtable['canary'] += 100 + $this.m4() + $canaryHashtable['canary'] = -6103 + } + + [void] m4() + { + $canaryHashtable = Get-Canary + $canaryHashtable['canary'] += 1000 + $this.ImThrow() + $canaryHashtable['canary'] = -6104 + } + + [void] ImThrow() + { + $canaryHashtable = Get-Canary + $canaryHashtable['canary'] += 10000 + + 1 / 0 + } +} +[C]::new().m1() +'@).Invoke() + + } catch {} + + $canaryHashtable['canary'] | Should Be 11111 + } + } + + Context 'Class method call PS function' { + + $body = @' +class C +{ + [void] m1() + { + m2 + } + + static [void] s1() + { + s2 + } +} + + +function m2() +{ + $canary = Get-Canary + $canary['canaryM'] = 45 + ImThrow + $canary['canaryM'] = 100 +} + +function s2() +{ + $canary = Get-Canary + $canary['canaryS'] = 46 + CallImThrow + $canary['canaryS'] = 100 +} + +function CallImThrow() +{ + ImThrow +} + +function ImThrow() +{ + 1 / 0 +} + +'@ + + It 'does not execute statements after function with exception called from instance method' { + + # Put try-catch outside to avoid try-catch logic altering analysis + try { + + $ps.AddScript($body).Invoke() + $ps.AddScript('$c = [C]::new(); $c.m1()').Invoke() + + } catch {} + + $canaryHashtable['canaryM'] | Should Be 45 + } + + It 'does not execute statements after function with exception called from static method' { + + # Put try-catch outside to avoid try-catch logic altering analysis + try { + + $ps.AddScript($body).Invoke() + $ps.AddScript('[C]::s1()').Invoke() + + } catch {} + + $canaryHashtable['canaryS'] | Should Be 46 + } + + } + + Context "No class is involved" { + It "functions calls continue execution by default" { + + try { + + $ps.AddScript( @' + +$canaryHashtable = Get-Canary +function foo() { 1 / 0; $canaryHashtable['canary'] += 10 } +$canaryHashtable['canary'] = 1 +foo +$canaryHashtable['canary'] += 100 + +'@).Invoke() + + } catch {} + + $canaryHashtable['canary'] | Should Be 111 + } + } +} + +Describe "Exception error position" -Tags "CI" { + class MSFT_3090412 + { + static f1() { [MSFT_3090412]::bar = 42 } + static f2() { throw "an error in f2" } + static f3() { "".Substring(0, 10) } + static f4() { dir nosuchfile -ea Stop } + } + + It "Setting a property that doesn't exist" { + try { + [MSFT_3090412]::f1() + throw "f1 should have thrown" + } catch { + $_.InvocationInfo.Line | Should Match ([regex]::Escape('[MSFT_3090412]::bar = 42')) + } + } + + It "Throwing an exception" { + try { + [MSFT_3090412]::f2() + throw "f2 should have thrown" + } catch { + $_.InvocationInfo.Line | Should Match ([regex]::Escape('throw "an error in f2"')) + } + } + + It "Calling a .Net method that throws" { + try { + [MSFT_3090412]::f3() + throw "f3 should have thrown" + } catch { + $_.InvocationInfo.Line | Should Match ([regex]::Escape('"".Substring(0, 10)')) + } + } + + It "Terminating error" { + try { + [MSFT_3090412]::f4() + throw "f4 should have thrown" + } catch { + $_.InvocationInfo.Line | Should Match ([regex]::Escape('dir nosuchfile -ea Stop')) + } + } +} + +Describe "Exception from initializer" -Tags "CI" { + class MSFT_6397334a + { + [int]$a = "zz" + MSFT_6397334a() {} + } + + class MSFT_6397334b + { + [int]$a = "zz" + } + + class MSFT_6397334c + { + static [int]$a = "zz" + static MSFT_6397334a() {} + } + + class MSFT_6397334d + { + static [int]$a = "zz" + } + + It "instance member w/ ctor" { + try { + [MSFT_6397334a]::new() + throw "[MSFT_6397334a]::new() should have thrown" + } + catch + { + $e = $_ + $e.FullyQualifiedErrorId | Should Be InvalidCastFromStringToInteger + $e.InvocationInfo.Line | Should Match 'a = "zz"' + } + } + + It "instance member w/o ctor" { + try { + [MSFT_6397334b]::new() + throw "[MSFT_6397334b]::new() should have thrown" + } + catch + { + $e = $_ + $e.FullyQualifiedErrorId | Should Be InvalidCastFromStringToInteger + $e.InvocationInfo.Line | Should Match 'a = "zz"' + } + } + + It "static member w/ ctor" { + try { + $null = [MSFT_6397334c]::a + throw "No Exception!" + } + catch + { + $_.Exception | Should BeOfType System.TypeInitializationException + $e = $_.Exception.InnerException.InnerException.ErrorRecord + $e.FullyQualifiedErrorId | Should Be InvalidCastFromStringToInteger + $e.InvocationInfo.Line | Should Match 'a = "zz"' + } + } + + It "static member w/o ctor" { + try { + $null = [MSFT_6397334d]::a + throw "No Exception!" + } + catch + { + $_.Exception | Should BeOfType System.TypeInitializationException + $e = $_.Exception.InnerException.InnerException.ErrorRecord + $e.FullyQualifiedErrorId | Should Be InvalidCastFromStringToInteger + $e.InvocationInfo.Line | Should Match 'a = "zz"' + } + } +} diff --git a/test/powershell/Language/Classes/Scripting.Classes.MiscOps.Tests.ps1 b/test/powershell/Language/Classes/Scripting.Classes.MiscOps.Tests.ps1 index 5696caed7f6..45162a1dcba 100644 --- a/test/powershell/Language/Classes/Scripting.Classes.MiscOps.Tests.ps1 +++ b/test/powershell/Language/Classes/Scripting.Classes.MiscOps.Tests.ps1 @@ -1,117 +1,117 @@ -Describe 'Misc Test' -Tags "CI" { - - Context 'Where' { - class C1 { - [int[]] $Wheels = @(1,2,3); - [string] Foo() { - return (1..10).Where({ $PSItem -in $this.Wheels; }) -join ';' - } - - [string] Bar() - { - return (1..10 | Where-Object { $PSItem -in $this.Wheels; }) -join ';' - } - } - It 'Invoke Where' { - [C1]::new().Foo() | should be "1;2;3" - } - It 'Pipe to where' { - [C1]::new().Bar() | should be "1;2;3" - } - } - - Context 'ForEach' { - class C1 { - [int[]] $Wheels = @(1,2,3); - [string] Foo() { - $ret="" - Foreach($PSItem in $this.Wheels) { $ret +="$PSItem;"} - return $ret - } - - [string] Bar() - { - $ret = "" - $this.Wheels | ForEach-Object { $ret += "$_;" } - return $ret - } - } - It 'Invoke Foreach' { - [C1]::new().Foo() | should be "1;2;3;" - } - It 'Pipe to Foreach' { - [C1]::new().Bar() | should be "1;2;3;" - } - } - - Context 'Class instantiation' { - Class C1 { - [string] Foo() { - return (Get-TestText) - } - } - - BeforeAll { - $ExpectedTextFromBoundInstance = "Class C1 was defined in this Runspace" - $ExpectedTextFromUnboundInstance = "New Runspace without class C1 defined" - - ## Define 'Get-TestText' in the current Runspace - function Get-TestText { return $ExpectedTextFromBoundInstance } - - $NewRunspaceFunctionDefinitions = @" - ## Define 'Get-TestText' in the new Runspace - function Get-TestText { return '$ExpectedTextFromUnboundInstance' } - - ## Define the function to create an instance of the given type using the default constructor - function New-UnboundInstance([Type]`$type) { `$type::new() } - - ## Define the function to call 'Foo()' on the given C1 instance, and return the result - function Run-Foo(`$C1Instance) { `$C1Instance.Foo() } -"@ - ## Create a new Runspace and define helper functions in it - $powershell = [powershell]::Create() - $powershell.AddScript($NewRunspaceFunctionDefinitions).Invoke() > $null - $powershell.Commands.Clear() - - function InstantiateInNewRunspace([Type]$type) { - try { - $result = $powershell.AddCommand("New-UnboundInstance").AddParameter("type", $type).Invoke() - $result.Count | Should Be 1 > $null - return $result[0] - } finally { - $powershell.Commands.Clear() - } - } - - function RunFooInNewRunspace($instance) { - try { - $result = $powershell.AddCommand("Run-Foo").AddParameter("C1Instance", $instance).Invoke() - $result.Count | Should Be 1 > $null - return $result[0] - } finally { - $powershell.Commands.Clear() - } - } - } - - AfterAll { - $powershell.Dispose() - } - - It "Create instance that is bound to a SessionState" { - $instance = [C1]::new() - ## For a bound class instance, the execution of an instance method is - ## done in the Runspace/SessionState the instance is bound to. - $instance.Foo() | Should Be $ExpectedTextFromBoundInstance - RunFooInNewRunspace $instance | Should Be $ExpectedTextFromBoundInstance - } - - It "Create instance that is NOT bound to a SessionState" { - $instance = InstantiateInNewRunspace ([C1]) - ## For an unbound class instance, the execution of an instance method is done in - ## the Runspace/SessionState where the call to the instance method is made. - $instance.Foo() | Should Be $ExpectedTextFromBoundInstance - RunFooInNewRunspace $instance | Should Be $ExpectedTextFromUnboundInstance - } - } -} +Describe 'Misc Test' -Tags "CI" { + + Context 'Where' { + class C1 { + [int[]] $Wheels = @(1,2,3); + [string] Foo() { + return (1..10).Where({ $PSItem -in $this.Wheels; }) -join ';' + } + + [string] Bar() + { + return (1..10 | Where-Object { $PSItem -in $this.Wheels; }) -join ';' + } + } + It 'Invoke Where' { + [C1]::new().Foo() | should be "1;2;3" + } + It 'Pipe to where' { + [C1]::new().Bar() | should be "1;2;3" + } + } + + Context 'ForEach' { + class C1 { + [int[]] $Wheels = @(1,2,3); + [string] Foo() { + $ret="" + Foreach($PSItem in $this.Wheels) { $ret +="$PSItem;"} + return $ret + } + + [string] Bar() + { + $ret = "" + $this.Wheels | ForEach-Object { $ret += "$_;" } + return $ret + } + } + It 'Invoke Foreach' { + [C1]::new().Foo() | should be "1;2;3;" + } + It 'Pipe to Foreach' { + [C1]::new().Bar() | should be "1;2;3;" + } + } + + Context 'Class instantiation' { + Class C1 { + [string] Foo() { + return (Get-TestText) + } + } + + BeforeAll { + $ExpectedTextFromBoundInstance = "Class C1 was defined in this Runspace" + $ExpectedTextFromUnboundInstance = "New Runspace without class C1 defined" + + ## Define 'Get-TestText' in the current Runspace + function Get-TestText { return $ExpectedTextFromBoundInstance } + + $NewRunspaceFunctionDefinitions = @" + ## Define 'Get-TestText' in the new Runspace + function Get-TestText { return '$ExpectedTextFromUnboundInstance' } + + ## Define the function to create an instance of the given type using the default constructor + function New-UnboundInstance([Type]`$type) { `$type::new() } + + ## Define the function to call 'Foo()' on the given C1 instance, and return the result + function Run-Foo(`$C1Instance) { `$C1Instance.Foo() } +"@ + ## Create a new Runspace and define helper functions in it + $powershell = [powershell]::Create() + $powershell.AddScript($NewRunspaceFunctionDefinitions).Invoke() > $null + $powershell.Commands.Clear() + + function InstantiateInNewRunspace([Type]$type) { + try { + $result = $powershell.AddCommand("New-UnboundInstance").AddParameter("type", $type).Invoke() + $result.Count | Should Be 1 > $null + return $result[0] + } finally { + $powershell.Commands.Clear() + } + } + + function RunFooInNewRunspace($instance) { + try { + $result = $powershell.AddCommand("Run-Foo").AddParameter("C1Instance", $instance).Invoke() + $result.Count | Should Be 1 > $null + return $result[0] + } finally { + $powershell.Commands.Clear() + } + } + } + + AfterAll { + $powershell.Dispose() + } + + It "Create instance that is bound to a SessionState" { + $instance = [C1]::new() + ## For a bound class instance, the execution of an instance method is + ## done in the Runspace/SessionState the instance is bound to. + $instance.Foo() | Should Be $ExpectedTextFromBoundInstance + RunFooInNewRunspace $instance | Should Be $ExpectedTextFromBoundInstance + } + + It "Create instance that is NOT bound to a SessionState" { + $instance = InstantiateInNewRunspace ([C1]) + ## For an unbound class instance, the execution of an instance method is done in + ## the Runspace/SessionState where the call to the instance method is made. + $instance.Foo() | Should Be $ExpectedTextFromBoundInstance + RunFooInNewRunspace $instance | Should Be $ExpectedTextFromUnboundInstance + } + } +} diff --git a/test/powershell/Language/Classes/Scripting.Classes.Modules.Tests.ps1 b/test/powershell/Language/Classes/Scripting.Classes.Modules.Tests.ps1 index 3f657e78c5a..c25cb49c5c6 100644 --- a/test/powershell/Language/Classes/Scripting.Classes.Modules.Tests.ps1 +++ b/test/powershell/Language/Classes/Scripting.Classes.Modules.Tests.ps1 @@ -1,113 +1,113 @@ -Describe 'PSModuleInfo.GetExportedTypeDefinitions()' -Tags "CI" { - It "doesn't throw for any module" { - $discard = Get-Module -ListAvailable | ForEach-Object { $_.GetExportedTypeDefinitions() } - $true | Should Be $true # we only verify that we didn't throw. This line contains a dummy Should to make pester happy. - } -} - -Describe 'use of a module from two runspaces' -Tags "CI" { - function New-TestModule { - param( - [string]$Name, - [string]$Content - ) - - $TestModulePath = Join-Path -Path $TestDrive -ChildPath "TestModule" - $ModuleFolder = Join-Path -Path $TestModulePath -ChildPath $Name - New-Item -Path $ModuleFolder -ItemType Directory -Force > $null - - Set-Content -Path "$ModuleFolder\$Name.psm1" -Value $Content - - $manifestParams = @{ - Path = "$ModuleFolder\$Name.psd1" - RootModule = "$Name.psm1" - } - New-ModuleManifest @manifestParams - - if ($env:PSModulePath -notlike "*$TestModulePath*") { - $env:PSModulePath += "$([System.IO.Path]::PathSeparator)$TestModulePath" - } - } - - $originalPSModulePath = $env:PSModulePath - try { - - New-TestModule -Name 'Random' -Content @' -$script:random = Get-Random -class RandomWrapper -{ - [int] getRandom() - { - return $script:random - } -} -'@ - - It 'use different sessionStates for different modules' { - $ps = 1..2 | ForEach-Object { $p = [powershell]::Create().AddScript(@' -Import-Module Random -'@) - $p.Invoke() > $null - $p - } - $res = 1..2 | ForEach-Object { - 0..1 | ForEach-Object { - $ps[$_].Commands.Clear() - # The idea: instance created inside the context, in one runspace. - # Method is called on instance in the different runspace, but it should know about the origin. - $w = $ps[$_].AddScript('& (Get-Module Random) { [RandomWrapper]::new() }').Invoke()[0] - $w.getRandom() - } - } - - $res.Count | Should Be 4 - $res[0] | Should Not Be $res[1] - $res[0] | Should Be $res[2] - $res[1] | Should Be $res[3] - } - - } finally { - $env:PSModulePath = $originalPSModulePath - } - -} - -Describe 'Module reloading with Class definition' -Tags "CI" { - - BeforeAll { - Set-Content -Path TestDrive:\TestModule.psm1 -Value @' -$passedArgs = $args -class Root { $passedIn = $passedArgs } -function Get-PassedArgsRoot { [Root]::new().passedIn } -function Get-PassedArgsNoRoot { $passedArgs } -'@ - $Arg_Hello = 'Hello' - $Arg_World = 'World' - } - - AfterEach { - Remove-Module TestModule -Force -ErrorAction SilentlyContinue - } - - It "Class execution reflects changes in module reloading with '-Force'" { - Import-Module TestDrive:\TestModule.psm1 -ArgumentList $Arg_Hello - Get-PassedArgsRoot | Should Be $Arg_Hello - Get-PassedArgsNoRoot | Should Be $Arg_Hello - - Import-Module TestDrive:\TestModule.psm1 -ArgumentList $Arg_World -Force - Get-PassedArgsRoot | Should Be $Arg_World - Get-PassedArgsNoRoot | Should Be $Arg_World - } - - It "Class execution reflects changes in module reloading with 'Remove-Module' and 'Import-Module'" { - Import-Module TestDrive:\TestModule.psm1 -ArgumentList $Arg_Hello - Get-PassedArgsRoot | Should Be $Arg_Hello - Get-PassedArgsNoRoot | Should Be $Arg_Hello - - Remove-Module TestModule - - Import-Module TestDrive:\TestModule.psm1 -ArgumentList $Arg_World - Get-PassedArgsRoot | Should Be $Arg_World - Get-PassedArgsNoRoot | Should Be $Arg_World - } -} +Describe 'PSModuleInfo.GetExportedTypeDefinitions()' -Tags "CI" { + It "doesn't throw for any module" { + $discard = Get-Module -ListAvailable | ForEach-Object { $_.GetExportedTypeDefinitions() } + $true | Should Be $true # we only verify that we didn't throw. This line contains a dummy Should to make pester happy. + } +} + +Describe 'use of a module from two runspaces' -Tags "CI" { + function New-TestModule { + param( + [string]$Name, + [string]$Content + ) + + $TestModulePath = Join-Path -Path $TestDrive -ChildPath "TestModule" + $ModuleFolder = Join-Path -Path $TestModulePath -ChildPath $Name + New-Item -Path $ModuleFolder -ItemType Directory -Force > $null + + Set-Content -Path "$ModuleFolder\$Name.psm1" -Value $Content + + $manifestParams = @{ + Path = "$ModuleFolder\$Name.psd1" + RootModule = "$Name.psm1" + } + New-ModuleManifest @manifestParams + + if ($env:PSModulePath -notlike "*$TestModulePath*") { + $env:PSModulePath += "$([System.IO.Path]::PathSeparator)$TestModulePath" + } + } + + $originalPSModulePath = $env:PSModulePath + try { + + New-TestModule -Name 'Random' -Content @' +$script:random = Get-Random +class RandomWrapper +{ + [int] getRandom() + { + return $script:random + } +} +'@ + + It 'use different sessionStates for different modules' { + $ps = 1..2 | ForEach-Object { $p = [powershell]::Create().AddScript(@' +Import-Module Random +'@) + $p.Invoke() > $null + $p + } + $res = 1..2 | ForEach-Object { + 0..1 | ForEach-Object { + $ps[$_].Commands.Clear() + # The idea: instance created inside the context, in one runspace. + # Method is called on instance in the different runspace, but it should know about the origin. + $w = $ps[$_].AddScript('& (Get-Module Random) { [RandomWrapper]::new() }').Invoke()[0] + $w.getRandom() + } + } + + $res.Count | Should Be 4 + $res[0] | Should Not Be $res[1] + $res[0] | Should Be $res[2] + $res[1] | Should Be $res[3] + } + + } finally { + $env:PSModulePath = $originalPSModulePath + } + +} + +Describe 'Module reloading with Class definition' -Tags "CI" { + + BeforeAll { + Set-Content -Path TestDrive:\TestModule.psm1 -Value @' +$passedArgs = $args +class Root { $passedIn = $passedArgs } +function Get-PassedArgsRoot { [Root]::new().passedIn } +function Get-PassedArgsNoRoot { $passedArgs } +'@ + $Arg_Hello = 'Hello' + $Arg_World = 'World' + } + + AfterEach { + Remove-Module TestModule -Force -ErrorAction SilentlyContinue + } + + It "Class execution reflects changes in module reloading with '-Force'" { + Import-Module TestDrive:\TestModule.psm1 -ArgumentList $Arg_Hello + Get-PassedArgsRoot | Should Be $Arg_Hello + Get-PassedArgsNoRoot | Should Be $Arg_Hello + + Import-Module TestDrive:\TestModule.psm1 -ArgumentList $Arg_World -Force + Get-PassedArgsRoot | Should Be $Arg_World + Get-PassedArgsNoRoot | Should Be $Arg_World + } + + It "Class execution reflects changes in module reloading with 'Remove-Module' and 'Import-Module'" { + Import-Module TestDrive:\TestModule.psm1 -ArgumentList $Arg_Hello + Get-PassedArgsRoot | Should Be $Arg_Hello + Get-PassedArgsNoRoot | Should Be $Arg_Hello + + Remove-Module TestModule + + Import-Module TestDrive:\TestModule.psm1 -ArgumentList $Arg_World + Get-PassedArgsRoot | Should Be $Arg_World + Get-PassedArgsNoRoot | Should Be $Arg_World + } +} diff --git a/test/powershell/Language/Classes/scripting.Classes.NestedModules.tests.ps1 b/test/powershell/Language/Classes/scripting.Classes.NestedModules.tests.ps1 index 307e6b85530..f16a39db1c9 100644 --- a/test/powershell/Language/Classes/scripting.Classes.NestedModules.tests.ps1 +++ b/test/powershell/Language/Classes/scripting.Classes.NestedModules.tests.ps1 @@ -1,124 +1,124 @@ -Describe 'NestedModules' -Tags "CI" { - - function New-TestModule { - param( - [string]$Name, - [string]$Content, - [string[]]$NestedContents - ) - - new-item -type directory -Force "TestDrive:\$Name" > $null - $manifestParams = @{ - Path = "TestDrive:\$Name\$Name.psd1" - } - - if ($Content) { - Set-Content -Path "${TestDrive}\$Name\$Name.psm1" -Value $Content - $manifestParams['RootModule'] = "$Name.psm1" - } - - if ($NestedContents) { - $manifestParams['NestedModules'] = 1..$NestedContents.Count | ForEach-Object { - $null = new-item -type directory TestDrive:\$Name\Nested$_ - $null = Set-Content -Path "${TestDrive}\$Name\Nested$_\Nested$_.psm1" -Value $NestedContents[$_ - 1] - "Nested$_" - } - } - - New-ModuleManifest @manifestParams - - $resolvedTestDrivePath = Split-Path ((get-childitem TestDrive:\)[0].FullName) - if (-not ($env:PSModulePath -like "*$resolvedTestDrivePath*")) { - $env:PSModulePath += "$([System.IO.Path]::PathSeparator)$resolvedTestDrivePath" - } - } - - $originalPSModulePath = $env:PSModulePath - - try { - - - # Create modules in TestDrive:\ - New-TestModule -Name NoRoot -NestedContents @( - 'class A { [string] foo() { return "A1"} }', - 'class A { [string] foo() { return "A2"} }' - ) - - New-TestModule -Name WithRoot -NestedContents @( - 'class A { [string] foo() { return "A1"} }', - 'class A { [string] foo() { return "A2"} }' - ) -Content 'class A { [string] foo() { return "A0"} }' - - New-TestModule -Name ABC -NestedContents @( - 'class A { [string] foo() { return "A"} }', - 'class B { [string] foo() { return "B"} }' - ) -Content 'class C { [string] foo() { return "C"} }' - - It 'Get-Module is able to find types' { - $module = Get-Module NoRoot -ListAvailable - $module.GetExportedTypeDefinitions().Count | Should Be 1 - - $module = Get-Module WithRoot -ListAvailable - $module.GetExportedTypeDefinitions().Count | Should Be 1 - - $module = Get-Module ABC -ListAvailable - $module.GetExportedTypeDefinitions().Count | Should Be 3 - } - - It 'Import-Module pick the right type' { - $module = Import-Module ABC -PassThru - $module.GetExportedTypeDefinitions().Count | Should Be 3 - $module = Import-Module ABC -PassThru -Force - $module.GetExportedTypeDefinitions().Count | Should Be 3 - - $module = Import-Module NoRoot -PassThru - $module.GetExportedTypeDefinitions().Count | Should Be 1 - $module = Import-Module NoRoot -PassThru -Force - $module.GetExportedTypeDefinitions().Count | Should Be 1 - [scriptblock]::Create(@' -using module NoRoot -[A]::new().foo() -'@ -).Invoke() | Should Be A2 - - $module = Import-Module WithRoot -PassThru - $module.GetExportedTypeDefinitions().Count | Should Be 1 - $module = Import-Module WithRoot -PassThru -Force - $module.GetExportedTypeDefinitions().Count | Should Be 1 - [scriptblock]::Create(@' -using module WithRoot -[A]::new().foo() -'@ -).Invoke() | Should Be A0 - } - - Context 'execute type creation in the module context' { - - # let's define types to make it more fun - class A { [string] foo() { return "local"} } - class B { [string] foo() { return "local"} } - class C { [string] foo() { return "local"} } - - # We need to think about it: should it work or not. - # Currently, types are resolved in compile-time to the 'local' versions - # So at runtime we don't call the module versions. - It 'Can execute type creation in the module context with new()' -pending { - & (Get-Module ABC) { [C]::new().foo() } | Should Be C - & (Get-Module NoRoot) { [A]::new().foo() } | Should Be A2 - & (Get-Module WithRoot) { [A]::new().foo() } | Should Be A0 - & (Get-Module ABC) { [A]::new().foo() } | Should Be A - } - - It 'Can execute type creation in the module context with New-Object' { - & (Get-Module ABC) { (New-Object C).foo() } | Should Be C - & (Get-Module NoRoot) { (New-Object A).foo() } | Should Be A2 - & (Get-Module WithRoot) { (New-Object A).foo() } | Should Be A0 - & (Get-Module ABC) { (New-Object A).foo() } | Should Be A - } - } - - } finally { - $env:PSModulePath = $originalPSModulePath - Get-Module @('ABC', 'NoRoot', 'WithRoot') | Remove-Module - } -} +Describe 'NestedModules' -Tags "CI" { + + function New-TestModule { + param( + [string]$Name, + [string]$Content, + [string[]]$NestedContents + ) + + new-item -type directory -Force "TestDrive:\$Name" > $null + $manifestParams = @{ + Path = "TestDrive:\$Name\$Name.psd1" + } + + if ($Content) { + Set-Content -Path "${TestDrive}\$Name\$Name.psm1" -Value $Content + $manifestParams['RootModule'] = "$Name.psm1" + } + + if ($NestedContents) { + $manifestParams['NestedModules'] = 1..$NestedContents.Count | ForEach-Object { + $null = new-item -type directory TestDrive:\$Name\Nested$_ + $null = Set-Content -Path "${TestDrive}\$Name\Nested$_\Nested$_.psm1" -Value $NestedContents[$_ - 1] + "Nested$_" + } + } + + New-ModuleManifest @manifestParams + + $resolvedTestDrivePath = Split-Path ((get-childitem TestDrive:\)[0].FullName) + if (-not ($env:PSModulePath -like "*$resolvedTestDrivePath*")) { + $env:PSModulePath += "$([System.IO.Path]::PathSeparator)$resolvedTestDrivePath" + } + } + + $originalPSModulePath = $env:PSModulePath + + try { + + + # Create modules in TestDrive:\ + New-TestModule -Name NoRoot -NestedContents @( + 'class A { [string] foo() { return "A1"} }', + 'class A { [string] foo() { return "A2"} }' + ) + + New-TestModule -Name WithRoot -NestedContents @( + 'class A { [string] foo() { return "A1"} }', + 'class A { [string] foo() { return "A2"} }' + ) -Content 'class A { [string] foo() { return "A0"} }' + + New-TestModule -Name ABC -NestedContents @( + 'class A { [string] foo() { return "A"} }', + 'class B { [string] foo() { return "B"} }' + ) -Content 'class C { [string] foo() { return "C"} }' + + It 'Get-Module is able to find types' { + $module = Get-Module NoRoot -ListAvailable + $module.GetExportedTypeDefinitions().Count | Should Be 1 + + $module = Get-Module WithRoot -ListAvailable + $module.GetExportedTypeDefinitions().Count | Should Be 1 + + $module = Get-Module ABC -ListAvailable + $module.GetExportedTypeDefinitions().Count | Should Be 3 + } + + It 'Import-Module pick the right type' { + $module = Import-Module ABC -PassThru + $module.GetExportedTypeDefinitions().Count | Should Be 3 + $module = Import-Module ABC -PassThru -Force + $module.GetExportedTypeDefinitions().Count | Should Be 3 + + $module = Import-Module NoRoot -PassThru + $module.GetExportedTypeDefinitions().Count | Should Be 1 + $module = Import-Module NoRoot -PassThru -Force + $module.GetExportedTypeDefinitions().Count | Should Be 1 + [scriptblock]::Create(@' +using module NoRoot +[A]::new().foo() +'@ +).Invoke() | Should Be A2 + + $module = Import-Module WithRoot -PassThru + $module.GetExportedTypeDefinitions().Count | Should Be 1 + $module = Import-Module WithRoot -PassThru -Force + $module.GetExportedTypeDefinitions().Count | Should Be 1 + [scriptblock]::Create(@' +using module WithRoot +[A]::new().foo() +'@ +).Invoke() | Should Be A0 + } + + Context 'execute type creation in the module context' { + + # let's define types to make it more fun + class A { [string] foo() { return "local"} } + class B { [string] foo() { return "local"} } + class C { [string] foo() { return "local"} } + + # We need to think about it: should it work or not. + # Currently, types are resolved in compile-time to the 'local' versions + # So at runtime we don't call the module versions. + It 'Can execute type creation in the module context with new()' -pending { + & (Get-Module ABC) { [C]::new().foo() } | Should Be C + & (Get-Module NoRoot) { [A]::new().foo() } | Should Be A2 + & (Get-Module WithRoot) { [A]::new().foo() } | Should Be A0 + & (Get-Module ABC) { [A]::new().foo() } | Should Be A + } + + It 'Can execute type creation in the module context with New-Object' { + & (Get-Module ABC) { (New-Object C).foo() } | Should Be C + & (Get-Module NoRoot) { (New-Object A).foo() } | Should Be A2 + & (Get-Module WithRoot) { (New-Object A).foo() } | Should Be A0 + & (Get-Module ABC) { (New-Object A).foo() } | Should Be A + } + } + + } finally { + $env:PSModulePath = $originalPSModulePath + Get-Module @('ABC', 'NoRoot', 'WithRoot') | Remove-Module + } +} diff --git a/test/powershell/Language/Classes/scripting.Classes.inheritance.tests.ps1 b/test/powershell/Language/Classes/scripting.Classes.inheritance.tests.ps1 index 0381c53c112..ac95bba2e27 100644 --- a/test/powershell/Language/Classes/scripting.Classes.inheritance.tests.ps1 +++ b/test/powershell/Language/Classes/scripting.Classes.inheritance.tests.ps1 @@ -1,545 +1,545 @@ -# -# Copyright (c) Microsoft Corporation, 2015 -# - -try { -# -# CrossGen'ed assemblies cause a hang to happen intermittently when running this test suite in Linux and macOS. -# The issue has been reported to CoreCLR team. We need to work around it for now with the following approach: -# 1. For pull request and push commit, build without '-CrossGen' and run the parsing tests -# 2. For daily build, build with '-CrossGen' but don't run the parsing tests -# In this way, we will continue to exercise these parsing tests for each CI build, and skip them for daily -# build to avoid a hang. -# Note: this change should be reverted once the 'CrossGen' issue is fixed by CoreCLR. The issue is tracked by -# https://github.com/dotnet/coreclr/issues/9745 -# -$isDailyBuild = $env:TRAVIS_EVENT_TYPE -eq 'cron' -or $env:TRAVIS_EVENT_TYPE -eq 'api' -$defaultParamValues = $PSdefaultParameterValues.Clone() -$IsSkipped = (!$IsWindows -and $isDailyBuild) -$PSDefaultParameterValues["it:skip"] = $IsSkipped -$PSDefaultParameterValues["ShouldBeParseError:SkipInTravisFullBuild"] = $IsSkipped - - -Describe 'Classes inheritance syntax' -Tags "CI" { - - It 'Base types' { - class C1 {} - class C2a : C1 {} - class C2b:C1 {} - - [C2a]::new().GetType().BaseType.Name | Should Be "C1" - [C2b].BaseType.Name | Should Be "C1" - } - - It 'inheritance from abstract base class with no abstract methods and protected ctor' { - class C3 : system.collections.collectionbase {} - - class C4 { C4([int]$a) {} } - class C5 : C4 { C5() : base(1) {} } - } - - It 'inheritance from base class with implicit ctor' { - class C6 {} - class C7 : C6 { C7() : base() {} } - } - - It 'inheritance syntax allows newlines in various places' { - class C {} - class C2a:C,system.IDisposable{ [void] Dispose() { }} - class C2b - : - C - , - system.IDisposable - { - [void] Dispose() {} - C2b() - : # there are extra spaces here - base - ( - ) - { - } - } - - [C2a].GetInterface("System.IDisposable") | Should Not Be $null - [C2b].GetInterface("System.IDisposable") | Should Not Be $null - } - - It 'can subclass .NET type' { - class MyIntList : system.collections.generic.list[int] {} - [MyIntList]::new().GetType().BaseType.FullName.StartsWith('System.Collections.Generic.List') | Should Be $true - } - - It 'can implement .NET interface' { - class MyComparable : system.IComparable - { - [int] CompareTo([object] $obj) - { - return 0; - } - } - [MyComparable].GetInterface("System.IComparable") | Should Not Be $null - } - - It 'allows use of defined later type as a property type' { - class A { static [B]$b } - class B : A {} - [A]::b = [B]::new() - try { - [A]::b = "bla" - throw "No Exception!" - } catch { - $_.Exception | Should BeOfType 'System.Management.Automation.SetValueInvocationException' - } - } -} - -Describe 'Classes inheritance syntax errors' -Tags "CI" { - ShouldBeParseError "class A : NonExistingClass {}" TypeNotFound 10 - ShouldBeParseError "class A : {}" TypeNameExpected 9 - ShouldBeParseError "class A {}; class B : A, {}" TypeNameExpected 24 - ShouldBeParseError "class A{} ; class B : A[] {}" SubtypeArray 22 -SkipAndCheckRuntimeError - ShouldBeParseError "class A : System.Collections.Generic.List``1 {}" SubtypeUnclosedGeneric 10 -SkipAndCheckRuntimeError - - ShouldBeParseError "class A {}; class B : A, NonExistingInterface {}" TypeNotFound 25 - ShouldBeParseError "class A {} ; class B {}; class C : A, B {}" InterfaceNameExpected 38 -SkipAndCheckRuntimeError - ShouldBeParseError "class A{} ; class B : A, System.IDisposable[] {}" SubtypeArray 25 -SkipAndCheckRuntimeError - ShouldBeParseError "class A {}; class B : A, NonExistingInterface {}" TypeNotFound 25 - - # base should be accepted only on instance ctors - ShouldBeParseError 'class A { A($a){} } ; class B : A { foo() : base(1) {} }' MissingFunctionBody 41 - ShouldBeParseError 'class A { static A() {} }; class B { static B() : base() {} }' MissingFunctionBody 47 - - # Incomplete input - ShouldBeParseError 'class A { A($a){} } ; class B : A { B() : bas {} }' MissingBaseCtorCall 41 - ShouldBeParseError 'class A { A($a){} } ; class B : A { B() : base( {} }' @('MissingEndParenthesisInMethodCall', 'MissingFunctionBody') @(50, 39) - ShouldBeParseError 'class A { A($a){} } ; class B : A { B() : base {} }' @('MissingMethodParameterList', 'UnexpectedToken') @(46, 50) - - # Sealed base - ShouldBeParseError "class baz : string {}" SealedBaseClass 12 -SkipAndCheckRuntimeError - # Non-existing Interface - ShouldBeParseError "class bar {}; class baz : bar, Non.Existing.Interface {}" TypeNotFound 31 -SkipAndCheckRuntimeError - - # .NET abstract method not implemented - ShouldBeParseError "class MyType : Type {}" TypeCreationError 0 -SkipAndCheckRuntimeError - - # inheritance doesn't allow non linear order - ShouldBeParseError "class A : B {}; class B {}" TypeNotFound 10 -SkipAndCheckRuntimeError - - # inheritance doesn't allow circular order - ShouldBeParseError "class A : B {}; class B : A {}" TypeNotFound 10 -SkipAndCheckRuntimeError - ShouldBeParseError "class A : C {}; class B : A {}; class C : B {}" TypeNotFound 10 -SkipAndCheckRuntimeError -} - -Describe 'Classes methods with inheritance' -Tags "CI" { - - Context 'Method calls' { - - It 'can call instance method on base class' { - class bar - { - [int]foo() {return 100500} - } - class baz : bar {} - [baz]::new().foo() | Should Be 100500 - } - - It 'can call static method on base class' { - class bar - { - static [int]foo() {return 100500} - } - class baz : bar {} - [baz]::foo() | Should Be 100500 - } - - It 'can access static and instance base class property' { - class A - { - static [int]$si - [int]$i - } - class B : A - { - [void]foo() - { - $this::si = 1001 - $this.i = 1003 - } - } - $b = [B]::new() - $b.foo() - [A]::si | Should Be 1001 - ($b.i) | Should Be 1003 - } - - It 'works with .NET types' { - class MyIntList : system.collections.generic.list[int] {} - $intList = [MyIntList]::new() - $intList.Add(100501) - $intList.Add(100502) - $intList.Count | Should Be 2 - $intList[0] | Should Be 100501 - $intList[1] | Should Be 100502 - } - - It 'overrides instance method' { - class bar - { - [int]foo() {return 100500} - } - class baz : bar - { - [int]foo() {return 200600} - } - [baz]::new().foo() | Should Be 200600 - } - - It 'allows base class method call and doesn''t fall into recursion' { - class bar - { - [int]foo() {return 1001} - } - class baz : bar - { - [int] $fooCallCounter - [int]foo() - { - if ($this.fooCallCounter++ -gt 0) - { - throw "Recursion happens" - } - return 3 * ([bar]$this).foo() - } - } - - $res = [baz]::new().foo() - $res | Should Be 3003 - } - - It 'case insensitive for base class method calls' { - class bar - { - [int]foo() {return 1001} - } - class baz : bar - { - [int] $fooCallCounter - [int]fOo() - { - if ($this.fooCallCounter++ -gt 0) - { - throw "Recursion happens" - } - return ([bAr]$this).fOo() + ([bAr]$this).FOO() - } - } - - $res = [baz]::new().foo() - $res | Should Be 2002 - } - - It 'allows any call from the inheritance hierarchy' { - class A - { - [string]GetName() {return "A"} - } - class B : A - { - [string]GetName() {return "B"} - } - class C : B - { - [string]GetName() {return "C"} - } - class D : C - { - [string]GetName() {return "D"} - } - $d = [D]::new() - - ([A]$d).GetName() | Should Be "A" - ([B]$d).GetName() | Should Be "B" - ([C]$d).GetName() | Should Be "C" - ([D]$d).GetName() | Should Be "D" - $d.GetName() | Should Be "D" - } - - It 'can call base method with params' { - class A - { - [string]ToStr([int]$a) {return "A" + $a} - } - class B : A - { - [string]ToStr([int]$a) {return "B" + $a} - } - $b = [B]::new() - ([A]$b).ToStr(101) | Should Be "A101" - $b.ToStr(100) | Should Be "B100" - } - - It 'can call base method with many params' { - class A - { - [string]ToStr([int]$a1, [int]$a2, [int]$a3, [int]$a4, [int]$a5, [int]$a6, [int]$a7, [int]$a8, [int]$a9, [int]$a10, [int]$a11, [int]$a12, [int]$a13, [int]$a14) - { - return "A" - } - - [void]Noop([int]$a1, [int]$a2, [int]$a3, [int]$a4, [int]$a5, [int]$a6, [int]$a7, [int]$a8, [int]$a9, [int]$a10, [int]$a11, [int]$a12, [int]$a13, [int]$a14) - { - } - } - class B : A - { - [string]ToStr([int]$a1, [int]$a2, [int]$a3, [int]$a4, [int]$a5, [int]$a6, [int]$a7, [int]$a8, [int]$a9, [int]$a10, [int]$a11, [int]$a12, [int]$a13, [int]$a14) - { - return "B" - } - - [void]Noop([int]$a1, [int]$a2, [int]$a3, [int]$a4, [int]$a5, [int]$a6, [int]$a7, [int]$a8, [int]$a9, [int]$a10, [int]$a11, [int]$a12, [int]$a13, [int]$a14) - { - } - } - $b = [B]::new() - - # we don't really care about methods results, we only checks that calls doesn't throw - - # 14 args is a limit - $b.ToStr(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14) | Should Be 'B' - ([A]$b).ToStr(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14) | Should Be 'A' - - # 14 args is a limit - $b.Noop(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14) - ([A]$b).Noop(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14) - } - - It 'overrides void method call' { - $script:voidOverrideVar = $null - class A - { - [void]SetStr([int]$a) {$script:voidOverrideVar = "A" + $a} - [void]SetStr() {$script:voidOverrideVar = "A"} - } - class B : A - { - [void]SetStr([int]$a) {$script:voidOverrideVar = "B" + $a} - [void]SetStr() {$script:voidOverrideVar = "B"} - } - $b = [B]::new() - ([A]$b).SetStr(101) - $script:voidOverrideVar | Should Be "A101" - $b.SetStr(100) - $script:voidOverrideVar | Should Be "B100" - ([A]$b).SetStr() - $script:voidOverrideVar | Should Be "A" - $b.SetStr() - $script:voidOverrideVar | Should Be "B" - } - - It 'hides final .NET method' { - class MyIntList : system.collections.generic.list[int] - { - # Add is final, can we hide it? - [void] Add([int]$arg) - { - ([system.collections.generic.list[int]]$this).Add($arg * 2) - } - } - - $intList = [MyIntList]::new() - $intList.Add(100201) - $intList.Count | Should Be 1 - $intList[0] | Should Be 200402 - } - } - - Context 'base static method call' { - class A - { - static [string]ToStr([int]$a) {return "A" + $a} - } - class B : A - { - static [string]ToStr([int]$a) {return "B" + $a} - } - - $b = [B]::new() - - # MSFT:1911652 - # MSFT:2973835 - It 'doesn''t affect static method call on type' -Skip { - ([A]$b)::ToStr(101) | Should Be "A101" - } - - It 'overrides static method call on instance' { - $b::ToStr(100) | Should Be "B100" - } - } -} - - -Describe 'Classes inheritance ctors syntax errors' -Tags "CI" { - - #DotNet.Interface.NotImplemented - ShouldBeParseError "class MyComparable : system.IComparable {}" TypeCreationError 0 -SkipAndCheckRuntimeError - - #DotNet.Interface.WrongSignature - ShouldBeParseError 'class MyComparable : system.IComparable { [void] CompareTo([object]$obj) {} }' TypeCreationError 0 -SkipAndCheckRuntimeError - - #DotNet.NoDefaultCtor - ShouldBeParseError "class MyCollection : System.Collections.ObjectModel.ReadOnlyCollection[int] {}" BaseClassNoDefaultCtor 0 -SkipAndCheckRuntimeError - - #NoDefaultCtor - ShouldBeParseError 'class A { A([int]$a) {} }; class B : A {}' BaseClassNoDefaultCtor 27 -SkipAndCheckRuntimeError -} - -Describe 'Classes inheritance ctors' -Tags "CI" { - - It 'can call base ctor' { - class A { - [int]$a - A([int]$a) - { - $this.a = $a - } - } - - class B : A - { - B([int]$a) : base($a * 2) {} - } - - $b = [B]::new(101) - $b.a | Should Be 202 - } - - # TODO: can we detect it in the parse time? - It 'cannot call base ctor with the wrong number of parameters' { - class A { - [int]$a - A([int]$a) - { - $this.a = $a - } - } - - class B : A - { - B([int]$a) : base($a * 2, 100) {} - } - - try { - [B]::new(101) - throw "No Exception!" - } catch { - $_.Exception | Should BeOfType "System.Management.Automation.MethodException" - } - } - - It 'call default base ctor implicitly' { - class A { - [int]$a - A() - { - $this.a = 1007 - } - } - - class B : A - { - B() {} - } - - class C : A - { - } - - $b = [B]::new() - $c = [C]::new() - $b.a | Should Be 1007 - $c.a | Should Be 1007 - } - - It 'doesn''t allow base ctor as an explicit method call' { - $o = [object]::new() - try { - # we should not allow direct .ctor call. - $o.{.ctor}() - } catch { - $_.FullyQualifiedErrorId | Should Be MethodNotFound - return - } - # Fail - '' | Should Be "Exception expected" - } - - It 'allow use conversion [string -> int] in base ctor call' { - class A { - [int]$a - A([int]$a) - { - $this.a = $a - } - } - - class B : A - { - B() : base("103") {} - } - - $b = [B]::new() - $b.a | Should Be 103 - } - - It 'resolves ctor call based on argument type' { - class A { - [int]$i - [string]$s - A([int]$a) - { - $this.i = $a - } - A([string]$a) - { - $this.s = $a - } - } - - class B : A - { - B($a) : base($a) {} - } - - $b1 = [B]::new("foo") - $b2 = [B]::new(1001) - $b1.s | Should Be "foo" - $b2.i | Should Be 1001 - } -} - -Describe 'Type creation' -Tags "CI" { - It 'can call super-class methods sequentially' { - $sb = [scriptblock]::Create(@' -class Base -{ - [int] foo() { return 100 } -} - -class Derived : Base -{ - [int] foo() { return 2 * ([Base]$this).foo() } -} - -[Derived]::new().foo() -'@) - $sb.Invoke() | Should Be 200 - $sb.Invoke() | Should Be 200 - } -} - -} finally { - $global:PSdefaultParameterValues = $defaultParamValues -} +# +# Copyright (c) Microsoft Corporation, 2015 +# + +try { +# +# CrossGen'ed assemblies cause a hang to happen intermittently when running this test suite in Linux and macOS. +# The issue has been reported to CoreCLR team. We need to work around it for now with the following approach: +# 1. For pull request and push commit, build without '-CrossGen' and run the parsing tests +# 2. For daily build, build with '-CrossGen' but don't run the parsing tests +# In this way, we will continue to exercise these parsing tests for each CI build, and skip them for daily +# build to avoid a hang. +# Note: this change should be reverted once the 'CrossGen' issue is fixed by CoreCLR. The issue is tracked by +# https://github.com/dotnet/coreclr/issues/9745 +# +$isDailyBuild = $env:TRAVIS_EVENT_TYPE -eq 'cron' -or $env:TRAVIS_EVENT_TYPE -eq 'api' +$defaultParamValues = $PSdefaultParameterValues.Clone() +$IsSkipped = (!$IsWindows -and $isDailyBuild) +$PSDefaultParameterValues["it:skip"] = $IsSkipped +$PSDefaultParameterValues["ShouldBeParseError:SkipInTravisFullBuild"] = $IsSkipped + + +Describe 'Classes inheritance syntax' -Tags "CI" { + + It 'Base types' { + class C1 {} + class C2a : C1 {} + class C2b:C1 {} + + [C2a]::new().GetType().BaseType.Name | Should Be "C1" + [C2b].BaseType.Name | Should Be "C1" + } + + It 'inheritance from abstract base class with no abstract methods and protected ctor' { + class C3 : system.collections.collectionbase {} + + class C4 { C4([int]$a) {} } + class C5 : C4 { C5() : base(1) {} } + } + + It 'inheritance from base class with implicit ctor' { + class C6 {} + class C7 : C6 { C7() : base() {} } + } + + It 'inheritance syntax allows newlines in various places' { + class C {} + class C2a:C,system.IDisposable{ [void] Dispose() { }} + class C2b + : + C + , + system.IDisposable + { + [void] Dispose() {} + C2b() + : # there are extra spaces here + base + ( + ) + { + } + } + + [C2a].GetInterface("System.IDisposable") | Should Not Be $null + [C2b].GetInterface("System.IDisposable") | Should Not Be $null + } + + It 'can subclass .NET type' { + class MyIntList : system.collections.generic.list[int] {} + [MyIntList]::new().GetType().BaseType.FullName.StartsWith('System.Collections.Generic.List') | Should Be $true + } + + It 'can implement .NET interface' { + class MyComparable : system.IComparable + { + [int] CompareTo([object] $obj) + { + return 0; + } + } + [MyComparable].GetInterface("System.IComparable") | Should Not Be $null + } + + It 'allows use of defined later type as a property type' { + class A { static [B]$b } + class B : A {} + [A]::b = [B]::new() + try { + [A]::b = "bla" + throw "No Exception!" + } catch { + $_.Exception | Should BeOfType 'System.Management.Automation.SetValueInvocationException' + } + } +} + +Describe 'Classes inheritance syntax errors' -Tags "CI" { + ShouldBeParseError "class A : NonExistingClass {}" TypeNotFound 10 + ShouldBeParseError "class A : {}" TypeNameExpected 9 + ShouldBeParseError "class A {}; class B : A, {}" TypeNameExpected 24 + ShouldBeParseError "class A{} ; class B : A[] {}" SubtypeArray 22 -SkipAndCheckRuntimeError + ShouldBeParseError "class A : System.Collections.Generic.List``1 {}" SubtypeUnclosedGeneric 10 -SkipAndCheckRuntimeError + + ShouldBeParseError "class A {}; class B : A, NonExistingInterface {}" TypeNotFound 25 + ShouldBeParseError "class A {} ; class B {}; class C : A, B {}" InterfaceNameExpected 38 -SkipAndCheckRuntimeError + ShouldBeParseError "class A{} ; class B : A, System.IDisposable[] {}" SubtypeArray 25 -SkipAndCheckRuntimeError + ShouldBeParseError "class A {}; class B : A, NonExistingInterface {}" TypeNotFound 25 + + # base should be accepted only on instance ctors + ShouldBeParseError 'class A { A($a){} } ; class B : A { foo() : base(1) {} }' MissingFunctionBody 41 + ShouldBeParseError 'class A { static A() {} }; class B { static B() : base() {} }' MissingFunctionBody 47 + + # Incomplete input + ShouldBeParseError 'class A { A($a){} } ; class B : A { B() : bas {} }' MissingBaseCtorCall 41 + ShouldBeParseError 'class A { A($a){} } ; class B : A { B() : base( {} }' @('MissingEndParenthesisInMethodCall', 'MissingFunctionBody') @(50, 39) + ShouldBeParseError 'class A { A($a){} } ; class B : A { B() : base {} }' @('MissingMethodParameterList', 'UnexpectedToken') @(46, 50) + + # Sealed base + ShouldBeParseError "class baz : string {}" SealedBaseClass 12 -SkipAndCheckRuntimeError + # Non-existing Interface + ShouldBeParseError "class bar {}; class baz : bar, Non.Existing.Interface {}" TypeNotFound 31 -SkipAndCheckRuntimeError + + # .NET abstract method not implemented + ShouldBeParseError "class MyType : Type {}" TypeCreationError 0 -SkipAndCheckRuntimeError + + # inheritance doesn't allow non linear order + ShouldBeParseError "class A : B {}; class B {}" TypeNotFound 10 -SkipAndCheckRuntimeError + + # inheritance doesn't allow circular order + ShouldBeParseError "class A : B {}; class B : A {}" TypeNotFound 10 -SkipAndCheckRuntimeError + ShouldBeParseError "class A : C {}; class B : A {}; class C : B {}" TypeNotFound 10 -SkipAndCheckRuntimeError +} + +Describe 'Classes methods with inheritance' -Tags "CI" { + + Context 'Method calls' { + + It 'can call instance method on base class' { + class bar + { + [int]foo() {return 100500} + } + class baz : bar {} + [baz]::new().foo() | Should Be 100500 + } + + It 'can call static method on base class' { + class bar + { + static [int]foo() {return 100500} + } + class baz : bar {} + [baz]::foo() | Should Be 100500 + } + + It 'can access static and instance base class property' { + class A + { + static [int]$si + [int]$i + } + class B : A + { + [void]foo() + { + $this::si = 1001 + $this.i = 1003 + } + } + $b = [B]::new() + $b.foo() + [A]::si | Should Be 1001 + ($b.i) | Should Be 1003 + } + + It 'works with .NET types' { + class MyIntList : system.collections.generic.list[int] {} + $intList = [MyIntList]::new() + $intList.Add(100501) + $intList.Add(100502) + $intList.Count | Should Be 2 + $intList[0] | Should Be 100501 + $intList[1] | Should Be 100502 + } + + It 'overrides instance method' { + class bar + { + [int]foo() {return 100500} + } + class baz : bar + { + [int]foo() {return 200600} + } + [baz]::new().foo() | Should Be 200600 + } + + It 'allows base class method call and doesn''t fall into recursion' { + class bar + { + [int]foo() {return 1001} + } + class baz : bar + { + [int] $fooCallCounter + [int]foo() + { + if ($this.fooCallCounter++ -gt 0) + { + throw "Recursion happens" + } + return 3 * ([bar]$this).foo() + } + } + + $res = [baz]::new().foo() + $res | Should Be 3003 + } + + It 'case insensitive for base class method calls' { + class bar + { + [int]foo() {return 1001} + } + class baz : bar + { + [int] $fooCallCounter + [int]fOo() + { + if ($this.fooCallCounter++ -gt 0) + { + throw "Recursion happens" + } + return ([bAr]$this).fOo() + ([bAr]$this).FOO() + } + } + + $res = [baz]::new().foo() + $res | Should Be 2002 + } + + It 'allows any call from the inheritance hierarchy' { + class A + { + [string]GetName() {return "A"} + } + class B : A + { + [string]GetName() {return "B"} + } + class C : B + { + [string]GetName() {return "C"} + } + class D : C + { + [string]GetName() {return "D"} + } + $d = [D]::new() + + ([A]$d).GetName() | Should Be "A" + ([B]$d).GetName() | Should Be "B" + ([C]$d).GetName() | Should Be "C" + ([D]$d).GetName() | Should Be "D" + $d.GetName() | Should Be "D" + } + + It 'can call base method with params' { + class A + { + [string]ToStr([int]$a) {return "A" + $a} + } + class B : A + { + [string]ToStr([int]$a) {return "B" + $a} + } + $b = [B]::new() + ([A]$b).ToStr(101) | Should Be "A101" + $b.ToStr(100) | Should Be "B100" + } + + It 'can call base method with many params' { + class A + { + [string]ToStr([int]$a1, [int]$a2, [int]$a3, [int]$a4, [int]$a5, [int]$a6, [int]$a7, [int]$a8, [int]$a9, [int]$a10, [int]$a11, [int]$a12, [int]$a13, [int]$a14) + { + return "A" + } + + [void]Noop([int]$a1, [int]$a2, [int]$a3, [int]$a4, [int]$a5, [int]$a6, [int]$a7, [int]$a8, [int]$a9, [int]$a10, [int]$a11, [int]$a12, [int]$a13, [int]$a14) + { + } + } + class B : A + { + [string]ToStr([int]$a1, [int]$a2, [int]$a3, [int]$a4, [int]$a5, [int]$a6, [int]$a7, [int]$a8, [int]$a9, [int]$a10, [int]$a11, [int]$a12, [int]$a13, [int]$a14) + { + return "B" + } + + [void]Noop([int]$a1, [int]$a2, [int]$a3, [int]$a4, [int]$a5, [int]$a6, [int]$a7, [int]$a8, [int]$a9, [int]$a10, [int]$a11, [int]$a12, [int]$a13, [int]$a14) + { + } + } + $b = [B]::new() + + # we don't really care about methods results, we only checks that calls doesn't throw + + # 14 args is a limit + $b.ToStr(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14) | Should Be 'B' + ([A]$b).ToStr(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14) | Should Be 'A' + + # 14 args is a limit + $b.Noop(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14) + ([A]$b).Noop(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14) + } + + It 'overrides void method call' { + $script:voidOverrideVar = $null + class A + { + [void]SetStr([int]$a) {$script:voidOverrideVar = "A" + $a} + [void]SetStr() {$script:voidOverrideVar = "A"} + } + class B : A + { + [void]SetStr([int]$a) {$script:voidOverrideVar = "B" + $a} + [void]SetStr() {$script:voidOverrideVar = "B"} + } + $b = [B]::new() + ([A]$b).SetStr(101) + $script:voidOverrideVar | Should Be "A101" + $b.SetStr(100) + $script:voidOverrideVar | Should Be "B100" + ([A]$b).SetStr() + $script:voidOverrideVar | Should Be "A" + $b.SetStr() + $script:voidOverrideVar | Should Be "B" + } + + It 'hides final .NET method' { + class MyIntList : system.collections.generic.list[int] + { + # Add is final, can we hide it? + [void] Add([int]$arg) + { + ([system.collections.generic.list[int]]$this).Add($arg * 2) + } + } + + $intList = [MyIntList]::new() + $intList.Add(100201) + $intList.Count | Should Be 1 + $intList[0] | Should Be 200402 + } + } + + Context 'base static method call' { + class A + { + static [string]ToStr([int]$a) {return "A" + $a} + } + class B : A + { + static [string]ToStr([int]$a) {return "B" + $a} + } + + $b = [B]::new() + + # MSFT:1911652 + # MSFT:2973835 + It 'doesn''t affect static method call on type' -Skip { + ([A]$b)::ToStr(101) | Should Be "A101" + } + + It 'overrides static method call on instance' { + $b::ToStr(100) | Should Be "B100" + } + } +} + + +Describe 'Classes inheritance ctors syntax errors' -Tags "CI" { + + #DotNet.Interface.NotImplemented + ShouldBeParseError "class MyComparable : system.IComparable {}" TypeCreationError 0 -SkipAndCheckRuntimeError + + #DotNet.Interface.WrongSignature + ShouldBeParseError 'class MyComparable : system.IComparable { [void] CompareTo([object]$obj) {} }' TypeCreationError 0 -SkipAndCheckRuntimeError + + #DotNet.NoDefaultCtor + ShouldBeParseError "class MyCollection : System.Collections.ObjectModel.ReadOnlyCollection[int] {}" BaseClassNoDefaultCtor 0 -SkipAndCheckRuntimeError + + #NoDefaultCtor + ShouldBeParseError 'class A { A([int]$a) {} }; class B : A {}' BaseClassNoDefaultCtor 27 -SkipAndCheckRuntimeError +} + +Describe 'Classes inheritance ctors' -Tags "CI" { + + It 'can call base ctor' { + class A { + [int]$a + A([int]$a) + { + $this.a = $a + } + } + + class B : A + { + B([int]$a) : base($a * 2) {} + } + + $b = [B]::new(101) + $b.a | Should Be 202 + } + + # TODO: can we detect it in the parse time? + It 'cannot call base ctor with the wrong number of parameters' { + class A { + [int]$a + A([int]$a) + { + $this.a = $a + } + } + + class B : A + { + B([int]$a) : base($a * 2, 100) {} + } + + try { + [B]::new(101) + throw "No Exception!" + } catch { + $_.Exception | Should BeOfType "System.Management.Automation.MethodException" + } + } + + It 'call default base ctor implicitly' { + class A { + [int]$a + A() + { + $this.a = 1007 + } + } + + class B : A + { + B() {} + } + + class C : A + { + } + + $b = [B]::new() + $c = [C]::new() + $b.a | Should Be 1007 + $c.a | Should Be 1007 + } + + It 'doesn''t allow base ctor as an explicit method call' { + $o = [object]::new() + try { + # we should not allow direct .ctor call. + $o.{.ctor}() + } catch { + $_.FullyQualifiedErrorId | Should Be MethodNotFound + return + } + # Fail + '' | Should Be "Exception expected" + } + + It 'allow use conversion [string -> int] in base ctor call' { + class A { + [int]$a + A([int]$a) + { + $this.a = $a + } + } + + class B : A + { + B() : base("103") {} + } + + $b = [B]::new() + $b.a | Should Be 103 + } + + It 'resolves ctor call based on argument type' { + class A { + [int]$i + [string]$s + A([int]$a) + { + $this.i = $a + } + A([string]$a) + { + $this.s = $a + } + } + + class B : A + { + B($a) : base($a) {} + } + + $b1 = [B]::new("foo") + $b2 = [B]::new(1001) + $b1.s | Should Be "foo" + $b2.i | Should Be 1001 + } +} + +Describe 'Type creation' -Tags "CI" { + It 'can call super-class methods sequentially' { + $sb = [scriptblock]::Create(@' +class Base +{ + [int] foo() { return 100 } +} + +class Derived : Base +{ + [int] foo() { return 2 * ([Base]$this).foo() } +} + +[Derived]::new().foo() +'@) + $sb.Invoke() | Should Be 200 + $sb.Invoke() | Should Be 200 + } +} + +} finally { + $global:PSdefaultParameterValues = $defaultParamValues +} diff --git a/test/powershell/Language/Classes/scripting.Classes.using.tests.ps1 b/test/powershell/Language/Classes/scripting.Classes.using.tests.ps1 index 7620696108d..b4120d072d4 100644 --- a/test/powershell/Language/Classes/scripting.Classes.using.tests.ps1 +++ b/test/powershell/Language/Classes/scripting.Classes.using.tests.ps1 @@ -1,549 +1,549 @@ -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 Be $null - } 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 Be '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 Be 'FooWithManifest' - $foo[1].GetModuleName() | Should Be '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 Be '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 Be 'Foo' - $fooModuleName[1] | Should Be 'FooWithManifest' - $fooModuleName[2] | Should Be 'This' - $fooModuleName[3] | Should Be '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 Be 'Foo' - $sb2.Invoke() | Should Be '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 Be 'Foo' - $fooModuleName[1] | Should Be 'Foo' - $fooModuleName[2] | Should Be 'This' - $fooModuleName[3] | Should Be 'This' - $fooModuleName[4] | Should Be '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 Be 'Foo' - } - - It "can instantiate class from another module" { - $foo = [scriptblock]::Create(@" -using module Foo -[Foo]::new() -"@).Invoke() - - $foo.GetModuleName() | Should Be 'Foo' - } - - It "cannot instantiate class from another module without using statement" { - $err = Get-RuntimeError @" -#using module Foo -[Foo]::new() -"@ - $err.FullyQualifiedErrorId | Should Be 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 Be '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 Be $null - } - - # 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 Be '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 Be '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 Be '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 Be '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 Be '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 Be '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 Be 'MissingUsingItemName' - } - - It "report an error when tokenizer encounters nothing" { - $err = Get-ParseResults "using module " - $err.Count | Should Be 1 - $err[0].ErrorId | Should Be '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 Be '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 Be 'MissingEndCurlyBrace' - $err[1].ErrorId | Should Be '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 Be '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 Be '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 Be '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 Be '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 Be AmbiguousTypeReference - } - - It "cannot use in New-Object" { - $err = Get-RuntimeError @" -using module Foo -using module FooWithManifest -New-Object Foo -"@ - $err.FullyQualifiedErrorId | Should Be '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 Be 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 Be '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 Be '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 Be '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 Be '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 Be '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 be Foo - $ps.Streams.Error | Should Be $null - - $ps1 = [powershell]::Create($iss) - $ps1.AddCommand("foo").Invoke() | Should be Foo - $ps1.Streams.Error | Should Be $null - - $ps.Commands.Clear() - $ps.Streams.Error.Clear() - $ps.AddScript(". foo").Invoke() | Should be Foo - $ps.Streams.Error | Should Be $null - } - } - - - # 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 Be $originalPSModulePath - } - - It "can be accessed by relative path" { - $barObject = & TestDrive:\FooRelativeConsumer\FooRelativeConsumer.ps1 - $barObject.GetModuleName() | Should Be 'FooForPaths' - } - - It "cannot be accessed by relative path without .\ from a script" { - $err = Get-RuntimeError '& TestDrive:\FooRelativeConsumerErr.ps1' - $err.FullyQualifiedErrorId | Should Be 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 Be '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 Be '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 Be ModuleNotFoundDuringParse - - Push-Location TestDrive:\modules - try { - $barObject = [scriptblock]::Create(@" -using module .\FooForPaths -[Foo]::new() -"@).Invoke() - $barObject.GetModuleName() | Should Be '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 Be ModuleNotFoundDuringParse - } finally { - Pop-Location - } - } - } - - 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 Be "TestClass" - } - } -} - +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 Be $null + } 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 Be '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 Be 'FooWithManifest' + $foo[1].GetModuleName() | Should Be '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 Be '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 Be 'Foo' + $fooModuleName[1] | Should Be 'FooWithManifest' + $fooModuleName[2] | Should Be 'This' + $fooModuleName[3] | Should Be '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 Be 'Foo' + $sb2.Invoke() | Should Be '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 Be 'Foo' + $fooModuleName[1] | Should Be 'Foo' + $fooModuleName[2] | Should Be 'This' + $fooModuleName[3] | Should Be 'This' + $fooModuleName[4] | Should Be '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 Be 'Foo' + } + + It "can instantiate class from another module" { + $foo = [scriptblock]::Create(@" +using module Foo +[Foo]::new() +"@).Invoke() + + $foo.GetModuleName() | Should Be 'Foo' + } + + It "cannot instantiate class from another module without using statement" { + $err = Get-RuntimeError @" +#using module Foo +[Foo]::new() +"@ + $err.FullyQualifiedErrorId | Should Be 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 Be '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 Be $null + } + + # 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 Be '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 Be '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 Be '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 Be '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 Be '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 Be '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 Be 'MissingUsingItemName' + } + + It "report an error when tokenizer encounters nothing" { + $err = Get-ParseResults "using module " + $err.Count | Should Be 1 + $err[0].ErrorId | Should Be '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 Be '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 Be 'MissingEndCurlyBrace' + $err[1].ErrorId | Should Be '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 Be '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 Be '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 Be '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 Be '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 Be AmbiguousTypeReference + } + + It "cannot use in New-Object" { + $err = Get-RuntimeError @" +using module Foo +using module FooWithManifest +New-Object Foo +"@ + $err.FullyQualifiedErrorId | Should Be '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 Be 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 Be '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 Be '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 Be '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 Be '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 Be '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 be Foo + $ps.Streams.Error | Should Be $null + + $ps1 = [powershell]::Create($iss) + $ps1.AddCommand("foo").Invoke() | Should be Foo + $ps1.Streams.Error | Should Be $null + + $ps.Commands.Clear() + $ps.Streams.Error.Clear() + $ps.AddScript(". foo").Invoke() | Should be Foo + $ps.Streams.Error | Should Be $null + } + } + + + # 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 Be $originalPSModulePath + } + + It "can be accessed by relative path" { + $barObject = & TestDrive:\FooRelativeConsumer\FooRelativeConsumer.ps1 + $barObject.GetModuleName() | Should Be 'FooForPaths' + } + + It "cannot be accessed by relative path without .\ from a script" { + $err = Get-RuntimeError '& TestDrive:\FooRelativeConsumerErr.ps1' + $err.FullyQualifiedErrorId | Should Be 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 Be '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 Be '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 Be ModuleNotFoundDuringParse + + Push-Location TestDrive:\modules + try { + $barObject = [scriptblock]::Create(@" +using module .\FooForPaths +[Foo]::new() +"@).Invoke() + $barObject.GetModuleName() | Should Be '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 Be ModuleNotFoundDuringParse + } finally { + Pop-Location + } + } + } + + 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 Be "TestClass" + } + } +} + diff --git a/test/powershell/Language/Classes/scripting.enums.tests.ps1 b/test/powershell/Language/Classes/scripting.enums.tests.ps1 index 8da968cb9c1..23a5532e53f 100644 --- a/test/powershell/Language/Classes/scripting.enums.tests.ps1 +++ b/test/powershell/Language/Classes/scripting.enums.tests.ps1 @@ -1,91 +1,91 @@ -# -# Copyright (c) Microsoft Corporation, 2015 -# - -Describe 'enums' -Tags "CI" { - Context 'basic enums' { - enum E1 - { - e0 - e1 - e2 - } - - It "has correct value 0" { [E1]::e0 | Should Be ([E1]0) } - It "has correct value 1" { [E1]::e1 | Should Be ([E1]1) } - It "has correct value 2" { [E1]::e2 | Should Be ([E1]2) } - It "cast from string" { [E1]::e1 | Should Be 'e1' } - It "cast to string" { 'e2' | Should Be ([E1]::e2) } - } - - Context 'Basic enum with initial value' { - enum E2 - { - e0 - e1 = 5 - e2 - } - - It "has correct value 0" { [E2]::e0 | Should Be ([E2]0) } - It "has correct value 5" { [E2]::e1 | Should Be ([E2]5) } - It "has correct value 6" { [E2]::e2 | Should Be ([E2]6) } - It "cast from string" { [E2]::e1 | Should Be 'e1' } - It "cast to string" { 'e2' | Should Be ([E2]::e2) } - } - - Context 'Basic enum with initial value expression' { - enum E3 - { - e0 - e1 = 5 - e2 = [int]::MaxValue - e3 = 1 # This shouldn't be an error even though previous member was max int - } - - It "has correct value 0" { [E3]::e0 | Should Be ([E3]0) } - It "has correct value 5" { [E3]::e1 | Should Be ([E3]5) } - It "has correct value [int]::MaxValue" { [E3]::e2 | Should Be ([E3]([int]::MaxValue)) } - It "has correct value 1" { [E3]::e3 | Should Be ([E3]1) } - It "cast from string" { [E3]::e2 | Should Be 'e2' } - It "cast to string" { 'e3' | Should Be ([E3]::e3) } - } - - Context 'Enum with complicated initial value' { - enum E4 - { - e0 = [E5]::e0 + 2 - } - - enum E5 - { - e0 = [E6]::e0 + 2 - } - - # Don't add space after 'e0 ='! Fix #2543 - enum E6 - { - e0 =38 - } - - It 'E4 has correct value' { [E4]::e0 | Should Be ([E4]42) } - It 'E5 has correct value' { [E5]::e0 | Should Be ([E5]40) } - It 'E6 has correct value' { [E6]::e0 | Should Be ([E6]38) } - } -} - -Describe 'Basic enum errors' -Tags "CI" { - ShouldBeParseError 'enum' MissingNameAfterKeyword 4 - ShouldBeParseError 'enum foo' MissingTypeBody 8 - ShouldBeParseError 'enum foo {' MissingEndCurlyBrace 10 - ShouldBeParseError 'enum foo { x = }' ExpectedValueExpression 14 - ShouldBeParseError 'enum foo { x =' ExpectedValueExpression,MissingEndCurlyBrace 14,10 - ShouldBeParseError 'enum foo {} enum foo {}' MemberAlreadyDefined 12 - ShouldBeParseError 'enum foo { x; x }' MemberAlreadyDefined 14 -SkipAndCheckRuntimeError - ShouldBeParseError 'enum foo { X; x }' MemberAlreadyDefined 14 -SkipAndCheckRuntimeError - ShouldBeParseError 'enum foo1 { x = [foo2]::x } enum foo2 { x = [foo1]::x }' CycleInEnumInitializers,CycleInEnumInitializers 0,28 -SkipAndCheckRuntimeError - ShouldBeParseError 'enum foo { e = [int]::MaxValue; e2 }' EnumeratorValueTooLarge 33 -SkipAndCheckRuntimeError - ShouldBeParseError 'enum foo { e = [int]::MaxValue + 1 }' EnumeratorValueTooLarge 15 -SkipAndCheckRuntimeError - ShouldBeParseError 'enum foo { e = $foo }' EnumeratorValueMustBeConstant 15 -SkipAndCheckRuntimeError - ShouldBeParseError 'enum foo { e = "hello" }' CannotConvertValue 15 -SkipAndCheckRuntimeError - ShouldBeParseError 'enum foo { a;b;c;' MissingEndCurlyBrace 10 -} +# +# Copyright (c) Microsoft Corporation, 2015 +# + +Describe 'enums' -Tags "CI" { + Context 'basic enums' { + enum E1 + { + e0 + e1 + e2 + } + + It "has correct value 0" { [E1]::e0 | Should Be ([E1]0) } + It "has correct value 1" { [E1]::e1 | Should Be ([E1]1) } + It "has correct value 2" { [E1]::e2 | Should Be ([E1]2) } + It "cast from string" { [E1]::e1 | Should Be 'e1' } + It "cast to string" { 'e2' | Should Be ([E1]::e2) } + } + + Context 'Basic enum with initial value' { + enum E2 + { + e0 + e1 = 5 + e2 + } + + It "has correct value 0" { [E2]::e0 | Should Be ([E2]0) } + It "has correct value 5" { [E2]::e1 | Should Be ([E2]5) } + It "has correct value 6" { [E2]::e2 | Should Be ([E2]6) } + It "cast from string" { [E2]::e1 | Should Be 'e1' } + It "cast to string" { 'e2' | Should Be ([E2]::e2) } + } + + Context 'Basic enum with initial value expression' { + enum E3 + { + e0 + e1 = 5 + e2 = [int]::MaxValue + e3 = 1 # This shouldn't be an error even though previous member was max int + } + + It "has correct value 0" { [E3]::e0 | Should Be ([E3]0) } + It "has correct value 5" { [E3]::e1 | Should Be ([E3]5) } + It "has correct value [int]::MaxValue" { [E3]::e2 | Should Be ([E3]([int]::MaxValue)) } + It "has correct value 1" { [E3]::e3 | Should Be ([E3]1) } + It "cast from string" { [E3]::e2 | Should Be 'e2' } + It "cast to string" { 'e3' | Should Be ([E3]::e3) } + } + + Context 'Enum with complicated initial value' { + enum E4 + { + e0 = [E5]::e0 + 2 + } + + enum E5 + { + e0 = [E6]::e0 + 2 + } + + # Don't add space after 'e0 ='! Fix #2543 + enum E6 + { + e0 =38 + } + + It 'E4 has correct value' { [E4]::e0 | Should Be ([E4]42) } + It 'E5 has correct value' { [E5]::e0 | Should Be ([E5]40) } + It 'E6 has correct value' { [E6]::e0 | Should Be ([E6]38) } + } +} + +Describe 'Basic enum errors' -Tags "CI" { + ShouldBeParseError 'enum' MissingNameAfterKeyword 4 + ShouldBeParseError 'enum foo' MissingTypeBody 8 + ShouldBeParseError 'enum foo {' MissingEndCurlyBrace 10 + ShouldBeParseError 'enum foo { x = }' ExpectedValueExpression 14 + ShouldBeParseError 'enum foo { x =' ExpectedValueExpression,MissingEndCurlyBrace 14,10 + ShouldBeParseError 'enum foo {} enum foo {}' MemberAlreadyDefined 12 + ShouldBeParseError 'enum foo { x; x }' MemberAlreadyDefined 14 -SkipAndCheckRuntimeError + ShouldBeParseError 'enum foo { X; x }' MemberAlreadyDefined 14 -SkipAndCheckRuntimeError + ShouldBeParseError 'enum foo1 { x = [foo2]::x } enum foo2 { x = [foo1]::x }' CycleInEnumInitializers,CycleInEnumInitializers 0,28 -SkipAndCheckRuntimeError + ShouldBeParseError 'enum foo { e = [int]::MaxValue; e2 }' EnumeratorValueTooLarge 33 -SkipAndCheckRuntimeError + ShouldBeParseError 'enum foo { e = [int]::MaxValue + 1 }' EnumeratorValueTooLarge 15 -SkipAndCheckRuntimeError + ShouldBeParseError 'enum foo { e = $foo }' EnumeratorValueMustBeConstant 15 -SkipAndCheckRuntimeError + ShouldBeParseError 'enum foo { e = "hello" }' CannotConvertValue 15 -SkipAndCheckRuntimeError + ShouldBeParseError 'enum foo { a;b;c;' MissingEndCurlyBrace 10 +} diff --git a/test/powershell/Language/CompletionTestSupport.psm1 b/test/powershell/Language/CompletionTestSupport.psm1 index 148714ae559..a15b57a4cec 100644 --- a/test/powershell/Language/CompletionTestSupport.psm1 +++ b/test/powershell/Language/CompletionTestSupport.psm1 @@ -1,142 +1,142 @@ - -class CompletionResult -{ - [string]$CompletionText - [string]$ListItemText - [System.Management.Automation.CompletionResultType]$ResultType - [string]$ToolTip - [bool]$Found - - [bool] Equals($Other) - { - if ($Other -isnot [CompletionResult] -and - $Other -isnot [System.Management.Automation.CompletionResult]) - { - return $false - } - - # Comparison is intentionally fuzzy - CompletionText and ResultType must be specified - # but the other properties don't need to match if they aren't specified - - if ($this.CompletionText -cne $Other.CompletionText -or - $this.ResultType -ne $Other.ResultType) - { - return $false - } - - if ($this.ListItemText -cne $Other.ListItemText -and - ![string]::IsNullOrEmpty($this.ListItemText) -and ![string]::IsNullOrEmpty($Other.ListItemText)) - { - return $false - } - - if ($this.ToolTip -cne $Other.ToolTip -and - ![string]::IsNullOrEmpty($this.ToolTip) -and ![string]::IsNullOrEmpty($Other.ToolTip)) - { - return $false - } - - return $true - } -} - -class CompletionTestCase -{ - [string]$Description - [CompletionResult[]]$ExpectedResults - [string[]]$NotExpectedResults - [hashtable]$TestInput -} - -function Get-Completions -{ - [CmdletBinding()] - param([string]$InputScript, [int]$CursorColumn, $Options = $null) - - if (!$PSBoundParameters.ContainsKey('CursorColumn')) - { - $CursorColumn = $InputScript.IndexOf('<#CURSOR#>') - if ($CursorColumn -lt 0) - { - $CursorColumn = $InputScript.Length - } - else - { - $InputScript = $InputScript -replace '<#CURSOR#>','' - } - } - - $results = [System.Management.Automation.CommandCompletion]::CompleteInput( - <#inputScript#> $InputScript, - <#cursorColumn#> $CursorColumn, - <#options#> $Options) - - return $results -} - -function Get-CompletionTestCaseData -{ - param( - [Parameter(ValueFromPipeline)] - [hashtable[]]$Data) - - process - { - Write-Output ([CompletionTestCase[]]$Data) - } -} - -function Test-Completions -{ - param( - [Parameter(ValueFromPipeline)] - [CompletionTestCase[]]$TestCases, - [string] - $Description) - - process - { - foreach ($test in $TestCases) - { - Describe $test.Description -Tags "CI" { - $hash = $Test.TestInput - $results = Get-Completions @hash - - foreach ($expected in $test.ExpectedResults) - { - foreach ($result in $results.CompletionMatches) - { - - if ($expected.Equals($result)) - { - It "Checking for duplicates of: $($expected.CompletionText)" { - # We should only find 1 of each expected result - $expected.Found | Should Be $false - } - $expected.Found = $true - } - } - } - - foreach ($expected in $test.ExpectedResults) - { - It "Checking for presence of expected result: $($expected.CompletionText)" { - $expected.Found | Should Be $true - } - } - - foreach ($notExpected in $test.NotExpectedResults) - { - foreach ($result in $results.CompletionMatches) - { - It "Checking for results that should not be found: $notExpected" { - $result.CompletionText -cne $notExpected | Should Be $true - } - } - } - - } - } - } -} - + +class CompletionResult +{ + [string]$CompletionText + [string]$ListItemText + [System.Management.Automation.CompletionResultType]$ResultType + [string]$ToolTip + [bool]$Found + + [bool] Equals($Other) + { + if ($Other -isnot [CompletionResult] -and + $Other -isnot [System.Management.Automation.CompletionResult]) + { + return $false + } + + # Comparison is intentionally fuzzy - CompletionText and ResultType must be specified + # but the other properties don't need to match if they aren't specified + + if ($this.CompletionText -cne $Other.CompletionText -or + $this.ResultType -ne $Other.ResultType) + { + return $false + } + + if ($this.ListItemText -cne $Other.ListItemText -and + ![string]::IsNullOrEmpty($this.ListItemText) -and ![string]::IsNullOrEmpty($Other.ListItemText)) + { + return $false + } + + if ($this.ToolTip -cne $Other.ToolTip -and + ![string]::IsNullOrEmpty($this.ToolTip) -and ![string]::IsNullOrEmpty($Other.ToolTip)) + { + return $false + } + + return $true + } +} + +class CompletionTestCase +{ + [string]$Description + [CompletionResult[]]$ExpectedResults + [string[]]$NotExpectedResults + [hashtable]$TestInput +} + +function Get-Completions +{ + [CmdletBinding()] + param([string]$InputScript, [int]$CursorColumn, $Options = $null) + + if (!$PSBoundParameters.ContainsKey('CursorColumn')) + { + $CursorColumn = $InputScript.IndexOf('<#CURSOR#>') + if ($CursorColumn -lt 0) + { + $CursorColumn = $InputScript.Length + } + else + { + $InputScript = $InputScript -replace '<#CURSOR#>','' + } + } + + $results = [System.Management.Automation.CommandCompletion]::CompleteInput( + <#inputScript#> $InputScript, + <#cursorColumn#> $CursorColumn, + <#options#> $Options) + + return $results +} + +function Get-CompletionTestCaseData +{ + param( + [Parameter(ValueFromPipeline)] + [hashtable[]]$Data) + + process + { + Write-Output ([CompletionTestCase[]]$Data) + } +} + +function Test-Completions +{ + param( + [Parameter(ValueFromPipeline)] + [CompletionTestCase[]]$TestCases, + [string] + $Description) + + process + { + foreach ($test in $TestCases) + { + Describe $test.Description -Tags "CI" { + $hash = $Test.TestInput + $results = Get-Completions @hash + + foreach ($expected in $test.ExpectedResults) + { + foreach ($result in $results.CompletionMatches) + { + + if ($expected.Equals($result)) + { + It "Checking for duplicates of: $($expected.CompletionText)" { + # We should only find 1 of each expected result + $expected.Found | Should Be $false + } + $expected.Found = $true + } + } + } + + foreach ($expected in $test.ExpectedResults) + { + It "Checking for presence of expected result: $($expected.CompletionText)" { + $expected.Found | Should Be $true + } + } + + foreach ($notExpected in $test.NotExpectedResults) + { + foreach ($result in $results.CompletionMatches) + { + It "Checking for results that should not be found: $notExpected" { + $result.CompletionText -cne $notExpected | Should Be $true + } + } + } + + } + } + } +} + diff --git a/test/powershell/Language/Parser/Ast.Tests.ps1 b/test/powershell/Language/Parser/Ast.Tests.ps1 index 5e2047f075c..25ffd6d05a3 100644 --- a/test/powershell/Language/Parser/Ast.Tests.ps1 +++ b/test/powershell/Language/Parser/Ast.Tests.ps1 @@ -1,40 +1,40 @@ -using Namespace System.Management.Automation.Language -Describe "The SafeGetValue method on AST returns safe values" -Tags "CI" { - It "A hashtable is returned from a HashtableAst" { - $HashtableAstType = [HashtableAst] - $HtAst = { - @{ one = 1 } - }.ast.Find({$args[0] -is $HashtableAstType}, $true) - $HtAst | Should Not BeNullOrEmpty - $HtAst.SafeGetValue() | Should BeOfType "Hashtable" - } - It "An Array is returned from a LiteralArrayAst" { - $ArrayAstType = [ArrayLiteralAst] - $ArrayAst = { - @( 1,2,3,4) - }.ast.Find({$args[0] -is $ArrayAstType}, $true) - $ArrayAst | Should Not BeNullOrEmpty - ,$ArrayAst.SafeGetValue() | Should BeOfType "Object[]" - } - It "The proper error is returned when a variable is referenced" { - $ast = { $a }.Ast.Find({$args[0] -is "VariableExpressionAst"},$true) - try { - $ast.SafeGetValue() | out-null - throw "No Exception!" - } - catch { - $_.FullyQualifiedErrorId | Should be "InvalidOperationException" - $_.ToString() | Should Match '\$a' - } - } - It "A ScriptBlock AST fails with the proper error" { - try { - { 1 }.Ast.SafeGetValue() - throw "No Exception!" - } - catch { - $_.FullyQualifiedErrorId | Should be "InvalidOperationException" - } - } - -} +using Namespace System.Management.Automation.Language +Describe "The SafeGetValue method on AST returns safe values" -Tags "CI" { + It "A hashtable is returned from a HashtableAst" { + $HashtableAstType = [HashtableAst] + $HtAst = { + @{ one = 1 } + }.ast.Find({$args[0] -is $HashtableAstType}, $true) + $HtAst | Should Not BeNullOrEmpty + $HtAst.SafeGetValue() | Should BeOfType "Hashtable" + } + It "An Array is returned from a LiteralArrayAst" { + $ArrayAstType = [ArrayLiteralAst] + $ArrayAst = { + @( 1,2,3,4) + }.ast.Find({$args[0] -is $ArrayAstType}, $true) + $ArrayAst | Should Not BeNullOrEmpty + ,$ArrayAst.SafeGetValue() | Should BeOfType "Object[]" + } + It "The proper error is returned when a variable is referenced" { + $ast = { $a }.Ast.Find({$args[0] -is "VariableExpressionAst"},$true) + try { + $ast.SafeGetValue() | out-null + throw "No Exception!" + } + catch { + $_.FullyQualifiedErrorId | Should be "InvalidOperationException" + $_.ToString() | Should Match '\$a' + } + } + It "A ScriptBlock AST fails with the proper error" { + try { + { 1 }.Ast.SafeGetValue() + throw "No Exception!" + } + catch { + $_.FullyQualifiedErrorId | Should be "InvalidOperationException" + } + } + +} diff --git a/test/powershell/Language/Parser/AutomaticVariables.Tests.ps1 b/test/powershell/Language/Parser/AutomaticVariables.Tests.ps1 index b73d68dfabd..b3948430775 100644 --- a/test/powershell/Language/Parser/AutomaticVariables.Tests.ps1 +++ b/test/powershell/Language/Parser/AutomaticVariables.Tests.ps1 @@ -1,21 +1,21 @@ - -Describe 'Automatic variable $input' -Tags "CI" { - # Skip on hold for discussion on https://github.com/PowerShell/PowerShell/issues/1563 - # $input type in advanced functions - It '$input Type should be enumerator' -Skip { - function from_begin { [cmdletbinding()]param() begin { Write-Output -NoEnumerate $input } } - function from_process { [cmdletbinding()]param() process { Write-Output -NoEnumerate $input } } - function from_end { [cmdletbinding()]param() end { Write-Output -NoEnumerate $input } } - - (from_begin) -is [System.Collections.IEnumerator] | Should Be $true - (from_process) -is [System.Collections.IEnumerator] | Should Be $true - (from_end) -is [System.Collections.IEnumerator] | Should Be $true - } - - It 'Empty $input really is empty' { - & { @($input).Count } | Should Be 0 - & { [cmdletbinding()]param() begin { @($input).Count } } | Should Be 0 - & { [cmdletbinding()]param() process { @($input).Count } } | Should Be 0 - & { [cmdletbinding()]param() end { @($input).Count } } | Should Be 0 - } -} + +Describe 'Automatic variable $input' -Tags "CI" { + # Skip on hold for discussion on https://github.com/PowerShell/PowerShell/issues/1563 + # $input type in advanced functions + It '$input Type should be enumerator' -Skip { + function from_begin { [cmdletbinding()]param() begin { Write-Output -NoEnumerate $input } } + function from_process { [cmdletbinding()]param() process { Write-Output -NoEnumerate $input } } + function from_end { [cmdletbinding()]param() end { Write-Output -NoEnumerate $input } } + + (from_begin) -is [System.Collections.IEnumerator] | Should Be $true + (from_process) -is [System.Collections.IEnumerator] | Should Be $true + (from_end) -is [System.Collections.IEnumerator] | Should Be $true + } + + It 'Empty $input really is empty' { + & { @($input).Count } | Should Be 0 + & { [cmdletbinding()]param() begin { @($input).Count } } | Should Be 0 + & { [cmdletbinding()]param() process { @($input).Count } } | Should Be 0 + & { [cmdletbinding()]param() end { @($input).Count } } | Should Be 0 + } +} diff --git a/test/powershell/Language/Parser/BNotOperator.Tests.ps1 b/test/powershell/Language/Parser/BNotOperator.Tests.ps1 index dd956ac7a35..3c8c0f12f75 100644 --- a/test/powershell/Language/Parser/BNotOperator.Tests.ps1 +++ b/test/powershell/Language/Parser/BNotOperator.Tests.ps1 @@ -1,142 +1,142 @@ - - -$baseTypes = @{ - [SByte] = 'sbyte'; [Byte] = 'byte' - [Int16] = 'short'; [UInt16] = 'ushort' - [Int32] = 'int'; [UInt32] = 'uint' - [Int64] = 'long'; [UInt64] = 'ulong' -} - -$ns = [Guid]::NewGuid() -replace '-','' - -$typeDefinition = "namespace ns_$ns`n{" - -$enumTypeNames = foreach ($baseType in $baseTypes.Keys) -{ - $baseTypeName = $baseTypes[$baseType] - $typeDefinition += @" - public enum E_$baseTypeName : $baseTypeName - { - Min = $($baseType::MinValue), - MinPlus1 = $($baseType::MinValue + 1), - MaxMinus1 = $($baseType::MaxValue - 1), - Max = $($baseType::MaxValue) - } -"@ - - "ns_$ns.E_$baseTypeName" -} - -$typeDefinition += "`n}" - -Write-Verbose $typeDefinition -Add-Type $typeDefinition - -Describe "bnot on enums" -Tags "CI" { - foreach ($enumType in [type[]]$enumTypeNames) - { - Context $enumType.Name { - It "max - 1" { - $res = -bnot $enumType::MaxMinus1 - $res | Should Be $enumType::MinPlus1 - $res | Should BeOfType $enumType - } - - It "min + 1" { - $res = -bnot $enumType::MinPlus1 - $res | Should Be $enumType::MaxMinus1 - $res | Should BeOfType $enumType - } - - It "Max" { - $res = -bnot $enumType::Max - $res | Should Be $enumType::Min - $res | Should BeOfType $enumType - } - - It "Min" { - $res = -bnot $enumType::Min - $res | Should Be $enumType::Max - $res | Should BeOfType $enumType - } - } - } -} - -Describe "bnot on integral types" -Tags "CI" { - foreach ($baseType in $baseTypes.Keys) - { - Context $baseType.Name { - - $max = $baseType::MaxValue - $maxMinus1 = $max - 1 - $min = $baseType::MinValue - $minPlus1 = $min + 1 - - if ([System.Runtime.InteropServices.Marshal]::SizeOf([type]$baseType) -lt 4) - { - $expectedResultType = [int] - } - else - { - $expectedResultType = $baseType - } - - if ($baseType -eq [byte] -or $baseType -eq [uint16]) - { - # Because of type promotion rules, unsigned types smaller than int - # don't quite follow the pattern of "flip all the bits", so our - # tests are a little different. - It "max - 1" { - $res = -bnot $maxMinus1 - $res | Should Be (-bnot [int]$maxMinus1) - $res | Should BeOfType $expectedResultType - } - - It "min + 1" { - $res = -bnot $minPlus1 - $res | Should Be (-bnot [int]$minPlus1) - $res | Should BeOfType $expectedResultType - } - - It "max" { - $res = -bnot $max - $res | Should Be (-bnot [int]$max) - $res | Should BeOfType $expectedResultType - } - - It "min" { - $res = -bnot $min - $res | Should Be (-bnot [int]$min) - $res | Should BeOfType $expectedResultType - } - return - } - - It "max - 1" { - $res = -bnot $maxMinus1 - $res | Should Be $minPlus1 - $res | Should BeOfType $expectedResultType - } - - It "min + 1" { - $res = -bnot $minPlus1 - $res | Should Be $maxMinus1 - $res | Should BeOfType $expectedResultType - } - - It "max" { - $res = -bnot $max - $res | Should Be $min - $res | Should BeOfType $expectedResultType - } - - It "min" { - $res = -bnot $min - $res | Should Be $max - $res | Should BeOfType $expectedResultType - } - } - } -} - + + +$baseTypes = @{ + [SByte] = 'sbyte'; [Byte] = 'byte' + [Int16] = 'short'; [UInt16] = 'ushort' + [Int32] = 'int'; [UInt32] = 'uint' + [Int64] = 'long'; [UInt64] = 'ulong' +} + +$ns = [Guid]::NewGuid() -replace '-','' + +$typeDefinition = "namespace ns_$ns`n{" + +$enumTypeNames = foreach ($baseType in $baseTypes.Keys) +{ + $baseTypeName = $baseTypes[$baseType] + $typeDefinition += @" + public enum E_$baseTypeName : $baseTypeName + { + Min = $($baseType::MinValue), + MinPlus1 = $($baseType::MinValue + 1), + MaxMinus1 = $($baseType::MaxValue - 1), + Max = $($baseType::MaxValue) + } +"@ + + "ns_$ns.E_$baseTypeName" +} + +$typeDefinition += "`n}" + +Write-Verbose $typeDefinition +Add-Type $typeDefinition + +Describe "bnot on enums" -Tags "CI" { + foreach ($enumType in [type[]]$enumTypeNames) + { + Context $enumType.Name { + It "max - 1" { + $res = -bnot $enumType::MaxMinus1 + $res | Should Be $enumType::MinPlus1 + $res | Should BeOfType $enumType + } + + It "min + 1" { + $res = -bnot $enumType::MinPlus1 + $res | Should Be $enumType::MaxMinus1 + $res | Should BeOfType $enumType + } + + It "Max" { + $res = -bnot $enumType::Max + $res | Should Be $enumType::Min + $res | Should BeOfType $enumType + } + + It "Min" { + $res = -bnot $enumType::Min + $res | Should Be $enumType::Max + $res | Should BeOfType $enumType + } + } + } +} + +Describe "bnot on integral types" -Tags "CI" { + foreach ($baseType in $baseTypes.Keys) + { + Context $baseType.Name { + + $max = $baseType::MaxValue + $maxMinus1 = $max - 1 + $min = $baseType::MinValue + $minPlus1 = $min + 1 + + if ([System.Runtime.InteropServices.Marshal]::SizeOf([type]$baseType) -lt 4) + { + $expectedResultType = [int] + } + else + { + $expectedResultType = $baseType + } + + if ($baseType -eq [byte] -or $baseType -eq [uint16]) + { + # Because of type promotion rules, unsigned types smaller than int + # don't quite follow the pattern of "flip all the bits", so our + # tests are a little different. + It "max - 1" { + $res = -bnot $maxMinus1 + $res | Should Be (-bnot [int]$maxMinus1) + $res | Should BeOfType $expectedResultType + } + + It "min + 1" { + $res = -bnot $minPlus1 + $res | Should Be (-bnot [int]$minPlus1) + $res | Should BeOfType $expectedResultType + } + + It "max" { + $res = -bnot $max + $res | Should Be (-bnot [int]$max) + $res | Should BeOfType $expectedResultType + } + + It "min" { + $res = -bnot $min + $res | Should Be (-bnot [int]$min) + $res | Should BeOfType $expectedResultType + } + return + } + + It "max - 1" { + $res = -bnot $maxMinus1 + $res | Should Be $minPlus1 + $res | Should BeOfType $expectedResultType + } + + It "min + 1" { + $res = -bnot $minPlus1 + $res | Should Be $maxMinus1 + $res | Should BeOfType $expectedResultType + } + + It "max" { + $res = -bnot $max + $res | Should Be $min + $res | Should BeOfType $expectedResultType + } + + It "min" { + $res = -bnot $min + $res | Should Be $max + $res | Should BeOfType $expectedResultType + } + } + } +} + diff --git a/test/powershell/Language/Parser/Conversions.Tests.ps1 b/test/powershell/Language/Parser/Conversions.Tests.ps1 index fcc116bb448..b568a34fa5b 100644 --- a/test/powershell/Language/Parser/Conversions.Tests.ps1 +++ b/test/powershell/Language/Parser/Conversions.Tests.ps1 @@ -1,129 +1,129 @@ -Describe 'conversion syntax' -Tags "CI" { - # these test suite covers ([]).() syntax. - # it mixes two purposes: casting and super-class method calls. - - It 'converts array of single enum to bool' { - # This test relies on the fact that [ConsoleColor]::Black is 0 and all other values are non-zero - [bool]@([ConsoleColor]::Black) | Should Be $false - [bool]@([ConsoleColor]::Yellow) | Should Be $true - } - - It 'calls virtual method non-virtually' { - ([object]"abc").ToString() | Should Be "System.String" - - # generate random string to avoid JIT optimization - $r = [guid]::NewGuid().Guid - ([object]($r + "a")).Equals(($r + "a")) | Should Be $false - } - - It 'calls method on a super-type, when conversion syntax used' { - # This test relies on the fact that there are overloads (at least 2) for ToString method. - ([System.Management.Automation.ActionPreference]"Stop").ToString() | Should Be "Stop" - } - - Context "Cast object[] to more narrow generic collection" { - BeforeAll { - $testCases1 = @( - ## It's intentional to have 'Command' to be `{$result = ...}` and run it with `. $Command`. - ## This is because `$result = & {[List[int]]@(1,2)}` will cause the resulted List to be unraveled, - ## and in that case `$result` would be just an object array. - ## To prevent unraveling, Command needs to be `{, [List[int]]@(1,2)}`, but then the test case title - ## would become `, [List[int]]@(1,2)`, which is more confusing than `$result = [List[int]]@(1,2)`. - ## This is why the current form of `$result = [List[int]]@(1,2)` is used intentionally here. - - @{ Command = {$result = [Collections.Generic.List[int]]@(1)}; CollectionType = 'List`1'; ElementType = "Int32"; Elements = @(1) } - @{ Command = {$result = [Collections.Generic.List[int]]@(1,2)}; CollectionType = 'List`1'; ElementType = "Int32"; Elements = @(1,2) } - @{ Command = {$result = [Collections.Generic.List[int]]"4"}; CollectionType = 'List`1'; ElementType = "Int32"; Elements = @(4) } - @{ Command = {$result = [Collections.Generic.List[int]]@("4","5")}; CollectionType = 'List`1'; ElementType = "Int32"; Elements = @(4,5) } - - @{ Command = {$result = [Collections.Generic.List[string]]@(1)}; CollectionType = 'List`1'; ElementType = "String"; Elements = @("1") } - @{ Command = {$result = [Collections.Generic.List[string]]@(1,2)}; CollectionType = 'List`1'; ElementType = "String"; Elements = @("1","2") } - @{ Command = {$result = [Collections.Generic.List[string]]1}; CollectionType = 'List`1'; ElementType = "String"; Elements = @("1") } - @{ Command = {$result = [Collections.Generic.List[string]]@("4")}; CollectionType = 'List`1'; ElementType = "String"; Elements = @("4") } - - @{ Command = {$result = [System.Collections.ObjectModel.Collection[int]]@(1)}; CollectionType = 'Collection`1'; ElementType = "Int32"; Elements = @(1) } - @{ Command = {$result = [System.Collections.ObjectModel.Collection[int]]@(1,2)}; CollectionType = 'Collection`1'; ElementType = "Int32"; Elements = @(1,2) } - @{ Command = {$result = [System.Collections.ObjectModel.Collection[int]]"4"}; CollectionType = 'Collection`1'; ElementType = "Int32"; Elements = @(4) } - @{ Command = {$result = [System.Collections.ObjectModel.Collection[int]]@("4","5")}; CollectionType = 'Collection`1'; ElementType = "Int32"; Elements = @(4,5) } - - @{ Command = {$result = [Collections.Generic.List[System.IO.FileInfo]]@('TestFile')}; - CollectionType = 'List`1'; ElementType = "FileInfo"; Elements = @('TestFile') } - @{ Command = {$result = [Collections.Generic.List[System.IO.FileInfo]]@('TestFile1', 'TestFile2')}; - CollectionType = 'List`1'; ElementType = "FileInfo"; Elements = @('TestFile1', 'TestFile2') } - @{ Command = {$result = [Collections.Generic.List[System.IO.FileInfo]]'TestFile'}; - CollectionType = 'List`1'; ElementType = "FileInfo"; Elements = @('TestFile') } - ) - } - - It "" -TestCases $testCases1 { - param($Command, $CollectionType, $ElementType, $Elements) - - $result = $null - . $Command - - $result | Should Not BeNullOrEmpty - $result.GetType().Name | Should Be $CollectionType - - $genericArgs = $result.GetType().GetGenericArguments() - $genericArgs.Length | Should Be 1 - $genericArgs[0].Name | Should Be $ElementType - - $result.Count | Should Be $Elements.Length - $result -join ";" | Should Be ($Elements -join ";") - } - } -} - -Describe "Type resolution should prefer assemblies in powershell assembly cache" -Tags "Feature" { - - BeforeAll { - $cmdletCode = @' -namespace TestTypeResolution { - using System.Management.Automation; - [Cmdlet("Test", "TypeResolution")] - public class TestTypeResolutionCommand : PSCmdlet { - [Parameter()] - public string Name { get; set; } - - protected override void BeginProcessing() { - WriteObject(Name); - } - } - - public class TestTypeFoo { - public string Foo { get; set; } - } -} -'@ - $dupTypeCode = @' -namespace TestTypeResolution { - public class TestTypeFoo { - public string Bar { get; set; } - } -} -'@ - - $cmdletDllDir = Join-Path $TestDrive "cmdlet" - $dupTypeDllDir = Join-Path $TestDrive "dupType" - - $null = New-Item -Path $cmdletDllDir, $dupTypeDllDir -ItemType Directory -Force - - $cmdletDllPath = Join-Path $cmdletDllDir "TestCmdlet.dll" - $dupTypeDllPath = Join-Path $dupTypeDllDir "TestType.dll" - - Add-Type $cmdletCode -OutputAssembly $cmdletDllPath - Add-Type $dupTypeCode -OutputAssembly $dupTypeDllPath - - $powershell = Join-Path $PSHOME "powershell" - } - - It "validate Type resolution should prefer the assembly loaded by Import-Module" { - $command = @" - Add-Type -Path $dupTypeDllPath - Import-Module $cmdletDllPath - [TestTypeResolution.TestTypeFoo].Assembly.Location -"@ - $location = & $powershell -noprofile -command $command - $location | Should Be $cmdletDllPath - } -} +Describe 'conversion syntax' -Tags "CI" { + # these test suite covers ([]).() syntax. + # it mixes two purposes: casting and super-class method calls. + + It 'converts array of single enum to bool' { + # This test relies on the fact that [ConsoleColor]::Black is 0 and all other values are non-zero + [bool]@([ConsoleColor]::Black) | Should Be $false + [bool]@([ConsoleColor]::Yellow) | Should Be $true + } + + It 'calls virtual method non-virtually' { + ([object]"abc").ToString() | Should Be "System.String" + + # generate random string to avoid JIT optimization + $r = [guid]::NewGuid().Guid + ([object]($r + "a")).Equals(($r + "a")) | Should Be $false + } + + It 'calls method on a super-type, when conversion syntax used' { + # This test relies on the fact that there are overloads (at least 2) for ToString method. + ([System.Management.Automation.ActionPreference]"Stop").ToString() | Should Be "Stop" + } + + Context "Cast object[] to more narrow generic collection" { + BeforeAll { + $testCases1 = @( + ## It's intentional to have 'Command' to be `{$result = ...}` and run it with `. $Command`. + ## This is because `$result = & {[List[int]]@(1,2)}` will cause the resulted List to be unraveled, + ## and in that case `$result` would be just an object array. + ## To prevent unraveling, Command needs to be `{, [List[int]]@(1,2)}`, but then the test case title + ## would become `, [List[int]]@(1,2)`, which is more confusing than `$result = [List[int]]@(1,2)`. + ## This is why the current form of `$result = [List[int]]@(1,2)` is used intentionally here. + + @{ Command = {$result = [Collections.Generic.List[int]]@(1)}; CollectionType = 'List`1'; ElementType = "Int32"; Elements = @(1) } + @{ Command = {$result = [Collections.Generic.List[int]]@(1,2)}; CollectionType = 'List`1'; ElementType = "Int32"; Elements = @(1,2) } + @{ Command = {$result = [Collections.Generic.List[int]]"4"}; CollectionType = 'List`1'; ElementType = "Int32"; Elements = @(4) } + @{ Command = {$result = [Collections.Generic.List[int]]@("4","5")}; CollectionType = 'List`1'; ElementType = "Int32"; Elements = @(4,5) } + + @{ Command = {$result = [Collections.Generic.List[string]]@(1)}; CollectionType = 'List`1'; ElementType = "String"; Elements = @("1") } + @{ Command = {$result = [Collections.Generic.List[string]]@(1,2)}; CollectionType = 'List`1'; ElementType = "String"; Elements = @("1","2") } + @{ Command = {$result = [Collections.Generic.List[string]]1}; CollectionType = 'List`1'; ElementType = "String"; Elements = @("1") } + @{ Command = {$result = [Collections.Generic.List[string]]@("4")}; CollectionType = 'List`1'; ElementType = "String"; Elements = @("4") } + + @{ Command = {$result = [System.Collections.ObjectModel.Collection[int]]@(1)}; CollectionType = 'Collection`1'; ElementType = "Int32"; Elements = @(1) } + @{ Command = {$result = [System.Collections.ObjectModel.Collection[int]]@(1,2)}; CollectionType = 'Collection`1'; ElementType = "Int32"; Elements = @(1,2) } + @{ Command = {$result = [System.Collections.ObjectModel.Collection[int]]"4"}; CollectionType = 'Collection`1'; ElementType = "Int32"; Elements = @(4) } + @{ Command = {$result = [System.Collections.ObjectModel.Collection[int]]@("4","5")}; CollectionType = 'Collection`1'; ElementType = "Int32"; Elements = @(4,5) } + + @{ Command = {$result = [Collections.Generic.List[System.IO.FileInfo]]@('TestFile')}; + CollectionType = 'List`1'; ElementType = "FileInfo"; Elements = @('TestFile') } + @{ Command = {$result = [Collections.Generic.List[System.IO.FileInfo]]@('TestFile1', 'TestFile2')}; + CollectionType = 'List`1'; ElementType = "FileInfo"; Elements = @('TestFile1', 'TestFile2') } + @{ Command = {$result = [Collections.Generic.List[System.IO.FileInfo]]'TestFile'}; + CollectionType = 'List`1'; ElementType = "FileInfo"; Elements = @('TestFile') } + ) + } + + It "" -TestCases $testCases1 { + param($Command, $CollectionType, $ElementType, $Elements) + + $result = $null + . $Command + + $result | Should Not BeNullOrEmpty + $result.GetType().Name | Should Be $CollectionType + + $genericArgs = $result.GetType().GetGenericArguments() + $genericArgs.Length | Should Be 1 + $genericArgs[0].Name | Should Be $ElementType + + $result.Count | Should Be $Elements.Length + $result -join ";" | Should Be ($Elements -join ";") + } + } +} + +Describe "Type resolution should prefer assemblies in powershell assembly cache" -Tags "Feature" { + + BeforeAll { + $cmdletCode = @' +namespace TestTypeResolution { + using System.Management.Automation; + [Cmdlet("Test", "TypeResolution")] + public class TestTypeResolutionCommand : PSCmdlet { + [Parameter()] + public string Name { get; set; } + + protected override void BeginProcessing() { + WriteObject(Name); + } + } + + public class TestTypeFoo { + public string Foo { get; set; } + } +} +'@ + $dupTypeCode = @' +namespace TestTypeResolution { + public class TestTypeFoo { + public string Bar { get; set; } + } +} +'@ + + $cmdletDllDir = Join-Path $TestDrive "cmdlet" + $dupTypeDllDir = Join-Path $TestDrive "dupType" + + $null = New-Item -Path $cmdletDllDir, $dupTypeDllDir -ItemType Directory -Force + + $cmdletDllPath = Join-Path $cmdletDllDir "TestCmdlet.dll" + $dupTypeDllPath = Join-Path $dupTypeDllDir "TestType.dll" + + Add-Type $cmdletCode -OutputAssembly $cmdletDllPath + Add-Type $dupTypeCode -OutputAssembly $dupTypeDllPath + + $powershell = Join-Path $PSHOME "powershell" + } + + It "validate Type resolution should prefer the assembly loaded by Import-Module" { + $command = @" + Add-Type -Path $dupTypeDllPath + Import-Module $cmdletDllPath + [TestTypeResolution.TestTypeFoo].Assembly.Location +"@ + $location = & $powershell -noprofile -command $command + $location | Should Be $cmdletDllPath + } +} diff --git a/test/powershell/Language/Parser/ExtensibleCompletion.Tests.ps1 b/test/powershell/Language/Parser/ExtensibleCompletion.Tests.ps1 index 61fc32b5201..2cd7b0ce1e8 100644 --- a/test/powershell/Language/Parser/ExtensibleCompletion.Tests.ps1 +++ b/test/powershell/Language/Parser/ExtensibleCompletion.Tests.ps1 @@ -1,451 +1,451 @@ -<# - Much of this script belongs in a module, but we don't support importing classes yet. -#> -using namespace System.Management.Automation -using namespace System.Management.Automation.Language -using namespace System.Collections -using namespace System.Collections.Generic - -#region Testcase infrastructure - -class CompletionTestResult -{ - [string]$CompletionText - [string]$ListItemText - [CompletionResultType]$ResultType - [string]$ToolTip - [bool]$Found - - [bool] Equals($Other) - { - if ($Other -isnot [CompletionTestResult] -and - $Other -isnot [CompletionResult]) - { - return $false - } - - # Comparison is intentionally fuzzy - CompletionText and ResultType must be specified - # but the other properties don't need to match if they aren't specified - - if ($this.CompletionText -cne $Other.CompletionText -or - $this.ResultType -ne $Other.ResultType) - { - return $false - } - - if ($this.ListItemText -cne $Other.ListItemText -and - ![string]::IsNullOrEmpty($this.ListItemText) -and ![string]::IsNullOrEmpty($Other.ListItemText)) - { - return $false - } - - if ($this.ToolTip -cne $Other.ToolTip -and - ![string]::IsNullOrEmpty($this.ToolTip) -and ![string]::IsNullOrEmpty($Other.ToolTip)) - { - return $false - } - - return $true - } -} - -class CompletionTestCase -{ - [CompletionTestResult[]]$ExpectedResults - [string[]]$NotExpectedResults - [string]$TestInput -} - -function Get-Completions -{ - param([string]$inputScript, [int]$cursorColumn = $inputScript.Length) - - $results = [System.Management.Automation.CommandCompletion]::CompleteInput( - <#inputScript#> $inputScript, - <#cursorColumn#> $cursorColumn, - <#options#> $null) - - return $results -} - -function Get-CompletionTestCaseData -{ - param( - [Parameter(ValueFromPipeline)] - [hashtable[]]$Data) - - process - { - Write-Output ([CompletionTestCase[]]$Data) - } -} - -function Test-Completions -{ - param( - [Parameter(ValueFromPipeline)] - [CompletionTestCase[]]$TestCases) - - process - { - foreach ($test in $TestCases) - { - Context ("Command line: <" + $test.TestInput + ">") { - $results = Get-Completions $test.TestInput - foreach ($result in $results.CompletionMatches) - { - foreach ($expected in $test.ExpectedResults) - { - if ($expected.Equals($result)) - { - $expected.Found = $true - } - } - } - foreach ($expected in $test.ExpectedResults) - { - $skip = $false - if ( $expected.CompletionText -match "System.Management.Automation.PerformanceData|System.Management.Automation.Security" ) { $skip = $true } - It ($expected.CompletionText) -skip:$skip { - $expected.Found | Should Be $true - } - } - - foreach ($notExpected in $test.NotExpectedResults) - { - It "Not expected: $notExpected" { - foreach ($result in $results.CompletionMatches) - { - ($result.CompletionText -ceq $notExpected) | Should Be $False - } - } - } - } - } - } -} - -#endregion Testcase infrastructure - -function AlphaArgumentCompleter -{ - param( - [string] $CommandName, - [string] $parameterName, - [string] $wordToComplete, - [CommandAst] $commandAst, - [IDictionary] $fakeBoundParameters) - - $beta = $fakeBoundParameters['beta'] - $gamma = $fakeBoundParameters['Gamma'] - $result = "beta: $beta gamma: $gamma command: $commandName parameterName: $parameterName wordToComplete: $wordToComplete" - [CompletionResult]::new($result, $result, "ParameterValue", $result) -} - -class BetaArgumentCompleter : IArgumentCompleter -{ - [IEnumerable[CompletionResult]] CompleteArgument( - [string] $CommandName, - [string] $parameterName, - [string] $wordToComplete, - [CommandAst] $commandAst, - [IDictionary] $fakeBoundParameters) - { - $resultList = [List[CompletionResult]]::new() - - $alpha = $fakeBoundParameters['Alpha'] - $gamma = $fakeBoundParameters['Gamma'] - $result = "alpha: $alpha gamma: $gamma command: $commandName parameterName: $parameterName wordToComplete: $wordToComplete" - $resultList.Add([CompletionResult]::new($result, $result, "ParameterValue", $result)) - - return $resultList - } -} - -function TestFunction -{ - param( - [ArgumentCompleter({ AlphaArgumentCompleter @args })] - $Alpha, - [ArgumentCompleter([BetaArgumentCompleter])] - $Beta, - $Gamma - ) -} - -Describe "Script block based extensible completion" -Tags "CI" { - @{ - ExpectedResults = @( - @{CompletionText = "beta: 11 gamma: 22 command: TestFunction parameterName: Alpha wordToComplete: aa" - ResultType = "ParameterValue"}) - TestInput = 'TestFunction -Beta 11 -Gamma 22 -Alpha aa' - } | Get-CompletionTestCaseData | Test-Completions -} - -Describe "Test class based extensible completion" -Tags "CI" { - @{ - ExpectedResults = @( - @{CompletionText = "alpha: 42 gamma: 44 command: TestFunction parameterName: Beta wordToComplete: zz" - ResultType = "ParameterValue"}) - TestInput = 'TestFunction -Alpha 42 -Gamma 44 -Beta zz' - } | Get-CompletionTestCaseData | Test-Completions -} - -Describe "Test registration based extensible completion" -Tags "CI" { - Register-ArgumentCompleter -Command TestFunction -Parameter Gamma -ScriptBlock { - param( - [string] $CommandName, - [string] $parameterName, - [string] $wordToComplete, - [CommandAst] $commandAst, - [IDictionary] $fakeBoundParameters) - - $beta = $fakeBoundParameters['beta'] - $alpha = $fakeBoundParameters['alpha'] - $result = "beta: $beta alpha: $alpha command: $commandName parameterName: $parameterName wordToComplete: $wordToComplete" - [CompletionResult]::new($result, $result, "ParameterValue", $result) - } - - @{ - ExpectedResults = @( - @{CompletionText = "beta: bb alpha: aa command: TestFunction parameterName: Gamma wordToComplete: 42" - ResultType = "ParameterValue"}) - TestInput = 'TestFunction -Alpha aa -Beta bb -Gamma 42' - } | Get-CompletionTestCaseData | Test-Completions -} - -Describe "Test extensible completion of native commands" -Tags "CI" { - Register-ArgumentCompleter -Command netsh -Native -ScriptBlock { - [CompletionResult]::new('advfirewall', 'advfirewall', "ParameterValue", 'advfirewall') - [CompletionResult]::new('bridge', 'bridge', "ParameterValue", 'bridge') - } - - @{ - ExpectedResults = @( - @{CompletionText = "advfirewall"; ResultType = "ParameterValue"} - @{CompletionText = "bridge"; ResultType = "ParameterValue"} - ) - TestInput = 'netsh ' - } | Get-CompletionTestCaseData | Test-Completions -} - -Describe "Test completion of parameters for native commands" -Tags "CI" { - Register-ArgumentCompleter -Native -CommandName foo -ScriptBlock { - Param($wordToComplete) - - @("-dir", "-verbose", "-help", "-version") | - Where-Object { - $_ -match "$wordToComplete*" - } | - ForEach-Object { - [CompletionResult]::new($_, $_, [CompletionResultType]::ParameterName, $_) - } - } - - @{ - ExpectedResults = @( - @{CompletionText = "-version"; ResultType = "ParameterName"} - @{CompletionText = "-verbose"; ResultType = "ParameterName"} - @{CompletionText = "-dir"; ResultType = "ParameterName"} - @{CompletionText = "-help"; ResultType = "ParameterName"} - ) - TestInput = 'foo -' - } | Get-CompletionTestCaseData | Test-Completions - - @{ - ExpectedResults = @( - @{CompletionText = "-version"; ResultType = "ParameterName"} - @{CompletionText = "-verbose"; ResultType = "ParameterName"} - ) - TestInput = 'foo -v' - } | Get-CompletionTestCaseData | Test-Completions -} - -Describe "Test extensible completion of using namespace" -Tags "CI" { - @{ - ExpectedResults = @( - @{CompletionText = "System"; ResultType = "Namespace"} - ) - TestInput = 'Using namespace sys' - }, - @{ - ExpectedResults = @( - @{CompletionText = "System.Xml"; ResultType = "Namespace"} - @{CompletionText = "System.Data"; ResultType = "Namespace"} - @{CompletionText = "System.Collections"; ResultType = "Namespace"} - @{CompletionText = "System.IO"; ResultType = "Namespace"} - ) - TestInput = 'Using namespace system.' - }, - @{ - ExpectedResults = @( - @{CompletionText = "System.Management.Automation"; ResultType = "Namespace"} - ) - TestInput = 'Using namespace System.Management.Automati' - }, - @{ - ExpectedResults = @( - @{CompletionText = "System.Management.Automation.Host"; ResultType = "Namespace"} - @{CompletionText = "System.Management.Automation.Internal"; ResultType = "Namespace"} - @{CompletionText = "System.Management.Automation.Language"; ResultType = "Namespace"} - @{CompletionText = "System.Management.Automation.PerformanceData"; ResultType = "Namespace"} - @{CompletionText = "System.Management.Automation.Provider"; ResultType = "Namespace"} - @{CompletionText = "System.Management.Automation.Remoting"; ResultType = "Namespace"} - @{CompletionText = "System.Management.Automation.Runspaces"; ResultType = "Namespace"} - @{CompletionText = "System.Management.Automation.Security"; ResultType = "Namespace"} - ) - TestInput = 'using namespace System.Management.Automation.' - } | Get-CompletionTestCaseData | Test-Completions -} - -Describe "Type extensible completion of type after using namespace" -Tags "CI" { - @{ - ExpectedResults = @( - @{CompletionText = "IO.TextReader"; ResultType = "Type"} - ) - TestInput = 'using namespace System; [TextR' - }, - @{ - ExpectedResults = @( - @{CompletionText = "TextReader"; ResultType = "Type"} - ) - TestInput = 'using namespace System.IO; [TextR' - }, - @{ - ExpectedResults = @( - @{CompletionText = "Alias"; ResultType = "Type"} - ) - TestInput = '[aliasatt' - }, - @{ - ExpectedResults = @( - @{CompletionText = "string"; ResultType = "Type"} - ) - TestInput = 'using namespace System; [strin' - } | Get-CompletionTestCaseData | Test-Completions -} - -Describe "Additional type name completion tests" -Tags "CI" { - @{ - ExpectedResults = @( - @{CompletionText = "System"; ResultType = "Namespace"} - @{CompletionText = "System.Security.AccessControl.SystemAcl"; ResultType = "Type"} - ) - TestInput = 'Get-Command -ParameterType System' - }, - @{ - ExpectedResults = @( - @{CompletionText = "System.Action"; ResultType = "Type"} - @{CompletionText = "System.Activator"; ResultType = "Type"} - ) - TestInput = 'Get-Command -ParameterType System.' - }, - @{ - ExpectedResults = @( - @{CompletionText = "System.Collections.Generic.LinkedList"; ResultType = "Type"; ListItemText = "LinkedList<>"; ToolTip = "System.Collections.Generic.LinkedList[T]"} - @{CompletionText = "System.Collections.Generic.LinkedListNode"; ResultType = "Type"; ListItemText = "LinkedListNode<>"; ToolTip = "System.Collections.Generic.LinkedListNode[T]"} - @{CompletionText = "System.Collections.Generic.List"; ResultType = "Type"; ListItemText = "List<>"; ToolTip = "System.Collections.Generic.List[T]"} - ) - TestInput = 'Get-Command -ParameterType System.Collections.Generic.Li' - }, - @{ - ExpectedResults = @( - @{CompletionText = "System.Collections.Generic.Dictionary"; ResultType = "Type"; ListItemText = "Dictionary<>"; ToolTip = "System.Collections.Generic.Dictionary[T1, T2]"} - ) - TestInput = 'Get-Command -ParameterType System.Collections.Generic.Dic' - } | Get-CompletionTestCaseData | Test-Completions -} - -Describe "ArgumentCompletionsAttribute tests" -Tags "CI" { - - BeforeAll { - function TestArgumentCompletionsAttribute - { - param( - [ArgumentCompletions("value1", "value2", "value3")] - $Alpha, - $Beta - ) - } - - function TestArgumentCompletionsAttribute1 - { - param( - [ArgumentCompletionsAttribute("value1", "value2", "value3")] - $Alpha, - $Beta - ) - } - - $cmdletSrc=@' - using System; - using System.Management.Automation; - using System.Collections.Generic; - - namespace Test.A { - - [Cmdlet(VerbsCommon.Get, "ArgumentCompletions")] - public class TestArgumentCompletionsAttributeCommand : PSCmdlet - { - [Parameter] - [ArgumentCompletions("value1", "value2", "value3")] - public string Param1; - - protected override void EndProcessing() - { - WriteObject(Param1); - } - } - - [Cmdlet(VerbsCommon.Get, "ArgumentCompletions1")] - public class TestArgumentCompletionsAttributeCommand1 : PSCmdlet - { - [Parameter] - [ArgumentCompletionsAttribute("value1", "value2", "value3")] - public string Param1; - - protected override void EndProcessing() - { - WriteObject(Param1); - } - } - } -'@ - $cls = Add-Type -TypeDefinition $cmdletSrc -PassThru | Select-Object -First 1 - $testModule = Import-Module $cls.Assembly -PassThru - - $testCasesScript = @( - @{ attributeName = "ArgumentCompletions" ; cmdletName = "TestArgumentCompletionsAttribute" }, - @{ attributeName = "ArgumentCompletionsAttribute"; cmdletName = "TestArgumentCompletionsAttribute1" } - ) - - $testCasesCSharp = @( - @{ attributeName = "ArgumentCompletions" ; cmdletName = "Get-ArgumentCompletions" }, - @{ attributeName = "ArgumentCompletionsAttribute"; cmdletName = "Get-ArgumentCompletions1" } - ) - } - - AfterAll { - Remove-Module -ModuleInfo $testModule - } - - It " works in script" -TestCases $testCasesScript { - param($attributeName, $cmdletName) - - $line = "$cmdletName -Alpha val" - $res = TaBexpansion2 -inputScript $line -cursorColumn $line.Length - $res.CompletionMatches.Count | Should Be 3 - $res.CompletionMatches.CompletionText -join " " | Should Be "value1 value2 value3" - { TestArgumentCompletionsAttribute -Alpha unExpectedValue } | Should Not Throw - } - - It " works in C#" -TestCases $testCasesCSharp { - param($attributeName, $cmdletName) - - $line = "$cmdletName -Param1 val" - $res = TaBexpansion2 -inputScript $line -cursorColumn $line.Length - $res.CompletionMatches.Count | Should Be 3 - $res.CompletionMatches.CompletionText -join " " | Should Be "value1 value2 value3" - { TestArgumentCompletionsAttribute -Param1 unExpectedValue } | Should Not Throw - } -} +<# + Much of this script belongs in a module, but we don't support importing classes yet. +#> +using namespace System.Management.Automation +using namespace System.Management.Automation.Language +using namespace System.Collections +using namespace System.Collections.Generic + +#region Testcase infrastructure + +class CompletionTestResult +{ + [string]$CompletionText + [string]$ListItemText + [CompletionResultType]$ResultType + [string]$ToolTip + [bool]$Found + + [bool] Equals($Other) + { + if ($Other -isnot [CompletionTestResult] -and + $Other -isnot [CompletionResult]) + { + return $false + } + + # Comparison is intentionally fuzzy - CompletionText and ResultType must be specified + # but the other properties don't need to match if they aren't specified + + if ($this.CompletionText -cne $Other.CompletionText -or + $this.ResultType -ne $Other.ResultType) + { + return $false + } + + if ($this.ListItemText -cne $Other.ListItemText -and + ![string]::IsNullOrEmpty($this.ListItemText) -and ![string]::IsNullOrEmpty($Other.ListItemText)) + { + return $false + } + + if ($this.ToolTip -cne $Other.ToolTip -and + ![string]::IsNullOrEmpty($this.ToolTip) -and ![string]::IsNullOrEmpty($Other.ToolTip)) + { + return $false + } + + return $true + } +} + +class CompletionTestCase +{ + [CompletionTestResult[]]$ExpectedResults + [string[]]$NotExpectedResults + [string]$TestInput +} + +function Get-Completions +{ + param([string]$inputScript, [int]$cursorColumn = $inputScript.Length) + + $results = [System.Management.Automation.CommandCompletion]::CompleteInput( + <#inputScript#> $inputScript, + <#cursorColumn#> $cursorColumn, + <#options#> $null) + + return $results +} + +function Get-CompletionTestCaseData +{ + param( + [Parameter(ValueFromPipeline)] + [hashtable[]]$Data) + + process + { + Write-Output ([CompletionTestCase[]]$Data) + } +} + +function Test-Completions +{ + param( + [Parameter(ValueFromPipeline)] + [CompletionTestCase[]]$TestCases) + + process + { + foreach ($test in $TestCases) + { + Context ("Command line: <" + $test.TestInput + ">") { + $results = Get-Completions $test.TestInput + foreach ($result in $results.CompletionMatches) + { + foreach ($expected in $test.ExpectedResults) + { + if ($expected.Equals($result)) + { + $expected.Found = $true + } + } + } + foreach ($expected in $test.ExpectedResults) + { + $skip = $false + if ( $expected.CompletionText -match "System.Management.Automation.PerformanceData|System.Management.Automation.Security" ) { $skip = $true } + It ($expected.CompletionText) -skip:$skip { + $expected.Found | Should Be $true + } + } + + foreach ($notExpected in $test.NotExpectedResults) + { + It "Not expected: $notExpected" { + foreach ($result in $results.CompletionMatches) + { + ($result.CompletionText -ceq $notExpected) | Should Be $False + } + } + } + } + } + } +} + +#endregion Testcase infrastructure + +function AlphaArgumentCompleter +{ + param( + [string] $CommandName, + [string] $parameterName, + [string] $wordToComplete, + [CommandAst] $commandAst, + [IDictionary] $fakeBoundParameters) + + $beta = $fakeBoundParameters['beta'] + $gamma = $fakeBoundParameters['Gamma'] + $result = "beta: $beta gamma: $gamma command: $commandName parameterName: $parameterName wordToComplete: $wordToComplete" + [CompletionResult]::new($result, $result, "ParameterValue", $result) +} + +class BetaArgumentCompleter : IArgumentCompleter +{ + [IEnumerable[CompletionResult]] CompleteArgument( + [string] $CommandName, + [string] $parameterName, + [string] $wordToComplete, + [CommandAst] $commandAst, + [IDictionary] $fakeBoundParameters) + { + $resultList = [List[CompletionResult]]::new() + + $alpha = $fakeBoundParameters['Alpha'] + $gamma = $fakeBoundParameters['Gamma'] + $result = "alpha: $alpha gamma: $gamma command: $commandName parameterName: $parameterName wordToComplete: $wordToComplete" + $resultList.Add([CompletionResult]::new($result, $result, "ParameterValue", $result)) + + return $resultList + } +} + +function TestFunction +{ + param( + [ArgumentCompleter({ AlphaArgumentCompleter @args })] + $Alpha, + [ArgumentCompleter([BetaArgumentCompleter])] + $Beta, + $Gamma + ) +} + +Describe "Script block based extensible completion" -Tags "CI" { + @{ + ExpectedResults = @( + @{CompletionText = "beta: 11 gamma: 22 command: TestFunction parameterName: Alpha wordToComplete: aa" + ResultType = "ParameterValue"}) + TestInput = 'TestFunction -Beta 11 -Gamma 22 -Alpha aa' + } | Get-CompletionTestCaseData | Test-Completions +} + +Describe "Test class based extensible completion" -Tags "CI" { + @{ + ExpectedResults = @( + @{CompletionText = "alpha: 42 gamma: 44 command: TestFunction parameterName: Beta wordToComplete: zz" + ResultType = "ParameterValue"}) + TestInput = 'TestFunction -Alpha 42 -Gamma 44 -Beta zz' + } | Get-CompletionTestCaseData | Test-Completions +} + +Describe "Test registration based extensible completion" -Tags "CI" { + Register-ArgumentCompleter -Command TestFunction -Parameter Gamma -ScriptBlock { + param( + [string] $CommandName, + [string] $parameterName, + [string] $wordToComplete, + [CommandAst] $commandAst, + [IDictionary] $fakeBoundParameters) + + $beta = $fakeBoundParameters['beta'] + $alpha = $fakeBoundParameters['alpha'] + $result = "beta: $beta alpha: $alpha command: $commandName parameterName: $parameterName wordToComplete: $wordToComplete" + [CompletionResult]::new($result, $result, "ParameterValue", $result) + } + + @{ + ExpectedResults = @( + @{CompletionText = "beta: bb alpha: aa command: TestFunction parameterName: Gamma wordToComplete: 42" + ResultType = "ParameterValue"}) + TestInput = 'TestFunction -Alpha aa -Beta bb -Gamma 42' + } | Get-CompletionTestCaseData | Test-Completions +} + +Describe "Test extensible completion of native commands" -Tags "CI" { + Register-ArgumentCompleter -Command netsh -Native -ScriptBlock { + [CompletionResult]::new('advfirewall', 'advfirewall', "ParameterValue", 'advfirewall') + [CompletionResult]::new('bridge', 'bridge', "ParameterValue", 'bridge') + } + + @{ + ExpectedResults = @( + @{CompletionText = "advfirewall"; ResultType = "ParameterValue"} + @{CompletionText = "bridge"; ResultType = "ParameterValue"} + ) + TestInput = 'netsh ' + } | Get-CompletionTestCaseData | Test-Completions +} + +Describe "Test completion of parameters for native commands" -Tags "CI" { + Register-ArgumentCompleter -Native -CommandName foo -ScriptBlock { + Param($wordToComplete) + + @("-dir", "-verbose", "-help", "-version") | + Where-Object { + $_ -match "$wordToComplete*" + } | + ForEach-Object { + [CompletionResult]::new($_, $_, [CompletionResultType]::ParameterName, $_) + } + } + + @{ + ExpectedResults = @( + @{CompletionText = "-version"; ResultType = "ParameterName"} + @{CompletionText = "-verbose"; ResultType = "ParameterName"} + @{CompletionText = "-dir"; ResultType = "ParameterName"} + @{CompletionText = "-help"; ResultType = "ParameterName"} + ) + TestInput = 'foo -' + } | Get-CompletionTestCaseData | Test-Completions + + @{ + ExpectedResults = @( + @{CompletionText = "-version"; ResultType = "ParameterName"} + @{CompletionText = "-verbose"; ResultType = "ParameterName"} + ) + TestInput = 'foo -v' + } | Get-CompletionTestCaseData | Test-Completions +} + +Describe "Test extensible completion of using namespace" -Tags "CI" { + @{ + ExpectedResults = @( + @{CompletionText = "System"; ResultType = "Namespace"} + ) + TestInput = 'Using namespace sys' + }, + @{ + ExpectedResults = @( + @{CompletionText = "System.Xml"; ResultType = "Namespace"} + @{CompletionText = "System.Data"; ResultType = "Namespace"} + @{CompletionText = "System.Collections"; ResultType = "Namespace"} + @{CompletionText = "System.IO"; ResultType = "Namespace"} + ) + TestInput = 'Using namespace system.' + }, + @{ + ExpectedResults = @( + @{CompletionText = "System.Management.Automation"; ResultType = "Namespace"} + ) + TestInput = 'Using namespace System.Management.Automati' + }, + @{ + ExpectedResults = @( + @{CompletionText = "System.Management.Automation.Host"; ResultType = "Namespace"} + @{CompletionText = "System.Management.Automation.Internal"; ResultType = "Namespace"} + @{CompletionText = "System.Management.Automation.Language"; ResultType = "Namespace"} + @{CompletionText = "System.Management.Automation.PerformanceData"; ResultType = "Namespace"} + @{CompletionText = "System.Management.Automation.Provider"; ResultType = "Namespace"} + @{CompletionText = "System.Management.Automation.Remoting"; ResultType = "Namespace"} + @{CompletionText = "System.Management.Automation.Runspaces"; ResultType = "Namespace"} + @{CompletionText = "System.Management.Automation.Security"; ResultType = "Namespace"} + ) + TestInput = 'using namespace System.Management.Automation.' + } | Get-CompletionTestCaseData | Test-Completions +} + +Describe "Type extensible completion of type after using namespace" -Tags "CI" { + @{ + ExpectedResults = @( + @{CompletionText = "IO.TextReader"; ResultType = "Type"} + ) + TestInput = 'using namespace System; [TextR' + }, + @{ + ExpectedResults = @( + @{CompletionText = "TextReader"; ResultType = "Type"} + ) + TestInput = 'using namespace System.IO; [TextR' + }, + @{ + ExpectedResults = @( + @{CompletionText = "Alias"; ResultType = "Type"} + ) + TestInput = '[aliasatt' + }, + @{ + ExpectedResults = @( + @{CompletionText = "string"; ResultType = "Type"} + ) + TestInput = 'using namespace System; [strin' + } | Get-CompletionTestCaseData | Test-Completions +} + +Describe "Additional type name completion tests" -Tags "CI" { + @{ + ExpectedResults = @( + @{CompletionText = "System"; ResultType = "Namespace"} + @{CompletionText = "System.Security.AccessControl.SystemAcl"; ResultType = "Type"} + ) + TestInput = 'Get-Command -ParameterType System' + }, + @{ + ExpectedResults = @( + @{CompletionText = "System.Action"; ResultType = "Type"} + @{CompletionText = "System.Activator"; ResultType = "Type"} + ) + TestInput = 'Get-Command -ParameterType System.' + }, + @{ + ExpectedResults = @( + @{CompletionText = "System.Collections.Generic.LinkedList"; ResultType = "Type"; ListItemText = "LinkedList<>"; ToolTip = "System.Collections.Generic.LinkedList[T]"} + @{CompletionText = "System.Collections.Generic.LinkedListNode"; ResultType = "Type"; ListItemText = "LinkedListNode<>"; ToolTip = "System.Collections.Generic.LinkedListNode[T]"} + @{CompletionText = "System.Collections.Generic.List"; ResultType = "Type"; ListItemText = "List<>"; ToolTip = "System.Collections.Generic.List[T]"} + ) + TestInput = 'Get-Command -ParameterType System.Collections.Generic.Li' + }, + @{ + ExpectedResults = @( + @{CompletionText = "System.Collections.Generic.Dictionary"; ResultType = "Type"; ListItemText = "Dictionary<>"; ToolTip = "System.Collections.Generic.Dictionary[T1, T2]"} + ) + TestInput = 'Get-Command -ParameterType System.Collections.Generic.Dic' + } | Get-CompletionTestCaseData | Test-Completions +} + +Describe "ArgumentCompletionsAttribute tests" -Tags "CI" { + + BeforeAll { + function TestArgumentCompletionsAttribute + { + param( + [ArgumentCompletions("value1", "value2", "value3")] + $Alpha, + $Beta + ) + } + + function TestArgumentCompletionsAttribute1 + { + param( + [ArgumentCompletionsAttribute("value1", "value2", "value3")] + $Alpha, + $Beta + ) + } + + $cmdletSrc=@' + using System; + using System.Management.Automation; + using System.Collections.Generic; + + namespace Test.A { + + [Cmdlet(VerbsCommon.Get, "ArgumentCompletions")] + public class TestArgumentCompletionsAttributeCommand : PSCmdlet + { + [Parameter] + [ArgumentCompletions("value1", "value2", "value3")] + public string Param1; + + protected override void EndProcessing() + { + WriteObject(Param1); + } + } + + [Cmdlet(VerbsCommon.Get, "ArgumentCompletions1")] + public class TestArgumentCompletionsAttributeCommand1 : PSCmdlet + { + [Parameter] + [ArgumentCompletionsAttribute("value1", "value2", "value3")] + public string Param1; + + protected override void EndProcessing() + { + WriteObject(Param1); + } + } + } +'@ + $cls = Add-Type -TypeDefinition $cmdletSrc -PassThru | Select-Object -First 1 + $testModule = Import-Module $cls.Assembly -PassThru + + $testCasesScript = @( + @{ attributeName = "ArgumentCompletions" ; cmdletName = "TestArgumentCompletionsAttribute" }, + @{ attributeName = "ArgumentCompletionsAttribute"; cmdletName = "TestArgumentCompletionsAttribute1" } + ) + + $testCasesCSharp = @( + @{ attributeName = "ArgumentCompletions" ; cmdletName = "Get-ArgumentCompletions" }, + @{ attributeName = "ArgumentCompletionsAttribute"; cmdletName = "Get-ArgumentCompletions1" } + ) + } + + AfterAll { + Remove-Module -ModuleInfo $testModule + } + + It " works in script" -TestCases $testCasesScript { + param($attributeName, $cmdletName) + + $line = "$cmdletName -Alpha val" + $res = TaBexpansion2 -inputScript $line -cursorColumn $line.Length + $res.CompletionMatches.Count | Should Be 3 + $res.CompletionMatches.CompletionText -join " " | Should Be "value1 value2 value3" + { TestArgumentCompletionsAttribute -Alpha unExpectedValue } | Should Not Throw + } + + It " works in C#" -TestCases $testCasesCSharp { + param($attributeName, $cmdletName) + + $line = "$cmdletName -Param1 val" + $res = TaBexpansion2 -inputScript $line -cursorColumn $line.Length + $res.CompletionMatches.Count | Should Be 3 + $res.CompletionMatches.CompletionText -join " " | Should Be "value1 value2 value3" + { TestArgumentCompletionsAttribute -Param1 unExpectedValue } | Should Not Throw + } +} diff --git a/test/powershell/Language/Parser/LanguageAndParser.TestFollowup.Tests.ps1 b/test/powershell/Language/Parser/LanguageAndParser.TestFollowup.Tests.ps1 index a1bd1ac23c3..288f3c45cc9 100644 --- a/test/powershell/Language/Parser/LanguageAndParser.TestFollowup.Tests.ps1 +++ b/test/powershell/Language/Parser/LanguageAndParser.TestFollowup.Tests.ps1 @@ -1,219 +1,219 @@ -$powershellexe = (get-process -id $PID).mainmodule.filename - -Describe "Clone array" -Tags "CI" { - It "Cast in target expr" { - (([int[]](42)).clone()) | Should Be 42 - (([int[]](1..5)).clone()).Length | Should Be 5 - (([int[]](1..5)).clone()).GetType() | Should Be ([int[]]) - - } - It "Cast not in target expr" { - $e = [int[]](42) - $e.Clone() | Should Be 42 - $e = [int[]](1..5) - $e.Clone().Length | Should Be 5 - $e.Clone().GetType() | Should Be ([int[]]) - } -} - -Describe "Set fields through PSMemberInfo" -Tags "CI" { - Add-Type @" - public struct AStruct { public string s; } -"@ - - It "via cast" { - ([AStruct]@{s = "abc" }).s | Should Be "abc" - } - It "via new-object" { - (new-object AStruct -prop @{s="abc"}).s | Should Be "abc" - } - It "via PSObject" { - $x = [AStruct]::new() - $x.psobject.properties['s'].Value = 'abc' - $x.s | Should Be "abc" - } -} - -Describe "MSFT:3309783" -Tags "CI" { - - It "Run in another process" { - # For a reliable test, we must run this in a new process because an earlier binding in this process - # could mask the bug/fix. - & $powershellexe -noprofile -command "[psobject] | ForEach-Object FullName" | Should Be System.Management.Automation.PSObject - } - - It "Run in current process" { - # For good measure, do the same thing in this process - [psobject] | ForEach-Object FullName | Should Be System.Management.Automation.PSObject - } - - It "Pipe objects derived from PSObject" { - # Related - make sure we can still pipe objects derived from PSObject - class MyPsObj : PSObject - { - MyPsObj($obj) : base($obj) { } - [string] ToString() { - # Don't change access via .psobject, that was also a bug. - return "MyObj: " + $this.psobject.BaseObject - } - } - - [MyPsObj]::new("abc").psobject.ToString() | Should Be "MyObj: abc" - [MyPsObj]::new("def") | Out-String | ForEach-Object Trim | Should Be "MyObj: def" - } -} - -Describe "ScriptBlockAst.GetScriptBlock throws on error" -Tags "CI" { - - $e = $null - - It "with parse error" { - $ast = [System.Management.Automation.Language.Parser]::ParseInput('function ', [ref]$null, [ref]$e) - { $ast.GetScriptBlock() } | Should Throw - } - - - It "with semantic errors" { - $ast = [System.Management.Automation.Language.Parser]::ParseInput('function foo{param()begin{}end{[ref][ref]1}dynamicparam{}}', [ref]$null, [ref]$e) - - { $ast.GetScriptBlock() } | Should Throw - { $ast.EndBlock.Statements[0].Body.GetScriptBlock() } | Should Throw - } -} - -Describe "Hashtable key property syntax" -Tags "CI" { - $script = @' - # First create a hashtable wrapped in PSObject - $hash = New-Object hashtable - $key = [ConsoleColor]::Red - $null = $hash.$key - $hash = @{} - $hash.$key = 'Hello' - # works in PS 2,3,4. Fails in PS 5: - $hash.$key -'@ - - It "In current process" { - # Run in current process, but something that ran earlier could influence - # the result - Invoke-Expression $script | Should Be Hello - } - - It "In different process" { - # So also run in a fresh process - $bytes = [System.Text.Encoding]::Unicode.GetBytes($script) - & $powershellexe -noprofile -encodedCommand ([Convert]::ToBase64String($bytes)) | Should Be Hello - } -} - -Describe "Assign automatic variables" -Tags "CI" { - - $autos = '_', 'args', 'this', 'input', 'pscmdlet', 'psboundparameters', 'myinvocation', 'psscriptroot', 'pscommandpath' - - foreach ($auto in $autos) - { - It "Assign auto w/ invalid type constraint - $auto" { - { & ([ScriptBlock]::Create("[datetime]`$$auto = 1")) } | Should Throw $auto - { . ([ScriptBlock]::Create("[datetime]`$$auto = 1")) } | Should Throw $auto - { & ([ScriptBlock]::Create("[runspace]`$$auto = 1")) } | Should Throw $auto - { . ([ScriptBlock]::Create("[runspace]`$$auto = 1")) } | Should Throw $auto - { & ([ScriptBlock]::Create("[notexist]`$$auto = 1")) } | Should Throw $auto - { . ([ScriptBlock]::Create("[notexist]`$$auto = 1")) } | Should Throw $auto - } - } - - foreach ($auto in $autos) - { - It "Assign auto w/o type constraint - $auto" { - & ([ScriptBlock]::Create("`$$auto = 1; `$$auto")) | Should Be 1 - . ([ScriptBlock]::Create("`$$auto = 1; `$$auto")) | Should Be 1 - } - } - - It "Assign auto w/ correct type constraint" { - & { [object]$_ = 1; $_ } | Should Be 1 - & { [object[]]$args = 1; $args } | Should Be 1 - & { [object]$this = 1; $this } | Should Be 1 - & { [object]$input = 1; $input } | Should Be 1 - # Can't test PSCmdlet or PSBoundParameters, they use an internal type - & { [System.Management.Automation.InvocationInfo]$myInvocation = $myInvocation; $myInvocation.Line } | Should Match Automation.InvocationInfo - & { [string]$PSScriptRoot = 'abc'; $PSScriptRoot } | Should Be abc - & { [string]$PSCommandPath = 'abc'; $PSCommandPath } | Should Be abc - } -} - -Describe "Assign readonly/constant variables" -Tags "CI" { - - $testCase = @( - @{ sb_wo_conversion = { $? = 1 }; name = '$? = 1' } - @{ sb_wo_conversion = { $HOME = 1 }; name = '$HOME = 1' } - @{ sb_wo_conversion = { $PID = 1 }; name = '$PID = 1' } - ) - - It "Assign readonly/constant variables w/o type constraint - ''" -TestCases $testCase { - param($sb_wo_conversion) - { & $sb_wo_conversion } | Should Throw - { . $sb_wo_conversion } | Should Throw - } - - $testCase = @( - @{ sb_w_conversion = { [datetime]$? = 1 }; name = '[datetime]$? = 1' } - @{ sb_w_conversion = { [datetime]$HOME = 1 }; name = '[datetime]$HOME = 1' } - @{ sb_w_conversion = { [datetime]$PID = 1 }; name = '[datetime]$PID = 1' } - ) - - It "Assign readonly/constant variables w/ type constraint - ''" -TestCases $testCase { - param($sb_w_conversion) - { & $sb_w_conversion } | Should Throw - { . $sb_w_conversion } | Should Throw - } -} - -Describe "Attribute error position" -Tags "CI" { - It "Ambiguous overloads" { - try - { - & { - param( - [ValidateNotNull(1,2,3,4)] - $param - ) - } - throw "Should have thrown" - } - catch - { - $_.InvocationInfo.Line | Should Match ValidateNotNull - $_.FullyQualifiedErrorId | Should Be MethodCountCouldNotFindBest - } - } -} - -Describe "Multiple alias attributes" -Tags "CI" { - It "basic test" { - function foo { - param( - [alias('aa')] - [alias('bb')] - $cc - ) - $cc - } - - foo -aa 1 | Should Be 1 - foo -bb 2 | Should Be 2 - foo -cc 3 | Should Be 3 - } -} - -Describe "Members of System.Type" -Tags "CI" { - It "Members in public classes derived from System.Type should be found" { - class MyType : System.Collections.IEnumerable - { - [System.Collections.IEnumerator] GetEnumerator() { return $null } - } - - [type] | Get-Member ImplementedInterfaces | Should Be 'System.Collections.Generic.IEnumerable[type] ImplementedInterfaces {get;}' - [MyType].ImplementedInterfaces | Should Be System.Collections.IEnumerable - } -} +$powershellexe = (get-process -id $PID).mainmodule.filename + +Describe "Clone array" -Tags "CI" { + It "Cast in target expr" { + (([int[]](42)).clone()) | Should Be 42 + (([int[]](1..5)).clone()).Length | Should Be 5 + (([int[]](1..5)).clone()).GetType() | Should Be ([int[]]) + + } + It "Cast not in target expr" { + $e = [int[]](42) + $e.Clone() | Should Be 42 + $e = [int[]](1..5) + $e.Clone().Length | Should Be 5 + $e.Clone().GetType() | Should Be ([int[]]) + } +} + +Describe "Set fields through PSMemberInfo" -Tags "CI" { + Add-Type @" + public struct AStruct { public string s; } +"@ + + It "via cast" { + ([AStruct]@{s = "abc" }).s | Should Be "abc" + } + It "via new-object" { + (new-object AStruct -prop @{s="abc"}).s | Should Be "abc" + } + It "via PSObject" { + $x = [AStruct]::new() + $x.psobject.properties['s'].Value = 'abc' + $x.s | Should Be "abc" + } +} + +Describe "MSFT:3309783" -Tags "CI" { + + It "Run in another process" { + # For a reliable test, we must run this in a new process because an earlier binding in this process + # could mask the bug/fix. + & $powershellexe -noprofile -command "[psobject] | ForEach-Object FullName" | Should Be System.Management.Automation.PSObject + } + + It "Run in current process" { + # For good measure, do the same thing in this process + [psobject] | ForEach-Object FullName | Should Be System.Management.Automation.PSObject + } + + It "Pipe objects derived from PSObject" { + # Related - make sure we can still pipe objects derived from PSObject + class MyPsObj : PSObject + { + MyPsObj($obj) : base($obj) { } + [string] ToString() { + # Don't change access via .psobject, that was also a bug. + return "MyObj: " + $this.psobject.BaseObject + } + } + + [MyPsObj]::new("abc").psobject.ToString() | Should Be "MyObj: abc" + [MyPsObj]::new("def") | Out-String | ForEach-Object Trim | Should Be "MyObj: def" + } +} + +Describe "ScriptBlockAst.GetScriptBlock throws on error" -Tags "CI" { + + $e = $null + + It "with parse error" { + $ast = [System.Management.Automation.Language.Parser]::ParseInput('function ', [ref]$null, [ref]$e) + { $ast.GetScriptBlock() } | Should Throw + } + + + It "with semantic errors" { + $ast = [System.Management.Automation.Language.Parser]::ParseInput('function foo{param()begin{}end{[ref][ref]1}dynamicparam{}}', [ref]$null, [ref]$e) + + { $ast.GetScriptBlock() } | Should Throw + { $ast.EndBlock.Statements[0].Body.GetScriptBlock() } | Should Throw + } +} + +Describe "Hashtable key property syntax" -Tags "CI" { + $script = @' + # First create a hashtable wrapped in PSObject + $hash = New-Object hashtable + $key = [ConsoleColor]::Red + $null = $hash.$key + $hash = @{} + $hash.$key = 'Hello' + # works in PS 2,3,4. Fails in PS 5: + $hash.$key +'@ + + It "In current process" { + # Run in current process, but something that ran earlier could influence + # the result + Invoke-Expression $script | Should Be Hello + } + + It "In different process" { + # So also run in a fresh process + $bytes = [System.Text.Encoding]::Unicode.GetBytes($script) + & $powershellexe -noprofile -encodedCommand ([Convert]::ToBase64String($bytes)) | Should Be Hello + } +} + +Describe "Assign automatic variables" -Tags "CI" { + + $autos = '_', 'args', 'this', 'input', 'pscmdlet', 'psboundparameters', 'myinvocation', 'psscriptroot', 'pscommandpath' + + foreach ($auto in $autos) + { + It "Assign auto w/ invalid type constraint - $auto" { + { & ([ScriptBlock]::Create("[datetime]`$$auto = 1")) } | Should Throw $auto + { . ([ScriptBlock]::Create("[datetime]`$$auto = 1")) } | Should Throw $auto + { & ([ScriptBlock]::Create("[runspace]`$$auto = 1")) } | Should Throw $auto + { . ([ScriptBlock]::Create("[runspace]`$$auto = 1")) } | Should Throw $auto + { & ([ScriptBlock]::Create("[notexist]`$$auto = 1")) } | Should Throw $auto + { . ([ScriptBlock]::Create("[notexist]`$$auto = 1")) } | Should Throw $auto + } + } + + foreach ($auto in $autos) + { + It "Assign auto w/o type constraint - $auto" { + & ([ScriptBlock]::Create("`$$auto = 1; `$$auto")) | Should Be 1 + . ([ScriptBlock]::Create("`$$auto = 1; `$$auto")) | Should Be 1 + } + } + + It "Assign auto w/ correct type constraint" { + & { [object]$_ = 1; $_ } | Should Be 1 + & { [object[]]$args = 1; $args } | Should Be 1 + & { [object]$this = 1; $this } | Should Be 1 + & { [object]$input = 1; $input } | Should Be 1 + # Can't test PSCmdlet or PSBoundParameters, they use an internal type + & { [System.Management.Automation.InvocationInfo]$myInvocation = $myInvocation; $myInvocation.Line } | Should Match Automation.InvocationInfo + & { [string]$PSScriptRoot = 'abc'; $PSScriptRoot } | Should Be abc + & { [string]$PSCommandPath = 'abc'; $PSCommandPath } | Should Be abc + } +} + +Describe "Assign readonly/constant variables" -Tags "CI" { + + $testCase = @( + @{ sb_wo_conversion = { $? = 1 }; name = '$? = 1' } + @{ sb_wo_conversion = { $HOME = 1 }; name = '$HOME = 1' } + @{ sb_wo_conversion = { $PID = 1 }; name = '$PID = 1' } + ) + + It "Assign readonly/constant variables w/o type constraint - ''" -TestCases $testCase { + param($sb_wo_conversion) + { & $sb_wo_conversion } | Should Throw + { . $sb_wo_conversion } | Should Throw + } + + $testCase = @( + @{ sb_w_conversion = { [datetime]$? = 1 }; name = '[datetime]$? = 1' } + @{ sb_w_conversion = { [datetime]$HOME = 1 }; name = '[datetime]$HOME = 1' } + @{ sb_w_conversion = { [datetime]$PID = 1 }; name = '[datetime]$PID = 1' } + ) + + It "Assign readonly/constant variables w/ type constraint - ''" -TestCases $testCase { + param($sb_w_conversion) + { & $sb_w_conversion } | Should Throw + { . $sb_w_conversion } | Should Throw + } +} + +Describe "Attribute error position" -Tags "CI" { + It "Ambiguous overloads" { + try + { + & { + param( + [ValidateNotNull(1,2,3,4)] + $param + ) + } + throw "Should have thrown" + } + catch + { + $_.InvocationInfo.Line | Should Match ValidateNotNull + $_.FullyQualifiedErrorId | Should Be MethodCountCouldNotFindBest + } + } +} + +Describe "Multiple alias attributes" -Tags "CI" { + It "basic test" { + function foo { + param( + [alias('aa')] + [alias('bb')] + $cc + ) + $cc + } + + foo -aa 1 | Should Be 1 + foo -bb 2 | Should Be 2 + foo -cc 3 | Should Be 3 + } +} + +Describe "Members of System.Type" -Tags "CI" { + It "Members in public classes derived from System.Type should be found" { + class MyType : System.Collections.IEnumerable + { + [System.Collections.IEnumerator] GetEnumerator() { return $null } + } + + [type] | Get-Member ImplementedInterfaces | Should Be 'System.Collections.Generic.IEnumerable[type] ImplementedInterfaces {get;}' + [MyType].ImplementedInterfaces | Should Be System.Collections.IEnumerable + } +} diff --git a/test/powershell/Language/Parser/MethodInvocation.Tests.ps1 b/test/powershell/Language/Parser/MethodInvocation.Tests.ps1 index 1c560cd41b2..7ec310f8772 100644 --- a/test/powershell/Language/Parser/MethodInvocation.Tests.ps1 +++ b/test/powershell/Language/Parser/MethodInvocation.Tests.ps1 @@ -1,81 +1,81 @@ -if ( $IsCoreCLR ) { - return -} - -Describe "Interface inheritance with remoting proxies" -Tags "CI" { - $src = @" -using System; -using System.ServiceModel; - -namespace MSFT_716893 -{ - [ServiceContract] - public interface IInterface1 - { - [OperationContract]string BaseOperation(int i); - } - - [ServiceContract] - public interface IInterface2 : IInterface1 - { - [OperationContract(Name="op1")]string Operation(string a); - [OperationContract(Name="op2")]string Operation(string a, string b); - } - - public class ServiceImplementation : IInterface2 - { - public string Operation(string a) { return "1 - " + a; } - public string Operation(string a, string b) { return "2 - " + a + " " + b; } - public string BaseOperation(int i) { return "3 - " + i; } - } - - public static class Service - { - static ServiceHost serviceHost; - - public static void Init() - { - Uri baseAddress = new Uri("http://localhost:8080/service"); - serviceHost = new ServiceHost(typeof(ServiceImplementation), baseAddress); - serviceHost.Open(); - } - - public static IInterface1 GetProxy() - { - ChannelFactory factory = new ChannelFactory( - serviceHost.Description.Endpoints[0].Binding, - serviceHost.Description.Endpoints[0].Address); - return factory.CreateChannel(); - } - - public static void Close() - { - serviceHost.Close(); - } - } -} -"@ - - Add-Type -TypeDefinition $src -ReferencedAssemblies System.ServiceModel.dll - - BeforeEach { - [MSFT_716893.Service]::Init() - $proxy = [MSFT_716893.Service]::GetProxy() - } - - AfterEach { - [MSFT_716893.Service]::Close() - } - - It "Direct invocation" { - $proxy.Operation("a") | Should Be "1 - a" - $proxy.Operation("a", "b") | Should Be "2 - a b" - $proxy.BaseOperation(42) | Should Be "3 - 42" - } - - It "Invocation via method constraints" { - ([MSFT_716893.IInterface2]$proxy).Operation("c") | Should Be "1 - c" - ([MSFT_716893.IInterface2]$proxy).Operation("d", "e") | Should Be "2 - d e" - ([MSFT_716893.IInterface1]$proxy).BaseOperation(22) | Should Be "3 - 22" - } -} +if ( $IsCoreCLR ) { + return +} + +Describe "Interface inheritance with remoting proxies" -Tags "CI" { + $src = @" +using System; +using System.ServiceModel; + +namespace MSFT_716893 +{ + [ServiceContract] + public interface IInterface1 + { + [OperationContract]string BaseOperation(int i); + } + + [ServiceContract] + public interface IInterface2 : IInterface1 + { + [OperationContract(Name="op1")]string Operation(string a); + [OperationContract(Name="op2")]string Operation(string a, string b); + } + + public class ServiceImplementation : IInterface2 + { + public string Operation(string a) { return "1 - " + a; } + public string Operation(string a, string b) { return "2 - " + a + " " + b; } + public string BaseOperation(int i) { return "3 - " + i; } + } + + public static class Service + { + static ServiceHost serviceHost; + + public static void Init() + { + Uri baseAddress = new Uri("http://localhost:8080/service"); + serviceHost = new ServiceHost(typeof(ServiceImplementation), baseAddress); + serviceHost.Open(); + } + + public static IInterface1 GetProxy() + { + ChannelFactory factory = new ChannelFactory( + serviceHost.Description.Endpoints[0].Binding, + serviceHost.Description.Endpoints[0].Address); + return factory.CreateChannel(); + } + + public static void Close() + { + serviceHost.Close(); + } + } +} +"@ + + Add-Type -TypeDefinition $src -ReferencedAssemblies System.ServiceModel.dll + + BeforeEach { + [MSFT_716893.Service]::Init() + $proxy = [MSFT_716893.Service]::GetProxy() + } + + AfterEach { + [MSFT_716893.Service]::Close() + } + + It "Direct invocation" { + $proxy.Operation("a") | Should Be "1 - a" + $proxy.Operation("a", "b") | Should Be "2 - a b" + $proxy.BaseOperation(42) | Should Be "3 - 42" + } + + It "Invocation via method constraints" { + ([MSFT_716893.IInterface2]$proxy).Operation("c") | Should Be "1 - c" + ([MSFT_716893.IInterface2]$proxy).Operation("d", "e") | Should Be "2 - d e" + ([MSFT_716893.IInterface1]$proxy).BaseOperation(22) | Should Be "3 - 22" + } +} diff --git a/test/powershell/Language/Parser/ParameterBinding.Tests.ps1 b/test/powershell/Language/Parser/ParameterBinding.Tests.ps1 index a34b92b1631..b259b5b07cd 100644 --- a/test/powershell/Language/Parser/ParameterBinding.Tests.ps1 +++ b/test/powershell/Language/Parser/ParameterBinding.Tests.ps1 @@ -1,100 +1,100 @@ - -Describe 'Argument transformation attribute on optional argument with explicit $null' -Tags "CI" { - $tdefinition = @' - using System; - using System.Management.Automation; - using System.Reflection; - - namespace MSFT_1407291 - { - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)] - public class AddressTransformationAttribute : ArgumentTransformationAttribute - { - public override object Transform(EngineIntrinsics engineIntrinsics, object inputData) - { - return (ulong) 42; - } - } - - [Cmdlet(VerbsLifecycle.Invoke, "CSharpCmdletTakesUInt64")] - [OutputType(typeof(System.String))] - public class Cmdlet1 : PSCmdlet - { - [Parameter(Mandatory = false)] - [AddressTransformation] - public ulong Address { get; set; } - - protected override void ProcessRecord() - { - WriteObject(Address); - } - } - - [Cmdlet(VerbsLifecycle.Invoke, "CSharpCmdletTakesObject")] - [OutputType(typeof(System.String))] - public class Cmdlet2 : PSCmdlet - { - [Parameter(Mandatory = false)] - [AddressTransformation] - public object Address { get; set; } - - protected override void ProcessRecord() - { - WriteObject(Address ?? "passed in null"); - } - } - } -'@ - $mod = Add-Type -PassThru -TypeDefinition $tdefinition - - Import-Module $mod[0].Assembly -ErrorVariable ErrorImportingModule - - function Invoke-ScriptFunctionTakesObject - { - param([MSFT_1407291.AddressTransformation()] - [Parameter(Mandatory = $false)] - [object]$Address = "passed in null") - - return $Address - } - - function Invoke-ScriptFunctionTakesUInt64 - { - param([MSFT_1407291.AddressTransformation()] - [Parameter(Mandatory = $false)] - [Uint64]$Address = 11) - - return $Address - } - - - It "There was no error importing the in-memory module" { - $ErrorImportingModule | Should Be $null - } - - It "Script function takes object" { - Invoke-ScriptFunctionTakesObject | Should Be 42 - } - It "Script function takes uint64" { - Invoke-ScriptFunctionTakesUInt64 | Should Be 42 - } - it "csharp cmdlet takes object" { - Invoke-CSharpCmdletTakesObject | Should Be "passed in null" - } - it "csharp cmdlet takes uint64" { - Invoke-CSharpCmdletTakesUInt64 | Should Be 0 - } - - it "script function takes object when parameter is null" { - Invoke-ScriptFunctionTakesObject -Address $null | Should Be 42 - } - it "script function takes unit64 when parameter is null" { - Invoke-ScriptFunctionTakesUInt64 -Address $null | Should Be 42 - } - it "script csharp cmdlet takes object when parameter is null" { - Invoke-CSharpCmdletTakesObject -Address $null | Should Be 42 - } - it "script csharp cmdlet takes uint64 when parameter is null" { - Invoke-CSharpCmdletTakesUInt64 -Address $null | Should Be 42 - } -} + +Describe 'Argument transformation attribute on optional argument with explicit $null' -Tags "CI" { + $tdefinition = @' + using System; + using System.Management.Automation; + using System.Reflection; + + namespace MSFT_1407291 + { + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)] + public class AddressTransformationAttribute : ArgumentTransformationAttribute + { + public override object Transform(EngineIntrinsics engineIntrinsics, object inputData) + { + return (ulong) 42; + } + } + + [Cmdlet(VerbsLifecycle.Invoke, "CSharpCmdletTakesUInt64")] + [OutputType(typeof(System.String))] + public class Cmdlet1 : PSCmdlet + { + [Parameter(Mandatory = false)] + [AddressTransformation] + public ulong Address { get; set; } + + protected override void ProcessRecord() + { + WriteObject(Address); + } + } + + [Cmdlet(VerbsLifecycle.Invoke, "CSharpCmdletTakesObject")] + [OutputType(typeof(System.String))] + public class Cmdlet2 : PSCmdlet + { + [Parameter(Mandatory = false)] + [AddressTransformation] + public object Address { get; set; } + + protected override void ProcessRecord() + { + WriteObject(Address ?? "passed in null"); + } + } + } +'@ + $mod = Add-Type -PassThru -TypeDefinition $tdefinition + + Import-Module $mod[0].Assembly -ErrorVariable ErrorImportingModule + + function Invoke-ScriptFunctionTakesObject + { + param([MSFT_1407291.AddressTransformation()] + [Parameter(Mandatory = $false)] + [object]$Address = "passed in null") + + return $Address + } + + function Invoke-ScriptFunctionTakesUInt64 + { + param([MSFT_1407291.AddressTransformation()] + [Parameter(Mandatory = $false)] + [Uint64]$Address = 11) + + return $Address + } + + + It "There was no error importing the in-memory module" { + $ErrorImportingModule | Should Be $null + } + + It "Script function takes object" { + Invoke-ScriptFunctionTakesObject | Should Be 42 + } + It "Script function takes uint64" { + Invoke-ScriptFunctionTakesUInt64 | Should Be 42 + } + it "csharp cmdlet takes object" { + Invoke-CSharpCmdletTakesObject | Should Be "passed in null" + } + it "csharp cmdlet takes uint64" { + Invoke-CSharpCmdletTakesUInt64 | Should Be 0 + } + + it "script function takes object when parameter is null" { + Invoke-ScriptFunctionTakesObject -Address $null | Should Be 42 + } + it "script function takes unit64 when parameter is null" { + Invoke-ScriptFunctionTakesUInt64 -Address $null | Should Be 42 + } + it "script csharp cmdlet takes object when parameter is null" { + Invoke-CSharpCmdletTakesObject -Address $null | Should Be 42 + } + it "script csharp cmdlet takes uint64 when parameter is null" { + Invoke-CSharpCmdletTakesUInt64 -Address $null | Should Be 42 + } +} diff --git a/test/powershell/Language/Parser/Parser.Tests.ps1 b/test/powershell/Language/Parser/Parser.Tests.ps1 index d1cddfa7efe..95b9eb2950c 100644 --- a/test/powershell/Language/Parser/Parser.Tests.ps1 +++ b/test/powershell/Language/Parser/Parser.Tests.ps1 @@ -1,918 +1,918 @@ -Describe "ParserTests (admin\monad\tests\monad\src\engine\core\ParserTests.cs)" -Tags "CI" { - BeforeAll { - $functionDefinitionFile = Join-Path -Path $TestDrive -ChildPath "functionDefinition.ps1" - $functionDefinition = @' - function testcmd-parserbvt - { - [CmdletBinding()] - param ( - [Parameter(Position = 0)] - [string] $Property1 = "unset", - - [Parameter(Position = 1)] - [string] $Property2 = "unset", - - [Parameter(Position = 2)] - [string] $Property3 = "unset", - - [Parameter()] - [ValidateSet("default","array","object","nestedobject","struct","mshobject","nulltostring")] - [string]$ReturnType = "default" - ) - - BEGIN {} - - PROCESS { - if ( ! $ReturnType ) { - $ReturnType = "default" - } - - switch ( $ReturnType ) - { - "default" { - $result = "$Property1;$Property2;$Property3" - break - } - "array" { - $result = 1,2,3 - break - } - "object" { - $result = new-object psobject - break - } - "nestedobject" { - $result = [pscustomobject]@{Name="John";Person=[pscustomobject]@{Name="John";Age=30}} - break - } - "struct" { - $result = [pscustomobject]@{Name="John";Age=30} - break - } - "mshobject" { - $result = new-object psobject - break - } - "nulltostring" { - $result = $null - break - } - default { - throw ([invalidoperationexception]::new("ReturnType parameter wasn't of any Expected value!")) - break - } - } - return $result - } - - END {} - } -'@ - $functionDefinition>$functionDefinitionFile - - $PowerShell = [powershell]::Create() - $PowerShell.AddScript(". $functionDefinitionFile").Invoke() - $PowerShell.Commands.Clear() - function ExecuteCommand { - param ([string]$command) - try { - $PowerShell.AddScript($command).Invoke() - } - finally { - $PowerShell.Commands.Clear() - } - } - } - BeforeEach { - $testfile = Join-Path -Path $TestDrive -ChildPath "testfile.ps1" - $shellfile = Join-Path -Path $TestDrive -ChildPath "testfile.cmd" - $testfolder1 = Join-Path -Path $TestDrive -ChildPath "dir1" - $testfolder2 = Join-Path -Path $testfolder1 -ChildPath "dir2" - if(-not(Test-Path $testfolder1)) - { - New-Item $testfolder1 -Type Directory - } - if(-not(Test-Path $testfolder2)) - { - New-Item $testfolder2 -Type Directory - } - $testdirfile1 = Join-Path -Path $testfolder2 -ChildPath "testdirfile1.txt" - $testdirfile2 = Join-Path -Path $testfolder2 -ChildPath "testdirfile2.txt" - "">$testdirfile1 - "">$testdirfile2 - } - AfterEach { - if(Test-Path $testfile) - { - Remove-Item $testfile - } - if(Test-Path $shellfile) - { - Remove-Item $shellfile - } - if(Test-Path $testdirfile1) - { - Remove-Item $testdirfile1 - } - if(Test-Path $testdirfile2) - { - Remove-Item $testdirfile2 - } - if(Test-Path $testfolder2) - { - Remove-Item $testfolder2 - } - if(Test-Path $testfolder1) - { - Remove-Item $testfolder1 - } - } - - AfterAll { - if(Test-Path $functionDefinitionFile) - { - Remove-Item $functionDefinitionFile - } - } - - It "Throws a syntax error when parsing a string without a closing quote. (line 164)" { - try { - ExecuteCommand '"This is a test' - throw "Execution OK" - } - catch { - $_.FullyQualifiedErrorId | Should be "IncompleteParseException" - } - } - - It "Throws an error if an open parenthesis is not closed (line 176)" { - try { - ExecuteCommand "(" - throw "Execution OK" - } - catch { - $_.FullyQualifiedErrorId | should be "IncompleteParseException" - } - } - - It "Throws an exception if the the first statement starts with an empty pipe element (line 188)" { - try { - ExecuteCommand "| get-location" - throw "Execution OK" - } - catch { - $_.FullyQualifiedErrorId | should be "ParseException" - } - } - - It "Throws an CommandNotFoundException exception if using a label in front of an if statement is not allowed. (line 225)"{ - $PowerShell.Streams.Error.Clear() - ExecuteCommand ":foo if ($x -eq 3) { 1 }" - $PowerShell.HadErrors | should be $true - $PowerShell.Streams.Error.FullyQualifiedErrorId | should be "CommandNotFoundException" - } - - It "Pipe an expression into a value expression. (line 237)" { - try { - ExecuteCommand "testcmd-parserbvt | 3" - throw "Execution OK" - } - catch { - $_.FullyQualifiedErrorId | should be "ParseException" - } - - try { - ExecuteCommand "testcmd-parserbvt | $(1 + 1)" - throw "Execution OK" - } - catch { - $_.FullyQualifiedErrorId | should be "ParseException" - } - - try { - ExecuteCommand "testcmd-parserbvt | 'abc'" - throw "Execution OK" - } - catch { - $_.FullyQualifiedErrorId | should be "ParseException" - } - } - - It "Throws when you pipe into a value expression (line 238)" { - foreach($command in "1;2;3|3",'1;2;3|$(1+1)',"1;2;3|'abc'") { - try { - ExecuteCommand $command - throw "Execution OK" - } - catch { - $_.FullyQualifiedErrorId | Should be "ParseException" - } - } - } - - It "Throws an incomplete parse exception when a comma follows an expression (line 247)" { - try { - ExecuteCommand "(1+ 1)," - throw "Execution OK" - } - catch { - $_.FullyQualifiedErrorId | Should be "IncompleteParseException" - } - } - - - It "Test that invoke has a higher precedence for a script than for an executable. (line 279)" { - "1">$testfile - $result = ExecuteCommand ". $testfile" - $result | should be 1 - } - - It "This test will check that a path is correctly interpreted when using '..' and '.' (line 364)" { - $result = ExecuteCommand "set-location $TestDrive; get-childitem dir1\.\.\.\..\dir1\.\dir2\..\..\dir1\.\dir2" - $result.Count | should be 2 - $result[0].Name | should be "testdirfile1.txt" - $result[1].Name | should be "testdirfile2.txt" - } - - It "This test will check that the parser can handle a mix of forward slashes and back slashes in the path (line 417)" { - $result = ExecuteCommand "get-childitem $TestDrive/dir1/./.\.\../dir1/.\dir2\../..\dir1\.\dir2" - $result.Count | should be 2 - $result[0].Name | should be "testdirfile1.txt" - $result[1].Name | should be "testdirfile2.txt" - } - - It "This test checks that the asterisk globs as expected. (line 545)" { - $result = ExecuteCommand "get-childitem $TestDrive/dir1\dir2\*.txt" - $result.Count | should be 2 - $result[0].Name | should be "testdirfile1.txt" - $result[1].Name | should be "testdirfile2.txt" - } - - It "This test checks that we can use a range for globbing: [1-2] (line 557)" { - $result = ExecuteCommand "get-childitem $TestDrive/dir1\dir2\testdirfile[1-2].txt" - $result.Count | should be 2 - $result[0].Name | should be "testdirfile1.txt" - $result[1].Name | should be "testdirfile2.txt" - } - - It "This test will check that escaping the $ sigil inside single quotes simply returns the $ character. (line 583)" { - $result = ExecuteCommand "'`$'" - $result | should be "`$" - } - - It "Test that escaping a space just returns that space. (line 593)" { - $result = ExecuteCommand '"foo` bar"' - $result | should be "foo bar" - } - - It "Test that escaping the character 'e' returns the ESC character (0x1b)." { - $result = ExecuteCommand '"`e"' - $result | should be ([char]0x1b) - } - - Context "Test Unicode escape sequences." { - # These tests require the file to be saved with a BOM. Unfortunately when this UTF8 file is read by - # PowerShell without a BOM, the file is incorrectly interpreted as ASCII. - It 'Test that the bracketed Unicode escape sequence `u{0} returns minimum char.' { - $result = ExecuteCommand '"`u{0}"' - [int]$result[0] | should be 0 - } - - It 'Test that the bracketed Unicode escape sequence `u{10FFFF} returns maximum surrogate char pair.' { - $result = ExecuteCommand '"`u{10FFFF}"' - [int]$result[0] | should be 0xDBFF # max value for high surrogate of surrogate pair - [int]$result[1] | should be 0xDFFF # max value for low surrogate of surrogate pair - } - - It 'Test that the bracketed Unicode escape sequence `u{a9} returns the © character.' { - $result = ExecuteCommand '"`u{a9}"' - $result | should be '©' - } - - It 'Test that Unicode escape sequence `u{2195} in string returns the ↕ character.' { - $result = ExecuteCommand '"foo`u{2195}abc"' - $result | should be "foo↕abc" - } - - It 'Test that the bracketed Unicode escape sequence `u{1f44d} returns surrogate pair for emoji 👍 character.' { - $result = ExecuteCommand '"`u{1f44d}"' - $result | should be "👍" - } - - It 'Test that Unicode escape sequence `u{2195} in here string returns the ↕ character.' { - $result = ExecuteCommand ("@`"`n`n" + 'foo`u{2195}abc' + "`n`n`"@") - $result | should be "`nfoo↕abc`n" - } - - It 'Test that Unicode escape sequence in single quoted is not processed.' { - $result = ExecuteCommand '''foo`u{2195}abc''' - $result | should be 'foo`u{2195}abc' - } - - It 'Test that Unicode escape sequence in single quoted here string is not processed.' { - $result = ExecuteCommand @" -@' - -foo``u{2195}abc - -'@ -"@ - $result | should be "`r`nfoo``u{2195}abc`r`n" - } - - It "Test that two consecutive Unicode escape sequences are tokenized correctly." { - $result = ExecuteCommand '"`u{007b}`u{007d}"' - $result | should be '{}' - } - - It "Test that a Unicode escape sequence can be used in a command name." { - function xyzzy`u{2195}($p) {$p} - $cmd = Get-Command xyzzy`u{2195} -ErrorAction SilentlyContinue - $cmd | should not BeNullOrEmpty - $cmd.Name | should be 'xyzzy↕' - xyzzy`u{2195} 42 | should be 42 - } - - It "Test that a Unicode escape sequence can be used in a variable name." { - ${fooxyzzy`u{2195}} = 42 - $var = Get-Variable -Name fooxyzzy* -ErrorAction SilentlyContinue - $var | should not BeNullOrEmpty - $var.Name | should be "fooxyzzy↕" - $var.Value | should be 42 - } - - It "Test that a Unicode escape sequence can be used in an argument." { - Write-Output `u{a9}` Acme` Inc | should be "© Acme Inc" - } - } - - It "Test that escaping any character with no special meaning just returns that char. (line 602)" { - $result = ExecuteCommand '"fo`obar"' - $result | should be "foobar" - } - - Context "Test that we support all of the C# escape sequences. We use the ` instead of \. (line 613)" { - # the first two sequences are tricky, because we need to provide something to - # execute without causing an incomplete parse error - $tests = @{ sequence = "write-output ""`'"""; expected = ([char]39) }, - @{ sequence = 'write-output "`""'; expected = ([char]34) }, - # this is a string, of 2 "\", the initial backtick should essentially be ignored - @{ sequence = '"`\\"'; expected = '\\' }, - # control sequences - @{ sequence = '"`0"'; expected = ([char]0) }, # null - @{ sequence = '"`a"'; expected = ([char]7) }, - @{ sequence = '"`b"'; expected = ([char]8) }, # backspace - @{ sequence = '"`f"'; expected = ([char]12) }, # form - @{ sequence = '"`n"'; expected = ([char]10) }, # newline - @{ sequence = '"`r"'; expected = ([char]13) }, # return - @{ sequence = '"`t"'; expected = ([char]9) }, # tab - @{ sequence = '"`v"'; expected = ([char]11) } - It "C# escape sequence is supported using `` instead of \. (line 613)" -TestCases $tests { - param ( $sequence, $expected ) - $result = ExecuteCommand $sequence - $result | should be $expected - } - } - - - It "This test checks that array substitution occurs inside double quotes. (line 646)" { - $result = ExecuteCommand '$MyArray = "a","b";"Hello $MyArray"' - $result | should be "Hello a b" - } - - It "This tests declaring an array in nested variable tables. (line 761)" { - $result = ExecuteCommand "`$Variable:vtbl1:vtbl2:b=@(5,6);`$Variable:vtbl1:vtbl2:b" - $result.Count | should be 2 - $result[0] | should be 5 - $result[1] | should be 6 - } - - It "Test a simple multiple assignment. (line 773)" { - $result = ExecuteCommand '$one,$two = 1,2,3; "One = $one"; "Two = $two"' - $result.Count | should be 2 - $result[0] | should be "One = 1" - $result[1] | should be "Two = 2 3" - } - - It "Tests script, global and local scopes from a function inside a script. (line 824)" { - "`$var = 'script';function func { `$var; `$var = 'local'; `$local:var; `$script:var; `$global:var };func;`$var;">$testfile - ExecuteCommand "`$var = 'global'" - $result = ExecuteCommand "$testfile" - $result.Count | should be 5 - $result[0] | should be "script" - $result[1] | should be "local" - $result[2] | should be "script" - $result[3] | should be "global" - $result[4] | should be "script" - } - - It "Use break inside of a loop that is inside another loop. (line 945)" { - $commands = " while (1) { 1; while(1) { 2; break; 3; }; 4; break; 5; } ", - " for (;;) { 1; for(;;) { 2; break; 3; }; 4; break; 5; } ", - " foreach(`$a in 1,2,3) { 1; foreach( `$b in 1,2,3 ) { 2; break; 3; }; 4; break; 5; } " - $results = "1", "2", "4" - $i = 0 - for(;$i -lt $commands.Count;$i++) - { - $result = ExecuteCommand $commands[$i] - $result | should be $results - } - } - - It "Use break in two loops with same label. (line 967)" { - $commands = " :foo while (1) { 1; :foo while(1) { 2; break foo; 3; }; 4; break; 5; } ", - " :foo for (;;) { 1; :foo for(;;) { 2; break foo; 3; }; 4; break; 5; } ", - " :foo foreach(`$a in 1,2,3) { 1; :foo foreach( `$b in 1,2,3 ) { 2; break foo; 3; }; 4; break; 5; } " - $results = "1", "2", "4" - $i = 0 - for(;$i -lt $commands.Count;$i++) - { - $result = ExecuteCommand $commands[$i] - $result | should be $results - } - } - - It "Try continue inside of different loop statements. (line 1039)" { - $commands = " `$a = 0; while (`$a -lt 2) { `$a; `$a += 1; continue; 2; } ", - " for (`$a = 0;`$a -lt 2; `$a += 1) { 9; continue; 3; } ", - " foreach(`$a in 0,1) { `$a; continue; 2; } " - $result = ExecuteCommand $commands[0] - $result | should be "0", "1" - $result = ExecuteCommand $commands[1] - $result | should be "9", "9" - $result = ExecuteCommand $commands[2] - $result | should be "0", "1" - } - - It "Use a label to continue an inner loop. (line 1059)" { - $commands = " `$x = 0; while (`$x -lt 1) { `$x += 1; `$x; `$a = 0; :foo while(`$a -lt 2) { `$a += 1; `$a; continue foo; 3; }; 4; continue; 5; } ", - " for (`$x = 0;`$x -lt 1;`$x += 1) { 1; :foo for(`$a = 0; `$a -lt 2; `$a += 1) { `$a; continue foo; 3; }; 4; continue; 5; } ", - " foreach(`$a in 1) { 1; :foo foreach( `$b in 1,2 ) { `$b; continue foo; 3; }; 4; continue; 5; } " - $result = ExecuteCommand $commands[0] - $result | should be "1", "1", "2", "4" - $result = ExecuteCommand $commands[1] - $result | should be "1", "0", "1", "4" - $result = ExecuteCommand $commands[2] - $result | should be "1", "1", "2", "4" - } - - It "Use continue with a label on a nested loop. (line 1059)" { - $commands = " `$x = 0; :foo while (`$x -lt 2) { `$x; `$x += 1; :bar while(1) { 2; continue foo; 3; }; 4; continue; 5; } ", - " :foo for (`$x = 0;`$x -lt 2;`$x += 1) { 1; :bar for(;;) { 2; continue foo; 3; }; 4; continue; 5; } ", - " :foo foreach(`$a in 1,2) { 1; :bar foreach( `$b in 1,2,3 ) { 2; continue foo; 3; }; 4; continue; 5; } " - $result = ExecuteCommand $commands[0] - $result | should be "0", "2", "1", "2" - $result = ExecuteCommand $commands[1] - $result | should be "1", "2", "1", "2" - $result = ExecuteCommand $commands[2] - $result | should be "1", "2", "1", "2" - } - - It "This test will check that it is a syntax error to use if without a code block. (line 1141)" { - try { - ExecuteCommand 'if ("true")' - throw "Execution OK" - } - catch { - $_.FullyQualifiedErrorId | Should be "IncompleteParseException" - } - } - - It "This test will check that it is a syntax error if the if condition is not complete. (line 1150)" { - try { - ExecuteCommand 'if (' - throw "Execution OK" - } - catch { - $_.FullyQualifiedErrorId | Should be "IncompleteParseException" - } - } - - It "This test will check that it is a syntax error to have an if condition without parentheses. (line 1159)" { - try { - ExecuteCommand 'if "true" { 1} else {2}' - throw "Execution OK" - } - catch { - $_.FullyQualifiedErrorId | Should be "ParseException" - } - } - - It "This test will check that the parser throws a syntax error when the if condition is missing the closing parentheses. (line 1168)" { - try { - ExecuteCommand 'if ("true" { 1};' - throw "Execution OK" - } - catch { - $_.FullyQualifiedErrorId | Should be "ParseException" - } - } - - It "This test will check that it is a syntax error to have an else keyword without the corresponding code block. (line 1177)" { - try { - ExecuteCommand 'if ("true") {1} else' - throw "Execution OK" - } - catch { - $_.FullyQualifiedErrorId | Should be "IncompleteParseException" - } - } - - It "This test will check that the parser throws a syntax error when a foreach loop is not complete. (line 1238)" { - try { - ExecuteCommand '$count=0;$files = $(get-childitem / -filter *.txt );foreach ($i ;$count' - throw "Execution OK" - } - catch { - $_.FullyQualifiedErrorId | Should be "ParseException" - } - } - - It "This test will check that the parser throws a syntax error if the foreach loop is not complete. (line 1248)" { - try { - ExecuteCommand '$count=0;$files = $(get-childitem / -filter *.txt );foreach ($i in ;$count' - throw "Execution OK" - } - catch { - $_.FullyQualifiedErrorId | Should be "ParseException" - } - } - - It "This will test that the parser throws a syntax error if the foreach loop is missing a closing parentheses. (line 1258)" { - try { - ExecuteCommand '$count=0;$files = $(get-childitem / -filter *.txt );foreach ($i in $files ;$count' - throw "Execution OK" - } - catch { - $_.FullyQualifiedErrorId | Should be "ParseException" - } - } - - It "Test that if an exception is thrown from the try block it will be caught in the appropropriate catch block and that the finally block will run regardless of whether an exception is thrown. (line 1317)" { - $result = ExecuteCommand 'try { try { throw (new-object System.ArgumentException) } catch [System.DivideByZeroException] { } finally { "Finally" } } catch { $_.Exception.GetType().FullName }' - $result | should be "Finally", "System.ArgumentException" - } - - It "Test that null can be passed to a method that expects a reference type. (line 1439)" { - $result = ExecuteCommand '$test = "String";$test.CompareTo($())' - $result | should be 1 - } - - It "Tests that command expansion operators can be used as a parameter to an object method. (line 1507)" { - $result = ExecuteCommand '$test = "String";$test.SubString($("hello" | foreach-object { $_.length - 2 } ))' - $result | should be "ing" - } - - It "Test that & can be used as a parameter as long as it is quoted. (line 1606)" { - $result = ExecuteCommand 'testcmd-parserbvt `&get-childitem' - $result | should be "&get-childitem;unset;unset" - $result = ExecuteCommand 'testcmd-parserbvt `&*' - $result | should be "&*;unset;unset" - } - - It "Run a command with parameters. (line 1621)" { - $result = ExecuteCommand 'testcmd-parserBVT -Property1 set' - $result | should be "set;unset;unset" - } - - It "Test that typing a number at the command line will return that number. (line 1630)" { - $result = ExecuteCommand '3' - $result | should be "3" - $result.gettype() |should be ([int]) - } - - It "This test will check that an msh script can be run without invoking. (line 1641)" { - "1">$testfile - $result = ExecuteCommand ". $testfile" - $result | should be 1 - } - - It "Test that an alias is resolved before a function. (line 1657)" { - $result = ExecuteCommand 'set-alias parserInvokeTest testcmd-parserBVT;function parserInvokeTest { 3 };parserInvokeTest' - $result | should be "unset;unset;unset" - } - - It "Test that functions are resolved before cmdlets. (line 1678)"{ - $result_cmdlet = $PowerShell.AddScript('function test-parserfunc { [CmdletBinding()] Param() PROCESS { "cmdlet" } };test-parserfunc').Invoke() - $result_func = ExecuteCommand 'function test-parserfunc { "func" };test-parserfunc' - $PowerShell.Commands.Clear() - $result_cmdlet | should be "cmdlet" - $result_func | should be "func" - } - - It "Check that a command that uses shell execute can be run from the command line and that no exception is thrown. (line 1702)" { - if ( $IsLinux -or $IsMacOS ) { - # because we execute on *nix based on executable bit, and the file name doesn't matter - # so we can use the same filename as for windows, just make sure it's executable with chmod - "#!/bin/sh`necho ""Hello World""" | out-file -encoding ASCII $shellfile - /bin/chmod +x $shellfile - } - else { - "@echo Hello, I'm a Cmd script!">$shellfile - } - { ExecuteCommand "$shellfile" } | Should Not Throw - } - - Context "Boolean Tests (starting at line 1723 to line 1772)" { - $testData = @( - @{ Script = '"False"'; Expected = $true } - @{ Script = 'if ("A") { $true } else { $false }'; Expected = $true } - @{ Script = 'if (" ") { $true } else { $false }'; Expected = $true } - @{ Script = 'if ("String with spaces") { $true } else { $false }'; Expected = $true } - @{ Script = 'if ("DoubleQuoted") { $true } else { $false }'; Expected = $true } - @{ Script = 'if ("StringWithNullVar$aEmbedded") { $true } else { $false }'; Expected = $true } - @{ Script = 'if (0) { $true } else { $false }'; Expected = $false } - @{ Script = '$a = $(0);if ($a) { $true } else { $false }'; Expected = $false } - @{ Script = '$obj = testcmd-parserBVT -ReturnType object;if ($obj) { $true } else { $false }'; Expected = $true } - ) - It "