diff --git a/README.md b/README.md index 1927d0a17..86cf7e430 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,10 @@ You can download and install a PowerShell package for any of the following platf | Platform | Releases | How to Install | |--------------|--------------------|--------------------------------| -| Windows | [.msi][rl-windows] | [How to Install][in-windows] | -| Ubuntu 14.04 | [.deb][rl-ubuntu] | [How to Install][in-ubuntu] | -| CentOS 7 | [.rpm][rl-centos] | [How to Install][in-centos] | -| OS X 10.11 | [.pkg][rl-osx] | [How to Install][in-osx] | +| Windows | [.msi][rl-windows] | [Instructions][in-windows] | +| Ubuntu 14.04 | [.deb][rl-ubuntu] | [Instructions][in-ubuntu] | +| CentOS 7 | [.rpm][rl-centos] | [Instructions][in-centos] | +| OS X 10.11 | [.pkg][rl-osx] | [Instructions][in-osx] | [rl-windows]: https://github.com/PowerShell/PowerShell/releases/download/v6.0.0-alpha.7/PowerShell_6.0.0.7.msi [rl-ubuntu]: https://github.com/PowerShell/PowerShell/releases/download/v6.0.0-alpha.7/powershell_6.0.0-alpha.7-1_amd64.deb diff --git a/docs/building/linux.md b/docs/building/linux.md index d76266823..a95d5a94a 100644 --- a/docs/building/linux.md +++ b/docs/building/linux.md @@ -106,7 +106,7 @@ Start-PSBuild Congratulations! If everything went right, PowerShell is now built. The `Start-PSBuild` script will output the location of the executable: -`./src/powershell/bin/Linux/netcoreapp1.0/ubuntu.14.04-x64/powershell`. +`./src/powershell-unix/bin/Linux/netcoreapp1.0/ubuntu.14.04-x64/powershell`. You should now be running the `powershell` that you just built, if your run the above executable. You can run our cross-platform Pester tests with `Start-PSPester`, and our xUnit tests with `Start-PSxUnit`. @@ -129,7 +129,7 @@ make test popd ``` -This library will be emitted in the `src/powershell` project, +This library will be emitted in the `src/powershell-unix` project, where `dotnet` consumes it as "content" and thus automatically deploys it. Build the managed projects @@ -141,7 +141,7 @@ The `--configuration Linux` flag is necessary to ensure that the preprocessor de ```sh dotnet restore -cd src/powershell +cd src/powershell-unix dotnet build --configuration Linux ``` diff --git a/docs/building/osx.md b/docs/building/osx.md index 50f77dc70..f844ceb03 100644 --- a/docs/building/osx.md +++ b/docs/building/osx.md @@ -76,6 +76,6 @@ download the `pkg` from our GitHub releases page using your browser, complete th start a `powershell` session, and use `Start-PSBuild` from the module. The output directory will be slightly different because your runtime identifier is different. -PowerShell will be at `./src/powershell/bin/Linux/netcoreapp1.0/osx.10.11-x64/powershell`, +PowerShell will be at `./src/powershell-unix/bin/Linux/netcoreapp1.0/osx.10.11-x64/powershell`, or `osx.10.10` depending on your operating system version. Note that configration is still `Linux` because it would be silly to make yet another separate configuration when it's used soley to work-around a CLI issue. diff --git a/docs/building/windows-core.md b/docs/building/windows-core.md index 6ba89241e..7388f24f2 100644 --- a/docs/building/windows-core.md +++ b/docs/building/windows-core.md @@ -72,7 +72,7 @@ Start-PSBuild ``` Congratulations! If everything went right, PowerShell is now built and -executable as `./src/powershell/bin/Debug/netcoreapp1.0/win10-x64/powershell`. +executable as `./src/powershell-win-core/bin/Debug/netcoreapp1.0/win10-x64/powershell`. This location is of the form `./[project]/bin/[configuration]/[framework]/[rid]/[binary name]`, and our diff --git a/docs/building/windows-full.md b/docs/building/windows-full.md index 56b06e9b9..a84b0a5a4 100644 --- a/docs/building/windows-full.md +++ b/docs/building/windows-full.md @@ -48,14 +48,11 @@ Build using our module Use `Start-PSBuild -FullCLR` from the `build.psm1` module. -Because the `ConsoleHost` project (*not* the `Host` project) is a -library and not an application (in the sense that .NET CLI does not -emit a native executable using .NET Core's `corehost`), it targets the -framework `netstandard1.6`, *not* `netcoreapp1.0`, and the build -output will *not* have a runtime identifier in the path. +The output location of `powershell.exe` will be -Thus the output location of `powershell.exe` will be -`./src/Microsoft.PowerShell.ConsoleHost/bin/Debug/net451/powershell.exe` +``` +.\src\powershell-win-full\bin\Debug\net451\win10-x64\publish\powershell.exe +``` Build manually ============== @@ -99,7 +96,7 @@ This command has a reasonable default to run `powershell.exe` from the build out If you are building an unusual configuration (i.e. not `Debug`), you can explicitly specify path to the bin directory ```powershell -Start-DevPowerShell -FullCLR -binDir .\src\Microsoft.PowerShell.ConsoleHost\bin\Debug\net451 +Start-DevPowerShell -FullCLR -binDir .\src\powershell-win-full\bin\Debug\net451\win10-x64\publish ``` Or more programmatically: diff --git a/docs/installation/linux.md b/docs/installation/linux.md index 00aab4e91..f74443c3f 100644 --- a/docs/installation/linux.md +++ b/docs/installation/linux.md @@ -22,7 +22,7 @@ sudo apt-get install libunwind8 libicu52 sudo dpkg -i powershell_6.0.0-alpha.7-1_amd64.deb ``` -> Please note that that Ubuntu 16.04 is not yet supported, but coming soon! +> Please note that Ubuntu 16.04 is not yet supported, but coming soon! [Ubuntu 14.04]: http://releases.ubuntu.com/14.04/ diff --git a/docs/testing-guidelines/Images/AppVeyor-Badge-Green.png b/docs/testing-guidelines/Images/AppVeyor-Badge-Green.png new file mode 100644 index 000000000..777ca2c4b Binary files /dev/null and b/docs/testing-guidelines/Images/AppVeyor-Badge-Green.png differ diff --git a/docs/testing-guidelines/Images/AppVeyor-Github.png b/docs/testing-guidelines/Images/AppVeyor-Github.png new file mode 100644 index 000000000..e467d28ac Binary files /dev/null and b/docs/testing-guidelines/Images/AppVeyor-Github.png differ diff --git a/docs/testing-guidelines/PesterDoAndDont.md b/docs/testing-guidelines/PesterDoAndDont.md old mode 100644 new mode 100755 index 56bcfe857..da4dbae4c --- a/docs/testing-guidelines/PesterDoAndDont.md +++ b/docs/testing-guidelines/PesterDoAndDont.md @@ -1,23 +1,34 @@ ## Do -1. Name your files .tests.ps1 +1. Name your files .tests.ps1 2. Keep tests simple 1. Test only what you need 2. Reduce dependencies -3. Be sure to tag your Describe blocks with "inner" and "outer" -4. Make sure that Describe/Context/It descriptions are useful +3. Be sure to tag your `Describe` blocks based on their purpose + 1. Tag `CI` indicates that it will be run as part of the continuous integration process. These should be unit test like, and generally take less than a second. + 2. Tag `Feature` indicates a higher level feature test (we will run these on a regular basis), for example, tests which go to remote resources, or test broader functionality + 3. Tag `Scenario` indicates tests of integration with other features (these will be run on a less regular basis and test even broader functionality than feature tests. +4. Make sure that `Describe`/`Context`/`It` descriptions are useful 1. The error message should not be the place where you describe the test -5. Use "Context" to group tests - 1. Multiple Contexts can help you group your test suite into logical sections -6. Use BeforeAll/AfterAll/BeforeEach/AfterEach instead of custom initiators -7. Use Try-Catch for expected errors and check $_.fullyQualifiedErrorId -8. Loop It blocks for checking multiple properties +5. Use `Context` to group tests + 1. Multiple `Context` blocks can help you group your test suite into logical sections +6. Use `BeforeAll`/`AfterAll`/`BeforeEach`/`AfterEach` instead of custom initiators +7. Prefer Try-Catch for expected errors and check $_.fullyQualifiedErrorId (don't use `should throw`) +8. Use `-testcases` when iterating over multiple `It` blocks 9. Use code coverage functionality where appropriate -10. Use Mock functionality when you don't have your entire environment -11. Avoid free code in a Describe block - 1. Use `[Before|After][Each|All]` _see Free Code in a Describe block_ +10. Use `Mock` functionality when you don't have your entire environment +11. Avoid free code in a `Describe` block + 1. Use `[Before|After][Each|All]` see [Free Code in a Describe block](WritingPesterTests.md#free-code-in-a-describe-block) +12. Avoid creating or using test files outside of TESTDRIVE: + 1. TESTDRIVE: has automatic clean-up +13. Keep in mind that we are creating cross platform tests + 1. Avoid using the registry + 2. Avoid using COM +14. Avoid being too specific about the _count_ of a resource as these can change platform to platform + 1. ex: checking for the count of loaded format files, check rather for format data for a specific type ## Don't -1. Have too many evaluations in a single It block - 1. The first "Should" failure will stop that block -2. Don't use "Should" anywhere but within an "It" Block - +1. Don't have too many evaluations in a single It block + 1. The first `Should` failure will stop that block +2. Don't use `Should` outside of an `It` Block +3. Don't use the word "Error" or "Fail" to test a positive case + 1. ex: "Get-Childitem TESTDRIVE: shouldn't fail", rather "Get-ChildItem should be able to retrieve file listing from TESTDRIVE" diff --git a/docs/testing-guidelines/Testing.md b/docs/testing-guidelines/Testing.md deleted file mode 100644 index d322cee69..000000000 --- a/docs/testing-guidelines/Testing.md +++ /dev/null @@ -1,106 +0,0 @@ -# DRAFT - -_I have more questions than answers_ - - -#### Current Test Infrastructure -We currently rely heavily on STEX environment for our testing, and we will continue to do so through the Server2016 release. We -need to use that current infrastructure to continue to test full PowerShell builds; it should be possible to build automation -which takes a full PowerShell build and lay it on an existing lab system, update the appropriate test files and execute -a test pass in the same way that we do with official builds. - -The test artifacts which are applicable to full PowerShell are not universally applicable to Core/Nano/OtherPlatform, we will need -to create tooling which allows us to apply a set of test artifacts to a configuration, and then execute tests. Eventually, we need -to have our CI environment test all the flavors of PowerShell we create. -**Question**: Can AppVeyor/Travis service that need? - - -#### Organization -**Proposal**: Create 3 tiers of testing: - -* Checkin - * These are run as part of the CI process, and should run quickly. How quickly is an open question. We need to determine - the right amount of coverage without spending too much time. It may be that we can improve our coverage here through parallelization - but we have not investigated enough to determine whether it's possible. -* Feature - * the tests which look at corner cases, and stand-alone modules (for example, the archive module tests could fall into this - category) -* Scenario - * these are tests which span features, and determine whether the whole product is working correctly. The current P3 tests fall - largely here - -**Actions**: Decide what goes where. My initial thoughts are to migrate our current TTEST unittests into tier 1 (Checkin) - -**Current Migration Activity** - -We have teams working on migrating tests which are in non-portable frameworks (TTest, Lite1, Lite3, etc) to portable frameworks. -The first effort is to migrate our TTEST cmdlet unit tests to Pester, we should be taking those migrated tests and get them into -SD - -##### Layout -We need to have a reasonable layout of our tests, not sure what that looks like yet. We need to make it -easy to find both feature and test code to reduce our maintainance burden. - -##### Self Hosting -Self-Hosting remains problematic while are still so early in the development phase, but it is _imperative_ -that we dog food as early as possible. This is especially true on the non-Windows platforms where we have made -assumptions about the working environment with regard to a number of issues: -* removal of well known aliases -* case sensitivity of some operations -* coverage -We should be using these non-windows platforms as much as possible to - -======= -# DRAFT - -_I have more questions than answers_ - - -#### Current Test Infrastructure -We currently rely heavily on STEX environment for our testing, and we will continue to do so through the Server2016 release. We -need to use that current infrastructure to continue to test full PowerShell builds; it should be possible to build automation -which takes a full PowerShell build and lay it on an existing lab system, update the appropriate test files and execute -a test pass in the same way that we do with official builds. - -The test artifacts which are applicable to full PowerShell are not universally applicable to Core/Nano/OtherPlatform, we will need -to create tooling which allows us to apply a set of test artifacts to a configuration, and then execute tests. Eventually, we need -to have our CI environment test all the flavors of PowerShell we create. -**Question**: Can AppVeyor/Travis service that need? - - -#### Organization -**Proposal**: Create 3 tiers of testing: - -* Checkin - * These are run as part of the CI process, and should run quickly. How quickly is an open question. We need to determine - the right amount of coverage without spending too much time. It may be that we can improve our coverage here through parallelization - but we have not investigated enough to determine whether it's possible. -* Feature - * the tests which look at corner cases, and stand-alone modules (for example, the archive module tests could fall into this - category) -* Scenario - * these are tests which span features, and determine whether the whole product is working correctly. The current P3 tests fall - largely here - -**Actions**: Decide what goes where. My initial thoughts are to migrate our current TTEST unittests into tier 1 (Checkin) - -**Current Migration Activity** - -We have teams working on migrating tests which are in non-portable frameworks (TTest, Lite1, Lite3, etc) to portable frameworks. -The first effort is to migrate our TTEST cmdlet unit tests to Pester, we should be taking those migrated tests and get them into -SD - -##### Layout -We need to have a reasonable layout of our tests, not sure what that looks like yet. We need to make it -easy to find both feature and test code to reduce our maintainance burden. - -##### Self Hosting -Self-Hosting remains problematic while are still so early in the development phase, but it is _imperative_ -that we dog food as early as possible. This is especially true on the non-Windows platforms where we have made -assumptions about the working environment with regard to a number of issues: -* removal of well known aliases -* case sensitivity of some operations -* coverage -We should be using these non-windows platforms as much as possible to - ->>>>>>> Create Testing.md diff --git a/docs/testing-guidelines/WritingPesterTests.md b/docs/testing-guidelines/WritingPesterTests.md old mode 100644 new mode 100755 index 88c532e78..a60c3a36e --- a/docs/testing-guidelines/WritingPesterTests.md +++ b/docs/testing-guidelines/WritingPesterTests.md @@ -1,9 +1,13 @@ -Because we are planning to continue to extend Pester to support remote/parallel execution, it's important to keep in mind the following when create tests: +### Writing Pester Tests +Note that this document does not replace the documents found in the [Pester](https://github.com/pester/pester "Pester") project. This is just +some quick tips and suggestions for creating Pester tests for this project. The Pester community is vibrant and active, if you have questions +about Pester or creating tests, the [Pester Wiki](https://github.com/pester/pester/wiki) has a lot of great information. + +When creating tests, keep the following in mind: * Tests should not be overly complicated and test too many things * boil down your tests to their essence, test only what you need * Tests should be as simple as they can * Tests should generally not rely on any other test - Examples: Here's the simplest of tests @@ -27,14 +31,28 @@ Describe "One is really one" { } It "1 is really an int" { $i = 1 - $i.GetType().FullName | Should Be System.Int32 + $i.GetType() | Should Be "int" } } ``` -If you are checking for proper errors, do that in a try catch, and then check fully qualified error id +alternatively, you could do the following: +``` +Describe "One is really one" { + It "Compare 1 to 1" { + $a = 1 + $a | Should be 1 + } + It "1 is really an int" { + $i = 1 + $i.GetType() | Should Be ([System.Int32]) + } +} +``` + +If you are checking for proper errors, do that in a `try/catch`, and then check `FullyQualifiedErrorId`. Checking against `FullyQualifiedErrorId` is recommended because it does not change based on culture as an error message might. ``` ... -it "Error should be PathNotFound" { +it "Get-Item on a nonexisting file should have error PathNotFound" { try { get-item "ThisFileCannotPossiblyExist" -ErrorAction Stop @@ -42,8 +60,7 @@ it "Error should be PathNotFound" { } catch { - $_.FullyQualifiedErrorId | - should be "PathNotFound,Microsoft.PowerShell.Commands.GetItemCommand" + $_.FullyQualifiedErrorId | should be "PathNotFound,Microsoft.PowerShell.Commands.GetItemCommand" } } ``` @@ -51,12 +68,10 @@ it "Error should be PathNotFound" { Note that if get-item were to succeed, a different FullyQualifiedErrorId would be thrown and the test will fail because the FQErrorId is wrong. This is the suggested path because Pester wants to check the error message, which will likely not work here because of localized builds, but the FullyQualifiedErrorId is constant regardless of the locale. ### Describe/Context/It - -From an organizational standpoint, a Describe block roughly compares to a LITE3 Test Suite and an It block roughly compares to a LITE3 testcase. Unlike LITE3, individual tests are not prioritized (remember that a test name in Pester is really a descriptive sentence, rather than a name), but a Describe block (TestSuite) may be tagged with a string. -For creation of PowerShell tests, the Describe block is the level of granularity suggested and one of two tags should be used: "Inner" or "Outer". If the tag is not provided, tests in that describe block will be run any time tests are executed. +For creation of PowerShell tests, the Describe block is the level of granularity suggested and one of three tags should be used: "CI", "Feature", or "Scenario". If the tag is not provided, tests in that describe block will be run any time tests are executed. #### Describe -Creates a logical group of tests. All Mocks and TestDrive contents defined within a Describe block are scoped to that Describe ; they will no longer be present when the Describe block exits. A Describe block may contain any number of Context and It blocks. +Creates a logical group of tests. All Mocks and TestDrive contents defined within a Describe block are scoped to that Describe; they will no longer be present when the Describe block exits. A `Describe block may contain any number of Context and It blocks. #### Context Provides logical grouping of It blocks within a single Describe block. Any Mocks defined inside a Context are removed at the end of the Context scope, as are any files or folders added to the TestDrive during the Context block's execution. Any BeforeEach or AfterEach blocks defined inside a Context also only apply to tests within that Context . @@ -92,16 +107,7 @@ Describe "Add-Footer" { When this test completes, the contents of the TestDrive PSDrive will be removed. #### Parameter Generation -This is quite a bit different from LITE3, but still possible, see the following: - ``` -Describe "A test" { -function Test-Xor { - param ($a, $b, $ExpectedResult) - It ("Applies XOR to inputs {0} and {1}" -f $a, $b) { - $a -xor $b | Should be $ExpectedResult - } -} $testCases = @( @{ a = 0; b = 1; ExpectedResult = 1 } @@ -109,13 +115,16 @@ $testCases = @( @{ a = 1; b = 1; ExpectedResult = 0 } @{ a = 0; b = 0; ExpectedResult = 0 } ) - foreach ($testCase in $testCases) { - Test-Xor @testCase - } + +Describe "A test" { + It " -xor should be " -testcase $testcases { + param ($a, $b, $ExpectedResult) + $a -xor $b | Should be $ExpectedResult + } } ``` -You can construct the values to pass as parameters, including the expected value, and use that iteratively in a loop. Note the location of the `It` block +You can also construct loops and pass values as parameters, including the expected value, but Pester does this for you. #### Mocking Mocks the behavior of an existing command with an alternate implementation. This creates new behavior for any existing command within the scope of a Describe or Context block. The function allows you to specify a script block that will become the command's new behavior. @@ -128,7 +137,7 @@ Context "Get-Random is not random" { } } ``` -More information may be found here: https://github.com/pester/Pester/wiki/Mock +More information may be found on the [wiki](https://github.com/pester/Pester/wiki/Mock) ### Free Code in a Describe block Code execution in Pester can be very subtle and can cause issues when executing test code. The execution of code which lays outside of the usual code blocks may not happen as you expect. Consider the following: ``` @@ -138,14 +147,14 @@ Describe it { Write-Host -for DarkRed "Before BeforeAll" BeforeAll { write-host -for Blue "In Context BeforeAll" } Write-Host -for DarkRed "After BeforeAll" -  + Write-Host -for DarkRed "Before AfterAll" AfterAll { Write-Host -for Blue "In Context AfterAll" } Write-Host -for DarkRed "After AfterAll" -  + BeforeEach { Write-Host -for Blue "In BeforeEach" } AfterEach { Write-Host -for Blue "In AfterEach" } -  + Write-Host -for DarkRed "Before It" It "should not be a surprise" { 1 | should be 1 @@ -189,3 +198,4 @@ The DESCRIBE BeforeAll block is executed before any other code even though it wa Generally, you should have code reside in one of the code block elements of `[Before|After][All|Each]`, especially if those block rely on state set by free code elsewhere in the block. + diff --git a/docs/testing-guidelines/testing-guidelines.md b/docs/testing-guidelines/testing-guidelines.md old mode 100644 new mode 100755 index 85a9b17e6..0d431f921 --- a/docs/testing-guidelines/testing-guidelines.md +++ b/docs/testing-guidelines/testing-guidelines.md @@ -1,7 +1,135 @@ -author: Jim + +# Testing Guidelines + +Testing is a critical part of the PowerShell project. + +The PowerShell team created nearly 100,000 tests over the last 12 years which we run as part of the release process for PowerShell in Windows. +Having all of those tests available for the initial release of OPS was not feasible, and we have targeted those tests which +we believe will provide us the ability to catch regressions in the areas which have had the largest changes for OPS. It is our +intent to continue to release more and more of our tests until we have the coverage we need. + +For creating new tests, please review the +[documents](https://github.com/PowerShell/PowerShell/tree/master/docs/testing-guidelines) on how to +create tests for OpenPowerShell + +## CI System + +We use [AppVeyor](http://www.appveyor.com/) as a continuous integration (CI) system for Windows +and [Travis CI](http://www.travis-ci.com) for non-Windows platforms. + +### AppVeyor + +In the `README.md` at the top of the repo, you can see AppVeyor badge. +It indicates the last build status of `master` branch. +Hopefully, it's green: + +![AppVeyor-Badge-Green.png](Images/AppVeyor-Badge-Green.png) + +This badge is **clickable**; you can open corresponding build page with logs, artifacts and tests results. +From there you can easily navigate to the build history. + +### Travis CI + +Travis CI works similarly to AppVeyor. For Travis CI there will be multiple badges: +The badges indicate the last build status of `master` branch for different platforms. +Hopefully, each badge is green: + +![Travis-CI-Badge-Green.png](Images/Travis-CI-Badge-Green.png) + +This badge is **clickable**; you can open corresponding build page with logs, artifacts and tests results. +From there you can easily navigate to the build history. + +### Getting CI Results + +CI System builds (Appveyor and Travis CI) and runs tests on every pull request and provides quick feedback about it. + +![AppVeyor-Github](Images/AppVeyor-Github.png) + +These green check boxes and red crosses are **clickable** as well. +They will bring you to the corresponding page with details. + +## Test Frameworks +### Pester +Our script based test framework is [Pester](https://github.com/Pester/Pester). This is the framework which we are using internally +at Microsoft for new script based tests, and a large number of the tests which are part of the OPS project have been migrated from that test base. Pester tests can be used to test most of PowerShell behavior (even some API operations can +easily be tested in Pester) + +Substantial changes were required, to get Pester executing on Non-Windows systems. These changes are not yet in the +official Pester code base. Some features of Pester may not be available or may have incorrect behavior. Please make sure +to create issues in [PowerShell/PowerShell](https://github.com/PowerShell/PowerShell/issues) (not Pester) for anything that you find. + +### xUnit +For those tests which are not easily run via Pester, we have decided to use [xUnit](https://xunit.github.io/) as the test framework. +Currently, we have a minuscule number of tests which are run via xUnit, + +## Running Tests outside of CI +When working on new features or fixes, it is natural to want to run those tests locally before +making a PR. Two helper functions are part of the build.psm1 module to help with that: +* `Start-PSPester` will execute all Pester tests which are run by the CI system +* `Start-PSxUnit` will execute the available xUnit tests run by the CI system +Our CI system runs these as well; there should be no difference between running these on your dev system, versus in CI. + +When running tests in this way, be sure that you have started PowerShell with `-noprofile` as some tests will fail if the +environment is not the default or has any customization. + +for example, to run all the Pester tests for CI (assuming you are at the root the PowerShell repo): +``` +import-module ./build.psm1 +Start-PSPester +``` +if you wish to run specific tests, that is possible as well: +``` +Start-PSPester -Directory test/powershell/engine/Api +``` +or a specific Pester test file: +``` +Start-PSPester -Directory test/powershell/engine/Api -Test XmlAdapter.Tests.Api +``` + +### What happens after your PR? +When your PR has successfully passed the CI test gates, your changes will be used to create PowerShell binaries which can be run +in Microsoft's internal test frameworks. The tests that you created for your change and the library of historical tests will be +run to determine if any regressions are present. If these tests find regressions, you'll be notified that your PR is not ready, and provide +you enough information for you to investigate why the failure happened. -> Jason/Jim: Testing requirements (code coverage, required for incoming features/enhancements) - > local build/test execution instructions - > tools needed for build/test execution - > our CI explained + +## Test Layout +We have taken a functional approach to the layout of our Pester tests. You should place new tests in their appropriate +location. If you are making a fix to a cmdlet in a module, the test belongs in the module directory. +If you are unsure; you can make it part of your PR, or create an issue. The current layout of tests is: +* test/powershell/engine +* test/powershell/engine/Api +* test/powershell/engine/Basic +* test/powershell/engine/ETS +* test/powershell/engine/Help +* test/powershell/engine/Logging +* test/powershell/engine/Module +* test/powershell/engine/ParameterBinding +* test/powershell/engine/Runspace +* test/powershell/engine/Logging/MessageAnalyzer +* test/powershell/Host +* test/powershell/Host/ConsoleHost +* test/powershell/Host/TabCompletion +* test/powershell/Language +* test/powershell/Modules +* test/powershell/Provider +* test/powershell/Scripting +* test/powershell/Scripting/Debugging +* test/powershell/Scripting/NativeExecution +* test/powershell/SDK +* test/powershell/Security +* test/powershell/Language/Classes +* test/powershell/Language/Interop +* test/powershell/Language/Operators +* test/powershell/Language/Parser +* test/powershell/Language/Interop/DotNet +* test/powershell/Modules/Microsoft.PowerShell.Archive +* test/powershell/Modules/Microsoft.PowerShell.Core +* test/powershell/Modules/Microsoft.PowerShell.Diagnostics +* test/powershell/Modules/Microsoft.PowerShell.Management +* test/powershell/Modules/Microsoft.PowerShell.Security +* test/powershell/Modules/Microsoft.PowerShell.Utility +* test/powershell/Modules/Microsoft.PowerShell.Security +* test/powershell/Modules/PSReadLine + diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/project.json b/src/Microsoft.PackageManagement.ArchiverProviders/project.json index 5111eb3de..fa6fe42ae 100644 --- a/src/Microsoft.PackageManagement.ArchiverProviders/project.json +++ b/src/Microsoft.PackageManagement.ArchiverProviders/project.json @@ -1,7 +1,6 @@ { "name": "Microsoft.PackageManagement.ArchiverProviders", "version": "1.0.0-*", - "authors": ["quoct"], "buildOptions": { "warningsAsErrors": true diff --git a/src/Microsoft.PackageManagement.CoreProviders/project.json b/src/Microsoft.PackageManagement.CoreProviders/project.json index d456919d2..db190187a 100644 --- a/src/Microsoft.PackageManagement.CoreProviders/project.json +++ b/src/Microsoft.PackageManagement.CoreProviders/project.json @@ -1,7 +1,6 @@ { "name": "Microsoft.PackageManagement.CoreProviders", "version": "1.0.0-*", - "authors": ["quoct"], "buildOptions": { "warningsAsErrors": true diff --git a/src/Microsoft.PackageManagement.MetaProvider.PowerShell/project.json b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/project.json index 59bff5d34..854a72ffc 100644 --- a/src/Microsoft.PackageManagement.MetaProvider.PowerShell/project.json +++ b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/project.json @@ -1,7 +1,6 @@ { "name": "Microsoft.PackageManagement.MetaProvider.PowerShell", "version": "1.0.0-*", - "authors": ["quoct"], "buildOptions": { "warningsAsErrors": true diff --git a/src/Microsoft.PackageManagement.MsiProvider/project.json b/src/Microsoft.PackageManagement.MsiProvider/project.json index 19a20a4ba..fd5319bb0 100644 --- a/src/Microsoft.PackageManagement.MsiProvider/project.json +++ b/src/Microsoft.PackageManagement.MsiProvider/project.json @@ -1,7 +1,6 @@ { "name": "Microsoft.PackageManagement.MsiProvider", "version": "1.0.0-*", - "authors": ["quoct"], "buildOptions": { "warningsAsErrors": true diff --git a/src/Microsoft.PackageManagement.MsuProvider/project.json b/src/Microsoft.PackageManagement.MsuProvider/project.json index 6a28ff49f..a87fb5303 100644 --- a/src/Microsoft.PackageManagement.MsuProvider/project.json +++ b/src/Microsoft.PackageManagement.MsuProvider/project.json @@ -1,7 +1,6 @@ { "name": "Microsoft.PackageManagement.MsuProvider", "version": "1.0.0-*", - "authors": ["quoct"], "buildOptions": { "warningsAsErrors": true diff --git a/src/Microsoft.PackageManagement.NuGetProvider/project.json b/src/Microsoft.PackageManagement.NuGetProvider/project.json index 691c28366..38ed12469 100644 --- a/src/Microsoft.PackageManagement.NuGetProvider/project.json +++ b/src/Microsoft.PackageManagement.NuGetProvider/project.json @@ -1,7 +1,6 @@ { "name": "Microsoft.PackageManagement.NuGetProvider", "version": "1.0.0-*", - "authors": ["quoct"], "buildOptions": { "warningsAsErrors": true diff --git a/src/Microsoft.PackageManagement.PackageSourceListProvider/project.json b/src/Microsoft.PackageManagement.PackageSourceListProvider/project.json index 1e9f81714..405f709ff 100644 --- a/src/Microsoft.PackageManagement.PackageSourceListProvider/project.json +++ b/src/Microsoft.PackageManagement.PackageSourceListProvider/project.json @@ -1,7 +1,6 @@ { "name": "Microsoft.PackageManagement.PackageSourceListProvider", "version": "1.0.0-*", - "authors": [ "quoct" ], "buildOptions": { "warningsAsErrors": true diff --git a/src/Microsoft.PackageManagement/project.json b/src/Microsoft.PackageManagement/project.json index cf7c2e1d4..56b0f2e4e 100644 --- a/src/Microsoft.PackageManagement/project.json +++ b/src/Microsoft.PackageManagement/project.json @@ -1,7 +1,6 @@ { "name": "Microsoft.PackageManagement", "version": "1.0.0-*", - "authors": [ "quoct" ], "buildOptions": { "warningsAsErrors": true diff --git a/src/Microsoft.PowerShell.PackageManagement/project.json b/src/Microsoft.PowerShell.PackageManagement/project.json index 818dd6d1e..fde8c2f7a 100644 --- a/src/Microsoft.PowerShell.PackageManagement/project.json +++ b/src/Microsoft.PowerShell.PackageManagement/project.json @@ -1,7 +1,6 @@ { "name": "Microsoft.PowerShell.PackageManagement", "version": "1.0.0-*", - "authors": ["quoct"], "buildOptions": { "warningsAsErrors": true diff --git a/src/Microsoft.PowerShell.Workflow.ServiceCore/project.json b/src/Microsoft.PowerShell.Workflow.ServiceCore/project.json index e2fb9c9d6..cd1817990 100644 --- a/src/Microsoft.PowerShell.Workflow.ServiceCore/project.json +++ b/src/Microsoft.PowerShell.Workflow.ServiceCore/project.json @@ -1,7 +1,6 @@ { "name": "Microsoft.PowerShell.Workflow.ServiceCore", "version": "1.0.0-*", - "authors": [ "OPS" ], "buildOptions": { "nowarn": [ "CS1570" ], diff --git a/src/Modules/Shared/Microsoft.PowerShell.Archive/Microsoft.PowerShell.Archive.psm1 b/src/Modules/Shared/Microsoft.PowerShell.Archive/Microsoft.PowerShell.Archive.psm1 index e179118c2..56daf78d8 100644 --- a/src/Modules/Shared/Microsoft.PowerShell.Archive/Microsoft.PowerShell.Archive.psm1 +++ b/src/Modules/Shared/Microsoft.PowerShell.Archive/Microsoft.PowerShell.Archive.psm1 @@ -662,13 +662,13 @@ function CompressSingleDirHelper } else { - $modifiedSourceDirFullName = $sourceDirFullName + "\" + $modifiedSourceDirFullName = $sourceDirFullName + ([io.path]::DirectorySeparatorChar) } } else { $sourceDirFullName = $sourceDirPath - $modifiedSourceDirFullName = $sourceDirFullName + "\" + $modifiedSourceDirFullName = $sourceDirFullName + ([io.path]::DirectorySeparatorChar) } $dirContents = Get-ChildItem -LiteralPath $sourceDirPath -Recurse @@ -688,7 +688,7 @@ function CompressSingleDirHelper $files = $currentContent.GetFiles() if($files.Count -eq 0) { - $subDirFiles.Add($currentContent.FullName + "\") + $subDirFiles.Add($currentContent.FullName + ([io.path]::DirectorySeparatorChar)) } } } @@ -780,7 +780,7 @@ function ZipArchiveHelper # If a directory needs to be added to an archive file, # by convention the .Net API's expect the path of the diretcory # to end with '\' to detect the path as an directory. - if(!$relativeFilePath.EndsWith("\", [StringComparison]::OrdinalIgnoreCase)) + if(!$relativeFilePath.EndsWith(([io.path]::DirectorySeparatorChar), [StringComparison]::OrdinalIgnoreCase)) { try { @@ -951,7 +951,7 @@ function ExpandArchiveHelper # The current archive entry is an empty directory # The FullName of the Archive Entry representing a directory would end with a trailing '\'. if($extension -eq [string]::Empty -and - $currentArchiveEntryPath.EndsWith("\", [StringComparison]::OrdinalIgnoreCase)) + $currentArchiveEntryPath.EndsWith(([io.path]::DirectorySeparatorChar), [StringComparison]::OrdinalIgnoreCase)) { $pathExists = Test-Path -LiteralPath $currentArchiveEntryPath diff --git a/src/System.Management.Automation/engine/remoting/fanin/WSManNativeAPI.cs b/src/System.Management.Automation/engine/remoting/fanin/WSManNativeAPI.cs index 6869f44ae..aa12e3d41 100644 --- a/src/System.Management.Automation/engine/remoting/fanin/WSManNativeAPI.cs +++ b/src/System.Management.Automation/engine/remoting/fanin/WSManNativeAPI.cs @@ -2299,9 +2299,11 @@ namespace System.Management.Automation.Remoting.Client #region DllImports ClientAPI #if !UNIX - internal const string WSManApiDll = @"WsmSvc.dll"; + internal const string WSManClientApiDll = @"WsmSvc.dll"; + internal const string WSManProviderApiDll = @"WsmSvc.dll"; #else - internal const string WSManApiDll = @"libpsrpomiprov"; + internal const string WSManClientApiDll = @"libpsrpclient"; + internal const string WSManProviderApiDll = @"libpsrpomiprov"; #endif /// @@ -2315,7 +2317,7 @@ namespace System.Management.Automation.Remoting.Client /// /// /// - [DllImport(WSManNativeApi.WSManApiDll, SetLastError = false, CharSet = CharSet.Unicode)] + [DllImport(WSManNativeApi.WSManClientApiDll, SetLastError = false, CharSet = CharSet.Unicode)] internal static extern int WSManInitialize(int flags, [In, Out] ref IntPtr wsManAPIHandle); @@ -2329,7 +2331,7 @@ namespace System.Management.Automation.Remoting.Client /// /// /// - [DllImport(WSManNativeApi.WSManApiDll, SetLastError = false, CharSet = CharSet.Unicode)] + [DllImport(WSManNativeApi.WSManClientApiDll, SetLastError = false, CharSet = CharSet.Unicode)] internal static extern int WSManDeinitialize(IntPtr wsManAPIHandle, int flags); /// @@ -2348,7 +2350,7 @@ namespace System.Management.Automation.Remoting.Client /// /// /// - [DllImport(WSManNativeApi.WSManApiDll, SetLastError = false, CharSet = CharSet.Unicode)] + [DllImport(WSManNativeApi.WSManClientApiDll, SetLastError = false, CharSet = CharSet.Unicode)] internal static extern int WSManCreateSession(IntPtr wsManAPIHandle, [MarshalAs(UnmanagedType.LPWStr)]string connection, int flags, @@ -2364,7 +2366,7 @@ namespace System.Management.Automation.Remoting.Client /// /// /// - [DllImport(WSManNativeApi.WSManApiDll, SetLastError = false, CharSet = CharSet.Unicode)] + [DllImport(WSManNativeApi.WSManClientApiDll, SetLastError = false, CharSet = CharSet.Unicode)] internal static extern void WSManCloseSession(IntPtr wsManSessionHandle, int flags); @@ -2389,7 +2391,7 @@ namespace System.Management.Automation.Remoting.Client } } - [DllImport(WSManNativeApi.WSManApiDll, SetLastError = false, CharSet = CharSet.Unicode)] + [DllImport(WSManNativeApi.WSManClientApiDll, SetLastError = false, CharSet = CharSet.Unicode)] internal static extern int WSManSetSessionOption(IntPtr wsManSessionHandle, WSManSessionOption option, IntPtr data); @@ -2404,7 +2406,7 @@ namespace System.Management.Automation.Remoting.Client /// An int (DWORD) data. /// /// - [DllImport(WSManNativeApi.WSManApiDll, SetLastError = false, CharSet = CharSet.Unicode)] + [DllImport(WSManNativeApi.WSManClientApiDll, SetLastError = false, CharSet = CharSet.Unicode)] internal static extern void WSManGetSessionOptionAsDword(IntPtr wsManSessionHandle, WSManSessionOption option, out int value); @@ -2460,7 +2462,7 @@ namespace System.Management.Automation.Remoting.Client return returnval; } - [DllImport(WSManNativeApi.WSManApiDll, SetLastError = false, CharSet = CharSet.Unicode)] + [DllImport(WSManNativeApi.WSManClientApiDll, SetLastError = false, CharSet = CharSet.Unicode)] private static extern int WSManGetSessionOptionAsString(IntPtr wsManSessionHandle, WSManSessionOption option, int optionLength, @@ -2510,7 +2512,7 @@ namespace System.Management.Automation.Remoting.Client openContent, asyncCallback, ref shellOperationHandle); } - [DllImport(WSManNativeApi.WSManApiDll, EntryPoint = "WSManCreateShellEx", SetLastError = false, CharSet = CharSet.Unicode)] + [DllImport(WSManNativeApi.WSManClientApiDll, EntryPoint = "WSManCreateShellEx", SetLastError = false, CharSet = CharSet.Unicode)] private static extern void WSManCreateShellExInternal(IntPtr wsManSessionHandle, int flags, [MarshalAs(UnmanagedType.LPWStr)]string resourceUri, @@ -2532,7 +2534,7 @@ namespace System.Management.Automation.Remoting.Client /// /// /// - [DllImport(WSManNativeApi.WSManApiDll, EntryPoint = "WSManConnectShell", SetLastError = false, CharSet = CharSet.Unicode)] + [DllImport(WSManNativeApi.WSManClientApiDll, EntryPoint = "WSManConnectShell", SetLastError = false, CharSet = CharSet.Unicode)] internal static extern void WSManConnectShellEx(IntPtr wsManSessionHandle, int flags, [MarshalAs(UnmanagedType.LPWStr)]string resourceUri, @@ -2550,7 +2552,7 @@ namespace System.Management.Automation.Remoting.Client /// /// /// - [DllImport(WSManNativeApi.WSManApiDll, EntryPoint = "WSManDisconnectShell", SetLastError = false, CharSet = CharSet.Unicode)] + [DllImport(WSManNativeApi.WSManClientApiDll, EntryPoint = "WSManDisconnectShell", SetLastError = false, CharSet = CharSet.Unicode)] internal static extern void WSManDisconnectShellEx(IntPtr wsManSessionHandle, int flags, IntPtr disconnectInfo, @@ -2562,13 +2564,13 @@ namespace System.Management.Automation.Remoting.Client /// /// /// - [DllImport(WSManNativeApi.WSManApiDll, EntryPoint = "WSManReconnectShell", SetLastError = false, CharSet = CharSet.Unicode)] + [DllImport(WSManNativeApi.WSManClientApiDll, EntryPoint = "WSManReconnectShell", SetLastError = false, CharSet = CharSet.Unicode)] internal static extern void WSManReconnectShellEx(IntPtr wsManSessionHandle, int flags, IntPtr asyncCallback); - [DllImport(WSManNativeApi.WSManApiDll, EntryPoint = "WSManReconnectShellCommand", SetLastError = false, CharSet = CharSet.Unicode)] + [DllImport(WSManNativeApi.WSManClientApiDll, EntryPoint = "WSManReconnectShellCommand", SetLastError = false, CharSet = CharSet.Unicode)] internal static extern void WSManReconnectShellCommandEx(IntPtr wsManCommandHandle, int flags, IntPtr asyncCallback); @@ -2597,7 +2599,7 @@ namespace System.Management.Automation.Remoting.Client /// An out parameter referening a WSMan shell operation handle /// for this command. /// - [DllImport(WSManNativeApi.WSManApiDll, EntryPoint = "WSManRunShellCommandEx", SetLastError = false, CharSet = CharSet.Unicode)] + [DllImport(WSManNativeApi.WSManClientApiDll, EntryPoint = "WSManRunShellCommandEx", SetLastError = false, CharSet = CharSet.Unicode)] internal static extern void WSManRunShellCommandEx(IntPtr shellOperationHandle, int flags, [MarshalAs(UnmanagedType.LPWStr)] @@ -2610,7 +2612,7 @@ namespace System.Management.Automation.Remoting.Client ref IntPtr commandOperationHandle); - [DllImport(WSManNativeApi.WSManApiDll, EntryPoint = "WSManConnectShellCommand", SetLastError = false, CharSet = CharSet.Unicode)] + [DllImport(WSManNativeApi.WSManClientApiDll, EntryPoint = "WSManConnectShellCommand", SetLastError = false, CharSet = CharSet.Unicode)] internal static extern void WSManConnectShellCommandEx(IntPtr shellOperationHandle, int flags, [MarshalAs(UnmanagedType.LPWStr)] @@ -2646,7 +2648,7 @@ namespace System.Management.Automation.Remoting.Client /// /// handle to use to cancel the operation. /// - [DllImport(WSManNativeApi.WSManApiDll, EntryPoint = "WSManReceiveShellOutput", SetLastError = false, CharSet = CharSet.Unicode)] + [DllImport(WSManNativeApi.WSManClientApiDll, EntryPoint = "WSManReceiveShellOutput", SetLastError = false, CharSet = CharSet.Unicode)] internal static extern void WSManReceiveShellOutputEx(IntPtr shellOperationHandle, IntPtr commandOperationHandle, int flags, @@ -2685,7 +2687,7 @@ namespace System.Management.Automation.Remoting.Client streamData, false, asyncCallback, ref sendOperationHandle); } - [DllImport(WSManNativeApi.WSManApiDll, EntryPoint = "WSManSendShellInput", SetLastError = false, CharSet = CharSet.Unicode)] + [DllImport(WSManNativeApi.WSManClientApiDll, EntryPoint = "WSManSendShellInput", SetLastError = false, CharSet = CharSet.Unicode)] private static extern void WSManSendShellInputExInternal(IntPtr shellOperationHandle, IntPtr commandOperationHandle, int flags, @@ -2710,7 +2712,7 @@ namespace System.Management.Automation.Remoting.Client /// /// callback to notify when the operation completes. /// - [DllImport(WSManNativeApi.WSManApiDll, SetLastError = false, CharSet = CharSet.Unicode)] + [DllImport(WSManNativeApi.WSManClientApiDll, SetLastError = false, CharSet = CharSet.Unicode)] internal static extern void WSManCloseShell(IntPtr shellHandle, int flags, IntPtr asyncCallback); @@ -2726,7 +2728,7 @@ namespace System.Management.Automation.Remoting.Client /// /// callback to notify when the operation completes. /// - [DllImport(WSManNativeApi.WSManApiDll, SetLastError = false, CharSet = CharSet.Unicode)] + [DllImport(WSManNativeApi.WSManClientApiDll, SetLastError = false, CharSet = CharSet.Unicode)] internal static extern void WSManCloseCommand(IntPtr cmdHandle, int flags, IntPtr asyncCallback); @@ -2742,7 +2744,7 @@ namespace System.Management.Automation.Remoting.Client /// /// /// - [DllImport(WSManNativeApi.WSManApiDll, EntryPoint = "WSManSignalShell", SetLastError = false, CharSet = CharSet.Unicode)] + [DllImport(WSManNativeApi.WSManClientApiDll, EntryPoint = "WSManSignalShell", SetLastError = false, CharSet = CharSet.Unicode)] internal static extern void WSManSignalShellEx(IntPtr shellOperationHandle, IntPtr cmdOperationHandle, int flags, @@ -2761,7 +2763,7 @@ namespace System.Management.Automation.Remoting.Client /// /// /// - [DllImport(WSManNativeApi.WSManApiDll, SetLastError = false, CharSet = CharSet.Unicode)] + [DllImport(WSManNativeApi.WSManClientApiDll, SetLastError = false, CharSet = CharSet.Unicode)] internal static extern void WSManCloseOperation(IntPtr operationHandle, int flags); /// @@ -2858,7 +2860,7 @@ namespace System.Management.Automation.Remoting.Client /// It cannot be NULL. If both "messageLength" and "message" parameters are 0, the function will return ERROR_INSUFFICIENT_BUFFER /// and "messageLengthUsed" parameter will be set to the number of characters needed, including NULL terminator /// - [DllImport(WSManNativeApi.WSManApiDll, SetLastError = false, CharSet = CharSet.Unicode)] + [DllImport(WSManNativeApi.WSManClientApiDll, SetLastError = false, CharSet = CharSet.Unicode)] internal static extern int WSManGetErrorMessage(IntPtr wsManAPIHandle, int flags, string languageCode, @@ -2879,7 +2881,7 @@ namespace System.Management.Automation.Remoting.Client /// Specifies the options that are available for retrieval. /// Specifies the result object (WSMAN_DATA). /// - [DllImport(WSManNativeApi.WSManApiDll, SetLastError = false, CharSet = CharSet.Unicode)] + [DllImport(WSManNativeApi.WSManProviderApiDll, SetLastError = false, CharSet = CharSet.Unicode)] internal static extern int WSManPluginGetOperationParameters( IntPtr requestDetails, int flags, @@ -2895,7 +2897,7 @@ namespace System.Management.Automation.Remoting.Client /// Reports any failure in the operation. Terminates on non-NO_ERROR status. /// XML document containing extra error information. /// - [DllImport(WSManNativeApi.WSManApiDll, SetLastError = false, CharSet = CharSet.Unicode)] + [DllImport(WSManNativeApi.WSManProviderApiDll, SetLastError = false, CharSet = CharSet.Unicode)] internal static extern int WSManPluginOperationComplete( IntPtr requestDetails, int flags, @@ -2942,7 +2944,7 @@ namespace System.Management.Automation.Remoting.Client /// Specifies the state of the command. It must be set to a value specified by the plugin. /// Only set when the commandState is terminating. /// - [DllImport(WSManNativeApi.WSManApiDll, SetLastError = false, CharSet = CharSet.Unicode)] + [DllImport(WSManNativeApi.WSManProviderApiDll, SetLastError = false, CharSet = CharSet.Unicode)] internal static extern int WSManPluginReceiveResult( IntPtr requestDetails, int flags, @@ -2961,7 +2963,7 @@ namespace System.Management.Automation.Remoting.Client /// /// Defines the value to pass into all future shell and command operations. Represents either the shell or the command. /// - [DllImport(WSManNativeApi.WSManApiDll, SetLastError = false, CharSet = CharSet.Unicode)] + [DllImport(WSManNativeApi.WSManProviderApiDll, SetLastError = false, CharSet = CharSet.Unicode)] internal static extern int WSManPluginReportContext( IntPtr requestDetails, int flags, @@ -2974,7 +2976,7 @@ namespace System.Management.Automation.Remoting.Client /// Callback to be executed on shutdown /// /// - [DllImport(WSManNativeApi.WSManApiDll, SetLastError = false, CharSet = CharSet.Unicode)] + [DllImport(WSManNativeApi.WSManProviderApiDll, SetLastError = false, CharSet = CharSet.Unicode)] internal static extern void WSManPluginRegisterShutdownCallback( IntPtr requestDetails, IntPtr shutdownCallback, diff --git a/test/powershell/Modules/Microsoft.PowerShell.Archive/Pester.Commands.Cmdlets.Archive.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Archive/Pester.Commands.Cmdlets.Archive.Tests.ps1 new file mode 100644 index 000000000..88dd27310 --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Archive/Pester.Commands.Cmdlets.Archive.Tests.ps1 @@ -0,0 +1,980 @@ +<############################################################################################ + # File: Pester.Commands.Cmdlets.ArchiveTests.ps1 + # Commands.Cmdlets.ArchiveTests suite contains Tests that are + # used for validating Microsoft.PowerShell.Archive module. + ############################################################################################> +$script:TestSourceRoot = $PSScriptRoot +Describe "Test suite for Microsoft.PowerShell.Archive module" -Tags "CI" { + + AfterAll { + $global:ProgressPreference = $_progressPreference + $env:PSMODULEPATH = $_modulePath + } + BeforeAll { + # remove the archive module forcefully, to be sure we get the correct version + if ( Get-Module Microsoft.PowerShell.Archive. ) { + Remove-Module Microsoft.PowerShell.Archive -force + } + # Version comparisons should use a System.Version rather than SemanticVersion + $PSVersion = $PSVersionTable.PSVersion -as [Version] + # Write-Progress not supported yet on Core + $_progressPreference = $ProgressPreference + # we need to be sure that we get the correct archive module + $_modulePath = $env:PSMODULEPATH + $powershellexe = (get-process -pid $PID).MainModule.FileName + $env:PSMODULEPATH = join-path ([io.path]::GetDirectoryName($powershellexe)) Modules + if ( $IsCoreCLR ) { $global:ProgressPreference = "SilentlyContinue" } + + Setup -d SourceDir + Setup -d SourceDir/ChildDir-1 + Setup -d SourceDir/ChildDir-2 + Setup -d SourceDir/ChildEmptyDir + + $content = "Some Data" + $Files = ( [io.path]::Combine("SourceDir","Sample-1.txt")), ([io.path]::Combine("SourceDir","Sample-2.txt")), + ([io.path]::Combine("SourceDir","ChildDir-1","Sample-3.txt")), ([io.path]::Combine("SourceDir","ChildDir-1","Sample-4.txt")), + ([io.path]::Combine("SourceDir","ChildDir-2","Sample-5.txt")), ([io.path]::Combine("SourceDir","ChildDir-2","Sample-6.txt")) + + foreach($file in $files ) { + Setup -f $file -content $content + } + + Setup -f Sample.unzip -content "Some Text" + Setup -f Sample.cab -content "Some Text" + + $preCreatedArchivePath = Join-Path $script:TestSourceRoot "SamplePreCreatedArchive.archive" + Copy-Item $preCreatedArchivePath $TestDrive/SamplePreCreatedArchive.zip -Force + } + + function Add-CompressionAssemblies { + Add-Type -AssemblyName System.IO.Compression + if (($psedition -eq "Core") -or $IsCoreCLR ) + { + Add-Type -AssemblyName System.IO.Compression.ZipFile + } + else + { + Add-Type -AssemblyName System.IO.Compression.FileSystem + } + } + + function CompressArchivePathParameterSetValidator { + param + ( + [string[]] $path, + [string] $destinationPath, + [string] $compressionLevel = "Optimal" + ) + + try + { + Compress-Archive -Path $path -DestinationPath $destinationPath -CompressionLevel $compressionLevel + trow "ValidateNotNullOrEmpty attribute is missing on one of parameters belonging to Path parameterset." + } + catch + { + $_.FullyQualifiedErrorId | Should Be "ParameterArgumentValidationError,Compress-Archive" + } + } + + function CompressArchiveLiteralPathParameterSetValidator { + param + ( + [string[]] $literalPath, + [string] $destinationPath, + [string] $compressionLevel = "Optimal" + ) + + try + { + Compress-Archive -LiteralPath $literalPath -DestinationPath $destinationPath -CompressionLevel $compressionLevel + throw "ValidateNotNullOrEmpty attribute is missing on one of parameters belonging to LiteralPath parameterset." + } + catch + { + $_.FullyQualifiedErrorId | Should Be "ParameterArgumentValidationError,Compress-Archive" + } + } + + + function CompressArchiveInValidPathValidator { + param + ( + [string[]] $path, + [string] $destinationPath, + [string] $invalidPath, + [string] $expectedFullyQualifiedErrorId + ) + + try + { + Compress-Archive -Path $path -DestinationPath $destinationPath + throw "Failed to validate that an invalid Path $invalidPath was supplied as input to Compress-Archive cmdlet." + } + catch + { + $_.FullyQualifiedErrorId | Should Be $expectedFullyQualifiedErrorId + } + } + + function CompressArchiveInValidArchiveFileExtensionValidator { + param + ( + [string[]] $path, + [string] $destinationPath, + [string] $invalidArchiveFileExtension + ) + + try + { + Compress-Archive -Path $path -DestinationPath $destinationPath + throw "Failed to validate that an invalid archive file format $invalidArchiveFileExtension was supplied as input to Compress-Archive cmdlet." + } + catch + { + $_.FullyQualifiedErrorId | Should Be "NotSupportedArchiveFileExtension,Compress-Archive" + } + } + + function Validate-ArchiveEntryCount { + param + ( + [string] $path, + [int] $expectedEntryCount + ) + + Add-CompressionAssemblies + try + { + $archiveFileStreamArgs = @($path, [System.IO.FileMode]::Open) + $archiveFileStream = New-Object -TypeName System.IO.FileStream -ArgumentList $archiveFileStreamArgs + + $zipArchiveArgs = @($archiveFileStream, [System.IO.Compression.ZipArchiveMode]::Read, $false) + $zipArchive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $zipArchiveArgs + + $actualEntryCount = $zipArchive.Entries.Count + $actualEntryCount | Should Be $expectedEntryCount + } + finally + { + if ($null -ne $zipArchive) { $zipArchive.Dispose()} + if ($null -ne $archiveFileStream) { $archiveFileStream.Dispose() } + } + } + + function ArchiveFileEntryContentValidator { + param + ( + [string] $path, + [string] $entryFileName, + [string] $expectedEntryFileContent + ) + + Add-CompressionAssemblies + try + { + $destFile = "$TestDrive/ExpandedFile"+([System.Guid]::NewGuid().ToString())+".txt" + + $archiveFileStreamArgs = @($path, [System.IO.FileMode]::Open) + $archiveFileStream = New-Object -TypeName System.IO.FileStream -ArgumentList $archiveFileStreamArgs + + $zipArchiveArgs = @($archiveFileStream, [System.IO.Compression.ZipArchiveMode]::Read, $false) + $zipArchive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $zipArchiveArgs + + $entryToBeUpdated = $zipArchive.Entries | ? {$_.FullName -eq $entryFileName} + + if($entryToBeUpdated -ne $null) + { + $srcStream = $entryToBeUpdated.Open() + $destStream = New-Object "System.IO.FileStream" -ArgumentList( $destFile, [System.IO.FileMode]::Create ) + $srcStream.CopyTo( $destStream ) + $destStream.Dispose() + $srcStream.Dispose() + Get-Content $destFile | Should Be $expectedEntryFileContent + } + else + { + throw "Failed to find the file $entryFileName in the archive file $path" + } + } + finally + { + if ($zipArchive) + { + $zipArchive.Dispose() + } + if ($archiveFileStream) + { + $archiveFileStream.Dispose() + } + } + } + + function ExpandArchiveInvalidParameterValidator { + param + ( + [boolean] $isLiteralPathParameterSet, + [string[]] $path, + [string] $destinationPath, + [string] $expectedFullyQualifiedErrorId + ) + + try + { + if($isLiteralPathParameterSet) + { + Expand-Archive -LiteralPath $literalPath -DestinationPath $destinationPath + } + else + { + Expand-Archive -Path $path -DestinationPath $destinationPath + } + + throw "Expand-Archive did NOT throw expected error" + } + catch + { + $_.FullyQualifiedErrorId | Should Be $expectedFullyQualifiedErrorId + } + } + + Context "Compress-Archive - Parameter validation test cases" { + + It "Validate errors from Compress-Archive with NULL & EMPTY values for Path, LiteralPath, DestinationPath, CompressionLevel parameters" { + $sourcePath = "$TestDrive/SourceDir" + $destinationPath = "$TestDrive/SampleSingleFile.zip" + + CompressArchivePathParameterSetValidator $null $destinationPath + CompressArchivePathParameterSetValidator $sourcePath $null + CompressArchivePathParameterSetValidator $null $null + + CompressArchivePathParameterSetValidator "" $destinationPath + CompressArchivePathParameterSetValidator $sourcePath "" + CompressArchivePathParameterSetValidator "" "" + + CompressArchivePathParameterSetValidator $null $null "NoCompression" + + CompressArchiveLiteralPathParameterSetValidator $null $destinationPath + CompressArchiveLiteralPathParameterSetValidator $sourcePath $null + CompressArchiveLiteralPathParameterSetValidator $null $null + + CompressArchiveLiteralPathParameterSetValidator "" $destinationPath + CompressArchiveLiteralPathParameterSetValidator $sourcePath "" + CompressArchiveLiteralPathParameterSetValidator "" "" + + CompressArchiveLiteralPathParameterSetValidator $null $null "NoCompression" + + CompressArchiveLiteralPathParameterSetValidator $sourcePath $destinationPath $null + CompressArchiveLiteralPathParameterSetValidator $sourcePath $destinationPath "" + } + + It "Validate errors from Compress-Archive when invalid path (non-existing path / non-filesystem path) is supplied for Path or LiteralPath parameters" { + CompressArchiveInValidPathValidator "$TestDrive/InvalidPath" $TestDrive "$TestDrive/InvalidPath" "ArchiveCmdletPathNotFound,Compress-Archive" + if ( ! $IsCoreCLR ) { + CompressArchiveInValidPathValidator "HKLM:/SOFTWARE" $TestDrive "HKLM:/SOFTWARE" "PathNotFound,Compress-Archive" + } + CompressArchiveInValidPathValidator "$TestDrive" "$TestDrive/NonExistingDirectory/sample.zip" "$TestDrive/NonExistingDirectory/sample.zip" "ArchiveCmdletPathNotFound,Compress-Archive" + + $path = @("$TestDrive", "$TestDrive/InvalidPath") + CompressArchiveInValidPathValidator $path $TestDrive "$TestDrive/InvalidPath" "ArchiveCmdletPathNotFound,Compress-Archive" + + if ( ! $IsCoreCLR ) { + $path = @("$TestDrive", "HKLM:/SOFTWARE") + CompressArchiveInValidPathValidator $path $TestDrive "HKLM:/SOFTWARE" "PathNotFound,Compress-Archive" + } + + $invalidUnZipFileFormat = "$TestDrive/Sample.unzip" + CompressArchiveInValidArchiveFileExtensionValidator $TestDrive "$invalidUnZipFileFormat" ".unzip" + + $invalidcabZipFileFormat = "$TestDrive/Sample.cab" + CompressArchiveInValidArchiveFileExtensionValidator $TestDrive "$invalidcabZipFileFormat" ".cab" + } + + It "Validate error from Compress-Archive when archive file already exists and -Update parameter is not specified" { + $sourcePath = "$TestDrive/SourceDir" + $destinationPath = "$TestDrive/ValidateErrorWhenUpdateNotSpecified.zip" + + try + { + "Some Data" > $destinationPath + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + throw "Failed to validate that an archive file format $destinationPath already exists and -Update switch parameter is not specified while running Compress-Archive command." + } + catch + { + $_.FullyQualifiedErrorId | Should Be "ArchiveFileExists,Compress-Archive" + } + } + + It "Validate error from Compress-Archive when duplicate paths are supplied as input to Path parameter" { + $sourcePath = @( + "$TestDrive/SourceDir/Sample-1.txt", + "$TestDrive/SourceDir/Sample-1.txt") + $destinationPath = "$TestDrive/DuplicatePaths.zip" + + try + { + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + throw "Failed to detect that duplicate Path $sourcePath is supplied as input to Path parameter." + } + catch + { + $_.FullyQualifiedErrorId | Should Be "DuplicatePathFound,Compress-Archive" + } + } + + It "Validate error from Compress-Archive when duplicate paths are supplied as input to LiteralPath parameter" { + $sourcePath = @( + "$TestDrive/SourceDir/Sample-1.txt", + "$TestDrive/SourceDir/Sample-1.txt") + $destinationPath = "$TestDrive/DuplicatePaths.zip" + + try + { + Compress-Archive -LiteralPath $sourcePath -DestinationPath $destinationPath + throw "Failed to detect that duplicate Path $sourcePath is supplied as input to LiteralPath parameter." + } + catch + { + $_.FullyQualifiedErrorId | Should Be "DuplicatePathFound,Compress-Archive" + } + } + + It "Validate that relative path can be specified as Path parameter of Compress-Archive cmdlet" { + $sourcePath = "./SourceDir" + $destinationPath = "RelativePathForPathParameter.zip" + try { + push-location $TESTDRIVE + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + Test-Path $destinationPath | Should Be $true + } + finally { + Pop-Location + } + } + It "Validate that relative path can be specified as LiteralPath parameter of Compress-Archive cmdlet" { + $sourcePath = "./SourceDir" + $destinationPath = "RelativePathForLiteralPathParameter.zip" + try { + push-location $TESTDRIVE + Compress-Archive -LiteralPath $sourcePath -DestinationPath $destinationPath + Test-Path $destinationPath | Should Be $true + } + finally { + Pop-Location + } + } + It "Validate that relative path can be specified as DestinationPath parameter of Compress-Archive cmdlet" { + $sourcePath = "$TestDrive/SourceDir" + $destinationPath = "./RelativePathForDestinationPathParameter.zip" + try { + push-location $TESTDRIVE + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + Test-Path $destinationPath | Should Be $true + } + finally { + Pop-Location + } + } + } + + Context "Compress-Archive - functional test cases" { + It "Validate that a single file can be compressed using Compress-Archive cmdlet" { + $sourcePath = [io.path]::Combine("$TestDrive","SourceDir","ChildDir-1","Sample-3.txt") + $destinationPath = [io.path]::Combine("$TestDrive","SampleSingleFile.zip") + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + $destinationPath | Should Exist + } + # This test requires a fix in PS5 to support reading paths with square bracket + It "Validate that Compress-Archive cmdlet can accept LiteralPath parameter with Special Characters" -skip:($PSVersion -lt "5.0") { + $sourcePath = "$TestDrive/SourceDir/ChildDir-1/Sample[]File.txt" + "Some Random Content" | Out-File -LiteralPath $sourcePath + $destinationPath = "$TestDrive/SampleSingleFileWithSpecialCharacters.zip" + try + { + Compress-Archive -LiteralPath $sourcePath -DestinationPath $destinationPath + $destinationPath | Should Exist + } + finally + { + get-item -literalPath $sourcePath | remove-Item -force + # Remove-Item -LiteralPath $sourcePath -Force + } + } + It "Validate that Compress-Archive cmdlet errors out when DestinationPath resolves to multiple locations" { + + New-Item $TestDrive/SampleDir/Child-1 -Type Directory -Force | Out-Null + New-Item $TestDrive/SampleDir/Child-2 -Type Directory -Force | Out-Null + New-Item $TestDrive/SampleDir/Test.txt -Type File -Force | Out-Null + + $destinationPath = "$TestDrive/SampleDir/Child-*/SampleChidArchive.zip" + $sourcePath = "$TestDrive/SampleDir/Test.txt" + try + { + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + throw "Failed to detect that destination $destinationPath can resolve to multiple paths" + } + catch + { + $_.FullyQualifiedErrorId | Should Be "InvalidArchiveFilePath,Compress-Archive" + } + finally + { + Remove-Item -LiteralPath $TestDrive/SampleDir -Force -Recurse + } + } + It "Validate that Compress-Archive cmdlet works when DestinationPath has wild card pattern and resolves to a single valid path" { + + New-Item $TestDrive/SampleDir/Child-1 -Type Directory -Force | Out-Null + New-Item $TestDrive/SampleDir/Test.txt -Type File -Force | Out-Null + + $destinationPath = "$TestDrive/SampleDir/Child-*/SampleChidArchive.zip" + $sourcePath = "$TestDrive/SampleDir/Test.txt" + try + { + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + $destinationPath | Should Exist + } + finally + { + Remove-Item -LiteralPath $TestDrive/SampleDir -Force -Recurse + } + } + # This test requires a fix in PS5 to support reading paths with square bracket + It "Validate that Compress-Archive cmdlet can accept LiteralPath parameter for a directory with Special Characters in the directory name" -skip:($PSVersion -lt "5.0") { + $sourcePath = "$TestDrive/Source[]Dir/ChildDir[]-1" + New-Item $sourcePath -Type Directory | Out-Null + "Some Random Content" | Out-File -LiteralPath "$sourcePath/Sample[]File.txt" + $destinationPath = "$TestDrive/SampleDirWithSpecialCharacters.zip" + try + { + Compress-Archive -LiteralPath $sourcePath -DestinationPath $destinationPath + $destinationPath | Should Exist + } + finally + { + get-item -LiteralPath $sourcePath | Remove-Item -Force -Recurse + # Remove-Item -LiteralPath $sourcePath -Force -Recurse + } + } + It "Validate that Compress-Archive cmdlet can accept DestinationPath parameter with Special Characters" { + $sourcePath = "$TestDrive/SourceDir/ChildDir-1/Sample-3.txt" + $destinationPath = "$TestDrive/Sample[]SingleFile.zip" + try + { + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + Test-Path -LiteralPath $destinationPath | Should Be $true + } + finally + { + Get-Item -LiteralPath $destinationPath | Remove-Item -Force + # Remove-Item -LiteralPath $destinationPath -Force + } + } + It "Validate that Source Path can be at SystemDrive location" -skip:($IsCoreCLR) { + $sourcePath = "$env:SystemDrive/SourceDir" + $destinationPath = "$TestDrive/SampleFromSystemDrive.zip" + New-Item $sourcePath -Type Directory | Out-Null + "Some Data" | Out-File -FilePath $sourcePath/SampleSourceFileForArchive.txt + try + { + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + Test-Path $destinationPath | Should Be $true + } + finally + { + remove-item "$sourcePath" -Force -Recurse -ErrorAction SilentlyContinue + } + } + It "Validate that multiple files can be compressed using Compress-Archive cmdlet" { + $sourcePath = @( + "$TestDrive/SourceDir/ChildDir-1/Sample-3.txt", + "$TestDrive/SourceDir/ChildDir-1/Sample-4.txt", + "$TestDrive/SourceDir/ChildDir-2/Sample-5.txt", + "$TestDrive/SourceDir/ChildDir-2/Sample-6.txt") + $destinationPath = "$TestDrive/SampleMultipleFiles.zip" + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + Test-Path $destinationPath | Should Be $true + } + It "Validate that multiple files and directories can be compressed using Compress-Archive cmdlet" { + $sourcePath = @( + "$TestDrive/SourceDir/Sample-1.txt", + "$TestDrive/SourceDir/Sample-2.txt", + "$TestDrive/SourceDir/ChildDir-1", + "$TestDrive/SourceDir/ChildDir-2") + $destinationPath = "$TestDrive/SampleMultipleFilesAndDirs.zip" + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + Test-Path $destinationPath | Should Be $true + } + It "Validate that a single directory can be compressed using Compress-Archive cmdlet" { + $sourcePath = @("$TestDrive/SourceDir/ChildDir-1") + $destinationPath = "$TestDrive/SampleSingleDir.zip" + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + Test-Path $destinationPath | Should Be $true + } + It "Validate that a single directory with multiple files and subdirectories can be compressed using Compress-Archive cmdlet" { + $sourcePath = @("$TestDrive/SourceDir") + $destinationPath = "$TestDrive/SampleSubTree.zip" + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + Test-Path $destinationPath | Should Be $true + } + It "Validate that a single directory & multiple files can be compressed using Compress-Archive cmdlet" { + $sourcePath = @( + "$TestDrive/SourceDir/ChildDir-1", + "$TestDrive/SourceDir/Sample-1.txt", + "$TestDrive/SourceDir/Sample-2.txt") + $destinationPath = "$TestDrive/SampleMultipleFilesAndSingleDir.zip" + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + Test-Path $destinationPath | Should Be $true + } + + It "Validate that if .zip extension is not supplied as input to DestinationPath parameter, then .zip extension is appended" { + $sourcePath = @("$TestDrive/SourceDir") + $destinationPath = "$TestDrive/SampleNoExtension.zip" + $destinationWithoutExtensionPath = "$TestDrive/SampleNoExtension" + Compress-Archive -Path $sourcePath -DestinationPath $destinationWithoutExtensionPath + Test-Path $destinationPath | Should Be $true + } + + It "Validate that -Update parameter makes Compress-Archive to not throw an error if archive file already exists" { + $sourcePath = @("$TestDrive/SourceDir") + $destinationPath = "$TestDrive/SampleUpdateTest.zip" + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + Test-Path $destinationPath | Should Be $true + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Update + Test-Path $destinationPath | Should Be $true + } + It "Validate -Update parameter by adding a new file to an existing archive file" { + $sourcePath = @("$TestDrive/SourceDir/ChildDir-1") + $destinationPath = "$TestDrive/SampleUpdateAdd1File.zip" + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + Test-Path $destinationPath | Should Be $true + New-Item $TestDrive/SourceDir/ChildDir-1/Sample-AddedNewFile.txt -Type File | Out-Null + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Update + Test-Path $destinationPath | Should Be $true + Validate-ArchiveEntryCount -path $destinationPath -expectedEntryCount 3 + } + + It "Validate that all CompressionLevel values can be used with Compress-Archive cmdlet" { + $sourcePath = "$TestDrive/SourceDir/Sample-1.txt" + + $destinationPath = "$TestDrive/FastestCompressionLevel.zip" + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -CompressionLevel Fastest + Test-Path $destinationPath | Should Be $true + + $destinationPath = "$TestDrive/OptimalCompressionLevel.zip" + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -CompressionLevel Optimal + Test-Path $destinationPath | Should Be $true + + $destinationPath = "$TestDrive/NoCompressionCompressionLevel.zip" + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -CompressionLevel NoCompression + Test-Path $destinationPath | Should Be $true + } + + It "Validate that -Update parameter is modifying a file that already exists in the archive file" { + $filePath = "$TestDrive/SourceDir/ChildDir-1/Sample-3.txt" + + $initialContent = "Initial Content" + $modifiedContent = "Modified Content" + + $initialContent | Set-Content $filePath + + $sourcePath = "$TestDrive/SourceDir" + $destinationPath = "$TestDrive/UpdatingModifiedFile.zip" + + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + Test-Path $destinationPath | Should Be $True + + $modifiedContent | Set-Content $filePath + + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath -Update + Test-Path $destinationPath | Should Be $True + + ArchiveFileEntryContentValidator "$destinationPath" ([io.path]::Combine("SourceDir","ChildDir-1","Sample-3.txt")) $modifiedContent + } + + It "Validate Compress-Archive cmdlet in pipleline scenario" { + $destinationPath = "$TestDrive/CompressArchiveFromPipeline.zip" + + # Piping a single file path to Compress-Archive + dir -Path $TestDrive/SourceDir/Sample-1.txt | Compress-Archive -DestinationPath $destinationPath + Test-Path $destinationPath | Should Be $True + + # Piping a string directory path to Compress-Archive + "$TestDrive/SourceDir/ChildDir-2" | Compress-Archive -DestinationPath $destinationPath -Update + Test-Path $destinationPath | Should Be $True + + # Piping the output of Get-ChildItem to Compress-Archive + dir "$TestDrive/SourceDir" -Recurse | Compress-Archive -DestinationPath $destinationPath -Update + Test-Path $destinationPath | Should Be $True + } + + It "Validate that Compress-Archive works on ReadOnly files" { + $sourcePath = "$TestDrive/ReadOnlyFile.txt" + $destinationPath = "$TestDrive/TestForReadOnlyFile.zip" + + "Some Content" | Out-File -FilePath $sourcePath + $createdItem = Get-Item $sourcePath + $createdItem.Attributes = 'ReadOnly' + + Compress-Archive -Path $sourcePath -DestinationPath $destinationPath + Test-Path $destinationPath | Should Be $true + } + + It "Validate that Compress-Archive generates Verbose messages" { + $sourcePath = "$TestDrive/SourceDir" + $destinationPath = "$TestDrive/Compress-Archive generates VerboseMessages.zip" + + try + { + $ps=[PowerShell]::Create() + $ps.Streams.Error.Clear() + $ps.Streams.Verbose.Clear() + $script = "Import-Module Microsoft.PowerShell.Archive; Compress-Archive -Path $sourcePath -DestinationPath `"$destinationPath`" -CompressionLevel Fastest -Verbose" + $ps.AddScript($script) + $ps.Invoke() + + $ps.Streams.Verbose.Count -gt 0 | Should Be $True + $ps.Streams.Error.Count | Should Be 0 + } + finally + { + $ps.Dispose() + } + } + } + + Context "Expand-Archive - Parameter validation test cases" { + It "Validate non existing archive -Path trows expected error message" { + $sourcePath = "$TestDrive/SourceDir" + $destinationPath = "$TestDrive/ExpandedArchive" + try + { + Expand-Archive -Path $sourcePath -DestinationPath $destinationPath + throw "Expand-Archive succeeded for non existing archive path" + } + catch + { + $_.FullyQualifiedErrorId | Should Be "PathNotFound,Expand-Archive" + } + } + + It "Validate errors from Expand-Archive with NULL & EMPTY values for Path, LiteralPath, DestinationPath parameters" { + ExpandArchiveInvalidParameterValidator $false $null "$TestDrive/SourceDir" "ParameterArgumentValidationError,Expand-Archive" + ExpandArchiveInvalidParameterValidator $false $null $null "ParameterArgumentValidationError,Expand-Archive" + + ExpandArchiveInvalidParameterValidator $false "$TestDrive/SourceDir" $null "ParameterArgumentTransformationError,Expand-Archive" + ExpandArchiveInvalidParameterValidator $false "" "$TestDrive/SourceDir" "ParameterArgumentTransformationError,Expand-Archive" + ExpandArchiveInvalidParameterValidator $false "$TestDrive/SourceDir" "" "ParameterArgumentTransformationError,Expand-Archive" + ExpandArchiveInvalidParameterValidator $false "" "" "ParameterArgumentTransformationError,Expand-Archive" + + ExpandArchiveInvalidParameterValidator $true $null "$TestDrive/SourceDir" "ParameterArgumentValidationError,Expand-Archive" + ExpandArchiveInvalidParameterValidator $true $null $null "ParameterArgumentValidationError,Expand-Archive" + + ExpandArchiveInvalidParameterValidator $true "$TestDrive/SourceDir" $null "ParameterArgumentValidationError,Expand-Archive" + ExpandArchiveInvalidParameterValidator $true "" "$TestDrive/SourceDir" "ParameterArgumentValidationError,Expand-Archive" + ExpandArchiveInvalidParameterValidator $true "$TestDrive/SourceDir" "" "ParameterArgumentValidationError,Expand-Archive" + ExpandArchiveInvalidParameterValidator $true "" "" "ParameterArgumentValidationError,Expand-Archive" + + ExpandArchiveInvalidParameterValidator $true $null "$TestDrive/SourceDir" "ParameterArgumentValidationError,Expand-Archive" + ExpandArchiveInvalidParameterValidator $true $null $null "ParameterArgumentValidationError,Expand-Archive" + + ExpandArchiveInvalidParameterValidator $true "$TestDrive/SourceDir" $null "ParameterArgumentValidationError,Expand-Archive" + ExpandArchiveInvalidParameterValidator $true "" "$TestDrive/SourceDir" "ParameterArgumentValidationError,Expand-Archive" + ExpandArchiveInvalidParameterValidator $true "$TestDrive/SourceDir" "" "ParameterArgumentValidationError,Expand-Archive" + ExpandArchiveInvalidParameterValidator $true "" "" "ParameterArgumentValidationError,Expand-Archive" + } + + It "Validate errors from Expand-Archive when invalid path (non-existing path / non-filesystem path) is supplied for Path or LiteralPath parameters" { + try { Expand-Archive -Path "$TestDrive/NonExistingArchive" -DestinationPath "$TestDrive/SourceDir"; throw "Expand-Archive did NOT throw expected error" } + catch { $_.FullyQualifiedErrorId | Should Be "ArchiveCmdletPathNotFound,Expand-Archive" } + + if ( ! $IsCoreCLR ) { + try { Expand-Archive -Path "HKLM:/SOFTWARE" -DestinationPath "$TestDrive/SourceDir"; throw "Expand-Archive did NOT throw expected error" } + catch { $_.FullyQualifiedErrorId | Should Be "PathNotFound,Expand-Archive" } + } + + try { Expand-Archive -LiteralPath "$TestDrive/NonExistingArchive" -DestinationPath "$TestDrive/SourceDir"; throw "Expand-Archive did NOT throw expected error" } + catch { $_.FullyQualifiedErrorId | Should Be "ArchiveCmdletPathNotFound,Expand-Archive" } + + if ( ! $IsCoreCLR ) { + try { Expand-Archive -LiteralPath "HKLM:/SOFTWARE" -DestinationPath "$TestDrive/SourceDir"; throw "Expand-Archive did NOT throw expected error" } + catch { $_.FullyQualifiedErrorId | Should Be "PathNotFound,Expand-Archive" } + } + } + + It "Validate error from Expand-Archive when invalid path (non-existing path / non-filesystem path) is supplied for DestinationPath parameter" { + $sourcePath = "$TestDrive/SamplePreCreatedArchive.zip" + # $destinationPath = "HKLM:/SOFTWARE" + $destinationPath = "Variable:/" + + try { Expand-Archive -Path $sourcePath -DestinationPath $destinationPath; throw "Expand-Archive did NOT throw expected error" } + catch { $_.FullyQualifiedErrorId | Should Be "InvalidDirectoryPath,Expand-Archive" } + } + } + + Context "Expand-Archive - functional test cases" { + It "Validate basic Expand-Archive scenario" { + $sourcePath = "$TestDrive/SamplePreCreatedArchive.zip" + $content = "Some Data" + $destinationPath = "$TestDrive/DestDirForBasicExpand" + $files = @("Sample-1.txt", "Sample-2.txt") + + # The files in "$TestDrive/SamplePreCreatedArchive.zip" are precreated. + $fileCreationTimeStamp = Get-Date -Year 2014 -Month 6 -Day 13 -Hour 15 -Minute 50 -Second 20 -Millisecond 0 + + Expand-Archive -Path $sourcePath -DestinationPath $destinationPath + foreach($currentFile in $files) + { + $expandedFile = Join-Path $destinationPath -ChildPath $currentFile + Test-Path $expandedFile | Should Be $True + + # We are validating to make sure that time stamps are preserved in the + # compressed archive are reflected back when the file is expanded. + (dir $expandedFile).LastWriteTime.CompareTo($fileCreationTimeStamp) | Should Be 0 + + Get-Content $expandedFile | Should Be $content + } + } + It "Validate that Expand-Archive cmdlet errors out when DestinationPath resolves to multiple locations" { + $testbasename = "TargetDir" + setup -d "$testbasename" + setup -d "$testbasename/Child-1" + setup -d "$testbasename/Child-2" + + $destinationPath = [io.path]::Combine("$TestDrive","$testbasename","Child-*") + $sourcePath = join-path "$TestDrive" "SamplePreCreatedArchive.zip" + try + { + Expand-Archive -Path $sourcePath -DestinationPath $destinationPath + throw "Failed to detect that destination $destinationPath can resolve to multiple paths" + } + catch + { + $_.FullyQualifiedErrorId | Should Be "InvalidDestinationPath,Expand-Archive" + } + finally + { + Remove-Item -LiteralPath "$TestDrive/$testbasename" -Force -Recurse + } + } + It "Validate that Expand-Archive cmdlet works when DestinationPath resolves has wild card pattern and resolves to a single valid path" { + $testbasename = "TargetDir" + setup -d "$testbasename" + setup -d "$testbasename/Child-1" + + $destinationPath = [io.path]::Combine("$TestDrive","$testbasename","Child-*") + $sourcePath = "$TestDrive/SamplePreCreatedArchive.zip" + try + { + Expand-Archive -Path $sourcePath -DestinationPath $destinationPath + $expandedFiles = Get-ChildItem $destinationPath -Recurse + $expandedFiles.Length | Should BeGreaterThan 1 + } + finally + { + Remove-Item -LiteralPath "$TestDrive/$testbasename" -Force -Recurse + } + } + It "Validate Expand-Archive scenario where DestinationPath has Special Characters" { + $sourcePath = "$TestDrive/SamplePreCreatedArchive.zip" + $content = "Some Data" + $destinationPath = "$TestDrive/DestDir[]Expand" + $files = @("Sample-1.txt", "Sample-2.txt") + + # The files in "$TestDrive/SamplePreCreatedArchive.zip" are precreated. + $fileCreationTimeStamp = Get-Date -Year 2014 -Month 6 -Day 13 -Hour 15 -Minute 50 -Second 20 -Millisecond 0 + + Expand-Archive -Path $sourcePath -DestinationPath $destinationPath + foreach($currentFile in $files) + { + $expandedFile = Join-Path $destinationPath -ChildPath $currentFile + Test-Path -LiteralPath $expandedFile | Should Be $True + + # We are validating to make sure that time stamps are preserved in the + # compressed archive are reflected back when the file is expanded. + (dir -LiteralPath $expandedFile).LastWriteTime.CompareTo($fileCreationTimeStamp) | Should Be 0 + + Get-Content -LiteralPath $expandedFile | Should Be $content + } + } + It "Invoke Expand-Archive with relative path in Path parameter and -Force parameter" { + $sourcePath = "./SamplePreCreatedArchive.zip" + $destinationPath = "$TestDrive/SomeOtherNonExistingDir/Path" + try + { + Push-Location $TestDrive + + Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -Force + $expandedFiles = Get-ChildItem $destinationPath -Recurse + $expandedFiles.Length | Should Be 2 + } + finally + { + Pop-Location + } + } + + It "Invoke Expand-Archive with relative path in LiteralPath parameter and -Force parameter" { + $sourcePath = "./SamplePreCreatedArchive.zip" + $destinationPath = "$TestDrive/SomeOtherNonExistingDir/LiteralPath" + try + { + Push-Location $TestDrive + + Expand-Archive -LiteralPath $sourcePath -DestinationPath $destinationPath -Force + $expandedFiles = Get-ChildItem $destinationPath -Recurse + $expandedFiles.Length | Should Be 2 + } + finally + { + Pop-Location + } + } + + It "Invoke Expand-Archive with non-existing relative directory in DestinationPath parameter and -Force parameter" { + $sourcePath = "$TestDrive/SamplePreCreatedArchive.zip" + $destinationPath = "./SomeOtherNonExistingDir/DestinationPath" + try + { + Push-Location $TestDrive + + Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -Force + $expandedFiles = Get-ChildItem $destinationPath -Recurse + $expandedFiles.Length | Should Be 2 + } + finally + { + Pop-Location + } + } + + It "Invoke Expand-Archive with unsupported archive format" { + $sourcePath = "$TestDrive/Sample.cab" + $destinationPath = "$TestDrive/UnsupportedArchiveFormatDir" + try + { + Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -Force + throw "Failed to detect unsupported archive format at $sourcePath" + } + catch + { + $_.FullyQualifiedErrorId | Should Be "NotSupportedArchiveFileExtension,Expand-Archive" + } + } + + It "Invoke Expand-Archive with archive file containing multiple files, directories with subdirectories and empty directories" { + $sourcePath = "$TestDrive/SourceDir" + $archivePath = "$TestDrive/FileAndDirTreeForExpand.zip" + $destinationPath = "$TestDrive/FileAndDirTree" + $sourceList = dir $sourcePath -Name + + Add-CompressionAssemblies + [System.IO.Compression.ZipFile]::CreateFromDirectory($sourcePath, $archivePath) + + Expand-Archive -Path $archivePath -DestinationPath $destinationPath + $extractedList = dir $destinationPath -Name + + Compare-Object -ReferenceObject $extractedList -DifferenceObject $sourceList -PassThru | Should Be $null + } + + It "Validate Expand-Archive cmdlet in pipleline scenario" { + $sourcePath = "$TestDrive/SamplePreCreated*.zip" + $destinationPath = "$TestDrive/PipeToExpandArchive" + + $content = "Some Data" + $files = @("Sample-1.txt", "Sample-2.txt") + + dir $sourcePath | Expand-Archive -DestinationPath $destinationPath + + foreach($currentFile in $files) + { + $expandedFile = Join-Path $destinationPath -ChildPath $currentFile + Test-Path $expandedFile | Should Be $True + Get-Content $expandedFile | Should Be $content + } + } + + It "Validate that Expand-Archive generates Verbose messages" { + $sourcePath = "$TestDrive/SamplePreCreatedArchive.zip" + $destinationPath = "$TestDrive/VerboseMessagesInExpandArchive" + + try + { + $ps=[PowerShell]::Create() + $ps.Streams.Error.Clear() + $ps.Streams.Verbose.Clear() + $script = "Import-Module Microsoft.PowerShell.Archive; Expand-Archive -Path $sourcePath -DestinationPath $destinationPath -Verbose" + $ps.AddScript($script) + $ps.Invoke() + + $ps.Streams.Verbose.Count -gt 0 | Should Be $True + $ps.Streams.Error.Count | Should Be 0 + } + finally + { + $ps.Dispose() + } + } + + It "Validate that without -Force parameter Expand-Archive generates non-terminating errors without overwriting existing files" { + $sourcePath = "$TestDrive/SamplePreCreatedArchive.zip" + $destinationPath = "$TestDrive/NoForceParameterExpandArchive" + + try + { + $ps=[PowerShell]::Create() + $ps.Streams.Error.Clear() + $ps.Streams.Verbose.Clear() + $script = "Import-Module Microsoft.PowerShell.Archive; Expand-Archive -Path $sourcePath -DestinationPath $destinationPath; Expand-Archive -Path $sourcePath -DestinationPath $destinationPath" + $ps.AddScript($script) + $ps.Invoke() + + $ps.Streams.Error.Count -gt 0 | Should Be $True + } + finally + { + $ps.Dispose() + } + } + + It "Validate that without DestinationPath parameter Expand-Archive cmdlet succeeds in expanding the archive" { + $sourcePath = "$TestDrive/SamplePreCreatedArchive.zip" + $archivePath = "$TestDrive/NoDestinationPathParameter.zip" + $destinationPath = "$TestDrive/NoDestinationPathParameter" + copy-item $sourcePath $archivePath -Force + + try + { + Push-Location $TestDrive + + Expand-Archive -Path $archivePath + (dir $destinationPath).Count | Should Be 2 + } + finally + { + Pop-Location + } + } + + It "Validate that without DestinationPath parameter Expand-Archive cmdlet succeeds in expanding the archive when destination directory exists" { + $sourcePath = "$TestDrive/SamplePreCreatedArchive.zip" + $archivePath = "$TestDrive/NoDestinationPathParameterDirExists.zip" + $destinationPath = "$TestDrive/NoDestinationPathParameterDirExists" + copy-item $sourcePath $archivePath -Force + New-Item -Path $destinationPath -ItemType Directory | Out-Null + + try + { + Push-Location $TestDrive + + Expand-Archive -Path $archivePath + (dir $destinationPath).Count | Should Be 2 + } + finally + { + Pop-Location + } + } + } +} diff --git a/test/powershell/Modules/Microsoft.PowerShell.Archive/SamplePreCreatedArchive.archive b/test/powershell/Modules/Microsoft.PowerShell.Archive/SamplePreCreatedArchive.archive new file mode 100644 index 000000000..c2aecb417 Binary files /dev/null and b/test/powershell/Modules/Microsoft.PowerShell.Archive/SamplePreCreatedArchive.archive differ diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/History.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/History.Tests.ps1 new file mode 100644 index 000000000..054e08f1f --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/History.Tests.ps1 @@ -0,0 +1,64 @@ +Describe "History cmdlet test cases" -Tags "CI" { + + It "Tests Invoke-History on a cmdlet that generates output on all streams" { + $streamSpammer = ' + function StreamSpammer + { + [CmdletBinding()] + param() + + Write-Debug "Debug" + Write-Error "Error" + Write-Information "Information" + Write-Progress "Progress" + Write-Verbose "Verbose" + Write-Warning "Warning" + "Output" + } + + $informationPreference = "Continue" + $debugPreference = "Continue" + $verbosePreference = "Continue" + ' + + $invocationSettings = New-Object System.Management.Automation.PSInvocationSettings + $invocationSettings.AddToHistory = $true + $ps = [PowerShell]::Create() + $null = $ps.AddScript($streamSpammer).Invoke() + $ps.Commands.Clear() + $null = $ps.AddScript("StreamSpammer"); + $null = $ps.Invoke($null, $invocationSettings) + $ps.Commands.Clear() + $null = $ps.AddScript("Invoke-History -id 1") + $result = $ps.Invoke($null, $invocationSettings) + $outputCount = $( + $ps.Streams.Error; + $ps.Streams.Progress; + $ps.Streams.Verbose; + $ps.Streams.Debug; + $ps.Streams.Warning; + $ps.Streams.Information).Count + $ps.Dispose() + + ## Twice per stream - once for the original invocatgion, and once for the re-invocation + $outputCount | Should be 12 + } + + It "Tests Invoke-History on a private command" { + + $invocationSettings = New-Object System.Management.Automation.PSInvocationSettings + $invocationSettings.AddToHistory = $true + $ps = [PowerShell]::Create() + $null = $ps.AddScript("(Get-Command Get-Process).Visibility = 'Private'").Invoke() + $ps.Commands.Clear() + $null = $ps.AddScript("Get-Process -id $pid") + $null = $ps.Invoke($null, $invocationSettings) + $ps.Commands.Clear() + $null = $ps.AddScript("Invoke-History -id 1") + $result = $ps.Invoke($null, $invocationSettings) + $errorResult = $ps.Streams.Error[0].FullyQualifiedErrorId + $ps.Dispose() + + $errorResult | Should be CommandNotFoundException + } +} diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/Pester.Commands.Cmdlets.GetCommand.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/Pester.Commands.Cmdlets.GetCommand.Tests.ps1 new file mode 100644 index 000000000..ce3511946 --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/Pester.Commands.Cmdlets.GetCommand.Tests.ps1 @@ -0,0 +1,52 @@ +## +## Copyright (c) Microsoft Corporation, 2015 +## + +Describe "Tests Get-Command with relative paths and wildcards" -Tag "CI" { + + BeforeAll { + # Create temporary EXE command files + $file1 = Setup -f WildCardCommandA.exe -pass + $file2 = Setup -f WildCardCommand[B].exe -pass + #$null = New-Item -ItemType File -Path (Join-Path $TestDrive WildCardCommandA.exe) -ErrorAction Ignore + #$null = New-Item -ItemType File -Path (Join-Path $TestDRive WildCardCommand[B].exe) -ErrorAction Ignore + if ( $IsLinux -or $IsOSX ) { + /bin/chmod 777 "$file1" + /bin/chmod 777 "$file2" + } + } + + It "Test wildcard with drive relative directory path" { + $pathName = Join-Path $TestDrive "WildCardCommandA*" + $driveOffset = $pathName.IndexOf(":") + $pathName = $pathName.Substring($driveOffset + 1) + $result = Get-Command -Name $pathName + $result | Should Not Be $null + $result.Name | Should Be WildCardCommandA.exe + } + + It "Test wildcard with relative directory path" { + push-location $TestDrive + $result = Get-Command -Name .\WildCardCommandA* + pop-location + $result | Should Not Be $null + $result | Should Be WildCardCommandA.exe + } + + It "Test with PowerShell wildcard and relative path" { + push-location $TestDrive + + # This should use the wildcard to find WildCardCommandA.exe + $result = Get-Command -Name .\WildCardCommand[A].exe + $result | Should Not Be $null + $result | Should Be WildCardCommandA.exe + + # This should find the file WildCardCommand[B].exe + $result = Get-Command -Name .\WildCardCommand[B].exe + $result | Should Not Be $null + $result | Should Be WildCardCommand[B].exe + + Pop-Location + } + +} diff --git a/test/powershell/Modules/Microsoft.PowerShell.Management/Copy.Item.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Management/Copy.Item.Tests.ps1 new file mode 100644 index 000000000..8888b9b5a --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Management/Copy.Item.Tests.ps1 @@ -0,0 +1,868 @@ +# This is a Pester test suite to validate Copy-Item remotely using a remote session. +# +# Copyright (c) Microsoft Corporation, 2015 +# +# + +# If PS Remoting is not available, do not run the suite. +function ShouldRun +{ + if ( $IsCoreCLR ) { return $false } + $result = Invoke-Command -ComputerName . -ScriptBlock {1} -ErrorAction SilentlyContinue + return ($result -eq 1) +} + +if (-not (ShouldRun)) +{ + Write-Host "PS Remoting is not available, skipping tests..." -ForegroundColor Cyan + return +} + +Describe "Validate Copy-Item Remotely" -Tags "CI" { + + # Validate a copy item operation. + # $filePath is the source file path + # + function ValidateCopyItemOperation + { + param ([string]$filePath, [string]$destination) + + if (-not $destination) + { + $copiedFilePath = ([string]$filePath).Replace("SourceDirectory", "DestinationDirectory") + } + else + { + $fileName = Split-Path $filePath -Leaf + $copiedFilePath = Join-Path $destination $fileName + } + + $copiedFilePath | should Exist + + # Validate file attributes + $originalFile = Get-Item $filePath -Force + $newFile = Get-Item $copiedFilePath -Force + + # Validate file Length + $newFile.Length | should be $originalFile.Length + + # Validate LastWriteTime + $newFile.LastWriteTime | should be $originalFile.LastWriteTime + $newFile.LastWriteTimeUtc | should be $originalFile.LastWriteTimeUtc + + # Validate Attributes + $newFile.Attributes.value__ | Should Be $originalFile.Attributes.value__ + } + + # Validate a copy item operation. + # $filePath is the source file path + # + function ValidateCopyItemOperationForAlternateDataStream + { + param ([string]$filePath, $streamName, $expectedStreamContent) + + $copiedFilePath = ([string]$filePath).Replace("SourceDirectory", "DestinationDirectory") + $copiedFilePath | should Exist + (Get-Item $copiedFilePath).Length | should be (Get-Item $filePath).Length + + # Validate the stream + $actualStreamContent = Get-Content -Path $copiedFilePath -Stream $streamName -ea SilentlyContinue + $actualStreamContent | Should Match $expectedStreamContent + } + + BeforeAll { + $s = New-PSSession -ComputerName . -ea SilentlyContinue + if (-not $s) + { + throw "Failed to create PSSession for remote copy operations." + } + + $destinationFolderName = "DestinationDirectory" + $sourceFolderName = "SourceDirectory" + $testDirectory = Join-Path "TestDrive:" "copyItemRemotely" + $destinationDirectory = Join-Path $testDirectory $destinationFolderName + $sourceDirectory = Join-Path $testDirectory $sourceFolderName + + # Creates one txt file + # + function CreateTestFile + { + param ([switch]$setReadOnlyAttribute = $false, [switch]$emptyFile = $false) + + # Create the test directory. + New-Item -Path $sourceDirectory -Force -ItemType Directory | Out-Null + + # Create the file. + $filePath = Join-Path $sourceDirectory "testfileone.txt" + if (-not $emptyFile) + { + "File test content" | Out-File $filePath -Force + } + else + { + "" | Out-File $filePath -Force + } + + if (-not (Test-Path $filePath)) + { + throw "Failed to create test file $filePath." + } + + if ($setReadOnlyAttribute) + { + Set-ItemProperty $filePath -Name IsReadOnly -value $true -Force + } + + return (Get-Item $filePath).FullName + } + + # Create a set of directories and files with the following structure: + # .\copyItemRemotely\SourceDirectory\A\a.txt + # .\copyItemRemotely\SourceDirectory\A\a2.txt + # .\copyItemRemotely\SourceDirectory\rootFile.txt + # .\copyItemRemotely\SourceDirectory\B\b.txt + # .\copyItemRemotely\SourceDirectory\C\D\d.txt + # + function CreateTestDirectory + { + param ([switch]$setReadOnlyAttribute = $false) + + $directoriesToCreate = @() + $directoriesToCreate += "A" + $directoriesToCreate += "B" + $directoriesToCreate += "C\D" + + $filesToCreate = @() + $filesToCreate += "rootFile.txt" + $filesToCreate += "A\a.txt" + $filesToCreate += "A\a2.txt" + $filesToCreate += "B\b.txt" + $filesToCreate += "C\D\d.txt" + + # Create the directories. + foreach ($directory in $directoriesToCreate) + { + $directoryPath = Join-Path $sourceDirectory $directory + New-Item -Path $directoryPath -Force -ItemType Directory | Out-Null + } + + $result = @{ + SourceDirectory = (Get-Item $sourceDirectory).FullName + Files = @() + } + + # Create the files. + foreach ($file in $filesToCreate) + { + $filePath = Join-Path $sourceDirectory $file + $file + "`r`n File test content" | Out-File $filePath -Force + + if (-not (Test-Path $filePath)) + { + throw "Failed to create test file $filePath." + } + + if ($setReadOnlyAttribute) + { + Set-ItemProperty $filePath -Name IsReadOnly -value $true -Force + } + + $result.Files += (Get-Item $filePath).FullName + } + + return $result + } + + function GenerateTestAssembly + { + $assemblyPath = Join-Path $env:TEMP TestModule + $outputPath = Join-Path $assemblyPath TestModule.dll + + if (-not (Test-Path $assemblyPath)) + { + New-Item $assemblyPath -Force -ItemType Directory | Out-Null + } + + if (-not (Test-Path $outputPath)) + { + $code = @" + namespace TestModule + { + using System; + using System.Management.Automation; + + [Cmdlet(VerbsCommon.Get, "TestModule")] + public class TestSameCmdlets : PSCmdlet + { + protected override void ProcessRecord() + { + WriteObject("TestModule"); + } + } + } +"@ + Add-Type -TypeDefinition $code -OutputAssembly $outputPath + } + + $result = @{ + ModuleName = "TestModule" + Path = (Get-Item $outputPath).FullName + } + + return $result + } + + function GetDestinationFolderPath + { + return (Get-Item $destinationDirectory).FullName + } + } + + AfterAll { + Remove-PSSession -Name $s.Name -ea SilentlyContinue + } + + BeforeEach { + <# Ensure we start with an empty test directory. Here is the file structure + + $destinationFolderName = "DestinationDirectory" + $sourceFolderName = "SourceDirectory" + $testDirectory = Join-Path "TestDrive:" "copyItemRemotely" + $destinationDirectory = Join-Path $testDirectory $destinationFolderName + $sourceDirectory = Join-Path $testDirectory $sourceFolderName + #> + + if (test-path $testDirectory) + { + Remove-Item $testDirectory -Force -ea SilentlyContinue -Recurse + } + + # Create testDirectory, and destinationDirectory + New-Item $testDirectory -ItemType Directory -Force | Out-Null + New-Item $destinationDirectory -ItemType Directory -Force | Out-Null + } + + Context "Validate Copy-Item Locally." { + It "Copy-Item -Path $filePath -Destination $destinationFolderPath" { + + $filePath = CreateTestFile + $destinationFolderPath = GetDestinationFolderPath + Copy-Item -Path $filePath -Destination $destinationFolderPath + ValidateCopyItemOperation -filePath $filePath + } + + It "Copy-Item -Path $($testObject.SourceDirectory) -Destination $destinationFolderPath -Recurse" { + + $testObject = CreateTestDirectory + $destinationFolderPath = GetDestinationFolderPath + Copy-Item -Path $testObject.SourceDirectory -Destination $destinationFolderPath -Recurse + foreach ($file in $testObject.Files) + { + $copiedFilePath = ([string]$file).Replace("SourceDirectory", "DestinationDirectory\SourceDirectory") + $copiedFilePath | should Exist + } + } + } + + Context "Validate Copy-Item to remote session." { + + It "Copy one file to remote session." { + $filePath = CreateTestFile + $destinationFolderPath = GetDestinationFolderPath + Copy-Item -Path $filePath -ToSession $s -Destination $destinationFolderPath + ValidateCopyItemOperation -filePath $filePath + } + + It "Copy one read only file to remote session." { + + $filePath = CreateTestFile -setReadOnlyAttribute + $destinationFolderPath = GetDestinationFolderPath + Copy-Item -Path $filePath -ToSession $s -Destination $destinationFolderPath -Force + ValidateCopyItemOperation -filePath $filePath + } + + It "Copy-Item works for a read only file when '-Force' is not used." { + + $filePath = CreateTestFile -setReadOnlyAttribute + $destinationFolderPath = GetDestinationFolderPath + Copy-Item -Path $filePath -ToSession $s -Destination $destinationFolderPath -Verbose + ValidateCopyItemOperation -filePath $filePath + } + + It "Copy one folder to session Recursively" { + + $testObject = CreateTestDirectory + $destinationFolderPath = GetDestinationFolderPath + Copy-Item -Path $testObject.SourceDirectory -ToSession $s -Destination $destinationFolderPath -Recurse + + foreach ($file in $testObject.Files) + { + $copiedFilePath = ([string]$file).Replace("SourceDirectory", "DestinationDirectory\SourceDirectory") + $copiedFilePath | should Exist + (Get-Item $copiedFilePath).Length | should be (Get-Item $file).Length + } + } + + It "Copy folder with read only files to remote session recursively." { + $testObject = CreateTestDirectory -setReadOnlyAttribute + $destinationFolderPath = GetDestinationFolderPath + Copy-Item -Path $testObject.SourceDirectory -ToSession $s -Destination $destinationFolderPath -Recurse -Force + + foreach ($file in $testObject.Files) + { + $copiedFilePath = ([string]$file).Replace("SourceDirectory", "DestinationDirectory\SourceDirectory") + $copiedFilePath | should Exist + (Get-Item $copiedFilePath).Length | should be (Get-Item $file).Length + } + } + + It "Copy one file to remote session fails when the remote directory does not exist." { + + $filePath = CreateTestFile + $destinationFolderPath = GetDestinationFolderPath + $destinationFolderPath = Join-Path $destinationFolderPath "A\B\C\D\E" + $expectedFullyQualifiedErrorId = 'RemotePathNotFound,Microsoft.PowerShell.Commands.CopyItemCommand' + + try + { + Copy-Item -Path $filePath -ToSession $s -Destination $destinationFolderPath -ErrorAction Stop + throw "CodeExecuted" + } + catch + { + $_.FullyQualifiedErrorId | should be $expectedFullyQualifiedErrorId + } + } + + It "Copy folder to remote session recursively works even if the target directory does not exist." { + $testObject = CreateTestDirectory -setReadOnlyAttribute + $destinationFolderPath = GetDestinationFolderPath + $destinationFolderPath = Join-Path $destinationFolderPath "FoderThatDoesNotExist" + Copy-Item -Path $testObject.SourceDirectory -ToSession $s -Destination $destinationFolderPath -Recurse -Force + + foreach ($file in $testObject.Files) + { + $copiedFilePath = ([string]$file).Replace("SourceDirectory", "DestinationDirectory\FoderThatDoesNotExist") + $copiedFilePath | should Exist + (Get-Item $copiedFilePath).Length | should be (Get-Item $file).Length + } + } + + It "Copy one empty file to remote session." { + + $filePath = CreateTestFile -emptyFile + $destinationFolderPath = GetDestinationFolderPath + $copiedFilePath = ([string]$filePath).Replace("SourceDirectory", "DestinationDirectory") + $copiedFilePath | should Not Exist + Copy-Item -Path $filePath -ToSession $s -Destination $destinationFolderPath + $copiedFilePath | should Exist + (Get-Item $copiedFilePath).Length | should be (Get-Item $filePath).Length + } + + It "Copy-Item to session supports alternate data streams." { + + $filePath = CreateTestFile + $destinationFolderPath = GetDestinationFolderPath + $streamContent = "This content is hidden" + $streamName = "Hidden" + Set-Content -Path $filePath -Value $streamContent -Stream $streamName + Copy-Item -Path $filePath -ToSession $s -Destination $destinationFolderPath -Verbose + ValidateCopyItemOperationForAlternateDataStream -filePath $filePath -streamName $streamName -expectedStreamContent $streamContent + } + } + + Context "Validate Copy-Item from remote session." { + + It "Copy one file from remote session." { + + $filePath = CreateTestFile + $destinationFolderPath = GetDestinationFolderPath + $copiedFilePath = ([string]$filePath).Replace("SourceDirectory", "DestinationDirectory") + $copiedFilePath | should Not Exist + Copy-Item -Path $filePath -FromSession $s -Destination $destinationFolderPath + ValidateCopyItemOperation -filePath $filePath + } + + It "Copy one empty file from remote session." { + + $filePath = CreateTestFile -emptyFile + $destinationFolderPath = GetDestinationFolderPath + $copiedFilePath = ([string]$filePath).Replace("SourceDirectory", "DestinationDirectory") + $copiedFilePath | should Not Exist + Copy-Item -Path $filePath -FromSession $s -Destination $destinationFolderPath + ValidateCopyItemOperation -filePath $filePath + } + + It "Copy folder from remote session recursively." { + + $testObject = CreateTestDirectory + $destinationFolderPath = GetDestinationFolderPath + $files = @(Get-ChildItem $destinationFolderPath -Recurse -Force) + Copy-Item -Path $testObject.SourceDirectory -FromSession $s -Destination $destinationFolderPath -Recurse + + foreach ($file in $testObject.Files) + { + $copiedFilePath = ([string]$file).Replace("SourceDirectory", "DestinationDirectory\SourceDirectory") + $copiedFilePath | should Exist + (Get-Item $copiedFilePath).Length | should be (Get-Item $file).Length + } + } + + It "Copy one file from remote session fails when the target directory does not exist." { + + $filePath = CreateTestFile + $destinationFolderPath = GetDestinationFolderPath + $destinationFolderPath = Join-Path $destinationFolderPath "A\B\C\D\E" + $expectedFullyQualifiedErrorId = 'CopyItemRemotelyIOError,Microsoft.PowerShell.Commands.CopyItemCommand' + try + { + Copy-Item -Path $filePath -FromSession $s -Destination $destinationFolderPath -ErrorAction Stop + throw "CodeExecuted" + } + catch + { + $_.FullyQualifiedErrorId | should be $expectedFullyQualifiedErrorId + } + } + + It "Copy folder from remote session recursively works even if the target directory does not exist." { + + $testObject = CreateTestDirectory + $destinationFolderPath = GetDestinationFolderPath + $destinationFolderPath = Join-Path $destinationFolderPath "FoderThatDoesNotExist" + $files = @(Get-ChildItem $destinationFolderPath -Recurse -Force) + Copy-Item -Path $testObject.SourceDirectory -FromSession $s -Destination $destinationFolderPath -Recurse + + foreach ($file in $testObject.Files) + { + $copiedFilePath = ([string]$file).Replace("SourceDirectory", "DestinationDirectory\FoderThatDoesNotExist") + $copiedFilePath | should Exist + (Get-Item $copiedFilePath).Length | should be (Get-Item $file).Length + } + } + + It "Copy a read only file from a remote session." { + + $filePath = CreateTestFile -setReadOnlyAttribute + $destinationFolderPath = GetDestinationFolderPath + $copiedFilePath = ([string]$filePath).Replace("SourceDirectory", "DestinationDirectory") + Copy-Item -Path $filePath -FromSession $s -Destination $destinationFolderPath -Force + ValidateCopyItemOperation -filePath $filePath + } + + It "Copy-Item for a read only file works with no '-force' parameter." { + + $filePath = CreateTestFile -setReadOnlyAttribute + $destinationFolderPath = GetDestinationFolderPath + Copy-Item -Path $filePath -FromSession $s -Destination $destinationFolderPath + ValidateCopyItemOperation -filePath $filePath + } + + It "Copy-Item -FromSession works even when trying to copy an assembly that is currently being used by another process." { + + $testAssembly = GenerateTestAssembly + $destinationFolderPath = GetDestinationFolderPath + Import-Module $testAssembly.Path -Force + try + { + Copy-Item -Path $testAssembly.Path -FromSession $s -Destination $destinationFolderPath + ValidateCopyItemOperation -filePath $testAssembly.Path + } + finally + { + Remove-Module $testAssembly.ModuleName -Force -ea SilentlyContinue + } + } + + It "Copy-Item from session supports alternate data streams." { + + $filePath = CreateTestFile + $destinationFolderPath = GetDestinationFolderPath + $streamContent = "This content is hidden" + $streamName = "Hidden" + Set-Content -Path $filePath -Value $streamContent -Stream $streamName + Copy-Item -Path $filePath -FromSession $s -Destination $destinationFolderPath + ValidateCopyItemOperationForAlternateDataStream -filePath $filePath -streamName $streamName -expectedStreamContent $streamContent + } + } + + Context "Validate Copy-Item Remotely using wildcards" { + + It "Copy-Item from session using wildcards." { + + $testObject = CreateTestDirectory + $destinationFolderPath = GetDestinationFolderPath + $sourcePathWithWildcards = "$($testObject.SourceDirectory)\A\*.txt" + Copy-Item -Path $sourcePathWithWildcards -FromSession $s -Destination $destinationFolderPath -Force + + $sourceFiles = @(Get-Item $sourcePathWithWildcards) + foreach ($file in $sourceFiles) + { + $copiedFilePath = Join-Path $destinationFolderPath (Split-Path $file -Leaf) + $copiedFilePath | Should Exist + (Get-Item $copiedFilePath).Length | Should Be (Get-Item $file).Length + } + } + + It "Copy-Item to session using wildcards." { + + $testObject = CreateTestDirectory + $destinationFolderPath = GetDestinationFolderPath + $sourcePathWithWildcards = "$($testObject.SourceDirectory)\A\*.txt" + Copy-Item -Path $sourcePathWithWildcards -ToSession $s -Destination $destinationFolderPath -Force + + $sourceFiles = @(Get-Item $sourcePathWithWildcards) + foreach ($file in $sourceFiles) + { + $copiedFilePath = Join-Path $destinationFolderPath (Split-Path $file -Leaf) + $copiedFilePath | Should Exist + (Get-Item $copiedFilePath).Length | Should Be (Get-Item $file).Length + } + } + } + + Context "Validate FullyQualifiedErrorIds for remote source and destination paths." { + + BeforeAll { + # Create test file. + $testFilePath = Join-Path "TestDrive:" "testfile.txt" + if (test-path $testFilePath) + { + Remove-Item $testFilePath -Force -ea SilentlyContinue + } + "File test content" | Out-File $testFilePath -Force + } + + function Test-CopyItemError + { + param ($path, $destination, $expectedFullyQualifiedErrorId, $fromSession = $false) + + if ($fromSession) + { + It "Copy-Item FromSession -Path '$path' throws $expectedFullyQualifiedErrorId" { + try + { + Copy-Item -Path $path -FromSession $s -Destination $destination -ErrorAction Stop + throw "CodeExecuted" + } + catch + { + $_.FullyQualifiedErrorId | should be $expectedFullyQualifiedErrorId + } + } + } + else + { + It "Copy-Item ToSession -Destination '$path' throws $expectedFullyQualifiedErrorId" { + try + { + Copy-Item -Path $path -ToSession $s -Destination $destination -ErrorAction Stop + throw "CodeExecuted" + } + catch + { + $_.FullyQualifiedErrorId | should be $expectedFullyQualifiedErrorId + } + } + } + } + + $invalidSourcePathtestCases = @( + @{ + Path = "HKLM:\SOFTWARE" + Destination = $env:SystemDrive + ExpectedFullyQualifiedErrorId = "NamedParameterNotFound,Microsoft.PowerShell.Commands.CopyItemCommand" + FromSession = $true + } + @{ + Path = ".\Source" + Destination = $env:SystemDrive + ExpectedFullyQualifiedErrorId = "RemotePathIsNotAbsolute,Microsoft.PowerShell.Commands.CopyItemCommand" + FromSession = $true + } + @{ + Path = $env:SystemDrive + "\X\Y\Z" + Destination = $env:SystemDrive + "\A\B\C" + ExpectedFullyQualifiedErrorId = "RemotePathNotFound,Microsoft.PowerShell.Commands.CopyItemCommand" + FromSession = $true + } + @{ + Path = $null + Destination = $env:SystemDrive + ExpectedFullyQualifiedErrorId = "ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.CopyItemCommand" + FromSession = $true + } + @{ + Path = '' + Destination = $env:SystemDrive + ExpectedFullyQualifiedErrorId = "ParameterArgumentValidationErrorEmptyStringNotAllowed,Microsoft.PowerShell.Commands.CopyItemCommand" + FromSession = $true + } + @{ + Path = "$env:SystemDrive\nonexistdir\*" + Destination = "$env:SystemDrive\psTest" + ExpectedFullyQualifiedErrorId = "RemotePathNotFound,Microsoft.PowerShell.Commands.CopyItemCommand" + FromSession = $true + } + ) + + foreach ($testCase in $invalidSourcePathtestCases) { + Test-CopyItemError @testCase + } + + $invalidDestinationPathtestCases = @( + @{ + Path = $testFilePath + Destination = ".\Source" + ExpectedFullyQualifiedErrorId = "RemotePathIsNotAbsolute,Microsoft.PowerShell.Commands.CopyItemCommand" + } + @{ + Path = $testFilePath + Destination = $env:SystemDrive + "\X\A\B\C" + ExpectedFullyQualifiedErrorId = "RemotePathNotFound,Microsoft.PowerShell.Commands.CopyItemCommand" + } + @{ + Path = $testFilePath + Destination = $null + ExpectedFullyQualifiedErrorId = "CopyItemRemoteDestinationIsNullOrEmpty,Microsoft.PowerShell.Commands.CopyItemCommand" + } + @{ + Path = $testFilePath + Destination = "" + ExpectedFullyQualifiedErrorId = "CopyItemRemoteDestinationIsNullOrEmpty,Microsoft.PowerShell.Commands.CopyItemCommand" + } + @{ + Path = "$env:SystemDrive\nonexistdir\*" + Destination = "$env:SystemDrive\psTest" + ExpectedFullyQualifiedErrorId = "PathNotFound,Microsoft.PowerShell.Commands.CopyItemCommand" + } + ) + + foreach ($testCase in $invalidDestinationPathtestCases) { + Test-CopyItemError @testCase + } + } +} + +Describe "Validate Copy-Item error for target sessions not in FullLanguageMode." -Tags "Feature" { + + BeforeAll { + + $testDirectory = "TestDrive:\" + + # Create the test file and directories. + $source = "$testDirectory\Source" + $destination = "$testDirectory\Destination" + + New-Item $source -ItemType Directory -Force | Out-Null + New-Item $destination -ItemType Directory -Force | Out-Null + + $testFilePath = Join-Path $source "testfile.txt" + "File test content" | Out-File $testFilePath -Force + + # Keep track of the sessions. + $testSessions = @{} + + # Keep track of the session names to be unregistered. + $sessionToUnregister = @() + + $languageModes = @("ConstrainedLanguage", "NoLanguage", "RestrictedLanguage") + $id = (Get-Random).ToString() + + foreach ($languageMode in $languageModes) + { + $sessionName = $languageMode + "_" + $id + $sessionToUnregister += $sessionName + $configFilePath = Join-Path $testDirectory "test.pssc" + + # Create the session. + Write-Host "Creating pssession with '$languageMode' ..." + New-PSSessionConfigurationFile -Path $configFilePath -SessionType Default -LanguageMode $languageMode + Register-PSSessionConfiguration -Name $sessionName -Path $configFilePath -Force | Out-Null + $testSession = New-PSSession -ConfigurationName $sessionName + + # Validate that the session is opened. + $testSession.State | Should Be "Opened" + + # Add the new session to the list. + $testSessions[$languageMode] = $testSession + + # Remove the pssc file. + Remove-Item $configFilePath -Force -ea SilentlyContinue + } + } + + AfterAll { + + $testSessions.Values | Remove-PSSession -ea SilentlyContinue + + $sessionToUnregister | foreach { + Unregister-PSSessionConfiguration -Name $_ -Force -ea SilentlyContinue + } + } + + foreach ($languageMode in $testSessions.Keys) + { + $session = $testSessions[$languageMode] + + It "Copy-Item throws 'SessionIsNotInFullLanguageMode' error for a session in '$languageMode'" { + + # FromSession + try + { + Copy-Item -Path $testFilePath -FromSession $session -Destination $destination -Force -Verbose -ea Stop + throw "CodeExecuted" + } + catch + { + $_.FullyQualifiedErrorId | should be "SessionIsNotInFullLanguageMode,Microsoft.PowerShell.Commands.CopyItemCommand" + } + + # ToSession + try + { + Copy-Item -Path $testFilePath -ToSession $session -Destination $destination -Force -Verbose -ea Stop + throw "CodeExecuted" + } + catch + { + $_.FullyQualifiedErrorId | should be "SessionIsNotInFullLanguageMode,Microsoft.PowerShell.Commands.CopyItemCommand" + } + } + } +} + +Describe "Copy-Item can use Recurse and Exclude together" -Tags "Feature" { + + Context "Local and Remote Tests" { + + BeforeAll { + $s = New-PSSession -ComputerName . -ea SilentlyContinue + if (-not $s) + { + throw "Failed to create PSSession for remote copy operations." + } + + $null = New-Item -ItemType Directory -Path "TestDrive:\Parent\Sub" + $null = New-Item -Path "TestDrive:\Parent\p1.txt" -Value "test" + $null = New-Item -Path "TestDrive:\Parent\p2.txt" -Value "test" + $null = New-Item -Path "TestDrive:\Parent\s4.txt" -Value "test" + $null = New-Item -Path "TestDrive:\Parent\Sub\s1.txt" -Value "test" + $null = New-Item -Path "TestDrive:\Parent\Sub\s2.txt" -Value "test" + $null = New-Item -Path "TestDrive:\Parent\Sub\s3.txt" -Value "test" + $null = New-Item -Path "TestDrive:\Parent\Sub\p3.txt" -Value "testcl" + } + + It "can exclude files at sub directory" { + Copy-Item -Path TestDrive:\Parent\* -Recurse -Exclude s*.txt -Destination TestDrive:\Temp -Force + $copiedFiles = Get-ChildItem -Recurse -Path TestDrive:\Temp + $copiedFiles.Count | Should Be 3 + } + + It "can exclude files at sub directory to a session" { + Copy-Item -Path TestDrive:\Parent\* -Recurse -Exclude s*.txt -Destination $TestDrive\Temp2 -Force -ToSession $s + $copiedFiles = Get-ChildItem -Recurse -Path TestDrive:\Temp + $copiedFiles.Count | Should Be 3 + } + + It "can exclude files at sub directory from a session" { + Copy-Item -Path $TestDrive\Parent\* -Recurse -Exclude s*.txt -Destination TestDrive:\Temp3 -FromSession $s + $copiedFiles = Get-ChildItem -Recurse -Path TestDrive:\Temp2 + $copiedFiles.Count | Should Be 3 + } + + AfterAll { + Remove-PSSession -Session $s -ErrorAction SilentlyContinue + } + } +} + +Describe "Copy-Item remotely bug fixes" -Tags "Feature" { + + BeforeAll { + $s = New-PSSession -ComputerName . -ErrorAction SilentlyContinue + if (-not $s) + { + throw "Failed to create PSSession for remote copy operations." + } + + $originalContent = "test file 1 - Source" + $newContent = "This is some new content" + + $null = New-Item -ItemType Directory -Path "TestDrive:\Source" + $null = New-Item -ItemType Directory -Path "TestDrive:\Destination" + } + + AfterAll { + Remove-PSSession -Session $s -ErrorAction SilentlyContinue + } + + BeforeEach { + + # Create the same file in the source and destination + Set-Content -Path "TestDrive:\Source\testFile1.txt" -Value $originalContent -Force + Set-Content -Path "TestDrive:\Destination\testFile1.txt" -Value $originalContent -Force + } + + Context "Copy-Item remotely overwrites a destination file if it exists." { + + BeforeEach { + + # Overwrite the source file + Set-Content -Path "TestDrive:\Source\testFile1.txt" -Value $newContent + } + + It "Copy item -tosession overwrites the content of an existing file." { + + # Copy file to session + Copy-Item -Path "TestDrive:\Source\testFile1.txt" -Destination "$TestDrive\Destination\testFile1.txt" -ToSession $s + + # Validate the the file was overwritten + $fileContent = Get-Content "TestDrive:\Destination\testFile1.txt" -ea SilentlyContinue -Raw + $fileContent | Should Match $newContent + } + + It "Copy item -fromsession overwrites the content of an existing file." { + + # Copy file to session + Copy-Item -Path "$TestDrive\Source\testFile1.txt" -Destination "TestDrive:\Destination\testFile1.txt" -FromSession $s + + # Validate the the file was overwritten + $fileContent = Get-Content "TestDrive:\Destination\testFile1.txt" -ea SilentlyContinue -Raw + $fileContent | Should Match $newContent + } + } + + Context "Copy-Item remotely creates a destination file if it does not exist." { + + BeforeEach { + + if (Test-Path "TestDrive:\AnotherDestination") + { + Remove-Item "TestDrive:\AnotherDestination" -Force -Recurse -ea SilentlyContinue + } + $null = New-Item -ItemType Directory -Path "TestDrive:\AnotherDestination" + + # Ensure the file does not exist + "TestDrive:\AnotherDestination\FileThatDoesNotExist.txt" | Should Not Exist + } + + It "Copy-Item -tosession creates the file if it does not exist on the remote destination." { + + # Copy file to session + Copy-Item -Path "TestDrive:\Source\testFile1.txt" -Destination "$TestDrive\AnotherDestination\FileThatDoesNotExist.txt" -ToSession $s + + # Verify that the file was created + "TestDrive:\AnotherDestination\FileThatDoesNotExist.txt" | Should Exist + } + + It "Copy-Item -fromsession creates the file if it does not exist on the local machine." { + + # Copy file from session + Copy-Item -Path "$TestDrive\Source\testFile1.txt" -Destination "TestDrive:\AnotherDestination\FileThatDoesNotExist.txt" -FromSession $s + + # Verify that the file was created + "TestDrive:\AnotherDestination\FileThatDoesNotExist.txt" | Should Exist + } + } +} diff --git a/test/powershell/Modules/Microsoft.PowerShell.Management/Pester.Commands.Cmdlets.NoNewlineParameter.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Management/Pester.Commands.Cmdlets.NoNewlineParameter.Tests.ps1 new file mode 100644 index 000000000..1be6c38dd --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Management/Pester.Commands.Cmdlets.NoNewlineParameter.Tests.ps1 @@ -0,0 +1,26 @@ +# Tests related to TFS item 1370133 [PSUpgrade] Need -NoNewline parameter on Out-File, Add-Content and Set-Content +# Connect request https://connect.microsoft.com/PowerShell/feedback/details/524739/need-nonewline-parameter-on-out-file-add-content-and-set-content + +Describe "Tests for -NoNewline parameter of Out-File, Add-Content and Set-Content" -tags "Feature" { + + It "NoNewline parameter works on Out-File" { + $temp = New-TemporaryFile + 1..5 | Out-File $temp.FullName -Encoding 'ASCII' -NoNewline + (Get-Content $temp -Encoding Byte).Count | Should Be 5 + Remove-Item $temp -ErrorAction SilentlyContinue -Force + } + + It "NoNewline parameter works on Set-Content" { + $temp = New-TemporaryFile + Set-Content -Path $temp.FullName -Value 'a','b','c' -Encoding 'ASCII' -NoNewline + (Get-Content $temp -Encoding Byte).Count | Should Be 3 + Remove-Item $temp -ErrorAction SilentlyContinue -Force + } + + It "NoNewline parameter works on Add-Content" { + $temp = New-TemporaryFile + 1..9 | %{Add-Content -Path $temp.FullName -Value $_ -Encoding 'ASCII' -NoNewline} + (Get-Content $temp -Encoding Byte).Count | Should Be 9 + Remove-Item $temp -ErrorAction SilentlyContinue -Force + } +} diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/FormatHex.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/FormatHex.Tests.ps1 new file mode 100644 index 000000000..f275a4524 --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/FormatHex.Tests.ps1 @@ -0,0 +1,221 @@ +# This is a Pester test suite to validate the Format-Hex cmdlet in the Microsoft.PowerShell.Utility module. +# +# Copyright (c) Microsoft Corporation, 2015 +# + +<# + Purpose: + Verify that Format-Hex display the Hexa decmial value for the input data. + + Action: + Run Format-Fex. + + Expected Result: + Hexa decimal equivalent of the input data is displayed. +#> + +Describe "FormatHex" -tags "CI" { + BeforeAll { + Setup -d FormatHexDataDir + $inputText1 = 'Hello World' + $inputText2 = 'This is a bit more text' + $inputFile1 = setup -f "FormatHexDataDir/SourceFile-1.txt" -content $inputText1 -pass + $inputFile2 = setup -f "FormatHexDataDir/SourceFile-2.txt" -content $inputText2 -pass + } + + # This test is to validate to pipeline support in Format-Hex cmdlet. + It "ValidatePipelineSupport" { + + # InputObject Parameter set should get invoked and + # the input data should be treated as string. + $result = $inputText1 | Format-Hex + $result | Should Not Be $null + $result.GetType().Name | Should Be 'ByteCollection' + $actualResult = $result.ToString() + ($actualResult -match $inputText1) | Should Be $true + } + + # This test is to validate to pipeline support in Format-Hex cmdlet. + It "ValidateByteArrayInputSupport" { + + # InputObject Parameter set should get invoked and + # the input data should be treated as byte[]. + $inputBytes = [System.Text.Encoding]::ASCII.GetBytes($inputText1) + + $result = Format-Hex -InputObject $inputBytes + $result | Should Not Be $null + $result.GetType().Name | Should Be 'ByteCollection' + $actualResult = $result.ToString() + ($actualResult -match $inputText1) | Should Be $true + } + + # This test is to validate to input given through Path paramter set in Format-Hex cmdlet. + It "ValidatePathParameterSet" { + + $result = Format-Hex -Path $inputFile1 + $result | Should Not Be $null + $result.GetType().Name | Should Be 'ByteCollection' + $actualResult = $result.ToString() + ($actualResult -match $inputText1) | Should Be $true + } + + # This test is to validate to Path paramter set is considered as default in Format-Hex cmdlet. + It "ValidatePathAsDefaultParameterSet" { + + $result = Format-Hex $inputFile1 + $result | Should Not BeNullOrEmpty + $result.GetType().Name | Should Be 'ByteCollection' + $actualResult = $result.ToString() + ($actualResult -match $inputText1) | Should Be $true + } + + # This test is to validate to input given through LiteralPath paramter set in Format-Hex cmdlet. + It "ValidateLiteralPathParameterSet" { + + $result = Format-Hex -LiteralPath $inputFile1 + $result | Should Not BeNullOrEmpty + $result.GetType().Name | Should Be 'ByteCollection' + $actualResult = $result.ToString() + ($actualResult -match $inputText1) | Should Be $true + } + + # This test is to validate to input given through pipeline. The input being piped from results of Get-hildItem + It "ValidateFileInfoPipelineInput" { + + $result = Get-ChildItem $inputFile1 | Format-Hex + $result | Should Not BeNullOrEmpty + $result.GetType().Name | Should Be 'ByteCollection' + $actualResult = $result.ToString() + ($actualResult -match $inputText1) | Should Be $true + } + + # This test is to validate Encoding formats functionality of Format-Hex cmdlet. + It "ValidateEncodingFormats" { + + $result = Format-Hex -InputObject $inputText1 -Encoding ASCII + $result | Should Not BeNullOrEmpty + $result.GetType().Name | Should Be 'ByteCollection' + $actualResult = $result.ToString() + ($actualResult -match $inputText1) | Should Be $true + } + + # This test is to validate that integers can be piped to the format-hex + It "ValidateIntegerInput" { + + $result = 1,2,3,4 | Format-Hex + $result | Should Not BeNullOrEmpty + $result.GetType().Name | Should Be 'ByteCollection' + $actualResult = $result.ToString() + # whitespace sensitive + $actualResult | should be "00000000 01 02 03 04 .... " + } + + # This test is to validate that integers can be piped to the format-hex + # and properly represented as characters in the string + It "ValidateIntegerInputThatPresentAsCharacters" { + + $result = 65..68 | Format-Hex + $result | Should Not BeNullOrEmpty + $result.GetType().Name | Should Be 'ByteCollection' + $actualResult = $result.ToString() + # whitespace sensitive + $actualResult | should be "00000000 41 42 43 44 ABCD " + } + + # This test is to validate that integers can be piped to the format-hex + It "ValidateIntegerRawInput" { + + $result = 1,2,3,4 | Format-Hex -Raw + $result | Should Not BeNullOrEmpty + $result.GetType().Name | Should Be 'ByteCollection' + $actualResult = $result.ToString() + # whitespace sensitive + $actualResult | should be "00000000 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 ................" + } + + # handle int64 + It "ValidateInteger64" { + + $result = [int64]::MaxValue | Format-Hex + $result | Should Not BeNullOrEmpty + $result.GetType().Name | Should Be 'ByteCollection' + $actualResult = $result.ToString() + # whitespace sensitive + $actualResult | Should Match "00000000 FF FF FF FF FF FF FF 7F ." + } + + # handle bytes, int16, int32, and int64 + It "Validate combined and reduced number formatting" { + $b = 65 # fits in a byte + $i16 = 32767 # fits in an int16 + $i32 = 2147483647 # an int32 + $i64 = 9223372036854775807 # an int64 + $result = $b,$i16,$i32,$i64 | format-Hex + $result.GetType().Name | should be 'ByteCollection' + $actualResult = $result.ToString() + $actualResult | should Match "00000000 41 FF 7F FF FF FF 7F FF FF FF FF FF FF FF 7F A" + } + + # handle bytes, int16, int32, and int64 + It "Validate combined and with raw number formatting" { + $b = 65 # fits in a byte + $i16 = 32767 # fits in an int16 + $i32 = 2147483647 # an int32 + $i64 = 9223372036854775807 # an int64 + # this will cause 2 lines to be emitted + $result = $b,$i16,$i32,$i64 | format-Hex -Raw + $result[0].GetType().Name | should be 'ByteCollection' + $result[1].GetType().Name | should be 'ByteCollection' + $result0 = $result[0].ToString() + $result0 | should match "00000000 41 00 00 00 FF 7F 00 00 FF FF FF 7F FF FF FF FF A" + $result1 = $result[1].ToString() + $result1 | should match "00000010 FF FF FF 7F .." + } + + # This test is to validate that streamed text does not have buffer underrun problems + It "ValidateEachBufferHasCorrectContentForStreamingText" { + $result = "a" * 30 | Format-Hex + $result | Should Not BeNullOrEmpty + $result.GetType().Name | Should Be 'ByteCollection' + $actualResult = $result.ToString() -split "`r`n" + $actualResult.Count | should be 2 + $actualResult[0].ToString() | Should be "00000000 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 aaaaaaaaaaaaaaaa" + $actualResult[1].ToString() | Should be "00000010 61 61 61 61 61 61 61 61 61 61 61 61 61 61 aaaaaaaaaaaaaa " + } + + # This test is to validate that files do not have buffer underrun problems + It "ValidateEachBufferHasCorrectContentForFiles" { + $result = Format-Hex -path $InputFile2 + $result | Should Not BeNullOrEmpty + $result.Count | should be 2 + $result[0].ToString() | Should be "00000000 54 68 69 73 20 69 73 20 61 20 62 69 74 20 6D 6F This is a bit mo" + if ( $IsCoreCLR ) { + $result[1].ToString() | Should be "00000010 72 65 20 74 65 78 74 re text " + } + } + + # This test ensures that if we stream bytes from a file, the output is correct + It "ValidateStreamOfBytesFromFileHasProperOutput" { + $result = Get-Content $InputFile1 -Encoding Byte | Format-Hex + $result | Should Not BeNullOrEmpty + $result.GetType().Name | Should Be "ByteCollection" + if ( $IsCoreCLR ) { + $result.ToString() | Should be "00000000 48 65 6C 6C 6F 20 57 6F 72 6C 64 Hello World " + } + } + + # This test is to validate the alias for Format-Hex cmdlet. + It "ValidateCmdletAlias" { + + try + { + $result = Get-Command fhx -ErrorAction Stop + $result | Should Not BeNullOrEmpty + $result.CommandType.ToString() | Should Be "Alias" + } + catch + { + $_ | Should BeNullOrEmpty + } + } +} diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/ImportExportCSV.Delimiter.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/ImportExportCSV.Delimiter.Tests.ps1 new file mode 100644 index 000000000..fcc8c9a6e --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/ImportExportCSV.Delimiter.Tests.ps1 @@ -0,0 +1,84 @@ +Describe "Using delimiters with Export-CSV and Import-CSV behave correctly" -tags "Feature" { + BeforeAll { + # note, we will not use "," as that's the default for CSV + $delimiters = "/", " ", "@", "#", "$", "\", "&", "(", ")", + "{", "}", "|", "<", ">", ";", "'", + '"', "~", "!", "%", "^", "*", "_", "+", ":", + "?", "-", "=", "[", "]", "." + $defaultDelimiter = [System.Globalization.CultureInfo]::CurrentCulture.TextInfo.ListSeparator + # With CORECLR the CurrentCulture.TextInfo.ListSeparator is not writable, so + # we need to use an entirely new CultureInfo which we can modify + $enCulture = [System.Globalization.CultureInfo]::new("en-us") + $d = get-date + $testCases = @( + foreach($del in $delimiters) + { + @{ Delimiter = $del; Data = $d; ExpectedResult = $d.Ticks } + } + ) + function Set-delimiter { + param ( $delimiter ) + if ( $IsCoreCLR ) { + $enCulture.TextInfo.ListSeparator = $delimiter + [System.Globalization.CultureInfo]::CurrentCulture = $enCulture + } + else { + [System.Globalization.cultureInfo]::CurrentCulture.TextInfo.ListSeparator = $delimiter + } + } + } + AfterEach { + if ( $IsCoreCLR ) { + $enCulture.TextInfo.ListSeparator = $defaultDelimiter + [System.Globalization.CultureInfo]::CurrentCulture = $enCulture + } + else { + [System.Globalization.CultureInfo]::CurrentCulture.TextInfo.ListSeparator = $defaultDelimiter + } + remove-item -force -ea silentlycontinue TESTDRIVE:/file.csv + } + + It "Disallow use of null delimiter" { + $d | export-csv TESTDRIVE:/file.csv + { import-csv -path TESTDRIVE:/file.csv -delimiter $null } | Should Throw "Delimiter" + } + + It "Disallow use of delimiter with useCulture parameter" { + $d | export-csv TESTDRIVE:/file.csv + { import-csv -path TESTDRIVE:/file.csv -useCulture "," } | Should Throw "','" + } + + It "Imports the same properties as exported" { + $a = [pscustomobject]@{ a = 1; b = 2; c = 3 } + $a | export-Csv TESTDRIVE:/file.csv + $b = import-csv TESTDRIVE:/file.csv + @($b.psobject.properties).count | should be 3 + $b.a | Should be $a.a + $b.b | Should be $a.b + $b.c | Should be $a.c + } + + # parameter generated tests + It 'Delimiter with CSV import will fail correctly when culture does not match' -testCases $testCases { + param ($delimiter, $Data, $ExpectedResult) + set-Delimiter $delimiter + $Data | export-CSV TESTDRIVE:\File.csv -useCulture + $i = Import-CSV TESTDRIVE:\File.csv + $i.Ticks | Should Not Be $ExpectedResult + } + + It 'Delimiter with CSV import will succeed when culture matches export' -testCases $testCases { + param ($delimiter, $Data, $ExpectedResult) + set-Delimiter $delimiter + $Data | export-CSV TESTDRIVE:\File.csv -useCulture + $i = Import-CSV TESTDRIVE:\File.csv -useCulture + $i.Ticks | Should Be $ExpectedResult + } + + It 'Delimiter with CSV import will succeed when delimiter is used explicitly' -testCases $testCases { + param ($delimiter, $Data, $ExpectedResult) + $Data | export-CSV TESTDRIVE:\File.csv -delimiter $delimiter + $i = Import-CSV TESTDRIVE:\File.csv -delimiter $delimiter + $i.Ticks | Should Be $ExpectedResult + } +} diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/MiscCmdletUpdates.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/MiscCmdletUpdates.Tests.ps1 new file mode 100644 index 000000000..c7a1f73f5 --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/MiscCmdletUpdates.Tests.ps1 @@ -0,0 +1,54 @@ +Describe "GetDateFormatUpdates" -Tags "Feature" { + + It "Verifies that FileDate format works" { + $date = Get-Date + $expectedFormat = "{0:yyyyMMdd}" -f $date + $actualFormat = Get-Date -Date $date -Format FileDate + + $actualFormat | Should be $expectedFormat + } + + It "Verifies that FileDateUniversal format works" { + $date = (Get-Date).ToUniversalTime() + $expectedFormat = "{0:yyyyMMddZ}" -f $date + $actualFormat = Get-Date -Date $date -Format FileDateUniversal + + $actualFormat | Should be $expectedFormat + } + + It "Verifies that FileDateTime format works" { + $date = Get-Date + $expectedFormat = "{0:yyyyMMddTHHmmssffff}" -f $date + $actualFormat = Get-Date -Date $date -Format FileDateTime + + $actualFormat | Should be $expectedFormat + } + + It "Verifies that FileDateTimeUniversal format works" { + $date = (Get-Date).ToUniversalTime() + $expectedFormat = "{0:yyyyMMddTHHmmssffffZ}" -f $date + $actualFormat = Get-Date -Date $date -Format FileDateTimeUniversal + + $actualFormat | Should be $expectedFormat + } + +} + +Describe "GetRandomMiscTests" -Tags "Feature" { + It "Shouldn't overflow when using max range" { + + $hadError = $false + + try + { + ## Don't actually need to validate + Get-Random -Minimum ([Int32]::MinValue) -Maximum ([Int32]::MaxValue) -ErrorAction Stop + } + catch + { + $hadError = $true + } + + $hadError | Should be $false + } +} diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/NewGuid.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/NewGuid.Tests.ps1 new file mode 100644 index 000000000..95fc37cf0 --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/NewGuid.Tests.ps1 @@ -0,0 +1,19 @@ +Describe "New-Guid" -Tags "CI" { + + It "returns a new guid" { + $guid = New-Guid + $guid.GetType().FullName | Should Be "System.Guid" + } + + It "should not be all zeros" { + $guid = New-Guid + $guid.ToString() | Should Not Be "00000000-0000-0000-0000-000000000000" + } + + It "should return different guids with each call" { + $guid1 = New-Guid + $guid2 = New-Guid + $guid1.ToString() | Should Not Be $guid2.ToString() + } +} + diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/NewTemporaryFile.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/NewTemporaryFile.Tests.ps1 new file mode 100644 index 000000000..87bbefb3b --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/NewTemporaryFile.Tests.ps1 @@ -0,0 +1,38 @@ +# This is a Pester test suite to validate the New-TemporaryFile cmdlet in the Microsoft.PowerShell.Utility module. +# +# Copyright (c) Microsoft Corporation, 2015 +# + +<# + Purpose: + Verify that New-TemporaryFile creates a temporary file. + + Action: + Run New-TemporaryFile. + + Expected Result: + A FileInfo object for the temporary file is returned. +#> + +Describe "NewTemporaryFile" -Tags "CI" { + + It "creates a new temporary file" { + $tempFile = New-TemporaryFile + + Test-Path $tempFile | Should be $true + $tempFile.GetType().Name | Should be "FileInfo" + + if(Test-Path $tempFile) + { + Remove-Item $tempFile -ErrorAction SilentlyContinue -Force + } + } + + It "with WhatIf does not create a file" { + New-TemporaryFile -WhatIf | Should Be $null + } + + It "has an OutputType of System.IO.FileInfo" { + (Get-Command New-TemporaryFile).OutputType | Should Be "System.IO.FileInfo" + } +} diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Pester.Commands.Cmdlets.Json.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Pester.Commands.Cmdlets.Json.Tests.ps1 new file mode 100644 index 000000000..0c180752c --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Pester.Commands.Cmdlets.Json.Tests.ps1 @@ -0,0 +1,1434 @@ +# +# Copyright (c) Microsoft Corporation, 2015 +# +# This is a Pester test suite which validate the Json cmdlets. +# + +# +# This 'Describe' is for tests that were converted from utscripts (SDXROOT/admin/monad/tests/monad/DRT/utscripts) +# and C# tests (SDXROOT/admin/monad/tests/monad/DRT/commands/utility/UnitTests) to Pester. +# +Describe "Json Tests" -Tags "Feature" { + + BeforeAll { + + function ValidateSampleObject + { + param ($result, [switch]$hasEmbeddedSampleObject ) + + Write-Verbose "validating deserialized SampleObject" -Verbose + $result.SampleInt | Should Be 98765 + $result.SampleString | Should Match "stringVal" + $result.SampleArray.Count | Should Be 2 + $result.SampleTrue | Should Be $true + $result.SampleFalse | Should Be $false + $result.SampleNull | Should Be $null + $result.SampleFloat | Should Be 9.8765E+43 + + if ($hasEmbeddedSampleObject) + { + Write-Verbose "validating deserialized Embedded SampleObject" -Verbose + ValidateSampleObject -result $result.SampleObject + } + } + + } + + Context "ConvertTo-Json Bug Fixes" { + + It "ConvertTo-JSON should not have hard coded english error message" { + + # Test follow-up for bug WinBlue: 163372 - ConvertTo-JSON has hard coded english error message. + $process = Get-Process -Id $PID + $hash = @{ $process = "def" } + $expectedFullyQualifiedErrorId = "NonStringKeyInDictionary,Microsoft.PowerShell.Commands.ConvertToJsonCommand" + + try + { + ConvertTo-Json -InputObject $hash + throw "CodeExecuted" + } + catch + { + $_.FullyQualifiedErrorId | should be $expectedFullyQualifiedErrorId + } + } + + It "ConvertTo-Json should handle terms with double quotes" { + + # Test follow-up for bug WinBlue: 11484 - ConvertTo-Json can't handle terms with double quotes. + + $notcompressed = ConvertTo-JSON @{ FirstName = 'Hello " World' } + $compressed = ConvertTo-Json @{ FirstName = 'Hello " World' } -Compress + $valueFromNotCompressedResult = ConvertFrom-Json -InputObject $notcompressed + $valueFromCompressedResult = ConvertFrom-Json -InputObject $compressed + + $valueFromNotCompressedResult.FirstName | Should Match $valueFromCompressedResult.FirstName + } + + It "Convertto-Json should handle Enum based on Int64" { + + # Test follow-up for bug Win8: 378368 Convertto-Json problems with Enum based on Int64. + if ( ("JsonEnumTest" -as "Type") -eq $null ) { + $enum1 = "TestEnum" + (get-random) + $enum2 = "TestEnum" + (get-random) + $enum3 = "TestEnum" + (get-random) + + $jsontype = add-type -pass -TypeDef " + public enum $enum1 : ulong { One = 1, Two = 2 }; + public enum $enum2 : long { One = 1, Two = 2 }; + public enum $enum3 : int { One = 1, Two = 2 }; + public class JsonEnumTest { + public $enum1 TestEnum1 = ${enum1}.One; + public $enum2 TestEnum2 = ${enum2}.Two; + public $enum3 TestEnum3 = ${enum3}.One; + }" + } + $op = [JsonEnumTest]::New() | convertto-json | convertfrom-json + $op.TestEnum1 | Should Be "One" + $op.TestEnum2 | Should Be "Two" + $op.TestEnum3 | Should Be 1 + } + + It "Test followup for Windows 8 bug 121627" { + + $JsonString = Get-Command Get-help |Select-Object Name, Noun, Verb| ConvertTo-Json + $actual = ConvertFrom-Json $JsonString + + $actual.Name | Should Be "Get-Help" + $actual.Noun | Should Be "Help" + $actual.Verb | Should Be "Get" + } + } + + Context "ConvertFrom and ConvertTo on JsonObject Tests" { + + It "Convert dictionary to PSObject" { + + $response = ConvertFrom-Json '{"d":{"__type":"SimpleJsonObject","Name":{"First":"Joel","Last":"Wood"},"Greeting":"Hello"}}' + $response.d.Name.First | Should Match "Joel" + } + + It "Convert to Json using PSObject" -pending:($IsCoreCLR) { + + $response = ConvertFrom-Json '{"d":{"__type":"SimpleJsonObject","Name":{"First":"Joel","Last":"Wood"},"Greeting":"Hello"}}' + + $response2 = ConvertTo-Json -InputObject $response -ErrorAction Continue + $response2 = ConvertTo-Json -InputObject $response -ErrorAction Inquire + $response2 = ConvertTo-Json -InputObject $response -ErrorAction SilentlyContinue + $response2 = ConvertTo-Json -InputObject $response -Depth 2 -Compress + $response2 | Should Be '{"d":{"Name":{"First":"Joel","Last":"Wood"},"Greeting":"Hello"}}' + + $response2 = ConvertTo-Json -InputObject $response -Depth 1 -Compress + $nameString = [System.Management.Automation.LanguagePrimitives]::ConvertTo($response.d.Name, [string]) + $response2 | Should Be "{`"d`":{`"Name`":`"$nameString`",`"Greeting`":`"Hello`"}}" + + $result1 = @" +{ + "d": { + "Name": { + "First": "Joel", + "Last": "Wood" + }, + "Greeting": "Hello" + } +} +"@ + $response2 = ConvertTo-Json -InputObject $response -Depth 2 + $response2 | Should Match $result1 + + $result2 = @" +{ + "d": { + "Name": "$nameString", + "Greeting": "Hello" + } +} +"@ + $response2 = ConvertTo-Json -InputObject $response -Depth 1 + $response2 | Should Match $result2 + + $arraylist = new-Object System.Collections.ArrayList + [void]$arraylist.Add("one") + [void]$arraylist.Add("two") + [void]$arraylist.Add("three") + $response2 = ConvertTo-Json -InputObject $arraylist -Compress + $response2 | Should Be '["one","two","three"]' + + $result3 = @" +[ + "one", + "two", + "three" +] +"@ + $response2 = ConvertTo-Json -InputObject $arraylist + $response2 | Should Be $result3 + + $response2 = $arraylist | ConvertTo-Json + $response2 | Should Be $result3 + } + + It "Convert to Json using hashtable" -pending:($IsCoreCLR) { + + $nameHash = @{First="Joe1";Last="Wood"} + $dHash = @{Name=$nameHash; Greeting="Hello"} + $rootHash = @{d=$dHash} + $response3 = ConvertTo-Json -InputObject $rootHash -Depth 2 -Compress + $response3 | Should Be '{"d":{"Greeting":"Hello","Name":{"Last":"Wood","First":"Joe1"}}}' + + $response3 = ConvertTo-Json -InputObject $rootHash -Depth 1 -Compress + $response3 | Should Be '{"d":{"Greeting":"Hello","Name":"System.Collections.Hashtable"}}' + + $result4 = @" +{ + "d": { + "Greeting": "Hello", + "Name": { + "Last": "Wood", + "First": "Joe1" + } + } +} +"@ + $response3 = ConvertTo-Json -InputObject $rootHash -Depth 2 + $response3 | Should Be $result4 + + $result5 = @" +{ + "d": { + "Greeting": "Hello", + "Name": "System.Collections.Hashtable" + } +} +"@ + $response3 = ConvertTo-Json -InputObject $rootHash -Depth 1 + $response3 | Should Be $result5 + } + + It "Convert from Json allows an empty string" { + + $emptyStringResult = ConvertFrom-Json "" + $emptyStringResult | Should Be $null + } + } + + Context "JsonObject Tests" { + + It "AddMember on JsonObject" { + + # create a Version object + $versionObject = New-Object System.Version 2, 3, 4, 14 + + # add a NoteProperty member called Note with a text note + $versionObject | Add-Member -MemberType NoteProperty -Name Note -Value "a version object" + + # add an AliasProperty called Rev as an alias to the Revison property + $versionObject | Add-Member -MemberType AliasProperty -Name Rev -Value Revision + + # add a ScriptProperty called IsOld which returns whether the version is an older version + $versionObject | Add-Member -MemberType ScriptProperty -Name IsOld -Value { ($this.Major -le 3) } + + $jstr = ConvertTo-Json $versionObject + + # convert the JSON string to a JSON object + $json = ConvertFrom-Json $jstr + + # Check the basic properties + $json.Major | Should Be 2 + $json.Minor | Should Be 3 + $json.Build | Should Be 4 + $json.Revision | Should Be 14 + $json.Note | Should Match "a version object" + + # Check the AliasProperty + $json.Rev | Should Be $json.Revision + + # Check the ScriptProperty + $json.IsOld | Should Be $true + } + + It "ConvertFrom-Json with a key value pair" { + + $json = "{name:1}" + $result = ConvertFrom-Json $json + $result.name | Should Be 1 + } + + It "ConvertFrom-Json with a simple array" { + + $json = "[1,2,3,4,5,6]" + $result = ConvertFrom-Json $json + $result.Count | Should Be 6 + $result.GetType().BaseType.fullname | Should Be "System.Array" + } + + It "ConvertFrom-Json with a float value" { + + $json = '{"SampleFloat1":1.2345E67, "SampleFloat2":-7.6543E-12}' + $result = ConvertFrom-Json $json + + $sampleFloat1 = Invoke-Expression 1.2345E67 + $result.SampleFloat1 | Should Be $sampleFloat1 + + $sampleFloat2 = Invoke-Expression -7.6543E-12 + $result.SampleFloat2 | Should Be $sampleFloat2 + } + + It "ConvertFrom-Json hash table nested in array" { + + $json = "['one', 'two', {'First':1,'Second':2,'Third':['Five','Six', 'Seven']}, 'four']" + $result = ConvertFrom-Json $json + + $result.Count | Should Be 4 + $result[0] | Should Be "one" + $result[1] | Should Be "two" + $result[3] | Should Be "four" + + $hash = $result[2] + $hash.First | Should Be 1 + $hash.Second | Should Be 2 + $hash.Third.Count | Should Be 3 + $hash.Third[0] | Should Be "Five" + $hash.Third[1] | Should Be "Six" + $hash.Third[2] | Should Be "Seven" + } + + It "ConvertFrom-Json array nested in hash table" { + + $json = '{"First":["one", "two", "three"], "Second":["four", "five"], "Third": {"blah": 4}}' + $result = ConvertFrom-Json $json + + $result.First.Count | Should Be 3 + $result.First[0] | Should Be "one" + $result.First[1] | Should Be "two" + $result.First[2] | Should Be "three" + + $result.Second.Count | Should Be 2 + $result.Second[0] | Should Be "four" + $result.Second[1] | Should Be "five" + + $result.Third.blah | Should Be "4" + } + + It "ConvertFrom-Json case insensitive test" { + + $json = '{"sAMPleValUE":12345}' + $result = ConvertFrom-Json $json + + $result.SampleValue | Should Be 12345 + } + + + It "ConvertFrom-Json sample values" { + + $json = '{"SampleInt":98765, "SampleString":"stringVal","SampleArray":[2,"two"], "SampleTrue":true, "SampleFalse":false,"SampleNull":null, "SampleFloat":9.8765E43}' + $result = ConvertFrom-Json $json + + # Validate the result object + ValidateSampleObject -result $result + + + $json = '{"SampleInt":98765, "SampleString":"stringVal","SampleArray":[2,"two"], "SampleTrue":true, ' + + '"SampleFalse":false,"SampleNull":null, "SampleFloat":9.8765E43, "SampleObject":'+ + '{"SampleInt":98765, "SampleString":"stringVal","SampleArray":[2,"two"], '+ + '"SampleTrue":true, "SampleFalse":false,"SampleNull":null, "SampleFloat":9.8765E43}}' + + # Validate the result object + $result = ConvertFrom-Json $json + ValidateSampleObject -result $result -hasEmbeddedSampleObject + } + + It "ConvertFrom-Json with special characters" { + + $json = '{"SampleValue":"\"\\\b\f\n\r\t\u4321\uD7FF"}' + $result = ConvertFrom-Json $json + $result.SampleValue[0] | Should Be '"' + $result.SampleValue[1] | Should Be '\' + $result.SampleValue[2] | Should Be 0x8 + $result.SampleValue[3] | Should Be 0xC + $result.SampleValue[4] | Should Be 0xA + $result.SampleValue[5] | Should Be 0xD + $result.SampleValue[6] | Should Be 0x9 + $result.SampleValue[7] | Should Be 0x4321 + $result.SampleValue[8] | Should Be 0xD7FF + } + } +} + +# This Describe is for new Json tests +# +Describe "Validate Json serialization" -Tags "CI" { + + Context "Validate Json serialization ascii values" { + + $testCases = @( + @{ + TestInput = 0 + ToJson = if ( $IsCoreCLR ) { '"\u0000"' } else { 'null' } + FromJson = '' + } + @{ + TestInput = 1 + ToJson = '"\u0001"' + FromJson = '' + } + @{ + TestInput = 2 + ToJson = '"\u0002"' + FromJson = '' + } + @{ + TestInput = 3 + ToJson = '"\u0003"' + FromJson = '' + } + @{ + TestInput = 4 + ToJson = '"\u0004"' + FromJson = '' + } + @{ + TestInput = 5 + ToJson = '"\u0005"' + FromJson = '' + } + @{ + TestInput = 6 + ToJson = '"\u0006"' + FromJson = '' + } + @{ + TestInput = 7 + ToJson = '"\u0007"' + FromJson = '' + } + @{ + TestInput = 8 + ToJson = '"\b"' + FromJson = '' + } + @{ + TestInput = 9 + ToJson = '"\t"' + FromJson = ' ' + } + @{ + TestInput = 10 + ToJson = '"\n"' + FromJson = "`n" + } + @{ + TestInput = 11 + ToJson = '"\u000b"' + FromJson = ' ' + } + @{ + TestInput = 12 + ToJson = '"\f"' + FromJson = ' ' + } + @{ + TestInput = 13 + ToJson = '"\r"' + FromJson = '' + } + @{ + TestInput = 14 + ToJson = '"\u000e"' + FromJson = '' + } + @{ + TestInput = 15 + ToJson = '"\u000f"' + FromJson = '' + } + @{ + TestInput = 16 + ToJson = '"\u0010"' + FromJson = '' + } + @{ + TestInput = 17 + ToJson = '"\u0011"' + FromJson = '' + } + @{ + TestInput = 18 + ToJson = '"\u0012"' + FromJson = '' + } + @{ + TestInput = 19 + ToJson = '"\u0013"' + FromJson = '' + } + @{ + TestInput = 20 + ToJson = '"\u0014"' + FromJson = '' + } + @{ + TestInput = 21 + ToJson = '"\u0015"' + FromJson = '' + } + @{ + TestInput = 22 + ToJson = '"\u0016"' + FromJson = '' + } + @{ + TestInput = 23 + ToJson = '"\u0017"' + FromJson = '' + } + @{ + TestInput = 24 + ToJson = '"\u0018"' + FromJson = '' + } + @{ + TestInput = 25 + ToJson = '"\u0019"' + FromJson = '' + } + @{ + TestInput = 26 + ToJson = '"\u001a"' + FromJson = '' + } + @{ + TestInput = 27 + ToJson = '"\u001b"' + FromJson = '' + } + @{ + TestInput = 28 + ToJson = '"\u001c"' + FromJson = '' + } + @{ + TestInput = 29 + ToJson = '"\u001d"' + FromJson = '' + } + @{ + TestInput = 30 + ToJson = '"\u001e"' + FromJson = '' + } + @{ + TestInput = 31 + ToJson = '"\u001f"' + FromJson = '' + } + @{ + TestInput = 32 + ToJson = '" "' + FromJson = ' ' + } + @{ + TestInput = 33 + ToJson = '"!"' + FromJson = '!' + } + @{ + TestInput = 34 + ToJson = '"\""' + FromJson = '"' + } + @{ + TestInput = 35 + ToJson = '"#"' + FromJson = '#' + } + @{ + TestInput = 36 + ToJson = '"$"' + FromJson = '$' + } + @{ + TestInput = 37 + ToJson = '"%"' + FromJson = '%' + } + @{ + TestInput = 38 + ToJson = if ( $IsCoreCLR ) { '"&"' } else { '"\u0026"' } + FromJson = '&' + } + @{ + TestInput = 39 + ToJson = if ( $IsCoreCLR ) { '"''"' } else { '"\u0027"' } + FromJson = "'" + } + @{ + TestInput = 40 + ToJson = '"("' + FromJson = '(' + } + @{ + TestInput = 41 + ToJson = '")"' + FromJson = ')' + } + @{ + TestInput = 42 + ToJson = '"*"' + FromJson = '*' + } + @{ + TestInput = 43 + ToJson = '"+"' + FromJson = '+' + } + @{ + TestInput = 44 + ToJson = '","' + FromJson = ',' + } + @{ + TestInput = 45 + ToJson = '"-"' + FromJson = '-' + } + @{ + TestInput = 46 + ToJson = '"."' + FromJson = '.' + } + @{ + TestInput = 47 + ToJson = '"/"' + FromJson = '/' + } + @{ + TestInput = 48 + ToJson = '"0"' + FromJson = '0' + } + @{ + TestInput = 49 + ToJson = '"1"' + FromJson = '1' + } + @{ + TestInput = 50 + ToJson = '"2"' + FromJson = '2' + } + @{ + TestInput = 51 + ToJson = '"3"' + FromJson = '3' + } + @{ + TestInput = 52 + ToJson = '"4"' + FromJson = '4' + } + @{ + TestInput = 53 + ToJson = '"5"' + FromJson = '5' + } + @{ + TestInput = 54 + ToJson = '"6"' + FromJson = '6' + } + @{ + TestInput = 55 + ToJson = '"7"' + FromJson = '7' + } + @{ + TestInput = 56 + ToJson = '"8"' + FromJson = '8' + } + @{ + TestInput = 57 + ToJson = '"9"' + FromJson = '9' + } + @{ + TestInput = 58 + ToJson = '":"' + FromJson = ':' + } + @{ + TestInput = 59 + ToJson = '";"' + FromJson = ';' + } + @{ + TestInput = 60 + ToJson = if ( $IsCoreCLR ) { '"<"' } else { '"\u003c"' } + FromJson = '<' + } + @{ + TestInput = 61 + ToJson = '"="' + FromJson = '=' + } + @{ + TestInput = 62 + ToJson = if ( $IsCoreCLR ) { '">"' } else { '"\u003e"' } + FromJson = '>' + } + @{ + TestInput = 63 + ToJson = '"?"' + FromJson = '?' + } + @{ + TestInput = 64 + ToJson = '"@"' + FromJson = '@' + } + @{ + TestInput = 65 + ToJson = '"A"' + FromJson = 'A' + } + @{ + TestInput = 66 + ToJson = '"B"' + FromJson = 'B' + } + @{ + TestInput = 67 + ToJson = '"C"' + FromJson = 'C' + } + @{ + TestInput = 68 + ToJson = '"D"' + FromJson = 'D' + } + @{ + TestInput = 69 + ToJson = '"E"' + FromJson = 'E' + } + @{ + TestInput = 70 + ToJson = '"F"' + FromJson = 'F' + } + @{ + TestInput = 71 + ToJson = '"G"' + FromJson = 'G' + } + @{ + TestInput = 72 + ToJson = '"H"' + FromJson = 'H' + } + @{ + TestInput = 73 + ToJson = '"I"' + FromJson = 'I' + } + @{ + TestInput = 74 + ToJson = '"J"' + FromJson = 'J' + } + @{ + TestInput = 75 + ToJson = '"K"' + FromJson = 'K' + } + @{ + TestInput = 76 + ToJson = '"L"' + FromJson = 'L' + } + @{ + TestInput = 77 + ToJson = '"M"' + FromJson = 'M' + } + @{ + TestInput = 78 + ToJson = '"N"' + FromJson = 'N' + } + @{ + TestInput = 79 + ToJson = '"O"' + FromJson = 'O' + } + @{ + TestInput = 80 + ToJson = '"P"' + FromJson = 'P' + } + @{ + TestInput = 81 + ToJson = '"Q"' + FromJson = 'Q' + } + @{ + TestInput = 82 + ToJson = '"R"' + FromJson = 'R' + } + @{ + TestInput = 83 + ToJson = '"S"' + FromJson = 'S' + } + @{ + TestInput = 84 + ToJson = '"T"' + FromJson = 'T' + } + @{ + TestInput = 85 + ToJson = '"U"' + FromJson = 'U' + } + @{ + TestInput = 86 + ToJson = '"V"' + FromJson = 'V' + } + @{ + TestInput = 87 + ToJson = '"W"' + FromJson = 'W' + } + @{ + TestInput = 88 + ToJson = '"X"' + FromJson = 'X' + } + @{ + TestInput = 89 + ToJson = '"Y"' + FromJson = 'Y' + } + @{ + TestInput = 90 + ToJson = '"Z"' + FromJson = 'Z' + } + @{ + TestInput = 91 + ToJson = '"["' + FromJson = '[' + } + @{ + TestInput = 92 + ToJson = '"\\"' + FromJson = '\' + } + @{ + TestInput = 93 + ToJson = '"]"' + FromJson = ']' + } + @{ + TestInput = 94 + ToJson = '"^"' + FromJson = '^' + } + @{ + TestInput = 95 + ToJson = '"_"' + FromJson = '_' + } + @{ + TestInput = 96 + ToJson = '"`"' + FromJson = '`' + } + @{ + TestInput = 97 + ToJson = '"a"' + FromJson = 'a' + } + @{ + TestInput = 98 + ToJson = '"b"' + FromJson = 'b' + } + @{ + TestInput = 99 + ToJson = '"c"' + FromJson = 'c' + } + @{ + TestInput = 100 + ToJson = '"d"' + FromJson = 'd' + } + @{ + TestInput = 101 + ToJson = '"e"' + FromJson = 'e' + } + @{ + TestInput = 102 + ToJson = '"f"' + FromJson = 'f' + } + @{ + TestInput = 103 + ToJson = '"g"' + FromJson = 'g' + } + @{ + TestInput = 104 + ToJson = '"h"' + FromJson = 'h' + } + @{ + TestInput = 105 + ToJson = '"i"' + FromJson = 'i' + } + @{ + TestInput = 106 + ToJson = '"j"' + FromJson = 'j' + } + @{ + TestInput = 107 + ToJson = '"k"' + FromJson = 'k' + } + @{ + TestInput = 108 + ToJson = '"l"' + FromJson = 'l' + } + @{ + TestInput = 109 + ToJson = '"m"' + FromJson = 'm' + } + @{ + TestInput = 110 + ToJson = '"n"' + FromJson = 'n' + } + @{ + TestInput = 111 + ToJson = '"o"' + FromJson = 'o' + } + @{ + TestInput = 112 + ToJson = '"p"' + FromJson = 'p' + } + @{ + TestInput = 113 + ToJson = '"q"' + FromJson = 'q' + } + @{ + TestInput = 114 + ToJson = '"r"' + FromJson = 'r' + } + @{ + TestInput = 115 + ToJson = '"s"' + FromJson = 's' + } + @{ + TestInput = 116 + ToJson = '"t"' + FromJson = 't' + } + @{ + TestInput = 117 + ToJson = '"u"' + FromJson = 'u' + } + @{ + TestInput = 118 + ToJson = '"v"' + FromJson = 'v' + } + @{ + TestInput = 119 + ToJson = '"w"' + FromJson = 'w' + } + @{ + TestInput = 120 + ToJson = '"x"' + FromJson = 'x' + } + @{ + TestInput = 121 + ToJson = '"y"' + FromJson = 'y' + } + @{ + TestInput = 122 + ToJson = '"z"' + FromJson = 'z' + } + @{ + TestInput = 123 + ToJson = '"{"' + FromJson = '{' + } + @{ + TestInput = 124 + ToJson = '"|"' + FromJson = '|' + } + @{ + TestInput = 125 + ToJson = '"}"' + FromJson = '}' + } + @{ + TestInput = 126 + ToJson = '"~"' + FromJson = '~' + } + @{ + TestInput = 127 + ToJson = '""' + FromJson = '' + } + ) + + function ValidateJsonSerializationForAsciiValues + { + param ($testCase) + + It "Validate 'ConvertTo-Json ([char]$($testCase.TestInput))', and 'ConvertTo-Json ([char]$($testCase.TestInput)) | ConvertFrom-Json'" { + + $result = @{ + ToJson = ConvertTo-Json ([char]$testCase.TestInput) + FromJson = ConvertTo-Json ([char]$testCase.TestInput) | ConvertFrom-Json + } + + if ($testCase.FromJson) + { + $result.FromJson | Should Be $testCase.FromJson + } + else + { + # There are two char for which the deserialized object must be compare to the serialized one via "Should Match" + # These values are [char]0 and [char]13. + $result.FromJson | Should Match $testCase.FromJson + } + $result.ToJson | Should Be $testCase.ToJson + } + } + + foreach ($testCase in $testCases) + { + ValidateJsonSerializationForAsciiValues $testCase + } + } + + Context "Validate Json serialization for types" { + + $testCases = @( + + ## Decimal types - Decimals are a 128-bit data type + @{ + TestInput = '[decimal]::MinValue' + FromJson = [decimal]::MinValue + ToJson = [decimal]::MinValue + } + @{ + TestInput = '[decimal]::MaxValue' + FromJson = [decimal]::MaxValue + ToJson = [decimal]::MaxValue + } + + # An sbyte is a signed 8-bit integer, and it ranges from -128 to 127. + # A byte is an unsigned 8-bit integer that ranges from 0 to 255 + @{ + TestInput = '[byte]::MinValue' + FromJson = [byte]::MinValue + ToJson = [byte]::MinValue + } + @{ + TestInput = '[byte]::MaxValue' + FromJson = [byte]::MaxValue + ToJson = [byte]::MaxValue + } + @{ + TestInput = '[sbyte]::MinValue' + FromJson = [sbyte]::MinValue + ToJson = [sbyte]::MinValue + } + @{ + TestInput = '[sbyte]::MaxValue' + FromJson = [sbyte]::MaxValue + ToJson = [sbyte]::MaxValue + } + @{ + TestInput = '[char]::MinValue' + FromJson = $null + ToJson = 'null' + } + @{ + TestInput = '[char]::MaxValue - 1' + FromJson = [char]::MaxValue - 1 + ToJson = [char]::MaxValue - 1 + } + @{ + TestInput = '[string]::Empty' + FromJson = [string]::Empty + ToJson = '""' + } + @{ + TestInput = '[string]"hello"' + FromJson = [string]"hello" + ToJson = '"hello"' + } + + # Int, int32, uint32, uint16, int16 + + # 32-bit signed integer + @{ + TestInput = '[int]::MaxValue' + FromJson = [int]::MaxValue + ToJson = [int]::MaxValue + } + @{ + TestInput = '[int]::MinValue' + FromJson = [int]::MinValue + ToJson = [int]::MinValue + } + @{ + TestInput = '[int32]::MaxValue' + FromJson = [int32]::MaxValue + ToJson = [int32]::MaxValue + } + @{ + TestInput = '[int32]::MinValue' + FromJson = [int32]::MinValue + ToJson = [int32]::MinValue + } + + # 32-bit unsigned integer + @{ + TestInput = '[uint32]::MaxValue' + FromJson = [uint32]::MaxValue + ToJson = [uint32]::MaxValue + } + @{ + TestInput = '[uint32]::MinValue' + FromJson = [uint32]::MinValue + ToJson = [uint32]::MinValue + } + + # 16-bit unsigned integer + @{ + TestInput = '[int16]::MinValue' + FromJson = [int16]::MinValue + ToJson = [int16]::MinValue + } + @{ + TestInput = '[uint16]::MaxValue' + FromJson = [uint16]::MaxValue + ToJson = [uint16]::MaxValue + } + + # 64-bit unsigned integer + @{ + TestInput = '[uint64]::MinValue' + FromJson = [uint64]::MinValue + ToJson = [uint64]::MinValue + } + @{ + TestInput = '[uint64]::MinValue' + FromJson = [uint64]::MinValue + ToJson = [uint64]::MinValue + } + + # 64 bit signed integer + @{ + TestInput = '[int64]::MaxValue' + FromJson = [int64]::MaxValue + ToJson = [int64]::MaxValue + } + @{ + TestInput = '[int64]::MinValue' + FromJson = [int64]::MinValue + ToJson = [int64]::MinValue + } + @{ + TestInput = '[long]::MaxValue' + FromJson = [long]::MaxValue + ToJson = [long]::MaxValue + } + @{ + TestInput = '[long]::MinValue' + FromJson = [long]::MinValue + ToJson = [long]::MinValue + } + + # Bool + @{ + TestInput = '[bool](1)' + FromJson = [bool](1) + ToJson = $true + } + @{ + TestInput = '[bool](0)' + FromJson = $false + ToJson = 'False' + } + + # Decimal + @{ + TestInput = '[decimal]::MaxValue' + FromJson = [decimal]::MaxValue + ToJson = [decimal]::MaxValue + } + @{ + TestInput = '[decimal]::MinValue' + FromJson = [decimal]::MinValue + ToJson = [decimal]::MinValue + } + + # Single + @{ + TestInput = '[single]::MaxValue' + FromJson = "3.40282347E+38" + ToJson = "3.40282347E+38" + } + @{ + TestInput = '[single]::MinValue' + FromJson = "-3.40282347E+38" + ToJson = "-3.40282347E+38" + } + + # Double + @{ + TestInput = '[double]::MaxValue' + FromJson = [double]::MaxValue + ToJson = [double]::MaxValue + } + @{ + TestInput = '[double]::MinValue' + FromJson = [double]::MinValue + ToJson = [double]::MinValue + } + ) + + function ValidateJsonSerialization + { + param ($testCase) + + if ( $TestCase.TestInput -eq "[char]::MinValue" ) { $pending = $true } else { $pending = $false } + It "Validate '$($testCase.TestInput) | ConvertTo-Json' and '$($testCase.TestInput) | ConvertTo-Json | ConvertFrom-Json'" -pending:$pending { + + # The test case input is executed via invoke-expression. Then, we use this value as an input to ConvertTo-Json, + # and the result is saved into in the $result.ToJson variable. Lastly, this value is deserialized back using + # ConvertFrom-Json, and the value is saved to $result.FromJson for comparison. + + $expression = Invoke-Expression $testCase.TestInput + $result = @{ + ToJson = $expression | ConvertTo-Json + FromJson = $expression | ConvertTo-Json | ConvertFrom-Json + } + + $result.ToJson | Should Be $testCase.ToJson + $result.FromJson | Should Be $testCase.FromJson + } + } + + foreach ($testCase in $testCases) + { + ValidateJsonSerialization $testCase + } + } + + + Context "Validate Json Serialization for 'Get-CimClass' and 'Get-Command'" { + + function ValidateProperties + { + param ( + $serialized, + $expected, + $properties + ) + + # Validate that the two collections are the same size. + $expected.Count | Should Be $serialized.Count + + for ($index = 0; $index -lt $serialized.Count; $index++) + { + $serializedObject = $serialized[$index] + $expectedObject = $expected[$index] + foreach ($property in $properties) + { + # Write-Verbose "Validating $property" -Verbose + if ($property -eq "Qualifiers") + { + $serializedObject.$property.Count | Should Be $expectedObject.$property.Count + } + else + { + $serializedObject.$property | Should Be $expectedObject.$property + } + } + } + } + + It "Validate that CimClass Properties for win32_bios can be serialized using ConvertTo-Json and ConvertFrom-Json" -skip { + + $class = Get-CimClass win32_bios + + $result = @{ + Expected = $class.CimClassProperties | ForEach-Object {$_} + SerializedViaJson = $class.CimClassProperties | ConvertTo-Json -Depth 10 | ConvertFrom-Json + } + + $propertiesToValidate = @("Name", "Flags", "Qualifiers", "ReferenceClassName") + + ValidateProperties -serialized $result.SerializedViaJson -expected $result.Expected -properties $propertiesToValidate + } + + It "Validate 'Get-Command Get-help' output with Json conversion" { + + $result = @{ + Expected = @(Get-Command Get-help) + SerializedViaJson = @(Get-Command Get-help | ConvertTo-Json | ConvertFrom-Json) + } + + $propertiesToValidate = @("Name", "Noun", "Verb") + ValidateProperties -serialized $result.SerializedViaJson -expected $result.Expected -properties $propertiesToValidate + } + + It "Validate 'Get-Command Get-Help, Get-command, Get-Member' output with Json conversion" { + + $result = @{ + Expected = @(Get-Command Get-Help, Get-Command, Get-Member) + SerializedViaJson = @(Get-Command Get-Help, Get-Command, Get-Member) | ConvertTo-Json | ConvertFrom-Json + } + + $propertiesToValidate = @("Name", "Source", "HelpFile") + ValidateProperties -serialized $result.SerializedViaJson -expected $result.Expected -properties $propertiesToValidate + } + + It "ConvertTo-JSON a dictionary of arrays" { + $a = 1..5 + $b = 6..10 + + # remove whitespace (and newline/cr) which reduces the complexity of a + # cross-plat test + $actual = ([ordered]@{'a'=$a;'b'=$b} | ConvertTo-Json) -replace "\s" + $expected = @' +{ + "a": [ + 1, + 2, + 3, + 4, + 5 + ], + "b": [ + 6, + 7, + 8, + 9, + 10 + ] +} +'@ + $expectedNoWhiteSpace = $expected -replace "\s" + $actual | Should Be $expectedNoWhiteSpace + } + } +} + +Describe "Json Bug fixes" -Tags "Feature" { + + function RunJsonTest + { + param ($testCase) + + It "$($testCase.Name)" { + + # Create a nested object + $start = 1 + $previous = @{ + Depth = $($testCase.NumberOfElements) + Next = $null + } + + ($($testCase.NumberOfElements)-1)..$start | foreach { + $current = @{ + Depth = $_ + Next = $previous + } + $previous = $current + } + + if ($testCase.ShouldThrow) + { + try + { + $previous | ConvertTo-Json -Depth $testCase.MaxDepth + throw "CodeExecuted" + } + catch + { + $_.FullyQualifiedErrorId | Should Be $testCase.FullyQualifiedErrorId + } + } + else + { + $theError = $null + try + { + $previous | ConvertTo-Json -Depth $testCase.MaxDepth | ConvertFrom-Json + } + catch + { + $theError = $_ + } + $theError | Should Be $null + } + } + } + + $testCases = @( + @{ + Name = "ConvertTo-Json -Depth 101 throws MaximumAllowedDepthReached when the user specifies a depth greather than 100." + NumberOfElements = 10 + MaxDepth = 101 + FullyQualifiedErrorId = "ReachedMaximumDepthAllowed,Microsoft.PowerShell.Commands.ConvertToJsonCommand" + ShouldThrow = $true + } + @{ + Name = "ConvertTo-Json and ConvertFrom-Json work for any depth less than or equal to 100." + NumberOfElements = 100 + MaxDepth = 100 + ShouldThrow = $false + } + @{ + Name = "ConvertTo-Json and ConvertFrom-Json work for depth 100 with an object larger than 100." + NumberOfElements = 105 + MaxDepth = 100 + ShouldThrow = $false + } + ) + + foreach ($testCase in $testCases) + { + RunJsonTest $testCase + } +} diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/PowerShellData.tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/PowerShellData.tests.ps1 new file mode 100644 index 000000000..5bdb79537 --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/PowerShellData.tests.ps1 @@ -0,0 +1,86 @@ +Describe "Tests for the Import-PowerShellDataFile cmdlet" -Tags "Feature" { + + It "Validates error on a missing path" { + + $foundError = "" + try + { + Import-PowerShellDataFile -Path /SomeMissingDirectory -ErrorAction Stop + } + catch + { + $foundError = $_.FullyQualifiedErrorId + } + + $foundError | Should be "PathNotFound,Microsoft.PowerShell.Commands.ResolvePathCommand" + } + + It "Validates error on a directory" { + + $foundError = "" + try + { + Import-PowerShellDataFile ${TESTDRIVE} -ErrorAction Stop + } + catch + { + $foundError = $_.FullyQualifiedErrorId + } + + $foundError | Should be "CouldNotParseAsPowerShellDataFile,Import-PowerShellDataFile" + } + + It "Generates a good error on an insecure file" { + + $path = New-TemporaryFile + Set-Content $path '@{ Foo = Get-Process }' + + $foundError = "" + try + { + Import-PowerShellDataFile $path -ErrorAction Stop + } + catch + { + $foundError = $_.FullyQualifiedErrorId + } + finally + { + Remove-Item $path + } + + $foundError | Should be "InvalidOperationException,Import-PowerShellDataFile" + } + + It "Generates a good error on a file that isn't a PowerShell Data File (missing the hashtable root)" { + + $path = New-TemporaryFile + Set-Content $path '"Hello World"' + + $foundError = "" + try + { + Import-PowerShellDataFile $path -ErrorAction Stop + } + catch + { + $foundError = $_.FullyQualifiedErrorId + } + finally + { + Remove-Item $path + } + + $foundError | Should be "CouldNotParseAsPowerShellDataFileNoHashtableRoot,Import-PowerShellDataFile" + } + + It "Can parse a PowerShell Data File (detailed tests are in AST.SafeGetValue tests)" { + + $path = New-TemporaryFile + Set-Content $path '@{ "Hello" = "World" }' + + $result = Import-PowerShellDataFile $path -ErrorAction Stop + $result.Hello | Should be "World" + } + +} diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/alias.tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/alias.tests.ps1 new file mode 100644 index 000000000..4317e8d21 --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/alias.tests.ps1 @@ -0,0 +1,160 @@ +Describe "Alias tests" -Tags "CI" { + + BeforeAll { + $testPath = Join-Path testdrive:\ ("testAlias\[.test") + New-Item -ItemType Directory -Path $testPath -Force | Out-Null + + class TestData + { + [string] $testName + [string] $testFile + [string] $expectedError + + TestData($name, $file, $error) + { + $this.testName = $name + $this.testFile = $file + $this.expectedError = $error + } + } + } + + Context "Export-Alias literal path" { + BeforeAll { + $csvFile = Join-Path $testPath "alias.csv" + $ps1File = Join-Path $testPath "alias.ps1" + + $testCases = @() + $testCases += [TestData]::new("CSV", $csvFile, [NullString]::Value) + $testCases += [TestData]::new("PS1", $ps1File, [NullString]::Value) + $testCases += [TestData]::new("Empty string", "", "ParameterArgumentValidationErrorEmptyStringNotAllowed,Microsoft.PowerShell.Commands.ExportAliasCommand") + $testCases += [TestData]::new("Null", [NullString]::Value, "ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.ExportAliasCommand") + $testCases += [TestData]::new("Non filesystem provider", 'cert:\alias.ps1', "ReadWriteFileNotFileSystemProvider,Microsoft.PowerShell.Commands.ExportAliasCommand") + } + + $testCases | % { + + It "for $($_.testName)" { + + $test = $_ + try + { + Export-Alias -LiteralPath $test.testFile -ErrorAction SilentlyContinue + } + catch + { + $exportAliasError = $_ + } + + if($test.expectedError -eq $null) + { + Test-Path -LiteralPath $test.testFile | Should Be $true + } + else + { + $exportAliasError.FullyqualifiedErrorId | Should Be $test.expectedError + } + } + + AfterEach { + Remove-Item -LiteralPath $test.testFile -Force -ErrorAction SilentlyContinue + } + } + + It "when file exists with NoClobber" { + Export-Alias -LiteralPath $csvFile + + try + { + Export-Alias -LiteralPath $csvFile -NoClobber + } + catch + { + $exportAliasError = $_ + } + + $exportAliasError.FullyQualifiedErrorId | Should Be "NoClobber,Microsoft.PowerShell.Commands.ExportAliasCommand" + } + } + + Context "Export-All inside a literal path" { + BeforeEach { + Push-Location -LiteralPath $testPath + } + + It "with a CSV file" { + Export-Alias "alias.csv" + Test-Path -LiteralPath (Join-Path $testPath "alias.csv") | Should Be $true + } + + It "with NoClobber" { + $path = Export-Alias alias.csv + + try + { + Export-Alias alias.csv -NoClobber + } + catch + { + $exportAliasError = $_ + } + + $exportAliasError.FullyQualifiedErrorId | Should Be "NoClobber,Microsoft.PowerShell.Commands.ExportAliasCommand" + } + + AfterEach { + Pop-Location + } + } + + Context "Import-Alias literal path" { + + BeforeAll { + $csvFile = Join-Path $testPath "alias.csv" + $ps1File = Join-Path $testPath "alias.ps1" + + $testCases = @() + $testCases += [TestData]::new("Empty string", "", "ParameterArgumentValidationErrorEmptyStringNotAllowed,Microsoft.PowerShell.Commands.ImportAliasCommand") + $testCases += [TestData]::new("Null", [NullString]::Value, "ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.ImportAliasCommand") + $testCases += [TestData]::new("Non filesystem provider", 'cert:\alias.ps1', "NotSupported,Microsoft.PowerShell.Commands.ImportAliasCommand") + } + + $testCases | % { + + It "for $($_.testName)" { + $test = $_ + + try + { + Import-Alias -LiteralPath $test.testFile -ErrorAction SilentlyContinue + } + catch + { + $exportAliasError = $_ + } + + $exportAliasError.FullyqualifiedErrorId | Should Be $test.expectedError + } + } + + It "can be done from a CSV file" { + + # alias file definition content + $aliasDefinition = @' + "myuh","update-help","","ReadOnly, AllScope" +'@ + + $aliasFile = Join-Path $testPath "alias.csv" + $aliasDefinition | Out-File -LiteralPath $aliasFile + + Import-Alias -LiteralPath $aliasFile + + # Verify that the alias was imported + $definedAlias = Get-Alias myuh + + $definedAlias | Should Not Be $null + $definedAlias.Name | Should Be "myuh" + $definedAlias.Definition | Should Be "update-help" + } + } +} diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/clixml.tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/clixml.tests.ps1 new file mode 100644 index 000000000..15477eae9 --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/clixml.tests.ps1 @@ -0,0 +1,187 @@ +Describe "CliXml test" -Tags "CI" { + + BeforeAll { + $testFilePath = Join-Path "testdrive:\" "testCliXml" + $subFilePath = Join-Path $testFilePath ".test" + + if(test-path $testFilePath) + { + Remove-Item $testFilePath -Force -Recurse + } + + # Create the test File and push the location into specified path + New-Item -Path $testFilePath -ItemType Directory | Out-Null + New-Item -Path $subFilePath -ItemType Directory | Out-Null + Push-Location $testFilePath + + class TestData + { + [string] $testName + [object] $inputObject + [string] $expectedError + [string] $testFile + + TestData($name, $file, $inputObj, $error) + { + $this.testName = $name + $this.inputObject = $inputObj + $this.expectedError = $error + $this.testFile = $file + } + } + } + + AfterAll { + Pop-Location + } + + Context "Export-CliXml" { + BeforeAll { + $gpsList = Get-Process powershell + $gps = $gpsList | Select-Object -First 1 + $filePath = Join-Path $subFilePath 'gps.xml' + + $testData = @() + $testData += [TestData]::new("with path as Null", [NullString]::Value, $gps, "ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.ExportClixmlCommand") + $testData += [TestData]::new("with path as Empty string", "", $gps, "ParameterArgumentValidationErrorEmptyStringNotAllowed,Microsoft.PowerShell.Commands.ExportClixmlCommand") + $testData += [TestData]::new("with path as non filesystem provider", "cert:\", $gps, "ReadWriteFileNotFileSystemProvider,Microsoft.PowerShell.Commands.ExportClixmlCommand") + } + + AfterEach { + Remove-Item $filePath -Force -ErrorAction SilentlyContinue + } + + $testData | % { + + It "$($_.testName)" { + $test = $_ + + try + { + Export-Clixml -LiteralPath $test.testFile -InputObject $test.inputObject -Force + } + catch + { + $exportCliXmlError = $_ + } + + $exportCliXmlError.FullyQualifiedErrorId | Should Be $test.expectedError + } + } + + It "can be created with literal path" { + + $filePath = Join-Path $subFilePath 'gps.xml' + Export-Clixml -LiteralPath $filePath -InputObject ($gpsList | Select-Object -First 1) + + $filePath | Should Exist + + $fileContent = Get-Content $filePath + $isExisted = $false + + foreach($item in $fileContent) + { + foreach($gpsItem in $gpsList) + { + $checkId = $gpsItem.Id + if (($null -ne $(Select-String -InputObject $item -SimpleMatch $checkId)) -and ($null -ne $(Select-String -InputObject $item -SimpleMatch "Id"))) + { + $isExisted = $true + break; + } + } + } + + $isExisted | Should Be $true + } + + It "can be created with literal path using pipeline" { + + + $filePath = Join-Path $subFilePath 'gps.xml' + ($gpsList | Select-Object -First 1) | Export-Clixml -LiteralPath $filePath + + $filePath | Should Exist + + $fileContent = Get-Content $filePath + $isExisted = $false + + foreach($item in $fileContent) + { + foreach($gpsItem in $gpsList) + { + $checkId = $gpsItem.Id + if (($null -ne $(Select-String -InputObject $item -SimpleMatch $checkId)) -and ($null -ne $(Select-String -InputObject $item -SimpleMatch "Id"))) + { + $isExisted = $true + break; + } + } + } + + $isExisted | Should Be $true + } + } + + Context "Import-CliXML" { + BeforeAll { + $gpsList = Get-Process powershell + $gps = $gpsList | Select-Object -First 1 + $filePath = Join-Path $subFilePath 'gps.xml' + + $testData = @() + $testData += [TestData]::new("with path as Null", [NullString]::Value, $null, "ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.ImportClixmlCommand") + $testData += [TestData]::new("with path as Empty string", "", $null, "ParameterArgumentValidationErrorEmptyStringNotAllowed,Microsoft.PowerShell.Commands.ImportClixmlCommand") + $testData += [TestData]::new("with path as non filesystem provider", "cert:\", $null, "ReadWriteFileNotFileSystemProvider,Microsoft.PowerShell.Commands.ImportClixmlCommand") + } + + $testData | % { + + It "$($_.testName)" { + $test = $_ + + try + { + Import-Clixml -LiteralPath $test.testFile + } + catch + { + $importCliXmlError = $_ + } + + $importCliXmlError.FullyQualifiedErrorId | Should Be $test.expectedError + } + } + + It "can import from a literal path" { + Export-Clixml -LiteralPath $filePath -InputObject $gps + $filePath | Should Exist + + $fileContent = Get-Content $filePath + $fileContent | Should Not Be $null + + $importedProcess = Import-Clixml $filePath + $gps.ProcessName | Should Be $importedProcess.ProcessName + $gps.Id | Should Be $importedProcess.Id + } + + It "can import from a literal path using pipeline" { + $gps | Export-Clixml -LiteralPath $filePath + $filePath | Should Exist + + $fileContent = Get-Content $filePath + $fileContent | Should Not Be $null + + $importedProcess = Import-Clixml $filePath + $gps.ProcessName | Should Be $importedProcess.ProcessName + $gps.Id | Should Be $importedProcess.Id + } + + It "test follow-up for WinBlue: 161470 - Export-CliXml errors in WhatIf scenarios" { + + $testPath = "testdrive:\Bug161470NonExistPath.txt" + Export-Clixml -Path $testPath -InputObject "string" -WhatIf + $testPath | Should Not Exist + } + } +} diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/command.tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/command.tests.ps1 new file mode 100644 index 000000000..443abc0a3 --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/command.tests.ps1 @@ -0,0 +1,80 @@ +Describe "Trace-Command" -tags "Feature" { + + Context "Listner options" { + BeforeAll { + $logFile = setup -f traceCommandLog.txt -pass + $actualLogFile = setup -f actualTraceCommandLog.txt -pass + } + + AfterEach { + if ( test-path $logfile ) { Remove-Item $logFile } + if ( test-path $actualLogFile ) { Remove-Item $actualLogFile } + } + + It "LogicalOperationStack works" -pending:($IsCoreCLR) { + $keyword = "Trace_Command_ListenerOption_LogicalOperationStack_Foo" + $stack = [System.Diagnostics.Trace]::CorrelationManager.LogicalOperationStack + $stack.Push($keyword) + + Trace-Command -Name * -Expression {write-output Foo} -ListenerOption LogicalOperationStack -FilePath $logfile + + $log = Get-Content $logfile | Where-Object {$_ -like "*LogicalOperationStack=$keyword*"} + $log.Count | Should BeGreaterThan 0 + } + + It "Callstack works" -pending:($IsCoreCLR) { + Trace-Command -Name * -Expression {write-output Foo} -ListenerOption Callstack -FilePath $logfile + $log = Get-Content $logfile | Where-Object {$_ -like "*Callstack= * System.Environment.GetStackTrace(Exception e, Boolean needFileInfo)*"} + $log.Count | Should BeGreaterThan 0 + } + + It "Datetime works" { + $expectedDate = Trace-Command -Name * -Expression {Get-Date} -ListenerOption DateTime -FilePath $logfile + $log = Get-Content $logfile | Where-Object {$_ -like "*DateTime=*"} + $results = $log | ForEach-Object {[DateTime]::Parse($_.Split("=")[1])} + + ## allow a gap of 6 seconds. All traces should be finished within 6 seconds. + $allowedGap = [timespan](60 * 1000 * 1000) + $results | ForEach-Object { + $actualGap = $_ - $expectedDate; + if ($expectedDate -gt $_) + { + $actualGap = $expectedDate - $_; + } + + $allowedGap | Should BeGreaterThan $actualGap + } + } + + It "None options has no effect" { + Trace-Command -Name * -Expression {write-output Foo} -ListenerOption None -FilePath $actualLogfile + Trace-Command -name * -Expression {write-output Foo} -FilePath $logfile + + Compare-Object (Get-Content $actualLogfile) (Get-Content $logfile) | Should BeNullOrEmpty + } + + It "ThreadID works" { + Trace-Command -Name * -Expression {write-output Foo} -ListenerOption ThreadId -FilePath $logfile + $log = Get-Content $logfile | Where-Object {$_ -like "*ThreadID=*"} + $results = $log | ForEach-Object {$_.Split("=")[1]} + + $results | % { $_ | Should Be ([threading.thread]::CurrentThread.ManagedThreadId) } + } + + It "Timestamp creates logs in ascending order" { + Trace-Command -Name * -Expression {write-output Foo} -ListenerOption Timestamp -FilePath $logfile + $log = Get-Content $logfile | Where-Object {$_ -like "*Timestamp=*"} + $results = $log | ForEach-Object {$_.Split("=")[1]} + $sortedResults = $results | Sort-Object + $sortedResults | Should Be $results + } + + It "ProcessId logs current process Id" { + Trace-Command -Name * -Expression {write-output Foo} -ListenerOption ProcessId -FilePath $logfile + $log = Get-Content $logfile | Where-Object {$_ -like "*ProcessID=*"} + $results = $log | ForEach-Object {$_.Split("=")[1]} + + $results | ForEach-Object { $_ | Should Be $pid } + } + } +} diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/formatdata.tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/formatdata.tests.ps1 new file mode 100644 index 000000000..14f8fc76d --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/formatdata.tests.ps1 @@ -0,0 +1,64 @@ +Describe "FormatData" -tags "Feature" { + + Context "Export" { + It "can export all types" { + try + { + $expectAllFormat = Get-FormatData -typename * + $expectAllFormat | Export-FormatData -path $TESTDRIVE\allformat.ps1xml -IncludeScriptBlock + + $sessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault() + $sessionState.Formats.Clear() + $sessionState.Types.Clear() + + $runspace = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace($sessionState) + $runspace.Open() + + $runspace.CreatePipeline("Update-FormatData -AppendPath $TESTDRIVE\allformat.ps1xml").Invoke() + $actualAllFormat = $runspace.CreatePipeline("Get-FormatData -TypeName *").Invoke() + + $expectAllFormat.Count | Should Be $actualAllFormat.Count + Compare-Object $expectAllFormat $actualAllFormat | Should Be $null + $runspace.Close() + } + finally + { + Remove-Item -Path $TESTDRIVE\allformat.ps1xml -Force -ErrorAction SilentlyContinue + } + } + + It "works with literal path" { + $filename = 'TestDrive:\[formats.ps1xml' + Get-FormatData -TypeName * | Export-FormatData -LiteralPath $filename + (Test-Path -LiteralPath $filename) | Should Be $true + } + + It "should overwrite the destination file" { + $filename = 'TestDrive:\ExportFormatDataWithForce.ps1xml' + $unexpected = "SHOULD BE OVERWRITTEN" + $unexpected | Out-File -FilePath $filename -Force + $file = Get-Item $filename + $file.IsReadOnly = $true + Get-FormatData -TypeName * | Export-FormatData -Path $filename -Force + + $actual = @(Get-Content $filename)[0] + $actual | Should Not Be $unexpected + } + + It "should not overwrite the destination file with NoClobber" { + $filename = "TestDrive:\ExportFormatDataWithNoClobber.ps1xml" + Get-FormatData -TypeName * | Export-FormatData -LiteralPath $filename + + try + { + Get-FormatData -TypeName * | Export-FormatData -LiteralPath $filename -NoClobber + } + catch + { + $exportFormatError = $_ + } + + $exportFormatError.FullyQualifiedErrorId | Should Be 'NoClobber,Microsoft.PowerShell.Commands.ExportFormatDataCommand' + } + } +} diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/object.tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/object.tests.ps1 new file mode 100644 index 000000000..6a8d1e25c --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/object.tests.ps1 @@ -0,0 +1,100 @@ +Describe "Object cmdlets" -Tags "CI" { + Context "Group-Object" { + It "AsHashtable returns a hashtable" { + $result = Get-Process | Group-Object -Property ProcessName -AsHashTable + $result["powershell"].Count | Should BeGreaterThan 0 + } + + It "AsString returns a string" { + $processes = Get-Process | Group-Object -Property ProcessName -AsHashTable -AsString + $result = $processes.Keys | ForEach-Object {$_.GetType()} + $result[0].Name | Should Be "String" + } + } + + Context "Tee-Object" { + It "with literal path" { + $path = "TestDrive:\[TeeObjectLiteralPathShouldWorkForSpecialFilename].txt" + Write-Output "Test" | Tee-Object -LiteralPath $path | Tee-Object -Variable TeeObjectLiteralPathShouldWorkForSpecialFilename + $TeeObjectLiteralPathShouldWorkForSpecialFilename | Should Be (Get-Content -LiteralPath $path) + } + } +} + +Describe "Object cmdlets" -Tags "CI" { + Context "Measure-Object" { + BeforeAll { + ## Powershell language prefers , as an array seperator without "". + ## If a number has comma in them it considers it to be the 1000 seperator like "1,000". + ## In de-DE language the comma is used as decimal point, but powershell still uses it as a 1000 seperator. + ## In case the number has a comma, it is ignored. So, "99,1" becomes 991. + ## To work around that behavior, we use ToString() on the expected answer and . for decimal in the input. + + $firstValue = "9995788.71" + $expectedFirstValue = $null + $null = [System.Management.Automation.LanguagePrimitives]::TryConvertTo($firstValue, [double], [cultureinfo]::InvariantCulture, [ref] $expectedFirstValue) + $firstObject = new-object psobject + $firstObject | Add-Member -NotePropertyName Header -NotePropertyValue $firstValue + + $secondValue = "15847577.7" + $expectedSecondValue = $null + $null = [System.Management.Automation.LanguagePrimitives]::TryConvertTo($secondValue, [double], [cultureinfo]::InvariantCulture, [ref] $expectedSecondValue) + $secondObject = new-object psobject + $secondObject | Add-Member -NotePropertyName Header -NotePropertyValue $secondValue + + $testCases = @( + @{ data = @("abc","ABC","Def"); min = "abc"; max = "Def"}, + @{ data = @([datetime]::Today, [datetime]::Today.AddDays(-1)); min = ([datetime]::Today.AddDays(-1)).ToString() ; max = [datetime]::Today.ToString() } + @{ data = @(1,2,3,"ABC"); min = 1; max = "ABC"}, + @{ data = @(4,2,3,"ABC",1); min = 1; max = "ABC"}, + @{ data = @(4,2,3,"ABC",1,"DEF"); min = 1; max = "DEF"}, + @{ data = @("111 Test","19"); min = "111 Test"; max = "19"}, + @{ data = @("19", "111 Test"); min = "111 Test"; max = "19"}, + @{ data = @("111 Test",19); min = "111 Test"; max = 19}, + @{ data = @(19, "111 Test"); min = "111 Test"; max = 19}, + @{ data = @(100,2,3, "A", 1); min = 1; max = "A"}, + @{ data = @(4,2,3, "ABC", 1, "DEF"); min = 1; max = "DEF"}, + @{ data = @("abc",[Datetime]::Today,"def"); min = [Datetime]::Today.ToString(); max = "def"} + ) + } + + It "can compare string representation for minimum" { + $minResult = $firstObject, $secondObject | Measure-Object Header -Minimum + $minResult.Minimum.ToString() | Should Be $expectedFirstValue.ToString() + } + + It "can compare string representation for maximum" { + $maxResult = $firstObject, $secondObject | Measure-Object Header -Maximum + $maxResult.Maximum.ToString() | Should Be $expectedSecondValue.ToString() + } + + It 'correctly find minimum of ()' -TestCases $testCases { + param($data, $min, $max) + + $output = $data | Measure-Object -Minimum + $output.Minimum.ToString() | Should Be $min + } + + It 'correctly find maximum of ()' -TestCases $testCases { + param($data, $min, $max) + + $output = $data | Measure-Object -Maximum + $output.Maximum.ToString() | Should Be $max + } + + It 'returns a GenericMeasureInfoObject' { + $gmi = 1,2,3 | measure-object -max -min + $gmi.GetType().FullName | Should Be 'Microsoft.PowerShell.Commands.GenericMeasureInfo' + } + + It 'should return correct error for non-numeric input' { + $gmi = "abc",[Datetime]::Now | measure -sum -max -ev err -ea silentlycontinue + $err | % { $_.FullyQualifiedErrorId | Should Be 'NonNumericInputObject,Microsoft.PowerShell.Commands.MeasureObjectCommand' } + } + + It 'should have the correct count' { + $gmi = "abc",[Datetime]::Now | measure -sum -max -ev err -ea silentlycontinue + $gmi.Count | Should Be 2 + } + } +} diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/string.tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/string.tests.ps1 new file mode 100644 index 000000000..0b367b334 --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/string.tests.ps1 @@ -0,0 +1,63 @@ +Describe "String cmdlets" -Tags "CI" { + Context "Select-String" { + BeforeAll { + $sep = [io.path]::DirectorySeparatorChar + $fileName = New-Item 'TestDrive:\selectStr[ingLi]teralPath.txt' + "abc" | Out-File -LiteralPath $fileName.fullname + "bcd" | Out-File -LiteralPath $fileName.fullname -Append + "cde" | Out-File -LiteralPath $fileName.fullname -Append + + $fileNameWithDots = $fileName.FullName.Replace("\", "\.\") + + $tempFile = New-TemporaryFile + "abc" | Out-File -LiteralPath $tempFile.fullname + "bcd" | Out-File -LiteralPath $tempFile.fullname -Append + "cde" | Out-File -LiteralPath $tempFile.fullname -Append + $driveLetter = $tempFile.PSDrive.Name + $fileNameAsNetworkPath = "\\localhost\$driveLetter`$" + $tempFile.FullName.SubString(2) + + Push-Location "$fileName\.." + } + + AfterAll { + Remove-Item $tempFile -Force -ErrorAction SilentlyContinue + Pop-Location + } + + It "LiteralPath with relative path" { + (select-string -LiteralPath (Get-Item -LiteralPath $fileName).Name "b").count | Should Be 2 + } + + It "LiteralPath with absolute path" { + (select-string -LiteralPath $fileName "b").count | Should Be 2 + } + + It "LiteralPath with dots in path" { + (select-string -LiteralPath $fileNameWithDots "b").count | Should Be 2 + } + + It "Network path" -skip:($IsCoreCLR) { + (select-string -LiteralPath $fileNameAsNetworkPath "b").count | Should Be 2 + } + + It "throws error for non filesystem providers" { + $aaa = "aaaaaaaaaa" + select-string -literalPath variable:\aaa "a" -ErrorAction SilentlyContinue -ErrorVariable selectStringError + $selectStringError.FullyQualifiedErrorId | Should Be 'ProcessingFile,Microsoft.PowerShell.Commands.SelectStringCommand' + } + + It "throws parameter binding exception for invalid context" { + { select-string It $PSScriptRoot -Context -1,-1 } | Should Throw Context + } + + It "match object supports RelativePath method" { + $file = "Modules${sep}Microsoft.PowerShell.Utility${sep}Microsoft.PowerShell.Utility.psd1" + + $match = Select-String CmdletsToExport $pshome/$file + + $match.RelativePath($pshome) | Should Be $file + $match.RelativePath($pshome.ToLower()) | Should Be $file + $match.RelativePath($pshome.ToUpper()) | Should Be $file + } + } +} diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/typedata.tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/typedata.tests.ps1 new file mode 100644 index 000000000..cf4c42337 --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/typedata.tests.ps1 @@ -0,0 +1,129 @@ +Describe "TestData cmdlets" -Tags "CI" { + Context "Get-TypeData" { + It "System.Array" { + (Get-TypeData System.Array).TypeName | Should Be System.Array + # Supports pipelining? + ("System.Array" | Get-TypeData).TypeName | Should Be System.Array + } + + It "Type accelerators" { + (Get-TypeData Array).TypeName | Should Be System.Array + (Get-TypeData psCredential).TypeName | Should Be System.Management.Automation.PSCredential + } + + It "Accept multiple types" { + $types = Get-TypeData System.Array, System.Management.Automation* | Sort-Object -Property TypeName + $types[0].TypeName | Should Be System.Array + for($i = 1; $i -lt $types.Count; $i++) + { + $types[$i].TypeName.StartsWith("System.Management.Automation") | Should Be $true + } + } + + It "System.Object" { + Get-TypeData System.Object | Should Be $null + } + } + + # The rest of these tests do their work in another runspace to avoid messing up the current runspace + Context "Update-TypeData" { + BeforeAll { + $script:ps = [PowerShell]::Create() + } + + BeforeEach { + $ps.Commands.Clear() + $ps.Streams.Error.Clear() + } + + AfterAll { + $ps.Dispose() + $script:ps = $null + } + + It "TypeAdapter parameter" { + $type = $ps.AddScript(@" +Update-TypeData -TypeName Void -TypeAdapter Microsoft.PowerShell.Cim.CimInstanceAdapter +Get-TypeData System.Void +"@).Invoke() + $type[0].TypeName | Should Be System.Void + $type[0].TypeAdapter.FullName | Should Be Microsoft.PowerShell.Cim.CimInstanceAdapter + } + } + + Context "Remove-TypeData" { + BeforeAll { + $script:ps = [PowerShell]::Create() + Setup -F dummy1.types.ps1xml -Content "yyyDummyyyyDummy'yyyDummy'" + Setup -F dummy2.types.ps1xml -Content "zzzDummyzzzDummy'zzzDummy'" + } + + BeforeEach { + $ps.Commands.Clear() + $ps.Streams.Error.Clear() + } + + AfterAll { + $ps.Dispose() + $script:ps = $null + } + + It "Remove type that doesn't exist" { + $typeName = "TypeThatDoesNotExistsAnywhere" + (Get-Random) + $ps.AddScript("Remove-TypeData -TypeName $typeName").Invoke() + $ps.Streams.Error[0].FullyQualifiedErrorId | Should Be "TypesDynamicRemoveException,Microsoft.PowerShell.Commands.RemoveTypeDataCommand" + } + + ##{ All of the following It blocks are intended to run in sequence, so don't reorder them. + + It "Add type file," { + $ps.AddScript("Update-TypeData -AppendPath $TestDrive\dummy1.types.ps1xml").Invoke() + $ps.Streams.Error.Count | Should Be 0 + } + + It "Get type data from file just added," { + $type = $ps.AddScript("Get-TypeData -TypeName yyyDummy").Invoke() + $ps.Streams.Error.Count | Should Be 0 + $type[0].TypeName | Should Be yyyDummy + } + + It "Remove type file just added," { + $ps.AddScript("Remove-TypeData -Path $TestDrive\dummy1.types.ps1xml").Invoke() + $ps.Streams.Error.Count | Should Be 0 + } + + It "Make sure type was removed" { + $type = $ps.AddScript("Get-TypeData -TypeName yyyDummy").Invoke() + $type.Count | Should Be 0 + } + + It "Add another type file," { + $ps.AddScript("Update-TypeData -AppendPath $TestDrive\dummy2.types.ps1xml").Invoke() + $ps.Streams.Error.Count | Should Be 0 + } + + It "Now add some type data to one of those newly added types" { + $ps.AddScript("Update-TypeData -TypeName zzzDummy -MemberType NoteProperty -MemberName DynamicDummyProperty -Value 10").Invoke() + $ps.Streams.Error.Count | Should Be 0 + } + + It "Remove the newly type file," { + $ps.AddScript("Remove-TypeData -Path $TestDrive\dummy2.types.ps1xml").Invoke() + $ps.Streams.Error.Count | Should Be 0 + } + + It "Remove the newly type file a second time, should error," { + $ps.AddScript("Remove-TypeData -Path $TestDrive\dummy2.types.ps1xml").Invoke() + $ps.Streams.Error.Count | Should Be 1 + $ps.Streams.Error[0].FullyQualifiedErrorId | Should Be "TypeFileNotExistsInCurrentSession,Microsoft.PowerShell.Commands.RemoveTypeDataCommand" + } + + It "Dynamic property added should still be there" { + $res = $ps.AddScript("Get-TypeData -TypeName zzzDummy").Invoke() + $res.Count | Should Be 1 + $res[0].Members.Keys[0] | Should Be DynamicDummyProperty + } + + ##} All of the preceding It blocks are intended to run in sequence, so don't reorder them. + } +} diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/xml.tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/xml.tests.ps1 new file mode 100644 index 000000000..71b124682 --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/xml.tests.ps1 @@ -0,0 +1,68 @@ +Describe "XML cmdlets" -Tags "Feature" { + Context "Select-XML" { + BeforeAll { + $fileName = New-Item -Path 'TestDrive:\testSelectXml.xml' + Push-Location "$fileName\.." + "" | out-file -LiteralPath $fileName + " " | out-file -LiteralPath $fileName -Append + "" | out-file -LiteralPath $fileName -Append + + $fileNameWithDots = $fileName.FullName.Replace("\", "\.\") + + $driveLetter = [string]($fileName.FullName)[0] + $fileNameAsNetworkPath = "\\localhost\$driveLetter`$" + $fileName.FullName.SubString(2) + + class TestData + { + [string] $testName + [hashtable] $parameters + + TestData($name, $parameters) + { + $this.testName = $name + $this.parameters = $parameters + } + } + + $testcases = @() + $testcases += [TestData]::new('literalpath with relative paths', @{LiteralPath = $fileName.Name; XPath = 'Root'}) + $testcases += [TestData]::new('literalpath with absolute paths', @{LiteralPath = $fileName.FullName; XPath = 'Root'}) + $testcases += [TestData]::new('literalpath with path with dots', @{LiteralPath = $fileNameWithDots; XPath = 'Root'}) + if ( ! $IsCoreCLR ) { + $testcases += [TestData]::new('literalpath with network path', @{LiteralPath = $fileNameAsNetworkPath; XPath = 'Root'}) + } + $testcases += [TestData]::new('path with relative paths', @{Path = $fileName.Name; XPath = 'Root'}) + $testcases += [TestData]::new('path with absolute paths', @{Path = $fileName.FullName; XPath = 'Root'}) + $testcases += [TestData]::new('path with path with dots', @{Path = $fileNameWithDots; XPath = 'Root'}) + if ( ! $IsCoreCLR ) { + $testcases += [TestData]::new('path with network path', @{Path = $fileNameAsNetworkPath; XPath = 'Root'}) + } + } + + AfterAll { + Remove-Item -LiteralPath $fileName -Force -ErrorAction SilentlyContinue + Pop-Location + } + + $testcases | % { + + $params = $_.parameters + + It $_.testName { + @(Select-XML @params).Count | Should Be 1 + } + } + + It "literalpath with non filesystem path" { + $__data = "abcdefg" + Select-XML -literalPath variable:__data "Root" -ErrorVariable selectXmlError -ErrorAction SilentlyContinue + $selectXmlError.FullyQualifiedErrorId | Should Be 'ProcessingFile,Microsoft.PowerShell.Commands.SelectXmlCommand' + } + + It "path with non filesystem path" { + $__data = "abcdefg" + Select-XML -Path variable:\__data "Root" -ErrorVariable selectXmlError -ErrorAction SilentlyContinue + $selectXmlError.FullyQualifiedErrorId | Should Be 'ProcessingFile,Microsoft.PowerShell.Commands.SelectXmlCommand' + } + } +} diff --git a/test/powershell/Modules/map.json b/test/powershell/Modules/map.json new file mode 100644 index 000000000..9f91f5633 --- /dev/null +++ b/test/powershell/Modules/map.json @@ -0,0 +1,23 @@ +{ + "monad/tests/ci/PowerShell/tests/Commands/Cmdlets/FormatHex.Tests.ps1": "Microsoft.PowerShell.Utility/FormatHex.Tests.ps1", + "monad/tests/ci/PowerShell/tests/Commands/Cmdlets/ImportExportCSV.Delimiter.Tests.ps1": "Microsoft.PowerShell.Utility/ImportExportCSV.Delimiter.Tests.ps1", + "monad/tests/ci/PowerShell/tests/Commands/Cmdlets/MiscCmdletUpdates.Tests.ps1": "Microsoft.PowerShell.Utility/MiscCmdletUpdates.Tests.ps1", + "monad/tests/ci/PowerShell/tests/Commands/Cmdlets/NewGuid.Tests.ps1": "Microsoft.PowerShell.Utility/NewGuid.Tests.ps1", + "monad/tests/ci/PowerShell/tests/Commands/Cmdlets/NewTemporaryFile.Tests.ps1": "Microsoft.PowerShell.Utility/NewTemporaryFile.Tests.ps1", + "monad/tests/ci/PowerShell/tests/Commands/Cmdlets/Pester.Commands.Cmdlets.ArchiveTests.ps1": "Microsoft.PowerShell.Archive/Pester.Commands.Cmdlets.Archive.Tests.ps1", + "monad/tests/ci/PowerShell/tests/Commands/Cmdlets/SamplePreCreatedArchive.archive": "Microsoft.PowerShell.Archive/SamplePreCreatedArchive.archive", + "monad/tests/ci/PowerShell/tests/Commands/Cmdlets/Pester.Commands.Cmdlets.GetCommand.ps1": "Microsoft.PowerShell.Core/Pester.Commands.Cmdlets.GetCommand.Tests.ps1", + "monad/tests/ci/PowerShell/tests/Commands/Cmdlets/Pester.Commands.Cmdlets.JsonTests.ps1": "Microsoft.PowerShell.Utility/Pester.Commands.Cmdlets.Json.Tests.ps1", + "monad/tests/ci/PowerShell/tests/Commands/Cmdlets/Pester.Commands.Cmdlets.NoNewlineParameter.Tests.ps1": "Microsoft.PowerShell.Management/Pester.Commands.Cmdlets.NoNewlineParameter.Tests.ps1", + "monad/tests/ci/PowerShell/tests/Commands/Cmdlets/Pester.History.Tests.ps1": "Microsoft.PowerShell.Core/History.Tests.ps1", + "monad/tests/ci/PowerShell/tests/Commands/Cmdlets/Pester.Management.Copy.Item.Tests.ps1": "Microsoft.PowerShell.Management/Copy.Item.Tests.ps1", + "monad/tests/ci/PowerShell/tests/Commands/Cmdlets/pester.utility.alias.tests.ps1": "Microsoft.PowerShell.Utility/alias.tests.ps1", + "monad/tests/ci/PowerShell/tests/Commands/Cmdlets/pester.utility.clixml.tests.ps1": "Microsoft.PowerShell.Utility/clixml.tests.ps1", + "monad/tests/ci/PowerShell/tests/Commands/Cmdlets/pester.utility.command.tests.ps1": "Microsoft.PowerShell.Utility/command.tests.ps1", + "monad/tests/ci/PowerShell/tests/Commands/Cmdlets/pester.utility.formatdata.tests.ps1": "Microsoft.PowerShell.Utility/formatdata.tests.ps1", + "monad/tests/ci/PowerShell/tests/Commands/Cmdlets/pester.utility.object.tests.ps1": "Microsoft.PowerShell.Utility/object.tests.ps1", + "monad/tests/ci/PowerShell/tests/Commands/Cmdlets/Pester.Utility.PowerShellData.tests.ps1": "Microsoft.PowerShell.Utility/PowerShellData.tests.ps1", + "monad/tests/ci/PowerShell/tests/Commands/Cmdlets/pester.utility.string.tests.ps1": "Microsoft.PowerShell.Utility/string.tests.ps1", + "monad/tests/ci/PowerShell/tests/Commands/Cmdlets/pester.utility.typedata.tests.ps1": "Microsoft.PowerShell.Utility/typedata.tests.ps1", + "monad/tests/ci/PowerShell/tests/Commands/Cmdlets/pester.utility.xml.tests.ps1": "Microsoft.PowerShell.Utility/xml.tests.ps1", +}