diff --git a/.github/ISSUE_TEMPLATE/test_descriptor.md b/.github/ISSUE_TEMPLATE/test_descriptor.md new file mode 100644 index 000000000..32de9a035 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/test_descriptor.md @@ -0,0 +1,5 @@ +--- +name: "package:test_descriptor" +about: "Create a bug or file a feature request against package:test_descriptor." +labels: "package:test_descriptor" +--- \ No newline at end of file diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 8cda374ed..e36e555bd 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable" @@ -44,7 +44,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0;packages:pkgs/fake_async;commands:analyze_1" @@ -74,7 +74,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:integration_tests/regression-integration_tests/wasm;commands:format-analyze_0" @@ -121,7 +121,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/checks-pkgs/matcher-pkgs/test_core;commands:analyze_1" @@ -169,7 +169,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:integration_tests/regression-integration_tests/spawn_hybrid-integration_tests/wasm-pkgs/checks-pkgs/fake_async-pkgs/matcher-pkgs/test-pkgs/test_api-pkgs/test_core;commands:format-analyze_0" @@ -355,7 +355,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0;packages:pkgs/fake_async;commands:command_00" @@ -393,7 +393,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:integration_tests/regression;commands:command_00" @@ -431,7 +431,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/checks;commands:command_00" @@ -469,7 +469,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/matcher;commands:command_00" @@ -507,7 +507,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test_core;commands:command_00" @@ -545,7 +545,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:integration_tests/spawn_hybrid;commands:test_1" @@ -583,7 +583,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:integration_tests/wasm;commands:test_2" @@ -621,7 +621,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test;commands:command_01" @@ -659,7 +659,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test;commands:command_02" @@ -697,7 +697,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test;commands:command_03" @@ -735,7 +735,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test;commands:command_04" @@ -773,7 +773,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test;commands:command_05" @@ -811,7 +811,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test_api;commands:command_11" @@ -849,7 +849,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:integration_tests/regression;commands:command_00" @@ -887,7 +887,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/checks;commands:command_00" @@ -925,7 +925,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/fake_async;commands:command_00" @@ -963,7 +963,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/matcher;commands:command_00" @@ -1001,7 +1001,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/test_core;commands:command_00" @@ -1039,7 +1039,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:integration_tests/spawn_hybrid;commands:test_1" @@ -1077,7 +1077,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:integration_tests/wasm;commands:test_2" @@ -1115,7 +1115,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/test;commands:command_01" @@ -1153,7 +1153,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/test;commands:command_02" @@ -1191,7 +1191,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/test;commands:command_03" @@ -1229,7 +1229,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/test;commands:command_04" @@ -1267,7 +1267,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/test;commands:command_05" @@ -1305,7 +1305,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/test_api;commands:command_11" @@ -1343,7 +1343,7 @@ jobs: runs-on: macos-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: "~/.pub-cache/hosted" key: "os:macos-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test;commands:command_06" @@ -1381,7 +1381,7 @@ jobs: runs-on: macos-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: "~/.pub-cache/hosted" key: "os:macos-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test;commands:command_07" @@ -1419,7 +1419,7 @@ jobs: runs-on: macos-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: "~/.pub-cache/hosted" key: "os:macos-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test;commands:command_08" @@ -1457,7 +1457,7 @@ jobs: runs-on: macos-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: "~/.pub-cache/hosted" key: "os:macos-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test;commands:command_09" @@ -1495,7 +1495,7 @@ jobs: runs-on: macos-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: "~/.pub-cache/hosted" key: "os:macos-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test;commands:command_10" diff --git a/.github/workflows/scorecards-analysis.yml b/.github/workflows/scorecards-analysis.yml index 3c5c10a2b..688cc3424 100644 --- a/.github/workflows/scorecards-analysis.yml +++ b/.github/workflows/scorecards-analysis.yml @@ -42,7 +42,7 @@ jobs: # Upload the results as artifacts (optional). - name: "Upload artifact" - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b with: name: SARIF file path: results.sarif @@ -50,6 +50,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5 + uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0 with: sarif_file: results.sarif diff --git a/.github/workflows/test_descriptor.yaml b/.github/workflows/test_descriptor.yaml new file mode 100644 index 000000000..928de57c2 --- /dev/null +++ b/.github/workflows/test_descriptor.yaml @@ -0,0 +1,72 @@ +name: package:test_descriptor + +on: + # Run on PRs and pushes to the default branch. + push: + branches: [ master ] + paths: + - '.github/workflows/test_descriptor.yaml' + - 'pkgs/test_descriptor/**' + pull_request: + branches: [ master ] + paths: + - '.github/workflows/test_descriptor.yaml' + - 'pkgs/test_descriptor/**' + schedule: + - cron: "0 0 * * 0" + +env: + PUB_ENVIRONMENT: bot.github + + +defaults: + run: + working-directory: pkgs/test_descriptor/ + +jobs: + # Check code formatting and static analysis on a single OS (linux) + # against Dart dev. + analyze: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + sdk: [dev] + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: ${{ matrix.sdk }} + - id: install + name: Install dependencies + run: dart pub get + - name: Check formatting + run: dart format --output=none --set-exit-if-changed . + if: always() && steps.install.outcome == 'success' + - name: Analyze code + run: dart analyze --fatal-infos + if: always() && steps.install.outcome == 'success' + + # Run tests on a matrix consisting of two dimensions: + # 1. OS: ubuntu-latest, (macos-latest, windows-latest) + # 2. release channel: dev + test: + needs: analyze + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + # Add macos-latest and/or windows-latest if relevant for this package. + os: [ubuntu-latest] + sdk: [3.1, dev] + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: ${{ matrix.sdk }} + - id: install + name: Install dependencies + run: dart pub get + - name: Run VM tests + run: dart test --platform vm + if: always() && steps.install.outcome == 'success' diff --git a/README.md b/README.md index 5b80a2811..f178ccbdf 100644 --- a/README.md +++ b/README.md @@ -22,4 +22,5 @@ literate API. | [test](pkgs/test/) | A full featured library for writing and running Dart tests across platforms. | [![pub package](https://img.shields.io/pub/v/test.svg)](https://pub.dev/packages/test) | | [test_api](pkgs/test_api/) | | [![pub package](https://img.shields.io/pub/v/test_api.svg)](https://pub.dev/packages/test_api) | | [test_core](pkgs/test_core/) | | [![pub package](https://img.shields.io/pub/v/test_core.svg)](https://pub.dev/packages/test_core) | +| [test_descriptor](pkgs/test_descriptor/) | An API for defining and verifying files and directory structures. | [![pub package](https://img.shields.io/pub/v/test_descriptor.svg)](https://pub.dev/packages/test_descriptor) | | [test_process](pkgs/test_process/) | Test processes: starting; validating stdout and stderr; checking exit code | [![pub package](https://img.shields.io/pub/v/test_process.svg)](https://pub.dev/packages/test_process) | diff --git a/pkgs/test_descriptor/.gitignore b/pkgs/test_descriptor/.gitignore new file mode 100644 index 000000000..0659a3398 --- /dev/null +++ b/pkgs/test_descriptor/.gitignore @@ -0,0 +1,9 @@ +.buildlog +.DS_Store +.idea +.settings/ +build/ +packages +.packages +pubspec.lock +.dart_tool/ diff --git a/pkgs/test_descriptor/AUTHORS b/pkgs/test_descriptor/AUTHORS new file mode 100644 index 000000000..e8063a8cd --- /dev/null +++ b/pkgs/test_descriptor/AUTHORS @@ -0,0 +1,6 @@ +# Below is a list of people and organizations that have contributed +# to the project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. diff --git a/pkgs/test_descriptor/CHANGELOG.md b/pkgs/test_descriptor/CHANGELOG.md new file mode 100644 index 000000000..b9d876660 --- /dev/null +++ b/pkgs/test_descriptor/CHANGELOG.md @@ -0,0 +1,59 @@ +## 2.0.2 + +* Require Dart 3.1 or later. +* Move to `dart-lang/test` monorepo. + +## 2.0.1 + +* Populate the pubspec `repository` field. +* Migrate to `package:lints`. +* Update the package's markdown badges. + +## 2.0.0 + +* Null safety stable release. +* BREAKING: Removed archive support. +* BREAKING: `DirectoryDescriptor.load` only supports a `String` path instead of + also accepting relative `Uri` objects. +* BREAKING: `DirectoryDescriptor.load` no longer has an optional `parents` + parameter - this was intended for internal use only. + +## 1.2.0 + +* Add an `ArchiveDescriptor` class and a corresponding `archive()` function that + can create and validate Zip and TAR archives. + +## 1.1.1 + +* Update to lowercase Dart core library constants. + +## 1.1.0 + +* Add a `path()` function that returns the a path within the sandbox directory. + +* Add `io` getters to `FileDescriptor` and `DirectoryDescriptor` that returns + `dart:io` `File` and `Directory` objects, respectively, within the sandbox + directory. + +## 1.0.4 + +* Support test `1.x.x'. + +## 1.0.3 + +* Stop using comment-based generics. + +## 1.0.2 + +* Declare support for `async` 2.0.0. + +## 1.0.1 + +* `FileDescriptor.validate()` now allows invalid UTF-8 files. + +* Fix a bug where `DirectoryDescriptor.load()` would incorrectly report that + multiple versions of a file or directory existed. + +## 1.0.0 + +* Initial version. diff --git a/pkgs/test_descriptor/LICENSE b/pkgs/test_descriptor/LICENSE new file mode 100644 index 000000000..237243134 --- /dev/null +++ b/pkgs/test_descriptor/LICENSE @@ -0,0 +1,27 @@ +Copyright 2016, the Dart project authors. + +Redistribution and use 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 Google LLC 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. diff --git a/pkgs/test_descriptor/README.md b/pkgs/test_descriptor/README.md new file mode 100644 index 000000000..9a1d27779 --- /dev/null +++ b/pkgs/test_descriptor/README.md @@ -0,0 +1,56 @@ +[![Build Status](https://github.com/dart-lang/test/actions/workflows/test_descriptor.yaml/badge.svg)](https://github.com/dart-lang/test/actions/workflows/test_descriptor.yaml) +[![pub package](https://img.shields.io/pub/v/test_descriptor.svg)](https://pub.dev/packages/test_descriptor) +[![package publisher](https://img.shields.io/pub/publisher/test_descriptor.svg)](https://pub.dev/packages/test_descriptor/publisher) + +The `test_descriptor` package provides a convenient, easy-to-read API for +defining and verifying directory structures in tests. + +## Usage + +We recommend that you import this library with the `d` prefix. The +[`d.dir()`][dir] and [`d.file()`][file] functions are the main entrypoints. They +define a filesystem structure that can be created using +[`Descriptor.create()`][create] and verified using +[`Descriptor.validate()`][validate]. For example: + +[dir]: https://pub.dev/documentation/test_descriptor/latest/test_descriptor/dir.html +[file]: https://pub.dev/documentation/test_descriptor/latest/test_descriptor/file.html +[create]: https://pub.dev/documentation/test_descriptor/latest/test_descriptor/Descriptor/create.html +[validate]: https://pub.dev/documentation/test_descriptor/latest/test_descriptor/Descriptor/validate.html + +```dart +import 'dart:io'; + +import 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +void main() { + test('Directory.rename', () async { + await d.dir('parent', [ + d.file('sibling', 'sibling-contents'), + d.dir('old-name', [d.file('child', 'child-contents')]) + ]).create(); + + await Directory('${d.sandbox}/parent/old-name') + .rename('${d.sandbox}/parent/new-name'); + + await d.dir('parent', [ + d.file('sibling', 'sibling-contents'), + d.dir('new-name', [d.file('child', 'child-contents')]) + ]).validate(); + }); +} +``` + +By default, descriptors create entries in a temporary sandbox directory, +[`d.sandbox`][sandbox]. A new sandbox is automatically created the first time +you create a descriptor in a given test, and automatically deleted once the test +finishes running. + +[sandbox]: https://pub.dev/documentation/test_descriptor/latest/test_descriptor/sandbox.html + +This package is [`term_glyph`][term_glyph] aware. It will decide whether to use +ASCII or Unicode glyphs based on the [`glyph.ascii`][ascii] attribute. + +[term_glyph]: https://pub.dev/packages/term_glyph +[ascii]: https://pub.dev/documentation/term_glyph/latest/term_glyph/ascii.html diff --git a/pkgs/test_descriptor/analysis_options.yaml b/pkgs/test_descriptor/analysis_options.yaml new file mode 100644 index 000000000..e9a8c394d --- /dev/null +++ b/pkgs/test_descriptor/analysis_options.yaml @@ -0,0 +1,33 @@ +# https://dart.dev/guides/language/analysis-options +include: package:dart_flutter_team_lints/analysis_options.yaml + +analyzer: + language: + strict-casts: true + strict-inference: true + strict-raw-types: true + +linter: + rules: + - avoid_bool_literals_in_conditional_expressions + - avoid_classes_with_only_static_members + - avoid_private_typedef_functions + - avoid_redundant_argument_values + - avoid_returning_this + - avoid_unused_constructor_parameters + - avoid_void_async + - cancel_subscriptions + - join_return_with_assignment + - literal_only_boolean_expressions + - missing_whitespace_between_adjacent_strings + - no_adjacent_strings_in_list + - no_runtimeType_toString + - prefer_const_declarations + - prefer_expression_function_bodies + - prefer_final_locals + - unnecessary_await_in_return + - unnecessary_raw_strings + - use_if_null_to_convert_nulls_to_bools + - use_raw_strings + - use_string_buffers + - require_trailing_commas diff --git a/pkgs/test_descriptor/example/example.dart b/pkgs/test_descriptor/example/example.dart new file mode 100644 index 000000000..457383257 --- /dev/null +++ b/pkgs/test_descriptor/example/example.dart @@ -0,0 +1,25 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +void main() { + test('Directory.rename', () async { + await d.dir('parent', [ + d.file('sibling', 'sibling-contents'), + d.dir('old-name', [d.file('child', 'child-contents')]), + ]).create(); + + await Directory('${d.sandbox}/parent/old-name') + .rename('${d.sandbox}/parent/new-name'); + + await d.dir('parent', [ + d.file('sibling', 'sibling-contents'), + d.dir('new-name', [d.file('child', 'child-contents')]), + ]).validate(); + }); +} diff --git a/pkgs/test_descriptor/lib/src/descriptor.dart b/pkgs/test_descriptor/lib/src/descriptor.dart new file mode 100644 index 000000000..0d6c30c35 --- /dev/null +++ b/pkgs/test_descriptor/lib/src/descriptor.dart @@ -0,0 +1,26 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'sandbox.dart'; + +/// A declarative description of a filesystem entry. +/// +/// This may be extended outside this package. +abstract class Descriptor { + /// This entry's basename. + final String name; + + Descriptor(this.name); + + /// Creates this entry within the [parent] directory, which defaults to + /// [sandbox]. + Future create([String? parent]); + + /// Validates that the physical file system under [parent] (which defaults to + /// [sandbox]) contains an entry that matches this descriptor. + Future validate([String? parent]); + + /// Returns a human-friendly tree-style description of this descriptor. + String describe(); +} diff --git a/pkgs/test_descriptor/lib/src/directory_descriptor.dart b/pkgs/test_descriptor/lib/src/directory_descriptor.dart new file mode 100644 index 000000000..c4f269452 --- /dev/null +++ b/pkgs/test_descriptor/lib/src/directory_descriptor.dart @@ -0,0 +1,132 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'package:async/async.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +import 'descriptor.dart'; +import 'file_descriptor.dart'; +import 'sandbox.dart'; +import 'utils.dart'; + +/// A descriptor describing a directory that may contain nested descriptors. +/// +/// In addition to the normal descriptor methods, this has a [load] method that +/// allows it to be used as a virtual filesystem. +/// +/// This may be extended outside this package. +class DirectoryDescriptor extends Descriptor { + /// Descriptors for entries in this directory. + /// + /// This may be modified. + final List contents; + + /// Returns a `dart:io` [Directory] object that refers to this file within + /// [sandbox]. + Directory get io => Directory(p.join(sandbox, name)); + + DirectoryDescriptor(super.name, Iterable contents) + : contents = contents.toList(); + + /// Creates a directory descriptor named [name] that describes the physical + /// directory at [path]. + factory DirectoryDescriptor.fromFilesystem(String name, String path) => + DirectoryDescriptor( + name, + Directory(path).listSync().map((entity) { + // Ignore hidden files. + if (p.basename(entity.path).startsWith('.')) return null; + + if (entity is Directory) { + return DirectoryDescriptor.fromFilesystem( + p.basename(entity.path), + entity.path, + ); + } else if (entity is File) { + return FileDescriptor( + p.basename(entity.path), + entity.readAsBytesSync(), + ); + } + // Ignore broken symlinks. + return null; + }).whereType(), + ); + + @override + Future create([String? parent]) async { + final fullPath = p.join(parent ?? sandbox, name); + await Directory(fullPath).create(recursive: true); + await Future.wait(contents.map((entry) => entry.create(fullPath))); + } + + @override + Future validate([String? parent]) async { + final fullPath = p.join(parent ?? sandbox, name); + if (!(await Directory(fullPath).exists())) { + fail('Directory not found: "${prettyPath(fullPath)}".'); + } + + await waitAndReportErrors( + contents.map((entry) => entry.validate(fullPath)), + ); + } + + /// Treats this descriptor as a virtual filesystem and loads the binary + /// contents of the [FileDescriptor] at the given relative [path]. + Stream> load(String path) => _load(path); + + /// Implementation of [load], tracks parents through recursive calls. + Stream> _load(String path, [String? parents]) { + if (!p.url.isWithin('.', path)) { + throw ArgumentError.value( + path, + 'path', + 'must be relative and beneath the base URL.', + ); + } + + return StreamCompleter.fromFuture( + Future.sync(() { + final split = p.url.split(p.url.normalize(path)); + final file = split.length == 1; + final matchingEntries = contents + .where( + (entry) => + entry.name == split.first && + (file + ? entry is FileDescriptor + : entry is DirectoryDescriptor), + ) + .toList(); + + final type = file ? 'file' : 'directory'; + final parentsAndSelf = + parents == null ? name : p.url.join(parents, name); + if (matchingEntries.isEmpty) { + fail( + 'Couldn\'t find a $type descriptor named "${split.first}" within ' + '"$parentsAndSelf".'); + } else if (matchingEntries.length > 1) { + fail('Found multiple $type descriptors named "${split.first}" within ' + '"$parentsAndSelf".'); + } else { + final remainingPath = split.sublist(1); + if (remainingPath.isEmpty) { + return (matchingEntries.first as FileDescriptor).readAsBytes(); + } else { + return (matchingEntries.first as DirectoryDescriptor) + ._load(p.url.joinAll(remainingPath), parentsAndSelf); + } + } + }), + ); + } + + @override + String describe() => describeDirectory(name, contents); +} diff --git a/pkgs/test_descriptor/lib/src/file_descriptor.dart b/pkgs/test_descriptor/lib/src/file_descriptor.dart new file mode 100644 index 000000000..2daedd52f --- /dev/null +++ b/pkgs/test_descriptor/lib/src/file_descriptor.dart @@ -0,0 +1,210 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; +import 'dart:io'; +import 'dart:math' as math; + +import 'package:collection/collection.dart'; +import 'package:path/path.dart' as p; +import 'package:term_glyph/term_glyph.dart' as glyph; +import 'package:test/test.dart'; + +import 'descriptor.dart'; +import 'sandbox.dart'; +import 'utils.dart'; + +/// A descriptor describing a single file. +/// +/// In addition to the normal descriptor methods, this has [read] and +/// [readAsBytes] methods that allows its contents to be read. +/// +/// This may be extended outside this package. +abstract class FileDescriptor extends Descriptor { + /// Creates a new [FileDescriptor] with [name] and [contents]. + /// + /// The [contents] may be a `String`, a `List`, or a [Matcher]. If it's a + /// string, [create] creates a UTF-8 file and [validate] parses the physical + /// file as UTF-8. If it's a [Matcher], [validate] matches it against the + /// physical file's contents parsed as UTF-8, and [create], [read], and + /// [readAsBytes] are unsupported. + /// + /// If [contents] isn't passed, [create] creates an empty file and [validate] + /// verifies that the file is empty. + /// + /// To match a [Matcher] against a file's binary contents, use + /// [FileDescriptor.binaryMatcher] instead. + factory FileDescriptor(String name, Object? contents) { + if (contents is String) return _StringFileDescriptor(name, contents); + if (contents is List) { + return _BinaryFileDescriptor(name, contents.cast()); + } + if (contents == null) return _BinaryFileDescriptor(name, []); + return _MatcherFileDescriptor(name, contents as Matcher); + } + + /// Returns a `dart:io` [File] object that refers to this file within + /// [sandbox]. + File get io => File(p.join(sandbox, name)); + + /// Creates a new binary [FileDescriptor] with [name] that matches its binary + /// contents against [matcher]. + /// + /// The [create], [read], and [readAsBytes] methods are unsupported for this + /// descriptor. + factory FileDescriptor.binaryMatcher(String name, Matcher matcher) => + _MatcherFileDescriptor(name, matcher, isBinary: true); + + /// A protected constructor that's only intended for subclasses. + FileDescriptor.protected(super.name); + + @override + Future create([String? parent]) async { + // Create the stream before we call [File.openWrite] because it may fail + // fast (e.g. if this is a matcher file). + final file = File(p.join(parent ?? sandbox, name)).openWrite(); + try { + await readAsBytes().forEach(file.add); + } finally { + await file.close(); + } + } + + @override + Future validate([String? parent]) async { + final fullPath = p.join(parent ?? sandbox, name); + final pretty = prettyPath(fullPath); + if (!(await File(fullPath).exists())) { + fail('File not found: "$pretty".'); + } + + await _validate(pretty, await File(fullPath).readAsBytes()); + } + + /// Validates that [binaryContents] matches the expected contents of + /// the descriptor. + /// + /// The [prettyPath] is a human-friendly representation of the path to the + /// descriptor. + FutureOr _validate(String prettyPath, List binaryContents); + + /// Reads and decodes the contents of this descriptor as a UTF-8 string. + /// + /// This isn't supported for matcher descriptors. + Future read() => utf8.decodeStream(readAsBytes()); + + /// Reads the contents of this descriptor as a byte stream. + /// + /// This isn't supported for matcher descriptors. + Stream> readAsBytes(); + + @override + String describe() => name; +} + +class _BinaryFileDescriptor extends FileDescriptor { + /// The contents of this descriptor's file. + final List _contents; + + _BinaryFileDescriptor(super.name, this._contents) : super.protected(); + + @override + Stream> readAsBytes() => Stream.fromIterable([_contents]); + + @override + Future _validate(String prettPath, List actualContents) async { + if (const IterableEquality().equals(_contents, actualContents)) return; + // TODO(nweiz): show a hex dump here if the data is small enough. + fail('File "$prettPath" didn\'t contain the expected binary data.'); + } +} + +class _StringFileDescriptor extends FileDescriptor { + /// The contents of this descriptor's file. + final String _contents; + + _StringFileDescriptor(super.name, this._contents) : super.protected(); + + @override + Future read() async => _contents; + + @override + Stream> readAsBytes() => + Stream.fromIterable([utf8.encode(_contents)]); + + @override + void _validate(String prettyPath, List actualContents) { + final actualContentsText = utf8.decode(actualContents); + if (_contents == actualContentsText) return; + fail(_textMismatchMessage(prettyPath, _contents, actualContentsText)); + } + + String _textMismatchMessage( + String prettyPath, + String expected, + String actual, + ) { + final expectedLines = expected.split('\n'); + final actualLines = actual.split('\n'); + + final results = []; + + // Compare them line by line to see which ones match. + final length = math.max(expectedLines.length, actualLines.length); + for (var i = 0; i < length; i++) { + if (i >= actualLines.length) { + // Missing output. + results.add('? ${expectedLines[i]}'); + } else if (i >= expectedLines.length) { + // Unexpected extra output. + results.add('X ${actualLines[i]}'); + } else { + final expectedLine = expectedLines[i]; + final actualLine = actualLines[i]; + + if (expectedLine != actualLine) { + // Mismatched lines. + results.add('X $actualLine'); + } else { + // Matched lines. + results.add('${glyph.verticalLine} $actualLine'); + } + } + } + + return 'File "$prettyPath" should contain:\n' + '${addBar(expected)}\n' + 'but actually contained:\n' + "${results.join('\n')}"; + } +} + +class _MatcherFileDescriptor extends FileDescriptor { + /// The matcher for this descriptor's contents. + final Matcher _matcher; + + /// Whether [_matcher] should match against the file's string or byte + /// contents. + final bool _isBinary; + + _MatcherFileDescriptor(super.name, this._matcher, {bool isBinary = false}) + : _isBinary = isBinary, + super.protected(); + + @override + Stream> readAsBytes() => + throw UnsupportedError("Matcher files can't be created or read."); + + @override + Future _validate(String prettyPath, List actualContents) async { + try { + expect( + _isBinary ? actualContents : utf8.decode(actualContents), + _matcher, + ); + } on TestFailure catch (error) { + fail('Invalid contents for file "$prettyPath":\n${error.message}'); + } + } +} diff --git a/pkgs/test_descriptor/lib/src/nothing_descriptor.dart b/pkgs/test_descriptor/lib/src/nothing_descriptor.dart new file mode 100644 index 000000000..da9e2a29d --- /dev/null +++ b/pkgs/test_descriptor/lib/src/nothing_descriptor.dart @@ -0,0 +1,38 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +import 'descriptor.dart'; +import 'sandbox.dart'; +import 'utils.dart'; + +/// A descriptor that validates that no file exists with the given name. +/// +/// Calling [create] does nothing. +class NothingDescriptor extends Descriptor { + NothingDescriptor(super.name); + + @override + Future create([String? parent]) async {} + + @override + Future validate([String? parent]) async { + final fullPath = p.join(parent ?? sandbox, name); + final pretty = prettyPath(fullPath); + if (File(fullPath).existsSync()) { + fail('Expected nothing to exist at "$pretty", but found a file.'); + } else if (Directory(fullPath).existsSync()) { + fail('Expected nothing to exist at "$pretty", but found a directory.'); + } else if (Link(fullPath).existsSync()) { + fail('Expected nothing to exist at "$pretty", but found a link.'); + } + } + + @override + String describe() => 'nothing at "$name"'; +} diff --git a/pkgs/test_descriptor/lib/src/pattern_descriptor.dart b/pkgs/test_descriptor/lib/src/pattern_descriptor.dart new file mode 100644 index 000000000..d55afc06e --- /dev/null +++ b/pkgs/test_descriptor/lib/src/pattern_descriptor.dart @@ -0,0 +1,105 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; +import 'dart:io'; + +import 'package:async/async.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +import 'descriptor.dart'; +import 'sandbox.dart'; +import 'utils.dart'; + +/// A descriptor that matches filesystem entity names by [Pattern] rather than +/// by exact [String]. +/// +/// This descriptor may only be used for validation. +class PatternDescriptor extends Descriptor { + /// The [Pattern] this matches filenames against. Note that the pattern must + /// match the entire basename of the file. + final Pattern pattern; + + /// The function used to generate the [Descriptor] for filesystem entities + /// matching [pattern]. + final Descriptor Function(String) _fn; + + PatternDescriptor(this.pattern, Descriptor Function(String basename) child) + : _fn = child, + super('$pattern'); + + /// Validates that there is some filesystem entity in [parent] that matches + /// [pattern] and the child entry. This finds all entities in [parent] + /// matching [pattern], then passes each of their names to `child` provided + /// in the constructor and validates the result. If exactly one succeeds, + /// `this` is considered valid. + @override + Future validate([String? parent]) async { + final inSandbox = parent == null; + parent ??= sandbox; + final matchingEntries = await Directory(parent) + .list() + .map( + (entry) => + entry is File ? entry.resolveSymbolicLinksSync() : entry.path, + ) + .where((entry) => matchesAll(pattern, p.basename(entry))) + .toList(); + matchingEntries.sort(); + + final location = inSandbox ? 'sandbox' : '"${prettyPath(parent)}"'; + if (matchingEntries.isEmpty) { + fail('No entries found in $location matching $_patternDescription.'); + } + + final results = await Future.wait( + matchingEntries + .map((entry) { + final basename = p.basename(entry); + return runZonedGuarded( + () => Result.capture( + Future.sync(() async { + await _fn(basename).validate(parent); + return basename; + }), + ), (_, __) { + // Validate may produce multiple errors, but we ignore all but the + // first to avoid cluttering the user with many different errors + // from many different un-matched entries. + }); + }) + .whereType>>() + .toList(), + ); + + final successes = results.where((result) => result.isValue).toList(); + if (successes.isEmpty) { + await waitAndReportErrors(results.map((result) => result.asFuture)); + } else if (successes.length > 1) { + fail('Multiple valid entries found in $location matching ' + '$_patternDescription:\n' + '${bullet(successes.map((result) => result.asValue!.value))}'); + } + } + + @override + String describe() => 'entry matching $_patternDescription'; + + String get _patternDescription { + if (pattern is String) return '"$pattern"'; + if (pattern is! RegExp) return '$pattern'; + + final regExp = pattern as RegExp; + final flags = StringBuffer(); + if (!regExp.isCaseSensitive) flags.write('i'); + if (regExp.isMultiLine) flags.write('m'); + return '/${regExp.pattern}/$flags'; + } + + @override + Future create([String? parent]) { + throw UnsupportedError("Pattern descriptors don't support create()."); + } +} diff --git a/pkgs/test_descriptor/lib/src/sandbox.dart b/pkgs/test_descriptor/lib/src/sandbox.dart new file mode 100644 index 000000000..128b6a94a --- /dev/null +++ b/pkgs/test_descriptor/lib/src/sandbox.dart @@ -0,0 +1,35 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'package:test/test.dart'; + +/// The sandbox directory in which descriptors are created and validated by +/// default. +/// +/// This is a temporary directory beneath [Directory.systemTemp]. A new one is +/// created the first time [sandbox] is accessed for each test case, and +/// automatically deleted after the test finishes running. +String get sandbox { + if (_sandbox != null) return _sandbox!; + // Resolve symlinks so we don't end up with inconsistent paths on Mac OS where + // /tmp is symlinked. + final sandbox = _sandbox = Directory.systemTemp + .createTempSync('dart_test_') + .resolveSymbolicLinksSync(); + + addTearDown(() async { + final sandbox = _sandbox!; + _sandbox = null; + await Directory(sandbox).delete(recursive: true); + }); + + return sandbox; +} + +String? _sandbox; + +/// Whether [sandbox] has been created. +bool get sandboxExists => _sandbox != null; diff --git a/pkgs/test_descriptor/lib/src/utils.dart b/pkgs/test_descriptor/lib/src/utils.dart new file mode 100644 index 000000000..f3542bdc9 --- /dev/null +++ b/pkgs/test_descriptor/lib/src/utils.dart @@ -0,0 +1,127 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:convert'; + +import 'package:path/path.dart' as p; +import 'package:term_glyph/term_glyph.dart' as glyph; +import 'package:test/test.dart'; + +import 'descriptor.dart'; +import 'sandbox.dart'; + +/// A UTF-8 codec that allows malformed byte sequences. +const utf8 = Utf8Codec(allowMalformed: true); + +/// Prepends a vertical bar to [text]. +String addBar(String text) => prefixLines( + text, + '${glyph.verticalLine} ', + first: '${glyph.downEnd} ', + last: '${glyph.upEnd} ', + single: '| ', + ); + +/// Indents [text], and adds a bullet at the beginning. +String addBullet(String text) => + prefixLines(text, ' ', first: '${glyph.bullet} '); + +/// Converts [strings] to a bulleted list. +String bullet(Iterable strings) => strings.map(addBullet).join('\n'); + +/// Returns a human-readable description of a directory with the given [name] +/// and [contents]. +String describeDirectory(String name, List contents) { + if (contents.isEmpty) return name; + + final buffer = StringBuffer(); + buffer.writeln(name); + for (var entry in contents.take(contents.length - 1)) { + final entryString = prefixLines( + entry.describe(), + '${glyph.verticalLine} ', + first: '${glyph.teeRight}${glyph.horizontalLine}' + '${glyph.horizontalLine} ', + ); + buffer.writeln(entryString); + } + + final lastEntryString = prefixLines( + contents.last.describe(), + ' ', + first: '${glyph.bottomLeftCorner}${glyph.horizontalLine}' + '${glyph.horizontalLine} ', + ); + buffer.write(lastEntryString); + return buffer.toString(); +} + +/// Prepends each line in [text] with [prefix]. +/// +/// If [first] or [last] is passed, the first and last lines, respectively, are +/// prefixed with those instead. If [single] is passed, it's used if there's +/// only a single line; otherwise, [first], [last], or [prefix] is used, in that +/// order of precedence. +String prefixLines( + String text, + String prefix, { + String? first, + String? last, + String? single, +}) { + single ??= first ?? last ?? prefix; + first ??= prefix; + last ??= prefix; + + final lines = text.split('\n'); + if (lines.length == 1) return '$single$text'; + + final buffer = StringBuffer('$first${lines.first}\n'); + for (var line in lines.skip(1).take(lines.length - 2)) { + buffer.writeln('$prefix$line'); + } + buffer.write('$last${lines.last}'); + return buffer.toString(); +} + +/// Returns a representation of [path] that's easy for humans to read. +/// +/// This may not be a valid path relative to [p.current]. +String prettyPath(String path) { + if (sandboxExists && p.isWithin(sandbox, path)) { + return p.relative(path, from: sandbox); + } else if (p.isWithin(p.current, path)) { + return p.relative(path); + } else { + return path; + } +} + +/// Returns whether [pattern] matches all of [string]. +bool matchesAll(Pattern pattern, String string) => + pattern.matchAsPrefix(string)?.end == string.length; + +/// Like [Future.wait] with `eagerError: true`, but reports errors after the +/// first using [registerException] rather than silently ignoring them. +Future> waitAndReportErrors(Iterable> futures) { + var errored = false; + return Future.wait( + futures.map( + (future) => + // Avoid async/await so that we synchronously add error handlers for the + // futures to keep them from top-leveling. + future.catchError( + // ignore: body_might_complete_normally_catch_error + (Object error, StackTrace stackTrace) { + if (!errored) { + errored = true; + throw error; // ignore: only_throw_errors + } else { + registerException(error, stackTrace); + } + }, + ), + ), + ); +} diff --git a/pkgs/test_descriptor/lib/test_descriptor.dart b/pkgs/test_descriptor/lib/test_descriptor.dart new file mode 100644 index 000000000..b302910f5 --- /dev/null +++ b/pkgs/test_descriptor/lib/test_descriptor.dart @@ -0,0 +1,80 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +import 'src/descriptor.dart'; +import 'src/directory_descriptor.dart'; +import 'src/file_descriptor.dart'; +import 'src/nothing_descriptor.dart'; +import 'src/pattern_descriptor.dart'; +import 'src/sandbox.dart'; + +export 'src/descriptor.dart'; +export 'src/directory_descriptor.dart'; +export 'src/file_descriptor.dart'; +export 'src/nothing_descriptor.dart'; +export 'src/pattern_descriptor.dart'; +export 'src/sandbox.dart' show sandbox; + +/// Creates a new [FileDescriptor] with [name] and [contents]. +/// +/// The [contents] may be a `String`, a `List`, or a [Matcher]. If it's a +/// string, [Descriptor.create] creates a UTF-8 file and [Descriptor.validate] +/// parses the physical file as UTF-8. If it's a [Matcher], +/// [Descriptor.validate] matches it against the physical file's contents parsed +/// as UTF-8, and [Descriptor.create] is unsupported. +/// +/// If [contents] isn't passed, [Descriptor.create] creates an empty file and +/// [Descriptor.validate] verifies that the file is empty. +/// +/// To match a [Matcher] against a file's binary contents, use +/// [FileDescriptor.binaryMatcher] instead. +FileDescriptor file(String name, [Object? contents]) => + FileDescriptor(name, contents); + +/// Creates a new [DirectoryDescriptor] descriptor with [name] and [contents]. +/// +/// [Descriptor.validate] requires that all descriptors in [contents] match +/// children of the physical diretory, but it *doesn't* require that no other +/// children exist. To ensure that a particular child doesn't exist, use +/// [nothing]. +DirectoryDescriptor dir(String name, [Iterable? contents]) => + DirectoryDescriptor(name, contents ?? []); + +/// Creates a new [NothingDescriptor] descriptor that asserts that no entry +/// named [name] exists. +/// +/// [Descriptor.create] does nothing for this descriptor. +NothingDescriptor nothing(String name) => NothingDescriptor(name); + +/// Creates a new [PatternDescriptor] descriptor that asserts than an entry with +/// a name matching [pattern] exists, and matches the [Descriptor] returned +/// by [child]. +/// +/// The [child] callback is passed the basename of each entry matching [name]. +/// It returns a descriptor that should match that entry. It's valid for +/// multiple entries to match [name] as long as only one of them matches +/// [child]. +/// +/// [Descriptor.create] is not supported for this descriptor. +PatternDescriptor pattern( + Pattern name, + Descriptor Function(String basename) child, +) => + PatternDescriptor(name, child); + +/// A convenience method for creating a [PatternDescriptor] descriptor that +/// constructs a [FileDescriptor] descriptor. +PatternDescriptor filePattern(Pattern name, [Object? contents]) => + pattern(name, (realName) => file(realName, contents)); + +/// A convenience method for creating a [PatternDescriptor] descriptor that +/// constructs a [DirectoryDescriptor] descriptor. +PatternDescriptor dirPattern(Pattern name, [Iterable? contents]) => + pattern(name, (realName) => dir(realName, contents)); + +/// Returns [path] within the [sandbox] directory. +String path(String path) => p.join(sandbox, path); diff --git a/pkgs/test_descriptor/pubspec.yaml b/pkgs/test_descriptor/pubspec.yaml new file mode 100644 index 000000000..d9a97ac91 --- /dev/null +++ b/pkgs/test_descriptor/pubspec.yaml @@ -0,0 +1,17 @@ +name: test_descriptor +version: 2.0.2 +description: An API for defining and verifying files and directory structures. +repository: https://github.com/dart-lang/test/tree/master/pkgs/test_descriptor + +environment: + sdk: ^3.1.0 + +dependencies: + async: ^2.5.0 + collection: ^1.15.0 + path: ^1.8.0 + term_glyph: ^1.2.0 + test: ^1.16.6 + +dev_dependencies: + dart_flutter_team_lints: ^3.0.0 diff --git a/pkgs/test_descriptor/test/directory_test.dart b/pkgs/test_descriptor/test/directory_test.dart new file mode 100644 index 000000000..bac615353 --- /dev/null +++ b/pkgs/test_descriptor/test/directory_test.dart @@ -0,0 +1,362 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +@TestOn('vm') +library; + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:term_glyph/term_glyph.dart' as term_glyph; +import 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import 'utils.dart'; + +void main() { + group('create()', () { + test('creates a directory and its contents', () async { + await d.dir('dir', [ + d.dir('subdir', [ + d.file('subfile1.txt', 'subcontents1'), + d.file('subfile2.txt', 'subcontents2'), + ]), + d.file('file1.txt', 'contents1'), + d.file('file2.txt', 'contents2'), + ]).create(); + + expect( + File(p.join(d.sandbox, 'dir', 'file1.txt')).readAsString(), + completion(equals('contents1')), + ); + expect( + File(p.join(d.sandbox, 'dir', 'file2.txt')).readAsString(), + completion(equals('contents2')), + ); + expect( + File(p.join(d.sandbox, 'dir', 'subdir', 'subfile1.txt')).readAsString(), + completion(equals('subcontents1')), + ); + expect( + File(p.join(d.sandbox, 'dir', 'subdir', 'subfile2.txt')).readAsString(), + completion(equals('subcontents2')), + ); + }); + + test('works if the directory already exists', () async { + await d.dir('dir').create(); + await d.dir('dir', [d.file('name.txt', 'contents')]).create(); + + expect( + File(p.join(d.sandbox, 'dir', 'name.txt')).readAsString(), + completion(equals('contents')), + ); + }); + }); + + group('validate()', () { + test('completes successfully if the filesystem matches the descriptor', + () async { + final dirPath = p.join(d.sandbox, 'dir'); + final subdirPath = p.join(dirPath, 'subdir'); + await Directory(subdirPath).create(recursive: true); + await File(p.join(dirPath, 'file1.txt')).writeAsString('contents1'); + await File(p.join(dirPath, 'file2.txt')).writeAsString('contents2'); + await File(p.join(subdirPath, 'subfile1.txt')) + .writeAsString('subcontents1'); + await File(p.join(subdirPath, 'subfile2.txt')) + .writeAsString('subcontents2'); + + await d.dir('dir', [ + d.dir('subdir', [ + d.file('subfile1.txt', 'subcontents1'), + d.file('subfile2.txt', 'subcontents2'), + ]), + d.file('file1.txt', 'contents1'), + d.file('file2.txt', 'contents2'), + ]).validate(); + }); + + test("fails if the directory doesn't exist", () async { + final dirPath = p.join(d.sandbox, 'dir'); + await Directory(dirPath).create(); + await File(p.join(dirPath, 'file1.txt')).writeAsString('contents1'); + await File(p.join(dirPath, 'file2.txt')).writeAsString('contents2'); + + expect( + d.dir('dir', [ + d.dir('subdir', [ + d.file('subfile1.txt', 'subcontents1'), + d.file('subfile2.txt', 'subcontents2'), + ]), + d.file('file1.txt', 'contents1'), + d.file('file2.txt', 'contents2'), + ]).validate(), + throwsA( + toString( + equals('Directory not found: "${p.join('dir', 'subdir')}".'), + ), + ), + ); + }); + + test('emits an error for each child that fails to validate', () async { + final dirPath = p.join(d.sandbox, 'dir'); + final subdirPath = p.join(dirPath, 'subdir'); + await Directory(subdirPath).create(recursive: true); + await File(p.join(dirPath, 'file1.txt')).writeAsString('contents1'); + await File(p.join(subdirPath, 'subfile2.txt')) + .writeAsString('subwrongtents2'); + + var errors = 0; + final controller = StreamController(); + runZonedGuarded( + () { + d.dir('dir', [ + d.dir('subdir', [ + d.file('subfile1.txt', 'subcontents1'), + d.file('subfile2.txt', 'subcontents2'), + ]), + d.file('file1.txt', 'contents1'), + d.file('file2.txt', 'contents2'), + ]).validate(); + }, + expectAsync2( + (error, _) { + errors++; + controller.add(error.toString()); + if (errors == 3) controller.close(); + }, + count: 3, + ), + ); + + expect( + controller.stream.toList(), + completion( + allOf([ + contains( + 'File not found: "${p.join('dir', 'subdir', 'subfile1.txt')}".', + ), + contains('File not found: "${p.join('dir', 'file2.txt')}".'), + contains( + startsWith('File "${p.join('dir', 'subdir', 'subfile2.txt')}" ' + 'should contain:'), + ), + ]), + ), + ); + }); + }); + + group('load()', () { + test('loads a file', () { + final dir = d.dir( + 'dir', + [d.file('name.txt', 'contents'), d.file('other.txt', 'wrong')], + ); + expect( + utf8.decodeStream(dir.load('name.txt')), + completion(equals('contents')), + ); + }); + + test('loads a deeply-nested file', () { + final dir = d.dir('dir', [ + d.dir( + 'subdir', + [d.file('name.txt', 'subcontents'), d.file('other.txt', 'wrong')], + ), + d.dir('otherdir', [d.file('other.txt', 'wrong')]), + d.file('name.txt', 'contents'), + ]); + + expect( + utf8.decodeStream(dir.load('subdir/name.txt')), + completion(equals('subcontents')), + ); + }); + + test('fails to load a nested directory', () { + final dir = d.dir('dir', [ + d.dir('subdir', [ + d.dir('subsubdir', [d.file('name.txt', 'subcontents')]), + ]), + d.file('name.txt', 'contents'), + ]); + + expect( + dir.load('subdir/subsubdir').toList(), + throwsA( + toString( + equals('Couldn\'t find a file descriptor named ' + '"subsubdir" within "dir/subdir".'), + ), + ), + ); + }); + + test('fails to load an absolute path', () { + final dir = d.dir('dir', [d.file('name.txt', 'contents')]); + expect(() => dir.load('/name.txt'), throwsArgumentError); + }); + + test("fails to load '..'", () { + final dir = d.dir('dir', [d.file('name.txt', 'contents')]); + expect(() => dir.load('..'), throwsArgumentError); + }); + + test("fails to load a file that doesn't exist", () { + final dir = d.dir('dir', [ + d.dir('subdir', [d.file('name.txt', 'contents')]), + ]); + + expect( + dir.load('subdir/not-name.txt').toList(), + throwsA( + toString( + equals('Couldn\'t find a file descriptor named ' + '"not-name.txt" within "dir/subdir".'), + ), + ), + ); + }); + + test('fails to load a file that exists multiple times', () { + final dir = d.dir('dir', [ + d.dir( + 'subdir', + [d.file('name.txt', 'contents'), d.file('name.txt', 'contents')], + ), + ]); + + expect( + dir.load('subdir/name.txt').toList(), + throwsA( + toString( + equals('Found multiple file descriptors named ' + '"name.txt" within "dir/subdir".'), + ), + ), + ); + }); + + test('loads a file next to a subdirectory with the same name', () { + final dir = d.dir('dir', [ + d.file('name', 'contents'), + d.dir('name', [d.file('subfile', 'contents')]), + ]); + + expect( + utf8.decodeStream(dir.load('name')), + completion(equals('contents')), + ); + }); + }); + + group('describe()', () { + late bool oldAscii; + setUpAll(() { + oldAscii = term_glyph.ascii; + term_glyph.ascii = true; + }); + + tearDownAll(() { + term_glyph.ascii = oldAscii; + }); + + test('lists the contents of the directory', () { + final dir = d.dir( + 'dir', + [d.file('file1.txt', 'contents1'), d.file('file2.txt', 'contents2')], + ); + + expect( + dir.describe(), + equals('dir\n' + '+-- file1.txt\n' + "'-- file2.txt"), + ); + }); + + test('lists the contents of nested directories', () { + final dir = d.dir('dir', [ + d.file('file1.txt', 'contents1'), + d.dir('subdir', [ + d.file('subfile1.txt', 'subcontents1'), + d.file('subfile2.txt', 'subcontents2'), + d.dir('subsubdir', [d.file('subsubfile.txt', 'subsubcontents')]), + ]), + d.file('file2.txt', 'contents2'), + ]); + + expect( + dir.describe(), + equals('dir\n' + '+-- file1.txt\n' + '+-- subdir\n' + '| +-- subfile1.txt\n' + '| +-- subfile2.txt\n' + "| '-- subsubdir\n" + "| '-- subsubfile.txt\n" + "'-- file2.txt"), + ); + }); + + test('with no contents returns the directory name', () { + expect(d.dir('dir').describe(), equals('dir')); + }); + }); + + group('fromFilesystem()', () { + test('creates a descriptor based on the physical filesystem', () async { + final dir = d.dir('dir', [ + d.dir('subdir', [ + d.file('subfile1.txt', 'subcontents1'), + d.file('subfile2.txt', 'subcontents2'), + ]), + d.file('file1.txt', 'contents1'), + d.file('file2.txt', 'contents2'), + ]); + + await dir.create(); + final descriptor = + d.DirectoryDescriptor.fromFilesystem('dir', p.join(d.sandbox, 'dir')); + await descriptor.create(p.join(d.sandbox, 'dir2')); + await dir.validate(p.join(d.sandbox, 'dir2')); + }); + + test('ignores hidden files', () async { + await d.dir('dir', [ + d.dir('subdir', [ + d.file('subfile1.txt', 'subcontents1'), + d.file('.hidden', 'subcontents2'), + ]), + d.file('file1.txt', 'contents1'), + d.file('.DS_Store', 'contents2'), + ]).create(); + + final descriptor = d.DirectoryDescriptor.fromFilesystem( + 'dir2', + p.join(d.sandbox, 'dir'), + ); + await descriptor.create(); + + await d.dir('dir2', [ + d.dir( + 'subdir', + [d.file('subfile1.txt', 'subcontents1'), d.nothing('.hidden')], + ), + d.file('file1.txt', 'contents1'), + d.nothing('.DS_Store'), + ]).validate(); + }); + }); + + test('io refers to the directory within the sandbox', () { + expect(d.file('dir').io.path, equals(p.join(d.sandbox, 'dir'))); + }); +} diff --git a/pkgs/test_descriptor/test/file_test.dart b/pkgs/test_descriptor/test/file_test.dart new file mode 100644 index 000000000..26e4b74b9 --- /dev/null +++ b/pkgs/test_descriptor/test/file_test.dart @@ -0,0 +1,196 @@ +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +@TestOn('vm') +library; + +import 'dart:convert'; +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import 'utils.dart'; + +void main() { + group('create()', () { + test('creates a text file', () async { + await d.file('name.txt', 'contents').create(); + + expect( + File(p.join(d.sandbox, 'name.txt')).readAsString(), + completion(equals('contents')), + ); + }); + + test('creates a binary file', () async { + await d.file('name.txt', [0, 1, 2, 3]).create(); + + expect( + File(p.join(d.sandbox, 'name.txt')).readAsBytes(), + completion(equals([0, 1, 2, 3])), + ); + }); + + test('fails to create a matcher file', () async { + expect( + d.file('name.txt', contains('foo')).create(), + throwsUnsupportedError, + ); + }); + + test('overwrites an existing file', () async { + await d.file('name.txt', 'contents1').create(); + await d.file('name.txt', 'contents2').create(); + + expect( + File(p.join(d.sandbox, 'name.txt')).readAsString(), + completion(equals('contents2')), + ); + }); + }); + + group('validate()', () { + test('succeeds if the filesystem matches a text descriptor', () async { + await File(p.join(d.sandbox, 'name.txt')).writeAsString('contents'); + await d.file('name.txt', 'contents').validate(); + }); + + test('succeeds if the filesystem matches a binary descriptor', () async { + await File(p.join(d.sandbox, 'name.txt')).writeAsBytes([0, 1, 2, 3]); + await d.file('name.txt', [0, 1, 2, 3]).validate(); + }); + + test('succeeds if the filesystem matches a text matcher', () async { + await File(p.join(d.sandbox, 'name.txt')).writeAsString('contents'); + await d.file('name.txt', contains('ent')).validate(); + }); + + test('succeeds if the filesystem matches a binary matcher', () async { + await File(p.join(d.sandbox, 'name.txt')).writeAsBytes([0, 1, 2, 3]); + await d.FileDescriptor.binaryMatcher('name.txt', contains(2)).validate(); + }); + + test('succeeds if invalid UTF-8 matches a text matcher', () async { + await File(p.join(d.sandbox, 'name.txt')).writeAsBytes([0xC3, 0x28]); + await d.file('name.txt', isNot(isEmpty)).validate(); + }); + + test("fails if the text contents don't match", () async { + await File(p.join(d.sandbox, 'name.txt')).writeAsString('wrong'); + + expect( + d.file('name.txt', 'contents').validate(), + throwsA(toString(startsWith('File "name.txt" should contain:'))), + ); + }); + + test("fails if the binary contents don't match", () async { + await File(p.join(d.sandbox, 'name.txt')).writeAsBytes([5, 4, 3, 2]); + + expect( + d.file('name.txt', [0, 1, 2, 3]).validate(), + throwsA( + toString( + equals( + 'File "name.txt" didn\'t contain the expected binary data.', + ), + ), + ), + ); + }); + + test("fails if the text contents don't match the matcher", () async { + await File(p.join(d.sandbox, 'name.txt')).writeAsString('wrong'); + + expect( + d.file('name.txt', contains('ent')).validate(), + throwsA( + toString(startsWith('Invalid contents for file "name.txt":')), + ), + ); + }); + + test("fails if the binary contents don't match the matcher", () async { + await File(p.join(d.sandbox, 'name.txt')).writeAsBytes([5, 4, 3, 2]); + + expect( + d.FileDescriptor.binaryMatcher('name.txt', contains(1)).validate(), + throwsA( + toString(startsWith('Invalid contents for file "name.txt":')), + ), + ); + }); + + test("fails if invalid UTF-8 doesn't match a text matcher", () async { + await File(p.join(d.sandbox, 'name.txt')).writeAsBytes([0xC3, 0x28]); + expect( + d.file('name.txt', isEmpty).validate(), + throwsA( + toString( + allOf([ + startsWith('Invalid contents for file "name.txt":'), + contains('�'), + ]), + ), + ), + ); + }); + + test("fails if there's no file", () { + expect( + d.file('name.txt', 'contents').validate(), + throwsA(toString(equals('File not found: "name.txt".'))), + ); + }); + }); + + group('reading', () { + test('read() returns the contents of a text file as a string', () { + expect( + d.file('name.txt', 'contents').read(), + completion(equals('contents')), + ); + }); + + test('read() returns the contents of a binary file as a string', () { + expect( + d.file('name.txt', [0x68, 0x65, 0x6c, 0x6c, 0x6f]).read(), + completion(equals('hello')), + ); + }); + + test('read() fails for a matcher file', () { + expect(d.file('name.txt', contains('hi')).read, throwsUnsupportedError); + }); + + test('readAsBytes() returns the contents of a text file as a byte stream', + () { + expect( + utf8.decodeStream(d.file('name.txt', 'contents').readAsBytes()), + completion(equals('contents')), + ); + }); + + test('readAsBytes() returns the contents of a binary file as a byte stream', + () { + expect( + byteStreamToList(d.file('name.txt', [0, 1, 2, 3]).readAsBytes()), + completion(equals([0, 1, 2, 3])), + ); + }); + + test('readAsBytes() fails for a matcher file', () { + expect( + d.file('name.txt', contains('hi')).readAsBytes, + throwsUnsupportedError, + ); + }); + }); + + test('io refers to the file within the sandbox', () { + expect(d.file('name.txt').io.path, equals(p.join(d.sandbox, 'name.txt'))); + }); +} diff --git a/pkgs/test_descriptor/test/nothing_test.dart b/pkgs/test_descriptor/test/nothing_test.dart new file mode 100644 index 000000000..2663034aa --- /dev/null +++ b/pkgs/test_descriptor/test/nothing_test.dart @@ -0,0 +1,70 @@ +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +@TestOn('vm') +library; + +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import 'utils.dart'; + +void main() { + test('create() does nothing', () async { + await d.nothing('foo').create(); + expect(File(p.join(d.sandbox, 'foo')).exists(), completion(isFalse)); + expect(Directory(p.join(d.sandbox, 'foo')).exists(), completion(isFalse)); + }); + + group('validate()', () { + test("succeeds if nothing's there", () async { + await d.nothing('foo').validate(); + }); + + test("fails if there's a file", () async { + await d.file('name.txt', 'contents').create(); + expect( + d.nothing('name.txt').validate(), + throwsA( + toString( + equals( + 'Expected nothing to exist at "name.txt", but found a file.', + ), + ), + ), + ); + }); + + test("fails if there's a directory", () async { + await d.dir('dir').create(); + expect( + d.nothing('dir').validate(), + throwsA( + toString( + equals( + 'Expected nothing to exist at "dir", but found a directory.', + ), + ), + ), + ); + }); + + test("fails if there's a broken link", () async { + await Link(p.join(d.sandbox, 'link')).create('nonexistent'); + expect( + d.nothing('link').validate(), + throwsA( + toString( + equals( + 'Expected nothing to exist at "link", but found a link.', + ), + ), + ), + ); + }); + }); +} diff --git a/pkgs/test_descriptor/test/pattern_test.dart b/pkgs/test_descriptor/test/pattern_test.dart new file mode 100644 index 000000000..988a66c64 --- /dev/null +++ b/pkgs/test_descriptor/test/pattern_test.dart @@ -0,0 +1,84 @@ +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +@TestOn('vm') +library; + +import 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import 'utils.dart'; + +void main() { + group('validate()', () { + test("succeeds if there's a file matching the pattern and the child", + () async { + await d.file('foo', 'blap').create(); + await d.filePattern(RegExp('f..'), 'blap').validate(); + }); + + test("succeeds if there's a directory matching the pattern and the child", + () async { + await d.dir('foo', [d.file('bar', 'baz')]).create(); + + await d.dirPattern(RegExp('f..'), [d.file('bar', 'baz')]).validate(); + }); + + test( + 'succeeds if multiple files match the pattern but only one matches ' + 'the child entry', () async { + await d.file('foo', 'blap').create(); + await d.file('fee', 'blak').create(); + await d.file('faa', 'blut').create(); + + await d.filePattern(RegExp('f..'), 'blap').validate(); + }); + + test("fails if there's no file matching the pattern", () { + expect( + d.filePattern(RegExp('f..'), 'bar').validate(), + throwsA( + toString(equals('No entries found in sandbox matching /f../.')), + ), + ); + }); + + test("fails if there's a file matching the pattern but not the entry", + () async { + await d.file('foo', 'bap').create(); + expect( + d.filePattern(RegExp('f..'), 'bar').validate(), + throwsA(toString(startsWith('File "foo" should contain:'))), + ); + }); + + test("fails if there's a dir matching the pattern but not the entry", + () async { + await d.dir('foo', [d.file('bar', 'bap')]).create(); + + expect( + d.dirPattern(RegExp('f..'), [d.file('bar', 'baz')]).validate(), + throwsA(toString(startsWith('File "foo/bar" should contain:'))), + ); + }); + + test( + 'fails if there are multiple files matching the pattern and the child ' + 'entry', () async { + await d.file('foo', 'bar').create(); + await d.file('fee', 'bar').create(); + await d.file('faa', 'bar').create(); + expect( + d.filePattern(RegExp('f..'), 'bar').validate(), + throwsA( + toString( + startsWith( + 'Multiple valid entries found in sandbox matching /f../:', + ), + ), + ), + ); + }); + }); +} diff --git a/pkgs/test_descriptor/test/sandbox_test.dart b/pkgs/test_descriptor/test/sandbox_test.dart new file mode 100644 index 000000000..b0eb32cb4 --- /dev/null +++ b/pkgs/test_descriptor/test/sandbox_test.dart @@ -0,0 +1,31 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +@TestOn('vm') +library; + +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +void main() { + test('accessing the getter creates the directory', () { + expect(Directory(d.sandbox).existsSync(), isTrue); + }); + + test('the directory is deleted after the test', () { + late String sandbox; + addTearDown(() { + expect(Directory(sandbox).existsSync(), isFalse); + }); + + sandbox = d.sandbox; + }); + + test('path() returns a path in the sandbox', () { + expect(d.path('foo'), equals(p.join(d.sandbox, 'foo'))); + }); +} diff --git a/pkgs/test_descriptor/test/utils.dart b/pkgs/test_descriptor/test/utils.dart new file mode 100644 index 000000000..2d696635d --- /dev/null +++ b/pkgs/test_descriptor/test/utils.dart @@ -0,0 +1,22 @@ +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test/test.dart'; + +/// Converts a [Stream]`>` to a flat byte future. +Future> byteStreamToList(Stream> stream) => + stream.fold([], (buffer, chunk) { + buffer.addAll(chunk); + return buffer; + }); + +/// Returns a matcher that verifies that the result of calling `toString()` +/// matches [matcher]. +Matcher toString(Object? matcher) => predicate( + (object) { + expect(object.toString(), matcher); + return true; + }, + 'toString() matches $matcher', + );