From 2ca5050865f94e033fda850961439d8fcb01f468 Mon Sep 17 00:00:00 2001 From: Cory Hall <43035978+corymhall@users.noreply.github.com> Date: Wed, 20 Apr 2022 14:57:18 -0400 Subject: [PATCH] feat(integ-runner): add missing features from the integ manifest (#19969) Originally the integ-runner was built to work with the "legacy" integration tests since that is all we currently have. Since then we have published the `integ-tests` library which adds support for the integ manifest. This PR adds additional coverage for the integ manifest. To make the implementation a little cleaner I've extracted the logic that deals with the manifest into `IntegTestCases` and `LegacyIntegTestCases` classes. This allows the runner to just deal with the manifest and not have to worry about handling where it came from. I've also split up the `runners.ts`/`runners.test.ts` into `integ-test-runner.ts`, `runner-base.ts`, and `snapshot-test-runner.ts` Coverage added: 1. `stackUpdateWorkflow` 2. `diffAssets` 3. `hooks` ---- ### All Submissions: * [ ] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md) ### Adding new Unconventional Dependencies: * [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md/#adding-new-unconventional-dependencies) ### New Features * [ ] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/master/INTEGRATION_TESTS.md)? * [ ] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)? *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- INTEGRATION_TESTS.md | 38 + .../schema/cloud-assembly.version.json | 2 +- .../schema/integ.schema.json | 2 +- packages/@aws-cdk/integ-runner/README.md | 13 +- .../integ-runner/THIRD_PARTY_LICENSES | 2098 +++++++++++++++-- packages/@aws-cdk/integ-runner/lib/cli.ts | 17 +- .../@aws-cdk/integ-runner/lib/runner/index.ts | 7 +- .../lib/runner/integ-test-runner.ts | 289 +++ .../lib/runner/integ-test-suite.ts | 225 ++ .../{integ-tests.ts => integration-tests.ts} | 17 +- .../lib/runner/private/cloud-assembly.ts | 53 +- .../integ-runner/lib/runner/runner-base.ts | 380 +++ .../integ-runner/lib/runner/runners.ts | 869 ------- .../lib/runner/snapshot-test-runner.ts | 195 ++ .../lib/{runner/private => }/utils.ts | 14 + .../integ-runner/lib/workers/common.ts | 9 +- .../lib/workers/extract/extract_worker.ts | 8 +- .../lib/workers/integ-snapshot-worker.ts | 5 +- .../lib/workers/integ-test-worker.ts | 5 +- packages/@aws-cdk/integ-runner/package.json | 1 + ...ners.test.ts => integ-test-runner.test.ts} | 419 ++-- .../test/runner/integ-test-suite.test.ts | 288 +++ .../test/runner/integration-tests.test.ts | 22 +- .../runner/private/cloud-assembly.test.ts | 77 +- .../test/runner/snapshot-test-runner.test.ts | 154 ++ .../integ.test-with-snapshot-assets-diff.ts | 2 +- .../index.js | 0 .../manifest.json | 14 + .../index.js | 0 .../integ.json | 13 + .../manifest.json | 8 + .../test-stack.assets.json | 32 + .../test/workers/integ-worker.test.ts | 39 +- .../test/workers/snapshot-worker.test.ts | 1 + packages/@aws-cdk/integ-runner/tsconfig.json | 4 +- .../test/integ.newpipeline-with-vpc.ts | 4 +- ...nteg.pipeline-with-assets-single-upload.ts | 2 +- .../test/integ.pipeline-with-assets.ts | 2 +- .../@aws-cdk/pipelines/test/integ.pipeline.ts | 2 +- 39 files changed, 4048 insertions(+), 1282 deletions(-) create mode 100644 packages/@aws-cdk/integ-runner/lib/runner/integ-test-runner.ts create mode 100644 packages/@aws-cdk/integ-runner/lib/runner/integ-test-suite.ts rename packages/@aws-cdk/integ-runner/lib/runner/{integ-tests.ts => integration-tests.ts} (89%) create mode 100644 packages/@aws-cdk/integ-runner/lib/runner/runner-base.ts delete mode 100644 packages/@aws-cdk/integ-runner/lib/runner/runners.ts create mode 100644 packages/@aws-cdk/integ-runner/lib/runner/snapshot-test-runner.ts rename packages/@aws-cdk/integ-runner/lib/{runner/private => }/utils.ts (75%) rename packages/@aws-cdk/integ-runner/test/runner/{runners.test.ts => integ-test-runner.test.ts} (54%) create mode 100644 packages/@aws-cdk/integ-runner/test/runner/integ-test-suite.test.ts create mode 100644 packages/@aws-cdk/integ-runner/test/runner/snapshot-test-runner.test.ts create mode 100644 packages/@aws-cdk/integ-runner/test/test-data/test-with-snapshot-assets-diff.integ.snapshot/asset.fec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509/index.js create mode 100644 packages/@aws-cdk/integ-runner/test/test-data/test-with-snapshot-assets.integ.snapshot/asset.be270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824/index.js create mode 100644 packages/@aws-cdk/integ-runner/test/test-data/test-with-snapshot-assets.integ.snapshot/integ.json create mode 100644 packages/@aws-cdk/integ-runner/test/test-data/test-with-snapshot-assets.integ.snapshot/test-stack.assets.json diff --git a/INTEGRATION_TESTS.md b/INTEGRATION_TESTS.md index cf6038748e3f5..f6c95623d1093 100644 --- a/INTEGRATION_TESTS.md +++ b/INTEGRATION_TESTS.md @@ -10,6 +10,7 @@ on what type of changes require integrations tests and how you should write inte - [New L2 Constructs](#new-l2-constructs) - [Existing L2 Constructs](#existing-l2-constructs) - [Assertions](#assertions) +- [Running Integration Tests](#running-integration-tests) ## What are CDK Integration Tests @@ -223,3 +224,40 @@ to deploy the Lambda Function _and_ then rerun the assertions to ensure that the ### Assertions ...Coming soon... + +## Running Integration Tests + +Most of the time you will only need to run integration tests for an individual module (i.e. `aws-lambda`). Other times you may need to run tests across multiple modules. +In this case I would recommend running from the root directory like below. + +_Run snapshot tests only_ +```bash +yarn integ-runner --directory packages/@aws-cdk +``` + +_Run snapshot tests and then re-run integration tests for failed snapshots_ +```bash +yarn integ-runner --directory packages/@aws-cdk --update-on-failed +``` + +One benefit of running from the root directory like this is that it will only collect tests from "built" modules. If you have built the entire +repo it will run all integration tests, but if you have only built a couple modules it will only run tests from those. + +### Running large numbers of Tests + +If you need to re-run a large number of tests you can run them in parallel like this. + +```bash +yarn integ-runner --directory packages/@aws-cdk --update-on-failed \ + --parallel-regions us-east-1 \ + --parallel-regions us-east-2 \ + --parallel-regions us-west-2 \ + --parallel-regions eu-west-1 \ + --profiles profile1 \ + --profiles profile2 \ + --profiles profile3 \ + --verbose +``` + +When using both `--parallel-regions` and `--profiles` it will execute (regions*profiles) tests in parallel (in this example 12) +If you want to execute more than 16 tests in parallel you can pass a higher value to `--max-workers`. diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json index 90bef2e09ad39..e152247929af7 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json @@ -1 +1 @@ -{"version":"17.0.0"} \ No newline at end of file +{"version":"17.0.0"} diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/integ.schema.json b/packages/@aws-cdk/cloud-assembly-schema/schema/integ.schema.json index 676c1bdab91dd..5d0c88761a12e 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/integ.schema.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/integ.schema.json @@ -471,4 +471,4 @@ } }, "$schema": "http://json-schema.org/draft-07/schema#" -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/integ-runner/README.md b/packages/@aws-cdk/integ-runner/README.md index a0c68a97f99d3..3f983273a4a1e 100644 --- a/packages/@aws-cdk/integ-runner/README.md +++ b/packages/@aws-cdk/integ-runner/README.md @@ -44,9 +44,7 @@ to be a self contained CDK app. The runner will execute the following for each f - `--clean` (default=`true`) Destroy stacks after deploy (use `--no-clean` for debugging) - `--verbose` (default=`false`) - verbose logging -- `--parallel` (default=`true`) - Run tests in parallel across default regions + verbose logging, including integration test metrics - `--parallel-regions` (default=`us-east-1`,`us-east-2`, `us-west-2`) List of regions to run tests in. If this is provided then all tests will be run in parallel across these regions @@ -66,11 +64,18 @@ to be a self contained CDK app. The runner will execute the following for each f Example: ```bash -integ-runner --update --parallel --parallel-regions us-east-1 --parallel-regions us-east-2 --parallel-regions us-west-2 --directory ./ +integ-runner --update-on-failed --parallel-regions us-east-1 --parallel-regions us-east-2 --parallel-regions us-west-2 --directory ./ ``` This will search for integration tests recursively from the current directory and then execute them in parallel across `us-east-1`, `us-east-2`, & `us-west-2`. +If you are providing a list of tests to execute, either as CLI arguments or from a file, the name of the test needs to be relative to the `directory`. +For example, if there is a test `aws-iam/test/integ.policy.js` and the current working directory is `aws-iam` you would provide `integ.policy.js` + +```bash +yarn integ integ.policy.js +``` + ### Common Workflow A common workflow to use when running integration tests is to first run the integration tests to see if there are any snapshot differences. diff --git a/packages/@aws-cdk/integ-runner/THIRD_PARTY_LICENSES b/packages/@aws-cdk/integ-runner/THIRD_PARTY_LICENSES index f7e66a705aa8c..b7b9f8547743e 100644 --- a/packages/@aws-cdk/integ-runner/THIRD_PARTY_LICENSES +++ b/packages/@aws-cdk/integ-runner/THIRD_PARTY_LICENSES @@ -53,6 +53,58 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** archiver-utils@2.1.0 - https://www.npmjs.com/package/archiver-utils/v/2.1.0 | MIT +Copyright (c) 2015 Chris Talkington. + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +---------------- + +** archiver@5.3.0 - https://www.npmjs.com/package/archiver/v/5.3.0 | MIT +Copyright (c) 2012-2014 Chris Talkington, contributors. + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + ---------------- ** astral-regex@2.0.0 - https://www.npmjs.com/package/astral-regex/v/2.0.0 | MIT @@ -67,6 +119,30 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** async@3.2.3 - https://www.npmjs.com/package/async/v/3.2.3 | MIT +Copyright (c) 2010-2018 Caolan McMahon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + ---------------- ** at-least-node@1.0.0 - https://www.npmjs.com/package/at-least-node/v/1.0.0 | ISC @@ -78,6 +154,74 @@ Permission to use, copy, modify, and/or distribute this software for any purpose THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +---------------- + +** aws-sdk@2.1111.0 - https://www.npmjs.com/package/aws-sdk/v/2.1111.0 | Apache-2.0 +AWS SDK for JavaScript +Copyright 2012-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +This product includes software developed at +Amazon Web Services, Inc. (http://aws.amazon.com/). + + +---------------- + +** balanced-match@1.0.2 - https://www.npmjs.com/package/balanced-match/v/1.0.2 | MIT + +---------------- + +** bl@4.1.0 - https://www.npmjs.com/package/bl/v/4.1.0 | MIT + +---------------- + +** brace-expansion@1.1.11 - https://www.npmjs.com/package/brace-expansion/v/1.1.11 | MIT +MIT License + +Copyright (c) 2013 Julian Gruber + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** buffer-crc32@0.2.13 - https://www.npmjs.com/package/buffer-crc32/v/0.2.13 | MIT +The MIT License + +Copyright (c) 2013 Brian J. Brennan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ---------------- ** chalk@4.1.2 - https://www.npmjs.com/package/chalk/v/4.1.2 | MIT @@ -151,168 +295,400 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ---------------- -** diff@5.0.0 - https://www.npmjs.com/package/diff/v/5.0.0 | BSD-3-Clause -Software License Agreement (BSD License) - -Copyright (c) 2009-2015, Kevin Decker - -All rights reserved. - -Redistribution and use of this software in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above - copyright notice, this list of conditions and the - following disclaimer. - -* Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the - following disclaimer in the documentation and/or other - materials provided with the distribution. - -* Neither the name of Kevin Decker nor the names of its - contributors may be used to endorse or promote products - derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR -IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND -FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER -IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT -OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ----------------- - -** emoji-regex@8.0.0 - https://www.npmjs.com/package/emoji-regex/v/8.0.0 | MIT -Copyright Mathias Bynens +** compress-commons@4.1.1 - https://www.npmjs.com/package/compress-commons/v/4.1.1 | MIT +Copyright (c) 2014 Chris Talkington, contributors. -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. ---------------- -** escalade@3.1.1 - https://www.npmjs.com/package/escalade/v/3.1.1 | MIT -MIT License - -Copyright (c) Luke Edwards (lukeed.com) +** concat-map@0.0.1 - https://www.npmjs.com/package/concat-map/v/0.0.1 | MIT +This software is released under the MIT license: -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------- -** fast-deep-equal@3.1.3 - https://www.npmjs.com/package/fast-deep-equal/v/3.1.3 | MIT -MIT License - -Copyright (c) 2017 Evgeny Poberezkin +** core-util-is@1.0.3 - https://www.npmjs.com/package/core-util-is/v/1.0.3 | MIT +Copyright Node.js contributors. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. ---------------- -** fs-extra@9.1.0 - https://www.npmjs.com/package/fs-extra/v/9.1.0 | MIT -(The MIT License) +** crc-32@1.2.2 - https://www.npmjs.com/package/crc-32/v/1.2.2 | Apache-2.0 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -Copyright (c) 2011-2017 JP Richardson + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files -(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, - merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: + 1. Definitions. -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS -OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, - ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. ----------------- + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. -** get-caller-file@2.0.5 - https://www.npmjs.com/package/get-caller-file/v/2.0.5 | ISC + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. ----------------- + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. -** graceful-fs@4.2.10 - https://www.npmjs.com/package/graceful-fs/v/4.2.10 | ISC -The ISC License + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). -Copyright (c) 2011-2022 Isaac Z. Schlueter, Ben Noordhuis, and Contributors + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR -IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (C) 2014-present SheetJS LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ---------------- -** has-flag@4.0.0 - https://www.npmjs.com/package/has-flag/v/4.0.0 | MIT -MIT License +** crc32-stream@4.0.2 - https://www.npmjs.com/package/crc32-stream/v/4.0.2 | MIT +Copyright (c) 2014 Chris Talkington, contributors. -Copyright (c) Sindre Sorhus (sindresorhus.com) +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** diff@5.0.0 - https://www.npmjs.com/package/diff/v/5.0.0 | BSD-3-Clause +Software License Agreement (BSD License) + +Copyright (c) 2009-2015, Kevin Decker + +All rights reserved. + +Redistribution and use of this software in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above + copyright notice, this list of conditions and the + following disclaimer. + +* Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the + following disclaimer in the documentation and/or other + materials provided with the distribution. + +* Neither the name of Kevin Decker nor the names of its + contributors may be used to endorse or promote products + derived from this software without specific prior + written permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---------------- -** is-fullwidth-code-point@3.0.0 - https://www.npmjs.com/package/is-fullwidth-code-point/v/3.0.0 | MIT +** emoji-regex@8.0.0 - https://www.npmjs.com/package/emoji-regex/v/8.0.0 | MIT +Copyright Mathias Bynens + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** end-of-stream@1.4.4 - https://www.npmjs.com/package/end-of-stream/v/1.4.4 | MIT +The MIT License (MIT) + +Copyright (c) 2014 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------- + +** escalade@3.1.1 - https://www.npmjs.com/package/escalade/v/3.1.1 | MIT MIT License -Copyright (c) Sindre Sorhus (sindresorhus.com) +Copyright (c) Luke Edwards (lukeed.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: @@ -323,28 +699,289 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ---------------- -** jsonfile@6.1.0 - https://www.npmjs.com/package/jsonfile/v/6.1.0 | MIT -(The MIT License) +** fast-deep-equal@3.1.3 - https://www.npmjs.com/package/fast-deep-equal/v/3.1.3 | MIT +MIT License -Copyright (c) 2012-2015, JP Richardson +Copyright (c) 2017 Evgeny Poberezkin -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files -(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, - merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS -OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, - ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. ---------------- -** jsonschema@1.4.0 - https://www.npmjs.com/package/jsonschema/v/1.4.0 | MIT -jsonschema is licensed under MIT license. +** fs-constants@1.0.0 - https://www.npmjs.com/package/fs-constants/v/1.0.0 | MIT +The MIT License (MIT) + +Copyright (c) 2018 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** fs-extra@9.1.0 - https://www.npmjs.com/package/fs-extra/v/9.1.0 | MIT +(The MIT License) + +Copyright (c) 2011-2017 JP Richardson + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files +(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, + merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** fs.realpath@1.0.0 - https://www.npmjs.com/package/fs.realpath/v/1.0.0 | ISC +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +---- + +This library bundles a version of the `fs.realpath` and `fs.realpathSync` +methods from Node.js v0.10 under the terms of the Node.js MIT license. + +Node's license follows, also included at the header of `old.js` which contains +the licensed code: + + Copyright Joyent, Inc. and other Node contributors. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + +---------------- + +** get-caller-file@2.0.5 - https://www.npmjs.com/package/get-caller-file/v/2.0.5 | ISC + +---------------- + +** glob@7.2.0 - https://www.npmjs.com/package/glob/v/7.2.0 | ISC +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +## Glob Logo + +Glob's logo created by Tanya Brassie , licensed +under a Creative Commons Attribution-ShareAlike 4.0 International License +https://creativecommons.org/licenses/by-sa/4.0/ + + +---------------- + +** graceful-fs@4.2.10 - https://www.npmjs.com/package/graceful-fs/v/4.2.10 | ISC +The ISC License + +Copyright (c) 2011-2022 Isaac Z. Schlueter, Ben Noordhuis, and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** has-flag@4.0.0 - https://www.npmjs.com/package/has-flag/v/4.0.0 | MIT +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** inflight@1.0.6 - https://www.npmjs.com/package/inflight/v/1.0.6 | ISC +The ISC License + +Copyright (c) Isaac Z. Schlueter + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** inherits@2.0.4 - https://www.npmjs.com/package/inherits/v/2.0.4 | ISC +The ISC License + +Copyright (c) Isaac Z. Schlueter + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + + +---------------- + +** is-fullwidth-code-point@3.0.0 - https://www.npmjs.com/package/is-fullwidth-code-point/v/3.0.0 | MIT +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** isarray@1.0.0 - https://www.npmjs.com/package/isarray/v/1.0.0 | MIT + +---------------- + +** jmespath@0.16.0 - https://www.npmjs.com/package/jmespath/v/0.16.0 | Apache-2.0 +Copyright 2014 James Saryerwinnie + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +---------------- + +** jsonfile@6.1.0 - https://www.npmjs.com/package/jsonfile/v/6.1.0 | MIT +(The MIT License) + +Copyright (c) 2012-2015, JP Richardson + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files +(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, + merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** jsonschema@1.4.0 - https://www.npmjs.com/package/jsonschema/v/1.4.0 | MIT +jsonschema is licensed under MIT license. Copyright (C) 2012-2015 Tom de Grunt @@ -355,76 +992,894 @@ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** lazystream@1.0.1 - https://www.npmjs.com/package/lazystream/v/1.0.1 | MIT +Copyright (c) 2013 J. Pommerening, contributors. + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + + +---------------- + +** lodash.defaults@4.2.0 - https://www.npmjs.com/package/lodash.defaults/v/4.2.0 | MIT +Copyright jQuery Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. + + +---------------- + +** lodash.difference@4.5.0 - https://www.npmjs.com/package/lodash.difference/v/4.5.0 | MIT +Copyright jQuery Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. + + +---------------- + +** lodash.flatten@4.4.0 - https://www.npmjs.com/package/lodash.flatten/v/4.4.0 | MIT +Copyright jQuery Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. + + +---------------- + +** lodash.isplainobject@4.0.6 - https://www.npmjs.com/package/lodash.isplainobject/v/4.0.6 | MIT +Copyright jQuery Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. + + +---------------- + +** lodash.truncate@4.4.2 - https://www.npmjs.com/package/lodash.truncate/v/4.4.2 | MIT +Copyright jQuery Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. + + +---------------- + +** lodash.union@4.6.0 - https://www.npmjs.com/package/lodash.union/v/4.6.0 | MIT +Copyright jQuery Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. + + +---------------- + +** lru-cache@7.8.0 - https://www.npmjs.com/package/lru-cache/v/7.8.0 | ISC +The ISC License + +Copyright (c) 2010-2022 Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** mime@2.6.0 - https://www.npmjs.com/package/mime/v/2.6.0 | MIT +The MIT License (MIT) + +Copyright (c) 2010 Benjamin Thomas, Robert Kieffer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** minimatch@3.1.2 - https://www.npmjs.com/package/minimatch/v/3.1.2 | ISC +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** normalize-path@3.0.0 - https://www.npmjs.com/package/normalize-path/v/3.0.0 | MIT +The MIT License (MIT) + +Copyright (c) 2014-2018, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** once@1.4.0 - https://www.npmjs.com/package/once/v/1.4.0 | ISC +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** path-is-absolute@1.0.1 - https://www.npmjs.com/package/path-is-absolute/v/1.0.1 | MIT +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** process-nextick-args@2.0.1 - https://www.npmjs.com/package/process-nextick-args/v/2.0.1 | MIT + +---------------- + +** readable-stream@2.3.7 - https://www.npmjs.com/package/readable-stream/v/2.3.7 | MIT +Node.js is licensed for use as follows: + +""" +Copyright Node.js contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + +This license applies to parts of Node.js originating from the +https://github.com/joyent/node repository: + +""" +Copyright Joyent, Inc. and other Node contributors. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + + +---------------- + +** readable-stream@3.6.0 - https://www.npmjs.com/package/readable-stream/v/3.6.0 | MIT +Node.js is licensed for use as follows: + +""" +Copyright Node.js contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + +This license applies to parts of Node.js originating from the +https://github.com/joyent/node repository: + +""" +Copyright Joyent, Inc. and other Node contributors. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + + +---------------- + +** readdir-glob@1.1.1 - https://www.npmjs.com/package/readdir-glob/v/1.1.1 | Apache-2.0 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 Yann Armelin + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +---------------- + +** require-directory@2.1.1 - https://www.npmjs.com/package/require-directory/v/2.1.1 | MIT +The MIT License (MIT) + +Copyright (c) 2011 Troy Goode + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** safe-buffer@5.1.2 - https://www.npmjs.com/package/safe-buffer/v/5.1.2 | MIT +The MIT License (MIT) + +Copyright (c) Feross Aboukhadijeh + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. ---------------- -** lodash.truncate@4.4.2 - https://www.npmjs.com/package/lodash.truncate/v/4.4.2 | MIT -Copyright jQuery Foundation and other contributors - -Based on Underscore.js, copyright Jeremy Ashkenas, -DocumentCloud and Investigative Reporters & Editors - -This software consists of voluntary contributions made by many -individuals. For exact contribution history, see the revision history -available at https://github.com/lodash/lodash - -The following license applies to all parts of this software except as -documented below: - -==== - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -==== +** safe-buffer@5.2.1 - https://www.npmjs.com/package/safe-buffer/v/5.2.1 | MIT +The MIT License (MIT) -Copyright and related rights for sample code are waived via CC0. Sample -code is defined as all source code displayed within the prose of the -documentation. +Copyright (c) Feross Aboukhadijeh -CC0: http://creativecommons.org/publicdomain/zero/1.0/ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -==== +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. -Files located in the node_modules and vendor directories are externally -maintained libraries used by this software which have their own -licenses; we recommend you read them, as their terms may differ from the -terms above. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. ---------------- -** lru-cache@7.8.0 - https://www.npmjs.com/package/lru-cache/v/7.8.0 | ISC +** sax@1.2.4 - https://www.npmjs.com/package/sax/v/1.2.4 | ISC The ISC License -Copyright (c) 2010-2022 Isaac Z. Schlueter and Contributors +Copyright (c) Isaac Z. Schlueter and Contributors Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above @@ -438,32 +1893,31 @@ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +==== ----------------- - -** require-directory@2.1.1 - https://www.npmjs.com/package/require-directory/v/2.1.1 | MIT -The MIT License (MIT) +`String.fromCodePoint` by Mathias Bynens used according to terms of MIT +License, as follows: -Copyright (c) 2011 Troy Goode + Copyright Mathias Bynens -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Software. + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------- @@ -501,6 +1955,112 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** string_decoder@1.1.1 - https://www.npmjs.com/package/string_decoder/v/1.1.1 | MIT +Node.js is licensed for use as follows: + +""" +Copyright Node.js contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + +This license applies to parts of Node.js originating from the +https://github.com/joyent/node repository: + +""" +Copyright Joyent, Inc. and other Node contributors. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + + + +---------------- + +** string_decoder@1.3.0 - https://www.npmjs.com/package/string_decoder/v/1.3.0 | MIT +Node.js is licensed for use as follows: + +""" +Copyright Node.js contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + +This license applies to parts of Node.js originating from the +https://github.com/joyent/node repository: + +""" +Copyright Joyent, Inc. and other Node contributors. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + + + ---------------- ** string-width@4.2.3 - https://www.npmjs.com/package/string-width/v/4.2.3 | MIT @@ -572,6 +2132,31 @@ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +---------------- + +** tar-stream@2.2.0 - https://www.npmjs.com/package/tar-stream/v/2.2.0 | MIT +The MIT License (MIT) + +Copyright (c) 2014 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + ---------------- ** universalify@2.0.0 - https://www.npmjs.com/package/universalify/v/2.0.0 | MIT @@ -597,6 +2182,39 @@ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** util-deprecate@1.0.2 - https://www.npmjs.com/package/util-deprecate/v/1.0.2 | MIT +(The MIT License) + +Copyright (c) 2014 Nathan Rajlich + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** uuid@3.3.2 - https://www.npmjs.com/package/uuid/v/3.3.2 | MIT + ---------------- ** workerpool@6.2.0 - https://www.npmjs.com/package/workerpool/v/6.2.0 | Apache-2.0 @@ -816,6 +2434,76 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** wrappy@1.0.2 - https://www.npmjs.com/package/wrappy/v/1.0.2 | ISC +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** xml2js@0.4.19 - https://www.npmjs.com/package/xml2js/v/0.4.19 | MIT +Copyright 2010, 2011, 2012, 2013. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + + +---------------- + +** xmlbuilder@9.0.7 - https://www.npmjs.com/package/xmlbuilder/v/9.0.7 | MIT +The MIT License (MIT) + +Copyright (c) 2013 Ozgur Ozcitak + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + ---------------- ** y18n@5.0.8 - https://www.npmjs.com/package/y18n/v/5.0.8 | ISC @@ -879,4 +2567,30 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** zip-stream@4.1.0 - https://www.npmjs.com/package/zip-stream/v/4.1.0 | MIT +Copyright (c) 2014 Chris Talkington, contributors. + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + ---------------- diff --git a/packages/@aws-cdk/integ-runner/lib/cli.ts b/packages/@aws-cdk/integ-runner/lib/cli.ts index 3bfbcd0c2b79a..e0deb9d9f8a51 100644 --- a/packages/@aws-cdk/integ-runner/lib/cli.ts +++ b/packages/@aws-cdk/integ-runner/lib/cli.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import * as chalk from 'chalk'; import * as workerpool from 'workerpool'; import * as logger from './logger'; -import { IntegrationTests, IntegTestConfig } from './runner/integ-tests'; +import { IntegrationTests, IntegTestConfig } from './runner/integration-tests'; import { runSnapshotTests, runIntegrationTests, IntegRunnerMetrics, IntegTestWorkerConfig, DestructiveChange } from './workers'; // https://github.com/yargs/yargs/issues/1929 @@ -17,13 +17,12 @@ async function main() { .usage('Usage: integ-runner [TEST...]') .option('list', { type: 'boolean', default: false, desc: 'List tests instead of running them' }) .option('clean', { type: 'boolean', default: true, desc: 'Skips stack clean up after test is completed (use --no-clean to negate)' }) - .option('verbose', { type: 'boolean', default: false, alias: 'v', desc: 'Verbose logs' }) + .option('verbose', { type: 'boolean', default: false, alias: 'v', desc: 'Verbose logs and metrics on integration tests durations' }) .option('dry-run', { type: 'boolean', default: false, desc: 'do not actually deploy the stack. just update the snapshot (not recommended!)' }) .option('update-on-failed', { type: 'boolean', default: false, desc: 'rerun integration tests and update snapshots for failed tests.' }) .option('force', { type: 'boolean', default: false, desc: 'Rerun all integration tests even if tests are passing' }) - .option('parallel', { type: 'boolean', default: false, desc: 'run integration tests in parallel' }) - .option('parallel-regions', { type: 'array', desc: 'if --parallel is used then these regions are used to run tests in parallel', nargs: 1, default: [] }) - .options('directory', { type: 'string', default: 'test', desc: 'starting directory to discover integration tests' }) + .option('parallel-regions', { type: 'array', desc: 'Tests are run in parallel across these regions. To prevent tests from running in parallel, provide only a single region', nargs: 1, default: [] }) + .options('directory', { type: 'string', default: 'test', desc: 'starting directory to discover integration tests. Tests will be discovered recursively from this directory' }) .options('profiles', { type: 'array', desc: 'list of AWS profiles to use. Tests will be run in parallel across each profile+regions', nargs: 1, default: [] }) .options('max-workers', { type: 'number', desc: 'The max number of workerpool workers to use when running integration tests in parallel', default: 16 }) .options('exclude', { type: 'boolean', desc: 'All tests should be run, except for the list of tests provided', default: false }) @@ -61,11 +60,11 @@ async function main() { if (argv._.length > 0 && fromFile) { throw new Error('A list of tests cannot be provided if "--from-file" is provided'); } else if (argv._.length === 0 && !fromFile) { - testsFromArgs.push(...(await new IntegrationTests(argv.directory).fromCliArgs())); + testsFromArgs.push(...(await new IntegrationTests(path.resolve(argv.directory)).fromCliArgs())); } else if (fromFile) { - testsFromArgs.push(...(await new IntegrationTests(argv.directory).fromFile(fromFile))); + testsFromArgs.push(...(await new IntegrationTests(path.resolve(argv.directory)).fromFile(path.resolve(fromFile)))); } else { - testsFromArgs.push(...(await new IntegrationTests(argv.directory).fromCliArgs(argv._.map((x: any) => x.toString()), exclude))); + testsFromArgs.push(...(await new IntegrationTests(path.resolve(argv.directory)).fromCliArgs(argv._.map((x: any) => x.toString()), exclude))); } // always run snapshot tests, but if '--force' is passed then @@ -93,7 +92,7 @@ async function main() { clean: argv.clean, dryRun: argv['dry-run'], verbose: argv.verbose, - updateWorkflow: !argv['disable-update-workflow'], + updateWorkflow: !!argv['disable-update-workflow'], }); if (argv.clean === false) { diff --git a/packages/@aws-cdk/integ-runner/lib/runner/index.ts b/packages/@aws-cdk/integ-runner/lib/runner/index.ts index 02b61b3dfe184..6445fb09b72eb 100644 --- a/packages/@aws-cdk/integ-runner/lib/runner/index.ts +++ b/packages/@aws-cdk/integ-runner/lib/runner/index.ts @@ -1,2 +1,5 @@ -export * from './runners'; -export * from './integ-tests'; +export * from './runner-base'; +export * from './integ-test-suite'; +export * from './integ-test-runner'; +export * from './snapshot-test-runner'; +export * from './integration-tests'; diff --git a/packages/@aws-cdk/integ-runner/lib/runner/integ-test-runner.ts b/packages/@aws-cdk/integ-runner/lib/runner/integ-test-runner.ts new file mode 100644 index 0000000000000..ca8e0b18a33e8 --- /dev/null +++ b/packages/@aws-cdk/integ-runner/lib/runner/integ-test-runner.ts @@ -0,0 +1,289 @@ +import * as path from 'path'; +import { TestCase, RequireApproval } from '@aws-cdk/cloud-assembly-schema'; +import { DeployOptions, DestroyOptions } from 'cdk-cli-wrapper'; +import * as logger from '../logger'; +import { chain, exec } from '../utils'; +import { DestructiveChange } from '../workers/common'; +import { IntegRunnerOptions, IntegRunner, DEFAULT_SYNTH_OPTIONS } from './runner-base'; + +/** + * Options for the integration test runner + */ +export interface RunOptions { + /** + * The test case to execute + */ + readonly testCase: TestCase; + + /** + * Whether or not to run `cdk destroy` and cleanup the + * integration test stacks. + * + * Set this to false if you need to perform any validation + * or troubleshooting after deployment. + * + * @default true + */ + readonly clean?: boolean; + + /** + * If set to true, the integration test will not deploy + * anything and will simply update the snapshot. + * + * You should NOT use this method since you are essentially + * bypassing the integration test. + * + * @default false + */ + readonly dryRun?: boolean; + + /** + * If this is set to false then the stack update workflow will + * not be run + * + * The update workflow exists to check for cases where a change would cause + * a failure to an existing stack, but not for a newly created stack. + * + * @default true + */ + readonly updateWorkflow?: boolean; +} + +/** + * An integration test runner that orchestrates executing + * integration tests + */ +export class IntegTestRunner extends IntegRunner { + constructor(options: IntegRunnerOptions, destructiveChanges?: DestructiveChange[]) { + super(options); + this._destructiveChanges = destructiveChanges; + } + + /** + * When running integration tests with the update path workflow + * it is important that the snapshot that is deployed is the current snapshot + * from the upstream branch. In order to guarantee that, first checkout the latest + * (to the user) snapshot from upstream + * + * It is not straightforward to figure out what branch the current + * working branch was created from. This is a best effort attempt to do so. + * This assumes that there is an 'origin'. `git remote show origin` returns a list of + * all branches and we then search for one that starts with `HEAD branch: ` + */ + private checkoutSnapshot(): void { + const cwd = path.dirname(this.snapshotDir); + // https://git-scm.com/docs/git-merge-base + let baseBranch: string | undefined = undefined; + // try to find the base branch that the working branch was created from + try { + const origin: string = exec(['git', 'remote', 'show', 'origin'], { + cwd, + }); + const originLines = origin.split('\n'); + for (const line of originLines) { + if (line.trim().startsWith('HEAD branch: ')) { + baseBranch = line.trim().split('HEAD branch: ')[1]; + } + } + } catch (e) { + logger.warning('%s\n%s', + 'Could not determine git origin branch.', + `You need to manually checkout the snapshot directory ${this.snapshotDir}` + + 'from the merge-base (https://git-scm.com/docs/git-merge-base)', + ); + logger.warning('error: %s', e); + } + + // if we found the base branch then get the merge-base (most recent common commit) + // and checkout the snapshot using that commit + if (baseBranch) { + try { + const base = exec(['git', 'merge-base', 'HEAD', baseBranch], { + cwd, + }); + exec(['git', 'checkout', base, '--', this.relativeSnapshotDir], { + cwd, + }); + } catch (e) { + logger.warning('%s\n%s', + `Could not checkout snapshot directory ${this.snapshotDir} using these commands: `, + `git merge-base HEAD ${baseBranch} && git checkout {merge-base} -- ${this.relativeSnapshotDir}`, + ); + logger.warning('error: %s', e); + } + } + } + + /** + * Orchestrates running integration tests. Currently this includes + * + * 1. (if update workflow is enabled) Deploying the snapshot test stacks + * 2. Deploying the integration test stacks + * 2. Saving the snapshot (if successful) + * 3. Destroying the integration test stacks (if clean=false) + * + * The update workflow exists to check for cases where a change would cause + * a failure to an existing stack, but not for a newly created stack. + */ + public runIntegTestCase(options: RunOptions): void { + const clean = options.clean ?? true; + const updateWorkflowEnabled = (options.updateWorkflow ?? true) && (options.testCase.stackUpdateWorkflow ?? true); + try { + if (!options.dryRun && (options.testCase.cdkCommandOptions?.deploy?.enabled ?? true)) { + this.deploy( + { + ...this.defaultArgs, + profile: this.profile, + stacks: options.testCase.stacks, + requireApproval: RequireApproval.NEVER, + output: this.cdkOutDir, + lookups: this.testSuite?.enableLookups, + ...options.testCase.cdkCommandOptions?.deploy?.args, + context: this.getContext(options.testCase.cdkCommandOptions?.deploy?.args?.context), + }, + updateWorkflowEnabled, + options.testCase, + ); + } else { + const env: Record = { + ...DEFAULT_SYNTH_OPTIONS.env, + CDK_CONTEXT_JSON: JSON.stringify(this.getContext()), + }; + this.cdk.synthFast({ + execCmd: this.cdkApp.split(' '), + env, + output: this.cdkOutDir, + }); + } + this.createSnapshot(); + } catch (e) { + throw e; + } finally { + if (!options.dryRun) { + if (clean && (options.testCase.cdkCommandOptions?.destroy?.enabled ?? true)) { + this.destroy(options.testCase, { + ...this.defaultArgs, + profile: this.profile, + stacks: options.testCase.stacks, + force: true, + app: this.cdkApp, + output: this.cdkOutDir, + ...options.testCase.cdkCommandOptions?.destroy?.args, + context: this.getContext(options.testCase.cdkCommandOptions?.destroy?.args?.context), + }); + } + } + this.cleanup(); + } + } + + /** + * Perform a integ test case stack destruction + */ + private destroy(testCase: TestCase, destroyArgs: DestroyOptions) { + try { + if (testCase.hooks?.preDestroy) { + exec([chain(testCase.hooks.preDestroy)], { + cwd: path.dirname(this.snapshotDir), + }); + } + this.cdk.destroy({ + ...destroyArgs, + }); + + if (testCase.hooks?.postDestroy) { + exec([chain(testCase.hooks.postDestroy)], { + cwd: path.dirname(this.snapshotDir), + }); + } + } catch (e) { + this.parseError(e, + testCase.cdkCommandOptions?.destroy?.expectError ?? false, + testCase.cdkCommandOptions?.destroy?.expectedMessage, + ); + } + } + + /** + * Perform a integ test case deployment, including + * peforming the update workflow + */ + private deploy( + deployArgs: DeployOptions, + updateWorkflowEnabled: boolean, + testCase: TestCase, + ): void { + try { + if (testCase.hooks?.preDeploy) { + exec([chain(testCase.hooks?.preDeploy)], { + cwd: path.dirname(this.snapshotDir), + }); + } + // if the update workflow is not disabled, first + // perform a deployment with the exising snapshot + // then perform a deployment (which will be a stack update) + // with the current integration test + // We also only want to run the update workflow if there is an existing + // snapshot (otherwise there is nothing to update) + if (updateWorkflowEnabled && this.hasSnapshot()) { + // make sure the snapshot is the latest from 'origin' + this.checkoutSnapshot(); + this.cdk.deploy({ + ...deployArgs, + app: this.relativeSnapshotDir, + }); + } + this.cdk.deploy({ + ...deployArgs, + app: this.cdkApp, + }); + if (testCase.hooks?.postDeploy) { + exec([chain(testCase.hooks?.postDeploy)], { + cwd: path.dirname(this.snapshotDir), + }); + } + } catch (e) { + this.parseError(e, + testCase.cdkCommandOptions?.deploy?.expectError ?? false, + testCase.cdkCommandOptions?.deploy?.expectedMessage, + ); + } + } + + /** + * Parses an error message returned from a CDK command + */ + private parseError(e: unknown, expectError: boolean, expectedMessage?: string) { + if (expectError) { + if (expectedMessage) { + const message = (e as Error).message; + if (!message.match(expectedMessage)) { + throw (e); + } + } + } else { + throw e; + } + } + + /** + * Generate a snapshot if one does not exist + * This will synth and then load the integration test manifest + */ + public generateSnapshot(): void { + if (this.hasSnapshot()) { + throw new Error(`${this.testName} already has a snapshot: ${this.snapshotDir}`); + } + + this.cdk.synthFast({ + execCmd: this.cdkApp.split(' '), + env: { + ...DEFAULT_SYNTH_OPTIONS.env, + CDK_CONTEXT_JSON: JSON.stringify(this.getContext()), + }, + output: this.cdkOutDir, + }); + this.loadManifest(this.cdkOutDir); + } +} + diff --git a/packages/@aws-cdk/integ-runner/lib/runner/integ-test-suite.ts b/packages/@aws-cdk/integ-runner/lib/runner/integ-test-suite.ts new file mode 100644 index 0000000000000..66bdce62d3678 --- /dev/null +++ b/packages/@aws-cdk/integ-runner/lib/runner/integ-test-suite.ts @@ -0,0 +1,225 @@ +import { TestCase, TestOptions } from '@aws-cdk/cloud-assembly-schema'; +import { ICdk, ListOptions } from 'cdk-cli-wrapper'; +import * as fs from 'fs-extra'; +import { IntegManifestReader } from './private/integ-manifest'; + +const CDK_INTEG_STACK_PRAGMA = '/// !cdk-integ'; +const PRAGMA_PREFIX = 'pragma:'; +const SET_CONTEXT_PRAGMA_PREFIX = 'pragma:set-context:'; +const VERIFY_ASSET_HASHES = 'pragma:include-assets-hashes'; +const DISABLE_UPDATE_WORKFLOW = 'pragma:disable-update-workflow'; +const ENABLE_LOOKUPS_PRAGMA = 'pragma:enable-lookups'; + +/** + * Represents an integration test + */ +export type TestSuite = { [testName: string]: TestCase }; + +/** + * Helper class for working with Integration tests + * This requires an `integ.json` file in the snapshot + * directory. For legacy test cases use LegacyIntegTestCases + */ +export class IntegTestSuite { + + /** + * Loads integ tests from a snapshot directory + */ + public static fromPath(path: string): IntegTestSuite { + const reader = IntegManifestReader.fromPath(path); + return new IntegTestSuite( + reader.tests.enableLookups, + reader.tests.testCases, + ); + } + + constructor( + public readonly enableLookups: boolean, + public readonly testSuite: TestSuite, + ) {} + + /** + * Returns a list of stacks that have stackUpdateWorkflow disabled + */ + public getStacksWithoutUpdateWorkflow(): string[] { + return Object.values(this.testSuite) + .filter(testCase => !(testCase.stackUpdateWorkflow ?? true)) + .flatMap((testCase: TestCase) => testCase.stacks); + } + + /** + * Returns test case options for a given stack + */ + public getOptionsForStack(stackId: string): TestOptions | undefined { + for (const testCase of Object.values(this.testSuite ?? {})) { + if (testCase.stacks.includes(stackId)) { + return { + hooks: testCase.hooks, + regions: testCase.regions, + diffAssets: testCase.diffAssets ?? false, + allowDestroy: testCase.allowDestroy, + cdkCommandOptions: testCase.cdkCommandOptions, + stackUpdateWorkflow: testCase.stackUpdateWorkflow ?? true, + }; + } + } + return undefined; + } +} + +/** + * Options for a reading a legacy test case manifest + */ +export interface LegacyTestCaseConfig { + /** + * The name of the test case + */ + readonly testName: string; + + /** + * Options to use when performing `cdk list` + * This is used to determine the name of the stacks + * in the test case + */ + readonly listOptions: ListOptions; + + /** + * An instance of the CDK CLI (e.g. CdkCliWrapper) + */ + readonly cdk: ICdk; + + /** + * The path to the integration test file + * i.e. integ.test.js + */ + readonly integSourceFilePath: string; +} + +/** + * Helper class for creating an integ manifest for legacy + * test cases, i.e. tests without a `integ.json`. + */ +export class LegacyIntegTestSuite extends IntegTestSuite { + + /** + * Returns the single test stack to use. + * + * If the test has a single stack, it will be chosen. Otherwise a pragma is expected within the + * test file the name of the stack: + * + * @example + * + * /// !cdk-integ + * + */ + public static fromLegacy(config: LegacyTestCaseConfig): LegacyIntegTestSuite { + const pragmas = this.pragmas(config.integSourceFilePath); + const tests: TestCase = { + stacks: [], + diffAssets: pragmas.includes(VERIFY_ASSET_HASHES), + stackUpdateWorkflow: !pragmas.includes(DISABLE_UPDATE_WORKFLOW), + }; + const pragma = this.readStackPragma(config.integSourceFilePath); + if (pragma.length > 0) { + tests.stacks.push(...pragma); + } else { + const stacks = (config.cdk.list({ + ...config.listOptions, + })).split('\n'); + if (stacks.length !== 1) { + throw new Error('"cdk-integ" can only operate on apps with a single stack.\n\n' + + ' If your app has multiple stacks, specify which stack to select by adding this to your test source:\n\n' + + ` ${CDK_INTEG_STACK_PRAGMA} STACK ...\n\n` + + ` Available stacks: ${stacks.join(' ')} (wildcards are also supported)\n`); + } + if (stacks.length === 1 && stacks[0] === '') { + throw new Error(`No stack found for test ${config.testName}`); + } + tests.stacks.push(...stacks); + } + + return new LegacyIntegTestSuite( + pragmas.includes(ENABLE_LOOKUPS_PRAGMA), + { + [config.testName]: tests, + }, + ); + } + + public static getPragmaContext(integSourceFilePath: string): Record { + const ctxPragmaContext: Record = {}; + + // apply context from set-context pragma + // usage: pragma:set-context:key=value + const ctxPragmas = (this.pragmas(integSourceFilePath)).filter(p => p.startsWith(SET_CONTEXT_PRAGMA_PREFIX)); + for (const p of ctxPragmas) { + const instruction = p.substring(SET_CONTEXT_PRAGMA_PREFIX.length); + const [key, value] = instruction.split('='); + if (key == null || value == null) { + throw new Error(`invalid "set-context" pragma syntax. example: "pragma:set-context:@aws-cdk/core:newStyleStackSynthesis=true" got: ${p}`); + } + + ctxPragmaContext[key] = value; + } + return { + ...ctxPragmaContext, + }; + } + + + /** + * Reads stack names from the "!cdk-integ" pragma. + * + * Every word that's NOT prefixed by "pragma:" is considered a stack name. + * + * @example + * + * /// !cdk-integ + */ + private static readStackPragma(integSourceFilePath: string): string[] { + return (this.readIntegPragma(integSourceFilePath)).filter(p => !p.startsWith(PRAGMA_PREFIX)); + } + + /** + * Read arbitrary cdk-integ pragma directives + * + * Reads the test source file and looks for the "!cdk-integ" pragma. If it exists, returns it's + * contents. This allows integ tests to supply custom command line arguments to "cdk deploy" and "cdk synth". + * + * @example + * + * /// !cdk-integ [...] + */ + private static readIntegPragma(integSourceFilePath: string): string[] { + const source = fs.readFileSync(integSourceFilePath, { encoding: 'utf-8' }); + const pragmaLine = source.split('\n').find(x => x.startsWith(CDK_INTEG_STACK_PRAGMA + ' ')); + if (!pragmaLine) { + return []; + } + + const args = pragmaLine.substring(CDK_INTEG_STACK_PRAGMA.length).trim().split(' '); + if (args.length === 0) { + throw new Error(`Invalid syntax for cdk-integ pragma. Usage: "${CDK_INTEG_STACK_PRAGMA} [STACK] [pragma:PRAGMA] [...]"`); + } + return args; + } + + /** + * Return the non-stack pragmas + * + * These are all pragmas that start with "pragma:". + * + * For backwards compatibility reasons, all pragmas that DON'T start with this + * string are considered to be stack names. + */ + private static pragmas(integSourceFilePath: string): string[] { + return (this.readIntegPragma(integSourceFilePath)).filter(p => p.startsWith(PRAGMA_PREFIX)); + } + + constructor( + public readonly enableLookups: boolean, + public readonly testSuite: TestSuite, + ) { + super(enableLookups, testSuite); + } +} diff --git a/packages/@aws-cdk/integ-runner/lib/runner/integ-tests.ts b/packages/@aws-cdk/integ-runner/lib/runner/integration-tests.ts similarity index 89% rename from packages/@aws-cdk/integ-runner/lib/runner/integ-tests.ts rename to packages/@aws-cdk/integ-runner/lib/runner/integration-tests.ts index 39c0ea197d073..98511a76daff8 100644 --- a/packages/@aws-cdk/integ-runner/lib/runner/integ-tests.ts +++ b/packages/@aws-cdk/integ-runner/lib/runner/integration-tests.ts @@ -11,6 +11,12 @@ export interface IntegTestConfig { * of integ.{test-name}.js */ readonly fileName: string; + + /** + * The base directory where the tests are + * discovered from + */ + readonly directory: string; } /** @@ -63,19 +69,20 @@ export class IntegrationTests { if (!requestedTests || requestedTests.length === 0) { return discoveredTests; } - const all = discoveredTests.map(x => x.fileName); + const all = discoveredTests.map(x => { + return path.relative(x.directory, x.fileName); + }); let foundAll = true; // Pare down found tests to filter const allTests = discoveredTests.filter(t => { - const parts = path.parse(t.fileName); if (exclude) { - return (!requestedTests.includes(t.fileName) && !requestedTests.includes(parts.base)); + return (!requestedTests.includes(path.relative(t.directory, t.fileName))); } - return (requestedTests.includes(t.fileName) || requestedTests.includes(parts.base)); + return (requestedTests.includes(path.relative(t.directory, t.fileName))); }); if (!exclude) { - const selectedNames = allTests.map(t => t.fileName); + const selectedNames = allTests.map(t => path.relative(t.directory, t.fileName)); for (const unmatched of requestedTests.filter(t => !selectedNames.includes(t))) { process.stderr.write(`No such integ test: ${unmatched}\n`); foundAll = false; diff --git a/packages/@aws-cdk/integ-runner/lib/runner/private/cloud-assembly.ts b/packages/@aws-cdk/integ-runner/lib/runner/private/cloud-assembly.ts index b4e404619b6f1..56f2bdcfa4445 100644 --- a/packages/@aws-cdk/integ-runner/lib/runner/private/cloud-assembly.ts +++ b/packages/@aws-cdk/integ-runner/lib/runner/private/cloud-assembly.ts @@ -1,5 +1,6 @@ import * as path from 'path'; -import { AssemblyManifest, Manifest, ArtifactType, AwsCloudFormationStackProperties, ArtifactManifest, MetadataEntry } from '@aws-cdk/cloud-assembly-schema'; +import { AssemblyManifest, Manifest, ArtifactType, AwsCloudFormationStackProperties, ArtifactManifest, MetadataEntry, AssetManifestProperties, ArtifactMetadataEntryType, ContainerImageAssetMetadataEntry, FileAssetMetadataEntry } from '@aws-cdk/cloud-assembly-schema'; +import { AssetManifest, FileManifestEntry, DockerImageManifestEntry } from 'cdk-assets'; import * as fs from 'fs-extra'; /** @@ -88,6 +89,56 @@ export class AssemblyManifestReader { Manifest.saveAssemblyManifest(newManifest, this.manifestFileName); } + /** + * For a given stackId return a list of assets that belong to the stack + */ + public getAssetsForStack(stackId: string): string[] { + const assets: string[] = []; + for (const artifact of Object.values(this.manifest.artifacts ?? {})) { + if (artifact.type === ArtifactType.ASSET_MANIFEST && (artifact.properties as AssetManifestProperties)?.file === `${stackId}.assets.json`) { + assets.push(...this.assetsFromAssetManifest(artifact)); + } else if (artifact.type === ArtifactType.AWS_CLOUDFORMATION_STACK) { + assets.push(...this.assetsFromAssemblyManifest(artifact)); + } + } + return assets; + } + + private assetsFromAssemblyManifest(artifact: ArtifactManifest): string[] { + const assets: string[] = []; + for (const metadata of Object.values(artifact.metadata ?? {})) { + metadata.forEach(data => { + if (data.type === ArtifactMetadataEntryType.ASSET) { + const assetPath = (data.data as ContainerImageAssetMetadataEntry | FileAssetMetadataEntry).path; + if (assetPath.startsWith('asset.')) { + assets.push(assetPath); + } + } + }); + } + return assets; + } + + private assetsFromAssetManifest(artifact: ArtifactManifest): string[] { + const assets: string[] = []; + const fileName = (artifact.properties as AssetManifestProperties).file; + const assetManifest = AssetManifest.fromFile(path.join(this.directory, fileName)); + assetManifest.entries.forEach(entry => { + if (entry.type === 'file') { + const source = (entry as FileManifestEntry).source; + if (source.path && source.path.startsWith('asset.')) { + assets.push((entry as FileManifestEntry).source.path!); + } + } else if (entry.type === 'docker-image') { + const source = (entry as DockerImageManifestEntry).source; + if (source.directory && source.directory.startsWith('asset.')) { + assets.push((entry as DockerImageManifestEntry).source.directory!); + } + } + }); + return assets; + } + /** * Clean the manifest of any unneccesary data. Currently that includes * the metadata trace information since this includes trace information like diff --git a/packages/@aws-cdk/integ-runner/lib/runner/runner-base.ts b/packages/@aws-cdk/integ-runner/lib/runner/runner-base.ts new file mode 100644 index 0000000000000..0ceb705d9bc47 --- /dev/null +++ b/packages/@aws-cdk/integ-runner/lib/runner/runner-base.ts @@ -0,0 +1,380 @@ +import * as path from 'path'; +import { TestCase, DefaultCdkOptions } from '@aws-cdk/cloud-assembly-schema'; +import { AVAILABILITY_ZONE_FALLBACK_CONTEXT_KEY, FUTURE_FLAGS, TARGET_PARTITIONS, FUTURE_FLAGS_EXPIRED, NEW_STYLE_STACK_SYNTHESIS_CONTEXT } from '@aws-cdk/cx-api'; +import { CdkCliWrapper, ICdk } from 'cdk-cli-wrapper'; +import * as fs from 'fs-extra'; +import { flatten } from '../utils'; +import { DestructiveChange } from '../workers/common'; +import { IntegTestSuite, LegacyIntegTestSuite } from './integ-test-suite'; +import { AssemblyManifestReader, ManifestTrace } from './private/cloud-assembly'; + +const CDK_OUTDIR_PREFIX = 'cdk-integ.out'; +const DESTRUCTIVE_CHANGES = '!!DESTRUCTIVE_CHANGES:'; + +/** + * Options for creating an integration test runner + */ +export interface IntegRunnerOptions { + /** + * The name of the file that contains the integration test + * This should be a JavaScript file + */ + readonly fileName: string, + + /** + * The base directory where the tests are + * discovered from. + */ + readonly directory: string, + + /** + * The AWS profile to use when invoking the CDK CLI + * + * @default - no profile is passed, the default profile is used + */ + readonly profile?: string; + + /** + * Additional environment variables that will be available + * to the CDK CLI + * + * @default - no additional environment variables + */ + readonly env?: { [name: string]: string }, + + /** + * tmp cdk.out directory + * + * @default - directory will be `cdk-integ.out.${testName}` + */ + readonly integOutDir?: string, + + /** + * Instance of the CDK CLI to use + * + * @default - CdkCliWrapper + */ + readonly cdk?: ICdk; +} + +/** + * Represents an Integration test runner + */ +export abstract class IntegRunner { + /** + * The directory where the snapshot will be stored + */ + public readonly snapshotDir: string; + + /** + * An instance of the CDK CLI + */ + public readonly cdk: ICdk; + + /** + * Pretty name of the test + */ + public readonly testName: string; + + /** + * The path to the integration test file + */ + protected readonly sourceFilePath: string; + + /** + * The value used in the '--app' CLI parameter + */ + protected readonly cdkApp: string; + + /** + * The path where the `cdk.context.json` file + * will be created + */ + protected readonly cdkContextPath: string; + + /** + * The relative path from the cwd to the snapshot directory + */ + protected readonly relativeSnapshotDir: string; + + /** + * The integration tests that this runner will execute + */ + protected testSuite?: IntegTestSuite; + + /** + * The working directory that the integration tests will be + * executed from + */ + protected readonly directory: string; + + /** + * Default options to pass to the CDK CLI + */ + protected readonly defaultArgs: DefaultCdkOptions = { + pathMetadata: false, + assetMetadata: false, + versionReporting: false, + } + + /** + * The directory where the CDK will be synthed to + */ + protected readonly cdkOutDir: string; + + protected readonly profile?: string; + + protected _destructiveChanges?: DestructiveChange[]; + private legacyContext?: Record; + + constructor(options: IntegRunnerOptions) { + const parsed = path.parse(options.fileName); + this.directory = parsed.dir; + const testName = parsed.name.slice(6); + + // if we are running in a package directory then juse use the fileName + // as the testname, but if we are running in a parent directory with + // multiple packages then use the directory/filename as the testname + if (parsed.dir === 'test') { + this.testName = testName; + } else { + const relativePath = path.relative(options.directory, parsed.dir); + this.testName = `${relativePath ? relativePath+'/' : ''}${parsed.name}`; + } + this.snapshotDir = path.join(this.directory, `${testName}.integ.snapshot`); + this.relativeSnapshotDir = `${testName}.integ.snapshot`; + this.sourceFilePath = path.join(this.directory, parsed.base); + this.cdkContextPath = path.join(this.directory, 'cdk.context.json'); + this.cdk = options.cdk ?? new CdkCliWrapper({ + cdkExecutable: require.resolve('aws-cdk/bin/cdk'), + directory: this.directory, + env: { + ...options.env, + }, + }); + this.cdkOutDir = options.integOutDir ?? `${CDK_OUTDIR_PREFIX}.${testName}`; + this.cdkApp = `node ${parsed.base}`; + this.profile = options.profile; + if (this.hasSnapshot()) { + this.loadManifest(); + } + } + + /** + * Return this list of test cases for this integration test + */ + public get tests(): { [testName: string]: TestCase } | undefined { + return this.testSuite?.testSuite; + } + + /** + * Returns true if a snapshot already exists for this test + */ + public hasSnapshot(): boolean { + if (fs.existsSync(this.snapshotDir)) { + return true; + } else { + return false; + } + } + + /** + * Load the integ manifest which contains information + * on how to execute the tests + * First we try and load the manifest from the integ manifest (i.e. integ.json) + * from the cloud assembly. If it doesn't exist, then we fallback to the + * "legacy mode" and create a manifest from pragma + */ + protected loadManifest(dir?: string): void { + try { + const testSuite = IntegTestSuite.fromPath(dir ?? this.snapshotDir); + this.testSuite = testSuite; + } catch (e) { + const testCases = LegacyIntegTestSuite.fromLegacy({ + cdk: this.cdk, + testName: this.testName, + integSourceFilePath: this.sourceFilePath, + listOptions: { + ...this.defaultArgs, + all: true, + app: this.cdkApp, + profile: this.profile, + output: this.cdkOutDir, + }, + }); + this.legacyContext = LegacyIntegTestSuite.getPragmaContext(this.sourceFilePath); + this.testSuite = testCases; + } + } + + protected cleanup(): void { + const cdkOutPath = path.join(this.directory, this.cdkOutDir); + if (fs.existsSync(cdkOutPath)) { + fs.removeSync(cdkOutPath); + } + } + + /** + * If there are any destructive changes to a stack then this will record + * those in the manifest.json file + */ + private renderTraceData(): ManifestTrace { + const traceData: ManifestTrace = new Map(); + const destructiveChanges = this._destructiveChanges ?? []; + destructiveChanges.forEach(change => { + const trace = traceData.get(change.stackName); + if (trace) { + trace.set(change.logicalId, `${DESTRUCTIVE_CHANGES} ${change.impact}`); + } else { + traceData.set(change.stackName, new Map([ + [change.logicalId, `${DESTRUCTIVE_CHANGES} ${change.impact}`], + ])); + } + }); + return traceData; + } + + /** + * In cases where we do not want to retain the assets, + * for example, if the assets are very large. + * + * Since it is possible to disable the update workflow for individual test + * cases, this needs to first get a list of stacks that have the update workflow + * disabled and then delete assets that relate to that stack. It does that + * by reading the asset manifest for the stack and deleting the asset source + */ + protected removeAssetsFromSnapshot(): void { + const stacks = this.testSuite?.getStacksWithoutUpdateWorkflow() ?? []; + const manifest = AssemblyManifestReader.fromPath(this.snapshotDir); + const assets = flatten(stacks.map(stack => { + return manifest.getAssetsForStack(stack) ?? []; + })); + + assets.forEach(asset => { + const fileName = path.join(this.snapshotDir, asset); + if (fs.existsSync(fileName)) { + if (fs.lstatSync(fileName).isDirectory()) { + fs.emptyDirSync(fileName); + fs.rmdirSync(fileName); + + } else { + fs.unlinkSync(fileName); + } + } + }); + } + + /** + * Remove the asset cache (.cache/) files from the snapshot. + * These are a cache of the asset zips, but we are fine with + * re-zipping on deploy + */ + protected removeAssetsCacheFromSnapshot(): void { + const files = fs.readdirSync(this.snapshotDir); + files.forEach(file => { + const fileName = path.join(this.snapshotDir, file); + if (fs.lstatSync(fileName).isDirectory() && file === '.cache') { + fs.emptyDirSync(fileName); + fs.rmdirSync(fileName); + } + }); + } + + protected createSnapshot(): void { + if (fs.existsSync(this.snapshotDir)) { + fs.removeSync(this.snapshotDir); + } + + // if lookups are enabled then we need to synth again + // using dummy context and save that as the snapshot + if (this.testSuite?.enableLookups) { + this.cdk.synthFast({ + execCmd: this.cdkApp.split(' '), + env: { + ...DEFAULT_SYNTH_OPTIONS.env, + CDK_CONTEXT_JSON: JSON.stringify(this.getContext()), + }, + output: this.relativeSnapshotDir, + }); + } else { + fs.moveSync(path.join(this.directory, this.cdkOutDir), this.snapshotDir, { overwrite: true }); + } + if (fs.existsSync(this.snapshotDir)) { + this.removeAssetsFromSnapshot(); + this.removeAssetsCacheFromSnapshot(); + const assembly = AssemblyManifestReader.fromPath(this.snapshotDir); + assembly.cleanManifest(); + assembly.recordTrace(this.renderTraceData()); + } + } + + protected getContext(additionalContext?: Record): Record { + const futureFlags: {[key: string]: any} = {}; + Object.entries(FUTURE_FLAGS) + .filter(([k, _]) => !FUTURE_FLAGS_EXPIRED.includes(k)) + .forEach(([k, v]) => futureFlags[k] = v); + + return { + // if lookups are enabled then we need to synth + // with the "dummy" context + ...this.testSuite?.enableLookups ? DEFAULT_SYNTH_OPTIONS.context : {}, + // This is needed so that there are no differences between + // running on v1 vs v2 + [NEW_STYLE_STACK_SYNTHESIS_CONTEXT]: false, + ...futureFlags, + ...this.legacyContext, + ...additionalContext, + }; + } +} + + +// Default context we run all integ tests with, so they don't depend on the +// account of the exercising user. +export const DEFAULT_SYNTH_OPTIONS = { + context: { + [AVAILABILITY_ZONE_FALLBACK_CONTEXT_KEY]: ['test-region-1a', 'test-region-1b', 'test-region-1c'], + 'availability-zones:account=12345678:region=test-region': ['test-region-1a', 'test-region-1b', 'test-region-1c'], + 'ssm:account=12345678:parameterName=/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2:region=test-region': 'ami-1234', + 'ssm:account=12345678:parameterName=/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2:region=test-region': 'ami-1234', + 'ssm:account=12345678:parameterName=/aws/service/ecs/optimized-ami/amazon-linux/recommended:region=test-region': '{"image_id": "ami-1234"}', + // eslint-disable-next-line max-len + 'ami:account=12345678:filters.image-type.0=machine:filters.name.0=amzn-ami-vpc-nat-*:filters.state.0=available:owners.0=amazon:region=test-region': 'ami-1234', + 'vpc-provider:account=12345678:filter.isDefault=true:region=test-region:returnAsymmetricSubnets=true': { + vpcId: 'vpc-60900905', + subnetGroups: [ + { + type: 'Public', + name: 'Public', + subnets: [ + { + subnetId: 'subnet-e19455ca', + availabilityZone: 'us-east-1a', + routeTableId: 'rtb-e19455ca', + }, + { + subnetId: 'subnet-e0c24797', + availabilityZone: 'us-east-1b', + routeTableId: 'rtb-e0c24797', + }, + { + subnetId: 'subnet-ccd77395', + availabilityZone: 'us-east-1c', + routeTableId: 'rtb-ccd77395', + }, + ], + }, + ], + }, + + // Restricting to these target partitions makes most service principals synthesize to + // `service.${URL_SUFFIX}`, which is technically *incorrect* (it's only `amazonaws.com` + // or `amazonaws.com.cn`, never UrlSuffix for any of the restricted regions) but it's what + // most existing integ tests contain, and we want to disturb as few as possible. + [TARGET_PARTITIONS]: ['aws', 'aws-cn'], + }, + env: { + CDK_INTEG_ACCOUNT: '12345678', + CDK_INTEG_REGION: 'test-region', + }, +}; diff --git a/packages/@aws-cdk/integ-runner/lib/runner/runners.ts b/packages/@aws-cdk/integ-runner/lib/runner/runners.ts deleted file mode 100644 index a5a4764aa688b..0000000000000 --- a/packages/@aws-cdk/integ-runner/lib/runner/runners.ts +++ /dev/null @@ -1,869 +0,0 @@ -import * as path from 'path'; -import { Writable, WritableOptions } from 'stream'; -import { StringDecoder, NodeStringDecoder } from 'string_decoder'; -import { TestCase, RequireApproval, DefaultCdkOptions } from '@aws-cdk/cloud-assembly-schema'; -import { diffTemplate, formatDifferences, ResourceDifference, ResourceImpact } from '@aws-cdk/cloudformation-diff'; -import { AVAILABILITY_ZONE_FALLBACK_CONTEXT_KEY, FUTURE_FLAGS, TARGET_PARTITIONS } from '@aws-cdk/cx-api'; -import { CdkCliWrapper, ICdk } from 'cdk-cli-wrapper'; -import * as fs from 'fs-extra'; -import * as logger from '../logger'; -import { Diagnostic, DiagnosticReason, DestructiveChange } from '../workers/common'; -import { canonicalizeTemplate } from './private/canonicalize-assets'; -import { AssemblyManifestReader, ManifestTrace } from './private/cloud-assembly'; -import { IntegManifestReader } from './private/integ-manifest'; -import { exec } from './private/utils'; - -const CDK_OUTDIR_PREFIX = 'cdk-integ.out'; -const CDK_INTEG_STACK_PRAGMA = '/// !cdk-integ'; -const PRAGMA_PREFIX = 'pragma:'; -const SET_CONTEXT_PRAGMA_PREFIX = 'pragma:set-context:'; -const VERIFY_ASSET_HASHES = 'pragma:include-assets-hashes'; -const ENABLE_LOOKUPS_PRAGMA = 'pragma:enable-lookups'; -const DISABLE_UPDATE_WORKFLOW = 'pragma:disable-update-workflow'; -const DESTRUCTIVE_CHANGES = '!!DESTRUCTIVE_CHANGES:'; - -/** - * Options for creating an integration test runner - */ -export interface IntegRunnerOptions { - /** - * The name of the file that contains the integration test - * This should be a JavaScript file - */ - readonly fileName: string, - - /** - * The AWS profile to use when invoking the CDK CLI - * - * @default - no profile is passed, the default profile is used - */ - readonly profile?: string; - - /** - * Additional environment variables that will be available - * to the CDK CLI - * - * @default - no additional environment variables - */ - readonly env?: { [name: string]: string }, - - /** - * tmp cdk.out directory - * - * @default - directory will be `cdk-integ.out.${testName}` - */ - readonly integOutDir?: string, - - /** - * Instance of the CDK CLI to use - * - * @default - CdkCliWrapper - */ - readonly cdk?: ICdk; -} - -/** - * Represents an Integration test runner - */ -export abstract class IntegRunner { - /** - * The directory where the snapshot will be stored - */ - public readonly snapshotDir: string; - - /** - * An instance of the CDK CLI - */ - public readonly cdk: ICdk; - - /** - * Pretty name of the test - */ - public readonly testName: string; - - /** - * The path to the integration test file - */ - protected readonly sourceFilePath: string; - - /** - * The value used in the '--app' CLI parameter - */ - protected readonly cdkApp: string; - - /** - * The path where the `cdk.context.json` file - * will be created - */ - protected readonly cdkContextPath: string; - - /** - * The relative path from the cwd to the snapshot directory - */ - protected readonly relativeSnapshotDir: string; - - /** - * The integration tests that this runner will execute - */ - protected _tests?: { [testName: string]: TestCase }; - - /** - * The working directory that the integration tests will be - * executed from - */ - protected readonly directory: string; - - /** - * Default options to pass to the CDK CLI - */ - protected readonly defaultArgs: DefaultCdkOptions = { - pathMetadata: false, - assetMetadata: false, - versionReporting: false, - } - - private _enableLookups?: boolean; - private _disableUpdateWorkflow?: boolean; - - /** - * The directory where the CDK will be synthed to - */ - protected readonly cdkOutDir: string; - - protected readonly profile?: string; - - protected _destructiveChanges?: DestructiveChange[]; - - constructor(options: IntegRunnerOptions) { - const parsed = path.parse(options.fileName); - this.directory = parsed.dir; - const testName = parsed.name.slice(6); - - // if we are running in a package directory then juse use the fileName - // as the testname, but if we are running in a parent directory with - // multiple packages then use the directory/filename as the testname - if (parsed.dir === 'test') { - this.testName = testName; - } else { - this.testName = `${parsed.dir}/${parsed.name}`; - } - this.snapshotDir = path.join(this.directory, `${testName}.integ.snapshot`); - this.relativeSnapshotDir = `${testName}.integ.snapshot`; - this.sourceFilePath = path.join(this.directory, parsed.base); - this.cdkContextPath = path.join(this.directory, 'cdk.context.json'); - this.cdk = options.cdk ?? new CdkCliWrapper({ - cdkExecutable: require.resolve('aws-cdk/bin/cdk'), - directory: this.directory, - env: { - ...options.env, - }, - }); - this.cdkOutDir = options.integOutDir ?? `${CDK_OUTDIR_PREFIX}.${testName}`; - this.cdkApp = `node ${parsed.base}`; - this.profile = options.profile; - if (this.hasSnapshot()) { - this.loadManifest(); - } - } - - /** - * Whether or not lookups are enabled for a given test case - */ - protected get enableLookups(): boolean { - return this._enableLookups ?? false; - } - - /** - * Whether or not to run the update workflow when - * updating the snapshot - */ - protected get disableUpdateWorkflow(): boolean { - return this._disableUpdateWorkflow ?? false; - } - - /** - * Return this list of test cases for this integration test - */ - public get tests(): { [testName: string]: TestCase } | undefined { - return this._tests; - } - - /** - * Returns true if a snapshot already exists for this test - */ - public hasSnapshot(): boolean { - if (fs.existsSync(this.snapshotDir)) { - return true; - } else { - return false; - } - } - - protected loadManifest(dir?: string): void { - try { - const reader = IntegManifestReader.fromPath(dir ?? this.snapshotDir); - this._tests = reader.tests.testCases; - this._enableLookups = reader.tests.enableLookups; - } catch (e) { - this._tests = this.renderTestCasesForLegacyTests(); - this._enableLookups = this.pragmas().includes(ENABLE_LOOKUPS_PRAGMA); - this._disableUpdateWorkflow = this.pragmas().includes(DISABLE_UPDATE_WORKFLOW); - } - } - - protected cleanup(): void { - const cdkOutPath = path.join(this.directory, this.cdkOutDir); - if (fs.existsSync(cdkOutPath)) { - fs.removeSync(cdkOutPath); - } - } - - /** - * If there are any destructive changes to a stack then this will record - * those in the manifest.json file - */ - private renderTraceData(): ManifestTrace { - const traceData: ManifestTrace = new Map(); - const destructiveChanges = this._destructiveChanges ?? []; - destructiveChanges.forEach(change => { - const trace = traceData.get(change.stackName); - if (trace) { - trace.set(change.logicalId, `${DESTRUCTIVE_CHANGES} ${change.impact}`); - } else { - traceData.set(change.stackName, new Map([ - [change.logicalId, `${DESTRUCTIVE_CHANGES} ${change.impact}`], - ])); - } - }); - return traceData; - } - - /** - * In cases where we do not want to retain the assets, - * for example, if the assets are very large - */ - protected removeAssetsFromSnapshot(): void { - const files = fs.readdirSync(this.snapshotDir); - files.forEach(file => { - const fileName = path.join(this.snapshotDir, file); - if (file.startsWith('asset.')) { - if (fs.lstatSync(fileName).isDirectory()) { - fs.emptyDirSync(fileName); - fs.rmdirSync(fileName); - } else { - fs.unlinkSync(fileName); - } - } - }); - } - - /** - * Remove the asset cache (.cache/) files from the snapshot. - * These are a cache of the asset zips, but we are fine with - * re-zipping on deploy - */ - protected removeAssetsCacheFromSnapshot(): void { - const files = fs.readdirSync(this.snapshotDir); - files.forEach(file => { - const fileName = path.join(this.snapshotDir, file); - if (fs.lstatSync(fileName).isDirectory() && file === '.cache') { - fs.emptyDirSync(fileName); - fs.rmdirSync(fileName); - } - }); - } - - protected createSnapshot(): void { - if (fs.existsSync(this.snapshotDir)) { - fs.removeSync(this.snapshotDir); - } - - // if lookups are enabled then we need to synth again - // using dummy context and save that as the snapshot - if (this.enableLookups) { - this.cdk.synthFast({ - execCmd: this.cdkApp.split(' '), - env: { - ...DEFAULT_SYNTH_OPTIONS.env, - CDK_CONTEXT_JSON: JSON.stringify(this.getContext(true)), - }, - output: this.relativeSnapshotDir, - }); - } else { - fs.moveSync(path.join(this.directory, this.cdkOutDir), this.snapshotDir, { overwrite: true }); - } - if (fs.existsSync(this.snapshotDir)) { - if (this.disableUpdateWorkflow) { - this.removeAssetsFromSnapshot(); - } - this.removeAssetsCacheFromSnapshot(); - const assembly = AssemblyManifestReader.fromPath(this.snapshotDir); - assembly.cleanManifest(); - assembly.recordTrace(this.renderTraceData()); - } - } - - /** - * Returns the single test stack to use. - * - * If the test has a single stack, it will be chosen. Otherwise a pragma is expected within the - * test file the name of the stack: - * - * @example - * - * /// !cdk-integ - * - */ - private renderTestCasesForLegacyTests(): { [testName: string]: TestCase } { - const tests: TestCase = { - stacks: [], - }; - const pragma = this.readStackPragma(); - if (pragma.length > 0) { - tests.stacks.push(...pragma); - } else { - const stacks = (this.cdk.list({ - ...this.defaultArgs, - all: true, - app: this.cdkApp, - profile: this.profile, - output: this.cdkOutDir, - })).split('\n'); - if (stacks.length !== 1) { - throw new Error('"cdk-integ" can only operate on apps with a single stack.\n\n' + - ' If your app has multiple stacks, specify which stack to select by adding this to your test source:\n\n' + - ` ${CDK_INTEG_STACK_PRAGMA} STACK ...\n\n` + - ` Available stacks: ${stacks.join(' ')} (wildcards are also supported)\n`); - } - if (stacks.length === 1 && stacks[0] === '') { - throw new Error(`No stack found for test ${this.testName}`); - } - tests.stacks.push(...stacks); - } - - return { - [this.testName]: tests, - }; - } - - /** - * Reads stack names from the "!cdk-integ" pragma. - * - * Every word that's NOT prefixed by "pragma:" is considered a stack name. - * - * @example - * - * /// !cdk-integ - */ - private readStackPragma(): string[] { - return (this.readIntegPragma()).filter(p => !p.startsWith(PRAGMA_PREFIX)); - } - - /** - * Read arbitrary cdk-integ pragma directives - * - * Reads the test source file and looks for the "!cdk-integ" pragma. If it exists, returns it's - * contents. This allows integ tests to supply custom command line arguments to "cdk deploy" and "cdk synth". - * - * @example - * - * /// !cdk-integ [...] - */ - private readIntegPragma(): string[] { - const source = fs.readFileSync(this.sourceFilePath, { encoding: 'utf-8' }); - const pragmaLine = source.split('\n').find(x => x.startsWith(CDK_INTEG_STACK_PRAGMA + ' ')); - if (!pragmaLine) { - return []; - } - - const args = pragmaLine.substring(CDK_INTEG_STACK_PRAGMA.length).trim().split(' '); - if (args.length === 0) { - throw new Error(`Invalid syntax for cdk-integ pragma. Usage: "${CDK_INTEG_STACK_PRAGMA} [STACK] [pragma:PRAGMA] [...]"`); - } - return args; - } - - /** - * Return the non-stack pragmas - * - * These are all pragmas that start with "pragma:". - * - * For backwards compatibility reasons, all pragmas that DON'T start with this - * string are considered to be stack names. - */ - protected pragmas(): string[] { - return (this.readIntegPragma()).filter(p => p.startsWith(PRAGMA_PREFIX)); - } - - protected getContext(enableLookups?: boolean, additionalContext?: Record): Record { - const ctxPragmaContext: Record = {}; - - // apply context from set-context pragma - // usage: pragma:set-context:key=value - const ctxPragmas = (this.pragmas()).filter(p => p.startsWith(SET_CONTEXT_PRAGMA_PREFIX)); - for (const p of ctxPragmas) { - const instruction = p.substring(SET_CONTEXT_PRAGMA_PREFIX.length); - const [key, value] = instruction.split('='); - if (key == null || value == null) { - throw new Error(`invalid "set-context" pragma syntax. example: "pragma:set-context:@aws-cdk/core:newStyleStackSynthesis=true" got: ${p}`); - } - - ctxPragmaContext[key] = value; - } - return { - ...enableLookups ? DEFAULT_SYNTH_OPTIONS.context : {}, - ...FUTURE_FLAGS, - ...ctxPragmaContext, - ...additionalContext, - }; - } -} - -/** - * Options for the integration test runner - */ -export interface RunOptions { - /** - * The test case to execute - */ - readonly testCase: TestCase; - - /** - * Whether or not to run `cdk destroy` and cleanup the - * integration test stacks. - * - * Set this to false if you need to perform any validation - * or troubleshooting after deployment. - * - * @default true - */ - readonly clean?: boolean; - - /** - * If set to true, the integration test will not deploy - * anything and will simply update the snapshot. - * - * You should NOT use this method since you are essentially - * bypassing the integration test. - * - * @default false - */ - readonly dryRun?: boolean; - - /** - * If this is set to false then the stack update workflow will - * not be run - * - * The update workflow exists to check for cases where a change would cause - * a failure to an existing stack, but not for a newly created stack. - * - * @default true - */ - readonly updateWorkflow?: boolean; -} - -/** - * An integration test runner that orchestrates executing - * integration tests - */ -export class IntegTestRunner extends IntegRunner { - constructor(options: IntegRunnerOptions, destructiveChanges?: DestructiveChange[]) { - super(options); - this._destructiveChanges = destructiveChanges; - } - - /** - * When running integration tests with the update path workflow - * it is important that the snapshot that is deployed is the current snapshot - * from the upstream branch. In order to guarantee that, first checkout the latest - * (to the user) snapshot from upstream - * - * It is not straightforward to figure out what branch the current - * working branch was created from. This is a best effort attempt to do so. - * This assumes that there is an 'origin'. `git remote show origin` returns a list of - * all branches and we then search for one that starts with `HEAD branch: ` - */ - private checkoutSnapshot(): void { - const cwd = path.dirname(this.snapshotDir); - // https://git-scm.com/docs/git-merge-base - let baseBranch: string | undefined = undefined; - // try to find the base branch that the working branch was created from - try { - const origin: string = exec(['git', 'remote', 'show', 'origin'], { - cwd, - }); - const originLines = origin.split('\n'); - for (const line of originLines) { - if (line.trim().startsWith('HEAD branch: ')) { - baseBranch = line.trim().split('HEAD branch: ')[1]; - } - } - } catch (e) { - logger.warning('%s\n%s', - 'Could not determine git origin branch.', - `You need to manually checkout the snapshot directory ${this.snapshotDir}` + - 'from the merge-base (https://git-scm.com/docs/git-merge-base)', - ); - logger.warning('error: %s', e); - } - - // if we found the base branch then get the merge-base (most recent common commit) - // and checkout the snapshot using that commit - if (baseBranch) { - try { - const base = exec(['git', 'merge-base', 'HEAD', baseBranch], { - cwd, - }); - exec(['git', 'checkout', base, '--', this.relativeSnapshotDir], { - cwd, - }); - } catch (e) { - logger.warning('%s\n%s', - `Could not checkout snapshot directory ${this.snapshotDir} using these commands: `, - `git merge-base HEAD ${baseBranch} && git checkout {merge-base} -- ${this.relativeSnapshotDir}`, - ); - logger.warning('error: %s', e); - } - } - } - - /** - * Orchestrates running integration tests. Currently this includes - * - * 1. (if update workflow is enabled) Deploying the snapshot test stacks - * 2. Deploying the integration test stacks - * 2. Saving the snapshot (if successful) - * 3. Destroying the integration test stacks (if clean=false) - * - * The update workflow exists to check for cases where a change would cause - * a failure to an existing stack, but not for a newly created stack. - */ - public runIntegTestCase(options: RunOptions): void { - const clean = options.clean ?? true; - const updateWorkflowEnabled = (options.updateWorkflow ?? true) && (options.testCase.stackUpdateWorkflow ?? true); - try { - if (!options.dryRun) { - // if the update workflow is not disabled, first - // perform a deployment with the exising snapshot - // then perform a deployment (which will be a stack update) - // with the current integration test - // We also only want to run the update workflow if there is an existing - // snapshot (otherwise there is nothing to update) - if (!this.disableUpdateWorkflow && updateWorkflowEnabled && this.hasSnapshot()) { - // make sure the snapshot is the latest from 'origin' - this.checkoutSnapshot(); - this.cdk.deploy({ - ...this.defaultArgs, - stacks: options.testCase.stacks, - requireApproval: RequireApproval.NEVER, - context: this.getContext(this.enableLookups), - output: this.cdkOutDir, - app: this.relativeSnapshotDir, - lookups: this.enableLookups, - ...options.testCase.cdkCommandOptions?.deploy, - }); - } - this.cdk.deploy({ - ...this.defaultArgs, - profile: this.profile, - stacks: options.testCase.stacks, - requireApproval: RequireApproval.NEVER, - output: this.cdkOutDir, - context: this.getContext(this.enableLookups), - app: this.cdkApp, - lookups: this.enableLookups, - ...options.testCase.cdkCommandOptions?.deploy, - }); - } else { - const env: Record = { - ...DEFAULT_SYNTH_OPTIONS.env, - CDK_CONTEXT_JSON: JSON.stringify(this.getContext(this.enableLookups)), - }; - // if lookups are enabled then we need to synth - // with the "dummy" context - this.cdk.synthFast({ - execCmd: this.cdkApp.split(' '), - env, - output: this.cdkOutDir, - }); - } - this.createSnapshot(); - } catch (e) { - throw e; - } finally { - if (!options.dryRun) { - if (clean) { - this.cdk.destroy({ - ...this.defaultArgs, - profile: this.profile, - stacks: options.testCase.stacks, - context: this.getContext(this.enableLookups), - force: true, - app: this.cdkApp, - output: this.cdkOutDir, - ...options.testCase.cdkCommandOptions?.destroy, - }); - } - } - this.cleanup(); - } - } - - /** - * Generate a snapshot if one does not exist - * This will synth and then load the integration test manifest - */ - public generateSnapshot(): void { - if (this.hasSnapshot()) { - throw new Error(`${this.testName} already has a snapshot: ${this.snapshotDir}`); - } - - this.cdk.synthFast({ - execCmd: this.cdkApp.split(' '), - env: { - ...DEFAULT_SYNTH_OPTIONS.env, - CDK_CONTEXT_JSON: JSON.stringify(this.getContext(true)), - }, - output: this.cdkOutDir, - }); - this.loadManifest(this.cdkOutDir); - } -} - -/** - * Runner for snapshot tests. This handles orchestrating - * the validation of the integration test snapshots - */ -export class IntegSnapshotRunner extends IntegRunner { - constructor(options: IntegRunnerOptions) { - super(options); - } - - /** - * Synth the integration tests and compare the templates - * to the existing snapshot. - * - * @returns any diagnostics and any destructive changes - */ - public testSnapshot(): { diagnostics: Diagnostic[], destructiveChanges: DestructiveChange[] } { - try { - // read the existing snapshot - const expectedStacks = this.readAssembly(this.snapshotDir); - - const env: Record = { - ...DEFAULT_SYNTH_OPTIONS.env, - CDK_CONTEXT_JSON: JSON.stringify(this.getContext(this.enableLookups)), - }; - // synth the integration test - this.cdk.synthFast({ - execCmd: this.cdkApp.split(' '), - env, - output: this.cdkOutDir, - }); - const actualStacks = this.readAssembly(path.join(this.directory, this.cdkOutDir)); - - // diff the existing snapshot (expected) with the integration test (actual) - const diagnostics = this.diffAssembly(expectedStacks, actualStacks); - return diagnostics; - } catch (e) { - throw e; - } finally { - this.cleanup(); - } - } - - /** - * For a given stack return all resource types that are allowed to be destroyed - * as part of a stack update - * - * @param stackId the stack id - * @returns a list of resource types or undefined if none are found - */ - private getAllowedDestroyTypesForStack(stackId: string): string[] | undefined { - for (const testCase of Object.values(this.tests ?? {})) { - if (testCase.stacks.includes(stackId)) { - return testCase.allowDestroy; - } - } - return undefined; - } - - /** - * Find any differences between the existing and expected snapshots - * - * @param existing - the existing (expected) snapshot - * @param actual - the new (actual) snapshot - * @returns any diagnostics and any destructive changes - */ - private diffAssembly( - existing: Record, - actual: Record, - ): { diagnostics: Diagnostic[], destructiveChanges: DestructiveChange[] } { - const verifyHashes = this.pragmas().includes(VERIFY_ASSET_HASHES); - const failures: Diagnostic[] = []; - const destructiveChanges: DestructiveChange[] = []; - - // check if there is a CFN template in the current snapshot - // that does not exist in the "actual" snapshot - for (const templateId of Object.keys(existing)) { - if (!actual.hasOwnProperty(templateId)) { - failures.push({ - testName: this.testName, - reason: DiagnosticReason.SNAPSHOT_FAILED, - message: `${templateId} exists in snapshot, but not in actual`, - }); - } - } - - for (const templateId of Object.keys(actual)) { - // check if there is a CFN template in the "actual" snapshot - // that does not exist in the current snapshot - if (!existing.hasOwnProperty(templateId)) { - failures.push({ - testName: this.testName, - reason: DiagnosticReason.SNAPSHOT_FAILED, - message: `${templateId} does not exist in snapshot, but does in actual`, - }); - } else { - let actualTemplate = actual[templateId]; - let expectedTemplate = existing[templateId]; - const allowedDestroyTypes = this.getAllowedDestroyTypesForStack(templateId) ?? []; - - // if we are not verifying asset hashes then remove the specific - // asset hashes from the templates so they are not part of the diff - // comparison - if (!verifyHashes) { - actualTemplate = canonicalizeTemplate(actualTemplate); - expectedTemplate = canonicalizeTemplate(expectedTemplate); - } - const templateDiff = diffTemplate(expectedTemplate, actualTemplate); - if (!templateDiff.isEmpty) { - // go through all the resource differences and check for any - // "destructive" changes - templateDiff.resources.forEachDifference((logicalId: string, change: ResourceDifference) => { - // if the change is a removal it will not show up as a 'changeImpact' - // so need to check for it separately, unless it is a resourceType that - // has been "allowed" to be destroyed - const resourceType = change.oldValue?.Type ?? change.newValue?.Type; - if (resourceType && allowedDestroyTypes.includes(resourceType)) { - return; - } - if (change.isRemoval) { - destructiveChanges.push({ - impact: ResourceImpact.WILL_DESTROY, - logicalId, - stackName: templateId, - }); - } else { - switch (change.changeImpact) { - case ResourceImpact.MAY_REPLACE: - case ResourceImpact.WILL_ORPHAN: - case ResourceImpact.WILL_DESTROY: - case ResourceImpact.WILL_REPLACE: - destructiveChanges.push({ - impact: change.changeImpact, - logicalId, - stackName: templateId, - }); - break; - } - } - }); - const writable = new StringWritable({}); - formatDifferences(writable, templateDiff); - failures.push({ - reason: DiagnosticReason.SNAPSHOT_FAILED, - message: writable.data, - testName: this.testName, - }); - } - } - } - - return { - diagnostics: failures, - destructiveChanges, - }; - } - - private readAssembly(dir: string): Record { - const assembly = AssemblyManifestReader.fromPath(dir); - const stacks = assembly.stacks; - - return stacks; - } -} - -class StringWritable extends Writable { - public data: string; - private _decoder: NodeStringDecoder; - constructor(options: WritableOptions) { - super(options); - this._decoder = new StringDecoder(); - this.data = ''; - } - - _write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void { - if (encoding === 'buffer') { - chunk = this._decoder.write(chunk); - } - - this.data += chunk; - callback(); - } - - _final(callback: (error?: Error | null) => void): void { - this.data += this._decoder.end(); - callback(); - } -} - -// Default context we run all integ tests with, so they don't depend on the -// account of the exercising user. -const DEFAULT_SYNTH_OPTIONS = { - context: { - [AVAILABILITY_ZONE_FALLBACK_CONTEXT_KEY]: ['test-region-1a', 'test-region-1b', 'test-region-1c'], - 'availability-zones:account=12345678:region=test-region': ['test-region-1a', 'test-region-1b', 'test-region-1c'], - 'ssm:account=12345678:parameterName=/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2:region=test-region': 'ami-1234', - 'ssm:account=12345678:parameterName=/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2:region=test-region': 'ami-1234', - 'ssm:account=12345678:parameterName=/aws/service/ecs/optimized-ami/amazon-linux/recommended:region=test-region': '{"image_id": "ami-1234"}', - // eslint-disable-next-line max-len - 'ami:account=12345678:filters.image-type.0=machine:filters.name.0=amzn-ami-vpc-nat-*:filters.state.0=available:owners.0=amazon:region=test-region': 'ami-1234', - 'vpc-provider:account=12345678:filter.isDefault=true:region=test-region:returnAsymmetricSubnets=true': { - vpcId: 'vpc-60900905', - subnetGroups: [ - { - type: 'Public', - name: 'Public', - subnets: [ - { - subnetId: 'subnet-e19455ca', - availabilityZone: 'us-east-1a', - routeTableId: 'rtb-e19455ca', - }, - { - subnetId: 'subnet-e0c24797', - availabilityZone: 'us-east-1b', - routeTableId: 'rtb-e0c24797', - }, - { - subnetId: 'subnet-ccd77395', - availabilityZone: 'us-east-1c', - routeTableId: 'rtb-ccd77395', - }, - ], - }, - ], - }, - - // Restricting to these target partitions makes most service principals synthesize to - // `service.${URL_SUFFIX}`, which is technically *incorrect* (it's only `amazonaws.com` - // or `amazonaws.com.cn`, never UrlSuffix for any of the restricted regions) but it's what - // most existing integ tests contain, and we want to disturb as few as possible. - [TARGET_PARTITIONS]: ['aws', 'aws-cn'], - }, - env: { - CDK_INTEG_ACCOUNT: '12345678', - CDK_INTEG_REGION: 'test-region', - }, -}; diff --git a/packages/@aws-cdk/integ-runner/lib/runner/snapshot-test-runner.ts b/packages/@aws-cdk/integ-runner/lib/runner/snapshot-test-runner.ts new file mode 100644 index 0000000000000..88f71adab5ea6 --- /dev/null +++ b/packages/@aws-cdk/integ-runner/lib/runner/snapshot-test-runner.ts @@ -0,0 +1,195 @@ +import * as path from 'path'; +import { Writable, WritableOptions } from 'stream'; +import { StringDecoder, NodeStringDecoder } from 'string_decoder'; +import { diffTemplate, formatDifferences, ResourceDifference, ResourceImpact } from '@aws-cdk/cloudformation-diff'; +import { Diagnostic, DiagnosticReason, DestructiveChange } from '../workers/common'; +import { canonicalizeTemplate } from './private/canonicalize-assets'; +import { AssemblyManifestReader } from './private/cloud-assembly'; +import { IntegRunnerOptions, IntegRunner, DEFAULT_SYNTH_OPTIONS } from './runner-base'; + +/** + * Runner for snapshot tests. This handles orchestrating + * the validation of the integration test snapshots + */ +export class IntegSnapshotRunner extends IntegRunner { + constructor(options: IntegRunnerOptions) { + super(options); + } + + /** + * Synth the integration tests and compare the templates + * to the existing snapshot. + * + * @returns any diagnostics and any destructive changes + */ + public testSnapshot(): { diagnostics: Diagnostic[], destructiveChanges: DestructiveChange[] } { + try { + // read the existing snapshot + const expectedStacks = this.readAssembly(this.snapshotDir); + + const env: Record = { + ...DEFAULT_SYNTH_OPTIONS.env, + CDK_CONTEXT_JSON: JSON.stringify(this.getContext()), + }; + // synth the integration test + this.cdk.synthFast({ + execCmd: this.cdkApp.split(' '), + env, + output: this.cdkOutDir, + }); + const actualStacks = this.readAssembly(path.join(this.directory, this.cdkOutDir)); + + // diff the existing snapshot (expected) with the integration test (actual) + const diagnostics = this.diffAssembly(expectedStacks, actualStacks); + return diagnostics; + } catch (e) { + throw e; + } finally { + this.cleanup(); + } + } + + /** + * For a given stack return all resource types that are allowed to be destroyed + * as part of a stack update + * + * @param stackId the stack id + * @returns a list of resource types or undefined if none are found + */ + private getAllowedDestroyTypesForStack(stackId: string): string[] | undefined { + for (const testCase of Object.values(this.tests ?? {})) { + if (testCase.stacks.includes(stackId)) { + return testCase.allowDestroy; + } + } + return undefined; + } + + /** + * Find any differences between the existing and expected snapshots + * + * @param existing - the existing (expected) snapshot + * @param actual - the new (actual) snapshot + * @returns any diagnostics and any destructive changes + */ + private diffAssembly( + existing: Record, + actual: Record, + ): { diagnostics: Diagnostic[], destructiveChanges: DestructiveChange[] } { + const failures: Diagnostic[] = []; + const destructiveChanges: DestructiveChange[] = []; + + // check if there is a CFN template in the current snapshot + // that does not exist in the "actual" snapshot + for (const templateId of Object.keys(existing)) { + if (!actual.hasOwnProperty(templateId)) { + failures.push({ + testName: this.testName, + reason: DiagnosticReason.SNAPSHOT_FAILED, + message: `${templateId} exists in snapshot, but not in actual`, + }); + } + } + + for (const templateId of Object.keys(actual)) { + // check if there is a CFN template in the "actual" snapshot + // that does not exist in the current snapshot + if (!existing.hasOwnProperty(templateId)) { + failures.push({ + testName: this.testName, + reason: DiagnosticReason.SNAPSHOT_FAILED, + message: `${templateId} does not exist in snapshot, but does in actual`, + }); + } else { + let actualTemplate = actual[templateId]; + let expectedTemplate = existing[templateId]; + const allowedDestroyTypes = this.getAllowedDestroyTypesForStack(templateId) ?? []; + + // if we are not verifying asset hashes then remove the specific + // asset hashes from the templates so they are not part of the diff + // comparison + if (!this.testSuite?.getOptionsForStack(templateId)?.diffAssets) { + actualTemplate = canonicalizeTemplate(actualTemplate); + expectedTemplate = canonicalizeTemplate(expectedTemplate); + } + const templateDiff = diffTemplate(expectedTemplate, actualTemplate); + if (!templateDiff.isEmpty) { + // go through all the resource differences and check for any + // "destructive" changes + templateDiff.resources.forEachDifference((logicalId: string, change: ResourceDifference) => { + // if the change is a removal it will not show up as a 'changeImpact' + // so need to check for it separately, unless it is a resourceType that + // has been "allowed" to be destroyed + const resourceType = change.oldValue?.Type ?? change.newValue?.Type; + if (resourceType && allowedDestroyTypes.includes(resourceType)) { + return; + } + if (change.isRemoval) { + destructiveChanges.push({ + impact: ResourceImpact.WILL_DESTROY, + logicalId, + stackName: templateId, + }); + } else { + switch (change.changeImpact) { + case ResourceImpact.MAY_REPLACE: + case ResourceImpact.WILL_ORPHAN: + case ResourceImpact.WILL_DESTROY: + case ResourceImpact.WILL_REPLACE: + destructiveChanges.push({ + impact: change.changeImpact, + logicalId, + stackName: templateId, + }); + break; + } + } + }); + const writable = new StringWritable({}); + formatDifferences(writable, templateDiff); + failures.push({ + reason: DiagnosticReason.SNAPSHOT_FAILED, + message: writable.data, + testName: this.testName, + }); + } + } + } + + return { + diagnostics: failures, + destructiveChanges, + }; + } + + private readAssembly(dir: string): Record { + const assembly = AssemblyManifestReader.fromPath(dir); + const stacks = assembly.stacks; + + return stacks; + } +} + +class StringWritable extends Writable { + public data: string; + private _decoder: NodeStringDecoder; + constructor(options: WritableOptions) { + super(options); + this._decoder = new StringDecoder(); + this.data = ''; + } + + _write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void { + if (encoding === 'buffer') { + chunk = this._decoder.write(chunk); + } + + this.data += chunk; + callback(); + } + + _final(callback: (error?: Error | null) => void): void { + this.data += this._decoder.end(); + callback(); + } +} diff --git a/packages/@aws-cdk/integ-runner/lib/runner/private/utils.ts b/packages/@aws-cdk/integ-runner/lib/utils.ts similarity index 75% rename from packages/@aws-cdk/integ-runner/lib/runner/private/utils.ts rename to packages/@aws-cdk/integ-runner/lib/utils.ts index b05ab7f3c5f8a..4eaef31727867 100644 --- a/packages/@aws-cdk/integ-runner/lib/runner/private/utils.ts +++ b/packages/@aws-cdk/integ-runner/lib/utils.ts @@ -26,3 +26,17 @@ export function exec(commandLine: string[], options: { cwd?: string, verbose?: b return output; } + +/** + * Flatten a list of lists into a list of elements + */ +export function flatten(xs: T[][]): T[] { + return Array.prototype.concat.apply([], xs); +} + +/** + * Chain commands + */ +export function chain(commands: string[]): string { + return commands.filter(c => !!c).join(' && '); +} diff --git a/packages/@aws-cdk/integ-runner/lib/workers/common.ts b/packages/@aws-cdk/integ-runner/lib/workers/common.ts index 298e36cfb8de0..8b9e297e772ec 100644 --- a/packages/@aws-cdk/integ-runner/lib/workers/common.ts +++ b/packages/@aws-cdk/integ-runner/lib/workers/common.ts @@ -1,7 +1,7 @@ import { ResourceImpact } from '@aws-cdk/cloudformation-diff'; import * as chalk from 'chalk'; import * as logger from '../logger'; -import { IntegTestConfig } from '../runner/integ-tests'; +import { IntegTestConfig } from '../runner/integration-tests'; /** * Config for an integration test @@ -212,10 +212,3 @@ export function printResults(diagnostic: Diagnostic): void { logger.error(' %s - Failed! %s\n%s', diagnostic.testName, chalk.gray(`${diagnostic.duration}s`), diagnostic.message); } } - -/** - * Flatten a list of lists into a list of elements - */ -export function flatten(xs: T[][]): T[] { - return Array.prototype.concat.apply([], xs); -} diff --git a/packages/@aws-cdk/integ-runner/lib/workers/extract/extract_worker.ts b/packages/@aws-cdk/integ-runner/lib/workers/extract/extract_worker.ts index 9b0c0854997a9..346ad06eddbe8 100644 --- a/packages/@aws-cdk/integ-runner/lib/workers/extract/extract_worker.ts +++ b/packages/@aws-cdk/integ-runner/lib/workers/extract/extract_worker.ts @@ -1,6 +1,6 @@ import * as workerpool from 'workerpool'; -import { IntegTestConfig } from '../../runner/integ-tests'; -import { IntegSnapshotRunner, IntegTestRunner } from '../../runner/runners'; +import { IntegSnapshotRunner, IntegTestRunner } from '../../runner'; +import { IntegTestConfig } from '../../runner/integration-tests'; import { DiagnosticReason, IntegTestWorkerConfig } from '../common'; import { IntegTestBatchRequest } from '../integ-test-worker'; @@ -16,6 +16,7 @@ export function integTestWorker(request: IntegTestBatchRequest): IntegTestWorker const failures: IntegTestConfig[] = []; for (const test of request.tests) { const runner = new IntegTestRunner({ + directory: test.directory, fileName: test.fileName, profile: request.profile, env: { @@ -77,7 +78,7 @@ export function integTestWorker(request: IntegTestBatchRequest): IntegTestWorker */ export function snapshotTestWorker(test: IntegTestConfig): IntegTestWorkerConfig[] { const failedTests = new Array(); - const runner = new IntegSnapshotRunner({ fileName: test.fileName }); + const runner = new IntegSnapshotRunner({ fileName: test.fileName, directory: test.directory }); const start = Date.now(); try { if (!runner.hasSnapshot()) { @@ -97,6 +98,7 @@ export function snapshotTestWorker(test: IntegTestConfig): IntegTestWorkerConfig })); failedTests.push({ fileName: test.fileName, + directory: test.directory, destructiveChanges, }); } else { diff --git a/packages/@aws-cdk/integ-runner/lib/workers/integ-snapshot-worker.ts b/packages/@aws-cdk/integ-runner/lib/workers/integ-snapshot-worker.ts index b1afb261809e6..66057110a5490 100644 --- a/packages/@aws-cdk/integ-runner/lib/workers/integ-snapshot-worker.ts +++ b/packages/@aws-cdk/integ-runner/lib/workers/integ-snapshot-worker.ts @@ -1,7 +1,8 @@ import * as workerpool from 'workerpool'; import * as logger from '../logger'; -import { IntegTestConfig } from '../runner/integ-tests'; -import { printSummary, printResults, flatten, IntegTestWorkerConfig } from './common'; +import { IntegTestConfig } from '../runner/integration-tests'; +import { flatten } from '../utils'; +import { printSummary, printResults, IntegTestWorkerConfig } from './common'; /** * Run Snapshot tests diff --git a/packages/@aws-cdk/integ-runner/lib/workers/integ-test-worker.ts b/packages/@aws-cdk/integ-runner/lib/workers/integ-test-worker.ts index bececbcfbe881..9c6b4fb2465c6 100644 --- a/packages/@aws-cdk/integ-runner/lib/workers/integ-test-worker.ts +++ b/packages/@aws-cdk/integ-runner/lib/workers/integ-test-worker.ts @@ -1,7 +1,8 @@ import * as workerpool from 'workerpool'; import * as logger from '../logger'; -import { IntegTestConfig } from '../runner/integ-tests'; -import { printResults, printSummary, IntegBatchResponse, IntegTestOptions, IntegRunnerMetrics, flatten } from './common'; +import { IntegTestConfig } from '../runner/integration-tests'; +import { flatten } from '../utils'; +import { printResults, printSummary, IntegBatchResponse, IntegTestOptions, IntegRunnerMetrics } from './common'; /** * Options for an integration test batch diff --git a/packages/@aws-cdk/integ-runner/package.json b/packages/@aws-cdk/integ-runner/package.json index 4f13a2849fc17..86689fd98a9a6 100644 --- a/packages/@aws-cdk/integ-runner/package.json +++ b/packages/@aws-cdk/integ-runner/package.json @@ -67,6 +67,7 @@ "@aws-cdk/cloud-assembly-schema": "0.0.0", "@aws-cdk/cloudformation-diff": "0.0.0", "@aws-cdk/cx-api": "0.0.0", + "cdk-assets": "0.0.0", "cdk-cli-wrapper": "0.0.0", "aws-cdk": "0.0.0", "chalk": "^4", diff --git a/packages/@aws-cdk/integ-runner/test/runner/runners.test.ts b/packages/@aws-cdk/integ-runner/test/runner/integ-test-runner.test.ts similarity index 54% rename from packages/@aws-cdk/integ-runner/test/runner/runners.test.ts rename to packages/@aws-cdk/integ-runner/test/runner/integ-test-runner.test.ts index 5bbaf6b5de710..9cb5090b1e9f9 100644 --- a/packages/@aws-cdk/integ-runner/test/runner/runners.test.ts +++ b/packages/@aws-cdk/integ-runner/test/runner/integ-test-runner.test.ts @@ -1,10 +1,7 @@ import * as child_process from 'child_process'; -import * as path from 'path'; -import { FUTURE_FLAGS } from '@aws-cdk/cx-api'; import { SynthFastOptions, DestroyOptions, ListOptions, SynthOptions, DeployOptions } from 'cdk-cli-wrapper'; import * as fs from 'fs-extra'; -import { IntegTestRunner, IntegSnapshotRunner } from '../../lib/runner/runners'; -import { DiagnosticReason } from '../../lib/workers/common'; +import { IntegTestRunner } from '../../lib/runner'; import { MockCdkProvider } from '../helpers'; let cdkMock: MockCdkProvider; @@ -13,6 +10,8 @@ let synthFastMock: (options: SynthFastOptions) => void; let deployMock: (options: DeployOptions) => void; let listMock: (options: ListOptions) => string; let destroyMock: (options: DestroyOptions) => void; +let spawnSyncMock: jest.SpyInstance; +let removeSyncMock: jest.SpyInstance; beforeEach(() => { cdkMock = new MockCdkProvider({ directory: 'test/test-data' }); listMock = jest.fn().mockImplementation(() => { @@ -27,11 +26,19 @@ beforeEach(() => { cdkMock.mockDeploy(deployMock); cdkMock.mockSynthFast(synthFastMock); cdkMock.mockDestroy(destroyMock); - jest.spyOn(child_process, 'spawnSync').mockImplementation(); + spawnSyncMock = jest.spyOn(child_process, 'spawnSync').mockReturnValue({ + status: 0, + stderr: Buffer.from('stderr'), + stdout: Buffer.from('stdout'), + pid: 123, + output: ['stdout', 'stderr'], + signal: null, + }); jest.spyOn(process.stderr, 'write').mockImplementation(() => { return true; }); jest.spyOn(process.stdout, 'write').mockImplementation(() => { return true; }); jest.spyOn(fs, 'moveSync').mockImplementation(() => { return true; }); jest.spyOn(fs, 'removeSync').mockImplementation(() => { return true; }); + removeSyncMock = jest.spyOn(fs, 'rmdirSync').mockImplementation(() => { return true; }); jest.spyOn(fs, 'writeFileSync').mockImplementation(() => { return true; }); }); @@ -41,118 +48,14 @@ afterEach(() => { jest.restoreAllMocks(); }); -describe('IntegTest runSnapshotTests', () => { - test('with defaults no diff', () => { +describe('IntegTest runIntegTests', () => { + test('with defaults', () => { // WHEN - const integTest = new IntegSnapshotRunner({ + const integTest = new IntegTestRunner({ cdk: cdkMock.cdk, fileName: 'test/test-data/integ.test-with-snapshot.js', - integOutDir: 'test-with-snapshot.integ.snapshot', - }); - integTest.testSnapshot(); - - // THEN - expect(synthFastMock).toHaveBeenCalledTimes(1); - expect(synthFastMock).toHaveBeenCalledWith({ - env: expect.objectContaining({ - CDK_INTEG_ACCOUNT: '12345678', - CDK_INTEG_REGION: 'test-region', - }), - execCmd: ['node', 'integ.test-with-snapshot.js'], - output: 'test-with-snapshot.integ.snapshot', - }); - }); - - test('with defaults and diff', () => { - // WHEN - const integTest = new IntegSnapshotRunner({ - cdk: cdkMock.cdk, - fileName: path.join(__dirname, '../test-data/integ.test-with-snapshot.js'), - integOutDir: 'test-with-snapshot-diff.integ.snapshot', - }); - const results = integTest.testSnapshot(); - - // THEN - expect(synthFastMock).toHaveBeenCalledTimes(1); - expect(synthFastMock).toHaveBeenCalledWith({ - execCmd: ['node', 'integ.test-with-snapshot.js'], - env: expect.objectContaining({ - CDK_INTEG_ACCOUNT: '12345678', - CDK_INTEG_REGION: 'test-region', - }), - output: 'test-with-snapshot-diff.integ.snapshot', - }); - expect(results.diagnostics).toEqual(expect.arrayContaining([expect.objectContaining({ - reason: DiagnosticReason.SNAPSHOT_FAILED, - testName: integTest.testName, - message: expect.stringContaining('foobar'), - })])); - expect(results.destructiveChanges).not.toEqual([{ - impact: 'WILL_DESTROY', - logicalId: 'MyFunction1ServiceRole9852B06B', - stackName: 'test-stack', - }]); - expect(results.destructiveChanges).toEqual([{ - impact: 'WILL_DESTROY', - logicalId: 'MyLambdaFuncServiceRoleDefaultPolicyBEB0E748', - stackName: 'test-stack', - }]); - }); - - test('dont diff asset hashes', () => { - // WHEN - const integTest = new IntegSnapshotRunner({ - cdk: cdkMock.cdk, - fileName: path.join(__dirname, '../test-data/integ.test-with-snapshot-assets-diff.js'), - integOutDir: 'test-with-snapshot-assets.integ.snapshot', - }); - expect(() => { - integTest.testSnapshot(); - }).not.toThrow(); - - // THEN - expect(synthFastMock).toHaveBeenCalledTimes(1); - expect(synthFastMock).toHaveBeenCalledWith({ - execCmd: ['node', 'integ.test-with-snapshot-assets-diff.js'], - env: expect.objectContaining({ - CDK_INTEG_ACCOUNT: '12345678', - CDK_INTEG_REGION: 'test-region', - }), - output: 'test-with-snapshot-assets.integ.snapshot', - }); - }); - - test('diff asset hashes', () => { - // WHEN - const integTest = new IntegSnapshotRunner({ - cdk: cdkMock.cdk, - fileName: path.join(__dirname, '../test-data/integ.test-with-snapshot-assets.js'), - integOutDir: 'test-with-snapshot-assets-diff.integ.snapshot', - }); - const results = integTest.testSnapshot(); - - // THEN - expect(synthFastMock).toHaveBeenCalledTimes(1); - expect(synthFastMock).toHaveBeenCalledWith({ - execCmd: ['node', 'integ.test-with-snapshot-assets.js'], - env: expect.objectContaining({ - CDK_INTEG_ACCOUNT: '12345678', - CDK_INTEG_REGION: 'test-region', - }), - output: 'test-with-snapshot-assets-diff.integ.snapshot', + directory: 'test/test-data', }); - expect(results.diagnostics).toEqual(expect.arrayContaining([expect.objectContaining({ - reason: DiagnosticReason.SNAPSHOT_FAILED, - testName: integTest.testName, - message: expect.stringContaining('Parameters'), - })])); - }); -}); - -describe('IntegTest runIntegTests', () => { - test('with defaults', () => { - // WHEN - const integTest = new IntegTestRunner({ cdk: cdkMock.cdk, fileName: 'test/test-data/integ.test-with-snapshot.js' }); integTest.runIntegTestCase({ testCase: { stacks: ['stack1'], @@ -168,9 +71,7 @@ describe('IntegTest runIntegTests', () => { requireApproval: 'never', pathMetadata: false, assetMetadata: false, - context: expect.objectContaining({ - ...FUTURE_FLAGS, - }), + context: expect.any(Object), versionReporting: false, lookups: false, stacks: ['stack1'], @@ -181,9 +82,7 @@ describe('IntegTest runIntegTests', () => { requireApproval: 'never', pathMetadata: false, assetMetadata: false, - context: expect.objectContaining({ - ...FUTURE_FLAGS, - }), + context: expect.any(Object), versionReporting: false, lookups: false, stacks: ['stack1'], @@ -193,9 +92,7 @@ describe('IntegTest runIntegTests', () => { app: 'node integ.test-with-snapshot.js', pathMetadata: false, assetMetadata: false, - context: expect.objectContaining({ - ...FUTURE_FLAGS, - }), + context: expect.any(Object), versionReporting: false, force: true, stacks: ['stack1'], @@ -203,9 +100,14 @@ describe('IntegTest runIntegTests', () => { }); }); - test('with profile', () => { + test('no snapshot', () => { // WHEN - const integTest = new IntegTestRunner({ cdk: cdkMock.cdk, fileName: 'test/test-data/integ.integ-test1.js' }); + const integTest = new IntegTestRunner({ + cdk: cdkMock.cdk, + fileName: 'test/test-data/integ.integ-test1.js', + directory: 'test/test-data', + }); + integTest.generateSnapshot(); integTest.runIntegTestCase({ testCase: { stacks: ['stack1'], @@ -215,7 +117,7 @@ describe('IntegTest runIntegTests', () => { // THEN expect(deployMock).toHaveBeenCalledTimes(1); expect(destroyMock).toHaveBeenCalledTimes(1); - expect(synthFastMock).toHaveBeenCalledTimes(0); + expect(synthFastMock).toHaveBeenCalledTimes(1); expect(deployMock).toHaveBeenCalledWith({ app: 'node integ.integ-test1.js', requireApproval: 'never', @@ -236,9 +138,7 @@ describe('IntegTest runIntegTests', () => { pathMetadata: false, assetMetadata: false, versionReporting: false, - context: expect.objectContaining({ - ...FUTURE_FLAGS, - }), + context: expect.any(Object), force: true, stacks: ['stack1'], output: 'cdk-integ.out.integ-test1', @@ -247,7 +147,11 @@ describe('IntegTest runIntegTests', () => { test('with lookups', () => { // WHEN - const integTest = new IntegTestRunner({ cdk: cdkMock.cdk, fileName: 'test/test-data/integ.test-with-snapshot-assets-diff.js' }); + const integTest = new IntegTestRunner({ + cdk: cdkMock.cdk, + fileName: 'test/test-data/integ.test-with-snapshot-assets-diff.js', + directory: 'test/test-data', + }); integTest.runIntegTestCase({ testCase: { stacks: ['test-stack'], @@ -315,7 +219,12 @@ describe('IntegTest runIntegTests', () => { test('no clean', () => { // WHEN - const integTest = new IntegTestRunner({ cdk: cdkMock.cdk, fileName: 'test/test-data/integ.integ-test1.js' }); + const integTest = new IntegTestRunner({ + cdk: cdkMock.cdk, + fileName: 'test/test-data/integ.integ-test1.js', + directory: 'test/test-data', + }); + integTest.generateSnapshot(); integTest.runIntegTestCase({ clean: false, testCase: { @@ -326,12 +235,17 @@ describe('IntegTest runIntegTests', () => { // THEN expect(deployMock).toHaveBeenCalledTimes(1); expect(destroyMock).toHaveBeenCalledTimes(0); - expect(synthFastMock).toHaveBeenCalledTimes(0); + expect(synthFastMock).toHaveBeenCalledTimes(1); }); test('dryrun', () => { // WHEN - const integTest = new IntegTestRunner({ cdk: cdkMock.cdk, fileName: 'test/test-data/integ.integ-test1.js' }); + const integTest = new IntegTestRunner({ + cdk: cdkMock.cdk, + fileName: 'test/test-data/integ.integ-test1.js', + directory: 'test/test-data', + }); + integTest.generateSnapshot(); integTest.runIntegTestCase({ dryRun: true, testCase: { @@ -342,17 +256,23 @@ describe('IntegTest runIntegTests', () => { // THEN expect(deployMock).toHaveBeenCalledTimes(0); expect(destroyMock).toHaveBeenCalledTimes(0); - expect(synthFastMock).toHaveBeenCalledTimes(1); + expect(synthFastMock).toHaveBeenCalledTimes(2); }); test('determine test stack via pragma', () => { // WHEN - const integTest = new IntegTestRunner({ cdk: cdkMock.cdk, fileName: 'test/test-data/integ.integ-test1.js' }); + const integTest = new IntegTestRunner({ + cdk: cdkMock.cdk, + fileName: 'test/test-data/integ.integ-test1.js', + directory: 'test', + }); integTest.generateSnapshot(); // THEN expect(integTest.tests).toEqual(expect.objectContaining({ - 'test/test-data/integ.integ-test1': { + 'test-data/integ.integ-test1': { + diffAssets: false, + stackUpdateWorkflow: true, stacks: ['stack1'], }, })); @@ -361,7 +281,11 @@ describe('IntegTest runIntegTests', () => { test('generate snapshot', () => { // WHEN - const integTest = new IntegTestRunner({ cdk: cdkMock.cdk, fileName: 'test/test-data/integ.integ-test1.js' }); + const integTest = new IntegTestRunner({ + cdk: cdkMock.cdk, + fileName: 'test/test-data/integ.integ-test1.js', + directory: 'test/test-data', + }); integTest.generateSnapshot(); // THEN @@ -375,17 +299,20 @@ describe('IntegTest runIntegTests', () => { }), }); }); -}); - -describe('IntegTest no pragma', () => { - test('get stacks from list', async () => { + test('get stacks from list, no pragma', async () => { // WHEN - const integTest = new IntegTestRunner({ cdk: cdkMock.cdk, fileName: 'test/test-data/integ.integ-test2.js' }); + const integTest = new IntegTestRunner({ + cdk: cdkMock.cdk, + fileName: 'test/test-data/integ.integ-test2.js', + directory: 'test', + }); integTest.generateSnapshot(); // THEN expect(integTest.tests).toEqual(expect.objectContaining({ - 'test/test-data/integ.integ-test2': { + 'test-data/integ.integ-test2': { + diffAssets: false, + stackUpdateWorkflow: true, stacks: ['stackabc'], }, })); @@ -399,12 +326,16 @@ describe('IntegTest no pragma', () => { output: 'cdk-integ.out.integ-test2', }); }); -}); -describe('IntegTest runIntegTests with profile', () => { - test('with defaults', () => { + test('with profile', () => { // WHEN - const integTest = new IntegTestRunner({ cdk: cdkMock.cdk, fileName: 'test/test-data/integ.integ-test1.js', profile: 'test-profile' }); + const integTest = new IntegTestRunner({ + cdk: cdkMock.cdk, + fileName: 'test/test-data/integ.integ-test1.js', + profile: 'test-profile', + directory: 'test/test-data', + }); + integTest.generateSnapshot(); integTest.runIntegTestCase({ testCase: { stacks: ['stack1'], @@ -414,16 +345,14 @@ describe('IntegTest runIntegTests with profile', () => { // THEN expect(deployMock).toHaveBeenCalledTimes(1); expect(destroyMock).toHaveBeenCalledTimes(1); - expect(synthFastMock).toHaveBeenCalledTimes(0); + expect(synthFastMock).toHaveBeenCalledTimes(1); expect(deployMock).toHaveBeenCalledWith({ app: 'node integ.integ-test1.js', requireApproval: 'never', pathMetadata: false, assetMetadata: false, versionReporting: false, - context: expect.objectContaining({ - ...FUTURE_FLAGS, - }), + context: expect.any(Object), profile: 'test-profile', lookups: false, stacks: ['stack1'], @@ -434,13 +363,197 @@ describe('IntegTest runIntegTests with profile', () => { pathMetadata: false, assetMetadata: false, versionReporting: false, - context: expect.objectContaining({ - ...FUTURE_FLAGS, - }), + context: expect.any(Object), profile: 'test-profile', force: true, stacks: ['stack1'], output: 'cdk-integ.out.integ-test1', }); }); + + test('with hooks', () => { + const integTest = new IntegTestRunner({ + cdk: cdkMock.cdk, + fileName: 'test/test-data/integ.test-with-snapshot.js', + directory: 'test/test-data', + }); + integTest.runIntegTestCase({ + testCase: { + hooks: { + preDeploy: ['echo "preDeploy"'], + postDeploy: ['echo "postDeploy"'], + preDestroy: ['echo "preDestroy"'], + postDestroy: ['echo "postDestroy"'], + }, + stacks: ['stack1'], + }, + }); + + // THEN + expect(spawnSyncMock.mock.calls).toEqual(expect.arrayContaining([ + expect.arrayContaining([ + 'echo "preDeploy"', + ]), + expect.arrayContaining([ + 'echo "postDeploy"', + ]), + expect.arrayContaining([ + 'echo "preDestroy"', + ]), + expect.arrayContaining([ + 'echo "postDestroy"', + ]), + ])); + }); + + test('git is used to checkout latest snapshot', () => { + // GIVEN + spawnSyncMock = jest.spyOn(child_process, 'spawnSync').mockReturnValueOnce({ + status: 0, + stderr: Buffer.from('HEAD branch: main'), + stdout: Buffer.from('HEAD branch: main'), + pid: 123, + output: ['stdout', 'stderr'], + signal: null, + }).mockReturnValueOnce({ + status: 0, + stderr: Buffer.from('abc'), + stdout: Buffer.from('abc'), + pid: 123, + output: ['stdout', 'stderr'], + signal: null, + }); + + // WHEN + const integTest = new IntegTestRunner({ + cdk: cdkMock.cdk, + fileName: 'test/test-data/integ.test-with-snapshot.js', + directory: 'test/test-data', + }); + integTest.runIntegTestCase({ + testCase: { + stacks: ['stack1'], + }, + }); + + // THEN + expect(spawnSyncMock.mock.calls).toEqual(expect.arrayContaining([ + expect.arrayContaining([ + 'git', ['remote', 'show', 'origin'], + ]), + expect.arrayContaining([ + 'git', ['merge-base', 'HEAD', 'main'], + ]), + expect.arrayContaining([ + 'git', ['checkout', 'abc', '--', 'test-with-snapshot.integ.snapshot'], + ]), + ])); + }); + + test('git is used and cannot determine origin', () => { + // GIVEN + spawnSyncMock = jest.spyOn(child_process, 'spawnSync').mockReturnValueOnce({ + status: 1, + stderr: Buffer.from('HEAD branch: main'), + stdout: Buffer.from('HEAD branch: main'), + pid: 123, + output: ['stdout', 'stderr'], + signal: null, + }); + const stderrMock = jest.spyOn(process.stderr, 'write').mockImplementation(() => { return true; }); + + // WHEN + const integTest = new IntegTestRunner({ + cdk: cdkMock.cdk, + fileName: 'test/test-data/integ.test-with-snapshot.js', + directory: 'test/test-data', + }); + integTest.runIntegTestCase({ + testCase: { + stacks: ['stack1'], + }, + }); + + // THEN + expect(stderrMock.mock.calls).toEqual(expect.arrayContaining([ + expect.arrayContaining([ + expect.stringMatching(/Could not determine git origin branch/), + ]), + ])); + }); + + test('git is used and cannot checkout snapshot', () => { + // GIVEN + spawnSyncMock = jest.spyOn(child_process, 'spawnSync').mockReturnValueOnce({ + status: 0, + stderr: Buffer.from('HEAD branch: main'), + stdout: Buffer.from('HEAD branch: main'), + pid: 123, + output: ['stdout', 'stderr'], + signal: null, + }).mockReturnValueOnce({ + status: 1, + stderr: Buffer.from('HEAD branch: main'), + stdout: Buffer.from('HEAD branch: main'), + pid: 123, + output: ['stdout', 'stderr'], + signal: null, + }); + const stderrMock = jest.spyOn(process.stderr, 'write').mockImplementation(() => { return true; }); + + // WHEN + const integTest = new IntegTestRunner({ + cdk: cdkMock.cdk, + fileName: 'test/test-data/integ.test-with-snapshot.js', + directory: 'test/test-data', + }); + integTest.runIntegTestCase({ + testCase: { + stacks: ['stack1'], + }, + }); + + // THEN + expect(stderrMock.mock.calls).toEqual(expect.arrayContaining([ + expect.arrayContaining([ + expect.stringMatching(/Could not checkout snapshot directory/), + ]), + ])); + }); + + test('with assets manifest, assets are removed if stackUpdateWorkflow is disabled', () => { + const integTest = new IntegTestRunner({ + cdk: cdkMock.cdk, + fileName: 'test/test-data/integ.test-with-snapshot-assets.js', + directory: 'test/test-data', + }); + integTest.runIntegTestCase({ + testCase: { + stackUpdateWorkflow: false, + stacks: ['test-stack'], + }, + }); + + expect(removeSyncMock.mock.calls).toEqual([[ + 'test/test-data/test-with-snapshot-assets.integ.snapshot/asset.be270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824', + ]]); + }); + + test('with assembly manifest, assets are removed if stackUpdateWorkflow is disabled', () => { + const integTest = new IntegTestRunner({ + cdk: cdkMock.cdk, + fileName: 'test/test-data/integ.test-with-snapshot-assets-diff.js', + directory: 'test/test-data', + }); + integTest.runIntegTestCase({ + testCase: { + stackUpdateWorkflow: false, + stacks: ['test-stack'], + }, + }); + + expect(removeSyncMock.mock.calls).toEqual([[ + 'test/test-data/test-with-snapshot-assets-diff.integ.snapshot/asset.fec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509', + ]]); + }); }); diff --git a/packages/@aws-cdk/integ-runner/test/runner/integ-test-suite.test.ts b/packages/@aws-cdk/integ-runner/test/runner/integ-test-suite.test.ts new file mode 100644 index 0000000000000..873f7d01e9426 --- /dev/null +++ b/packages/@aws-cdk/integ-runner/test/runner/integ-test-suite.test.ts @@ -0,0 +1,288 @@ +import * as path from 'path'; +import { ListOptions } from 'cdk-cli-wrapper'; +import * as mockfs from 'mock-fs'; +import { IntegTestSuite, LegacyIntegTestSuite } from '../../lib/runner/integ-test-suite'; +import { MockCdkProvider } from '../helpers'; + +describe('Integration test cases', () => { + const testsFile = '/tmp/foo/bar/does/not/exist/integ.json'; + afterEach(() => { + mockfs.restore(); + }); + + test('basic manifest', () => { + // GIVEN + mockfs({ + [testsFile]: JSON.stringify({ + version: 'v1.0.0', + testCases: { + test1: { + stacks: [ + 'test-stack', + ], + }, + }, + }), + }); + // WHEN + const testCases = IntegTestSuite.fromPath(path.dirname(testsFile)); + + // THEN + expect(testCases.enableLookups).toEqual(false); + expect(testCases.getStacksWithoutUpdateWorkflow().length).toEqual(0); + expect(testCases.testSuite).toEqual({ + test1: { + stacks: [ + 'test-stack', + ], + }, + }); + }); + + test('manifest with non defaults', () => { + // GIVEN + mockfs({ + [testsFile]: JSON.stringify({ + version: 'v1.0.0', + enableLookups: true, + testCases: { + test1: { + stackUpdateWorkflow: false, + diffAssets: true, + allowDestroy: ['AWS::IAM::Role'], + stacks: [ + 'test-stack', + ], + }, + }, + }), + }); + // WHEN + const testCases = IntegTestSuite.fromPath(path.dirname(testsFile)); + + // THEN + expect(testCases.enableLookups).toEqual(true); + expect(testCases.getStacksWithoutUpdateWorkflow().length).toEqual(1); + expect(testCases.testSuite).toEqual({ + test1: { + stackUpdateWorkflow: false, + diffAssets: true, + allowDestroy: ['AWS::IAM::Role'], + stacks: [ + 'test-stack', + ], + }, + }); + }); + + test('get options for stack', () => { + // GIVEN + mockfs({ + [testsFile]: JSON.stringify({ + version: 'v1.0.0', + enableLookups: true, + testCases: { + test1: { + stackUpdateWorkflow: false, + diffAssets: true, + allowDestroy: ['AWS::IAM::Role'], + stacks: [ + 'test-stack1', + ], + }, + test2: { + diffAssets: false, + stacks: [ + 'test-stack2', + ], + }, + }, + }), + }); + // WHEN + const testCases = IntegTestSuite.fromPath(path.dirname(testsFile)); + + // THEN + expect(testCases.getOptionsForStack('test-stack1')).toEqual({ + diffAssets: true, + regions: undefined, + hooks: undefined, + cdkCommandOptions: undefined, + stackUpdateWorkflow: false, + allowDestroy: ['AWS::IAM::Role'], + }); + expect(testCases.getOptionsForStack('test-stack2')).toEqual({ + diffAssets: false, + allowDestroy: undefined, + regions: undefined, + hooks: undefined, + stackUpdateWorkflow: true, + cdkCommandOptions: undefined, + }); + expect(testCases.getOptionsForStack('test-stack-does-not-exist')).toBeUndefined(); + }); +}); + +describe('Legacy Integration test cases', () => { + let cdkMock: MockCdkProvider; + let listMock: (options: ListOptions) => string; + const testsFile = '/tmp/foo/bar/does/not/exist/integ.test.js'; + beforeEach(() => { + cdkMock = new MockCdkProvider({ directory: 'test/test-data' }); + }); + + afterEach(() => { + mockfs.restore(); + jest.clearAllMocks(); + jest.resetAllMocks(); + jest.restoreAllMocks(); + }); + + test('basic manifest', () => { + // GIVEN + mockfs({ + [testsFile]: '/// !cdk-integ test-stack', + }); + listMock = jest.fn().mockImplementation(() => { + return 'stackabc'; + }); + cdkMock.mockList(listMock); + + // WHEN + const testCases = LegacyIntegTestSuite.fromLegacy({ + cdk: cdkMock.cdk, + testName: 'test', + listOptions: {}, + integSourceFilePath: testsFile, + }); + + // THEN + expect(listMock).not.toHaveBeenCalled(); + expect(testCases.enableLookups).toEqual(false); + expect(testCases.getStacksWithoutUpdateWorkflow().length).toEqual(0); + expect(testCases.testSuite).toEqual({ + test: { + stackUpdateWorkflow: true, + diffAssets: false, + stacks: [ + 'test-stack', + ], + }, + }); + }); + + test('manifest with pragma', () => { + // GIVEN + mockfs({ + [testsFile]: '/// !cdk-integ test-stack pragma:enable-lookups pragma:disable-update-workflow pragma:include-assets-hashes', + }); + listMock = jest.fn().mockImplementation(() => { + return 'stackabc'; + }); + cdkMock.mockList(listMock); + + // WHEN + const testCases = LegacyIntegTestSuite.fromLegacy({ + cdk: cdkMock.cdk, + testName: 'test', + listOptions: {}, + integSourceFilePath: testsFile, + }); + + // THEN + expect(listMock).not.toHaveBeenCalled(); + expect(testCases.enableLookups).toEqual(true); + expect(testCases.getStacksWithoutUpdateWorkflow().length).toEqual(1); + expect(testCases.testSuite).toEqual({ + test: { + stackUpdateWorkflow: false, + diffAssets: true, + stacks: [ + 'test-stack', + ], + }, + }); + }); + + test('manifest with no pragma', () => { + // GIVEN + mockfs({ + [testsFile]: '', + }); + listMock = jest.fn().mockImplementation(() => { + return 'stackabc'; + }); + cdkMock.mockList(listMock); + + // WHEN + const testCases = LegacyIntegTestSuite.fromLegacy({ + cdk: cdkMock.cdk, + testName: 'test', + listOptions: {}, + integSourceFilePath: testsFile, + }); + + // THEN + expect(listMock).toHaveBeenCalled(); + expect(testCases.enableLookups).toEqual(false); + expect(testCases.getStacksWithoutUpdateWorkflow().length).toEqual(0); + expect(testCases.testSuite).toEqual({ + test: { + stackUpdateWorkflow: true, + diffAssets: false, + stacks: [ + 'stackabc', + ], + }, + }); + }); + + test('manifest with no pragma and multiple stack throws', () => { + // GIVEN + mockfs({ + [testsFile]: '', + }); + listMock = jest.fn().mockImplementation(() => { + return 'stack1\nstack2'; + }); + cdkMock.mockList(listMock); + + // THEN + expect(() => { + LegacyIntegTestSuite.fromLegacy({ + cdk: cdkMock.cdk, + testName: 'test', + listOptions: {}, + integSourceFilePath: testsFile, + }); + }).toThrow(); + }); + + test('can get context from pragma', () => { + // GIVEN + mockfs({ + [testsFile]: '/// !cdk-integ test-stack pragma:set-context:@aws-cdk/core:newStyleStackSynthesis=true', + }); + + // WHEN + const context = LegacyIntegTestSuite.getPragmaContext(testsFile); + + //THEN + expect(context).toEqual({ + '@aws-cdk/core:newStyleStackSynthesis': 'true', + }); + + }); + + test('invalid pragma context throws', () => { + // GIVEN + mockfs({ + [testsFile]: '/// !cdk-integ test-stack pragma:set-context:@aws-cdk/core:newStyleStackSynthesis true', + }); + + // WHEN + expect(() => { + LegacyIntegTestSuite.getPragmaContext(testsFile); + }).toThrow(); + }); +}); diff --git a/packages/@aws-cdk/integ-runner/test/runner/integration-tests.test.ts b/packages/@aws-cdk/integ-runner/test/runner/integration-tests.test.ts index 153c919cd5cb8..1185f848a232a 100644 --- a/packages/@aws-cdk/integ-runner/test/runner/integration-tests.test.ts +++ b/packages/@aws-cdk/integ-runner/test/runner/integration-tests.test.ts @@ -1,5 +1,5 @@ import * as mockfs from 'mock-fs'; -import { IntegrationTests } from '../../lib/runner/integ-tests'; +import { IntegrationTests } from '../../lib/runner/integration-tests'; describe('IntegrationTests', () => { const testsFile = '/tmp/foo/bar/does/not/exist/tests.json'; @@ -17,12 +17,12 @@ describe('IntegrationTests', () => { [testsFileExclude]: JSON.stringify({ exclude: true, tests: [ - 'test/test-data/integ.integ-test1.js', + 'test-data/integ.integ-test1.js', ], }), [testsFile]: JSON.stringify({ tests: [ - 'test/test-data/integ.integ-test1.js', + 'test-data/integ.integ-test1.js', ], }), }); @@ -33,26 +33,26 @@ describe('IntegrationTests', () => { }); test('from cli args', async () => { - const integTests = await tests.fromCliArgs(['test/test-data/integ.integ-test1.js']); + const integTests = await tests.fromCliArgs(['test-data/integ.integ-test1.js']); expect(integTests.length).toEqual(1); expect(integTests[0].fileName).toEqual(expect.stringMatching(/integ.integ-test1.js$/)); }); test('from cli args, test not found', async () => { - const integTests = await tests.fromCliArgs(['test/test-data/integ.integ-test16.js']); + const integTests = await tests.fromCliArgs(['test-data/integ.integ-test16.js']); expect(integTests.length).toEqual(0); expect(stderrMock.mock.calls[0][0]).toContain( - 'No such integ test: test/test-data/integ.integ-test16.js', + 'No such integ test: test-data/integ.integ-test16.js', ); expect(stderrMock.mock.calls[1][0]).toContain( - 'Available tests: test/test-data/integ.integ-test1.js test/test-data/integ.integ-test2.js test/test-data/integ.integ-test3.js', + 'Available tests: test-data/integ.integ-test1.js test-data/integ.integ-test2.js test-data/integ.integ-test3.js', ); }); test('from cli args, exclude', async () => { - const integTests = await tests.fromCliArgs(['test/test-data/integ.integ-test1.js'], true); + const integTests = await tests.fromCliArgs(['test-data/integ.integ-test1.js'], true); const fileNames = integTests.map(test => test.fileName); expect(integTests.length).toEqual(2); @@ -78,7 +78,7 @@ describe('IntegrationTests', () => { }, [testsFile]: JSON.stringify({ tests: [ - 'test/test-data/integ.integ-test16.js', + 'test-data/integ.integ-test16.js', ], }), }); @@ -86,10 +86,10 @@ describe('IntegrationTests', () => { expect(integTests.length).toEqual(0); expect(stderrMock.mock.calls[0][0]).toContain( - 'No such integ test: test/test-data/integ.integ-test16.js', + 'No such integ test: test-data/integ.integ-test16.js', ); expect(stderrMock.mock.calls[1][0]).toContain( - 'Available tests: test/test-data/integ.integ-test1.js test/test-data/integ.integ-test2.js test/test-data/integ.integ-test3.js', + 'Available tests: test-data/integ.integ-test1.js test-data/integ.integ-test2.js test-data/integ.integ-test3.js', ); }); diff --git a/packages/@aws-cdk/integ-runner/test/runner/private/cloud-assembly.test.ts b/packages/@aws-cdk/integ-runner/test/runner/private/cloud-assembly.test.ts index 7ee3ffbc60370..77b6a2c7e6936 100644 --- a/packages/@aws-cdk/integ-runner/test/runner/private/cloud-assembly.test.ts +++ b/packages/@aws-cdk/integ-runner/test/runner/private/cloud-assembly.test.ts @@ -51,6 +51,53 @@ describe('cloud assembly manifest reader', () => { }, displayName: 'test-stack', }, + 'test-stack2': { + type: 'aws:cloudformation:stack', + environment: 'aws://unknown-account/unknown-region', + properties: { + templateFile: 'test-stack.template.json', + validateOnSynth: false, + }, + metadata: { + '/test-stack/asset1': [ + { + type: 'aws:cdk:asset', + data: { + path: 'asset.a820140ad8525b8ed56ad2a7bcd9da99d6afc2490e8c91e34620886c011bdc91', + }, + }, + ], + '/test-stack/asset2': [ + { + type: 'aws:cdk:asset', + data: { + path: 'test-stack2.template.json', + }, + }, + ], + '/test-stack/MyFunction1/ServiceRole/Resource': [ + { + type: 'aws:cdk:logicalId', + data: 'MyFunction1ServiceRole9852B06B', + trace: [ + 'some trace', + 'some more trace', + ], + }, + ], + '/test-stack/MyFunction1/Resource': [ + { + type: 'aws:cdk:logicalId', + data: 'MyFunction12A744C2E', + trace: [ + 'some trace', + 'some more trace', + ], + }, + ], + }, + displayName: 'test-stack', + }, }, }), }); @@ -88,6 +135,7 @@ describe('cloud assembly manifest reader', () => { expect(manifest.stacks).toEqual({ 'test-stack': { data: 'data' }, + 'test-stack2': { data: 'data' }, }); }); @@ -99,8 +147,8 @@ describe('cloud assembly manifest reader', () => { // THEN const newManifest = Manifest.loadAssetManifest(manifestFile); expect(newManifest).toEqual({ - version: '17.0.0', - artifacts: { + version: expect.any(String), + artifacts: expect.objectContaining({ 'Tree': { type: 'cdk:tree', properties: { @@ -130,7 +178,7 @@ describe('cloud assembly manifest reader', () => { }, displayName: 'test-stack', }, - }, + }), }); }); @@ -146,8 +194,8 @@ describe('cloud assembly manifest reader', () => { // THEN const newManifest = Manifest.loadAssetManifest(manifestFile); expect(newManifest).toEqual({ - version: '17.0.0', - artifacts: { + version: expect.any(String), + artifacts: expect.objectContaining({ 'Tree': { type: 'cdk:tree', properties: { @@ -178,7 +226,7 @@ describe('cloud assembly manifest reader', () => { }, displayName: 'test-stack', }, - }, + }), }); }); @@ -194,8 +242,8 @@ describe('cloud assembly manifest reader', () => { // THEN const newManifest = Manifest.loadAssetManifest(manifestFile); expect(newManifest).toEqual({ - version: '17.0.0', - artifacts: { + version: expect.any(String), + artifacts: expect.objectContaining({ 'Tree': { type: 'cdk:tree', properties: { @@ -232,7 +280,18 @@ describe('cloud assembly manifest reader', () => { }, displayName: 'test-stack', }, - }, + }), }); }); + + test('can get assets from assembly manifest', () => { + // WHEN + const manifest = AssemblyManifestReader.fromFile(manifestFile); + const assets = manifest.getAssetsForStack('test-stack2'); + + // THEN + expect(assets).toEqual([ + 'asset.a820140ad8525b8ed56ad2a7bcd9da99d6afc2490e8c91e34620886c011bdc91', + ]); + }); }); diff --git a/packages/@aws-cdk/integ-runner/test/runner/snapshot-test-runner.test.ts b/packages/@aws-cdk/integ-runner/test/runner/snapshot-test-runner.test.ts new file mode 100644 index 0000000000000..ce0ef662e923e --- /dev/null +++ b/packages/@aws-cdk/integ-runner/test/runner/snapshot-test-runner.test.ts @@ -0,0 +1,154 @@ +import * as child_process from 'child_process'; +import * as path from 'path'; +import { SynthFastOptions, DestroyOptions, ListOptions, SynthOptions, DeployOptions } from 'cdk-cli-wrapper'; +import * as fs from 'fs-extra'; +import { IntegSnapshotRunner } from '../../lib/runner'; +import { DiagnosticReason } from '../../lib/workers/common'; +import { MockCdkProvider } from '../helpers'; + +let cdkMock: MockCdkProvider; +let synthMock: (options: SynthOptions) => void; +let synthFastMock: (options: SynthFastOptions) => void; +let deployMock: (options: DeployOptions) => void; +let listMock: (options: ListOptions) => string; +let destroyMock: (options: DestroyOptions) => void; +beforeEach(() => { + cdkMock = new MockCdkProvider({ directory: 'test/test-data' }); + listMock = jest.fn().mockImplementation(() => { + return 'stackabc'; + }); + synthMock = jest.fn().mockImplementation(); + deployMock = jest.fn().mockImplementation(); + destroyMock = jest.fn().mockImplementation(); + synthFastMock = jest.fn().mockImplementation(); + cdkMock.mockSynth(synthMock); + cdkMock.mockList(listMock); + cdkMock.mockDeploy(deployMock); + cdkMock.mockSynthFast(synthFastMock); + cdkMock.mockDestroy(destroyMock); + jest.spyOn(child_process, 'spawnSync').mockImplementation(); + jest.spyOn(process.stderr, 'write').mockImplementation(() => { return true; }); + jest.spyOn(process.stdout, 'write').mockImplementation(() => { return true; }); + jest.spyOn(fs, 'moveSync').mockImplementation(() => { return true; }); + jest.spyOn(fs, 'removeSync').mockImplementation(() => { return true; }); + jest.spyOn(fs, 'writeFileSync').mockImplementation(() => { return true; }); + jest.spyOn(fs, 'rmdirSync').mockImplementation(() => { return true; }); +}); + +afterEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + jest.restoreAllMocks(); +}); + +describe('IntegTest runSnapshotTests', () => { + test('with defaults no diff', () => { + // WHEN + const integTest = new IntegSnapshotRunner({ + cdk: cdkMock.cdk, + fileName: 'test/test-data/integ.test-with-snapshot.js', + directory: 'test/test-data', + integOutDir: 'test-with-snapshot.integ.snapshot', + }); + integTest.testSnapshot(); + + // THEN + expect(synthFastMock).toHaveBeenCalledTimes(1); + expect(synthFastMock).toHaveBeenCalledWith({ + env: expect.objectContaining({ + CDK_INTEG_ACCOUNT: '12345678', + CDK_INTEG_REGION: 'test-region', + }), + execCmd: ['node', 'integ.test-with-snapshot.js'], + output: 'test-with-snapshot.integ.snapshot', + }); + }); + + test('with defaults and diff', () => { + // WHEN + const integTest = new IntegSnapshotRunner({ + cdk: cdkMock.cdk, + fileName: path.join(__dirname, '../test-data/integ.test-with-snapshot.js'), + directory: 'test/test-data', + integOutDir: 'test-with-snapshot-diff.integ.snapshot', + }); + const results = integTest.testSnapshot(); + + // THEN + expect(synthFastMock).toHaveBeenCalledTimes(1); + expect(synthFastMock).toHaveBeenCalledWith({ + execCmd: ['node', 'integ.test-with-snapshot.js'], + env: expect.objectContaining({ + CDK_INTEG_ACCOUNT: '12345678', + CDK_INTEG_REGION: 'test-region', + }), + output: 'test-with-snapshot-diff.integ.snapshot', + }); + expect(results.diagnostics).toEqual(expect.arrayContaining([expect.objectContaining({ + reason: DiagnosticReason.SNAPSHOT_FAILED, + testName: integTest.testName, + message: expect.stringContaining('foobar'), + })])); + expect(results.destructiveChanges).not.toEqual([{ + impact: 'WILL_DESTROY', + logicalId: 'MyFunction1ServiceRole9852B06B', + stackName: 'test-stack', + }]); + expect(results.destructiveChanges).toEqual([{ + impact: 'WILL_DESTROY', + logicalId: 'MyLambdaFuncServiceRoleDefaultPolicyBEB0E748', + stackName: 'test-stack', + }]); + }); + + test('dont diff asset hashes', () => { + // WHEN + const integTest = new IntegSnapshotRunner({ + cdk: cdkMock.cdk, + fileName: path.join(__dirname, '../test-data/integ.test-with-snapshot-assets-diff.js'), + directory: 'test/test-data', + integOutDir: 'test-with-snapshot-assets.integ.snapshot', + }); + expect(() => { + integTest.testSnapshot(); + }).not.toThrow(); + + // THEN + expect(synthFastMock).toHaveBeenCalledTimes(1); + expect(synthFastMock).toHaveBeenCalledWith({ + execCmd: ['node', 'integ.test-with-snapshot-assets-diff.js'], + env: expect.objectContaining({ + CDK_INTEG_ACCOUNT: '12345678', + CDK_INTEG_REGION: 'test-region', + }), + output: 'test-with-snapshot-assets.integ.snapshot', + }); + }); + + test('diff asset hashes', () => { + // WHEN + const integTest = new IntegSnapshotRunner({ + cdk: cdkMock.cdk, + fileName: path.join(__dirname, '../test-data/integ.test-with-snapshot-assets.js'), + directory: 'test/test-data', + integOutDir: 'test-with-snapshot-assets-diff.integ.snapshot', + }); + const results = integTest.testSnapshot(); + + // THEN + expect(synthFastMock).toHaveBeenCalledTimes(1); + expect(synthFastMock).toHaveBeenCalledWith({ + execCmd: ['node', 'integ.test-with-snapshot-assets.js'], + env: expect.objectContaining({ + CDK_INTEG_ACCOUNT: '12345678', + CDK_INTEG_REGION: 'test-region', + }), + output: 'test-with-snapshot-assets-diff.integ.snapshot', + }); + expect(results.diagnostics).toEqual(expect.arrayContaining([expect.objectContaining({ + reason: DiagnosticReason.SNAPSHOT_FAILED, + testName: integTest.testName, + message: expect.stringContaining('Parameters'), + })])); + }); +}); diff --git a/packages/@aws-cdk/integ-runner/test/test-data/integ.test-with-snapshot-assets-diff.ts b/packages/@aws-cdk/integ-runner/test/test-data/integ.test-with-snapshot-assets-diff.ts index bcdc92fbcdcf2..8546bc895c5c8 100644 --- a/packages/@aws-cdk/integ-runner/test/test-data/integ.test-with-snapshot-assets-diff.ts +++ b/packages/@aws-cdk/integ-runner/test/test-data/integ.test-with-snapshot-assets-diff.ts @@ -1 +1 @@ -/// !cdk-integ test-stack pragma:enable-lookups +/// !cdk-integ test-stack pragma:enable-lookups pragma:disable-update-workflow diff --git a/packages/@aws-cdk/integ-runner/test/test-data/test-with-snapshot-assets-diff.integ.snapshot/asset.fec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509/index.js b/packages/@aws-cdk/integ-runner/test/test-data/test-with-snapshot-assets-diff.integ.snapshot/asset.fec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509/index.js new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/@aws-cdk/integ-runner/test/test-data/test-with-snapshot-assets-diff.integ.snapshot/manifest.json b/packages/@aws-cdk/integ-runner/test/test-data/test-with-snapshot-assets-diff.integ.snapshot/manifest.json index 94868e43c61de..3cd05541a2226 100644 --- a/packages/@aws-cdk/integ-runner/test/test-data/test-with-snapshot-assets-diff.integ.snapshot/manifest.json +++ b/packages/@aws-cdk/integ-runner/test/test-data/test-with-snapshot-assets-diff.integ.snapshot/manifest.json @@ -16,6 +16,20 @@ "validateOnSynth": false }, "metadata": { + "/test-stack": [ + { + "type": "aws:cdk:asset", + "data": { + "path": "asset.fec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509", + "id": "fec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509", + "packaging": "zip", + "sourceHash": "fec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509", + "s3BucketParameter": "AssetParametersfec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509S3BucketBF50F97C", + "s3KeyParameter": "AssetParametersfec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509S3VersionKeyF21AC8C1", + "artifactHashParameter": "AssetParametersfec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509ArtifactHash5D8C129B" + } + } + ], "/test-stack/MyFunction1/ServiceRole/Resource": [ { "type": "aws:cdk:logicalId", diff --git a/packages/@aws-cdk/integ-runner/test/test-data/test-with-snapshot-assets.integ.snapshot/asset.be270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824/index.js b/packages/@aws-cdk/integ-runner/test/test-data/test-with-snapshot-assets.integ.snapshot/asset.be270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824/index.js new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/@aws-cdk/integ-runner/test/test-data/test-with-snapshot-assets.integ.snapshot/integ.json b/packages/@aws-cdk/integ-runner/test/test-data/test-with-snapshot-assets.integ.snapshot/integ.json new file mode 100644 index 0000000000000..c6021138c3137 --- /dev/null +++ b/packages/@aws-cdk/integ-runner/test/test-data/test-with-snapshot-assets.integ.snapshot/integ.json @@ -0,0 +1,13 @@ +{ + "version": "v1.0.0", + "testCases": { + "test1": { + "stacks": ["test-stack"], + "stackUpdateWorkflow": false, + "diffAssets": true, + "allowDestroy": [ + "AWS::IAM::Role" + ] + } + } +} diff --git a/packages/@aws-cdk/integ-runner/test/test-data/test-with-snapshot-assets.integ.snapshot/manifest.json b/packages/@aws-cdk/integ-runner/test/test-data/test-with-snapshot-assets.integ.snapshot/manifest.json index 94868e43c61de..2a3663dbb6c29 100644 --- a/packages/@aws-cdk/integ-runner/test/test-data/test-with-snapshot-assets.integ.snapshot/manifest.json +++ b/packages/@aws-cdk/integ-runner/test/test-data/test-with-snapshot-assets.integ.snapshot/manifest.json @@ -8,6 +8,14 @@ }, "metadata": {} }, + "test-stack.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "test-stack.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, "test-stack": { "type": "aws:cloudformation:stack", "environment": "aws://unknown-account/unknown-region", diff --git a/packages/@aws-cdk/integ-runner/test/test-data/test-with-snapshot-assets.integ.snapshot/test-stack.assets.json b/packages/@aws-cdk/integ-runner/test/test-data/test-with-snapshot-assets.integ.snapshot/test-stack.assets.json new file mode 100644 index 0000000000000..9fce39f0cb708 --- /dev/null +++ b/packages/@aws-cdk/integ-runner/test/test-data/test-with-snapshot-assets.integ.snapshot/test-stack.assets.json @@ -0,0 +1,32 @@ +{ + "version": "17.0.0", + "files": { + "be270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824": { + "source": { + "path": "asset.be270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824", + "packaging": "zip" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "be270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824.zip", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + }, + "cb05f22f001734dbadeb4f07d875c6ab8180f703346a8d66fca572a0e94c54a9": { + "source": { + "path": "test-stack.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "cb05f22f001734dbadeb4f07d875c6ab8180f703346a8d66fca572a0e94c54a9.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} diff --git a/packages/@aws-cdk/integ-runner/test/workers/integ-worker.test.ts b/packages/@aws-cdk/integ-runner/test/workers/integ-worker.test.ts index fe9def07bc544..520f146fa02c8 100644 --- a/packages/@aws-cdk/integ-runner/test/workers/integ-worker.test.ts +++ b/packages/@aws-cdk/integ-runner/test/workers/integ-worker.test.ts @@ -15,6 +15,7 @@ beforeEach(() => { jest.spyOn(fs, 'emptyDirSync').mockImplementation(() => { return true; }); jest.spyOn(fs, 'unlinkSync').mockImplementation(() => { return true; }); jest.spyOn(fs, 'removeSync').mockImplementation(() => { return true; }); + jest.spyOn(fs, 'rmdirSync').mockImplementation(() => { return true; }); jest.spyOn(fs, 'writeFileSync').mockImplementation(() => { return true; }); spawnSyncMock = jest.spyOn(child_process, 'spawnSync').mockReturnValueOnce({ status: 0, @@ -55,6 +56,7 @@ describe('test runner', () => { // WHEN const test = { fileName: 'test/test-data/integ.integ-test1.js', + directory: 'test/test-data', }; integTestWorker({ tests: [test], @@ -77,6 +79,7 @@ describe('test runner', () => { // WHEN const test = { fileName: 'test/test-data/integ.integ-test2.js', + directory: 'test/test-data', }; jest.spyOn(child_process, 'spawnSync').mockImplementation(); const results = integTestWorker({ @@ -84,13 +87,17 @@ describe('test runner', () => { region: 'us-east-1', }); - expect(results[0]).toEqual({ fileName: expect.stringMatching(/integ.integ-test2.js$/) }); + expect(results[0]).toEqual({ + fileName: expect.stringMatching(/integ.integ-test2.js$/), + directory: 'test/test-data', + }); }); test('has snapshot', () => { // WHEN const test = { fileName: 'test/test-data/integ.test-with-snapshot.js', + directory: 'test/test-data', }; const results = integTestWorker({ tests: [test], @@ -128,6 +135,7 @@ describe('test runner', () => { // WHEN const test = { fileName: 'test/test-data/integ.test-with-snapshot.js', + directory: 'test/test-data', }; jest.spyOn(child_process, 'spawnSync').mockReturnValue({ status: 1, @@ -142,7 +150,10 @@ describe('test runner', () => { region: 'us-east-1', }); - expect(results[0]).toEqual({ fileName: 'test/test-data/integ.test-with-snapshot.js' }); + expect(results[0]).toEqual({ + fileName: 'test/test-data/integ.test-with-snapshot.js', + directory: 'test/test-data', + }); }); }); @@ -151,9 +162,11 @@ describe('parallel worker', () => { const tests = [ { fileName: 'integ.test-with-snapshot.js', + directory: 'test/test-data', }, { fileName: 'integ.another-test-with-snapshot.js', + directory: 'test/test-data', }, ]; await runIntegrationTests({ @@ -179,6 +192,7 @@ describe('parallel worker', () => { test('run tests', async () => { const tests = [{ fileName: 'integ.test-with-snapshot.js', + directory: 'test/test-data', }]; const results = await runIntegrationTestsInParallel({ pool, @@ -193,6 +207,7 @@ describe('parallel worker', () => { failedTests: expect.arrayContaining([ { fileName: 'integ.test-with-snapshot.js', + directory: 'test/test-data', }, ]), metrics: expect.arrayContaining([ @@ -211,15 +226,19 @@ describe('parallel worker', () => { const tests = [ { fileName: 'integ.test-with-snapshot.js', + directory: 'test/test-data', }, { fileName: 'integ.another-test-with-snapshot.js', + directory: 'test/test-data', }, { fileName: 'integ.another-test-with-snapshot2.js', + directory: 'test/test-data', }, { fileName: 'integ.another-test-with-snapshot3.js', + directory: 'test/test-data', }, ]; const results = await runIntegrationTestsInParallel({ @@ -245,15 +264,19 @@ describe('parallel worker', () => { failedTests: expect.arrayContaining([ { fileName: 'integ.test-with-snapshot.js', + directory: 'test/test-data', }, { fileName: 'integ.another-test-with-snapshot.js', + directory: 'test/test-data', }, { fileName: 'integ.another-test-with-snapshot2.js', + directory: 'test/test-data', }, { fileName: 'integ.another-test-with-snapshot3.js', + directory: 'test/test-data', }, ]), metrics: expect.arrayContaining([ @@ -297,9 +320,11 @@ describe('parallel worker', () => { const tests = [ { fileName: 'integ.test-with-snapshot.js', + directory: 'test/test-data', }, { fileName: 'integ.another-test-with-snapshot.js', + directory: 'test/test-data', }, ]; const results = await runIntegrationTestsInParallel({ @@ -318,9 +343,11 @@ describe('parallel worker', () => { failedTests: expect.arrayContaining([ { fileName: 'integ.test-with-snapshot.js', + directory: 'test/test-data', }, { fileName: 'integ.another-test-with-snapshot.js', + directory: 'test/test-data', }, ]), metrics: expect.arrayContaining([ @@ -346,9 +373,11 @@ describe('parallel worker', () => { const tests = [ { fileName: 'integ.test-with-snapshot.js', + directory: 'test/test-data', }, { fileName: 'integ.another-test-with-snapshot.js', + directory: 'test/test-data', }, ]; const results = await runIntegrationTestsInParallel({ @@ -367,9 +396,11 @@ describe('parallel worker', () => { failedTests: expect.arrayContaining([ { fileName: 'integ.another-test-with-snapshot.js', + directory: 'test/test-data', }, { fileName: 'integ.test-with-snapshot.js', + directory: 'test/test-data', }, ]), metrics: expect.arrayContaining([ @@ -389,9 +420,11 @@ describe('parallel worker', () => { const tests = [ { fileName: 'integ.test-with-snapshot.js', + directory: 'test/test-data', }, { fileName: 'integ.another-test-with-snapshot.js', + directory: 'test/test-data', }, ]; const results = await runIntegrationTestsInParallel({ @@ -410,9 +443,11 @@ describe('parallel worker', () => { failedTests: expect.arrayContaining([ { fileName: 'integ.test-with-snapshot.js', + directory: 'test/test-data', }, { fileName: 'integ.another-test-with-snapshot.js', + directory: 'test/test-data', }, ]), metrics: expect.arrayContaining([ diff --git a/packages/@aws-cdk/integ-runner/test/workers/snapshot-worker.test.ts b/packages/@aws-cdk/integ-runner/test/workers/snapshot-worker.test.ts index fa9a7d80015c6..5b47873fd9987 100644 --- a/packages/@aws-cdk/integ-runner/test/workers/snapshot-worker.test.ts +++ b/packages/@aws-cdk/integ-runner/test/workers/snapshot-worker.test.ts @@ -49,6 +49,7 @@ describe('Snapshot tests', () => { jest.spyOn(child_process, 'spawnSync').mockRejectedValue; const test = { fileName: path.join(directory, 'integ.test-with-snapshot-assets.js'), + directory, destructiveChanges: [], }; const result = snapshotTestWorker(test); diff --git a/packages/@aws-cdk/integ-runner/tsconfig.json b/packages/@aws-cdk/integ-runner/tsconfig.json index 04e0404f04442..602608087631f 100644 --- a/packages/@aws-cdk/integ-runner/tsconfig.json +++ b/packages/@aws-cdk/integ-runner/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { - "target": "ES2018", + "target": "ES2019", "module": "commonjs", - "lib": ["es2018", "dom"], + "lib": ["es2019", "dom"], "strict": true, "alwaysStrict": true, "declaration": true, diff --git a/packages/@aws-cdk/pipelines/test/integ.newpipeline-with-vpc.ts b/packages/@aws-cdk/pipelines/test/integ.newpipeline-with-vpc.ts index 590757335081f..f5bc697b6bad1 100644 --- a/packages/@aws-cdk/pipelines/test/integ.newpipeline-with-vpc.ts +++ b/packages/@aws-cdk/pipelines/test/integ.newpipeline-with-vpc.ts @@ -1,5 +1,5 @@ // eslint-disable-next-line import/no-extraneous-dependencies -/// !cdk-integ PipelineStack +/// !cdk-integ PipelineStack pragma:set-context:@aws-cdk/core:newStyleStackSynthesis=true import * as path from 'path'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as s3_assets from '@aws-cdk/aws-s3-assets'; @@ -54,4 +54,4 @@ const app = new App({ new PipelineStack(app, 'PipelineStack', { env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, }); -app.synth(); \ No newline at end of file +app.synth(); diff --git a/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets-single-upload.ts b/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets-single-upload.ts index 2dc718ca525a9..0ca088bd43635 100644 --- a/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets-single-upload.ts +++ b/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets-single-upload.ts @@ -1,4 +1,4 @@ -/// !cdk-integ PipelineStack +/// !cdk-integ PipelineStack pragma:set-context:@aws-cdk/core:newStyleStackSynthesis=true import * as path from 'path'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions'; diff --git a/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets.ts b/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets.ts index c2c9d0a733df8..1ee0185355b4d 100644 --- a/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets.ts +++ b/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets.ts @@ -1,4 +1,4 @@ -/// !cdk-integ PipelineStack +/// !cdk-integ PipelineStack pragma:set-context:@aws-cdk/core:newStyleStackSynthesis=true import * as path from 'path'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions'; diff --git a/packages/@aws-cdk/pipelines/test/integ.pipeline.ts b/packages/@aws-cdk/pipelines/test/integ.pipeline.ts index 29963e50ebc3b..ba14a3b41829d 100644 --- a/packages/@aws-cdk/pipelines/test/integ.pipeline.ts +++ b/packages/@aws-cdk/pipelines/test/integ.pipeline.ts @@ -1,4 +1,4 @@ -/// !cdk-integ PipelineStack +/// !cdk-integ PipelineStack pragma:set-context:@aws-cdk/core:newStyleStackSynthesis=true import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions'; import * as s3 from '@aws-cdk/aws-s3';